Download model/legacy/model_utils.py from OneScience-Group/NeuralGCM: direct link, hf CLI and curl.
- Browser
- Download file 20.3 kB
-
https://huggingface.co/OneScience-Group/NeuralGCM/resolve/main/model/legacy/model_utils.py
- Command line
-
hf download hf://OneScience-Group/NeuralGCM/model/legacy/model_utils.py
-
curl -L -o model_utils.py https://huggingface.co/OneScience-Group/NeuralGCM/resolve/main/model/legacy/model_utils.py
20.3 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. | |
| """Helper methods for constructing augmented trajectory functions.""" | |
| import dataclasses | |
| import functools | |
| from typing import Any, Callable, Sequence, Tuple | |
| from dinosaur import coordinate_systems | |
| from dinosaur import pytree_utils | |
| from dinosaur import typing | |
| import haiku as hk | |
| import jax | |
| import jax.numpy as jnp | |
| Array = typing.Array | |
| DynamicalSystem = Any # to prevent circular dependency on model_builder | |
| Pytree = typing.Pytree | |
| tree_map = jax.tree_util.tree_map | |
| tree_leaves = jax.tree_util.tree_leaves | |
| # Linter confused by wrapped functions | |
| # pylint: disable=g-bare-generic | |
| def with_preprocessing( | |
| fn: Callable[..., Pytree], | |
| preprocess_fn: Callable, | |
| ) -> Callable[..., Pytree]: | |
| """Generates a function that computes `fn` on `preprocess_fn(x)`.""" | |
| def apply_fn(x, *args, **kwargs): | |
| return fn(preprocess_fn(x), *args, **kwargs) | |
| return apply_fn | |
| def with_post_processing( | |
| fn: Callable[..., Pytree], | |
| post_process_fn: Callable, | |
| ) -> Callable[..., Pytree]: | |
| """Generates a function that applies `post_process_fn` to outputs of `fn`.""" | |
| def apply_fn(*args, **kwargs): | |
| return post_process_fn(*fn(*args, **kwargs)) | |
| return apply_fn | |
| def with_forcing( | |
| fn: Callable[..., Pytree], | |
| forcing_fn: typing.ForcingFn, | |
| forcing_data: typing.ForcingData, | |
| ) -> Callable[..., Pytree]: | |
| """Converts fn(x, forcing) to fn(x).""" | |
| # evaluates forcing=forcing_fn(forcing_data, x.sim_time) | |
| # when x does not have sim_time, forcing_fn will get sim_time=None | |
| def wrapped(x, forcing_fn=forcing_fn): | |
| # handle dataclass or dict for state data | |
| if dataclasses.is_dataclass(x): | |
| if isinstance(x, typing.ModelState): | |
| sim_time = dataclasses.asdict(x.state).get('sim_time', None) | |
| else: | |
| sim_time = dataclasses.asdict(x).get('sim_time', None) | |
| else: | |
| sim_time = x.get('sim_time', None) | |
| # handle sim_time of ndim 0 or 1 | |
| if sim_time is not None: | |
| sim_time = jax.numpy.asarray(sim_time) | |
| if sim_time.ndim: | |
| forcing_fn = jax.vmap(forcing_fn, in_axes=(None, 0)) | |
| forcing = forcing_fn(forcing_data, sim_time) # pyrefly: ignore[bad-argument-type] | |
| return fn(x, forcing=forcing) | |
| return wrapped | |
| def with_split_input( | |
| fn: Callable[..., Pytree], | |
| split_index: int, | |
| time_axis: int = 0, | |
| ) -> Callable[..., Pytree]: | |
| """Decorates `fn` to be evaluated on first `split_index` time slices. | |
| The returned function is a generalization to pytrees of the function: | |
| `fn(x[:split_index], *args, **kwargs)` | |
| Args: | |
| fn: function to be transformed. | |
| split_index: number of input elements along the time axis to use. | |
| time_axis: axis corresponding to time dimension in `x` to decorated `fn`. | |
| Returns: | |
| decorated `fn` that is evaluated on only `split_index` first time slices of | |
| provided inputs. | |
| """ | |
| def apply_fn(x, *args, **kwargs): | |
| init, _ = pytree_utils.split_along_axis(x, split_index, axis=time_axis) | |
| return fn(init, *args, **kwargs) | |
| return apply_fn | |
| def with_input_included( | |
| trajectory_fn: typing.TrajectoryFn, | |
| time_axis: int = 0, | |
| num_last_input_frames_to_trim: int = 0, | |
| ) -> typing.TrajectoryFn: | |
| """Returns a `trajectory_fn` that concatenates inputs `x` to trajectory.""" | |
| if num_last_input_frames_to_trim > 0: | |
| num_last_input_frames_to_trim = -num_last_input_frames_to_trim | |
| else: | |
| num_last_input_frames_to_trim = None # pyrefly: ignore[bad-assignment] | |
| inputs_time_slice = slice(None, num_last_input_frames_to_trim) | |
| def _trajectory(x, *args, **kwargs): | |
| final, unroll = trajectory_fn(x, *args, **kwargs) | |
| x_concat = pytree_utils.slice_along_axis(x, time_axis, inputs_time_slice) | |
| return final, pytree_utils.concat_along_axis([x_concat, unroll], time_axis) | |
| return _trajectory | |
| def trajectory_with_inputs_and_forcing( | |
| model: DynamicalSystem, | |
| num_init_frames: int, | |
| start_with_input: bool = False, | |
| ) -> typing.TrajectoryFn: | |
| """Returns trajectory_fn that comuptes model trajectory from target data. | |
| Wraps the default model.trajectory_fn to operate on data representation. It | |
| corresponds to slicing `num_init_frames` from the inputs, encoding and | |
| unrolling the trajectory. | |
| Args: | |
| model: model of a dynamical system used to obtain the trajectory. | |
| num_init_frames: number of time frames used from the physics trajectory to | |
| initialize the model state. | |
| start_with_input: whether the firest decoded step in the output trajectory | |
| should correspond to last input time or first future output. | |
| Returns: | |
| Trajectory function that operates on target data trajectory by encoding | |
| the `initial_frames` inputs and unrolls trajectory in a model space. | |
| """ | |
| def _trajectory_fn(x, forcing_data, outer_steps, inner_steps=1): | |
| # configure the model.trajectory function with a decoder on the output. | |
| trajectory_fn = functools.partial( | |
| model.trajectory, | |
| outer_steps=outer_steps, | |
| inner_steps=inner_steps, | |
| forcing_data=forcing_data, | |
| start_with_input=start_with_input) | |
| # add preprocessing to encode input to model state. | |
| encode_fn = with_forcing(model.encode, model.forcing_fn, forcing_data) | |
| trajectory_fn = with_preprocessing(trajectory_fn, encode_fn) | |
| trajectory_fn = with_split_input(trajectory_fn, num_init_frames) | |
| return trajectory_fn(x) | |
| return _trajectory_fn | |
| def trajectory_with_inputs_and_forcing_and_stop_gradients( | |
| model: DynamicalSystem, | |
| num_init_frames: int, | |
| start_with_input: bool = False, | |
| stop_gradient_outer_steps: Sequence[int] = (), | |
| ) -> typing.TrajectoryFn: | |
| """Returns trajectory_fn that comuptes model trajectory from target data. | |
| This extension of `trajectory_with_inputs_and_forcing` allows adding stop | |
| gradients to the trajectory at designated steps. For example, if | |
| `stop_gradient_outer_steps = [2]`, then gradients along the trajectory stop | |
| at t=2. This does not mean that gradients with respect to X[2] will be zero. | |
| It simply means that, for t > 2, gradients of X[t] with respect to X[2] will | |
| be zero. | |
| Wraps the default model.trajectory_fn to operate on data representation. It | |
| corresponds to slicing `num_init_frames` from the inputs, encoding and | |
| unrolling the trajectory. | |
| Args: | |
| model: model of a dynamical system used to obtain the trajectory. | |
| num_init_frames: number of time frames used from the physics trajectory to | |
| initialize the model state. | |
| start_with_input: whether the firest decoded step in the output trajectory | |
| should correspond to last input time or first future output. | |
| stop_gradient_outer_steps: Tuple (possibly empty) indicating outer steps at | |
| which to place stop gradients. | |
| Returns: | |
| Trajectory function that operates on target data trajectory by encoding | |
| the `initial_frames` inputs and unrolls trajectory in a model space. | |
| Decoding is not done by this function. | |
| """ | |
| stop_gradient_outer_steps = list(sorted(stop_gradient_outer_steps)) | |
| if num_init_frames != 1: | |
| raise ValueError(f'{num_init_frames=} is not supported yet.') | |
| if stop_gradient_outer_steps and min(stop_gradient_outer_steps) <= 0: | |
| raise ValueError( | |
| f'{stop_gradient_outer_steps=} contained non-positive values' | |
| ) | |
| expand_dim0 = lambda tree: tree_map(lambda x_i: x_i[jnp.newaxis], tree) | |
| concat_dim0 = lambda trees: pytree_utils.concat_along_axis(trees, axis=0) | |
| slice_dim0 = lambda tree, idx: pytree_utils.slice_along_axis( | |
| tree, axis=0, idx=idx | |
| ) | |
| def concat_trajectories_with_stop_grads( | |
| x, forcing_data, outer_steps, inner_steps=1 | |
| ): | |
| if ( | |
| stop_gradient_outer_steps | |
| and max(stop_gradient_outer_steps) > outer_steps | |
| ): | |
| raise ValueError( | |
| f'{stop_gradient_outer_steps=} contained values > {outer_steps=}' | |
| ) | |
| outer_steps_seq = list(stop_gradient_outer_steps) | |
| if not outer_steps_seq or outer_steps_seq[-1] != outer_steps: | |
| outer_steps_seq.append(outer_steps) | |
| # The first leg needs to encode the input. So use | |
| # trajectory_with_inputs_and_forcing, which does the encoding. | |
| final_state, first_leg = trajectory_with_inputs_and_forcing( | |
| model, | |
| num_init_frames=num_init_frames, | |
| start_with_input=start_with_input, | |
| )( | |
| x, | |
| forcing_data=forcing_data, | |
| outer_steps=outer_steps_seq[0], | |
| inner_steps=inner_steps, | |
| ) | |
| # At this point, sections contains times [0, ..., outer_steps_seq[0]] | |
| sections = [ | |
| first_leg, | |
| ] | |
| # Subsequent legs do not need encoding, so use model.trajectory directly. | |
| trajectory_fn = functools.partial( | |
| model.trajectory, | |
| inner_steps=inner_steps, | |
| forcing_data=forcing_data, | |
| start_with_input=start_with_input, | |
| ) | |
| for i in range(1, len(outer_steps_seq)): | |
| # outer_steps_seq[-1] may or may not be in stop_gradient_outer_steps. | |
| # The other steps will be by construction. | |
| assert set(outer_steps_seq[:-1]).issubset(stop_gradient_outer_steps) | |
| stop_grad_at_start = outer_steps_seq[i - 1] in stop_gradient_outer_steps | |
| initial_state = final_state | |
| # this_leg contains times [outer_steps_seq[0]+1, ..., outer_steps_seq[1]] | |
| final_state, this_leg = trajectory_fn( | |
| jax.lax.stop_gradient(initial_state) | |
| if stop_grad_at_start | |
| else initial_state, | |
| outer_steps=outer_steps_seq[i] - outer_steps_seq[i - 1], | |
| ) | |
| if stop_grad_at_start and start_with_input: | |
| # Replace the initial point that had a stop gradient on it. | |
| this_leg = concat_dim0([ | |
| expand_dim0(initial_state), | |
| slice_dim0(this_leg, idx=slice(1, None)), | |
| ]) | |
| sections.append(this_leg) | |
| return final_state, concat_dim0(sections) | |
| return concat_trajectories_with_stop_grads | |
| def decoded_trajectory_with_forcing( | |
| model: DynamicalSystem, | |
| start_with_input: bool = False, | |
| ) -> typing.TrajectoryFn: | |
| """Returns trajectory_fn that comuptes decoded trajectory values. | |
| Args: | |
| model: model of a dynamical system used to obtain the trajectory. | |
| start_with_input: whether the firest decoded step in the output trajectory | |
| should correspond to last input time or first future output. | |
| Returns: | |
| Trajectory function that additionally decodes trajectory values. | |
| """ | |
| def _trajectory_fn(x, forcing_data, outer_steps, inner_steps=1): | |
| # configure the model.trajectory function with a decoder on the output. | |
| trajectory_fn = functools.partial( | |
| model.trajectory, | |
| forcing_data=forcing_data, | |
| post_process_fn=with_forcing(model.decode, | |
| model.forcing_fn, forcing_data), | |
| start_with_input=start_with_input) | |
| return trajectory_fn(x, outer_steps, inner_steps) | |
| return _trajectory_fn | |
| def decoded_trajectory_with_inputs_and_forcing( | |
| model: DynamicalSystem, | |
| num_init_frames: int, | |
| start_with_input: bool = False, | |
| ) -> typing.TrajectoryFn: | |
| """Returns trajectory_fn operating on decoded input and forcing data. | |
| The returned function uses `num_init_frames` of the physics space trajectory | |
| provided as an input to model.encode_fn to initialize the model state, then | |
| unrolls the trajectory of specified length that is decoded to the physics | |
| space using `model.decode_fn`. | |
| Args: | |
| model: model of a dynamical system used to obtain the trajectory. | |
| num_init_frames: number of time frames used from the physics trajectory to | |
| initialize the model state. | |
| start_with_input: whether the firest decoded step in the output trajectory | |
| should correspond to last input time or first future output. | |
| Returns: | |
| Trajectory function that operates on physics space trajectories | |
| and returns unrolls in physics space. | |
| """ | |
| def _trajectory_fn(x, forcing_data, outer_steps, inner_steps=1): | |
| # configure the model.trajectory function with a decoder on the output. | |
| trajectory_fn = decoded_trajectory_with_forcing(model, start_with_input) | |
| trajectory_fn = functools.partial( | |
| trajectory_fn, | |
| forcing_data=forcing_data, | |
| outer_steps=outer_steps, | |
| inner_steps=inner_steps) | |
| # add preprocessing to encode input to model state. | |
| trajectory_fn = with_preprocessing( | |
| trajectory_fn, with_forcing(model.encode, | |
| model.forcing_fn, forcing_data)) | |
| # concatenate input trajectory to output trajectory for easier comparison. | |
| trajectory_fn = with_input_included( | |
| trajectory_fn, num_last_input_frames_to_trim=int(start_with_input)) | |
| # make trajectories operate on full examples by splitting the init. | |
| trajectory_fn = with_split_input(trajectory_fn, num_init_frames) | |
| return trajectory_fn(x) | |
| return _trajectory_fn | |
| def process_trajectory( | |
| input_trajectory: Pytree, | |
| process_fn: Callable[[Pytree], Pytree], | |
| ) -> Pytree: | |
| """Processes trajectory by applying `process_fn` along time axis.""" | |
| step_fn = lambda c, x: tuple([None, hk.remat(process_fn)(x)]) | |
| _, out = hk.scan(step_fn, None, xs=input_trajectory) | |
| return out | |
| def _maybe_to_nodal_with_physics_sharding(x, /, coords): | |
| x = coordinate_systems.maybe_to_nodal(x, coords) | |
| x = coords.with_physics_sharding(x) | |
| return x | |
| def _maybe_to_modal_with_physics_sharding(x, /, coords): | |
| x = coordinate_systems.maybe_to_modal(x, coords) | |
| x = coords.with_physics_sharding(x) | |
| return x | |
| def compute_prediction_representations( | |
| predicted_trajectory: typing.Pytree, | |
| forcing_data: typing.ForcingData, | |
| model: DynamicalSystem, | |
| ) -> typing.TrajectoryRepresentations: | |
| """Computes TrajectoryRepresentations for predicted trajectory. | |
| Args: | |
| predicted_trajectory: predictions on `model.coords` coordinates. | |
| forcing_data: forcing data to be used for encode/decode transformations. | |
| model: model used for conversion between representations. | |
| Returns: | |
| `TrajectoryRepresentations` for predictions. | |
| """ | |
| decode_fn = with_forcing(model.decode, model.forcing_fn, forcing_data) | |
| data_to_nodal = functools.partial( | |
| _maybe_to_nodal_with_physics_sharding, coords=model.output_coords) | |
| data_to_modal = functools.partial( | |
| _maybe_to_modal_with_physics_sharding, coords=model.output_coords) | |
| model_to_nodal = functools.partial( | |
| _maybe_to_nodal_with_physics_sharding, coords=model.coords) | |
| model_to_modal = functools.partial( | |
| _maybe_to_modal_with_physics_sharding, coords=model.coords) | |
| predicted_data_trajectory = process_trajectory( | |
| predicted_trajectory, decode_fn) | |
| # Note: we pass original prediction to the decoder, but use dict for outputs. | |
| if isinstance(predicted_trajectory, typing.ModelState): | |
| predicted_trajectory = predicted_trajectory.state | |
| if dataclasses.is_dataclass(predicted_trajectory): | |
| # Losses operate on dicts: convert struct to dict if needed. | |
| predicted_trajectory = predicted_trajectory.asdict() | |
| return typing.TrajectoryRepresentations( | |
| data_nodal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword] | |
| predicted_data_trajectory, data_to_nodal), | |
| data_modal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword] | |
| predicted_data_trajectory, data_to_modal), | |
| model_nodal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword] | |
| predicted_trajectory, model_to_nodal), | |
| model_modal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword] | |
| predicted_trajectory, model_to_modal), | |
| ) | |
| def compute_target_representations( | |
| target_trajectory: typing.Pytree, | |
| forcing_data: typing.ForcingData, | |
| model: DynamicalSystem, | |
| ) -> typing.TrajectoryRepresentations: | |
| """Computes TrajectoryRepresentations for target trajectory. | |
| Note: currently this method only supports models that use a single time slice | |
| for initialization. | |
| Args: | |
| target_trajectory: target trajectory on `model.output_coords` coordinates. | |
| forcing_data: forcing data to be used for encode/decode transformations. | |
| model: model used for conversion between representations. | |
| Returns: | |
| `TrajectoryRepresentations` for predictions. | |
| """ | |
| encode_slice_fn = with_forcing(model.encode, model.forcing_fn, forcing_data) | |
| encode_fn = lambda tree: encode_slice_fn( # pylint: disable=g-long-lambda. | |
| jax.tree_util.tree_map(lambda x: jnp.expand_dims(x, 0), tree)) | |
| data_to_nodal = functools.partial( | |
| _maybe_to_nodal_with_physics_sharding, coords=model.output_coords) | |
| data_to_modal = functools.partial( | |
| _maybe_to_modal_with_physics_sharding, coords=model.output_coords) | |
| model_to_nodal = functools.partial( | |
| _maybe_to_nodal_with_physics_sharding, coords=model.coords) | |
| model_to_modal = functools.partial( | |
| _maybe_to_modal_with_physics_sharding, coords=model.coords) | |
| target_model_trajectory = process_trajectory( | |
| target_trajectory, encode_fn) | |
| if isinstance(target_model_trajectory, typing.ModelState): | |
| target_model_trajectory = target_model_trajectory.state | |
| if dataclasses.is_dataclass(target_model_trajectory): | |
| # Losses operate on dicts: convert struct to dict if needed. | |
| target_model_trajectory = target_model_trajectory.asdict() | |
| return typing.TrajectoryRepresentations( | |
| data_nodal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword] | |
| target_trajectory, data_to_nodal), | |
| data_modal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword] | |
| target_trajectory, data_to_modal), | |
| model_nodal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword] | |
| target_model_trajectory, model_to_nodal), | |
| model_modal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword] | |
| target_model_trajectory, model_to_modal), | |
| ) | |
| def compute_prediction_and_target_representations( | |
| predicted_model_trajectory: typing.Pytree, | |
| target_data_trajectory: typing.Pytree, | |
| forcing_data: typing.ForcingData, | |
| model: DynamicalSystem, | |
| ) -> Tuple[typing.TrajectoryRepresentations, typing.TrajectoryRepresentations]: | |
| """Computes TrajectoryRepresentations for predicted and target trajectories. | |
| Note: currently this method only supports models that use a single time slice | |
| for initialization. While computing all terms seems wasteful, once jit-ed | |
| all unused computations are optimized away. It is also tempting to compute | |
| all representations at once, but as of 2023-02-28 compiler doesn't manage to | |
| remove unused computation from a single primitive. | |
| Args: | |
| predicted_model_trajectory: predictions on `model.coords` coordinates. | |
| target_data_trajectory: target data on `model.output_coords` coordinates. | |
| forcing_data: forcing data to be used for encode/decode transformations. | |
| model: model used for conversion between representations. | |
| Returns: | |
| Tuple of `TrajectoryRepresentations` for predictions and targets. | |
| """ | |
| prediction_representations = compute_prediction_representations( | |
| predicted_model_trajectory, forcing_data, model) | |
| target_representations = compute_target_representations( | |
| target_data_trajectory, forcing_data, model) | |
| return prediction_representations, target_representations | |
| def safe_sqrt(x: Array) -> jax.Array: | |
| """Sqrt(x) with gradient = 0 for x near 0.""" | |
| return jnp.sqrt(x) | |
| def safe_sqrt_jvp( | |
| primals: Array, | |
| tangents: Array, | |
| ) -> tuple[jax.Array, jax.Array]: | |
| (x,) = primals | |
| (x_dot,) = tangents | |
| primal_out = safe_sqrt(x) | |
| eps = jnp.finfo(x.dtype).eps | |
| safe_x = jnp.where(x > eps, x, 1.0) | |
| tangent_out = jnp.where(x > eps, x_dot / (2 * safe_sqrt(safe_x)), 0) | |
| return primal_out, tangent_out | |