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,221 @@
|
|
|
1
|
+
"""Self-consistent detaching 1D SOL model (evolved temperature).
|
|
2
|
+
|
|
3
|
+
Extends the recycling SOL to an *evolved* plasma pressure so the target
|
|
4
|
+
temperature responds to the plasma conditions -- the ingredient a detachment
|
|
5
|
+
study needs. Along the field it evolves ion density, parallel momentum, plasma
|
|
6
|
+
pressure, and a diffusive neutral density, with:
|
|
7
|
+
|
|
8
|
+
- **Spitzer parallel conduction** ``kappa ~ T^{5/2}`` solved implicitly (a solvax
|
|
9
|
+
tridiagonal), so the stiff parabolic heat transport is unconditionally stable;
|
|
10
|
+
- **radiative / ionization energy loss** from the AMJUEL fits, applied as a
|
|
11
|
+
*self-limiting* semi-implicit sink (``P <- P / (1 + dt * loss_rate)``) so the
|
|
12
|
+
loss cannot drive the pressure negative and switches off as the plasma cools;
|
|
13
|
+
- a **Bohm sheath heat sink** at the target and an **upstream power source**; and
|
|
14
|
+
- the neutral **recycling**, parallel diffusion, and ionization/recombination /
|
|
15
|
+
charge-exchange coupling of the recycling model.
|
|
16
|
+
|
|
17
|
+
As the upstream density rises at fixed upstream power the target cools, crosses
|
|
18
|
+
into the recombining regime below ~1 eV, and the target ion flux rolls over --
|
|
19
|
+
the classic SD1D detachment signature. Everything is pure ``jax.numpy`` (with the
|
|
20
|
+
solvax tridiagonal solves) and differentiable. Fields are hermes-3 normalized;
|
|
21
|
+
time is normalized to the parallel transit time.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import NamedTuple
|
|
28
|
+
|
|
29
|
+
import jax
|
|
30
|
+
import jax.numpy as jnp
|
|
31
|
+
|
|
32
|
+
from solvax import tridiagonal_solve
|
|
33
|
+
|
|
34
|
+
from .atomic_rates import (
|
|
35
|
+
charge_exchange_rate_coefficient,
|
|
36
|
+
energy_loss_coefficient,
|
|
37
|
+
rate_coefficient,
|
|
38
|
+
)
|
|
39
|
+
from .reactions import PlasmaNormalization
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"DetachmentSolParameters",
|
|
43
|
+
"DetachmentSolState",
|
|
44
|
+
"detachment_sol_step",
|
|
45
|
+
"detachment_sol_run",
|
|
46
|
+
"detachment_diagnostics",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class DetachmentSolParameters:
|
|
52
|
+
"""Parameters of the self-consistent detaching SOL model."""
|
|
53
|
+
|
|
54
|
+
parallel_length: float = 30.0
|
|
55
|
+
upstream_density: float = 4.0
|
|
56
|
+
upstream_power: float = 6.0 # normalized upstream power source
|
|
57
|
+
power_width: float = 0.2 # parallel width of the power source
|
|
58
|
+
conduction_coefficient: float = 2.0 # Spitzer kappa0 (normalized)
|
|
59
|
+
sheath_transmission: float = 7.0 # gamma (Te + Ti)
|
|
60
|
+
neutral_diffusion: float = 8.0
|
|
61
|
+
recycling_fraction: float = 0.95
|
|
62
|
+
ion_mass: float = 2.0
|
|
63
|
+
density_floor: float = 1.0e-4
|
|
64
|
+
pressure_floor: float = 1.0e-5
|
|
65
|
+
temperature_floor: float = 1.0e-3
|
|
66
|
+
# A scrape-off-layer reference temperature (upstream ~ tens of eV); the
|
|
67
|
+
# detachment transition and target-flux rollover live in this regime.
|
|
68
|
+
normalization: PlasmaNormalization = PlasmaNormalization(Tnorm=50.0)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DetachmentSolState(NamedTuple):
|
|
72
|
+
"""Evolved fields: ion density, ion momentum, plasma pressure, neutral density."""
|
|
73
|
+
|
|
74
|
+
ion_density: jnp.ndarray
|
|
75
|
+
ion_momentum: jnp.ndarray
|
|
76
|
+
plasma_pressure: jnp.ndarray
|
|
77
|
+
neutral_density: jnp.ndarray
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _temperature(density, pressure, params):
|
|
81
|
+
return jnp.maximum(pressure / (2.0 * jnp.maximum(density, params.density_floor)), params.temperature_floor)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _sound_speed(density, pressure, params):
|
|
85
|
+
return jnp.sqrt(jnp.maximum(pressure / (params.ion_mass * jnp.maximum(density, params.density_floor)), 0.0))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _hyperbolic_rhs(density, momentum, pressure, params):
|
|
89
|
+
mass = params.ion_mass
|
|
90
|
+
dz = 1.0 / density.shape[0]
|
|
91
|
+
velocity = momentum / (mass * jnp.maximum(density, params.density_floor))
|
|
92
|
+
speed = jnp.abs(velocity) + _sound_speed(density, pressure, params)
|
|
93
|
+
face_speed = jnp.maximum(speed[:-1], speed[1:])
|
|
94
|
+
flux_n = 0.5 * (density[:-1] * velocity[:-1] + density[1:] * velocity[1:]) - 0.5 * face_speed * (density[1:] - density[:-1])
|
|
95
|
+
flux_m = 0.5 * (momentum[:-1] * velocity[:-1] + pressure[:-1] + momentum[1:] * velocity[1:] + pressure[1:]) - 0.5 * face_speed * (momentum[1:] - momentum[:-1])
|
|
96
|
+
flux_p = 0.5 * ((5.0 / 3.0) * pressure[:-1] * velocity[:-1] + (5.0 / 3.0) * pressure[1:] * velocity[1:]) - 0.5 * face_speed * (pressure[1:] - pressure[:-1])
|
|
97
|
+
target_velocity = jnp.maximum(velocity[-1], _sound_speed(density, pressure, params)[-1])
|
|
98
|
+
flux_n = jnp.concatenate([jnp.zeros(1), flux_n, (density[-1] * target_velocity)[None]])
|
|
99
|
+
flux_m = jnp.concatenate([pressure[:1], flux_m, (mass * density[-1] * target_velocity**2 + pressure[-1])[None]])
|
|
100
|
+
flux_p = jnp.concatenate([jnp.zeros(1), flux_p, ((5.0 / 3.0) * pressure[-1] * target_velocity)[None]])
|
|
101
|
+
pressure_gradient = jnp.gradient(pressure, dz)
|
|
102
|
+
d_density = -(flux_n[1:] - flux_n[:-1]) / dz
|
|
103
|
+
d_momentum = -(flux_m[1:] - flux_m[:-1]) / dz
|
|
104
|
+
d_pressure = -(flux_p[1:] - flux_p[:-1]) / dz + (2.0 / 3.0) * velocity * pressure_gradient
|
|
105
|
+
return d_density, d_momentum, d_pressure
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _implicit_diffusion(field, face_diffusivity, dt):
|
|
109
|
+
n = field.shape[0]
|
|
110
|
+
dz = 1.0 / n
|
|
111
|
+
coefficient = dt / dz**2
|
|
112
|
+
lower = jnp.zeros(n).at[1:].set(-coefficient * face_diffusivity)
|
|
113
|
+
upper = jnp.zeros(n).at[:-1].set(-coefficient * face_diffusivity)
|
|
114
|
+
diagonal = jnp.ones(n).at[1:-1].set(1.0 + coefficient * (face_diffusivity[:-1] + face_diffusivity[1:]))
|
|
115
|
+
diagonal = diagonal.at[0].set(1.0 + coefficient * face_diffusivity[0])
|
|
116
|
+
diagonal = diagonal.at[-1].set(1.0 + coefficient * face_diffusivity[-1])
|
|
117
|
+
return tridiagonal_solve(lower, diagonal, upper, field, method="thomas")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def detachment_sol_step(state, params, dt):
|
|
121
|
+
"""Advance the self-consistent detaching SOL one operator-split step."""
|
|
122
|
+
|
|
123
|
+
norm = params.normalization
|
|
124
|
+
nz = state.ion_density.shape[0]
|
|
125
|
+
dz = 1.0 / nz
|
|
126
|
+
z = (jnp.arange(nz, dtype=jnp.float64) + 0.5) / nz
|
|
127
|
+
density_floor = params.density_floor
|
|
128
|
+
pressure_floor = params.pressure_floor
|
|
129
|
+
rate_scale = norm.Nnorm * (params.parallel_length / norm.sound_speed)
|
|
130
|
+
energy_scale = rate_scale / norm.Tnorm
|
|
131
|
+
|
|
132
|
+
density = jnp.maximum(state.ion_density, density_floor)
|
|
133
|
+
momentum = state.ion_momentum
|
|
134
|
+
pressure = jnp.maximum(state.plasma_pressure, pressure_floor)
|
|
135
|
+
neutral_density = state.neutral_density
|
|
136
|
+
|
|
137
|
+
# Hyperbolic transport + upstream power source.
|
|
138
|
+
d_density, d_momentum, d_pressure = _hyperbolic_rhs(density, momentum, pressure, params)
|
|
139
|
+
power_source = params.upstream_power * jnp.exp(-(z**2) / params.power_width**2)
|
|
140
|
+
density = jnp.maximum(density + dt * d_density, density_floor)
|
|
141
|
+
momentum = momentum + dt * d_momentum
|
|
142
|
+
pressure = jnp.maximum(pressure + dt * d_pressure + dt * (2.0 / 3.0) * power_source, pressure_floor)
|
|
143
|
+
|
|
144
|
+
# Atomic reactions: particle exchange (implicit) + momentum friction.
|
|
145
|
+
temperature = _temperature(density, pressure, params)
|
|
146
|
+
physical_temperature = temperature * norm.Tnorm
|
|
147
|
+
electron_density_m3 = jnp.maximum(density, density_floor) * norm.Nnorm
|
|
148
|
+
electron = jnp.maximum(density, density_floor)
|
|
149
|
+
ionization = rate_coefficient("d", "iz", physical_temperature, electron_density_m3) * rate_scale
|
|
150
|
+
recombination = rate_coefficient("d", "rec", physical_temperature, electron_density_m3) * rate_scale
|
|
151
|
+
ionization_frequency = ionization * electron
|
|
152
|
+
recombination_frequency = recombination * electron
|
|
153
|
+
total = density + neutral_density
|
|
154
|
+
density = jnp.maximum((density + dt * ionization_frequency * total) / (1.0 + dt * (ionization_frequency + recombination_frequency)), density_floor)
|
|
155
|
+
neutral_density = jnp.maximum(total - density, 0.0)
|
|
156
|
+
effective_temperature = jnp.clip(2.0 * temperature * norm.Tnorm / params.ion_mass, 0.01, 1.0e4)
|
|
157
|
+
charge_exchange_frequency = charge_exchange_rate_coefficient(effective_temperature) * electron * rate_scale
|
|
158
|
+
momentum = momentum / (1.0 + dt * (charge_exchange_frequency * neutral_density + recombination_frequency))
|
|
159
|
+
|
|
160
|
+
# Self-limiting radiative / ionization energy loss.
|
|
161
|
+
ionization_energy = energy_loss_coefficient("d", "iz", physical_temperature, electron_density_m3)
|
|
162
|
+
recombination_energy = energy_loss_coefficient("d", "rec", physical_temperature, electron_density_m3)
|
|
163
|
+
energy_loss = (neutral_density * electron * ionization_energy + electron * electron * recombination_energy) * energy_scale
|
|
164
|
+
loss_rate = (2.0 / 3.0) * jnp.maximum(energy_loss, 0.0) / jnp.maximum(pressure, pressure_floor)
|
|
165
|
+
pressure = pressure / (1.0 + dt * loss_rate)
|
|
166
|
+
|
|
167
|
+
# Bohm sheath heat sink at the target (semi-implicit).
|
|
168
|
+
sound_speed = _sound_speed(density, pressure, params)
|
|
169
|
+
sheath_rate = jnp.zeros(nz).at[-1].set(
|
|
170
|
+
(2.0 / 3.0) * params.sheath_transmission * density[-1] * sound_speed[-1] * _temperature(density, pressure, params)[-1] / dz / jnp.maximum(pressure[-1], pressure_floor)
|
|
171
|
+
)
|
|
172
|
+
pressure = pressure / (1.0 + dt * sheath_rate)
|
|
173
|
+
|
|
174
|
+
# Implicit Spitzer conduction.
|
|
175
|
+
temperature = _temperature(density, pressure, params)
|
|
176
|
+
conductivity = params.conduction_coefficient * temperature**2.5
|
|
177
|
+
diffusivity = conductivity / (3.0 * jnp.maximum(density, density_floor))
|
|
178
|
+
pressure = jnp.maximum(_implicit_diffusion(pressure, 0.5 * (diffusivity[:-1] + diffusivity[1:]), dt), pressure_floor)
|
|
179
|
+
|
|
180
|
+
density = density.at[0].set(params.upstream_density)
|
|
181
|
+
|
|
182
|
+
# Neutral parallel diffusion + target recycling.
|
|
183
|
+
neutral_diffusivity = jnp.full(nz - 1, params.neutral_diffusion * 0.04 / (params.ion_mass * 0.05))
|
|
184
|
+
sound_speed = _sound_speed(density, pressure, params)
|
|
185
|
+
recycling_source = jnp.zeros(nz).at[-1].set(params.recycling_fraction * density[-1] * sound_speed[-1] / dz)
|
|
186
|
+
neutral_density = jnp.maximum(_implicit_diffusion(neutral_density + dt * recycling_source, neutral_diffusivity, dt), 0.0)
|
|
187
|
+
|
|
188
|
+
return DetachmentSolState(density, momentum, pressure, neutral_density)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def detachment_sol_run(state, params, *, dt, steps):
|
|
192
|
+
"""Advance ``steps`` operator-split steps with a jitted ``lax.scan``."""
|
|
193
|
+
|
|
194
|
+
@jax.jit
|
|
195
|
+
def _run(initial):
|
|
196
|
+
def body(carry, _):
|
|
197
|
+
return detachment_sol_step(carry, params, dt), None
|
|
198
|
+
|
|
199
|
+
final, _ = jax.lax.scan(body, initial, None, length=steps)
|
|
200
|
+
return final
|
|
201
|
+
|
|
202
|
+
return _run(state)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class DetachmentDiagnostics(NamedTuple):
|
|
206
|
+
target_ion_flux: jnp.ndarray
|
|
207
|
+
target_temperature_ev: jnp.ndarray
|
|
208
|
+
target_density: jnp.ndarray
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def detachment_diagnostics(state, params) -> DetachmentDiagnostics:
|
|
212
|
+
"""Target ion flux (``n c_s``), target temperature [eV], and target density."""
|
|
213
|
+
|
|
214
|
+
density = jnp.maximum(state.ion_density, params.density_floor)
|
|
215
|
+
temperature = _temperature(density, jnp.maximum(state.plasma_pressure, params.pressure_floor), params)
|
|
216
|
+
sound_speed = _sound_speed(density, jnp.maximum(state.plasma_pressure, params.pressure_floor), params)
|
|
217
|
+
return DetachmentDiagnostics(
|
|
218
|
+
target_ion_flux=density[-1] * sound_speed[-1],
|
|
219
|
+
target_temperature_ev=temperature[-1] * params.normalization.Tnorm,
|
|
220
|
+
target_density=density[-1],
|
|
221
|
+
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Plasma <-> neutral atomic reaction sources for the 1D SOL model.
|
|
2
|
+
|
|
3
|
+
Assembles the ionization / recombination / charge-exchange source channels that
|
|
4
|
+
couple a hydrogenic plasma fluid (ion density ``Ni``, parallel momentum, ion
|
|
5
|
+
temperature ``Ti``, electron temperature ``Te``) to a neutral fluid (``Nn``,
|
|
6
|
+
momentum, ``Tn``), following the hermes-3 hydrogen reaction closure. Particle,
|
|
7
|
+
momentum, and (thermal) energy transfers use the Galilean-invariant form: each
|
|
8
|
+
particle transfer carries the source species' momentum ``m V`` and thermal
|
|
9
|
+
energy ``1.5 T``, and charge exchange adds a frictional heating ``0.5 m R dV^2``
|
|
10
|
+
from the ion-atom velocity difference. The electron channel is the ionization
|
|
11
|
+
cost / recombination radiation from the AMJUEL energy-loss fits.
|
|
12
|
+
|
|
13
|
+
All fields are in hermes-3 normalized units (density / ``Nnorm``, temperature /
|
|
14
|
+
``Tnorm`` [eV], velocity / ``Cs0``); the returned source rates are normalized to
|
|
15
|
+
``Nnorm * Omega_ci`` (particle / momentum) and ``Nnorm * Tnorm * Omega_ci``
|
|
16
|
+
(energy). Everything is pure ``jax.numpy`` and differentiable.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
|
|
23
|
+
import jax.numpy as jnp
|
|
24
|
+
|
|
25
|
+
from .atomic_rates import (
|
|
26
|
+
charge_exchange_rate_coefficient,
|
|
27
|
+
energy_loss_coefficient,
|
|
28
|
+
rate_coefficient,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = ["PlasmaNormalization", "HydrogenReactionSources", "compute_hydrogen_reaction_sources"]
|
|
32
|
+
|
|
33
|
+
_ELEMENTARY_CHARGE = 1.602176634e-19
|
|
34
|
+
_PROTON_MASS = 1.67262192369e-27
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class PlasmaNormalization:
|
|
39
|
+
"""hermes-3 normalization constants (deck defaults: 1e19 m^-3, 100 eV, 1 T)."""
|
|
40
|
+
|
|
41
|
+
Nnorm: float = 1.0e19
|
|
42
|
+
Tnorm: float = 100.0
|
|
43
|
+
Bnorm: float = 1.0
|
|
44
|
+
ion_mass: float = 2.0 # deuterium, in proton masses
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def sound_speed(self) -> float:
|
|
48
|
+
return float((_ELEMENTARY_CHARGE * self.Tnorm / _PROTON_MASS) ** 0.5)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def cyclotron_frequency(self) -> float:
|
|
52
|
+
return float(_ELEMENTARY_CHARGE * self.Bnorm / _PROTON_MASS)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class HydrogenReactionSources:
|
|
57
|
+
"""Normalized reaction source channels for the ion, neutral, and electron fluids."""
|
|
58
|
+
|
|
59
|
+
ion_density: jnp.ndarray
|
|
60
|
+
neutral_density: jnp.ndarray
|
|
61
|
+
ion_momentum: jnp.ndarray
|
|
62
|
+
neutral_momentum: jnp.ndarray
|
|
63
|
+
ion_energy: jnp.ndarray
|
|
64
|
+
neutral_energy: jnp.ndarray
|
|
65
|
+
electron_energy: jnp.ndarray
|
|
66
|
+
ionization_rate: jnp.ndarray
|
|
67
|
+
recombination_rate: jnp.ndarray
|
|
68
|
+
charge_exchange_rate: jnp.ndarray
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def compute_hydrogen_reaction_sources(
|
|
72
|
+
ion_density: jnp.ndarray,
|
|
73
|
+
ion_velocity: jnp.ndarray,
|
|
74
|
+
ion_temperature: jnp.ndarray,
|
|
75
|
+
electron_temperature: jnp.ndarray,
|
|
76
|
+
neutral_density: jnp.ndarray,
|
|
77
|
+
neutral_velocity: jnp.ndarray,
|
|
78
|
+
neutral_temperature: jnp.ndarray,
|
|
79
|
+
*,
|
|
80
|
+
normalization: PlasmaNormalization = PlasmaNormalization(),
|
|
81
|
+
charge_exchange_multiplier: float = 1.0,
|
|
82
|
+
) -> HydrogenReactionSources:
|
|
83
|
+
"""Return the normalized ion/neutral/electron reaction source channels."""
|
|
84
|
+
|
|
85
|
+
mass = float(normalization.ion_mass)
|
|
86
|
+
nnorm = float(normalization.Nnorm)
|
|
87
|
+
tnorm = float(normalization.Tnorm)
|
|
88
|
+
omega = normalization.cyclotron_frequency
|
|
89
|
+
rate_scale = nnorm / omega
|
|
90
|
+
energy_scale = nnorm / (tnorm * omega)
|
|
91
|
+
|
|
92
|
+
# Physical inputs for the rate fits.
|
|
93
|
+
electron_temperature_ev = electron_temperature * tnorm
|
|
94
|
+
electron_density_m3 = ion_density * nnorm # quasineutral, Z = 1
|
|
95
|
+
|
|
96
|
+
ionization_sigmav = rate_coefficient("d", "iz", electron_temperature_ev, electron_density_m3)
|
|
97
|
+
recombination_sigmav = rate_coefficient("d", "rec", electron_temperature_ev, electron_density_m3)
|
|
98
|
+
ionization_energy = energy_loss_coefficient("d", "iz", electron_temperature_ev, electron_density_m3)
|
|
99
|
+
recombination_energy = energy_loss_coefficient("d", "rec", electron_temperature_ev, electron_density_m3)
|
|
100
|
+
|
|
101
|
+
effective_temperature = jnp.clip(
|
|
102
|
+
(neutral_temperature / mass + ion_temperature / mass) * tnorm, 0.01, 1.0e4
|
|
103
|
+
)
|
|
104
|
+
cx_sigmav = charge_exchange_rate_coefficient(effective_temperature)
|
|
105
|
+
|
|
106
|
+
# Normalized volumetric reaction rates.
|
|
107
|
+
ionization_rate = neutral_density * ion_density * ionization_sigmav * rate_scale
|
|
108
|
+
recombination_rate = ion_density * ion_density * recombination_sigmav * rate_scale
|
|
109
|
+
charge_exchange_rate = (
|
|
110
|
+
ion_density * neutral_density * cx_sigmav * rate_scale * float(charge_exchange_multiplier)
|
|
111
|
+
)
|
|
112
|
+
ionization_radiation = neutral_density * ion_density * ionization_energy * energy_scale
|
|
113
|
+
recombination_radiation = ion_density * ion_density * recombination_energy * energy_scale
|
|
114
|
+
|
|
115
|
+
# Momentum carried by each particle transfer (source species' m V).
|
|
116
|
+
ionization_momentum = ionization_rate * mass * neutral_velocity
|
|
117
|
+
recombination_momentum = recombination_rate * mass * ion_velocity
|
|
118
|
+
cx_neutral_momentum = charge_exchange_rate * mass * neutral_velocity
|
|
119
|
+
cx_ion_momentum = charge_exchange_rate * mass * ion_velocity
|
|
120
|
+
|
|
121
|
+
# Thermal energy carried by each particle transfer, plus CX frictional heating.
|
|
122
|
+
ionization_energy_transfer = 1.5 * ionization_rate * neutral_temperature
|
|
123
|
+
recombination_energy_transfer = 1.5 * recombination_rate * ion_temperature
|
|
124
|
+
cx_neutral_thermal = 1.5 * charge_exchange_rate * neutral_temperature
|
|
125
|
+
cx_ion_thermal = 1.5 * charge_exchange_rate * ion_temperature
|
|
126
|
+
velocity_delta = ion_velocity - neutral_velocity
|
|
127
|
+
cx_frictional_heat = 0.5 * mass * charge_exchange_rate * velocity_delta**2
|
|
128
|
+
|
|
129
|
+
ion_density_source = ionization_rate - recombination_rate
|
|
130
|
+
neutral_density_source = -ionization_rate + recombination_rate
|
|
131
|
+
ion_momentum_source = (
|
|
132
|
+
ionization_momentum - recombination_momentum + cx_neutral_momentum - cx_ion_momentum
|
|
133
|
+
)
|
|
134
|
+
neutral_momentum_source = (
|
|
135
|
+
-ionization_momentum + recombination_momentum - cx_neutral_momentum + cx_ion_momentum
|
|
136
|
+
)
|
|
137
|
+
ion_energy_source = (
|
|
138
|
+
ionization_energy_transfer
|
|
139
|
+
- recombination_energy_transfer
|
|
140
|
+
+ cx_neutral_thermal
|
|
141
|
+
- cx_ion_thermal
|
|
142
|
+
+ cx_frictional_heat
|
|
143
|
+
)
|
|
144
|
+
neutral_energy_source = (
|
|
145
|
+
-ionization_energy_transfer
|
|
146
|
+
+ recombination_energy_transfer
|
|
147
|
+
- cx_neutral_thermal
|
|
148
|
+
+ cx_ion_thermal
|
|
149
|
+
+ cx_frictional_heat
|
|
150
|
+
)
|
|
151
|
+
electron_energy_source = -ionization_radiation - recombination_radiation
|
|
152
|
+
|
|
153
|
+
return HydrogenReactionSources(
|
|
154
|
+
ion_density=ion_density_source,
|
|
155
|
+
neutral_density=neutral_density_source,
|
|
156
|
+
ion_momentum=ion_momentum_source,
|
|
157
|
+
neutral_momentum=neutral_momentum_source,
|
|
158
|
+
ion_energy=ion_energy_source,
|
|
159
|
+
neutral_energy=neutral_energy_source,
|
|
160
|
+
electron_energy=electron_energy_source,
|
|
161
|
+
ionization_rate=ionization_rate,
|
|
162
|
+
recombination_rate=recombination_rate,
|
|
163
|
+
charge_exchange_rate=charge_exchange_rate,
|
|
164
|
+
)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Coupled 1D scrape-off-layer recycling model: plasma + diffusive neutral.
|
|
2
|
+
|
|
3
|
+
A reduced SOL model that couples a hydrogenic plasma fluid to a recycled neutral
|
|
4
|
+
fluid through the packaged atomic reactions. Along the field coordinate ``z``
|
|
5
|
+
(stagnation midplane at ``z = 0``, target plate at ``z = L``) it evolves the ion
|
|
6
|
+
density and parallel momentum on a *prescribed* parallel temperature profile
|
|
7
|
+
(hot upstream, cold target -- the imposed-temperature closure common in reduced
|
|
8
|
+
SOL studies, which sidesteps the stiff self-consistent conduction/radiation
|
|
9
|
+
energy balance), together with a neutral density that is:
|
|
10
|
+
|
|
11
|
+
- **recycled** from the Bohm ion flux at the target (fraction ``R``),
|
|
12
|
+
- **transported** by parallel diffusion (solved implicitly with a tridiagonal
|
|
13
|
+
solve, so the parabolic term is unconditionally stable), and
|
|
14
|
+
- **ionized / recombined** back into the plasma via the AMJUEL rate coefficients
|
|
15
|
+
(an operator-split, per-cell implicit update that is stable against the stiff
|
|
16
|
+
ionization source).
|
|
17
|
+
|
|
18
|
+
The result is an attached recycling SOL: neutrals born at the target penetrate
|
|
19
|
+
upstream, ionize where the plasma is hot, and the plasma flows back to the target
|
|
20
|
+
at the Bohm speed. Self-consistent detachment (an *evolved* temperature with
|
|
21
|
+
conduction and radiative rollover) is a further extension.
|
|
22
|
+
|
|
23
|
+
Everything is pure ``jax.numpy`` (with the solvax tridiagonal solve) and therefore
|
|
24
|
+
``jit``/``grad``/``vmap`` transparent. Fields are hermes-3 normalized
|
|
25
|
+
(density / ``Nnorm``, temperature / ``Tnorm``, velocity / the reference sound
|
|
26
|
+
speed); the reaction rates use physical temperature and density internally.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from typing import NamedTuple
|
|
33
|
+
|
|
34
|
+
import jax
|
|
35
|
+
import jax.numpy as jnp
|
|
36
|
+
|
|
37
|
+
from solvax import tridiagonal_solve
|
|
38
|
+
|
|
39
|
+
from .atomic_rates import charge_exchange_rate_coefficient, rate_coefficient
|
|
40
|
+
from .reactions import PlasmaNormalization
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"SolRecyclingParameters",
|
|
44
|
+
"SolRecyclingState",
|
|
45
|
+
"linear_target_temperature_profile",
|
|
46
|
+
"sol_recycling_step",
|
|
47
|
+
"sol_recycling_run",
|
|
48
|
+
"target_ion_flux",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class SolRecyclingParameters:
|
|
54
|
+
"""Parameters of the reduced recycling SOL model."""
|
|
55
|
+
|
|
56
|
+
parallel_length: float = 30.0 # m, stagnation -> target
|
|
57
|
+
upstream_density: float = 1.0 # fixed upstream ion density (normalized)
|
|
58
|
+
recycling_fraction: float = 1.0 # target recycling fraction R
|
|
59
|
+
neutral_diffusion: float = 8.0 # neutral parallel diffusion enhancement
|
|
60
|
+
neutral_temperature: float = 0.04 # normalized (cold, ~2 eV) neutral temperature
|
|
61
|
+
ion_mass: float = 2.0 # deuterium
|
|
62
|
+
density_floor: float = 1.0e-4
|
|
63
|
+
normalization: PlasmaNormalization = PlasmaNormalization()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SolRecyclingState(NamedTuple):
|
|
67
|
+
"""Evolved fields: ion density, ion momentum (A n v), neutral density."""
|
|
68
|
+
|
|
69
|
+
ion_density: jnp.ndarray
|
|
70
|
+
ion_momentum: jnp.ndarray
|
|
71
|
+
neutral_density: jnp.ndarray
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def linear_target_temperature_profile(nz: int, *, upstream_ev: float = 30.0, target_ev: float = 2.0,
|
|
75
|
+
normalization: PlasmaNormalization = PlasmaNormalization()) -> jnp.ndarray:
|
|
76
|
+
"""Prescribed parallel temperature (normalized): quadratic hot->cold to the target."""
|
|
77
|
+
|
|
78
|
+
z = (jnp.arange(nz, dtype=jnp.float64) + 0.5) / nz
|
|
79
|
+
profile_ev = upstream_ev - (upstream_ev - target_ev) * z**2
|
|
80
|
+
return profile_ev / normalization.Tnorm
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _sound_speed(density, temperature, params):
|
|
84
|
+
return jnp.sqrt(jnp.maximum(2.0 * temperature / params.ion_mass, 0.0))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _velocity(momentum, density, params):
|
|
88
|
+
return momentum / (params.ion_mass * jnp.maximum(density, params.density_floor))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _hyperbolic_rhs(density, momentum, temperature, params):
|
|
92
|
+
mass = params.ion_mass
|
|
93
|
+
dz = 1.0 / density.shape[0]
|
|
94
|
+
velocity = _velocity(momentum, density, params)
|
|
95
|
+
pressure = 2.0 * density * temperature
|
|
96
|
+
speed = jnp.abs(velocity) + _sound_speed(density, temperature, params)
|
|
97
|
+
face_speed = jnp.maximum(speed[:-1], speed[1:])
|
|
98
|
+
flux_n = 0.5 * (density[:-1] * velocity[:-1] + density[1:] * velocity[1:]) - 0.5 * face_speed * (density[1:] - density[:-1])
|
|
99
|
+
flux_m = 0.5 * (momentum[:-1] * velocity[:-1] + pressure[:-1] + momentum[1:] * velocity[1:] + pressure[1:]) - 0.5 * face_speed * (momentum[1:] - momentum[:-1])
|
|
100
|
+
# noflow stagnation at z=0 (pressure force only); Bohm outflow at the target.
|
|
101
|
+
target_velocity = jnp.maximum(velocity[-1], _sound_speed(density, temperature, params)[-1])
|
|
102
|
+
flux_n = jnp.concatenate([jnp.zeros(1), flux_n, (density[-1] * target_velocity)[None]])
|
|
103
|
+
flux_m = jnp.concatenate([pressure[:1], flux_m, (mass * density[-1] * target_velocity**2 + pressure[-1])[None]])
|
|
104
|
+
d_density = -(flux_n[1:] - flux_n[:-1]) / dz
|
|
105
|
+
d_momentum = -(flux_m[1:] - flux_m[:-1]) / dz
|
|
106
|
+
return d_density, d_momentum
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _implicit_diffusion(field, face_diffusivity, source, dt):
|
|
110
|
+
"""Backward-Euler parallel diffusion with no-flux ends (tridiagonal solve)."""
|
|
111
|
+
|
|
112
|
+
n = field.shape[0]
|
|
113
|
+
dz = 1.0 / n
|
|
114
|
+
coefficient = dt / dz**2
|
|
115
|
+
lower = jnp.zeros(n).at[1:].set(-coefficient * face_diffusivity)
|
|
116
|
+
upper = jnp.zeros(n).at[:-1].set(-coefficient * face_diffusivity)
|
|
117
|
+
diagonal = jnp.ones(n).at[1:-1].set(1.0 + coefficient * (face_diffusivity[:-1] + face_diffusivity[1:]))
|
|
118
|
+
diagonal = diagonal.at[0].set(1.0 + coefficient * face_diffusivity[0])
|
|
119
|
+
diagonal = diagonal.at[-1].set(1.0 + coefficient * face_diffusivity[-1])
|
|
120
|
+
return tridiagonal_solve(lower, diagonal, upper, field + dt * source, method="thomas")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _reaction_update(density, momentum, neutral_density, temperature, params, dt):
|
|
124
|
+
"""Operator-split implicit ionization/recombination particle exchange, plus the
|
|
125
|
+
charge-exchange + recombination momentum friction that drags the plasma flow
|
|
126
|
+
toward the (stationary) neutrals -- the drag that keeps the target sonic."""
|
|
127
|
+
|
|
128
|
+
norm = params.normalization
|
|
129
|
+
physical_temperature = temperature * norm.Tnorm
|
|
130
|
+
density_floor = params.density_floor
|
|
131
|
+
electron_density_m3 = jnp.maximum(density, density_floor) * norm.Nnorm
|
|
132
|
+
# Time is normalized to the parallel transit time L / c_s, so a physical
|
|
133
|
+
# volumetric rate n^2 <sigma v> maps to n^2 <sigma v> * Nnorm * (L / c_s).
|
|
134
|
+
rate_scale = norm.Nnorm * (params.parallel_length / norm.sound_speed)
|
|
135
|
+
ionization = rate_coefficient("d", "iz", physical_temperature, electron_density_m3) * rate_scale
|
|
136
|
+
recombination = rate_coefficient("d", "rec", physical_temperature, electron_density_m3) * rate_scale
|
|
137
|
+
effective_temperature = jnp.clip(2.0 * temperature * norm.Tnorm / params.ion_mass, 0.01, 1.0e4)
|
|
138
|
+
charge_exchange = charge_exchange_rate_coefficient(effective_temperature) * rate_scale
|
|
139
|
+
electron = jnp.maximum(density, density_floor)
|
|
140
|
+
ionization_frequency = ionization * electron
|
|
141
|
+
recombination_frequency = recombination * electron
|
|
142
|
+
total = density + neutral_density
|
|
143
|
+
new_density = (density + dt * ionization_frequency * total) / (1.0 + dt * (ionization_frequency + recombination_frequency))
|
|
144
|
+
# Momentum friction: charge exchange and recombination remove ion parallel
|
|
145
|
+
# momentum (neutrals are ~stationary); implicit drag for stability.
|
|
146
|
+
friction_frequency = charge_exchange * neutral_density + recombination_frequency
|
|
147
|
+
new_momentum = momentum / (1.0 + dt * friction_frequency)
|
|
148
|
+
return jnp.maximum(new_density, density_floor), new_momentum, jnp.maximum(total - new_density, 0.0)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def sol_recycling_step(state, temperature, params, dt):
|
|
152
|
+
"""Advance the recycling SOL one operator-split step."""
|
|
153
|
+
|
|
154
|
+
density = jnp.maximum(state.ion_density, params.density_floor)
|
|
155
|
+
momentum = state.ion_momentum
|
|
156
|
+
neutral_density = state.neutral_density
|
|
157
|
+
|
|
158
|
+
d_density, d_momentum = _hyperbolic_rhs(density, momentum, temperature, params)
|
|
159
|
+
density = jnp.maximum(density + dt * d_density, params.density_floor)
|
|
160
|
+
momentum = momentum + dt * d_momentum
|
|
161
|
+
|
|
162
|
+
density, momentum, neutral_density = _reaction_update(density, momentum, neutral_density, temperature, params, dt)
|
|
163
|
+
|
|
164
|
+
diffusivity = params.neutral_diffusion * params.neutral_temperature / (params.ion_mass * 0.05)
|
|
165
|
+
face_diffusivity = jnp.full(density.shape[0] - 1, diffusivity)
|
|
166
|
+
sound_speed = _sound_speed(density, temperature, params)
|
|
167
|
+
dz = 1.0 / density.shape[0]
|
|
168
|
+
recycling_source = jnp.zeros(density.shape[0]).at[-1].set(
|
|
169
|
+
params.recycling_fraction * density[-1] * sound_speed[-1] / dz
|
|
170
|
+
)
|
|
171
|
+
neutral_density = jnp.maximum(_implicit_diffusion(neutral_density, face_diffusivity, recycling_source, dt), 0.0)
|
|
172
|
+
|
|
173
|
+
# Pin the upstream ion density (Dirichlet) at the end of the step.
|
|
174
|
+
density = density.at[0].set(params.upstream_density)
|
|
175
|
+
|
|
176
|
+
return SolRecyclingState(ion_density=density, ion_momentum=momentum, neutral_density=neutral_density)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def sol_recycling_run(state, temperature, params, *, dt, steps):
|
|
180
|
+
"""Advance ``steps`` operator-split steps with a jitted ``lax.scan``."""
|
|
181
|
+
|
|
182
|
+
@jax.jit
|
|
183
|
+
def _run(initial):
|
|
184
|
+
def body(carry, _):
|
|
185
|
+
return sol_recycling_step(carry, temperature, params, dt), None
|
|
186
|
+
|
|
187
|
+
final, _ = jax.lax.scan(body, initial, None, length=steps)
|
|
188
|
+
return final
|
|
189
|
+
|
|
190
|
+
return _run(state)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def target_ion_flux(state, temperature, params) -> jnp.ndarray:
|
|
194
|
+
"""Bohm ion particle flux to the target plate ``n c_s``."""
|
|
195
|
+
|
|
196
|
+
density = jnp.maximum(state.ion_density, params.density_floor)
|
|
197
|
+
return density[-1] * _sound_speed(density, temperature, params)[-1]
|