marEx 4.0__tar.gz → 4.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. marex-4.0.3/.codecov.yml +34 -0
  2. {marex-4.0 → marex-4.0.3}/.github/workflows/ci.yml +5 -0
  3. {marex-4.0/marEx.egg-info → marex-4.0.3}/PKG-INFO +1 -1
  4. marex-4.0.3/examples/batch jobs/run_detect.py +91 -0
  5. marex-4.0.3/examples/batch jobs/run_track.py +131 -0
  6. marex-4.0.3/examples/batch jobs/submit_track.sh +79 -0
  7. {marex-4.0 → marex-4.0.3}/marEx/__init__.py +5 -0
  8. {marex-4.0 → marex-4.0.3}/marEx/detect.py +213 -78
  9. {marex-4.0 → marex-4.0.3}/marEx/helper.py +185 -6
  10. {marex-4.0 → marex-4.0.3/marEx.egg-info}/PKG-INFO +1 -1
  11. {marex-4.0 → marex-4.0.3}/marEx.egg-info/SOURCES.txt +4 -0
  12. {marex-4.0 → marex-4.0.3}/tests/conftest.py +6 -0
  13. {marex-4.0 → marex-4.0.3}/tests/test_detect_detrending.py +1 -1
  14. {marex-4.0 → marex-4.0.3}/tests/test_error_handling.py +30 -0
  15. {marex-4.0 → marex-4.0.3}/tests/test_track_edge_cases.py +2 -5
  16. {marex-4.0 → marex-4.0.3}/.github/workflows/README.md +0 -0
  17. {marex-4.0 → marex-4.0.3}/.github/workflows/release.yml +0 -0
  18. {marex-4.0 → marex-4.0.3}/.github/workflows/validate.py +0 -0
  19. {marex-4.0 → marex-4.0.3}/.readthedocs.yaml +0 -0
  20. {marex-4.0 → marex-4.0.3}/CITATION.cff +0 -0
  21. {marex-4.0 → marex-4.0.3}/CONTRIBUTING.md +0 -0
  22. {marex-4.0 → marex-4.0.3}/MANIFEST.in +0 -0
  23. {marex-4.0 → marex-4.0.3}/README.md +0 -0
  24. {marex-4.0 → marex-4.0.3}/examples/gridded data/01_preprocess_extremes.ipynb +0 -0
  25. {marex-4.0 → marex-4.0.3}/examples/gridded data/02_id_track_events.ipynb +0 -0
  26. {marex-4.0 → marex-4.0.3}/examples/gridded data/03_visualise_events.ipynb +0 -0
  27. {marex-4.0 → marex-4.0.3}/examples/regional data/01_preprocess_extremes.ipynb +0 -0
  28. {marex-4.0 → marex-4.0.3}/examples/regional data/02_id_track_events.ipynb +0 -0
  29. {marex-4.0 → marex-4.0.3}/examples/regional data/03_visualise_events.ipynb +0 -0
  30. {marex-4.0 → marex-4.0.3}/examples/unstructured data/01_preprocess_extremes.ipynb +0 -0
  31. {marex-4.0 → marex-4.0.3}/examples/unstructured data/02_id_track_events.ipynb +0 -0
  32. {marex-4.0 → marex-4.0.3}/examples/unstructured data/03_visualise_events.ipynb +0 -0
  33. {marex-4.0 → marex-4.0.3}/marEx/_coverage_init.py +0 -0
  34. {marex-4.0 → marex-4.0.3}/marEx/_dependencies.py +0 -0
  35. {marex-4.0 → marex-4.0.3}/marEx/exceptions.py +0 -0
  36. {marex-4.0 → marex-4.0.3}/marEx/logging_config.py +0 -0
  37. {marex-4.0 → marex-4.0.3}/marEx/plotX/__init__.py +0 -0
  38. {marex-4.0 → marex-4.0.3}/marEx/plotX/base.py +0 -0
  39. {marex-4.0 → marex-4.0.3}/marEx/plotX/gridded.py +0 -0
  40. {marex-4.0 → marex-4.0.3}/marEx/plotX/unstructured.py +0 -0
  41. {marex-4.0 → marex-4.0.3}/marEx/track.py +0 -0
  42. {marex-4.0 → marex-4.0.3}/marEx.egg-info/dependency_links.txt +0 -0
  43. {marex-4.0 → marex-4.0.3}/marEx.egg-info/requires.txt +0 -0
  44. {marex-4.0 → marex-4.0.3}/marEx.egg-info/top_level.txt +0 -0
  45. {marex-4.0 → marex-4.0.3}/pyproject.toml +0 -0
  46. {marex-4.0 → marex-4.0.3}/setup.cfg +0 -0
  47. {marex-4.0 → marex-4.0.3}/tests/__init__.py +0 -0
  48. {marex-4.0 → marex-4.0.3}/tests/coverage_config.py +0 -0
  49. {marex-4.0 → marex-4.0.3}/tests/make_test_data.ipynb +0 -0
  50. {marex-4.0 → marex-4.0.3}/tests/test_detect_helpers.py +0 -0
  51. {marex-4.0 → marex-4.0.3}/tests/test_exceptions.py +0 -0
  52. {marex-4.0 → marex-4.0.3}/tests/test_gridded_preprocessing.py +0 -0
  53. {marex-4.0 → marex-4.0.3}/tests/test_gridded_tracking.py +0 -0
  54. {marex-4.0 → marex-4.0.3}/tests/test_integration.py +0 -0
  55. {marex-4.0 → marex-4.0.3}/tests/test_logging_system.py +0 -0
  56. {marex-4.0 → marex-4.0.3}/tests/test_plotx.py +0 -0
  57. {marex-4.0 → marex-4.0.3}/tests/test_track_helpers.py +0 -0
  58. {marex-4.0 → marex-4.0.3}/tests/test_unstructured_preprocessing.py +0 -0
  59. {marex-4.0 → marex-4.0.3}/tests/test_unstructured_tracking.py +0 -0
@@ -0,0 +1,34 @@
1
+ # Codecov configuration for marEx
2
+ # https://docs.codecov.com/docs/codecov-yaml
3
+
4
+ coverage:
5
+ status:
6
+ # Overall project coverage
7
+ project:
8
+ default:
9
+ target: 95% # Maintain 95% overall project coverage
10
+ threshold: 1%
11
+ if_ci_failed: error
12
+
13
+ # Coverage for new code in patches/PRs - disabled
14
+ patch:
15
+ default:
16
+ enabled: false # Do not enforce patch coverage requirements
17
+
18
+ # Comment configuration
19
+ comment:
20
+ layout: "reach,diff,flags,tree"
21
+ behavior: default
22
+ require_changes: false
23
+ require_base: false
24
+ require_head: true
25
+
26
+ # Ignore paths
27
+ ignore:
28
+ - "tests/"
29
+ - "docs/"
30
+ - "examples/"
31
+ - "setup.py"
32
+ - "**/_dependencies.py"
33
+ - "**/_coverage_init.py"
34
+ - "**/plotX/unstructured.py"
@@ -64,6 +64,7 @@ jobs:
64
64
  name: Test Suite
65
65
  runs-on: ${{ matrix.os }}
66
66
  needs: code-quality
67
+ timeout-minutes: 90 # Windows can be significantly slower than Linux/macOS
67
68
  strategy:
68
69
  fail-fast: false
69
70
  matrix:
@@ -144,18 +145,22 @@ jobs:
144
145
  pytest tests/test_logging_system.py -n 2 -v --tb=short
145
146
 
146
147
  - name: Run preprocessing tests (gridded)
148
+ timeout-minutes: 30 # Windows can be slow for these tests
147
149
  run: |
148
150
  pytest tests/test_gridded_preprocessing.py -n 2 -v --tb=short
149
151
 
150
152
  - name: Run preprocessing tests (unstructured)
153
+ timeout-minutes: 30 # Windows can be slow for these tests
151
154
  run: |
152
155
  pytest tests/test_unstructured_preprocessing.py -n 2 -v --tb=short
153
156
 
154
157
  - name: Run tracking tests (gridded)
158
+ timeout-minutes: 30 # Windows can be slow for these tests
155
159
  run: |
156
160
  pytest tests/test_gridded_tracking.py -n 2 -v --tb=short
157
161
 
158
162
  - name: Run tracking tests (unstructured)
163
+ timeout-minutes: 30 # Windows can be slow for these tests
159
164
  run: |
160
165
  pytest tests/test_unstructured_tracking.py -n 2 -v --tb=short
161
166
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marEx
3
- Version: 4.0
3
+ Version: 4.0.3
4
4
  Summary: Marine Extremes Detection and Tracking
5
5
  Author-email: Aaron Wienkers <aaron@wienkers.com>
6
6
  Project-URL: Homepage, https://github.com/wienkers/marEx
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Global Daily SST (OSTIA) Analysis: Identifying Marine Extremes with MarEx-Detect
4
+
5
+ Pre-Process with Shifting-Baseline & Hobday et al. (2016) Definition for Extremes
6
+
7
+ Run from login node - this script will launch SLURM jobs via Dask distributed cluster
8
+
9
+ NOTE: This script uses a distributed cluster because detect.py does not contain
10
+ Numba JIT-compiled functions that execute locally. Unlike run_track.py, this
11
+ approach works correctly for detection/preprocessing tasks. If you prefer to
12
+ follow HPC best practices and run everything in SLURM jobs, consider creating
13
+ a submit_detect.sh script similar to submit_track.sh.
14
+ """
15
+
16
+ import os
17
+ from getpass import getuser
18
+ from pathlib import Path
19
+
20
+ import xarray as xr
21
+
22
+ import marEx
23
+ import marEx.helper as hpc
24
+
25
+
26
+ def main():
27
+ """Run MarEx preprocessing and extreme detection pipeline on distributed SLURM cluster."""
28
+ # Configuration from environment variables
29
+ n_workers = int(os.getenv("DASK_N_WORKERS", "128"))
30
+ workers_per_node = int(os.getenv("DASK_WORKERS_PER_NODE", "64"))
31
+ runtime = int(os.getenv("DASK_RUNTIME", "39"))
32
+ slurm_account = os.getenv("SLURM_ACCOUNT", "bk1377")
33
+
34
+ # Lustre Scratch Directory
35
+ scratch_dir = Path("/scratch") / getuser()[0] / getuser()
36
+
37
+ # Start Distributed Dask Cluster on SLURM
38
+ print("Starting distributed Dask cluster on SLURM...")
39
+ print(f" Workers: {n_workers}")
40
+ print(f" Workers per node: {workers_per_node}")
41
+ print(f" Runtime: {runtime} minutes")
42
+ print(f" SLURM account: {slurm_account}")
43
+
44
+ client = hpc.start_distributed_cluster(
45
+ n_workers=n_workers,
46
+ workers_per_node=workers_per_node,
47
+ runtime=runtime,
48
+ scratch_dir=scratch_dir / "clients",
49
+ account=slurm_account,
50
+ )
51
+
52
+ # Choose optimal chunk size & load data
53
+ print("Loading data...")
54
+ file_name = scratch_dir / "mhws" / "ostia.zarr"
55
+ chunk_size = {"time": 30, "lat": 360, "lon": 720}
56
+ sst = xr.open_zarr(str(file_name), chunks=chunk_size).sst
57
+ print(f"Data loaded: {sst}")
58
+
59
+ # Process Data using MarEx-Detect helper functions:
60
+ print("Processing extremes data...")
61
+ extremes_ds = marEx.preprocess_data(
62
+ sst,
63
+ method_anomaly="shifting_baseline", # Anomalies from a rolling climatology using previous window_year years
64
+ method_extreme="hobday_extreme", # Local day-of-year specific thresholds with windowing
65
+ threshold_percentile=95, # Use the 95th percentile as the extremes threshold
66
+ window_year_baseline=15,
67
+ smooth_days_baseline=21, # Defines the rolling climatology window (15 years) and smoothing window (21 days)
68
+ window_days_hobday=11, # Defines the window (11 days) of compiled samples collected for extremes detection
69
+ dimensions={
70
+ "time": "time",
71
+ "x": "lon",
72
+ "y": "lat",
73
+ }, # Define the dimensions of the data -- if 'y' exists, MarEx-Detect knows this is a gridded dataset
74
+ dask_chunks={"time": 25}, # Dask chunks for *output* data
75
+ use_temp_checkpoints=True, # Enable checkpointing to prevent expensive recomputations
76
+ )
77
+ print(f"Processing complete: {extremes_ds}")
78
+
79
+ # Save Extremes Data to zarr for more efficient parallel I/O
80
+ print("Saving extremes data...")
81
+ output_file = scratch_dir / "mhws" / "extremes_binary_gridded_shifting_hobday_batch.zarr"
82
+ extremes_ds.to_zarr(output_file, mode="w")
83
+ print(f"Data saved to: {output_file}")
84
+
85
+ # Close the Dask cluster
86
+ client.close()
87
+ print("Processing complete!")
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Global Daily Event Analysis: Marine Heatwave ID & Tracking using MarEx
4
+
5
+ Performs morphological preprocessing, blob identification, temporal tracking,
6
+ and graph reduction to create globally unique IDs for each tracked extreme event.
7
+
8
+ This script should be run INSIDE a SLURM job (via sbatch submit_track.sh).
9
+ It uses a local Dask cluster that utilizes the compute resources allocated to the job.
10
+ This ensures that all computation, including Numba JIT-compiled functions, runs on
11
+ compute nodes rather than the login node.
12
+ """
13
+
14
+ import os
15
+ from getpass import getuser
16
+ from pathlib import Path
17
+
18
+ import xarray as xr
19
+
20
+ import marEx
21
+ import marEx.helper as hpc
22
+
23
+
24
+ def main():
25
+ """Run MarEx event tracking pipeline on local Dask cluster within SLURM job allocation."""
26
+ # Configuration from environment variables
27
+ n_workers = int(os.getenv("DASK_N_WORKERS", "32"))
28
+ threads_per_worker = int(os.getenv("DASK_THREADS_PER_WORKER", "1"))
29
+
30
+ # Tracking configuration
31
+ run_basic = os.getenv("RUN_BASIC_TRACKER", "false").lower() == "true"
32
+ grid_resolution = float(os.getenv("GRID_RESOLUTION", "0.25"))
33
+ area_filter = int(os.getenv("AREA_FILTER", "600"))
34
+ r_fill = int(os.getenv("R_FILL", "12"))
35
+ t_fill = int(os.getenv("T_FILL", "4"))
36
+ overlap_threshold = float(os.getenv("OVERLAP_THRESHOLD", "0.25"))
37
+
38
+ # Lustre Scratch Directory
39
+ scratch_dir = Path("/scratch") / getuser()[0] / getuser()
40
+
41
+ # Start Local Dask Cluster
42
+ # This cluster runs within the SLURM job allocation, ensuring all computation
43
+ # (including Numba JIT-compiled functions) executes on compute nodes
44
+ print("Starting local Dask cluster within SLURM job allocation...")
45
+ print(f" Workers: {n_workers}")
46
+ print(f" Threads per worker: {threads_per_worker}")
47
+
48
+ client = hpc.start_local_cluster(
49
+ n_workers=n_workers,
50
+ threads_per_worker=threads_per_worker,
51
+ scratch_dir=scratch_dir / "clients",
52
+ )
53
+
54
+ # Choose optimal chunk size & load preprocessed data
55
+ print("\nLoading preprocessed extremes data...")
56
+ input_file = scratch_dir / "mhws" / "extremes_binary_gridded_shifting_hobday_batch.zarr"
57
+ chunk_size = {"time": 25, "lat": -1, "lon": -1}
58
+ ds = xr.open_zarr(str(input_file), chunks=chunk_size)
59
+ print(f"Data loaded: {ds}")
60
+
61
+ # Run ID, Tracking, & Merging
62
+ print("\n" + "=" * 80)
63
+ print("Running Full Tracking with Merge/Split Detection...")
64
+ print("=" * 80)
65
+
66
+ tracker = marEx.tracker(
67
+ ds.extreme_events,
68
+ ds.mask.where(
69
+ (ds.lat < 85) & (ds.lat > -90), other=False
70
+ ), # Modify Mask: Anisotropy of lat/lon grid near poles biases ID & Tracking
71
+ grid_resolution=grid_resolution, # Grid resolution in degrees, used to calculate object areas on the globe
72
+ area_filter_absolute=area_filter, # Remove objects smaller than specified cells
73
+ R_fill=r_fill, # Fill small holes with radius < R_fill _cells_
74
+ T_fill=t_fill, # Allow gaps of T_fill days and still continue event tracking with same ID
75
+ allow_merging=True, # Allow extreme events to split/merge. Keeps track of merge events & unique IDs
76
+ overlap_threshold=overlap_threshold, # Overlap threshold for merging events
77
+ nn_partitioning=True, # Use new NN method to partition merged children areas
78
+ verbose=True,
79
+ )
80
+
81
+ extreme_events_ds, merges_ds = tracker.run(return_merges=True)
82
+ print(f"\nTracking complete: {extreme_events_ds}")
83
+ print(f"Merges dataset: {merges_ds}")
84
+
85
+ # Save IDed/Tracked/Merged Events to zarr for efficient parallel I/O
86
+ print("\nSaving tracked events data...")
87
+ output_file = scratch_dir / "mhws" / "extreme_events_merged_gridded_shifting_batch.zarr"
88
+ extreme_events_ds.to_zarr(output_file, mode="w")
89
+ print(f"Events data saved to: {output_file}")
90
+
91
+ # Save Merges Dataset to netcdf
92
+ print("Saving merges data...")
93
+ merges_file = scratch_dir / "mhws" / "extreme_events_merged_gridded_shifting_merges_batch.nc"
94
+ merges_ds.to_netcdf(merges_file, mode="w")
95
+ print(f"Merges data saved to: {merges_file}")
96
+
97
+ # Optionally run basic tracking for comparison
98
+ if run_basic:
99
+ print("\n" + "=" * 80)
100
+ print("Running Basic Tracking (No Merge/Split, No Temporal Gap Filling)...")
101
+ print("=" * 80)
102
+
103
+ tracker_basic = marEx.tracker(
104
+ ds.extreme_events,
105
+ ds.mask.where(
106
+ (ds.lat < 85) & (ds.lat > -90), other=False
107
+ ), # Modify Mask: Anisotropy of lat/lon grid near poles biases ID & Tracking
108
+ area_filter_absolute=area_filter, # Remove objects smaller than specified cells
109
+ R_fill=r_fill, # Fill small holes with radius < R_fill _cells_
110
+ T_fill=0, # No temporal hole filling
111
+ allow_merging=False, # Do not allow extreme events to split/merge
112
+ )
113
+
114
+ extreme_events_basic_ds = tracker_basic.run()
115
+ print(f"\nBasic tracking complete: {extreme_events_basic_ds}")
116
+
117
+ # Save Basic IDed Events to zarr
118
+ print("\nSaving basic tracked events data...")
119
+ basic_output_file = scratch_dir / "mhws" / "extreme_events_basic_gridded_shifting_batch.zarr"
120
+ extreme_events_basic_ds.to_zarr(basic_output_file, mode="w")
121
+ print(f"Basic events data saved to: {basic_output_file}")
122
+
123
+ # Close the Dask cluster
124
+ client.close()
125
+ print("\n" + "=" * 80)
126
+ print("Tracking complete!")
127
+ print("=" * 80)
128
+
129
+
130
+ if __name__ == "__main__":
131
+ main()
@@ -0,0 +1,79 @@
1
+ #!/bin/bash
2
+ #SBATCH --job-name=marEx_track
3
+ #SBATCH --partition=compute
4
+ #SBATCH --cpus-per-task=128
5
+ #SBATCH --account=bk1377
6
+ #SBATCH --output=slurm_%j.out
7
+ #SBATCH --time=02:59:00
8
+ #SBATCH --mem=0
9
+ #SBATCH --exclusive
10
+
11
+ # SLURM Batch Script for MarEx Event Tracking
12
+ #
13
+ # This script submits run_track.py as a SLURM job, ensuring all computation
14
+ # (including Numba JIT-compiled functions) runs on allocated compute nodes.
15
+ #
16
+ # Usage:
17
+ # sbatch submit_track.sh
18
+ #
19
+ # Configuration via environment variables (optional):
20
+ # export DASK_N_WORKERS=32 # Number of Dask workers
21
+ # export DASK_THREADS_PER_WORKER=1 # Threads per worker
22
+ # export RUN_BASIC_TRACKER=true # Run basic tracker comparison
23
+ # export GRID_RESOLUTION=0.25 # Grid resolution in degrees
24
+ # export AREA_FILTER=600 # Minimum object area (cells)
25
+ # export R_FILL=12 # Spatial hole filling radius
26
+ # export T_FILL=4 # Temporal gap filling (days)
27
+ # export OVERLAP_THRESHOLD=0.25 # Merge overlap threshold
28
+ # sbatch submit_track.sh
29
+
30
+ # Print job information
31
+ echo "========================================="
32
+ echo "SLURM Job Information"
33
+ echo "========================================="
34
+ echo "Job ID: $SLURM_JOB_ID"
35
+ echo "Job Name: $SLURM_JOB_NAME"
36
+ echo "Node: $SLURM_NODELIST"
37
+ echo "Start Time: $(date)"
38
+ echo "Working Directory: $(pwd)"
39
+ echo "========================================="
40
+ echo ""
41
+
42
+ # Set ulimits for stack size
43
+ ulimit -S -s unlimited
44
+ ulimit -S -c unlimited
45
+
46
+ # Initialise conda for bash shell
47
+ source $(conda info --base)/etc/profile.d/conda.sh
48
+ conda activate super
49
+
50
+ # Set default environment variables if not already set
51
+ export DASK_N_WORKERS=${DASK_N_WORKERS:-32}
52
+ export DASK_THREADS_PER_WORKER=${DASK_THREADS_PER_WORKER:-1}
53
+
54
+ # Print configuration
55
+ echo "Configuration:"
56
+ echo " DASK_N_WORKERS: $DASK_N_WORKERS"
57
+ echo " DASK_THREADS_PER_WORKER: $DASK_THREADS_PER_WORKER"
58
+ echo " RUN_BASIC_TRACKER: ${RUN_BASIC_TRACKER:-false}"
59
+ echo " GRID_RESOLUTION: ${GRID_RESOLUTION:-0.25}"
60
+ echo " AREA_FILTER: ${AREA_FILTER:-600}"
61
+ echo " R_FILL: ${R_FILL:-12}"
62
+ echo " T_FILL: ${T_FILL:-4}"
63
+ echo " OVERLAP_THRESHOLD: ${OVERLAP_THRESHOLD:-0.25}"
64
+ echo ""
65
+
66
+ # Run the tracking script
67
+ echo "Starting MarEx tracking..."
68
+ python run_track.py
69
+
70
+ # Capture exit status
71
+ EXIT_STATUS=$?
72
+
73
+ echo ""
74
+ echo "========================================="
75
+ echo "Job completed at: $(date)"
76
+ echo "Exit status: $EXIT_STATUS"
77
+ echo "========================================="
78
+
79
+ exit $EXIT_STATUS
@@ -57,6 +57,9 @@ from .exceptions import ( # Main exception hierarchy; Convenience constructors
57
57
  wrap_exception,
58
58
  )
59
59
 
60
+ # Import HPC helper utilities
61
+ from .helper import configure_dask
62
+
60
63
  # Import logging configuration functions
61
64
  from .logging_config import (
62
65
  configure_logging,
@@ -116,6 +119,8 @@ __all__ = [
116
119
  "is_verbose_mode",
117
120
  "is_quiet_mode",
118
121
  "get_logger",
122
+ # HPC helper utilities
123
+ "configure_dask",
119
124
  ]
120
125
 
121
126
  # Version information