Physics driver l2#1301
Conversation
msimberg
left a comment
There was a problem hiding this comment.
This is not really a review, but more questions and comments about minor and major things. Mostly for my own understanding and reminder to revisit as the PR evolves, but adding the comments in case someone else has thought about them already and has answers.
| def __call__( | ||
| self, state: dict[str, model.DataField], time_step: datetime.datetime | ||
| ) -> dict[str, model.DataField]: |
There was a problem hiding this comment.
Question for myself:I suppose this is one of the critical interfaces, though the signature could likely be tightened to something resembling state: (Something?)State, time_step: datetime.datetime? Or the whole class should inherit from a protocol.
There was a problem hiding this comment.
Minor note as well that may need broader discussion: If this is indeed the protocol, my preference is to give this a name rather than using the generic __call__ operator, but I also know we already use __call__ extensively, so may be me still not being used to __call__ being used everywhere.
There was a problem hiding this comment.
And further thinking out loud: Could this be based on a more generic TendencyAdaptor? The muphys-specific parts seem quite minimal and the interesting logic seems to be the _to_tendency parts.
There was a problem hiding this comment.
and time_step should be datetime.timedelta if we decide to use timedeltas for parametrizing (as opposed to the integer time steps)
| self, | ||
| prognostic: prognostics.PrognosticState, | ||
| outputs: dict[str, fa.CellKField[ta.wpfloat]], | ||
| dt: float, |
There was a problem hiding this comment.
I guess this is in seconds? I wonder if we can add a bit more type information to this (something from datetime?) and just convert to the correct units as late as possible (i.e. when passing into gt4py programs).
There was a problem hiding this comment.
after a separate discussion with @msimberg we came up with this for the standalone driver:
https://github.com/C2SM/icon4py/pull/1304/changes#diff-1374141d85d171d3fa2c5711b656072708d65ff1080beaf0cdeedcc64f105bd7R37
should we use it here too?
| @@ -0,0 +1,3 @@ | |||
| # icon4py-atmosphere-physics-interface | |||
|
|
|||
| The L2 physics orchestrator (`PhysicsDriver`) and its process registry (`PhysicsProcess`, `ProcessTimeControl`, `ForcingMode`) for icon4py, modelled on ICON-MPIM's AES physics interface (`aes_phy_main`). | |||
There was a problem hiding this comment.
let's not use two or more names for the same thing ;-)
see discussion in #icon4py-dev
There was a problem hiding this comment.
Also, this is still referencing the L2 naming. Please check if there are other places that need to have the LN naming changed.
| return dict( | ||
| standard_name=f"tendency_of_{base['standard_name']}", | ||
| units=f"{base['units']} s-1", | ||
| kind="tendency", | ||
| ) |
There was a problem hiding this comment.
also from a cheap-friend:
🟡 risk: _tendency_of returns dict(...) not model.FieldMetaData — TypedDict construction bypasses key validation. Use explicit model.FieldMetaData(standard_name=..., units=..., kind="tendency")
jcanton
left a comment
There was a problem hiding this comment.
some comments here and there
There was a problem hiding this comment.
Pull request overview
This PR introduces an L2 physics orchestration layer (PhysicsDriver) as a new workspace package and wires it into the standalone driver to enable running fast-physics (currently: muphys) after dynamics/diffusion/transport, along with supporting metadata and small math utilities needed by the new adapters.
Changes:
- Added new
icon4py-atmosphere-physics-interfacepackage containingPhysicsDriver,PhysicsProcess,ProcessTimeControl, andForcingMode. - Integrated a muphys physics process into
model/standalone_driver(granule initialization + per-timestep call site) and added muphys state/component/config/data adapters. - Extended common field metadata with a
kindtag and added shared CF metadata registries for microphysics precipitation diagnostics and generic tendencies.
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Adds new workspace member + dependency entries for icon4py-atmosphere-physics-interface and updates standalone_driver deps. |
| tach.toml | Registers physics_interface in the dependency graph; standalone_driver now depends on muphys + physics_interface. |
| pyproject.toml | Adds physics_interface to workspace dependencies, mypy paths, and pythonpath. |
| model/standalone_driver/src/icon4py/model/standalone_driver/standalone_driver.py | Injects ModelTimeVariables and invokes physics granule each timestep. |
| model/standalone_driver/src/icon4py/model/standalone_driver/driver_utils.py | Initializes PhysicsDriver granule and constructs a muphys PhysicsProcess. |
| model/standalone_driver/src/icon4py/model/standalone_driver/driver_states.py | Refactors ModelTimeVariables initialization and time-step count derivation. |
| model/standalone_driver/src/icon4py/model/standalone_driver/config.py | Adds muphys config field (placeholder; parsing TODOs). |
| model/standalone_driver/pyproject.toml | Adds dependencies on muphys and the new physics-interface package. |
| model/common/tests/common/components/unit_tests/init.py | Adds package marker/license header for new tests package path. |
| model/common/tests/common/components/init.py | Adds package marker/license header for new tests package path. |
| model/common/src/icon4py/model/common/states/model.py | Adds optional kind metadata field for physics outputs (tendency vs diagnostic). |
| model/common/src/icon4py/model/common/states/data.py | Adds shared CF metadata for microphysics precipitation diagnostics and generic tendencies. |
| model/common/src/icon4py/model/common/math/stencils/generic_math_operations.py | Adds GT4Py programs for a + coeff*b and copy operations used by physics adapters. |
| model/common/src/icon4py/model/common/math/operators.py | Adds underlying field_operators for a + coeff*b and copy. |
| model/common/src/icon4py/model/common/components/physics_state.py | Introduces PhysicsState protocol (driver-facing state adapter interface). |
| model/atmosphere/subgrid_scale_physics/physics_interface/tests/physics_interface/unit_tests/init.py | Initializes physics_interface unit test package (no tests yet). |
| model/atmosphere/subgrid_scale_physics/physics_interface/tests/physics_interface/init.py | Initializes physics_interface tests package. |
| model/atmosphere/subgrid_scale_physics/physics_interface/src/icon4py/model/atmosphere/subgrid_scale_physics/physics_interface/physics_driver.py | Adds PhysicsDriver orchestrator and time-control primitives. |
| model/atmosphere/subgrid_scale_physics/physics_interface/src/icon4py/model/atmosphere/subgrid_scale_physics/physics_interface/init.py | Package metadata/version for physics_interface. |
| model/atmosphere/subgrid_scale_physics/physics_interface/README.md | Brief package-level description of the new orchestrator. |
| model/atmosphere/subgrid_scale_physics/physics_interface/pyproject.toml | New package definition for icon4py-atmosphere-physics-interface. |
| model/atmosphere/subgrid_scale_physics/muphys/tests/muphys/unit_tests/init.py | Initializes muphys unit test package path (no unit tests added here). |
| model/atmosphere/subgrid_scale_physics/muphys/src/icon4py/model/atmosphere/subgrid_scale_physics/muphys/state.py | Adds muphys PhysicsState adapter to gather/scatter prognostic state and apply tendencies. |
| model/atmosphere/subgrid_scale_physics/muphys/src/icon4py/model/atmosphere/subgrid_scale_physics/muphys/data.py | Defines muphys component input/output field metadata mappings. |
| model/atmosphere/subgrid_scale_physics/muphys/src/icon4py/model/atmosphere/subgrid_scale_physics/muphys/config.py | Adds MuphysConfig (currently only qnc). |
| model/atmosphere/subgrid_scale_physics/muphys/src/icon4py/model/atmosphere/subgrid_scale_physics/muphys/component.py | Adds MuphysComponent wrapper that runs muphys and converts state updates to tendencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for proc in self._processes: | ||
| state = proc.state | ||
| state.gather_from_prognostic(prognostic, tracers) | ||
| tc = proc.time_control | ||
| if not tc.enable_process: | ||
| continue | ||
| if not tc.is_in_window(simulation_current_datetime): | ||
| # outside the process window: no forcing | ||
| continue | ||
| # Compute on a firing (active) step, and also on the first in-window step -- when | ||
| # there is nothing cached to recycle yet. Otherwise reuse the last computed forcing. | ||
| if tc.is_active(simulation_current_datetime) or proc.name not in self._recycle_cache: | ||
| # compute | ||
| outputs = proc.component(state.as_component_input(), simulation_current_datetime) | ||
| self._recycle_cache[proc.name] = outputs | ||
| else: | ||
| # recycle | ||
| outputs = self._recycle_cache[proc.name] | ||
| state.scatter_to_prognostic(prognostic, outputs, dt) |
| elapsed = (simulation_current_datetime - self.start_date).total_seconds() | ||
| interval_s = self.interval.total_seconds() | ||
| quotient = elapsed / interval_s | ||
| return abs(quotient - round(quotient)) == 0.0 |
| from icon4py.model.common.states import model | ||
|
|
||
| _SPECIES = ("v", "c", "r", "s", "i", "g") | ||
| # TODO (Yilu): avoid duplicating specieis |
| # TODO (Yilu): this can belong to a standalone file (with the exception of ForcingMode.APPLY)? | ||
| @dataclasses.dataclass(frozen=True) | ||
| class ProcessTimeControl: | ||
| """icon4py analogue of the per-process fields in AES `aes_phy_tc`. | ||
|
|
msimberg
left a comment
There was a problem hiding this comment.
Just a few minor additoinal comments and todos.
|
|
||
| from icon4py.model.atmosphere.subgrid_scale_physics.muphys import data as muphys_data | ||
| from icon4py.model.atmosphere.subgrid_scale_physics.muphys.core.definitions import Q | ||
| from icon4py.model.atmosphere.subgrid_scale_physics.muphys.driver import run_full_muphys |
There was a problem hiding this comment.
Nit, maybe:
| from icon4py.model.atmosphere.subgrid_scale_physics.muphys.driver import run_full_muphys | |
| from icon4py.model.atmosphere.subgrid_scale_physics.muphys.driver import muphys_driver |
?
I see run_full_muphys was used elsewhere already, but I don't think it hurts to clean up this name.
| new_field=new, | ||
| tendency=out, | ||
| horizontal_start=0, | ||
| horizontal_end=self._ncells, |
There was a problem hiding this comment.
Have you checked if this needs to be computed all the way to _ncells or would the local patch be sufficient?
| return cast( | ||
| "dict[str, model.DataField]", |
| """Configuration for the muphys microphysics component. | ||
| """ | ||
|
|
||
| qnc: float |
There was a problem hiding this comment.
Just noting so it's not forgotten: was this a namelist option or a field in the end?
There was a problem hiding this comment.
it's a hardcoded float in fortran :-)
so we make it a configurable float, but not read from namelists
| _ACTIVE_TOLERANCE = 1e-6 # relative tolerance on the modulo, in fractions of `interval` | ||
|
|
||
|
|
||
| @dataclasses.dataclass(frozen=True) | ||
| class ProcessTimeControl: | ||
| """icon4py analogue of the per-process fields in AES `aes_phy_tc`. | ||
|
|
||
| Mirrors `mo_aes_phy_main.f90` semantics: | ||
| - `interval` (`dt_xxx`): zero means the process is disabled. | ||
| - `start_date` (`sd_xxx`), `end_date` (`ed_xxx`): half-open | ||
| `[start, end)` window during which the process exists at all. | ||
| - `forcing_mode` (`fc_xxx`): DIAGNOSTIC (compute only) or APPLY. | ||
| """ | ||
|
|
||
| interval: datetime.timedelta | ||
| start_date: datetime.datetime | ||
| end_date: datetime.datetime | ||
| forcing_mode: ForcingMode = ForcingMode.APPLY | ||
|
|
||
| def is_enabled(self) -> bool: | ||
| return self.interval > datetime.timedelta(0) | ||
|
|
||
| def is_in_window(self, now: datetime.datetime) -> bool: | ||
| return self.start_date <= now < self.end_date | ||
|
|
||
| def is_active(self, now: datetime.datetime) -> bool: | ||
| """True if the process's mtime-event fires on the given step. | ||
|
|
||
| Equivalent to AES `isCurrentEventActive(ev_xxx, datetime)`. Uses a | ||
| small relative tolerance to absorb datetime/timedelta floating-point | ||
| jitter accumulated over many steps. | ||
|
|
||
| """ | ||
| if now < self.start_date or not self.is_enabled(): | ||
| return False | ||
| elapsed = (now - self.start_date).total_seconds() | ||
| interval_s = self.interval.total_seconds() | ||
| quotient = elapsed / interval_s | ||
| return abs(quotient - round(quotient)) < _ACTIVE_TOLERANCE |
There was a problem hiding this comment.
Just noting that this I would like to see moved to a separate file and split from forcing_mode in this PR.
| elapsed = (simulation_current_datetime - self.start_date).total_seconds() | ||
| interval_s = self.interval.total_seconds() | ||
| quotient = elapsed / interval_s | ||
| return abs(quotient - round(quotient)) == 0.0 |
There was a problem hiding this comment.
I forget if we discussed whether or not this is allowed to run on non-integer-multiples of timesteps? If it should only be integer multiples, this needs at the very least a TODO/issue.
There was a problem hiding this comment.
no, only integer multiples, but with datetime comparisons, not converting to floats
| @@ -0,0 +1,3 @@ | |||
| # icon4py-atmosphere-physics-interface | |||
|
|
|||
| The L2 physics orchestrator (`PhysicsDriver`) and its process registry (`PhysicsProcess`, `ProcessTimeControl`, `ForcingMode`) for icon4py, modelled on ICON-MPIM's AES physics interface (`aes_phy_main`). | |||
There was a problem hiding this comment.
Also, this is still referencing the L2 naming. Please check if there are other places that need to have the LN naming changed.
| # TODO (Yilu): we can add a post_int() | ||
| # TODO (Yilu): use the name from ModelTimeVariables | ||
|
|
| class MuphysGranule: | ||
| """L4 per-process adapter wrapping the muphys microphysics program.""" | ||
| class MuphysComponent: | ||
| # TODO (Yilu): later on this should be cleared that it inherit from the component protocol |
There was a problem hiding this comment.
The Component protocol seems to need changes (it's got some generic type parameters where it's unclear if it makes sense to keep or not; in principle more structured checking is good, but I'm not about what requirements it puts on those who inherit from it). I've started investigations to make it usable (with a friend), but I think it's best not to mix it into this PR, so far it looks like a not entirely trivial change.
| backend: gtx_typing.Backend | None = None, | ||
| *, | ||
| muphys_step: Callable[..., Any] | None = None, | ||
| step: Callable[..., Any] | None = None, |
There was a problem hiding this comment.
ignorance of mine and not finding this:
is this ever passed in as argument? or should we rather remove it from the args and just set self._step = run_full_muphys.setup_muphys below?
| """Configuration for the muphys microphysics component. | ||
| """ | ||
|
|
||
| qnc: float |
There was a problem hiding this comment.
I think this could already use the facilities that @DropD has set up.
| "dz": model.FieldMetaData( | ||
| standard_name="layer_thickness", units="m" | ||
| ), |
There was a problem hiding this comment.
get this guy from metrics attributes ddqdz as discussed
There was a problem hiding this comment.
but also check if fortran for some reason re-computes it in a different way, just in case
| def _require(field: fa.CellKField[ta.wpfloat] | None, name: str) -> fa.CellKField[ta.wpfloat]: | ||
| """Return ``field``, or raise if it is inactive (``None``). | ||
|
|
||
| @gtx.field_operator | ||
| def _add_scaled_tendency( | ||
| field: fa.CellKField[ta.wpfloat], | ||
| tendency: fa.CellKField[ta.wpfloat], | ||
| dt: ta.wpfloat, | ||
| ) -> fa.CellKField[ta.wpfloat]: | ||
| return field + tendency * dt | ||
|
|
||
|
|
||
| @gtx.program(grid_type=gtx.GridType.UNSTRUCTURED) | ||
| def apply_tendency( # noqa: PLR0917 # stencil params referenced in domain specs must stay positional | ||
| field: fa.CellKField[ta.wpfloat], | ||
| tendency: fa.CellKField[ta.wpfloat], | ||
| dt: ta.wpfloat, | ||
| result: fa.CellKField[ta.wpfloat], | ||
| horizontal_start: gtx.int32, | ||
| horizontal_end: gtx.int32, | ||
| vertical_start: gtx.int32, | ||
| vertical_end: gtx.int32, | ||
| ) -> None: | ||
| """``result = field + tendency * dt`` (Lie-Trotter split-update). | ||
|
|
||
| Pass ``result=field`` for an in-place update, or a distinct buffer to keep | ||
| the original (e.g. computing the new temperature without clobbering ``te``). | ||
| muphys needs all six moisture species; ``TracerState`` fields are optional | ||
| (a tracer may be inactive per ``TracerConfig``), so we fail loudly here rather | ||
| than feed ``None`` into the microphysics. | ||
| """ | ||
| _add_scaled_tendency( | ||
| field, | ||
| tendency, | ||
| dt, | ||
| out=result, | ||
| domain={ | ||
| dims.CellDim: (horizontal_start, horizontal_end), | ||
| dims.KDim: (vertical_start, vertical_end), | ||
| }, | ||
| ) | ||
| if field is None: | ||
| raise ValueError(f"muphys requires tracer '{name}' to be active in the TracerState") | ||
| return field |
There was a problem hiding this comment.
you can use TracerState.active_names() instead of a specific helper here
There was a problem hiding this comment.
do you mean TracerState.active_fields() right?
| self.rho = prognostic.rho | ||
| self.q = {s: prognostic.tracer[_STUB_TRACER_INDEX[s]] for s in _SPECIES} | ||
|
|
||
| self.q = { |
There was a problem hiding this comment.
more generic: do you need this to be a dictionary?
or can this be a TracerState instead so you avoid passing around 6 separate variables with string identifiers. then you unpack it to the individual tracers only when calling the actual muphys.run()
There was a problem hiding this comment.
Related to the above and #1301 (comment), but orthogonal to your actual comment about TracerState, I want to note for context: together with formalizing the Component protocol I'd like to formalize the State concept/protocol whatever, so that we don't pass around plain dicts (the State might just be dict-like, but we should give it a name and a shape). My comment needs no further action in this PR, @jcanton's probably does.
Now that the above reminder is updated I'd like to encourage you to try out the new default pipeline to run only tests that you've added in this PR or that you're affecting. |
|
Mandatory Tests Before merging, run the When developing, you can test your changes on CSCS CI before merge with the You can pass options to override pipeline variables, for example:
Available options are:
See The Optional Tests To run benchmarks you can use:
For more detailed information please look at CI in the EXCLAIM universe. |
No description provided.