Download model/legacy/forcings.py from OneScience-Group/NeuralGCM: direct link, hf CLI and curl.
- Browser
- Download file 9.62 kB
-
https://huggingface.co/OneScience-Group/NeuralGCM/resolve/main/model/legacy/forcings.py
- Command line
-
hf download hf://OneScience-Group/NeuralGCM/model/legacy/forcings.py
-
curl -L -o forcings.py https://huggingface.co/OneScience-Group/NeuralGCM/resolve/main/model/legacy/forcings.py
9.62 kB
| # Copyright 2024 Google LLC | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # https://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """Defines `forcing` modules that produce time-dependent focing values.""" | |
| from __future__ import annotations | |
| import functools | |
| import logging | |
| from typing import Any, Optional, Union | |
| from dinosaur import coordinate_systems | |
| from dinosaur import scales | |
| from dinosaur import typing | |
| import gin | |
| import haiku as hk | |
| import jax | |
| import jax.numpy as jnp | |
| from model.legacy import transforms | |
| import numpy as np | |
| tree_leaves = jax.tree_util.tree_leaves | |
| tree_map = jax.tree_util.tree_map | |
| units = scales.units | |
| Pytree = typing.Pytree | |
| ForcingData = typing.ForcingData | |
| ForcingFn = typing.ForcingFn | |
| Forcing = typing.Forcing | |
| TransformModule = typing.TransformModule | |
| Quantity = units.Quantity | |
| QuantityOrStr = Union[str, scales.Quantity] | |
| # _FORCING_ERRORS global will store errors obtained during a io_callback. | |
| # The user can periodically call _check_errors to see if errors have accumulated | |
| # TODO(langmore) Use a more universal mechanism (not just in forcings.py) to | |
| # handle errors, if we like this, then make public. | |
| _FORCING_ERRORS = [] | |
| # pylint: disable=logging-fstring-interpolation | |
| class ForcingDataError(Exception): | |
| """To raise when an error is encountered with forcing data.""" | |
| class NoForcing(hk.Module): | |
| """Module that returns an empty Forcing object.""" | |
| def __init__( | |
| self, | |
| coords: coordinate_systems.CoordinateSystem, | |
| dt: float, | |
| physics_specs: Any, | |
| aux_features: typing.AuxFeatures, | |
| time_axis: int = 0, | |
| name: Optional[str] = None, | |
| ): | |
| super().__init__(name=name) | |
| del coords, dt, physics_specs, aux_features, time_axis | |
| def __call__( | |
| self, | |
| forcing_data: ForcingData, | |
| sim_time: float, | |
| ) -> Forcing: | |
| """Returns forcings at the specified sim_time.""" | |
| del forcing_data, sim_time | |
| return {} | |
| class DynamicDataForcing(hk.Module): | |
| """Modules that returns forcing values by querying time-varying data. | |
| Input to __call__ `sim_time` must match a value in forcing_data['sim_time'] | |
| within dt_tolerance, or else it returns nan for all pytree values. | |
| """ | |
| def __init__( | |
| self, | |
| coords: coordinate_systems.CoordinateSystem, | |
| dt: float, | |
| physics_specs: Any, | |
| aux_features: typing.AuxFeatures, | |
| inputs_to_units_mapping: dict[str, str], | |
| forcing_transform: TransformModule = transforms.IdentityTransform, | |
| time_axis: int = 0, | |
| data_time_step: float | QuantityOrStr | None = None, | |
| dt_tolerance: Union[float, QuantityOrStr] = '1 hour', | |
| # TODO(langmore) Remove checking once bug arising from http://cl/624039690 | |
| # is fixed. | |
| check_sim_time_errors: bool = False, | |
| name: Optional[str] = None, | |
| ): | |
| logging.info(f'[NGCM] Initializing DynamicDataForcing with {dt_tolerance=}') | |
| # TODO(shoyer): remove data_time_step entirely, once we're sure that no | |
| # saved checkpoints that we care about will break. | |
| del data_time_step # no longer used | |
| super().__init__(name=name) | |
| self.time_axis = time_axis | |
| self.nondim_transform_fn = transforms.NondimensionalizeTransform( | |
| coords, | |
| dt, | |
| physics_specs, | |
| aux_features, | |
| input_coords=None, | |
| inputs_to_units_mapping=inputs_to_units_mapping, | |
| ) | |
| self.forcing_transform_fn = forcing_transform( | |
| coords, dt, physics_specs, aux_features | |
| ) | |
| if isinstance(dt_tolerance, (str, scales.Quantity)): # pyrefly: ignore[invalid-argument] | |
| dt_tolerance = physics_specs.nondimensionalize( | |
| scales.Quantity(dt_tolerance) | |
| ) | |
| self.dt_tolerance = dt_tolerance | |
| self._check_sim_time_errors = check_sim_time_errors | |
| def __call__( | |
| self, | |
| forcing_data: ForcingData, | |
| sim_time: float, | |
| ) -> Forcing: | |
| """Returns forcings at the specified sim_time.""" | |
| forcing_data = self.nondim_transform_fn(forcing_data) | |
| times = forcing_data['sim_time'] | |
| approx_index = jnp.interp(sim_time, times, jnp.arange(times.size)) | |
| index = jnp.round(approx_index).astype('int32') | |
| # Slice leaf values by index | |
| field_index_fn = functools.partial( | |
| jax.lax.dynamic_index_in_dim, | |
| index=index, | |
| axis=self.time_axis, | |
| keepdims=False, | |
| ) | |
| _assert_no_scalars(forcing_data) | |
| forcing = tree_map(field_index_fn, forcing_data) | |
| # Replace leaf values with nan if forcing['sim_time'] does not match | |
| # the requested sim_time value within dt_tolerance. | |
| abs_error = jnp.abs(forcing['sim_time'] - sim_time) | |
| is_valid = abs_error < self.dt_tolerance | |
| forcing = jax.tree_util.tree_map( | |
| lambda x: jnp.where(is_valid, x, jnp.nan), forcing | |
| ) | |
| # Also add errors (if any) to _FORCING_ERRORS so _check_errors can be called | |
| # to raise. | |
| if self._check_sim_time_errors: | |
| jax.experimental.io_callback( | |
| _check_sim_time_close_to_forcing_sim_time, | |
| None, # Returns None | |
| sim_time=sim_time, | |
| forcing_sim_time=forcing['sim_time'], | |
| tolerance=self.dt_tolerance, | |
| ) | |
| return self.forcing_transform_fn(forcing) | |
| class PersistenceDataForcing(hk.Module): | |
| """Modules that returns forcing using first time index of forcing_data.""" | |
| def __init__( | |
| self, | |
| coords: coordinate_systems.CoordinateSystem, | |
| dt: float, | |
| physics_specs: Any, | |
| aux_features: typing.AuxFeatures, | |
| inputs_to_units_mapping: dict[str, str], | |
| forcing_transform: TransformModule = transforms.IdentityTransform, | |
| time_axis: int = 0, | |
| name: Optional[str] = None, | |
| ): | |
| logging.info('[NGCM] Initializing PersistenceDataForcing') | |
| super().__init__(name=name) | |
| self.time_axis = time_axis | |
| self.nondim_transform_fn = transforms.NondimensionalizeTransform( | |
| coords, | |
| dt, | |
| physics_specs, | |
| aux_features, | |
| input_coords=None, | |
| inputs_to_units_mapping=inputs_to_units_mapping, | |
| ) | |
| self.forcing_transform_fn = forcing_transform( | |
| coords, dt, physics_specs, aux_features | |
| ) | |
| def __call__( | |
| self, | |
| forcing_data: ForcingData, | |
| sim_time: float, | |
| ) -> Forcing: | |
| """Returns forcings from the first time index of sim_time.""" | |
| del sim_time # unused | |
| forcing_data = self.nondim_transform_fn(forcing_data) | |
| idx = 0 | |
| # Slice leaf values by index | |
| field_index_fn = functools.partial( | |
| jax.lax.dynamic_index_in_dim, | |
| index=idx, | |
| axis=self.time_axis, | |
| keepdims=False, | |
| ) | |
| _assert_no_scalars(forcing_data) | |
| forcing = tree_map(field_index_fn, forcing_data) | |
| return self.forcing_transform_fn(forcing) | |
| class IncrementSSTForcingTransform(hk.Module): | |
| """Transform Forcing by uniformly incrementing sea surface temperature.""" | |
| def __init__( | |
| self, | |
| coords: coordinate_systems.CoordinateSystem, | |
| dt: float, | |
| physics_specs: Any, | |
| aux_features: typing.AuxFeatures, | |
| temperature_change: Quantity, | |
| key: str = 'sea_surface_temperature', | |
| name: Optional[str] = None, | |
| ): | |
| super().__init__(name=name) | |
| del coords, dt, aux_features # unused | |
| self.temperature_change = physics_specs.nondimensionalize( | |
| units.Quantity(temperature_change) # pyrefly: ignore[not-callable] | |
| ) | |
| self.key = key | |
| def __call__(self, forcing: Forcing) -> Forcing: | |
| assert isinstance(forcing, dict) | |
| forcing = forcing.copy() | |
| forcing[self.key] = forcing[self.key] + self.temperature_change | |
| return forcing | |
| def _assert_no_scalars(tree: Pytree): | |
| dims = tree_map(lambda x: len(jnp.shape(x)), tree) | |
| if not all(d > 0 for d in tree_leaves(dims)): | |
| raise ValueError(f'Scalar shapes encountered: {dims=}') | |
| # TODO(langmore) Use a more universal mechanism (not just in forcings.py) to | |
| # handle errors, if we like this, then make public. | |
| def _check_sim_time_close_to_forcing_sim_time( | |
| sim_time: np.ndarray, | |
| forcing_sim_time: np.ndarray, | |
| tolerance: float, | |
| ) -> None: | |
| """Checks |sim_time - forcing_sim_time| < tolerance add to _FORCING_ERRORS.""" | |
| abs_error = np.abs(forcing_sim_time - sim_time) | |
| if abs_error < tolerance: | |
| return | |
| err_msg = ( | |
| f'{sim_time=} differed from {forcing_sim_time=} by {abs_error=} which is ' | |
| f'> {tolerance=}' | |
| ) | |
| _FORCING_ERRORS.append(err_msg) | |
| # TODO(langmore) Use a more universal mechanism (not just in forcings.py) to | |
| # handle errors, if we like this, then make public. | |
| def _check_errors( # pylint: disable=dangerous-default-value | |
| max_to_print: int = 4, | |
| err_list: list[str] = _FORCING_ERRORS, | |
| ) -> None: | |
| """Check err_list and raise ForcingDataError if nonempty.""" | |
| n_err = len(err_list) | |
| if n_err: | |
| raise ForcingDataError( | |
| f'ForcingDataError found: {n_err} exceptions: ' | |
| f'The first {min(n_err, max_to_print)} are: ' | |
| f'{", ".join(err_list[:max_to_print])}' | |
| ) | |