roms-tools 3.1.2__py3-none-any.whl → 3.2.0__py3-none-any.whl

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 (41) hide show
  1. roms_tools/__init__.py +3 -0
  2. roms_tools/analysis/cdr_analysis.py +203 -0
  3. roms_tools/analysis/cdr_ensemble.py +198 -0
  4. roms_tools/analysis/roms_output.py +80 -46
  5. roms_tools/data/grids/GLORYS_global_grid.nc +0 -0
  6. roms_tools/download.py +4 -0
  7. roms_tools/plot.py +75 -21
  8. roms_tools/setup/boundary_forcing.py +44 -19
  9. roms_tools/setup/cdr_forcing.py +122 -8
  10. roms_tools/setup/cdr_release.py +161 -8
  11. roms_tools/setup/datasets.py +626 -340
  12. roms_tools/setup/grid.py +138 -137
  13. roms_tools/setup/initial_conditions.py +113 -48
  14. roms_tools/setup/mask.py +63 -7
  15. roms_tools/setup/nesting.py +67 -42
  16. roms_tools/setup/river_forcing.py +45 -19
  17. roms_tools/setup/surface_forcing.py +4 -6
  18. roms_tools/setup/tides.py +1 -2
  19. roms_tools/setup/topography.py +4 -4
  20. roms_tools/setup/utils.py +134 -22
  21. roms_tools/tests/test_analysis/test_cdr_analysis.py +144 -0
  22. roms_tools/tests/test_analysis/test_cdr_ensemble.py +202 -0
  23. roms_tools/tests/test_analysis/test_roms_output.py +61 -3
  24. roms_tools/tests/test_setup/test_boundary_forcing.py +54 -52
  25. roms_tools/tests/test_setup/test_cdr_forcing.py +54 -0
  26. roms_tools/tests/test_setup/test_cdr_release.py +118 -1
  27. roms_tools/tests/test_setup/test_datasets.py +392 -44
  28. roms_tools/tests/test_setup/test_grid.py +222 -115
  29. roms_tools/tests/test_setup/test_initial_conditions.py +94 -41
  30. roms_tools/tests/test_setup/test_surface_forcing.py +2 -1
  31. roms_tools/tests/test_setup/test_utils.py +91 -1
  32. roms_tools/tests/test_setup/utils.py +71 -0
  33. roms_tools/tests/test_tiling/test_join.py +241 -0
  34. roms_tools/tests/test_utils.py +139 -17
  35. roms_tools/tiling/join.py +189 -0
  36. roms_tools/utils.py +131 -99
  37. {roms_tools-3.1.2.dist-info → roms_tools-3.2.0.dist-info}/METADATA +12 -2
  38. {roms_tools-3.1.2.dist-info → roms_tools-3.2.0.dist-info}/RECORD +41 -33
  39. {roms_tools-3.1.2.dist-info → roms_tools-3.2.0.dist-info}/WHEEL +0 -0
  40. {roms_tools-3.1.2.dist-info → roms_tools-3.2.0.dist-info}/licenses/LICENSE +0 -0
  41. {roms_tools-3.1.2.dist-info → roms_tools-3.2.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
- from datetime import datetime
1
+ from datetime import datetime, timedelta
2
2
  from unittest import mock
3
3
 
4
+ import numpy as np
4
5
  import pytest
5
6
  from pydantic import ValidationError
6
7
 
@@ -377,3 +378,119 @@ class TestTracerPerturbation:
377
378
  def test_get_tracer_metadata(self):
378
379
  d = VolumeRelease.get_tracer_metadata()
379
380
  assert len(d) == NUM_TRACERS
381
+
382
+
383
+ class TestReleaseAccounting:
384
+ def setup_method(self):
385
+ self.start = datetime(2020, 1, 1)
386
+ self.end = datetime(2020, 1, 11) # 10 days later
387
+
388
+ def make_release(self, release_type, fluxes=None, concentrations=None, times=None):
389
+ times = times or [self.start, self.end]
390
+
391
+ if release_type == "volume":
392
+ vr = VolumeRelease(
393
+ name="test",
394
+ lat=0.0,
395
+ lon=0.0,
396
+ depth=100.0,
397
+ times=times,
398
+ volume_fluxes=Flux("volume", fluxes),
399
+ tracer_concentrations={"DIC": Concentration("DIC", concentrations)},
400
+ )
401
+ vr._extend_to_endpoints(self.start, self.end)
402
+ return vr
403
+
404
+ elif release_type == "tracer":
405
+ tp = TracerPerturbation(
406
+ name="test",
407
+ lat=0.0,
408
+ lon=0.0,
409
+ depth=100.0,
410
+ times=times,
411
+ tracer_fluxes={"DIC": Flux("DIC", fluxes)},
412
+ )
413
+ tp._extend_to_endpoints(self.start, self.end)
414
+ return tp
415
+ else:
416
+ raise ValueError(f"Unknown release type {release_type}")
417
+
418
+ @pytest.mark.parametrize("release_type", ["volume", "tracer"])
419
+ def test_constant_flux(self, release_type):
420
+ """Case 0: constant fluxes."""
421
+ flux = 2.0
422
+ conc = 3.0 # only used for VolumeRelease
423
+ vr = self.make_release(release_type, fluxes=flux, concentrations=conc)
424
+ roms_stamps = np.array([0.0, 5.0, 10.0])
425
+ result = vr._do_accounting(roms_stamps, self.start)
426
+
427
+ expected = flux * (roms_stamps[-1] - roms_stamps[0])
428
+ if release_type == "volume":
429
+ expected *= conc
430
+
431
+ assert result["DIC"] == pytest.approx(expected)
432
+
433
+ @pytest.mark.parametrize("release_type", ["volume", "tracer"])
434
+ def test_aligned_releases(self, release_type):
435
+ """Case 1: release times exactly aligned with ROMS stamps."""
436
+ # ROMS time step: 5 days (we want to cover the simulation time of 10 days exactly)
437
+ roms = np.array([0.0, 5.0, 10.0]) * 24 * 3600 # seconds
438
+ release_times = [self.start + timedelta(seconds=s) for s in roms]
439
+ fluxes = [1, 3, 5]
440
+ concs = [1, 2, 1]
441
+
442
+ if release_type == "volume":
443
+ r = self.make_release(
444
+ release_type, fluxes=fluxes, concentrations=concs, times=release_times
445
+ )
446
+ elif release_type == "tracer":
447
+ r = self.make_release(release_type, fluxes=fluxes, times=release_times)
448
+
449
+ # Expected calculation
450
+ series = (
451
+ np.array([f * c for f, c in zip(fluxes, concs)])
452
+ if release_type == "volume"
453
+ else np.array(fluxes)
454
+ )
455
+ dt = np.diff(roms)
456
+ expected = np.sum(series[:-1] * dt)
457
+
458
+ result = r._do_accounting(roms, self.start)
459
+ assert result["DIC"] == pytest.approx(expected)
460
+
461
+ @pytest.mark.parametrize("release_type", ["volume", "tracer"])
462
+ def test_unaligned_releases(self, release_type):
463
+ """Case 2: release times unaligned with ROMS stamps."""
464
+ # ROMS time step: 5 days (we want to cover the simulation time of 10 days exactly)
465
+ roms = np.array([0.0, 2.5, 5.0, 7.5, 10.0]) * 24 * 3600 # seconds
466
+ rel_release_times = np.array([0.0, 1.0, 3.0, 6.0, 10.0]) * 24 * 3600 # seconds
467
+ release_times = [self.start + timedelta(seconds=s) for s in rel_release_times]
468
+ fluxes = [1, 2, 3, 4, 5]
469
+ concs = [1, 1.5, 2, 2.5, 1]
470
+
471
+ if release_type == "volume":
472
+ vr = self.make_release(
473
+ release_type, fluxes=fluxes, concentrations=concs, times=release_times
474
+ )
475
+ elif release_type == "tracer":
476
+ vr = self.make_release(release_type, fluxes=fluxes, times=release_times)
477
+
478
+ # Expected calculation
479
+ series = (
480
+ np.array([f * c for f, c in zip(fluxes, concs)])
481
+ if release_type == "volume"
482
+ else np.array(fluxes)
483
+ )
484
+ interp = np.interp(roms, rel_release_times, series)
485
+ dt = np.diff(roms)
486
+ expected = np.sum(interp[:-1] * dt)
487
+
488
+ result = vr._do_accounting(roms, self.start)
489
+ assert result["DIC"] == pytest.approx(expected)
490
+
491
+ @pytest.mark.parametrize("release_type", ["volume", "tracer"])
492
+ def test_raises_with_single_roms_timestamp(self, release_type):
493
+ vr = self.make_release(release_type, fluxes=1.0, concentrations=1.0)
494
+ roms_stamps = np.array([0.0])
495
+ with pytest.raises(ValueError, match="at least two ROMS time stamps"):
496
+ vr._do_accounting(roms_stamps, self.start)