NeuralGCM / model /legacy /steps.py
yzt15806542928's picture
Upload folder using huggingface_hub
f4a39ee verified
Raw
History Blame Contribute Delete
11.7 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.
"""Modules that parameterize composed time-steppers."""
import abc
import functools
from typing import Any, Callable, Optional, Sequence
from dinosaur import coordinate_systems
from dinosaur import primitive_equations
from dinosaur import time_integration
from dinosaur import typing
import gin
import haiku as hk
from model.legacy import diagnostics
from model.legacy import integrators
from model.legacy import perturbations
from model.legacy import stochastic
DiagnosticModule = diagnostics.DiagnosticModule
Forcing = typing.Forcing
Pytree = typing.Pytree
ModelState = typing.ModelState
EquationModule = Callable[..., time_integration.ImplicitExplicitODE]
CorrectorModule = typing.CorrectorModule
PerturbationModule = perturbations.PerturbationModule
RandomnessModule = stochastic.RandomnessModule
PyTreeStepFilterModule = typing.PyTreeStepFilterModule
TimeIntegrator = integrators.TimeIntegrator
TransformModule = typing.TransformModule
class BaseStep(abc.ABC):
"""Base class for Step modules."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
diagnostics_module: DiagnosticModule = diagnostics.NoDiagnostics,
randomness_module: RandomnessModule = stochastic.NoRandomField,
):
self.diagnostics_fn = diagnostics_module(
coords, dt, physics_specs, aux_features)
self.randomness_fn = randomness_module(
coords, dt, physics_specs, aux_features)
@abc.abstractmethod
def __call__(
self,
state: ModelState,
forcing: typing.Forcing,
) -> ModelState:
"""Computes the state of the system evolved in time by `self.dt`."""
def finalize_state(
self,
x: ModelState,
forcing: typing.Forcing,
) -> ModelState:
"""Finalizes initialization of a model state `x`, encoded from data.
This method ensures that state has all of the `ModelState` fields
initialized in a way compatible with this step function. This includes
populating initial `diagnostics`, `memory` and `randomness` fields.
Args:
x: Initial values for the model state typically provided by the encoder.
forcing: Data covariates from the same time slice as `x`.
Returns:
Initialized model state.
"""
x.randomness = self.randomness_fn.unconditional_sample(
hk.maybe_next_rng_key()
)
x.diagnostics = self.diagnostics_fn(
x, physics_tendencies=None, forcing=forcing)
return x
@gin.register
class EquationStep(BaseStep, hk.Module):
"""Step module that advances the state by integrating an equation in time."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
equation_module: EquationModule,
time_integrator: TimeIntegrator = integrators.imex_rk_sil3,
filter_modules: Sequence[PyTreeStepFilterModule] = tuple(),
checkpoint_explicit_terms: bool = True,
name: Optional[str] = None,
):
hk.Module.__init__(self, name=name)
BaseStep.__init__(self, coords, dt, physics_specs, aux_features)
equation = equation_module(coords, dt, physics_specs, aux_features)
if checkpoint_explicit_terms:
equation = time_integration.ImplicitExplicitODE.from_functions(
hk.remat(equation.explicit_terms),
equation.implicit_terms,
equation.implicit_inverse) # pyrefly: ignore[bad-argument-type]
step_fn = time_integrator(equation, dt)
filter_fns = [
module(coords, dt, physics_specs, aux_features)
for module in filter_modules]
self.dt = dt
self.step_fn = time_integration.step_with_filters(step_fn, filter_fns)
def __call__(
self,
x: ModelState,
forcing: Optional[typing.Forcing] = None,
) -> ModelState:
"""Computes the state of the system evolved in time by `dt`."""
del forcing
next_state = time_integration.maybe_fix_sim_time_roundoff(
self.step_fn(x.state), self.dt)
return ModelState(next_state) # pyrefly: ignore[bad-argument-count]
@gin.register
class RepeatedStep(BaseStep, hk.Module):
"""Step module that consists of repeated substeps of the same form."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
inner_step_module: typing.StepModule,
num_inner_steps: int = 1,
name: Optional[str] = None,
):
hk.Module.__init__(self, name=name)
BaseStep.__init__(self, coords, dt, physics_specs, aux_features)
inner_dt = dt / num_inner_steps
self.step_fn = inner_step_module(
coords, inner_dt, physics_specs, aux_features)
self.num_inner_steps = num_inner_steps
def __call__(
self,
state: ModelState,
forcing: typing.Forcing,
) -> ModelState:
"""Computes the state of the system evolved in time by `dt`."""
step_fn = functools.partial(self.step_fn, forcing=forcing)
step_fn = time_integration.repeated(step_fn, self.num_inner_steps, hk.scan)
return step_fn(state)
@gin.register
class CustomCoordsStep(BaseStep, hk.Module):
"""Step module that uses gin-configured coordinates instead of coords.
This class currently supports model states in spectral representation. It
could be easily extended to nodal-state models by converting to modal space
prior to spectral interpolation and back after the timestep if performed.
"""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
step_module: typing.StepModule,
custom_coords: coordinate_systems.CoordinateSystem = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
name: Optional[str] = None,
):
hk.Module.__init__(self, name=name)
BaseStep.__init__(self, coords, dt, physics_specs, aux_features)
self.step_fn = step_module(
custom_coords, dt, physics_specs, aux_features)
self.to_custom_coords_fn = coordinate_systems.get_spectral_interpolate_fn(
coords, custom_coords)
self.from_custom_coords_fn = coordinate_systems.get_spectral_interpolate_fn(
custom_coords, coords)
def __call__(
self,
x: typing.PyTreeState,
forcing: typing.Forcing,
) -> typing.PyTreeState:
del forcing # currently not supported.
x = self.to_custom_coords_fn(x)
custom_out = self.step_fn(x, None)
return self.from_custom_coords_fn(custom_out)
@gin.register
class StochasticPhysicsParameterizationStep(BaseStep, hk.Module):
"""Step module that uses stochastic physics tendencies with dycore."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
corrector_module: CorrectorModule,
physics_parameterization_module: typing.ParameterizationModule,
num_substeps: int = 1,
diagnostics_module: DiagnosticModule = diagnostics.NoDiagnostics,
randomness_module: RandomnessModule = stochastic.ZerosRandomField,
perturbation_module: PerturbationModule = perturbations.NoPerturbation,
checkpoint_substep: bool = False,
name: Optional[str] = None,
):
hk.Module.__init__(self, name=name)
BaseStep.__init__(
self, coords, dt, physics_specs, aux_features,
diagnostics_module=diagnostics_module,
randomness_module=randomness_module)
inner_dt = dt / num_substeps
self.num_substeps = num_substeps
self.corrector_fn = corrector_module(
coords, inner_dt, physics_specs, aux_features)
self.physics_parameterization_fn = physics_parameterization_module(
coords, inner_dt, physics_specs, aux_features)
self.perturbation_fn = perturbation_module(
coords, inner_dt, physics_specs, aux_features)
self.checkpoint_substep = checkpoint_substep
self.coords = coords
def finalize_state(
self,
x: ModelState,
forcing: typing.Forcing,
) -> ModelState:
"""Finalizes initialization of a model state `x`, encoded from data.
This method ensures that state has all of the `ModelState` fields
initialized in a way compatible with this step function. This includes
populating initial `diagnostics`, `memory` and `randomness` fields.
This is called by StochasticModularStepModel.encode, after encoding the data
Args:
x: Initial values for the model state typically provided by the encoder.
forcing: Data covariates from the same time slice as `x`.
Returns:
Initialized model state.
"""
# TODO(dkochkov) Consider adding an option of not overriding randomness.
x.randomness = self.randomness_fn.unconditional_sample(
hk.maybe_next_rng_key()
)
pp_tendency = self.physics_parameterization_fn(
x.state, x.memory, x.diagnostics, x.randomness.nodal_value, forcing
)
x.diagnostics = self.diagnostics_fn(x, pp_tendency, forcing)
return x
def __call__(
self,
state: ModelState,
forcing: typing.Forcing,
) -> ModelState:
"""Computes the state of the system evolved in time by `dt`."""
def step_fn(x):
x = self.coords.with_dycore_sharding(x)
# TODO(dkochkov) Consider passing `x` to physics_parameterization.
pp_tendency = self.physics_parameterization_fn(
x.state, x.memory, x.diagnostics, x.randomness.nodal_value, forcing
)
pp_tendency = self.perturbation_fn(
pp_tendency,
state=x.state,
randomness=x.randomness.nodal_value,
)
next_state = self.corrector_fn(x.state, pp_tendency, forcing)
# TODO(dkochkov) update stochastic modules to take optional state.
next_randomness = self.randomness_fn.advance(x.randomness)
next_memory = x.state if x.memory is not None else None
next_diagnostics = self.diagnostics_fn(x, pp_tendency, forcing)
x_next = ModelState(
state=next_state, memory=next_memory, diagnostics=next_diagnostics, # pyrefly: ignore[unexpected-keyword]
randomness=next_randomness) # pyrefly: ignore[unexpected-keyword]
x_next = self.coords.with_dycore_sharding(x_next)
return x_next
if self.checkpoint_substep:
step_fn = hk.remat(step_fn)
step_fn = time_integration.repeated(step_fn, self.num_substeps, hk.scan)
return step_fn(state)
# TODO(dkochkov) Move vertical advection step to transforms.py.
@gin.register
class SemiLagrangianVerticalAdvectionStep(hk.Module):
"""Step module that applies vertical advection for the primitive equations."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
name: Optional[str] = None,
):
super().__init__(name=name)
self.coords = coords
self.dt = dt
def __call__(self, state):
return primitive_equations.semi_lagrangian_vertical_advection_step(
state, self.coords, self.dt
)