NeuralGCM / model /legacy /transforms.py
yzt15806542928's picture
Upload folder using huggingface_hub
f4a39ee verified
Raw
History Blame Contribute Delete
25.8 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.
"""Transformation modules that convert or pre/post process data structures."""
import dataclasses
import functools
import re
from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple
from dinosaur import coordinate_systems
from dinosaur import pytree_utils
from dinosaur import sigma_coordinates
from dinosaur import typing
import gin
import haiku as hk
import jax
import jax.numpy as jnp
from model.legacy import filters
import numpy as np
KeyWithCosLatFactor = typing.KeyWithCosLatFactor
TransformModule = typing.TransformModule
@gin.register
class EmptyTransform(hk.Module):
"""Transform returns an empty dict."""
def __init__(self, *args, name: Optional[str] = None):
del args # unused.
super().__init__(name=name)
def __call__(self, inputs) -> typing.Pytree:
return {}
@gin.register
class IdentityTransform(hk.Module):
"""Transform does not modify inputs."""
def __init__(self, *args, name: Optional[str] = None, **kwargs):
del args, kwargs # unused.
super().__init__(name=name)
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
return inputs
@gin.register
class ShiftAndNormalize(hk.Module):
"""Transforms inputs by shifting and normalizing values by `shifts/scales`."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
shifts: typing.Pytree,
scales: typing.Pytree,
features_to_exclude: Sequence[str] = tuple(),
global_scale: Optional[float] = None,
name: Optional[str] = None,
):
del coords, dt, physics_specs, aux_features # unused.
super().__init__(name=name)
self.shifts = shifts
if global_scale is not None:
scales = jax.tree_util.tree_map(lambda x: x * global_scale, scales)
self.scales = scales
def __call__(self, inputs):
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
shifts = pytree_utils.replace_with_matching_or_default(
inputs, self.shifts, default=None, check_used_all_replace_keys=False)
scales = pytree_utils.replace_with_matching_or_default(
inputs, self.scales, default=None, check_used_all_replace_keys=False)
# if shifts/scales have missing values present in `inputs`, we insert `None`
# for the default. If corresponding `inputs` is not `None`, this will raise
# an error, as expected. This works because tree_map skips `None` values in
# the first argument, as long as all dictionary keys match.
result = jax.tree_util.tree_map(
lambda x, y, z: (x - y) / z, inputs, shifts, scales)
return from_dict_fn(result)
@gin.register
class InverseShiftAndNormalize(hk.Module):
"""Inverse of the `ShiftAndNormalize` for the same `shifts/scales`."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
shifts: typing.Pytree,
scales: typing.Pytree,
global_scale: Optional[float] = None,
name: Optional[str] = None,
):
del coords, dt, physics_specs, aux_features # unused.
super().__init__(name=name)
self.shifts = shifts
if global_scale is not None:
scales = jax.tree_util.tree_map(lambda x: x * global_scale, scales)
self.scales = scales
def __call__(self, inputs):
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
shifts = pytree_utils.replace_with_matching_or_default(
inputs, self.shifts, default=None, check_used_all_replace_keys=False)
scales = pytree_utils.replace_with_matching_or_default(
inputs, self.scales, default=None, check_used_all_replace_keys=False)
# if shifts/scales have missing values present in `inputs`, we insert `None`
# for the default. If corresponding `inputs` is not `None`, this will raise
# an error, as expected. This works because tree_map skips `None` values in
# the first argument, as long as all dictionary keys match.
result = jax.tree_util.tree_map(
lambda x, y, z: (None if x is None else x * z + y),
inputs,
shifts,
scales,
is_leaf=lambda x: x is None,
)
return from_dict_fn(result)
@gin.register
class ToModalWithDivCurlTransform(hk.Module):
"""Module that converts inputs to modal replacing velocity with div/curl."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
name: Optional[str] = None,
):
del dt, physics_specs, aux_features # unused.
super().__init__(name=name)
self.coords = coords
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
if 'u' not in inputs or 'v' not in inputs:
raise ValueError('Inputs to ToModalWithDivCurlTransform must include `u, '
f'v`, got keys: {inputs.keys()}')
sec_lat = 1 / self.coords.horizontal.cos_lat
u, v = inputs.pop('u'), inputs.pop('v')
# here u,v stand for velocity / cos(lat), but the cos(lat) is cancelled in
# divergence and curl operators below.
inputs['u'] = u * sec_lat
inputs['v'] = v * sec_lat
to_modal_fn = lambda x: (self.coords.horizontal.to_modal(x) # pylint: disable=g-long-lambda
if x is not None else None)
modal_outputs = jax.tree_util.tree_map(to_modal_fn, inputs)
u, v = modal_outputs.pop('u'), modal_outputs.pop('v')
modal_outputs['divergence'] = self.coords.horizontal.div_cos_lat((u, v))
modal_outputs['vorticity'] = self.coords.horizontal.curl_cos_lat((u, v))
return modal_outputs
@gin.register
class ToModalDiffOperators(hk.Module):
"""Module that returns grad and laplacian features of inputs fields.
To avoid accidental accumulation of the cos(lat) factors, features must be
keyed using typing.KeyWithCosLatFactor namedtuple.
"""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
name: Optional[str] = None,
):
del dt, physics_specs, aux_features # unused.
super().__init__(name=name)
self.coords = coords
def __call__(
self,
inputs: Mapping[typing.KeyWithCosLatFactor, typing.Array],
) -> Mapping[typing.KeyWithCosLatFactor, typing.Array]:
features = {}
for k, value in inputs.items():
name, cos_lat_order = k.name, k.factor_order
d_value_dlon, d_value_dlat = self.coords.horizontal.cos_lat_grad(value)
laplacian_value = self.coords.horizontal.laplacian(value)
dlon_key = typing.KeyWithCosLatFactor(name + '_dlon', cos_lat_order + 1)
dlat_key = typing.KeyWithCosLatFactor(name + '_dlat', cos_lat_order + 1)
del2_key = typing.KeyWithCosLatFactor(name + '_del2', cos_lat_order)
features[dlon_key] = d_value_dlon
features[dlat_key] = d_value_dlat
features[del2_key] = laplacian_value
return features
@gin.register
class ModalToNodalTransform(hk.Module):
"""Transform that converts modal inputs to nodal representation."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
name: Optional[str] = None,
):
del dt, physics_specs, aux_features # unused.
super().__init__(name=name)
self.coords = coords
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
return self.coords.horizontal.to_nodal(inputs)
@gin.register
class NodalToModalTransform(hk.Module):
"""Transform that converts nodal inputs to modal representation."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
name: Optional[str] = None,
):
del dt, physics_specs, aux_features # unused.
super().__init__(name=name)
self.coords = coords
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
return self.coords.horizontal.to_modal(inputs)
@gin.register
class ClipTransform(hk.Module):
"""Transform that clips highest total wavenumber in inputs."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
wavenumbers_to_clip: int = 1,
name: Optional[str] = None,
):
"""See `time_integration.exponential_filter` for details."""
del dt, physics_specs, aux_features # unused.
super().__init__(name=name)
self.coords = coords
self.wavenumbers_to_clip = wavenumbers_to_clip
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
return self.coords.horizontal.clip_wavenumbers(
inputs, self.wavenumbers_to_clip
)
@gin.register
class NondimensionalizeTransform(hk.Module):
"""Transform that nondimensionalizes inputs."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
input_coords: coordinate_systems.CoordinateSystem,
inputs_to_units_mapping: Dict[str, str],
name: Optional[str] = None,
):
"""See `time_integration.exponential_filter` for details."""
del coords, dt, aux_features, input_coords # unused.
super().__init__(name=name)
self.inputs_to_units_mapping = inputs_to_units_mapping
self.nondimensionalize = physics_specs.nondimensionalize
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
inputs_to_units_mapping = pytree_utils.replace_with_matching_or_default(
inputs, self.inputs_to_units_mapping, default=None,
check_used_all_replace_keys=False,
)
nondim_fn = lambda x, y: self.nondimensionalize(x * typing.Quantity(y))
result = jax.tree_util.tree_map(nondim_fn, inputs, inputs_to_units_mapping)
return from_dict_fn(result)
@gin.register
class RedimensionalizeTransform(hk.Module):
"""Transform that redimensionalizes inputs."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
output_coords: coordinate_systems.CoordinateSystem,
inputs_to_units_mapping: Dict[str, str],
name: Optional[str] = None,
):
"""See `time_integration.exponential_filter` for details."""
del coords, dt, aux_features, output_coords # unused.
super().__init__(name=name)
self.inputs_to_units_mapping = inputs_to_units_mapping
self.dimensionalize = physics_specs.dimensionalize
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
inputs_to_units_mapping = pytree_utils.replace_with_matching_or_default(
inputs, self.inputs_to_units_mapping, default=None,
check_used_all_replace_keys=False,
)
dim_fn = lambda x, y: self.dimensionalize(x, typing.Quantity(y)).m
result = jax.tree_util.tree_map(dim_fn, inputs, inputs_to_units_mapping)
return from_dict_fn(result)
@gin.register
class SequentialTransform(hk.Module):
"""Transform module that combines multiple transforms applied sequentially."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
transform_modules: Sequence[TransformModule],
name: Optional[str] = None,
):
super().__init__(name=name)
self.transform_fns = [module(coords, dt, physics_specs, aux_features)
for module in transform_modules]
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
for transform_fn in self.transform_fns:
inputs = transform_fn(inputs)
return inputs
@gin.register
class LevelScale(hk.Module):
"""Transforms inputs by scaling different vertical levels."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
scales: Sequence[float],
keys_to_scale: Sequence[str] = tuple(),
name: Optional[str] = None,
):
del coords, dt, physics_specs, aux_features # unused.
super().__init__(name=name)
self.scale_fn = functools.partial(
coordinate_systems.scale_levels_for_matching_keys,
scales=np.asarray(scales),
keys_to_scale=keys_to_scale)
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
return self.scale_fn(inputs)
@gin.register
class InverseLevelScale(hk.Module):
"""Transforms inputs by inverse scaling different vertical levels."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
scales: Sequence[float],
keys_to_scale: Sequence[str] = tuple(),
name: Optional[str] = None,
):
del coords, dt, physics_specs, aux_features # unused.
super().__init__(name=name)
self.scale_fn = functools.partial(
coordinate_systems.scale_levels_for_matching_keys,
scales=1/np.asarray(scales),
keys_to_scale=keys_to_scale)
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
return self.scale_fn(inputs)
@gin.register
class HardClip(hk.Module):
"""Transforms inputs by hard clipping inputs to (-max_value, max_value)."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
max_value: float,
name: Optional[str] = None,
):
del coords, dt, physics_specs, aux_features # unused.
super().__init__(name=name)
self.clip_fn = functools.partial(
jnp.clip, min=-max_value, max=max_value)
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
return jax.tree_util.tree_map(self.clip_fn, inputs)
@gin.register
class SoftClip(hk.Module):
"""Transforms inputs by clipping values to a range with smooth boundaries.
Attributes:
coords: horizontal and vertical descritization.
dt: time step of the model.
physics_specs: object describing the scales and physical constants.
aux_features: dictionary holding static features that the model may use.
max_value: specifies the range (-max_value, max_value) of return values.
hinge_softness: controls the softness of the smoothing at the boundaries;
values outside of the max_value range are mapped into intervals of width
approximately `log(2) * hinge_softness` on the interior of each boundary.
name: optional name of the module.
"""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
max_value: float,
hinge_softness: float = 1.0,
name: Optional[str] = None,
):
del coords, dt, physics_specs, aux_features # unused.
if max_value < 0 or hinge_softness < 0:
raise ValueError('max_value and hinge_softness must be positive, '
f'{max_value=}, {hinge_softness=}')
super().__init__(name=name)
low = -max_value
high = max_value
hinge = hinge_softness
softplus_fn = lambda x: hinge * jax.nn.softplus(x / hinge)
self.clip_fn = lambda x: ( # pylint: disable=g-long-lambda
-softplus_fn(high - low - softplus_fn(x - low)) *
(high - low) / (softplus_fn(high - low)) + high)
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
return jax.tree_util.tree_map(self.clip_fn, inputs)
@gin.register
class ToModalDiffOperatorsWithFiltering(hk.Module):
"""Module that returns filtered grad and laplacian features of inputs fields.
To avoid accidental accumulation of the cos(lat) factors, features must be
keyed using typing.KeyWithCosLatFactor namedtuple.
"""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
filter_attenuations: Tuple[float, ...] = tuple(),
name: Optional[str] = None,
):
super().__init__(name=name)
self.coords = coords
self.attenuations = filter_attenuations
feature_filters = []
for attenuation in filter_attenuations:
feature_filters.append(
filters.DataExponentialFilter(
coords, dt, physics_specs, aux_features,
order=1, attenuation=attenuation))
self.feature_filters = feature_filters
def __call__(
self,
inputs: Mapping[KeyWithCosLatFactor, typing.Array],
) -> Mapping[KeyWithCosLatFactor, typing.Array]:
features = {}
for k, value in inputs.items():
name, cos_lat_order = k.name, k.factor_order
for filter_fn, att in zip(self.feature_filters, self.attenuations):
filtered_value = filter_fn(value)
d_value_dlon, d_value_dlat = self.coords.horizontal.cos_lat_grad(
filtered_value)
laplacian_value = self.coords.horizontal.laplacian(filtered_value)
# since gradient values picked up cos_lat factor we increment the
# corresponding key. This factor is adjusted at the caller level.
dlon_key = KeyWithCosLatFactor(
name + f'_dlon_{att}', cos_lat_order + 1, att)
dlat_key = KeyWithCosLatFactor(
name + f'_dlat_{att}', cos_lat_order + 1, att)
del2_key = KeyWithCosLatFactor(
name + f'_del2_{att}', cos_lat_order, att)
features[dlon_key] = d_value_dlon
features[dlat_key] = d_value_dlat
features[del2_key] = laplacian_value
return features
@gin.register
class TruncateSigmaLevels(hk.Module):
"""Transform module that truncates vertical levels for specified variables."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
sigma_ranges: dict[str, Tuple[float, float]],
name: Optional[str] = None,
):
super().__init__(name=name)
del dt, physics_specs, aux_features # unused.
self.sigma_ranges = sigma_ranges
self.sigma_levels = coords.vertical.centers
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
"""Returns `inputs` where only specified levels are retained."""
def _slice_fn(x, sigma_range):
"""Returns `x` sliced to include values in `sigma_range`."""
sigma_min_slice, sigma_max_slice = sigma_range
lower_index = np.argmax((self.sigma_levels - sigma_min_slice) > 0)
if sigma_max_slice > np.max(self.sigma_levels):
upper_index = len(self.sigma_levels)
else:
upper_index = np.argmin((self.sigma_levels - sigma_max_slice) < 0)
return x[slice(lower_index, upper_index), ...]
def recurse_and_replace(x: dict[str, Any],
y: dict[str, Any],
default=None) -> dict[str, Any]:
"""Copy x, setting leaf values to `default` or value from y if keys match."""
return {
k: (
y.get(k, default)
if not isinstance(v, dict)
else recurse_and_replace(v, y, default)
)
for k, v in x.items()
}
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
sigma_ranges_extended = recurse_and_replace(
inputs, self.sigma_ranges, default=(0, 1)
)
outputs = jax.tree_util.tree_map(_slice_fn, inputs, sigma_ranges_extended)
return from_dict_fn(outputs)
@gin.register
class TakeSurfaceAdjacentSigmaLevel(hk.Module):
"""Transform module that retains only the vertical level nearest to Earth surface for all variables."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
name: Optional[str] = None,
):
super().__init__(name=name)
del coords, dt, physics_specs, aux_features # unused.
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
"""Returns `inputs` where only last sigma level is retained."""
def _slice_fn(x):
return x[slice(-1, None), ...]
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
outputs = jax.tree_util.tree_map(_slice_fn, inputs)
return from_dict_fn(outputs)
@gin.register
@dataclasses.dataclass
class FeatureSelector:
"""Features transform that retains items whose keys match against regex.
Attributes:
regex_patterns: regular expression pattern that specifies the set of keys
from `inputs` that will be returned by __call__ method.
"""
regex_patterns: str
def __call__(
self,
inputs: Dict[str, typing.Array],
) -> Dict[str, typing.Array]:
outputs = {}
for k, v in inputs.items():
if re.fullmatch(self.regex_patterns, k):
outputs[k] = v
return outputs
@gin.register
class BroadcastTransform:
"""Features transform that broadcasts all features."""
def __init__(self, *args, **kwargs):
del args, kwargs # unused.
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
leaves, tree_def = jax.tree_util.tree_flatten(inputs)
leaves = jnp.broadcast_arrays(*leaves)
return jax.tree_util.tree_unflatten(tree_def, leaves)
@gin.register
class SquashLevelsTransform:
"""Transform that "squashes" values of inputs depending on their sigma level.
Multiplies inputs by the piecewise linear values used to "squash" inputs
by sigma level. See function χ definition at: http://screen/5V3jzU7ZFA4vVJP
The squash is paramtereizaed by low_cutoffs and high_cutoffs.
On Palmer 2009 (http://shortn/_56HCcQwmSS) page 4, the cutoffs for
perturbations are given. Below are translated to sigma levels values:
low_cutoffs: (100hPa, 50hPa)
low_cutoffs: (0.05, 0.1),
high_cutoffs: (1300m, 300m)
high_cutoffs: (0.86, 0.965)
Inputs that have a singleton or no level dimension are assumed defined at
the highest value of sigma ("surface level").
Attributes:
coords: horizontal and vertical descritization.
dt: time step of the model.
physics_specs: object describing the scales and physical constants.
aux_features: dictionary holding static features that the model may use.
low_cutoffs: σ=low_cutoffs[0] is when χ starts linearly increasing from 0.
σ=low_cutoffs[1] is when χ levels out at 1
high_cutoffs: σ=high_cutoffs[0] is when χ starts linearly decreasing from 1.
σ=high_cutoffs[1] is when χ reaches 0.
"""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
low_cutoffs: Sequence[float] = (0.05, 0.1),
high_cutoffs: Sequence[float] = (0.86, 0.965),
):
del dt, physics_specs, aux_features # unused.
if not isinstance(coords.vertical, sigma_coordinates.SigmaCoordinates):
raise ValueError(f'Cannot apply sigma_squash on {coords.vertical=}')
sigma = coords.vertical.centers
if len(low_cutoffs) != 2:
raise ValueError(f'{len(low_cutoffs)=} but should have been 2.')
if len(high_cutoffs) != 2:
raise ValueError(f'{len(high_cutoffs)=} but should have been 2.')
low_func = (sigma - low_cutoffs[0]) / (low_cutoffs[1] - low_cutoffs[0])
high_func = (high_cutoffs[1] - sigma) / (high_cutoffs[1] - high_cutoffs[0])
# lower_bound is a function equal to the squasher between
# low_cutoffs[0] and high_cutoffs[1].
# It becomes negative outside that range.
lower_bound = np.minimum(1., np.minimum(low_func, high_func))
self._sigma_squash = np.maximum(0., lower_bound)[:, np.newaxis, np.newaxis]
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
def squash_per_level_only(x):
shape = jnp.shape(x)
ndim = len(shape)
if ndim >= 3 and shape[-3] > 1: # If defined per-level
return x * self._sigma_squash
elif ndim in {2, 3}:
return x * self._sigma_squash[-1] # If defined at surface level
else:
return x
return jax.tree_util.tree_map(squash_per_level_only, inputs)
@gin.register
def add_prefix(features: dict[str, Any], prefix: str) -> dict[str, Any]:
"""Adds prefix to keys in features."""
return {prefix + k: v for k, v in features.items()}
def straight_through(
f: Callable[[typing.Array], typing.Array],
) -> Callable[[typing.Array], typing.Array]:
"""Straight-through estimator of `func`.
The "straight-through" estimator is a trick that fools auto-diff into
assigning a constant gradient (≡ 1) to a function.
See http://shortn/_kRQjMbF2QF
Args:
f: Callable mapping arrays to arrays. May be non-differentiable.
Returns:
g: Function g such that g(x) ≡ f(x) and g'(x) ≡ 1.
"""
def straight_through_f(x):
zero = x - jax.lax.stop_gradient(x)
return zero + jax.lax.stop_gradient(f(x))
return straight_through_f