-
Notifications
You must be signed in to change notification settings - Fork 16
State V0 #242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
FlorianDeconinck
merged 12 commits into
NOAA-GFDL:develop
from
FlorianDeconinck:feature/state_v0
Oct 2, 2025
Merged
State V0 #242
Changes from 5 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
646764f
State v0 + utest
FlorianDeconinck 8a4993d
`dacite` as a new dependancy
FlorianDeconinck 6aac410
Fix for `data` setter in quantity reseting the `compute_view` for `fi…
FlorianDeconinck f77228d
Lint
FlorianDeconinck 376d66a
Fix circular import
FlorianDeconinck f9f480b
Add update/init distinction
FlorianDeconinck e8da572
Harden `quantity.data = ...` & add unit test
FlorianDeconinck 3a9fe33
Use extent
FlorianDeconinck 04079d3
Proper test fix
FlorianDeconinck e00a8c3
Lint
FlorianDeconinck af1529f
Better raise programming for `Quantity.data`
FlorianDeconinck ed3c77a
Better docs, naming for states. Improve quantity unit test
FlorianDeconinck File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,11 @@ | ||
| from .metadata import QuantityHaloSpec, QuantityMetadata | ||
| from .quantity import Quantity | ||
| from .state import State | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "Quantity", | ||
| "QuantityMetadata", | ||
| "QuantityHaloSpec", | ||
| "FieldBundle", | ||
| "FieldBundleType", | ||
| "State", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import dataclasses | ||
| from typing import TYPE_CHECKING, Any, Self | ||
|
|
||
| import dacite | ||
| import xarray as xr | ||
| from mpi4py import MPI | ||
| from numpy.typing import ArrayLike | ||
|
|
||
|
|
||
| if TYPE_CHECKING: | ||
| from ndsl import QuantityFactory | ||
|
|
||
|
|
||
| @dataclasses.dataclass | ||
| class State: | ||
| """Base class for State object in models that bundles a collection | ||
| of functions to deal with nested dataclasses and common usage of States: | ||
| - init (zero, from memory, zero copy buffer swap) | ||
| - IO (save to NetCDF, from NetCDF) | ||
|
|
||
| The State expects Quantities. | ||
| """ | ||
|
FlorianDeconinck marked this conversation as resolved.
Outdated
|
||
|
|
||
| @classmethod | ||
| def zeros(cls, quantity_factory: QuantityFactory) -> Self: | ||
| """Init all quantities to zeros - included nested ones""" | ||
|
FlorianDeconinck marked this conversation as resolved.
Outdated
|
||
|
|
||
| def _zeros_recursive(cls): | ||
| initial_quantities = {} | ||
| for _field in dataclasses.fields(cls): | ||
| if dataclasses.is_dataclass(_field.type): | ||
| initial_quantities[_field.name] = _zeros_recursive(_field.type) | ||
| else: | ||
| if "dims" not in _field.metadata.keys(): | ||
| raise ValueError( | ||
| "Malformed state - no dims to init " | ||
| f"Quantity in {_field.name} of type {_field.type}" | ||
| ) | ||
|
|
||
| initial_quantities[_field.name] = quantity_factory.zeros( | ||
| _field.metadata["dims"], | ||
| _field.metadata["units"], | ||
| dtype=_field.metadata["dtype"], | ||
| allow_mismatch_float_precision=True, | ||
| ) | ||
|
|
||
| return initial_quantities | ||
|
|
||
| dict_of_qty = _zeros_recursive(cls) | ||
|
FlorianDeconinck marked this conversation as resolved.
Outdated
|
||
| return dacite.from_dict(data_class=cls, data=dict_of_qty) | ||
|
|
||
| def init_from_memory(self, memory_map: dict[str, Any]): | ||
| """Will copy data from the memory map if it follows the nested | ||
| naming convention of the dataclass""" | ||
|
FlorianDeconinck marked this conversation as resolved.
Outdated
|
||
|
|
||
| def _init_from_memory_recursive(dataclss, memory_map: dict[str, Any]): | ||
| for name, array in memory_map.items(): | ||
| if isinstance(array, dict): | ||
| _init_from_memory_recursive(dataclss.__getattribute__(name), array) | ||
| else: | ||
| try: | ||
| dataclss.__getattribute__(name).field[:] = array | ||
| except ValueError as e: | ||
|
FlorianDeconinck marked this conversation as resolved.
Outdated
|
||
| e.add_note( | ||
| f"Error when initializing field {name} on state {type(self)}" | ||
| ) | ||
| raise e | ||
|
|
||
| _init_from_memory_recursive(self, memory_map) | ||
|
|
||
| def init_zero_copy(self, memory_map: dict[str, Any], check: bool = True): | ||
|
FlorianDeconinck marked this conversation as resolved.
Outdated
|
||
| """Swap buffers given into the Quantities carried by the state | ||
| by following dataclass naming convention""" | ||
|
|
||
| def _init_zero_copy_recursive(dataclss, memory_map: dict[str, Any | ArrayLike]): | ||
| for name, array in memory_map.items(): | ||
| if isinstance(array, dict): | ||
| _init_zero_copy_recursive(dataclss.__getattribute__(name), array) | ||
| else: | ||
| qty = dataclss.__getattribute__(name) | ||
|
FlorianDeconinck marked this conversation as resolved.
Outdated
|
||
| if check: | ||
| if array.shape != qty.field.shape: | ||
| e = ValueError("Shape mismatch on zero copy for") | ||
| e.add_note(f" Error on {name} for {type(dataclss)}") | ||
| e.add_note(f" Shapes: {array.shape} != {qty.field.shape}") | ||
| raise e | ||
| if array.strides != qty.data.strides: | ||
| e = ValueError("Stride mismatch on zero copy for") | ||
| e.add_note(f" Error on {name} for {type(dataclss)}") | ||
| e.add_note( | ||
| f" Strides: {array.strides} != {qty.data.strides}" | ||
| ) | ||
| raise e | ||
|
|
||
| qty.data = array | ||
|
|
||
| _init_zero_copy_recursive(self, memory_map) | ||
|
|
||
| def to_netcdf(self, path: str = "./"): | ||
|
FlorianDeconinck marked this conversation as resolved.
Outdated
|
||
| def _save_recursive(datclss: State): | ||
|
FlorianDeconinck marked this conversation as resolved.
Outdated
|
||
| local_data = {} | ||
| for _field in dataclasses.fields(datclss): | ||
| if dataclasses.is_dataclass(_field.type): | ||
| local_data[_field.name] = xr.Dataset( | ||
| data_vars=_save_recursive(datclss.__getattribute__(_field.name)) | ||
| ) | ||
| else: | ||
| if "dims" not in _field.metadata.keys(): | ||
| raise ValueError( | ||
| "Malformed state - no dims to init " | ||
| f"Quantity in {_field.name} of type {_field.type}" | ||
| ) | ||
|
|
||
| local_data[_field.name] = datclss.__getattribute__( | ||
| _field.name | ||
| ).field_as_xarray | ||
|
|
||
| return local_data | ||
|
|
||
| datatree = _save_recursive(self) | ||
|
|
||
| # Move top-level into their own dataset in the "/" prefix | ||
| # to match DataTree expected format | ||
| top_level = {} | ||
| for key, value in datatree.items(): | ||
| if not isinstance(value, xr.Dataset): | ||
| top_level[key] = value | ||
| for key, value in top_level.items(): | ||
| datatree.pop(key) | ||
| datatree["/"] = xr.Dataset(data_vars=top_level) | ||
|
|
||
| # Resolve rank-tied postfix if needed | ||
| rank_postfix = "" | ||
| if MPI.COMM_WORLD.Get_size() > 1: | ||
| rank_postfix = f"_rank{MPI.COMM_WORLD.Get_rank()}" | ||
|
|
||
| xr.DataTree.from_dict(datatree).to_netcdf( | ||
| f"{path}{type(self).__name__}{rank_postfix}.nc4" | ||
| ) | ||
|
|
||
| def from_netcdf(self, path: str): | ||
|
FlorianDeconinck marked this conversation as resolved.
Outdated
|
||
| datatree = xr.open_datatree(path) | ||
| datatree_as_dict = datatree.to_dict() | ||
|
|
||
| # All other cases - recursing downward | ||
| def _load_recursive(data_tree_as_dict: dict[str, xr.Dataset] | xr.Dataset): | ||
| local_data_dict = {} | ||
| for name, data_array in data_tree_as_dict.items(): | ||
| # Case of the top_level "/" | ||
| if name == "/": | ||
| for root_name, root_data_array in datatree_as_dict["/"].items(): | ||
| local_data_dict[root_name] = root_data_array.to_numpy() | ||
| else: | ||
| # Get the leading `/` out | ||
| if isinstance(data_array, xr.Dataset): | ||
| local_data_dict[name[1:]] = _load_recursive(data_array) | ||
| else: | ||
| local_data_dict[name] = data_array.to_numpy() | ||
|
|
||
| return local_data_dict | ||
|
|
||
| data_as_numpy_dict = _load_recursive(datatree_as_dict) | ||
|
|
||
| self.init_from_memory(data_as_numpy_dict) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import dataclasses | ||
|
|
||
| import numpy as np | ||
|
|
||
| from ndsl import Quantity, State | ||
| from ndsl.boilerplate import get_factories_single_tile | ||
| from ndsl.constants import X_DIM, Y_DIM, Z_DIM, Float | ||
|
|
||
|
|
||
| @dataclasses.dataclass | ||
| class CodeState(State): | ||
| @dataclasses.dataclass | ||
| class InnerA: | ||
| A: Quantity = dataclasses.field( | ||
| metadata={ | ||
| "name": "A", | ||
| "dims": [X_DIM, Y_DIM, Z_DIM], | ||
| "units": "kg kg-1", | ||
| "intent": "?", | ||
| "dtype": Float, | ||
| } | ||
| ) | ||
|
|
||
| @dataclasses.dataclass | ||
| class InnerB: | ||
| B: Quantity = dataclasses.field( | ||
| metadata={ | ||
| "name": "B", | ||
| "dims": [X_DIM, Y_DIM, Z_DIM], | ||
| "units": "1", | ||
| "intent": "?", | ||
| "dtype": Float, | ||
| } | ||
| ) | ||
|
|
||
| inner_A: InnerA | ||
| inner_B: InnerB | ||
| C: Quantity = dataclasses.field( | ||
| metadata={ | ||
| "name": "C", | ||
| "dims": [X_DIM, Y_DIM, Z_DIM], | ||
| "units": "kg kg-1", | ||
| "intent": "?", | ||
| "dtype": Float, | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def test_state(): | ||
| _, qty_factry = get_factories_single_tile(5, 5, 3, 0, backend="dace:cpu_kfirst") | ||
|
FlorianDeconinck marked this conversation as resolved.
Outdated
|
||
|
|
||
| microphys_state = CodeState.zeros(qty_factry) | ||
| microphys_state.inner_A.A.field[:] = 42.42 | ||
| microphys_state.to_netcdf() | ||
|
FlorianDeconinck marked this conversation as resolved.
|
||
| microphys_state2 = CodeState.zeros(qty_factry) | ||
| microphys_state2.from_netcdf("CodeState.nc4") | ||
| assert (microphys_state2.inner_A.A.field[:] == 42.42).all() | ||
| a = np.ones((5, 5, 3)) | ||
| b = np.ones((5, 5, 3)) | ||
| c = np.ones((5, 5, 3)) | ||
| b[:] = 23.23 | ||
| microphys_state2.init_zero_copy( | ||
| {"inner_A": {"A": a}, "inner_B": {"B": b}, "C": c}, | ||
| check=False, | ||
| ) | ||
| assert (microphys_state2.inner_A.A.field[:] == 1.0).all() | ||
| assert (microphys_state2.inner_B.B.field[:] == 23.23).all() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.