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.
Files changed (56) hide show
  1. marex-4.0.2/.codecov.yml +34 -0
  2. {marex-4.0/marEx.egg-info → marex-4.0.2}/PKG-INFO +1 -1
  3. {marex-4.0 → marex-4.0.2}/marEx/detect.py +42 -39
  4. {marex-4.0 → marex-4.0.2/marEx.egg-info}/PKG-INFO +1 -1
  5. {marex-4.0 → marex-4.0.2}/marEx.egg-info/SOURCES.txt +1 -0
  6. {marex-4.0 → marex-4.0.2}/tests/test_detect_detrending.py +1 -1
  7. {marex-4.0 → marex-4.0.2}/tests/test_error_handling.py +30 -0
  8. {marex-4.0 → marex-4.0.2}/tests/test_track_edge_cases.py +2 -5
  9. {marex-4.0 → marex-4.0.2}/.github/workflows/README.md +0 -0
  10. {marex-4.0 → marex-4.0.2}/.github/workflows/ci.yml +0 -0
  11. {marex-4.0 → marex-4.0.2}/.github/workflows/release.yml +0 -0
  12. {marex-4.0 → marex-4.0.2}/.github/workflows/validate.py +0 -0
  13. {marex-4.0 → marex-4.0.2}/.readthedocs.yaml +0 -0
  14. {marex-4.0 → marex-4.0.2}/CITATION.cff +0 -0
  15. {marex-4.0 → marex-4.0.2}/CONTRIBUTING.md +0 -0
  16. {marex-4.0 → marex-4.0.2}/MANIFEST.in +0 -0
  17. {marex-4.0 → marex-4.0.2}/README.md +0 -0
  18. {marex-4.0 → marex-4.0.2}/examples/gridded data/01_preprocess_extremes.ipynb +0 -0
  19. {marex-4.0 → marex-4.0.2}/examples/gridded data/02_id_track_events.ipynb +0 -0
  20. {marex-4.0 → marex-4.0.2}/examples/gridded data/03_visualise_events.ipynb +0 -0
  21. {marex-4.0 → marex-4.0.2}/examples/regional data/01_preprocess_extremes.ipynb +0 -0
  22. {marex-4.0 → marex-4.0.2}/examples/regional data/02_id_track_events.ipynb +0 -0
  23. {marex-4.0 → marex-4.0.2}/examples/regional data/03_visualise_events.ipynb +0 -0
  24. {marex-4.0 → marex-4.0.2}/examples/unstructured data/01_preprocess_extremes.ipynb +0 -0
  25. {marex-4.0 → marex-4.0.2}/examples/unstructured data/02_id_track_events.ipynb +0 -0
  26. {marex-4.0 → marex-4.0.2}/examples/unstructured data/03_visualise_events.ipynb +0 -0
  27. {marex-4.0 → marex-4.0.2}/marEx/__init__.py +0 -0
  28. {marex-4.0 → marex-4.0.2}/marEx/_coverage_init.py +0 -0
  29. {marex-4.0 → marex-4.0.2}/marEx/_dependencies.py +0 -0
  30. {marex-4.0 → marex-4.0.2}/marEx/exceptions.py +0 -0
  31. {marex-4.0 → marex-4.0.2}/marEx/helper.py +0 -0
  32. {marex-4.0 → marex-4.0.2}/marEx/logging_config.py +0 -0
  33. {marex-4.0 → marex-4.0.2}/marEx/plotX/__init__.py +0 -0
  34. {marex-4.0 → marex-4.0.2}/marEx/plotX/base.py +0 -0
  35. {marex-4.0 → marex-4.0.2}/marEx/plotX/gridded.py +0 -0
  36. {marex-4.0 → marex-4.0.2}/marEx/plotX/unstructured.py +0 -0
  37. {marex-4.0 → marex-4.0.2}/marEx/track.py +0 -0
  38. {marex-4.0 → marex-4.0.2}/marEx.egg-info/dependency_links.txt +0 -0
  39. {marex-4.0 → marex-4.0.2}/marEx.egg-info/requires.txt +0 -0
  40. {marex-4.0 → marex-4.0.2}/marEx.egg-info/top_level.txt +0 -0
  41. {marex-4.0 → marex-4.0.2}/pyproject.toml +0 -0
  42. {marex-4.0 → marex-4.0.2}/setup.cfg +0 -0
  43. {marex-4.0 → marex-4.0.2}/tests/__init__.py +0 -0
  44. {marex-4.0 → marex-4.0.2}/tests/conftest.py +0 -0
  45. {marex-4.0 → marex-4.0.2}/tests/coverage_config.py +0 -0
  46. {marex-4.0 → marex-4.0.2}/tests/make_test_data.ipynb +0 -0
  47. {marex-4.0 → marex-4.0.2}/tests/test_detect_helpers.py +0 -0
  48. {marex-4.0 → marex-4.0.2}/tests/test_exceptions.py +0 -0
  49. {marex-4.0 → marex-4.0.2}/tests/test_gridded_preprocessing.py +0 -0
  50. {marex-4.0 → marex-4.0.2}/tests/test_gridded_tracking.py +0 -0
  51. {marex-4.0 → marex-4.0.2}/tests/test_integration.py +0 -0
  52. {marex-4.0 → marex-4.0.2}/tests/test_logging_system.py +0 -0
  53. {marex-4.0 → marex-4.0.2}/tests/test_plotx.py +0 -0
  54. {marex-4.0 → marex-4.0.2}/tests/test_track_helpers.py +0 -0
  55. {marex-4.0 → marex-4.0.2}/tests/test_unstructured_preprocessing.py +0 -0
  56. {marex-4.0 → marex-4.0.2}/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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marEx
3
- Version: 4.0
3
+ Version: 4.0.2
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
@@ -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 a mask for valid (finite) data
221
- finite_mask = np.isfinite(da)
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 finite_mask.any():
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 dataset are NaN or infinite",
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
- "finite_values": 0,
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
- # Create ocean/land mask from first time step to identify valid spatial locations
242
- spatial_mask = np.isfinite(da.isel({dimensions["time"]: 0}))
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
- # For each spatial location that should have data, check if all time points are finite
245
- if spatial_mask.any():
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
- if not temporal_validity.all():
250
- # Count problematic locations and values
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
- # Count different types of invalid values
255
- nan_count = da.isnull().sum().compute()
256
- inf_count = (np.isinf(da)).sum().compute()
257
- total_invalid = nan_count + inf_count
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
- raise create_data_validation_error(
260
- f"Dataset contains invalid values in {invalid_locations} of {total_locations} valid locations",
261
- details=f"Found {total_invalid} invalid values: {nan_count} NaN, {inf_count} infinite",
262
- suggestions=[
263
- "Remove or interpolate NaN/infinite values before preprocessing",
264
- "Check data quality and loading procedures",
265
- "Consider using data.fillna() or data.interpolate() methods",
266
- "Verify coordinate/dimension alignment in your dataset",
267
- ],
268
- data_info={
269
- "invalid_locations": int(invalid_locations),
270
- "total_locations": int(total_locations),
271
- "nan_values": int(nan_count),
272
- "inf_values": int(inf_count),
273
- "total_values": int(da.size),
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
  # ============================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marEx
3
- Version: 4.0
3
+ Version: 4.0.2
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
@@ -1,3 +1,4 @@
1
+ .codecov.yml
1
2
  .readthedocs.yaml
2
3
  CITATION.cff
3
4
  CONTRIBUTING.md
@@ -143,4 +143,4 @@ class TestHigherOrderDetrendingWarnings:
143
143
 
144
144
  # Should complete without issues
145
145
  assert result is not None
146
- assert "dat_anomaly" in result
146
+ assert "dat_anomaly" in result
@@ -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