marEx 4.0__tar.gz → 4.0.2__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.
- marex-4.0.2/.codecov.yml +34 -0
- {marex-4.0/marEx.egg-info → marex-4.0.2}/PKG-INFO +1 -1
- {marex-4.0 → marex-4.0.2}/marEx/detect.py +42 -39
- {marex-4.0 → marex-4.0.2/marEx.egg-info}/PKG-INFO +1 -1
- {marex-4.0 → marex-4.0.2}/marEx.egg-info/SOURCES.txt +1 -0
- {marex-4.0 → marex-4.0.2}/tests/test_detect_detrending.py +1 -1
- {marex-4.0 → marex-4.0.2}/tests/test_error_handling.py +30 -0
- {marex-4.0 → marex-4.0.2}/tests/test_track_edge_cases.py +2 -5
- {marex-4.0 → marex-4.0.2}/.github/workflows/README.md +0 -0
- {marex-4.0 → marex-4.0.2}/.github/workflows/ci.yml +0 -0
- {marex-4.0 → marex-4.0.2}/.github/workflows/release.yml +0 -0
- {marex-4.0 → marex-4.0.2}/.github/workflows/validate.py +0 -0
- {marex-4.0 → marex-4.0.2}/.readthedocs.yaml +0 -0
- {marex-4.0 → marex-4.0.2}/CITATION.cff +0 -0
- {marex-4.0 → marex-4.0.2}/CONTRIBUTING.md +0 -0
- {marex-4.0 → marex-4.0.2}/MANIFEST.in +0 -0
- {marex-4.0 → marex-4.0.2}/README.md +0 -0
- {marex-4.0 → marex-4.0.2}/examples/gridded data/01_preprocess_extremes.ipynb +0 -0
- {marex-4.0 → marex-4.0.2}/examples/gridded data/02_id_track_events.ipynb +0 -0
- {marex-4.0 → marex-4.0.2}/examples/gridded data/03_visualise_events.ipynb +0 -0
- {marex-4.0 → marex-4.0.2}/examples/regional data/01_preprocess_extremes.ipynb +0 -0
- {marex-4.0 → marex-4.0.2}/examples/regional data/02_id_track_events.ipynb +0 -0
- {marex-4.0 → marex-4.0.2}/examples/regional data/03_visualise_events.ipynb +0 -0
- {marex-4.0 → marex-4.0.2}/examples/unstructured data/01_preprocess_extremes.ipynb +0 -0
- {marex-4.0 → marex-4.0.2}/examples/unstructured data/02_id_track_events.ipynb +0 -0
- {marex-4.0 → marex-4.0.2}/examples/unstructured data/03_visualise_events.ipynb +0 -0
- {marex-4.0 → marex-4.0.2}/marEx/__init__.py +0 -0
- {marex-4.0 → marex-4.0.2}/marEx/_coverage_init.py +0 -0
- {marex-4.0 → marex-4.0.2}/marEx/_dependencies.py +0 -0
- {marex-4.0 → marex-4.0.2}/marEx/exceptions.py +0 -0
- {marex-4.0 → marex-4.0.2}/marEx/helper.py +0 -0
- {marex-4.0 → marex-4.0.2}/marEx/logging_config.py +0 -0
- {marex-4.0 → marex-4.0.2}/marEx/plotX/__init__.py +0 -0
- {marex-4.0 → marex-4.0.2}/marEx/plotX/base.py +0 -0
- {marex-4.0 → marex-4.0.2}/marEx/plotX/gridded.py +0 -0
- {marex-4.0 → marex-4.0.2}/marEx/plotX/unstructured.py +0 -0
- {marex-4.0 → marex-4.0.2}/marEx/track.py +0 -0
- {marex-4.0 → marex-4.0.2}/marEx.egg-info/dependency_links.txt +0 -0
- {marex-4.0 → marex-4.0.2}/marEx.egg-info/requires.txt +0 -0
- {marex-4.0 → marex-4.0.2}/marEx.egg-info/top_level.txt +0 -0
- {marex-4.0 → marex-4.0.2}/pyproject.toml +0 -0
- {marex-4.0 → marex-4.0.2}/setup.cfg +0 -0
- {marex-4.0 → marex-4.0.2}/tests/__init__.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/conftest.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/coverage_config.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/make_test_data.ipynb +0 -0
- {marex-4.0 → marex-4.0.2}/tests/test_detect_helpers.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/test_exceptions.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/test_gridded_preprocessing.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/test_gridded_tracking.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/test_integration.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/test_logging_system.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/test_plotx.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/test_track_helpers.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/test_unstructured_preprocessing.py +0 -0
- {marex-4.0 → marex-4.0.2}/tests/test_unstructured_tracking.py +0 -0
marex-4.0.2/.codecov.yml
ADDED
|
@@ -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"
|
|
@@ -217,14 +217,14 @@ def _validate_data_values(da: xr.DataArray, dimensions: Dict[str, str]) -> None:
|
|
|
217
217
|
DataValidationError
|
|
218
218
|
If any unmasked data contains NaN or infinite values
|
|
219
219
|
"""
|
|
220
|
-
# Create
|
|
221
|
-
|
|
220
|
+
# Create spatial mask from first time step (2D array)
|
|
221
|
+
spatial_mask = np.isfinite(da.isel({dimensions["time"]: 0}))
|
|
222
222
|
|
|
223
|
-
# Check if there's any data at all
|
|
224
|
-
if not
|
|
223
|
+
# Check if there's any valid data at all
|
|
224
|
+
if not spatial_mask.any().compute():
|
|
225
225
|
raise create_data_validation_error(
|
|
226
226
|
"Dataset contains no valid (finite) data",
|
|
227
|
-
details="All values in the
|
|
227
|
+
details="All values in the first time step are NaN or infinite",
|
|
228
228
|
suggestions=[
|
|
229
229
|
"Check your input data for data quality issues",
|
|
230
230
|
"Verify the data was loaded correctly",
|
|
@@ -232,47 +232,50 @@ def _validate_data_values(da: xr.DataArray, dimensions: Dict[str, str]) -> None:
|
|
|
232
232
|
],
|
|
233
233
|
data_info={
|
|
234
234
|
"total_values": int(da.size),
|
|
235
|
-
"
|
|
236
|
-
"nan_values": int(da.isnull().sum().compute()),
|
|
237
|
-
"inf_values": int((~np.isfinite(da) & da.notnull()).sum().compute()),
|
|
235
|
+
"total_spatial_locations": int(np.prod([da.sizes[d] for d in da.dims if d != dimensions["time"]])),
|
|
238
236
|
},
|
|
239
237
|
)
|
|
240
238
|
|
|
241
|
-
#
|
|
242
|
-
|
|
239
|
+
# Reduce first, then mask (avoids broadcasting across time)
|
|
240
|
+
# Count invalid values at each spatial location across time dimension
|
|
241
|
+
# This produces a 2D spatial array instead of a 3D array
|
|
242
|
+
finite_mask = np.isfinite(da)
|
|
243
|
+
invalid_per_location = (~finite_mask).sum(dim=dimensions["time"])
|
|
243
244
|
|
|
244
|
-
#
|
|
245
|
-
|
|
246
|
-
# Check for locations that have valid data in first timestep but invalid data elsewhere
|
|
247
|
-
temporal_validity = finite_mask.where(spatial_mask, True).all(dim=dimensions["time"])
|
|
245
|
+
# Now apply spatial mask to this 2D result (no broadcasting across time!)
|
|
246
|
+
invalid_in_valid_locations = invalid_per_location.where(spatial_mask, 0)
|
|
248
247
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
invalid_locations = (~temporal_validity).sum().compute()
|
|
252
|
-
total_locations = spatial_mask.sum().compute()
|
|
248
|
+
# Check if any valid ocean location has invalid data
|
|
249
|
+
max_invalid = invalid_in_valid_locations.max().compute()
|
|
253
250
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
251
|
+
if max_invalid > 0:
|
|
252
|
+
total_invalid_in_ocean = int(invalid_in_valid_locations.sum().compute())
|
|
253
|
+
total_ocean_locations = int(spatial_mask.sum().compute())
|
|
254
|
+
locations_affected = int((invalid_in_valid_locations > 0).sum().compute())
|
|
255
|
+
total_time_steps = int(da.sizes[dimensions["time"]])
|
|
258
256
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
257
|
+
raise create_data_validation_error(
|
|
258
|
+
f"Dataset contains {total_invalid_in_ocean} invalid values in {locations_affected} ocean locations",
|
|
259
|
+
details=(
|
|
260
|
+
f"Found invalid data across time series. Worst location has {int(max_invalid)} "
|
|
261
|
+
f"invalid time steps out of {total_time_steps}."
|
|
262
|
+
),
|
|
263
|
+
suggestions=[
|
|
264
|
+
"Remove or interpolate NaN/infinite values before preprocessing",
|
|
265
|
+
"Check data quality and loading procedures",
|
|
266
|
+
"Consider using data.fillna() or data.interpolate_na() methods",
|
|
267
|
+
"Verify coordinate/dimension alignment in your dataset",
|
|
268
|
+
"For ocean data, ensure land mask is properly applied before preprocessing",
|
|
269
|
+
],
|
|
270
|
+
data_info={
|
|
271
|
+
"total_invalid_values_in_ocean": total_invalid_in_ocean,
|
|
272
|
+
"locations_affected": locations_affected,
|
|
273
|
+
"total_ocean_locations": total_ocean_locations,
|
|
274
|
+
"max_invalid_at_one_location": int(max_invalid),
|
|
275
|
+
"total_time_steps": total_time_steps,
|
|
276
|
+
"percentage_affected": f"{100.0 * locations_affected / total_ocean_locations:.2f}%",
|
|
277
|
+
},
|
|
278
|
+
)
|
|
276
279
|
|
|
277
280
|
|
|
278
281
|
# ============================
|
|
@@ -1184,6 +1184,36 @@ class TestDetectDataValidationEdgeCases:
|
|
|
1184
1184
|
method_anomaly="detrend_harmonic",
|
|
1185
1185
|
)
|
|
1186
1186
|
|
|
1187
|
+
def test_partial_invalid_data_error(self, dimensions_gridded, dask_chunks):
|
|
1188
|
+
"""Test error when some ocean locations have NaN/infinite values across time."""
|
|
1189
|
+
import pandas as pd
|
|
1190
|
+
|
|
1191
|
+
# Create data with mostly valid values
|
|
1192
|
+
data = np.random.rand(100, 3, 4) * 5 + 15
|
|
1193
|
+
|
|
1194
|
+
# Make some specific locations have invalid values across time
|
|
1195
|
+
data[:, 1, 2] = np.nan # One location all NaN
|
|
1196
|
+
data[50:, 0, 1] = np.inf # Another location partially infinite
|
|
1197
|
+
data[:20, 2, 0] = np.nan # Another location with some NaN values
|
|
1198
|
+
|
|
1199
|
+
partial_invalid_data = xr.DataArray(
|
|
1200
|
+
data,
|
|
1201
|
+
dims=["time", "lat", "lon"],
|
|
1202
|
+
coords={
|
|
1203
|
+
"time": pd.date_range("2020-01-01", periods=100),
|
|
1204
|
+
"lat": np.linspace(-10, 10, 3),
|
|
1205
|
+
"lon": np.linspace(0, 20, 4),
|
|
1206
|
+
},
|
|
1207
|
+
).chunk({"time": 25})
|
|
1208
|
+
|
|
1209
|
+
with pytest.raises(DataValidationError, match="contains.*invalid values.*ocean locations"):
|
|
1210
|
+
marEx.preprocess_data(
|
|
1211
|
+
partial_invalid_data,
|
|
1212
|
+
dimensions=dimensions_gridded,
|
|
1213
|
+
dask_chunks=dask_chunks,
|
|
1214
|
+
method_anomaly="detrend_harmonic",
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1187
1217
|
|
|
1188
1218
|
class TestQuantileThresholdWarnings:
|
|
1189
1219
|
"""Test warnings for quantile threshold edge cases."""
|
|
@@ -259,9 +259,7 @@ class TestComplexMergeSplitValidation:
|
|
|
259
259
|
# Create tracker with aggressive merging settings
|
|
260
260
|
tracker = marEx.tracker(
|
|
261
261
|
extremes_gridded.extreme_events,
|
|
262
|
-
extremes_gridded.mask.where(
|
|
263
|
-
(extremes_gridded.lat < 85) & (extremes_gridded.lat > -90), other=False
|
|
264
|
-
),
|
|
262
|
+
extremes_gridded.mask.where((extremes_gridded.lat < 85) & (extremes_gridded.lat > -90), other=False),
|
|
265
263
|
area_filter_quartile=0.4,
|
|
266
264
|
R_fill=8,
|
|
267
265
|
T_fill=2, # Must be even
|
|
@@ -720,7 +718,7 @@ class TestAbsoluteAreaFiltering:
|
|
|
720
718
|
).chunk({"time": 5})
|
|
721
719
|
|
|
722
720
|
# Add larger, more connected objects that will survive morphological operations
|
|
723
|
-
data.values[0, 2:8, 2:8] = True
|
|
721
|
+
data.values[0, 2:8, 2:8] = True
|
|
724
722
|
data.values[0, 10:16, 10:16] = True
|
|
725
723
|
|
|
726
724
|
mask = xr.ones_like(data.isel(time=0), dtype=bool)
|
|
@@ -956,4 +954,3 @@ class TestEnforceOverlapThreshold:
|
|
|
956
954
|
# Verify processing completed and filtered correctly
|
|
957
955
|
assert result is not None
|
|
958
956
|
assert len(result) > 0 # Should have at least one valid overlap
|
|
959
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|