Skip to content

Commit d73c5d4

Browse files
committed
Centralized the logging level and made it settable via CLI and config file. Moved the resample logic into a resampling engine, added a convenience for_each domain expansion, changed time_interval to just TIME and made generally possible to define multiple time domains there as a list, removed time iteration to plot and added a _plot_one func, eventually this plot func will abstract iteration and move up to Plot class
1 parent 393a652 commit d73c5d4

30 files changed

Lines changed: 407 additions & 96 deletions

ClimateGraph/appkernel.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
from ClimateGraph.utils.parser import Parser
66

7-
logging.basicConfig(level=logging.INFO)
8-
97

108
class AppKernel:
119
"""ClimateGraph execution and state manager. Orquestrates the other modules."""
@@ -65,19 +63,34 @@ def set_analysis_data(self, analysis: dict | None = None):
6563
self.output_path = analysis.get("output_path", Path("./"))
6664
self.workers = analysis.get("workers")
6765

68-
def run(self, control_path: Path):
66+
def _configure_logging(self):
67+
"""_configure_logging Set the root logger level from self.debug.
68+
69+
The only place in the codebase that calls ``logging.basicConfig``.
70+
``force=True`` so this always wins regardless of import order, even if
71+
some other library configured logging before this runs.
72+
"""
73+
level = logging.DEBUG if self.debug else logging.INFO
74+
logging.basicConfig(level=level, force=True)
75+
76+
def run(self, control_path: Path, debug_override: bool = False):
6977
"""run Run the ClimateGraph routine.
7078
7179
Parameters
7280
----------
7381
control_path : Path
7482
Path of the configuration file for the ClimateGraph run.
83+
debug_override : bool, optional
84+
CLI override for the control file's ``debug`` setting. ORed with
85+
the control file's value, so passing True always wins. By default False
7586
"""
7687
self.analysis, self.data, self.plots, self.domains = self.read_control(
7788
control_path
7889
)
7990

8091
self.set_analysis_data()
92+
self.debug = debug_override or self.debug
93+
self._configure_logging()
8194
# if self.eager : self.load_data()
8295
with self._dask_client():
8396
self.plot()

ClimateGraph/cli.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,26 @@ def run(
2424
help="File defining the analysis.",
2525
),
2626
],
27+
debug: Annotated[
28+
bool,
29+
typer.Option(
30+
"--debug",
31+
help="Force DEBUG-level logging. ORed with the control file's debug setting.",
32+
),
33+
] = False,
2734
):
28-
"""run Run the ClimateGraph routine with a control file.
35+
"""Run the ClimateGraph routine with a control file.
2936
3037
Parameters
3138
----------
3239
control_file : Annotated[ Path, typer.Argument, optional
3340
Path of the configuration file for the ClimateGraph run, by default True, dir_okay=False, file_okay=True, resolve_path=True, help="File defining the analysis.", ), ]
41+
debug : Annotated[bool, typer.Option, optional
42+
When passed, forces DEBUG-level logging regardless of the control file's
43+
debug setting. By default False
3444
"""
3545
appK = AppKernel()
36-
appK.run(control_file)
46+
appK.run(control_file, debug_override=debug)
3747

3848

3949
@app.command()

ClimateGraph/data/data.py

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import numpy as np
77
import pandas as pd
88
import xarray as xr
9-
from pyresample import kd_tree
109

1110
from ClimateGraph.reader import Reader
1211
from ClimateGraph.reader.reader.reader import ReadSpec
@@ -19,6 +18,7 @@
1918
)
2019
from ClimateGraph.utils.general_utils import ReductionMethodEnum
2120
from ClimateGraph.utils.registry import RegistryMixin
21+
from ClimateGraph.utils.resample_engine import ResampleEngine, get_engine
2222

2323

2424
class Data(RegistryMixin, ABC):
@@ -398,7 +398,6 @@ def get_coordinates(
398398
coords = coords[0]
399399
return coords
400400

401-
# Might need to become a whole pairing engine in some time. But for now this will do
402401
def resample_vars(
403402
self,
404403
other: "Data",
@@ -407,8 +406,10 @@ def resample_vars(
407406
time_interval: str | None = None,
408407
radius_of_influence: int = 10000,
409408
time_tolerance: str | None = "30min",
409+
engine: str | ResampleEngine = "pyresample",
410+
engine_kwargs: dict | None = None,
410411
) -> xr.DataArray | xr.Dataset:
411-
"""resample_vars Resample the requested vars using Pyresample and the geom attributes. Until now only NearestNeighbour method is being used.
412+
"""Resample the requested vars using the specified ResampleEngine.
412413
413414
Parameters
414415
----------
@@ -421,9 +422,15 @@ def resample_vars(
421422
time_interval : str | None, optional
422423
Time interval in dd/mm/yyyy-dd/mm/yyyy or d/m/yyyy-d/m/yyyy, by default None
423424
radius_of_influence : int, optional
424-
Radius length in meters to use for resampling with nearest neighbours. Lower improves computation time but may result in less resulting data, by default 10000
425+
Radius length in meters to use for resampling. by default 10000
425426
time_tolerance : str | None, optional
426-
Pandas-style timedelta used as the tolerance when snapping ``other``'s time axis onto ``self``'s via nearest-neighbour reindex. Handles cases where the two sources are on the same cadence but offset (e.g. CHIMERE at HH:30 vs. point-surface at HH:00). Set to ``None`` to disable snapping. Default ``"30min"``.
427+
Pandas-style timedelta used as the tolerance when snapping ``other``'s
428+
time axis onto ``self``'s via nearest-neighbour reindex. Default ``"30min"``.
429+
engine : str | ResampleEngine, optional
430+
Resample backend name or instance. Default ``"pyresample"``.
431+
engine_kwargs : dict | None, optional
432+
Extra kwargs forwarded to the engine constructor
433+
(e.g. ``method``, ``sigmas``). Default None.
427434
428435
Returns
429436
-------
@@ -433,20 +440,17 @@ def resample_vars(
433440
if isinstance(vars, str):
434441
vars = [vars]
435442

443+
resample_engine = get_engine(engine, **(engine_kwargs or {}))
436444
src_geom = other.geom
437445
dst_geom = self.geom
438446

439-
valid_input, valid_output, index_array, dist_array = kd_tree.get_neighbour_info(
447+
info = resample_engine.prepare(
440448
src_geom,
441449
dst_geom,
442450
radius_of_influence=radius_of_influence,
443-
neighbours=1,
444451
)
452+
_resample = resample_engine.make_resampler(info, dst_geom.shape)
445453

446-
# Establish the destination time grid up front so var_src can be snapped onto
447-
# it before resampling. Without this, sources whose time axis is offset from
448-
# self's (e.g. CHIMERE labelled at HH:30 vs point-surface at HH:00) end up
449-
# producing a resampled array whose time length doesn't match self's.
450454
if self.resampled is None:
451455
self.resampled = self.obj.drop_vars(list(self.obj.data_vars))
452456
self.resampled = time_resampling(
@@ -458,14 +462,10 @@ def resample_vars(
458462
var_dst_dims = self.get_var(var).sizes
459463
var_src = other.get_var(var)
460464

461-
# The caller wants the var either in a specified unit (dict form) or
462-
# in the unit of the resampling base (list/str form). var_unit yields
463-
# None when units weren't declared, and change_unit no-ops on None.
464465
dst_unit = self.var_unit(var) if isinstance(vars, list | str) else vars[var]
465466
src_unit = other.var_unit(var)
466467
var_src = change_unit(var_src, src_unit, dst_unit)
467468

468-
# time resampling
469469
var_src = time_resampling(
470470
var_src, timestep=timestep, time_interval=time_interval
471471
)
@@ -477,22 +477,6 @@ def resample_vars(
477477
tolerance=pd.Timedelta(time_tolerance),
478478
)
479479

480-
def _resample(x):
481-
return kd_tree.get_sample_from_neighbour_info(
482-
"nn",
483-
dst_geom.shape,
484-
x,
485-
valid_input,
486-
valid_output,
487-
index_array,
488-
dist_array,
489-
fill_value=np.nan,
490-
)
491-
492-
# input_core_dims: only the geom_dims of the source (the horizontal dims
493-
# the pyresample geometry was built from). Any extra dims like z are left
494-
# out so apply_ufunc loops over them automatically, broadcasting the
495-
# horizontal resampling across each vertical level independently.
496480
src_geom_dims = [d for d in var_src.dims if d in set(other.geom_dims)]
497481
dst_geom_dims = [d for d in var_dst_dims if d in set(self.geom_dims)]
498482
resampled = xr.apply_ufunc(

ClimateGraph/domain/domain.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import logging
21
from abc import ABC, abstractmethod
32

43
import xarray as xr
54
from pydantic import BaseModel
65

76
from ClimateGraph.utils.registry import RegistryMixin
87

9-
logging.basicConfig(level=logging.INFO) # TODO: make this settable from yaml file.
10-
118
# TODO: Change the use of BaseModel for actual attributes to improve modularization.
129

1310

ClimateGraph/domain/domains.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class AttributeConfig(BaseModel):
1616
type: Literal["attribute", "attr"]
1717
field_name: str
1818
field_value: Any
19+
one_for_each: bool = False
20+
"""When field_value is a list and this is True, the parser expands this
21+
single domain block into one Attribute domain per value. When False (default),
22+
the set is analyzed together."""
1923

2024

2125
class Attribute(Domain):
@@ -39,7 +43,11 @@ def apply(self, data: xr.Dataset | xr.DataArray):
3943
"""
4044
field_name = self.domain_config.field_name
4145
field_value = self.domain_config.field_value
42-
return data.where((data[field_name] == field_value).compute(), drop=True)
46+
if isinstance(field_value, list):
47+
mask = data[field_name].isin(field_value)
48+
else:
49+
mask = data[field_name] == field_value
50+
return data.where(mask.compute(), drop=True)
4351

4452

4553
class PolygonConfig(BaseModel):

ClimateGraph/plot/plot.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import logging
21
from abc import ABC, abstractmethod
32
from pathlib import Path
43

@@ -12,8 +11,6 @@
1211
from ClimateGraph.domain import Domain
1312
from ClimateGraph.utils.registry import RegistryMixin
1413

15-
logging.basicConfig(level=logging.INFO) # TODO: make this settable from yaml file.
16-
1714
# Which keys in plot_kwargs get routed to which matplotlib call. A given key
1815
# may legitimately belong to more than one sink (e.g. `dpi` applies to both
1916
# the figure and savefig), so the sets overlap on purpose. matplotlib rejects

0 commit comments

Comments
 (0)