ClimateGraph 0.4.0__tar.gz → 0.4.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 (54) hide show
  1. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/appkernel.py +6 -2
  2. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/data/data.py +5 -0
  3. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/plot/plots.py +15 -1
  4. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/reader.py +12 -2
  5. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/utils/dataset_utils.py +2 -1
  6. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/utils/general_utils.py +7 -2
  7. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph.egg-info/PKG-INFO +1 -1
  8. {climategraph-0.4.0 → climategraph-0.4.2}/PKG-INFO +1 -1
  9. {climategraph-0.4.0 → climategraph-0.4.2}/pyproject.toml +1 -1
  10. {climategraph-0.4.0 → climategraph-0.4.2}/tests/test_appkernel.py +41 -0
  11. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/__init__.py +0 -0
  12. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/cli.py +0 -0
  13. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/data/__init__.py +0 -0
  14. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/data/point_surface.py +0 -0
  15. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/data/regular_grid.py +0 -0
  16. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/data/satellite_swath.py +0 -0
  17. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/domain/__init__.py +0 -0
  18. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/domain/domain.py +0 -0
  19. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/domain/domains.py +0 -0
  20. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/plot/__init__.py +0 -0
  21. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/plot/plot.py +0 -0
  22. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/plot/primitive.py +0 -0
  23. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/plot/primitives.py +0 -0
  24. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/__init__.py +0 -0
  25. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/__init__.py +0 -0
  26. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/point_surface/__init__.py +0 -0
  27. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/point_surface/csv_base.py +0 -0
  28. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/point_surface/default.py +0 -0
  29. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/point_surface/dmc.py +0 -0
  30. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/point_surface/sinca.py +0 -0
  31. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/point_surface/single_file.py +0 -0
  32. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/point_surface/station_per_file.py +0 -0
  33. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/point_surface/variable_per_file.py +0 -0
  34. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/regular_grid/__init__.py +0 -0
  35. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/regular_grid/chimere.py +0 -0
  36. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/regular_grid/default.py +0 -0
  37. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/reader/reader/regular_grid/wrf.py +0 -0
  38. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/utils/__init__.py +0 -0
  39. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/utils/control_model.py +0 -0
  40. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/utils/parser.py +0 -0
  41. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/utils/registry.py +0 -0
  42. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph/utils/resample_engine.py +0 -0
  43. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph.egg-info/SOURCES.txt +0 -0
  44. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph.egg-info/dependency_links.txt +0 -0
  45. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph.egg-info/entry_points.txt +0 -0
  46. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph.egg-info/requires.txt +0 -0
  47. {climategraph-0.4.0 → climategraph-0.4.2}/ClimateGraph.egg-info/top_level.txt +0 -0
  48. {climategraph-0.4.0 → climategraph-0.4.2}/LICENSE +0 -0
  49. {climategraph-0.4.0 → climategraph-0.4.2}/README.md +0 -0
  50. {climategraph-0.4.0 → climategraph-0.4.2}/setup.cfg +0 -0
  51. {climategraph-0.4.0 → climategraph-0.4.2}/tests/test_against_manual_filter.py +0 -0
  52. {climategraph-0.4.0 → climategraph-0.4.2}/tests/test_cli.py +0 -0
  53. {climategraph-0.4.0 → climategraph-0.4.2}/tests/test_parser.py +0 -0
  54. {climategraph-0.4.0 → climategraph-0.4.2}/tests/test_parser_2.py +0 -0
@@ -129,6 +129,10 @@ class AppKernel:
129
129
  def _dask_client(self):
130
130
  """Yield a dask client, or ``None`` when no cluster was requested.
131
131
 
132
+ One thread per worker: libnetcdf is not thread-safe, so reads serialise
133
+ behind a global lock that is held per process. Threads therefore queue
134
+ on it while processes read in parallel.
135
+
132
136
  Yields
133
137
  ------
134
138
  dask.distributed.Client or None
@@ -138,9 +142,9 @@ class AppKernel:
138
142
  return
139
143
  from dask.distributed import Client, LocalCluster
140
144
 
141
- cluster = LocalCluster(n_workers=1, threads_per_worker=self.workers)
145
+ cluster = LocalCluster(n_workers=self.workers, threads_per_worker=1)
142
146
  client = Client(cluster)
143
- log.info(f"Dask Client started: 1 worker x {self.workers} threads.")
147
+ log.info(f"Dask Client started: {self.workers} workers x 1 thread.")
144
148
  try:
145
149
  yield client
146
150
  finally:
@@ -477,6 +477,11 @@ class Data(RegistryMixin, ABC):
477
477
  var_src = other.get_var(var)
478
478
  src_geom_dims = [d for d in var_src.dims if d in set(other.geom_dims)]
479
479
 
480
+ # apply_ufunc needs a core dim whole, and a NetCDF4 file's internal
481
+ # chunking splits the geometry dims when nothing names them.
482
+ if var_src.chunks:
483
+ var_src = var_src.chunk(dict.fromkeys(src_geom_dims, -1))
484
+
480
485
  resampled = xr.apply_ufunc(
481
486
  _resample,
482
487
  var_src,
@@ -737,17 +737,31 @@ class TimeCycleConfig(BasePlotConfig):
737
737
  return self
738
738
  # map both to hours for comparison
739
739
  bucket_hours = {
740
+ "minute": 1 / 60,
740
741
  "hour": 1,
741
742
  "day": 24,
743
+ "week": 168,
742
744
  "month": 720,
743
745
  "dayofyear": 24,
744
746
  "season": 2160,
747
+ "quarter": 2160,
745
748
  }
746
749
  timestep_hours = {
750
+ TimestepEnum.nanoseconds: 1 / 3.6e12,
751
+ TimestepEnum.microseconds: 1 / 3.6e9,
752
+ TimestepEnum.milliseconds: 1 / 3.6e6,
753
+ TimestepEnum.secondly: 1 / 3600,
754
+ TimestepEnum.minutely: 1 / 60,
747
755
  TimestepEnum.hourly: 1,
748
756
  TimestepEnum.daily: 24,
757
+ TimestepEnum.business_day: 24,
758
+ TimestepEnum.weekly: 168,
749
759
  TimestepEnum.monthly: 720,
750
- # extend as needed
760
+ TimestepEnum.monthly_start: 720,
761
+ TimestepEnum.quarterly: 2160,
762
+ TimestepEnum.quarterly_start: 2160,
763
+ TimestepEnum.yearly: 8760,
764
+ TimestepEnum.yearly_start: 8760,
751
765
  }
752
766
  if timestep_hours[self.timestep] > bucket_hours[self.time_buckets.value]:
753
767
  raise ValueError(
@@ -277,6 +277,12 @@ class Reader(ABC):
277
277
  return manage_path(spec.paths, sort=spec.load_mode == "unsafe")
278
278
 
279
279
  open_engine: str = "netcdf4"
280
+ # One chunk per file, every dim. Chunking runs before `_preprocess`, so dim
281
+ # names are still file-native here and a dim-keyed spec silently misses;
282
+ # a scalar sidesteps that. The alternative, deferring to a NetCDF4 file's
283
+ # internal layout, is often one timestep per chunk. Override per dataset
284
+ # with `chunks:` in the data block, keyed on the file's own dim names.
285
+ default_chunks: int | dict = -1
280
286
 
281
287
  @classmethod
282
288
  def _open_many(cls, paths: list[Path], spec: ReadSpec) -> xr.Dataset:
@@ -301,7 +307,7 @@ class Reader(ABC):
301
307
  engine = spec.engine or cls.open_engine
302
308
  return xr.open_mfdataset(
303
309
  paths,
304
- chunks={"time": "auto"},
310
+ chunks=spec.extras.get("chunks", cls.default_chunks),
305
311
  engine=engine,
306
312
  parallel=engine != "netcdf4",
307
313
  preprocess=per_file,
@@ -311,7 +317,11 @@ class Reader(ABC):
311
317
  def _open_one(cls, path: Path, spec: ReadSpec) -> Any:
312
318
  """Open a single file (unsafe path)."""
313
319
  engine = spec.engine or cls.open_engine
314
- return xr.open_dataset(path, chunks={"time": "auto"}, engine=engine)
320
+ return xr.open_dataset(
321
+ path,
322
+ chunks=spec.extras.get("chunks", cls.default_chunks),
323
+ engine=engine,
324
+ )
315
325
 
316
326
  @classmethod
317
327
  def _to_xarray(cls, raw: Any, spec: ReadSpec) -> xr.Dataset:
@@ -243,7 +243,8 @@ def time_resampling(
243
243
  ds = ds.sel({"time": slice(start, end)})
244
244
  _record(ds, f"selected time interval {start} to {end}")
245
245
  if timestep is not None:
246
- ds = getattr(ds.resample(time=timestep), reduction_method.value)()
246
+ freq = getattr(timestep, "value", timestep)
247
+ ds = getattr(ds.resample(time=freq), reduction_method.value)()
247
248
  _record(ds, f"resampled time to {timestep} ({reduction_method.value})")
248
249
  return ds
249
250
 
@@ -29,8 +29,13 @@ class TimestepEnum(str, Enum):
29
29
  daily = "D"
30
30
  weekly = "W"
31
31
  monthly = "ME"
32
- quarterly = "Q"
33
- yearly = "Y"
32
+ # Period-end spellings: pandas 2.2 deprecated "Q"/"Y" and 3.0 removed them.
33
+ quarterly = "QE"
34
+ yearly = "YE"
35
+ # Period-start spellings: label each bucket with its first instant.
36
+ monthly_start = "MS"
37
+ quarterly_start = "QS"
38
+ yearly_start = "YS"
34
39
  hourly = "h"
35
40
  minutely = "min"
36
41
  secondly = "s"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ClimateGraph
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: A CLI tool for graphing Climate Data
5
5
  Author-email: Antonio Andrés Torga Mellado <antonio.torga@ug.uchile.cl>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ClimateGraph
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: A CLI tool for graphing Climate Data
5
5
  Author-email: Antonio Andrés Torga Mellado <antonio.torga@ug.uchile.cl>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name= "ClimateGraph"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "A CLI tool for graphing Climate Data"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -102,3 +102,44 @@ class TestDebugOverride:
102
102
  kernel = AppKernel()
103
103
  kernel.run(tmp_path / "ignored.yaml", debug_override=False)
104
104
  assert kernel.debug is False
105
+
106
+
107
+ class TestDaskClient:
108
+ """``workers`` must buy worker processes, not threads.
109
+
110
+ libnetcdf is not thread-safe, so xarray serialises reads behind a lock held
111
+ per process. Threads queue on it and measure slower than no cluster at all;
112
+ processes each hold their own and read in parallel.
113
+ """
114
+
115
+ @pytest.fixture
116
+ def captured_cluster(self, monkeypatch):
117
+ captured = {}
118
+
119
+ class _FakeCluster:
120
+ def __init__(self, **kwargs):
121
+ captured.update(kwargs)
122
+
123
+ def close(self):
124
+ pass
125
+
126
+ import dask.distributed
127
+
128
+ monkeypatch.setattr(dask.distributed, "LocalCluster", _FakeCluster)
129
+ monkeypatch.setattr(dask.distributed, "Client", lambda cluster: MagicMock())
130
+ return captured
131
+
132
+ def test_workers_become_processes(self, captured_cluster):
133
+ kernel = AppKernel()
134
+ kernel.workers = 8
135
+ with kernel._dask_client() as client:
136
+ assert client is not None
137
+ assert captured_cluster["n_workers"] == 8
138
+ assert captured_cluster["threads_per_worker"] == 1
139
+
140
+ def test_no_workers_means_no_cluster(self, captured_cluster):
141
+ kernel = AppKernel()
142
+ kernel.workers = None
143
+ with kernel._dask_client() as client:
144
+ assert client is None
145
+ assert captured_cluster == {}
File without changes
File without changes
File without changes