drbx 2.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- drbx/__init__.py +20 -0
- drbx/__main__.py +5 -0
- drbx/cli.py +586 -0
- drbx/config/__init__.py +20 -0
- drbx/config/boutinp.py +556 -0
- drbx/config/model.py +34 -0
- drbx/config/normalization.py +66 -0
- drbx/data/atomic_rates/__init__.py +1 -0
- drbx/data/atomic_rates/iz_AMJUEL_H.x_2.1.5.json +213 -0
- drbx/data/atomic_rates/iz_AMJUEL_H.x_2.3.9a.json +213 -0
- drbx/data/atomic_rates/rec_AMJUEL_H.x_2.1.8.json +213 -0
- drbx/data/atomic_rates/rec_AMJUEL_H.x_2.3.13a.json +213 -0
- drbx/geometry/__init__.py +207 -0
- drbx/geometry/embedding.py +56 -0
- drbx/geometry/essos_import.py +1385 -0
- drbx/geometry/fci_geometry.py +4622 -0
- drbx/geometry/fci_maps.py +85 -0
- drbx/geometry/island_divertor.py +291 -0
- drbx/geometry/metric_tensor.py +99 -0
- drbx/geometry/open_slab.py +150 -0
- drbx/geometry/rotating_ellipse.py +253 -0
- drbx/geometry/shifted_torus.py +225 -0
- drbx/geometry/stellarator.py +287 -0
- drbx/geometry/vmec_extender_import.py +499 -0
- drbx/geometry/vmec_jax_import.py +306 -0
- drbx/linear/__init__.py +37 -0
- drbx/linear/dispersion.py +138 -0
- drbx/linear/eigen.py +91 -0
- drbx/native/__init__.py +224 -0
- drbx/native/array_backend.py +64 -0
- drbx/native/deck_runner.py +779 -0
- drbx/native/electromagnetic.py +250 -0
- drbx/native/expression.py +173 -0
- drbx/native/fci.py +295 -0
- drbx/native/fci_2_field_rhs.py +182 -0
- drbx/native/fci_4_field_rhs.py +1267 -0
- drbx/native/fci_boundaries.py +2494 -0
- drbx/native/fci_differentiable_case.py +304 -0
- drbx/native/fci_drb_EB_rhs.py +1243 -0
- drbx/native/fci_drb_rhs.py +190 -0
- drbx/native/fci_halo.py +1575 -0
- drbx/native/fci_helpers.py +350 -0
- drbx/native/fci_model.py +294 -0
- drbx/native/fci_neutral.py +139 -0
- drbx/native/fci_operators.py +4081 -0
- drbx/native/fci_sharding.py +597 -0
- drbx/native/fci_sheath_recycling.py +206 -0
- drbx/native/fci_time_integrator.py +96 -0
- drbx/native/fci_vorticity.py +198 -0
- drbx/native/fluid_1d.py +330 -0
- drbx/native/hasegawa_wakatani.py +196 -0
- drbx/native/limiters.py +57 -0
- drbx/native/mesh.py +238 -0
- drbx/native/metrics.py +234 -0
- drbx/native/neutrals/__init__.py +58 -0
- drbx/native/neutrals/atomic_rates.py +134 -0
- drbx/native/neutrals/detachment_sol_model.py +221 -0
- drbx/native/neutrals/reactions.py +164 -0
- drbx/native/neutrals/recycling_sol_model.py +197 -0
- drbx/native/sol_flux_tube.py +133 -0
- drbx/native/stellarator_turbulence.py +343 -0
- drbx/native/transport.py +134 -0
- drbx/native/units.py +32 -0
- drbx/native/vorticity.py +252 -0
- drbx/runtime/__init__.py +53 -0
- drbx/runtime/artifacts.py +161 -0
- drbx/runtime/memory.py +144 -0
- drbx/runtime/output.py +374 -0
- drbx/runtime/paths.py +9 -0
- drbx/runtime/performance.py +161 -0
- drbx/runtime/run_config.py +184 -0
- drbx/runtime/scheduler.py +99 -0
- drbx/runtime/state.py +40 -0
- drbx/validation/__init__.py +424 -0
- drbx/validation/autodiff_diffusion.py +329 -0
- drbx/validation/autodiff_diffusion_uncertainty.py +235 -0
- drbx/validation/diverted_tokamak_movie.py +558 -0
- drbx/validation/essos_fieldline_import_campaign.py +181 -0
- drbx/validation/essos_imported_artifact_audit.py +535 -0
- drbx/validation/essos_imported_drb_movie_campaign.py +2826 -0
- drbx/validation/essos_imported_fci_campaign.py +5241 -0
- drbx/validation/essos_imported_pytree_campaign.py +406 -0
- drbx/validation/essos_vmec_closed_field_campaign.py +314 -0
- drbx/validation/essos_vmec_closed_field_transient_campaign.py +629 -0
- drbx/validation/essos_vmec_fieldline_surface_campaign.py +620 -0
- drbx/validation/fluid_1d_mms_convergence.py +250 -0
- drbx/validation/geometry_lineouts.py +136 -0
- drbx/validation/geometry_slices.py +178 -0
- drbx/validation/publication_plotting.py +91 -0
- drbx/validation/stellarator_drb_pytree_campaign.py +621 -0
- drbx/validation/stellarator_fci_geometry_campaign.py +200 -0
- drbx/validation/stellarator_fci_operator_campaign.py +304 -0
- drbx/validation/stellarator_fci_suite_campaign.py +264 -0
- drbx/validation/stellarator_metric_mms_campaign.py +289 -0
- drbx/validation/stellarator_neutral_physics_campaign.py +255 -0
- drbx/validation/stellarator_sheath_recycling_campaign.py +331 -0
- drbx/validation/stellarator_sol_showcase.py +628 -0
- drbx/validation/stellarator_vorticity_campaign.py +304 -0
- drbx/validation/vmec_extender_edge_field_campaign.py +260 -0
- drbx/validation/vmec_extender_sol_smoke_campaign.py +365 -0
- drbx-2.0.0.dev0.dist-info/METADATA +380 -0
- drbx-2.0.0.dev0.dist-info/RECORD +106 -0
- drbx-2.0.0.dev0.dist-info/WHEEL +5 -0
- drbx-2.0.0.dev0.dist-info/entry_points.txt +2 -0
- drbx-2.0.0.dev0.dist-info/licenses/LICENSE +21 -0
- drbx-2.0.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import jax.numpy as jnp
|
|
6
|
+
|
|
7
|
+
from ..geometry import FciMaps
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class FciTargetMasks:
|
|
12
|
+
"""Open-field endpoint masks derived from traced FCI plane intersections."""
|
|
13
|
+
|
|
14
|
+
forward: jnp.ndarray
|
|
15
|
+
backward: jnp.ndarray
|
|
16
|
+
endpoint_count: jnp.ndarray
|
|
17
|
+
active: jnp.ndarray
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class FciSheathRecyclingResult:
|
|
22
|
+
"""JAX-native sheath-loss and target-recycling diagnostic fields."""
|
|
23
|
+
|
|
24
|
+
masks: FciTargetMasks
|
|
25
|
+
sound_speed: jnp.ndarray
|
|
26
|
+
forward_ion_particle_flux: jnp.ndarray
|
|
27
|
+
backward_ion_particle_flux: jnp.ndarray
|
|
28
|
+
ion_particle_loss: jnp.ndarray
|
|
29
|
+
electron_particle_loss: jnp.ndarray
|
|
30
|
+
electron_heat_loss: jnp.ndarray
|
|
31
|
+
ion_heat_loss: jnp.ndarray
|
|
32
|
+
target_heat_load: jnp.ndarray
|
|
33
|
+
recycled_particle_source: jnp.ndarray
|
|
34
|
+
recycled_neutral_energy_source: jnp.ndarray
|
|
35
|
+
plasma_particle_sink: jnp.ndarray
|
|
36
|
+
plasma_energy_sink: jnp.ndarray
|
|
37
|
+
current_residual: jnp.ndarray
|
|
38
|
+
total_ion_particle_loss: jnp.ndarray
|
|
39
|
+
total_electron_particle_loss: jnp.ndarray
|
|
40
|
+
total_recycled_particle_source: jnp.ndarray
|
|
41
|
+
total_target_heat_load: jnp.ndarray
|
|
42
|
+
total_recycled_neutral_energy: jnp.ndarray
|
|
43
|
+
particle_recycling_residual: jnp.ndarray
|
|
44
|
+
neutral_energy_recycling_residual: jnp.ndarray
|
|
45
|
+
current_balance_residual: jnp.ndarray
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_fci_target_masks(maps: FciMaps) -> FciTargetMasks:
|
|
49
|
+
"""Build endpoint masks from forward/backward field-line map exits."""
|
|
50
|
+
|
|
51
|
+
forward = jnp.asarray(maps.forward_boundary, dtype=jnp.float64)
|
|
52
|
+
backward = jnp.asarray(maps.backward_boundary, dtype=jnp.float64)
|
|
53
|
+
endpoint_count = forward + backward
|
|
54
|
+
active = endpoint_count > 0.0
|
|
55
|
+
return FciTargetMasks(
|
|
56
|
+
forward=forward,
|
|
57
|
+
backward=backward,
|
|
58
|
+
endpoint_count=endpoint_count,
|
|
59
|
+
active=active,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def fci_bohm_sound_speed(
|
|
64
|
+
electron_temperature: jnp.ndarray,
|
|
65
|
+
ion_temperature: jnp.ndarray,
|
|
66
|
+
*,
|
|
67
|
+
ion_mass: float = 1.0,
|
|
68
|
+
temperature_floor: float = 1.0e-12,
|
|
69
|
+
) -> jnp.ndarray:
|
|
70
|
+
"""Return the normalized sheath-entry sound speed."""
|
|
71
|
+
|
|
72
|
+
te = jnp.maximum(jnp.asarray(electron_temperature, dtype=jnp.float64), float(temperature_floor))
|
|
73
|
+
ti = jnp.maximum(jnp.asarray(ion_temperature, dtype=jnp.float64), float(temperature_floor))
|
|
74
|
+
return jnp.sqrt((te + ti) / float(ion_mass))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compute_fci_sheath_recycling(
|
|
78
|
+
density: jnp.ndarray,
|
|
79
|
+
electron_temperature: jnp.ndarray,
|
|
80
|
+
ion_temperature: jnp.ndarray,
|
|
81
|
+
maps: FciMaps,
|
|
82
|
+
*,
|
|
83
|
+
recycling_fraction: float = 0.98,
|
|
84
|
+
electron_sheath_transmission: float = 5.0,
|
|
85
|
+
ion_sheath_transmission: float = 3.5,
|
|
86
|
+
recycled_neutral_energy: float = 0.03,
|
|
87
|
+
ion_mass: float = 1.0,
|
|
88
|
+
density_floor: float = 1.0e-12,
|
|
89
|
+
temperature_floor: float = 1.0e-12,
|
|
90
|
+
) -> FciSheathRecyclingResult:
|
|
91
|
+
"""Evaluate target loss and recycled neutral sources on FCI endpoints.
|
|
92
|
+
|
|
93
|
+
The closure is the first non-axisymmetric target gate. It applies a
|
|
94
|
+
normalized Bohm flux on every traced endpoint and closes exact accounting
|
|
95
|
+
identities for recycled particle and neutral-energy sources. The electron
|
|
96
|
+
particle flux is reconstructed from zero-current balance for singly charged
|
|
97
|
+
ions, so the current residual is an explicit regression field.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
masks = build_fci_target_masks(maps)
|
|
101
|
+
n = jnp.maximum(jnp.asarray(density, dtype=jnp.float64), float(density_floor))
|
|
102
|
+
te = jnp.maximum(jnp.asarray(electron_temperature, dtype=jnp.float64), float(temperature_floor))
|
|
103
|
+
ti = jnp.maximum(jnp.asarray(ion_temperature, dtype=jnp.float64), float(temperature_floor))
|
|
104
|
+
cs = fci_bohm_sound_speed(
|
|
105
|
+
te,
|
|
106
|
+
ti,
|
|
107
|
+
ion_mass=ion_mass,
|
|
108
|
+
temperature_floor=temperature_floor,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
forward_flux = masks.forward * n * cs
|
|
112
|
+
backward_flux = masks.backward * n * cs
|
|
113
|
+
ion_loss = forward_flux + backward_flux
|
|
114
|
+
electron_loss = ion_loss
|
|
115
|
+
electron_heat_loss = float(electron_sheath_transmission) * ion_loss * te
|
|
116
|
+
ion_heat_loss = float(ion_sheath_transmission) * ion_loss * ti
|
|
117
|
+
target_heat_load = electron_heat_loss + ion_heat_loss
|
|
118
|
+
recycled_particle_source = float(recycling_fraction) * ion_loss
|
|
119
|
+
recycled_neutral_energy_source = float(recycled_neutral_energy) * recycled_particle_source
|
|
120
|
+
plasma_particle_sink = -ion_loss
|
|
121
|
+
plasma_energy_sink = -target_heat_load
|
|
122
|
+
current_residual = electron_loss - ion_loss
|
|
123
|
+
|
|
124
|
+
total_ion_loss = jnp.sum(ion_loss)
|
|
125
|
+
total_electron_loss = jnp.sum(electron_loss)
|
|
126
|
+
total_recycled_source = jnp.sum(recycled_particle_source)
|
|
127
|
+
total_heat_load = jnp.sum(target_heat_load)
|
|
128
|
+
total_neutral_energy = jnp.sum(recycled_neutral_energy_source)
|
|
129
|
+
particle_recycling_residual = total_recycled_source - float(recycling_fraction) * total_ion_loss
|
|
130
|
+
neutral_energy_recycling_residual = total_neutral_energy - float(recycled_neutral_energy) * total_recycled_source
|
|
131
|
+
current_balance_residual = total_electron_loss - total_ion_loss
|
|
132
|
+
|
|
133
|
+
return FciSheathRecyclingResult(
|
|
134
|
+
masks=masks,
|
|
135
|
+
sound_speed=cs,
|
|
136
|
+
forward_ion_particle_flux=forward_flux,
|
|
137
|
+
backward_ion_particle_flux=backward_flux,
|
|
138
|
+
ion_particle_loss=ion_loss,
|
|
139
|
+
electron_particle_loss=electron_loss,
|
|
140
|
+
electron_heat_loss=electron_heat_loss,
|
|
141
|
+
ion_heat_loss=ion_heat_loss,
|
|
142
|
+
target_heat_load=target_heat_load,
|
|
143
|
+
recycled_particle_source=recycled_particle_source,
|
|
144
|
+
recycled_neutral_energy_source=recycled_neutral_energy_source,
|
|
145
|
+
plasma_particle_sink=plasma_particle_sink,
|
|
146
|
+
plasma_energy_sink=plasma_energy_sink,
|
|
147
|
+
current_residual=current_residual,
|
|
148
|
+
total_ion_particle_loss=total_ion_loss,
|
|
149
|
+
total_electron_particle_loss=total_electron_loss,
|
|
150
|
+
total_recycled_particle_source=total_recycled_source,
|
|
151
|
+
total_target_heat_load=total_heat_load,
|
|
152
|
+
total_recycled_neutral_energy=total_neutral_energy,
|
|
153
|
+
particle_recycling_residual=particle_recycling_residual,
|
|
154
|
+
neutral_energy_recycling_residual=neutral_energy_recycling_residual,
|
|
155
|
+
current_balance_residual=current_balance_residual,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def fci_sheath_recycling_field_rhs(
|
|
160
|
+
fields: dict[str, jnp.ndarray],
|
|
161
|
+
maps: FciMaps,
|
|
162
|
+
*,
|
|
163
|
+
ion_density_name: str = "Ni",
|
|
164
|
+
electron_density_name: str = "Ne",
|
|
165
|
+
neutral_density_name: str = "Nn",
|
|
166
|
+
ion_pressure_name: str = "Pi",
|
|
167
|
+
electron_pressure_name: str = "Pe",
|
|
168
|
+
neutral_pressure_name: str = "Pn",
|
|
169
|
+
recycling_fraction: float = 0.98,
|
|
170
|
+
electron_sheath_transmission: float = 5.0,
|
|
171
|
+
ion_sheath_transmission: float = 3.5,
|
|
172
|
+
recycled_neutral_energy: float = 0.03,
|
|
173
|
+
ion_mass: float = 1.0,
|
|
174
|
+
density_floor: float = 1.0e-12,
|
|
175
|
+
temperature_floor: float = 1.0e-12,
|
|
176
|
+
) -> dict[str, jnp.ndarray]:
|
|
177
|
+
"""Return fixed-field RHS arrays for FCI sheath and recycling sources."""
|
|
178
|
+
|
|
179
|
+
ion_density = jnp.asarray(fields[ion_density_name], dtype=jnp.float64)
|
|
180
|
+
electron_density = jnp.asarray(fields.get(electron_density_name, ion_density), dtype=jnp.float64)
|
|
181
|
+
ion_pressure = jnp.asarray(fields[ion_pressure_name], dtype=jnp.float64)
|
|
182
|
+
electron_pressure = jnp.asarray(fields[electron_pressure_name], dtype=jnp.float64)
|
|
183
|
+
ion_temperature = ion_pressure / jnp.maximum(ion_density, float(density_floor))
|
|
184
|
+
electron_temperature = electron_pressure / jnp.maximum(electron_density, float(density_floor))
|
|
185
|
+
result = compute_fci_sheath_recycling(
|
|
186
|
+
ion_density,
|
|
187
|
+
electron_temperature,
|
|
188
|
+
ion_temperature,
|
|
189
|
+
maps,
|
|
190
|
+
recycling_fraction=recycling_fraction,
|
|
191
|
+
electron_sheath_transmission=electron_sheath_transmission,
|
|
192
|
+
ion_sheath_transmission=ion_sheath_transmission,
|
|
193
|
+
recycled_neutral_energy=recycled_neutral_energy,
|
|
194
|
+
ion_mass=ion_mass,
|
|
195
|
+
density_floor=density_floor,
|
|
196
|
+
temperature_floor=temperature_floor,
|
|
197
|
+
)
|
|
198
|
+
rhs = {
|
|
199
|
+
ion_density_name: -result.ion_particle_loss,
|
|
200
|
+
electron_density_name: -result.electron_particle_loss,
|
|
201
|
+
neutral_density_name: result.recycled_particle_source,
|
|
202
|
+
ion_pressure_name: -result.ion_heat_loss,
|
|
203
|
+
electron_pressure_name: -result.electron_heat_loss,
|
|
204
|
+
neutral_pressure_name: result.recycled_neutral_energy_source,
|
|
205
|
+
}
|
|
206
|
+
return {name: value for name, value in rhs.items() if name in fields}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from typing import Callable, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
import jax
|
|
8
|
+
|
|
9
|
+
from .fci_model import FciModelState
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
StateT = TypeVar("StateT", bound=FciModelState)
|
|
13
|
+
CarryT = TypeVar("CarryT")
|
|
14
|
+
AuxT = TypeVar("AuxT")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@jax.tree_util.register_pytree_node_class
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Rk4StepResult(Generic[StateT, CarryT, AuxT]):
|
|
20
|
+
"""Container for a single RK4 advance.
|
|
21
|
+
|
|
22
|
+
``stage_aux`` stores the four auxiliary payloads returned by the stage RHS
|
|
23
|
+
calls. The auxiliary payload is intentionally opaque so that callers can
|
|
24
|
+
thread timings, solver diagnostics, warm-start carries, or any other
|
|
25
|
+
model-specific stage information without the RK4 core knowing about it.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
state: StateT
|
|
29
|
+
carry: CarryT
|
|
30
|
+
stage_aux: tuple[AuxT, AuxT, AuxT, AuxT]
|
|
31
|
+
|
|
32
|
+
def tree_flatten(self):
|
|
33
|
+
aux_1, aux_2, aux_3, aux_4 = self.stage_aux
|
|
34
|
+
return (self.state, self.carry, aux_1, aux_2, aux_3, aux_4), None
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def tree_unflatten(cls, _aux_data, children):
|
|
38
|
+
state, carry, aux_1, aux_2, aux_3, aux_4 = children
|
|
39
|
+
return cls(state=state, carry=carry, stage_aux=(aux_1, aux_2, aux_3, aux_4))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _rk4_weighted_rhs(k1: StateT, k2: StateT, k3: StateT, k4: StateT) -> StateT:
|
|
43
|
+
"""Return ``k1 + 2*k2 + 2*k3 + k4`` using the shared state algebra."""
|
|
44
|
+
|
|
45
|
+
return k1.axpy(k2, scale=2.0).axpy(k3, scale=2.0).axpy(k4, scale=1.0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def rk4_step(
|
|
49
|
+
state: StateT,
|
|
50
|
+
*,
|
|
51
|
+
time: float | jax.Array,
|
|
52
|
+
timestep: float | jax.Array,
|
|
53
|
+
rhs_fn: Callable[[StateT, float | jax.Array, CarryT], tuple[StateT, CarryT, AuxT]],
|
|
54
|
+
carry: CarryT,
|
|
55
|
+
) -> Rk4StepResult[StateT, CarryT, AuxT]:
|
|
56
|
+
"""Advance ``state`` by one classical RK4 step.
|
|
57
|
+
|
|
58
|
+
The RK4 algebra is model-agnostic:
|
|
59
|
+
|
|
60
|
+
- ``rhs_fn`` computes the stage RHS for the current stage state and returns
|
|
61
|
+
the RHS, the next carry value, and an arbitrary auxiliary payload.
|
|
62
|
+
- The carry is threaded from stage to stage so models can warm-start local
|
|
63
|
+
solves, keep stage caches, or propagate other stage-local context.
|
|
64
|
+
- The final carry returned by the step is the carry produced by the fourth
|
|
65
|
+
stage evaluation.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
k1, carry_1, aux_1 = rhs_fn(state, time, carry)
|
|
69
|
+
stage_1 = state.axpy(k1, scale=0.5 * timestep)
|
|
70
|
+
|
|
71
|
+
k2, carry_2, aux_2 = rhs_fn(stage_1, time + 0.5 * timestep, carry_1)
|
|
72
|
+
stage_2 = state.axpy(k2, scale=0.5 * timestep)
|
|
73
|
+
|
|
74
|
+
k3, carry_3, aux_3 = rhs_fn(stage_2, time + 0.5 * timestep, carry_2)
|
|
75
|
+
stage_3 = state.axpy(k3, scale=timestep)
|
|
76
|
+
|
|
77
|
+
k4, carry_4, aux_4 = rhs_fn(stage_3, time + timestep, carry_3)
|
|
78
|
+
next_state = state.axpy(_rk4_weighted_rhs(k1, k2, k3, k4), scale=timestep / 6.0)
|
|
79
|
+
return Rk4StepResult(state=next_state, carry=carry_4, stage_aux=(aux_1, aux_2, aux_3, aux_4))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def sum_stage_outputs(stage_outputs: tuple[AuxT, AuxT, AuxT, AuxT]) -> AuxT:
|
|
83
|
+
"""Reduce four stage payloads by addition.
|
|
84
|
+
|
|
85
|
+
This is handy when each stage returns a timing vector or another additive
|
|
86
|
+
PyTree. Models with non-additive diagnostics can ignore this helper and
|
|
87
|
+
reduce their stage payloads manually.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def _add(left: AuxT, right: AuxT) -> AuxT:
|
|
91
|
+
return jax.tree_util.tree_map(lambda lhs, rhs: lhs + rhs, left, right)
|
|
92
|
+
|
|
93
|
+
return reduce(_add, stage_outputs[1:], stage_outputs[0])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
__all__ = ["Rk4StepResult", "rk4_step", "sum_stage_outputs"]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
import jax.numpy as jnp
|
|
7
|
+
|
|
8
|
+
from ..geometry import MetricTensor3D
|
|
9
|
+
from .fci import conservative_perp_diffusion_xz
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class FciVorticitySolveResult:
|
|
14
|
+
potential: jnp.ndarray
|
|
15
|
+
residual: jnp.ndarray
|
|
16
|
+
residual_l2: jnp.ndarray
|
|
17
|
+
iterations: int
|
|
18
|
+
preconditioner: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def apply_fci_vorticity_operator(
|
|
22
|
+
potential: jnp.ndarray,
|
|
23
|
+
density: jnp.ndarray,
|
|
24
|
+
metric: MetricTensor3D,
|
|
25
|
+
*,
|
|
26
|
+
boussinesq: bool = True,
|
|
27
|
+
regularization: float = 1.0e-9,
|
|
28
|
+
) -> jnp.ndarray:
|
|
29
|
+
"""Apply the positive perpendicular vorticity operator to ``phi``."""
|
|
30
|
+
|
|
31
|
+
phi = _remove_mean(jnp.asarray(potential, dtype=jnp.float64), metric)
|
|
32
|
+
n = jnp.asarray(density, dtype=jnp.float64)
|
|
33
|
+
if boussinesq:
|
|
34
|
+
coefficient = jnp.ones_like(phi) * jnp.mean(n / jnp.square(metric.Bxy))
|
|
35
|
+
else:
|
|
36
|
+
coefficient = n / jnp.maximum(jnp.square(metric.Bxy), 1.0e-30)
|
|
37
|
+
operator = -conservative_perp_diffusion_xz(phi, coefficient, metric)
|
|
38
|
+
return _remove_mean(operator, metric) + float(regularization) * phi
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def solve_fci_vorticity_potential_cg(
|
|
42
|
+
vorticity: jnp.ndarray,
|
|
43
|
+
density: jnp.ndarray,
|
|
44
|
+
metric: MetricTensor3D,
|
|
45
|
+
*,
|
|
46
|
+
iterations: int = 80,
|
|
47
|
+
boussinesq: bool = True,
|
|
48
|
+
regularization: float = 1.0e-9,
|
|
49
|
+
preconditioner: str | None = None,
|
|
50
|
+
) -> FciVorticitySolveResult:
|
|
51
|
+
"""Solve the metric-weighted perpendicular vorticity inversion with CG."""
|
|
52
|
+
|
|
53
|
+
rhs = _remove_mean(jnp.asarray(vorticity, dtype=jnp.float64), metric)
|
|
54
|
+
x0 = jnp.zeros_like(rhs, dtype=jnp.float64)
|
|
55
|
+
normalized_preconditioner = _normalize_preconditioner_name(preconditioner)
|
|
56
|
+
|
|
57
|
+
def apply_operator(value: jnp.ndarray) -> jnp.ndarray:
|
|
58
|
+
return apply_fci_vorticity_operator(
|
|
59
|
+
value,
|
|
60
|
+
density,
|
|
61
|
+
metric,
|
|
62
|
+
boussinesq=boussinesq,
|
|
63
|
+
regularization=regularization,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
r0 = rhs - apply_operator(x0)
|
|
67
|
+
if normalized_preconditioner == "jacobi":
|
|
68
|
+
inverse_diagonal = _fci_vorticity_jacobi_inverse_diagonal(
|
|
69
|
+
density,
|
|
70
|
+
metric,
|
|
71
|
+
boussinesq=boussinesq,
|
|
72
|
+
regularization=regularization,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def apply_preconditioner(residual: jnp.ndarray) -> jnp.ndarray:
|
|
76
|
+
return _remove_mean(inverse_diagonal * residual, metric)
|
|
77
|
+
|
|
78
|
+
z0 = apply_preconditioner(r0)
|
|
79
|
+
p0 = z0
|
|
80
|
+
rz0 = _inner(r0, z0, metric)
|
|
81
|
+
|
|
82
|
+
def body(
|
|
83
|
+
_index: int,
|
|
84
|
+
carry: tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray],
|
|
85
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
86
|
+
x, r, p, rz = carry
|
|
87
|
+
ap = apply_operator(p)
|
|
88
|
+
alpha = rz / jnp.maximum(_inner(p, ap, metric), 1.0e-30)
|
|
89
|
+
x_next = _remove_mean(x + alpha * p, metric)
|
|
90
|
+
r_next = r - alpha * ap
|
|
91
|
+
z_next = apply_preconditioner(r_next)
|
|
92
|
+
rz_next = _inner(r_next, z_next, metric)
|
|
93
|
+
beta = rz_next / jnp.maximum(rz, 1.0e-30)
|
|
94
|
+
p_next = z_next + beta * p
|
|
95
|
+
return x_next, r_next, p_next, rz_next
|
|
96
|
+
|
|
97
|
+
potential, residual, _direction, _rz = jax.lax.fori_loop(
|
|
98
|
+
0,
|
|
99
|
+
int(iterations),
|
|
100
|
+
body,
|
|
101
|
+
(x0, r0, p0, rz0),
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
p0 = r0
|
|
105
|
+
|
|
106
|
+
def body(
|
|
107
|
+
_index: int,
|
|
108
|
+
carry: tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray],
|
|
109
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
110
|
+
x, r, p = carry
|
|
111
|
+
ap = apply_operator(p)
|
|
112
|
+
rs = _inner(r, r, metric)
|
|
113
|
+
alpha = rs / jnp.maximum(_inner(p, ap, metric), 1.0e-30)
|
|
114
|
+
x_next = _remove_mean(x + alpha * p, metric)
|
|
115
|
+
r_next = r - alpha * ap
|
|
116
|
+
rs_next = _inner(r_next, r_next, metric)
|
|
117
|
+
beta = rs_next / jnp.maximum(rs, 1.0e-30)
|
|
118
|
+
p_next = r_next + beta * p
|
|
119
|
+
return x_next, r_next, p_next
|
|
120
|
+
|
|
121
|
+
potential, residual, _direction = jax.lax.fori_loop(
|
|
122
|
+
0,
|
|
123
|
+
int(iterations),
|
|
124
|
+
body,
|
|
125
|
+
(x0, r0, p0),
|
|
126
|
+
)
|
|
127
|
+
residual = rhs - apply_operator(potential)
|
|
128
|
+
residual_l2 = jnp.sqrt(
|
|
129
|
+
_inner(residual, residual, metric)
|
|
130
|
+
/ jnp.maximum(_inner(rhs, rhs, metric), 1.0e-30)
|
|
131
|
+
)
|
|
132
|
+
return FciVorticitySolveResult(
|
|
133
|
+
potential=potential,
|
|
134
|
+
residual=residual,
|
|
135
|
+
residual_l2=residual_l2,
|
|
136
|
+
iterations=int(iterations),
|
|
137
|
+
preconditioner=normalized_preconditioner,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _normalize_preconditioner_name(preconditioner: str | None) -> str | None:
|
|
142
|
+
if preconditioner is None:
|
|
143
|
+
return None
|
|
144
|
+
normalized = str(preconditioner).strip().lower().replace("-", "_")
|
|
145
|
+
if normalized in {"", "none", "unpreconditioned"}:
|
|
146
|
+
return None
|
|
147
|
+
if normalized in {"jacobi", "diagonal", "diag"}:
|
|
148
|
+
return "jacobi"
|
|
149
|
+
raise ValueError(f"Unsupported FCI vorticity preconditioner {preconditioner!r}.")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _fci_vorticity_jacobi_inverse_diagonal(
|
|
153
|
+
density: jnp.ndarray,
|
|
154
|
+
metric: MetricTensor3D,
|
|
155
|
+
*,
|
|
156
|
+
boussinesq: bool,
|
|
157
|
+
regularization: float,
|
|
158
|
+
) -> jnp.ndarray:
|
|
159
|
+
n = jnp.asarray(density, dtype=jnp.float64)
|
|
160
|
+
if boussinesq:
|
|
161
|
+
coefficient = jnp.ones_like(n) * jnp.mean(n / jnp.square(metric.Bxy))
|
|
162
|
+
else:
|
|
163
|
+
coefficient = n / jnp.maximum(jnp.square(metric.Bxy), 1.0e-30)
|
|
164
|
+
jac = jnp.asarray(metric.J, dtype=jnp.float64)
|
|
165
|
+
dx = jnp.asarray(metric.dx, dtype=jnp.float64)
|
|
166
|
+
dz = jnp.asarray(metric.dz, dtype=jnp.float64)
|
|
167
|
+
kx = jac * coefficient * jnp.asarray(metric.g11, dtype=jnp.float64)
|
|
168
|
+
kz = jac * coefficient * jnp.asarray(metric.g33, dtype=jnp.float64)
|
|
169
|
+
|
|
170
|
+
dx_face = 0.5 * (dx[1:, :, :] + dx[:-1, :, :])
|
|
171
|
+
kx_face = 0.5 * (kx[1:, :, :] + kx[:-1, :, :])
|
|
172
|
+
x_plus = jnp.zeros_like(n).at[:-1, :, :].set(
|
|
173
|
+
kx_face / jnp.maximum(dx_face, 1.0e-30)
|
|
174
|
+
)
|
|
175
|
+
x_minus = jnp.zeros_like(n).at[1:, :, :].set(
|
|
176
|
+
kx_face / jnp.maximum(dx_face, 1.0e-30)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
dz_plus = 0.5 * (dz + jnp.roll(dz, -1, axis=2))
|
|
180
|
+
kz_face = 0.5 * (kz + jnp.roll(kz, -1, axis=2))
|
|
181
|
+
z_plus = kz_face / jnp.maximum(dz_plus, 1.0e-30)
|
|
182
|
+
z_minus = jnp.roll(z_plus, 1, axis=2)
|
|
183
|
+
diagonal = (
|
|
184
|
+
(x_plus + x_minus) / jnp.maximum(dx, 1.0e-30)
|
|
185
|
+
+ (z_plus + z_minus) / jnp.maximum(dz, 1.0e-30)
|
|
186
|
+
) / jnp.maximum(jac, 1.0e-30)
|
|
187
|
+
diagonal = diagonal + float(regularization)
|
|
188
|
+
return 1.0 / jnp.maximum(diagonal, 1.0e-30)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _inner(left: jnp.ndarray, right: jnp.ndarray, metric: MetricTensor3D) -> jnp.ndarray:
|
|
192
|
+
return jnp.sum(jnp.asarray(metric.J, dtype=jnp.float64) * left * right)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _remove_mean(value: jnp.ndarray, metric: MetricTensor3D) -> jnp.ndarray:
|
|
196
|
+
weights = jnp.asarray(metric.J, dtype=jnp.float64)
|
|
197
|
+
mean = jnp.sum(weights * value) / jnp.maximum(jnp.sum(weights), 1.0e-30)
|
|
198
|
+
return value - mean
|