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.
Files changed (106) hide show
  1. drbx/__init__.py +20 -0
  2. drbx/__main__.py +5 -0
  3. drbx/cli.py +586 -0
  4. drbx/config/__init__.py +20 -0
  5. drbx/config/boutinp.py +556 -0
  6. drbx/config/model.py +34 -0
  7. drbx/config/normalization.py +66 -0
  8. drbx/data/atomic_rates/__init__.py +1 -0
  9. drbx/data/atomic_rates/iz_AMJUEL_H.x_2.1.5.json +213 -0
  10. drbx/data/atomic_rates/iz_AMJUEL_H.x_2.3.9a.json +213 -0
  11. drbx/data/atomic_rates/rec_AMJUEL_H.x_2.1.8.json +213 -0
  12. drbx/data/atomic_rates/rec_AMJUEL_H.x_2.3.13a.json +213 -0
  13. drbx/geometry/__init__.py +207 -0
  14. drbx/geometry/embedding.py +56 -0
  15. drbx/geometry/essos_import.py +1385 -0
  16. drbx/geometry/fci_geometry.py +4622 -0
  17. drbx/geometry/fci_maps.py +85 -0
  18. drbx/geometry/island_divertor.py +291 -0
  19. drbx/geometry/metric_tensor.py +99 -0
  20. drbx/geometry/open_slab.py +150 -0
  21. drbx/geometry/rotating_ellipse.py +253 -0
  22. drbx/geometry/shifted_torus.py +225 -0
  23. drbx/geometry/stellarator.py +287 -0
  24. drbx/geometry/vmec_extender_import.py +499 -0
  25. drbx/geometry/vmec_jax_import.py +306 -0
  26. drbx/linear/__init__.py +37 -0
  27. drbx/linear/dispersion.py +138 -0
  28. drbx/linear/eigen.py +91 -0
  29. drbx/native/__init__.py +224 -0
  30. drbx/native/array_backend.py +64 -0
  31. drbx/native/deck_runner.py +779 -0
  32. drbx/native/electromagnetic.py +250 -0
  33. drbx/native/expression.py +173 -0
  34. drbx/native/fci.py +295 -0
  35. drbx/native/fci_2_field_rhs.py +182 -0
  36. drbx/native/fci_4_field_rhs.py +1267 -0
  37. drbx/native/fci_boundaries.py +2494 -0
  38. drbx/native/fci_differentiable_case.py +304 -0
  39. drbx/native/fci_drb_EB_rhs.py +1243 -0
  40. drbx/native/fci_drb_rhs.py +190 -0
  41. drbx/native/fci_halo.py +1575 -0
  42. drbx/native/fci_helpers.py +350 -0
  43. drbx/native/fci_model.py +294 -0
  44. drbx/native/fci_neutral.py +139 -0
  45. drbx/native/fci_operators.py +4081 -0
  46. drbx/native/fci_sharding.py +597 -0
  47. drbx/native/fci_sheath_recycling.py +206 -0
  48. drbx/native/fci_time_integrator.py +96 -0
  49. drbx/native/fci_vorticity.py +198 -0
  50. drbx/native/fluid_1d.py +330 -0
  51. drbx/native/hasegawa_wakatani.py +196 -0
  52. drbx/native/limiters.py +57 -0
  53. drbx/native/mesh.py +238 -0
  54. drbx/native/metrics.py +234 -0
  55. drbx/native/neutrals/__init__.py +58 -0
  56. drbx/native/neutrals/atomic_rates.py +134 -0
  57. drbx/native/neutrals/detachment_sol_model.py +221 -0
  58. drbx/native/neutrals/reactions.py +164 -0
  59. drbx/native/neutrals/recycling_sol_model.py +197 -0
  60. drbx/native/sol_flux_tube.py +133 -0
  61. drbx/native/stellarator_turbulence.py +343 -0
  62. drbx/native/transport.py +134 -0
  63. drbx/native/units.py +32 -0
  64. drbx/native/vorticity.py +252 -0
  65. drbx/runtime/__init__.py +53 -0
  66. drbx/runtime/artifacts.py +161 -0
  67. drbx/runtime/memory.py +144 -0
  68. drbx/runtime/output.py +374 -0
  69. drbx/runtime/paths.py +9 -0
  70. drbx/runtime/performance.py +161 -0
  71. drbx/runtime/run_config.py +184 -0
  72. drbx/runtime/scheduler.py +99 -0
  73. drbx/runtime/state.py +40 -0
  74. drbx/validation/__init__.py +424 -0
  75. drbx/validation/autodiff_diffusion.py +329 -0
  76. drbx/validation/autodiff_diffusion_uncertainty.py +235 -0
  77. drbx/validation/diverted_tokamak_movie.py +558 -0
  78. drbx/validation/essos_fieldline_import_campaign.py +181 -0
  79. drbx/validation/essos_imported_artifact_audit.py +535 -0
  80. drbx/validation/essos_imported_drb_movie_campaign.py +2826 -0
  81. drbx/validation/essos_imported_fci_campaign.py +5241 -0
  82. drbx/validation/essos_imported_pytree_campaign.py +406 -0
  83. drbx/validation/essos_vmec_closed_field_campaign.py +314 -0
  84. drbx/validation/essos_vmec_closed_field_transient_campaign.py +629 -0
  85. drbx/validation/essos_vmec_fieldline_surface_campaign.py +620 -0
  86. drbx/validation/fluid_1d_mms_convergence.py +250 -0
  87. drbx/validation/geometry_lineouts.py +136 -0
  88. drbx/validation/geometry_slices.py +178 -0
  89. drbx/validation/publication_plotting.py +91 -0
  90. drbx/validation/stellarator_drb_pytree_campaign.py +621 -0
  91. drbx/validation/stellarator_fci_geometry_campaign.py +200 -0
  92. drbx/validation/stellarator_fci_operator_campaign.py +304 -0
  93. drbx/validation/stellarator_fci_suite_campaign.py +264 -0
  94. drbx/validation/stellarator_metric_mms_campaign.py +289 -0
  95. drbx/validation/stellarator_neutral_physics_campaign.py +255 -0
  96. drbx/validation/stellarator_sheath_recycling_campaign.py +331 -0
  97. drbx/validation/stellarator_sol_showcase.py +628 -0
  98. drbx/validation/stellarator_vorticity_campaign.py +304 -0
  99. drbx/validation/vmec_extender_edge_field_campaign.py +260 -0
  100. drbx/validation/vmec_extender_sol_smoke_campaign.py +365 -0
  101. drbx-2.0.0.dev0.dist-info/METADATA +380 -0
  102. drbx-2.0.0.dev0.dist-info/RECORD +106 -0
  103. drbx-2.0.0.dev0.dist-info/WHEEL +5 -0
  104. drbx-2.0.0.dev0.dist-info/entry_points.txt +2 -0
  105. drbx-2.0.0.dev0.dist-info/licenses/LICENSE +21 -0
  106. 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