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
@@ -5,6 +5,8 @@ from datetime import datetime
5
5
  from enum import StrEnum, auto
6
6
  from typing import Annotated, Literal
7
7
 
8
+ import numpy as np
9
+ import pandas as pd
8
10
  from annotated_types import Ge, Le
9
11
  from pydantic import (
10
12
  BaseModel,
@@ -16,10 +18,17 @@ from pydantic import (
16
18
  )
17
19
  from pydantic_core.core_schema import ValidationInfo
18
20
 
19
- from roms_tools.setup.utils import get_tracer_defaults, get_tracer_metadata_dict
21
+ from roms_tools.setup.utils import (
22
+ convert_to_relative_days,
23
+ get_tracer_defaults,
24
+ get_tracer_metadata_dict,
25
+ )
20
26
 
21
27
  NonNegativeFloat = Annotated[float, Ge(0)]
22
28
 
29
+ # Show all columns when printing a DataFrame
30
+ pd.set_option("display.max_columns", None)
31
+
23
32
 
24
33
  @dataclass
25
34
  class ValueArray(ABC):
@@ -272,10 +281,68 @@ class Release(BaseModel):
272
281
  if self.times[-1] < end_time:
273
282
  self.times.append(end_time)
274
283
 
275
- @staticmethod
276
- def get_tracer_metadata():
284
+ @classmethod
285
+ def get_tracer_metadata(cls):
277
286
  return {}
278
287
 
288
+ @classmethod
289
+ def get_metadata(cls):
290
+ return pd.DataFrame(cls.get_tracer_metadata())
291
+
292
+ def _compute_integrated_tracers(
293
+ self,
294
+ roms_time_stamps: np.ndarray,
295
+ model_reference_date: datetime,
296
+ tracer_series_dict: dict[str, np.ndarray],
297
+ ) -> dict[str, float]:
298
+ """
299
+ Compute time-integrated tracer quantities over ROMS time steps using a left-hold rule.
300
+
301
+ This method performs a left-hold (stepwise constant) integration of tracer fluxes
302
+ over the intervals defined by the ROMS time stamps. It first interpolates the
303
+ tracer time series from the release schedule onto the ROMS time stamps, then
304
+ multiplies the value at the start of each interval by the duration of that interval.
305
+
306
+ Parameters
307
+ ----------
308
+ roms_time_stamps : np.ndarray
309
+ 1D array of ROMS model time stamps in seconds since `model_reference_date`.
310
+ Must be strictly increasing and contain at least two entries.
311
+ model_reference_date : datetime
312
+ Reference datetime of the ROMS model calendar, used to compute relative times
313
+ for interpolation.
314
+ tracer_series_dict : dict[str, np.ndarray]
315
+ Dictionary mapping tracer names to 1D arrays of tracer flux values at the
316
+ release schedule times (`self.times`). Each array must have the same length
317
+ as `self.times`.
318
+
319
+ Returns
320
+ -------
321
+ dict[str, float]
322
+ Dictionary mapping each tracer name to its integrated quantity over the
323
+ ROMS time period. Integration is performed using the left-hold rule,
324
+ ignoring the last release point because it defines the end of the final interval.
325
+
326
+ Raises
327
+ ------
328
+ ValueError
329
+ If `roms_time_stamps` has fewer than two entries, since at least one interval
330
+ is required for integration.
331
+ """
332
+ if len(roms_time_stamps) < 2:
333
+ raise ValueError("Need at least two ROMS time stamps to define intervals.")
334
+
335
+ dt = np.diff(roms_time_stamps)
336
+ results = {}
337
+ for tracer, series in tracer_series_dict.items():
338
+ interp_values = np.interp(
339
+ roms_time_stamps,
340
+ convert_to_relative_days(self.times, model_reference_date) * 3600 * 24,
341
+ series,
342
+ )
343
+ results[tracer] = np.sum(interp_values[:-1] * dt)
344
+ return results
345
+
279
346
 
280
347
  class VolumeRelease(Release):
281
348
  """Represents a CDR release with volume flux and tracer concentrations.
@@ -389,9 +456,11 @@ class VolumeRelease(Release):
389
456
  num_times = len(self.times)
390
457
 
391
458
  for tracer_concentrations in self.tracer_concentrations.values():
392
- tracer_concentrations.check_length(num_times)
459
+ if isinstance(tracer_concentrations, Concentration):
460
+ tracer_concentrations.check_length(num_times)
393
461
 
394
- self.volume_fluxes.check_length(num_times)
462
+ if isinstance(self.volume_fluxes, Flux):
463
+ self.volume_fluxes.check_length(num_times)
395
464
 
396
465
  return self
397
466
 
@@ -410,7 +479,52 @@ class VolumeRelease(Release):
410
479
  @staticmethod
411
480
  def get_tracer_metadata():
412
481
  """Returns long names and expected units for the tracer concentrations."""
413
- return get_tracer_metadata_dict(include_bgc=True, with_flux_units=False)
482
+ return get_tracer_metadata_dict(include_bgc=True, unit_type="concentration")
483
+
484
+ def _do_accounting(
485
+ self,
486
+ roms_time_stamps: np.ndarray,
487
+ model_reference_date: datetime,
488
+ ) -> dict[str, float]:
489
+ """
490
+ Compute time-integrated tracer quantities over ROMS time steps.
491
+
492
+ This method interpolates tracer flux time series from the CDR schedule
493
+ onto the provided ROMS time stamps (in seconds since model reference date),
494
+ then applies a "left-hold" rule: the interpolated value at t₀ is applied
495
+ across the full interval [t₀, t₁).
496
+
497
+ Parameters
498
+ ----------
499
+ roms_time_stamps : np.ndarray
500
+ 1D array of ROMS time stamps (seconds since `model_reference_date`).
501
+ Must be strictly increasing.
502
+ model_reference_date : datetime
503
+ Reference date of the ROMS model calendar.
504
+
505
+ Returns
506
+ -------
507
+ dict[str, float]
508
+ Dictionary mapping tracer names to the total integrated quantity over
509
+ the entire ROMS time period. Each value is the sum of the interpolated
510
+ tracer fluxes multiplied by the corresponding ROMS time step durations.
511
+ """
512
+ tracer_series_dict = {}
513
+ volume_array = (
514
+ np.asarray(self.volume_fluxes.values)
515
+ if isinstance(self.volume_fluxes, Flux)
516
+ else np.asarray(self.volume_fluxes)
517
+ )
518
+ for tracer, conc in self.tracer_concentrations.items():
519
+ tracer_array = (
520
+ np.asarray(conc.values)
521
+ if isinstance(conc, Concentration)
522
+ else np.asarray(conc)
523
+ )
524
+ tracer_series_dict[tracer] = volume_array * tracer_array
525
+ return self._compute_integrated_tracers(
526
+ roms_time_stamps, model_reference_date, tracer_series_dict
527
+ )
414
528
 
415
529
  @model_serializer(mode="wrap")
416
530
  def _simplified_dump(self, pydantic_serializer) -> dict:
@@ -503,7 +617,8 @@ class TracerPerturbation(Release):
503
617
  def _check_tracer_flux_lengths(self):
504
618
  num_times = len(self.times)
505
619
  for flux in self.tracer_fluxes.values():
506
- flux.check_length(num_times)
620
+ if isinstance(flux, Flux):
621
+ flux.check_length(num_times)
507
622
  return self
508
623
 
509
624
  def _extend_to_endpoints(self, start_time, end_time):
@@ -520,7 +635,45 @@ class TracerPerturbation(Release):
520
635
  @staticmethod
521
636
  def get_tracer_metadata():
522
637
  """Returns long names and expected units for the tracer fluxes."""
523
- return get_tracer_metadata_dict(include_bgc=True, with_flux_units=True)
638
+ return get_tracer_metadata_dict(include_bgc=True, unit_type="flux")
639
+
640
+ def _do_accounting(
641
+ self,
642
+ roms_time_stamps: np.ndarray,
643
+ model_reference_date: datetime,
644
+ ) -> dict[str, float]:
645
+ """
646
+ Compute time-integrated tracer quantities over ROMS time steps.
647
+
648
+ This method interpolates tracer flux time series from the CDR schedule
649
+ onto the provided ROMS time stamps (in days since model reference date),
650
+ then applies a "left-hold" rule: the interpolated value at t₀ is applied
651
+ across the full interval [t₀, t₁).
652
+
653
+ Parameters
654
+ ----------
655
+ roms_time_stamps : np.ndarray
656
+ 1D array of ROMS time stamps (days since `model_reference_date`).
657
+ Must be strictly increasing.
658
+ model_reference_date : datetime
659
+ Reference date of the ROMS model calendar.
660
+
661
+ Returns
662
+ -------
663
+ dict[str, float]
664
+ Dictionary mapping tracer names to the total integrated quantity over
665
+ the entire ROMS time period. Each value is the sum of the interpolated
666
+ tracer fluxes multiplied by the corresponding ROMS time step durations.
667
+ """
668
+ tracer_series_dict = {
669
+ tracer: np.asarray(flux.values)
670
+ if isinstance(flux, Flux)
671
+ else np.asarray(flux)
672
+ for tracer, flux in self.tracer_fluxes.items()
673
+ }
674
+ return self._compute_integrated_tracers(
675
+ roms_time_stamps, model_reference_date, tracer_series_dict
676
+ )
524
677
 
525
678
  @model_serializer(mode="wrap")
526
679
  def _simplified_dump(self, pydantic_serializer) -> dict: