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,133 @@
|
|
|
1
|
+
"""Reduced isothermal scrape-off-layer (SOL) flux-tube parallel transport.
|
|
2
|
+
|
|
3
|
+
An open-field-line model: on the open slab geometry
|
|
4
|
+
(:func:`drbx.geometry.build_open_slab_geometry`), plasma is transported along
|
|
5
|
+
the field to the two target plates, where a Bohm sheath drains it at the sound
|
|
6
|
+
speed. The model evolves the parallel density ``n`` and parallel momentum
|
|
7
|
+
``m = n v`` as an isothermal Euler system in the field-parallel coordinate ``z``,
|
|
8
|
+
|
|
9
|
+
d n / dt + d (n v) / dz = S_n
|
|
10
|
+
d m / dt + d (n v^2 + n c_s^2) / dz = 0
|
|
11
|
+
|
|
12
|
+
with an upstream particle source ``S_n`` and Bohm sheath outflow (``|v| >= c_s``)
|
|
13
|
+
at ``z = 0`` and ``z = L``. Faces use a Rusanov (local Lax--Friedrichs) flux; the
|
|
14
|
+
whole update is pure JAX (``jit``/``grad``/``vmap`` transparent). The steady
|
|
15
|
+
state is the classic two-point SOL solution: the flow accelerates from a
|
|
16
|
+
stagnation point to the sound speed at each target, and the target density is
|
|
17
|
+
half the upstream density.
|
|
18
|
+
|
|
19
|
+
The transport acts along the parallel (``z``) axis of the ``(nx, ny, nz)`` field
|
|
20
|
+
arrays, so each ``(x, y)`` column is an independent flux tube sharing the open
|
|
21
|
+
target plates.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
|
|
28
|
+
import jax
|
|
29
|
+
import jax.numpy as jnp
|
|
30
|
+
|
|
31
|
+
from ..geometry import FciGeometry3D
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"SolFluxTubeParameters",
|
|
35
|
+
"sol_flux_tube_source",
|
|
36
|
+
"sol_flux_tube_rhs",
|
|
37
|
+
"sol_flux_tube_step",
|
|
38
|
+
"sol_flux_tube_run",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class SolFluxTubeParameters:
|
|
44
|
+
"""Parameters of the reduced isothermal SOL flux tube."""
|
|
45
|
+
|
|
46
|
+
sound_speed: float = 1.0 # normalized isothermal sound speed c_s
|
|
47
|
+
source_amplitude: float = 0.02 # peak upstream particle source
|
|
48
|
+
source_width: float = 4.0 # parallel width of the upstream source
|
|
49
|
+
density_floor: float = 1.0e-6
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def sol_flux_tube_source(geometry: FciGeometry3D, params: SolFluxTubeParameters) -> jnp.ndarray:
|
|
53
|
+
"""Upstream particle source: a Gaussian centered at the parallel midplane."""
|
|
54
|
+
|
|
55
|
+
z = jnp.asarray(geometry.grid.z.centers, dtype=jnp.float64)
|
|
56
|
+
midplane = 0.5 * (float(z[0]) + float(z[-1]))
|
|
57
|
+
profile = params.source_amplitude * jnp.exp(-((z - midplane) ** 2) / params.source_width**2)
|
|
58
|
+
return jnp.broadcast_to(profile[None, None, :], geometry.shape)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _flux(density: jnp.ndarray, momentum: jnp.ndarray, c_s: float) -> tuple[jnp.ndarray, jnp.ndarray]:
|
|
62
|
+
velocity = momentum / density
|
|
63
|
+
return momentum, momentum * velocity + density * c_s**2
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _rusanov(nL, mL, nR, mR, c_s):
|
|
67
|
+
fL_n, fL_m = _flux(nL, mL, c_s)
|
|
68
|
+
fR_n, fR_m = _flux(nR, mR, c_s)
|
|
69
|
+
speed = jnp.maximum(jnp.abs(mL / nL), jnp.abs(mR / nR)) + c_s
|
|
70
|
+
flux_n = 0.5 * (fL_n + fR_n) - 0.5 * speed * (nR - nL)
|
|
71
|
+
flux_m = 0.5 * (fL_m + fR_m) - 0.5 * speed * (mR - mL)
|
|
72
|
+
return flux_n, flux_m
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def sol_flux_tube_rhs(
|
|
76
|
+
density: jnp.ndarray,
|
|
77
|
+
momentum: jnp.ndarray,
|
|
78
|
+
geometry: FciGeometry3D,
|
|
79
|
+
params: SolFluxTubeParameters,
|
|
80
|
+
source: jnp.ndarray,
|
|
81
|
+
) -> tuple[jnp.ndarray, jnp.ndarray]:
|
|
82
|
+
"""Parallel-transport RHS ``(dn/dt, dm/dt)`` with Bohm sheath targets."""
|
|
83
|
+
|
|
84
|
+
c_s = float(params.sound_speed)
|
|
85
|
+
dz = jnp.asarray(geometry.spacing.dz, dtype=jnp.float64)
|
|
86
|
+
n = jnp.maximum(density, float(params.density_floor))
|
|
87
|
+
v = momentum / n
|
|
88
|
+
|
|
89
|
+
# Interior faces along z (between adjacent cells).
|
|
90
|
+
interior_n, interior_m = _rusanov(n[:, :, :-1], momentum[:, :, :-1], n[:, :, 1:], momentum[:, :, 1:], c_s)
|
|
91
|
+
|
|
92
|
+
# Bohm sheath outflow at the two targets: |v| >= c_s pointing into the plate.
|
|
93
|
+
v_left = jnp.minimum(v[:, :, 0], -c_s)
|
|
94
|
+
v_right = jnp.maximum(v[:, :, -1], c_s)
|
|
95
|
+
left_n, left_m = _flux(n[:, :, 0], n[:, :, 0] * v_left, c_s)
|
|
96
|
+
right_n, right_m = _flux(n[:, :, -1], n[:, :, -1] * v_right, c_s)
|
|
97
|
+
|
|
98
|
+
face_n = jnp.concatenate([left_n[:, :, None], interior_n, right_n[:, :, None]], axis=2)
|
|
99
|
+
face_m = jnp.concatenate([left_m[:, :, None], interior_m, right_m[:, :, None]], axis=2)
|
|
100
|
+
|
|
101
|
+
d_density = -(face_n[:, :, 1:] - face_n[:, :, :-1]) / dz + source
|
|
102
|
+
d_momentum = -(face_m[:, :, 1:] - face_m[:, :, :-1]) / dz
|
|
103
|
+
return d_density, d_momentum
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def sol_flux_tube_step(density, momentum, geometry, params, source, dt):
|
|
107
|
+
"""One RK4 step of the reduced SOL flux tube (density floored to stay positive)."""
|
|
108
|
+
|
|
109
|
+
def rhs(n, m):
|
|
110
|
+
return sol_flux_tube_rhs(n, m, geometry, params, source)
|
|
111
|
+
|
|
112
|
+
k1n, k1m = rhs(density, momentum)
|
|
113
|
+
k2n, k2m = rhs(density + 0.5 * dt * k1n, momentum + 0.5 * dt * k1m)
|
|
114
|
+
k3n, k3m = rhs(density + 0.5 * dt * k2n, momentum + 0.5 * dt * k2m)
|
|
115
|
+
k4n, k4m = rhs(density + dt * k3n, momentum + dt * k3m)
|
|
116
|
+
density = density + dt / 6.0 * (k1n + 2 * k2n + 2 * k3n + k4n)
|
|
117
|
+
momentum = momentum + dt / 6.0 * (k1m + 2 * k2m + 2 * k3m + k4m)
|
|
118
|
+
return jnp.maximum(density, float(params.density_floor)), momentum
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def sol_flux_tube_run(density, momentum, geometry, params, source, *, dt, steps):
|
|
122
|
+
"""Advance ``steps`` RK4 steps with a jitted ``lax.scan``; return the final state."""
|
|
123
|
+
|
|
124
|
+
@jax.jit
|
|
125
|
+
def _run(n0, m0):
|
|
126
|
+
def body(carry, _):
|
|
127
|
+
n, m = carry
|
|
128
|
+
return sol_flux_tube_step(n, m, geometry, params, source, dt), None
|
|
129
|
+
|
|
130
|
+
(n_final, m_final), _ = jax.lax.scan(body, (n0, m0), None, length=steps)
|
|
131
|
+
return n_final, m_final
|
|
132
|
+
|
|
133
|
+
return _run(density, momentum)
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Reusable four-field stellarator turbulence driver (closed and open field lines).
|
|
2
|
+
|
|
3
|
+
A thin, reusable harness around the validated four-field interchange RK4 step:
|
|
4
|
+
seed a multi-mode density perturbation on a stellarator FCI geometry (rotating
|
|
5
|
+
ellipse, island divertor, ...) and let the curvature drive develop
|
|
6
|
+
turbulence-like interchange dynamics. On an open geometry (endpoint masks set,
|
|
7
|
+
e.g. via ``limiter_radius`` or traced island-divertor masks) a Bohm sheath
|
|
8
|
+
density sink drains the open-endpoint cells each step, so the scrape-off layer
|
|
9
|
+
empties onto the target while the core stays closed.
|
|
10
|
+
|
|
11
|
+
Public pieces, each reusable on its own:
|
|
12
|
+
|
|
13
|
+
- :func:`build_free_decay_boundary_conditions` -- Dirichlet phi / Neumann
|
|
14
|
+
fields at the radial walls, bundled as :class:`FourFieldBoundaryConditions`;
|
|
15
|
+
- :func:`build_four_field_phi_solver` -- the GMRES perpendicular-Laplacian
|
|
16
|
+
inverter used by the phi inversion;
|
|
17
|
+
- :func:`four_field_rk4_step` -- one classic RK4 step of the four-field blob
|
|
18
|
+
RHS (``compute_4field_blob_rhs``), carrying the phi guess between stages;
|
|
19
|
+
- :func:`multi_mode_state` -- the seeded multi-mode initial state;
|
|
20
|
+
- :func:`run_stellarator_turbulence` -- the full loop, returning a
|
|
21
|
+
:class:`TurbulenceRun` with frames and particle/sheath-flux traces.
|
|
22
|
+
|
|
23
|
+
Used by ``tests/test_stellarator_turbulence.py`` and
|
|
24
|
+
``tests/test_island_divertor.py`` (fast physics gates). The example scripts in
|
|
25
|
+
``examples/stellarator/`` show the same anatomy written out step by step.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import NamedTuple
|
|
31
|
+
|
|
32
|
+
import jax
|
|
33
|
+
import jax.numpy as jnp
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
from ..geometry import (
|
|
37
|
+
ConservativeStencilBuilder,
|
|
38
|
+
FciGeometry3D,
|
|
39
|
+
LocalStencilBuilder,
|
|
40
|
+
RegularFaceGeometry3D,
|
|
41
|
+
build_conservative_stencil_from_field,
|
|
42
|
+
build_curvature_coefficients,
|
|
43
|
+
build_local_stencil_from_field,
|
|
44
|
+
)
|
|
45
|
+
from .fci_4_field_rhs import (
|
|
46
|
+
Fci4FieldBlobParameters,
|
|
47
|
+
Fci4FieldState,
|
|
48
|
+
compute_4field_blob_rhs,
|
|
49
|
+
)
|
|
50
|
+
from .fci_boundaries import (
|
|
51
|
+
BC_DIRICHLET,
|
|
52
|
+
BC_NEUMANN,
|
|
53
|
+
BoundaryFaceBC3D,
|
|
54
|
+
CutWallBC3D,
|
|
55
|
+
CutWallGeometry3D,
|
|
56
|
+
FourFieldBoundaryConditions,
|
|
57
|
+
)
|
|
58
|
+
from .fci_operators import PerpLaplacianInverseSolver, build_perp_laplacian_face_projectors
|
|
59
|
+
from .fci_sheath_recycling import compute_fci_sheath_recycling
|
|
60
|
+
|
|
61
|
+
TEMPERATURE = 1.0 # normalized Te = Ti for the sheath sound speed
|
|
62
|
+
SHEATH_DENSITY_FLOOR = 1.0e-4 # positivity floor after the sheath sink
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"TEMPERATURE",
|
|
66
|
+
"SHEATH_DENSITY_FLOOR",
|
|
67
|
+
"FourFieldBoundaryConditions",
|
|
68
|
+
"TurbulenceRun",
|
|
69
|
+
"apply_sheath_sink",
|
|
70
|
+
"build_free_decay_boundary_conditions",
|
|
71
|
+
"build_four_field_phi_solver",
|
|
72
|
+
"four_field_rk4_step",
|
|
73
|
+
"multi_mode_state",
|
|
74
|
+
"radial_dirichlet_face_bc",
|
|
75
|
+
"radial_neumann_face_bc",
|
|
76
|
+
"run_stellarator_turbulence",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def radial_dirichlet_face_bc(geometry: FciGeometry3D) -> BoundaryFaceBC3D:
|
|
81
|
+
"""Zero-Dirichlet at the two radial (x) walls; periodic elsewhere."""
|
|
82
|
+
|
|
83
|
+
face = RegularFaceGeometry3D.unit(geometry)
|
|
84
|
+
return BoundaryFaceBC3D(
|
|
85
|
+
kind_x=jnp.zeros_like(face.x_area, dtype=jnp.int32).at[0].set(BC_DIRICHLET).at[-1].set(BC_DIRICHLET),
|
|
86
|
+
kind_y=jnp.zeros_like(face.y_area, dtype=jnp.int32),
|
|
87
|
+
kind_z=jnp.zeros_like(face.z_area, dtype=jnp.int32),
|
|
88
|
+
value_x=jnp.zeros_like(face.x_area, dtype=jnp.float64),
|
|
89
|
+
value_y=jnp.zeros_like(face.y_area, dtype=jnp.float64),
|
|
90
|
+
value_z=jnp.zeros_like(face.z_area, dtype=jnp.float64),
|
|
91
|
+
mask_x=jnp.zeros_like(face.x_open_mask, dtype=bool).at[0].set(True).at[-1].set(True),
|
|
92
|
+
mask_y=jnp.zeros_like(face.y_open_mask, dtype=bool),
|
|
93
|
+
mask_z=jnp.zeros_like(face.z_open_mask, dtype=bool),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def radial_neumann_face_bc(geometry: FciGeometry3D) -> BoundaryFaceBC3D:
|
|
98
|
+
"""Zero-Neumann at the two radial (x) walls; periodic elsewhere."""
|
|
99
|
+
|
|
100
|
+
face = RegularFaceGeometry3D.unit(geometry)
|
|
101
|
+
return BoundaryFaceBC3D(
|
|
102
|
+
kind_x=jnp.zeros_like(face.x_area, dtype=jnp.int32).at[0].set(BC_NEUMANN).at[-1].set(BC_NEUMANN),
|
|
103
|
+
kind_y=jnp.zeros_like(face.y_area, dtype=jnp.int32),
|
|
104
|
+
kind_z=jnp.zeros_like(face.z_area, dtype=jnp.int32),
|
|
105
|
+
value_x=jnp.zeros_like(face.x_area, dtype=jnp.float64),
|
|
106
|
+
value_y=jnp.zeros_like(face.y_area, dtype=jnp.float64),
|
|
107
|
+
value_z=jnp.zeros_like(face.z_area, dtype=jnp.float64),
|
|
108
|
+
mask_x=jnp.zeros_like(face.x_open_mask, dtype=bool).at[0].set(True).at[-1].set(True),
|
|
109
|
+
mask_y=jnp.zeros_like(face.y_open_mask, dtype=bool),
|
|
110
|
+
mask_z=jnp.zeros_like(face.z_open_mask, dtype=bool),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_free_decay_boundary_conditions(geometry: FciGeometry3D) -> FourFieldBoundaryConditions:
|
|
115
|
+
"""Free-decay walls: Dirichlet phi = 0, zero-Neumann for the evolved fields."""
|
|
116
|
+
|
|
117
|
+
neumann = radial_neumann_face_bc(geometry)
|
|
118
|
+
return FourFieldBoundaryConditions(
|
|
119
|
+
phi_face_bc=radial_dirichlet_face_bc(geometry),
|
|
120
|
+
density_face_bc=neumann,
|
|
121
|
+
omega_face_bc=neumann,
|
|
122
|
+
v_ion_parallel_face_bc=neumann,
|
|
123
|
+
v_electron_parallel_face_bc=neumann,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def build_four_field_phi_solver(
|
|
128
|
+
geometry: FciGeometry3D,
|
|
129
|
+
parameters: Fci4FieldBlobParameters,
|
|
130
|
+
*,
|
|
131
|
+
conservative_stencil_builder: ConservativeStencilBuilder,
|
|
132
|
+
face_projectors: tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray] | None,
|
|
133
|
+
) -> PerpLaplacianInverseSolver:
|
|
134
|
+
"""GMRES inverter for the perpendicular Laplacian used by the phi inversion."""
|
|
135
|
+
|
|
136
|
+
return PerpLaplacianInverseSolver(
|
|
137
|
+
geometry,
|
|
138
|
+
conservative_stencil_builder,
|
|
139
|
+
tol=float(parameters.phi_inversion_tol),
|
|
140
|
+
maxiter=int(parameters.phi_inversion_maxiter),
|
|
141
|
+
restart=int(parameters.phi_inversion_restart),
|
|
142
|
+
face_projectors=face_projectors,
|
|
143
|
+
regular_face_geometry=RegularFaceGeometry3D.unit(geometry),
|
|
144
|
+
cut_wall_geometry=CutWallGeometry3D.empty(),
|
|
145
|
+
cut_wall_bc=CutWallBC3D.empty(),
|
|
146
|
+
periodic_axes=(False, True, True),
|
|
147
|
+
pin_point=None,
|
|
148
|
+
pin_value=0.0,
|
|
149
|
+
project_mean_zero=False,
|
|
150
|
+
target_mean_phi=None,
|
|
151
|
+
regularization_epsilon=0.0,
|
|
152
|
+
gmres_debug=False,
|
|
153
|
+
# Hot-loop configuration: skip the per-solve host-synced residual
|
|
154
|
+
# check so the phi inversion (and any step built on it) can run fully
|
|
155
|
+
# inside jit. The validation harnesses build their own solvers with
|
|
156
|
+
# check_residual=True.
|
|
157
|
+
check_residual=False,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def four_field_rk4_step(
|
|
162
|
+
state: Fci4FieldState,
|
|
163
|
+
*,
|
|
164
|
+
geometry: FciGeometry3D,
|
|
165
|
+
timestep: float,
|
|
166
|
+
parameters: Fci4FieldBlobParameters,
|
|
167
|
+
curvature_coefficients: jnp.ndarray,
|
|
168
|
+
stencil_builder: LocalStencilBuilder,
|
|
169
|
+
conservative_stencil_builder: ConservativeStencilBuilder,
|
|
170
|
+
boundary_conditions: FourFieldBoundaryConditions,
|
|
171
|
+
phi_face_projectors: tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray] | None,
|
|
172
|
+
phi_inverse_solver: PerpLaplacianInverseSolver,
|
|
173
|
+
phi_guess: jnp.ndarray | None = None,
|
|
174
|
+
) -> tuple[Fci4FieldState, jnp.ndarray]:
|
|
175
|
+
"""One classic RK4 step of the four-field blob RHS.
|
|
176
|
+
|
|
177
|
+
The electrostatic potential from each stage seeds the GMRES phi inversion of
|
|
178
|
+
the next, so passing the returned phi back in as ``phi_guess`` keeps the
|
|
179
|
+
inversion warm across steps. Returns ``(next_state, phi_of_last_stage)``.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
common_kwargs = dict(
|
|
183
|
+
geometry=geometry,
|
|
184
|
+
stencil_builder=stencil_builder,
|
|
185
|
+
conservative_stencil_builder=conservative_stencil_builder,
|
|
186
|
+
parameters=parameters,
|
|
187
|
+
curvature_coefficients=curvature_coefficients,
|
|
188
|
+
boundary_conditions=boundary_conditions,
|
|
189
|
+
phi_face_projectors=phi_face_projectors,
|
|
190
|
+
phi_inverse_solver=phi_inverse_solver,
|
|
191
|
+
gmres_debug=False,
|
|
192
|
+
return_phi=True,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
rhs_1, _timings, phi_1 = compute_4field_blob_rhs(state, phi_guess=phi_guess, **common_kwargs)
|
|
196
|
+
k1 = rhs_1.rhs
|
|
197
|
+
stage_1 = state.axpy(k1, scale=0.5 * timestep)
|
|
198
|
+
|
|
199
|
+
rhs_2, _timings, phi_2 = compute_4field_blob_rhs(stage_1, phi_guess=phi_1, **common_kwargs)
|
|
200
|
+
k2 = rhs_2.rhs
|
|
201
|
+
stage_2 = state.axpy(k2, scale=0.5 * timestep)
|
|
202
|
+
|
|
203
|
+
rhs_3, _timings, phi_3 = compute_4field_blob_rhs(stage_2, phi_guess=phi_2, **common_kwargs)
|
|
204
|
+
k3 = rhs_3.rhs
|
|
205
|
+
stage_3 = state.axpy(k3, scale=timestep)
|
|
206
|
+
|
|
207
|
+
rhs_4, _timings, phi_4 = compute_4field_blob_rhs(stage_3, phi_guess=phi_3, **common_kwargs)
|
|
208
|
+
k4 = rhs_4.rhs
|
|
209
|
+
|
|
210
|
+
increment = Fci4FieldState(
|
|
211
|
+
density=(k1.density + 2.0 * k2.density + 2.0 * k3.density + k4.density) / 6.0,
|
|
212
|
+
omega=(k1.omega + 2.0 * k2.omega + 2.0 * k3.omega + k4.omega) / 6.0,
|
|
213
|
+
v_ion_parallel=(
|
|
214
|
+
k1.v_ion_parallel + 2.0 * k2.v_ion_parallel + 2.0 * k3.v_ion_parallel + k4.v_ion_parallel
|
|
215
|
+
)
|
|
216
|
+
/ 6.0,
|
|
217
|
+
v_electron_parallel=(
|
|
218
|
+
k1.v_electron_parallel
|
|
219
|
+
+ 2.0 * k2.v_electron_parallel
|
|
220
|
+
+ 2.0 * k3.v_electron_parallel
|
|
221
|
+
+ k4.v_electron_parallel
|
|
222
|
+
)
|
|
223
|
+
/ 6.0,
|
|
224
|
+
)
|
|
225
|
+
return state.axpy(increment, scale=timestep), phi_4
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def apply_sheath_sink(
|
|
229
|
+
state: Fci4FieldState,
|
|
230
|
+
geometry: FciGeometry3D,
|
|
231
|
+
dt: float,
|
|
232
|
+
*,
|
|
233
|
+
temperature: float = TEMPERATURE,
|
|
234
|
+
) -> tuple[Fci4FieldState, float]:
|
|
235
|
+
"""Bohm sheath density sink on the open-endpoint cells (explicit Euler).
|
|
236
|
+
|
|
237
|
+
Returns the drained state and the total ion particle loss rate of the
|
|
238
|
+
*pre-sink* state (the instantaneous sheath flux to the target).
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
temperature_field = jnp.full(geometry.shape, float(temperature))
|
|
242
|
+
sheath = compute_fci_sheath_recycling(state.density, temperature_field, temperature_field, geometry.maps)
|
|
243
|
+
drained = Fci4FieldState(
|
|
244
|
+
density=jnp.maximum(state.density - dt * sheath.ion_particle_loss, SHEATH_DENSITY_FLOOR),
|
|
245
|
+
omega=state.omega,
|
|
246
|
+
v_ion_parallel=state.v_ion_parallel,
|
|
247
|
+
v_electron_parallel=state.v_electron_parallel,
|
|
248
|
+
)
|
|
249
|
+
return drained, float(sheath.total_ion_particle_loss)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def multi_mode_state(
|
|
253
|
+
geometry: FciGeometry3D,
|
|
254
|
+
*,
|
|
255
|
+
amplitude: float = 0.08,
|
|
256
|
+
seed: int = 0,
|
|
257
|
+
modes: tuple[tuple[int, int], ...] = ((2, 1), (3, 2), (4, 1), (5, 3)),
|
|
258
|
+
) -> Fci4FieldState:
|
|
259
|
+
"""Seed a random-phase multi-mode density perturbation with a radial envelope."""
|
|
260
|
+
|
|
261
|
+
rng = np.random.default_rng(seed)
|
|
262
|
+
x = np.asarray(geometry.grid.x.centers)[:, None, None]
|
|
263
|
+
theta = np.asarray(geometry.grid.y.centers)[None, :, None]
|
|
264
|
+
zeta = np.asarray(geometry.grid.z.centers)[None, None, :]
|
|
265
|
+
x_norm = (x - x.min()) / (x.max() - x.min())
|
|
266
|
+
envelope = np.sin(np.pi * x_norm)
|
|
267
|
+
perturbation = np.zeros(geometry.shape)
|
|
268
|
+
for m, n in modes:
|
|
269
|
+
perturbation += rng.uniform(0.5, 1.0) * np.cos(m * theta + n * zeta + rng.uniform(0, 2 * np.pi))
|
|
270
|
+
density = 1.0 + amplitude * envelope * perturbation
|
|
271
|
+
zeros = jnp.zeros(geometry.shape, dtype=jnp.float64)
|
|
272
|
+
return Fci4FieldState(density=jnp.asarray(density), omega=zeros,
|
|
273
|
+
v_ion_parallel=zeros, v_electron_parallel=zeros)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class TurbulenceRun(NamedTuple):
|
|
277
|
+
"""Density/vorticity frames plus particle-content and sheath-flux traces."""
|
|
278
|
+
|
|
279
|
+
density_frames: np.ndarray # (n_frames, nx, ny, nz), float32
|
|
280
|
+
omega_frames: np.ndarray
|
|
281
|
+
times: np.ndarray
|
|
282
|
+
particle_content: np.ndarray # Jacobian-weighted total density per frame
|
|
283
|
+
target_flux: np.ndarray # total sheath ion loss per frame (0 when closed)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def run_stellarator_turbulence(geometry: FciGeometry3D, *, steps: int, dt: float,
|
|
287
|
+
amplitude: float = 0.08, seed: int = 0,
|
|
288
|
+
sheath_sink: bool = False, frame_stride: int = 1) -> TurbulenceRun:
|
|
289
|
+
"""Advance the seeded four-field state; optionally drain open endpoints."""
|
|
290
|
+
|
|
291
|
+
parameters = Fci4FieldBlobParameters(rho_star=1.0, phi_inversion_tol=5.0e-5,
|
|
292
|
+
phi_inversion_maxiter=100, phi_inversion_restart=200)
|
|
293
|
+
stencil_builder = LocalStencilBuilder(build_local_stencil_from_field.build_fn)
|
|
294
|
+
conservative_builder = ConservativeStencilBuilder(build_conservative_stencil_from_field.build_fn)
|
|
295
|
+
boundary_conditions = build_free_decay_boundary_conditions(geometry)
|
|
296
|
+
curvature = build_curvature_coefficients(geometry, periodic_axes=(False, True, True))
|
|
297
|
+
projectors = build_perp_laplacian_face_projectors(geometry)
|
|
298
|
+
phi_solver = build_four_field_phi_solver(
|
|
299
|
+
geometry, parameters,
|
|
300
|
+
conservative_stencil_builder=conservative_builder, face_projectors=projectors,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
temperature = jnp.full(geometry.shape, TEMPERATURE)
|
|
304
|
+
jacobian = np.asarray(geometry.cell_metric.J)
|
|
305
|
+
state = multi_mode_state(geometry, amplitude=amplitude, seed=seed)
|
|
306
|
+
phi_guess = jnp.zeros(geometry.shape, dtype=jnp.float64)
|
|
307
|
+
density_frames, omega_frames, times, content, flux = [], [], [], [], []
|
|
308
|
+
|
|
309
|
+
# The whole RK4 step (four RHS evaluations, each with a GMRES phi
|
|
310
|
+
# inversion) is sync-free, so compile it once and run it as a single
|
|
311
|
+
# XLA program.
|
|
312
|
+
jitted_step = jax.jit(
|
|
313
|
+
lambda current, guess: four_field_rk4_step(
|
|
314
|
+
current, geometry=geometry, timestep=dt, parameters=parameters,
|
|
315
|
+
curvature_coefficients=curvature, stencil_builder=stencil_builder,
|
|
316
|
+
conservative_stencil_builder=conservative_builder,
|
|
317
|
+
boundary_conditions=boundary_conditions, phi_face_projectors=projectors,
|
|
318
|
+
phi_inverse_solver=phi_solver, phi_guess=guess,
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def record(step_index, current):
|
|
323
|
+
density = np.asarray(current.density, dtype=np.float32)
|
|
324
|
+
density_frames.append(density)
|
|
325
|
+
omega_frames.append(np.asarray(current.omega, dtype=np.float32))
|
|
326
|
+
times.append(step_index * dt)
|
|
327
|
+
content.append(float(np.sum(np.asarray(current.density, dtype=np.float64) * jacobian)))
|
|
328
|
+
if sheath_sink:
|
|
329
|
+
sheath = compute_fci_sheath_recycling(current.density, temperature, temperature, geometry.maps)
|
|
330
|
+
flux.append(float(sheath.total_ion_particle_loss))
|
|
331
|
+
else:
|
|
332
|
+
flux.append(0.0)
|
|
333
|
+
|
|
334
|
+
record(0, state)
|
|
335
|
+
for step_index in range(1, steps + 1):
|
|
336
|
+
state, phi_guess = jitted_step(state, phi_guess)
|
|
337
|
+
if sheath_sink:
|
|
338
|
+
state, _loss = apply_sheath_sink(state, geometry, dt)
|
|
339
|
+
if step_index % frame_stride == 0 or step_index == steps:
|
|
340
|
+
record(step_index, state)
|
|
341
|
+
|
|
342
|
+
return TurbulenceRun(np.stack(density_frames), np.stack(omega_frames),
|
|
343
|
+
np.asarray(times), np.asarray(content), np.asarray(flux))
|
drbx/native/transport.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import jax.numpy as jnp
|
|
6
|
+
from jax.scipy.linalg import expm
|
|
7
|
+
|
|
8
|
+
from ..runtime import runtime_jax_dtype
|
|
9
|
+
from .mesh import StructuredMesh, apply_field_boundaries
|
|
10
|
+
from .metrics import StructuredMetrics
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class OneStepDiffusionResult:
|
|
15
|
+
density: jnp.ndarray
|
|
16
|
+
pressure: jnp.ndarray
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class DiffusionHistoryResult:
|
|
21
|
+
density_history: jnp.ndarray
|
|
22
|
+
pressure_history: jnp.ndarray
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def advance_anomalous_diffusion_one_step(
|
|
26
|
+
density: jnp.ndarray,
|
|
27
|
+
pressure: jnp.ndarray,
|
|
28
|
+
*,
|
|
29
|
+
mesh: StructuredMesh,
|
|
30
|
+
metrics: StructuredMetrics,
|
|
31
|
+
anomalous_D: float,
|
|
32
|
+
density_boundary: str,
|
|
33
|
+
pressure_boundary: str,
|
|
34
|
+
timestep: float,
|
|
35
|
+
) -> OneStepDiffusionResult:
|
|
36
|
+
dtype = runtime_jax_dtype()
|
|
37
|
+
if mesh.nz != 1:
|
|
38
|
+
raise NotImplementedError("Native one-step anomalous diffusion currently supports nz = 1 only.")
|
|
39
|
+
if density_boundary.strip().lower() != "neumann" or pressure_boundary.strip().lower() != "neumann":
|
|
40
|
+
raise NotImplementedError("Native one-step anomalous diffusion currently supports Neumann X boundaries only.")
|
|
41
|
+
tolerance = 1e-6 if dtype == jnp.float32 else 1e-12
|
|
42
|
+
if not jnp.allclose(density, pressure, rtol=tolerance, atol=tolerance):
|
|
43
|
+
raise NotImplementedError(
|
|
44
|
+
"Native one-step anomalous diffusion currently requires identical density and pressure initial states."
|
|
45
|
+
)
|
|
46
|
+
if not jnp.allclose(metrics.g23, 0.0, rtol=tolerance, atol=tolerance):
|
|
47
|
+
raise NotImplementedError("Native one-step anomalous diffusion currently supports g23 = 0 structured metrics only.")
|
|
48
|
+
|
|
49
|
+
operator = _build_radial_diffusion_operator(mesh, metrics, anomalous_D)
|
|
50
|
+
propagator = expm(operator * timestep)
|
|
51
|
+
density_next = _advance_field_with_operator(density, propagator, mesh, boundary_kind=density_boundary)
|
|
52
|
+
pressure_next = _advance_field_with_operator(pressure, propagator, mesh, boundary_kind=pressure_boundary)
|
|
53
|
+
return OneStepDiffusionResult(density=density_next, pressure=pressure_next)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def advance_anomalous_diffusion_history(
|
|
57
|
+
density: jnp.ndarray,
|
|
58
|
+
pressure: jnp.ndarray,
|
|
59
|
+
*,
|
|
60
|
+
mesh: StructuredMesh,
|
|
61
|
+
metrics: StructuredMetrics,
|
|
62
|
+
anomalous_D: float,
|
|
63
|
+
density_boundary: str,
|
|
64
|
+
pressure_boundary: str,
|
|
65
|
+
timestep: float,
|
|
66
|
+
steps: int,
|
|
67
|
+
) -> DiffusionHistoryResult:
|
|
68
|
+
if steps < 0:
|
|
69
|
+
raise ValueError("steps must be non-negative")
|
|
70
|
+
if mesh.nz != 1:
|
|
71
|
+
raise NotImplementedError("Native anomalous diffusion history currently supports nz = 1 only.")
|
|
72
|
+
if density_boundary.strip().lower() != "neumann" or pressure_boundary.strip().lower() != "neumann":
|
|
73
|
+
raise NotImplementedError("Native anomalous diffusion history currently supports Neumann X boundaries only.")
|
|
74
|
+
|
|
75
|
+
dtype = runtime_jax_dtype()
|
|
76
|
+
operator = _build_radial_diffusion_operator(mesh, metrics, anomalous_D)
|
|
77
|
+
propagator = expm(operator * timestep)
|
|
78
|
+
density_history = [jnp.asarray(density, dtype=dtype)]
|
|
79
|
+
pressure_history = [jnp.asarray(pressure, dtype=dtype)]
|
|
80
|
+
current_density = density_history[0]
|
|
81
|
+
current_pressure = pressure_history[0]
|
|
82
|
+
for _ in range(steps):
|
|
83
|
+
current_density = _advance_field_with_operator(current_density, propagator, mesh, boundary_kind=density_boundary)
|
|
84
|
+
current_pressure = _advance_field_with_operator(current_pressure, propagator, mesh, boundary_kind=pressure_boundary)
|
|
85
|
+
density_history.append(current_density)
|
|
86
|
+
pressure_history.append(current_pressure)
|
|
87
|
+
return DiffusionHistoryResult(
|
|
88
|
+
density_history=jnp.stack(density_history, axis=0),
|
|
89
|
+
pressure_history=jnp.stack(pressure_history, axis=0),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _build_radial_diffusion_operator(
|
|
94
|
+
mesh: StructuredMesh,
|
|
95
|
+
metrics: StructuredMetrics,
|
|
96
|
+
anomalous_D: float,
|
|
97
|
+
) -> jnp.ndarray:
|
|
98
|
+
y_index = mesh.ystart
|
|
99
|
+
z_index = 0
|
|
100
|
+
dx = metrics.dx[:, y_index, z_index]
|
|
101
|
+
J = metrics.J[:, y_index, z_index]
|
|
102
|
+
g11 = metrics.g11[:, y_index, z_index]
|
|
103
|
+
dtype = runtime_jax_dtype()
|
|
104
|
+
|
|
105
|
+
interior_nx = mesh.xend - mesh.xstart + 1
|
|
106
|
+
matrix = jnp.zeros((interior_nx, interior_nx), dtype=dtype)
|
|
107
|
+
|
|
108
|
+
for global_index in range(mesh.xstart, mesh.xend):
|
|
109
|
+
left = global_index - mesh.xstart
|
|
110
|
+
right = left + 1
|
|
111
|
+
face_coef = anomalous_D * (J[global_index] * g11[global_index] + J[global_index + 1] * g11[global_index + 1])
|
|
112
|
+
face_coef /= dx[global_index] + dx[global_index + 1]
|
|
113
|
+
|
|
114
|
+
matrix = matrix.at[left, left].add(-face_coef / (dx[global_index] * J[global_index]))
|
|
115
|
+
matrix = matrix.at[left, right].add(face_coef / (dx[global_index] * J[global_index]))
|
|
116
|
+
matrix = matrix.at[right, left].add(face_coef / (dx[global_index + 1] * J[global_index + 1]))
|
|
117
|
+
matrix = matrix.at[right, right].add(-face_coef / (dx[global_index + 1] * J[global_index + 1]))
|
|
118
|
+
|
|
119
|
+
return matrix
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _advance_field_with_operator(
|
|
123
|
+
field: jnp.ndarray,
|
|
124
|
+
propagator: jnp.ndarray,
|
|
125
|
+
mesh: StructuredMesh,
|
|
126
|
+
*,
|
|
127
|
+
boundary_kind: str,
|
|
128
|
+
) -> jnp.ndarray:
|
|
129
|
+
result = jnp.asarray(field, dtype=runtime_jax_dtype())
|
|
130
|
+
interior = result[mesh.xstart : mesh.xend + 1, mesh.ystart : mesh.yend + 1, 0]
|
|
131
|
+
updated = propagator @ interior
|
|
132
|
+
result = result.at[mesh.xstart : mesh.xend + 1, mesh.ystart : mesh.yend + 1, 0].set(updated)
|
|
133
|
+
result = apply_field_boundaries(result, mesh, x_boundary=boundary_kind)
|
|
134
|
+
return result
|
drbx/native/units.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..config.normalization import ELEMENTARY_CHARGE, PROTON_MASS
|
|
4
|
+
from ..runtime.run_config import RunConfiguration
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def resolved_dataset_scalars(run_config: RunConfiguration) -> dict[str, float]:
|
|
8
|
+
if run_config.normalization is not None:
|
|
9
|
+
normalization = run_config.normalization
|
|
10
|
+
return {
|
|
11
|
+
"Nnorm": normalization.Nnorm,
|
|
12
|
+
"Tnorm": normalization.Tnorm,
|
|
13
|
+
"Bnorm": normalization.Bnorm,
|
|
14
|
+
"Cs0": normalization.Cs0,
|
|
15
|
+
"Omega_ci": normalization.Omega_ci,
|
|
16
|
+
"rho_s0": normalization.rho_s0,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Nnorm = float(run_config.model_scalars.get("Nnorm", 1.0e19))
|
|
20
|
+
Tnorm = float(run_config.model_scalars.get("Tnorm", 100.0))
|
|
21
|
+
Bnorm = float(run_config.model_scalars.get("Bnorm", 1.0))
|
|
22
|
+
Cs0 = float((ELEMENTARY_CHARGE * Tnorm / PROTON_MASS) ** 0.5)
|
|
23
|
+
Omega_ci = float(ELEMENTARY_CHARGE * Bnorm / PROTON_MASS)
|
|
24
|
+
rho_s0 = float(Cs0 / Omega_ci)
|
|
25
|
+
return {
|
|
26
|
+
"Nnorm": Nnorm,
|
|
27
|
+
"Tnorm": Tnorm,
|
|
28
|
+
"Bnorm": Bnorm,
|
|
29
|
+
"Cs0": Cs0,
|
|
30
|
+
"Omega_ci": Omega_ci,
|
|
31
|
+
"rho_s0": rho_s0,
|
|
32
|
+
}
|