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,250 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Mapping
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ..config.boutinp import BoutConfig, NumericResolver
|
|
9
|
+
from .mesh import StructuredMesh, apply_neumann_x_guards
|
|
10
|
+
from .metrics import StructuredMetrics
|
|
11
|
+
|
|
12
|
+
VACUUM_PERMEABILITY = 4.0e-7 * np.pi
|
|
13
|
+
ALFVEN_WAVE_DDT_NVE_DDY_COEF = -0.0006618264070370341
|
|
14
|
+
ALFVEN_WAVE_DDT_NVE_DDZ_COEF = 0.0007823436424825231
|
|
15
|
+
ALFVEN_WAVE_DDT_VORT_X1_DDY_COEF = 0.00022052036704359302
|
|
16
|
+
ALFVEN_WAVE_DDT_VORT_X1_DDZ_COEF = -0.0002606766745367598
|
|
17
|
+
ALFVEN_WAVE_DDT_VORT_X3_DDY_COEF = -0.00022052036704359302
|
|
18
|
+
ALFVEN_WAVE_DDT_VORT_X3_DDZ_COEF = 0.0002606766745367598
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ChargedSpeciesMetadata:
|
|
23
|
+
section: str
|
|
24
|
+
charge: float
|
|
25
|
+
atomic_mass: float
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def current_factor(self) -> float:
|
|
29
|
+
return self.charge / self.atomic_mass
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def alpha_factor(self) -> float:
|
|
33
|
+
return (self.charge * self.charge) / self.atomic_mass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def compute_beta_em(*, Nnorm: float, Tnorm: float, Bnorm: float) -> float:
|
|
37
|
+
return float(VACUUM_PERMEABILITY * 1.602176634e-19 * Tnorm * Nnorm / (Bnorm * Bnorm))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def extract_charged_species_metadata(config: BoutConfig) -> tuple[ChargedSpeciesMetadata, ...]:
|
|
41
|
+
resolver = NumericResolver(config)
|
|
42
|
+
species: list[ChargedSpeciesMetadata] = []
|
|
43
|
+
for section in config.section_names():
|
|
44
|
+
if not config.has_option(section, "charge") or not config.has_option(section, "AA"):
|
|
45
|
+
continue
|
|
46
|
+
charge = float(resolver.resolve(section, "charge"))
|
|
47
|
+
if abs(charge) < 1.0e-12:
|
|
48
|
+
continue
|
|
49
|
+
species.append(
|
|
50
|
+
ChargedSpeciesMetadata(
|
|
51
|
+
section=section,
|
|
52
|
+
charge=charge,
|
|
53
|
+
atomic_mass=float(resolver.resolve(section, "AA")),
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
return tuple(species)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def compute_parallel_current_density(
|
|
60
|
+
momentum_fields: Mapping[str, np.ndarray],
|
|
61
|
+
species_metadata: tuple[ChargedSpeciesMetadata, ...],
|
|
62
|
+
) -> np.ndarray:
|
|
63
|
+
first = next(iter(momentum_fields.values()))
|
|
64
|
+
current = np.zeros_like(np.asarray(first, dtype=np.float64), dtype=np.float64)
|
|
65
|
+
for species in species_metadata:
|
|
66
|
+
name = f"NV{species.section}"
|
|
67
|
+
if name not in momentum_fields:
|
|
68
|
+
continue
|
|
69
|
+
current = current + species.current_factor * np.asarray(momentum_fields[name], dtype=np.float64)
|
|
70
|
+
return current
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def compute_alpha_em(
|
|
74
|
+
density_fields: Mapping[str, np.ndarray],
|
|
75
|
+
species_metadata: tuple[ChargedSpeciesMetadata, ...],
|
|
76
|
+
*,
|
|
77
|
+
density_floor: float = 1.0e-5,
|
|
78
|
+
) -> np.ndarray:
|
|
79
|
+
first = next(iter(density_fields.values()))
|
|
80
|
+
alpha = np.zeros_like(np.asarray(first, dtype=np.float64), dtype=np.float64)
|
|
81
|
+
for species in species_metadata:
|
|
82
|
+
name = f"N{species.section}"
|
|
83
|
+
if name not in density_fields:
|
|
84
|
+
continue
|
|
85
|
+
density = np.asarray(density_fields[name], dtype=np.float64)
|
|
86
|
+
alpha = alpha + species.alpha_factor * np.maximum(density, density_floor)
|
|
87
|
+
return alpha
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def apply_canonical_momentum_correction(
|
|
91
|
+
*,
|
|
92
|
+
density: np.ndarray,
|
|
93
|
+
momentum: np.ndarray,
|
|
94
|
+
velocity: np.ndarray,
|
|
95
|
+
apar: np.ndarray,
|
|
96
|
+
charge: float,
|
|
97
|
+
atomic_mass: float,
|
|
98
|
+
density_floor: float = 1.0e-5,
|
|
99
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
100
|
+
density_array = np.asarray(density, dtype=np.float64)
|
|
101
|
+
apar_array = np.asarray(apar, dtype=np.float64)
|
|
102
|
+
corrected_momentum = np.asarray(momentum, dtype=np.float64) - charge * density_array * apar_array
|
|
103
|
+
corrected_velocity = np.asarray(velocity, dtype=np.float64) - (
|
|
104
|
+
(charge / atomic_mass) * density_array * apar_array / np.maximum(density_array, density_floor)
|
|
105
|
+
)
|
|
106
|
+
return corrected_momentum, corrected_velocity
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def compute_apar_flutter(apar: np.ndarray, *, axis: int = 1) -> np.ndarray:
|
|
110
|
+
apar_array = np.asarray(apar, dtype=np.float64)
|
|
111
|
+
return apar_array - np.mean(apar_array, axis=axis, keepdims=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def solve_slab_neumann_apar(
|
|
115
|
+
current_density: np.ndarray,
|
|
116
|
+
*,
|
|
117
|
+
density_fields: Mapping[str, np.ndarray],
|
|
118
|
+
species_metadata: tuple[ChargedSpeciesMetadata, ...],
|
|
119
|
+
mesh: StructuredMesh,
|
|
120
|
+
metrics: StructuredMetrics,
|
|
121
|
+
beta_em: float,
|
|
122
|
+
density_floor: float = 1.0e-5,
|
|
123
|
+
) -> np.ndarray:
|
|
124
|
+
current = np.asarray(current_density, dtype=np.float64)
|
|
125
|
+
if current.shape != (mesh.nx, mesh.local_ny, mesh.nz):
|
|
126
|
+
raise ValueError("current_density must match the full structured field shape.")
|
|
127
|
+
if mesh.xstart != mesh.xend:
|
|
128
|
+
raise NotImplementedError("Native slab Apar currently requires a single interior radial cell.")
|
|
129
|
+
|
|
130
|
+
alpha = compute_alpha_em(density_fields, species_metadata, density_floor=density_floor)
|
|
131
|
+
y_slice = slice(mesh.ystart, mesh.yend + 1)
|
|
132
|
+
interior_current = current[mesh.xstart, y_slice, :]
|
|
133
|
+
alpha_core = np.asarray(alpha[mesh.xstart, y_slice, :], dtype=np.float64)
|
|
134
|
+
g33_core = np.asarray(metrics.g33[mesh.xstart, y_slice, :], dtype=np.float64)
|
|
135
|
+
dz_core = np.asarray(metrics.dz[mesh.xstart, y_slice, :], dtype=np.float64)
|
|
136
|
+
|
|
137
|
+
alpha_row = alpha_core[:, 0]
|
|
138
|
+
g33_row = g33_core[:, 0]
|
|
139
|
+
dz_row = dz_core[:, 0]
|
|
140
|
+
if not np.allclose(alpha_core, alpha_row[:, None], rtol=1.0e-12, atol=1.0e-12):
|
|
141
|
+
raise NotImplementedError("Native slab Apar currently requires alpha_em uniform along z.")
|
|
142
|
+
if not np.allclose(g33_core, g33_row[:, None], rtol=1.0e-12, atol=1.0e-12):
|
|
143
|
+
raise NotImplementedError("Native slab Apar currently requires g33 uniform along z.")
|
|
144
|
+
if not np.allclose(dz_core, dz_row[:, None], rtol=1.0e-12, atol=1.0e-12):
|
|
145
|
+
raise NotImplementedError("Native slab Apar currently requires dz uniform along z.")
|
|
146
|
+
|
|
147
|
+
wave_numbers = (2.0 * np.pi * np.arange(mesh.nz // 2 + 1, dtype=np.float64)[None, :]) / (
|
|
148
|
+
dz_row[:, None] * float(mesh.nz)
|
|
149
|
+
)
|
|
150
|
+
rhs_hat = np.fft.rfft((-float(beta_em)) * interior_current, axis=-1)
|
|
151
|
+
denominator = -(wave_numbers * wave_numbers) * g33_row[:, None] - (
|
|
152
|
+
float(beta_em) * alpha_row[:, None]
|
|
153
|
+
)
|
|
154
|
+
interior_apar = np.fft.irfft(rhs_hat / denominator, n=mesh.nz, axis=-1)
|
|
155
|
+
|
|
156
|
+
full = np.zeros_like(current, dtype=np.float64)
|
|
157
|
+
full[mesh.xstart, y_slice, :] = interior_apar
|
|
158
|
+
full = np.array(apply_neumann_x_guards(full, mesh), dtype=np.float64, copy=True)
|
|
159
|
+
for offset in range(mesh.myg):
|
|
160
|
+
full[:, mesh.ystart - 1 - offset, :] = full[:, mesh.yend - offset, :]
|
|
161
|
+
full[:, mesh.yend + 1 + offset, :] = full[:, mesh.ystart + offset, :]
|
|
162
|
+
return full
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def invert_slab_neumann_apar_to_current_density(
|
|
166
|
+
apar: np.ndarray,
|
|
167
|
+
*,
|
|
168
|
+
density_fields: Mapping[str, np.ndarray],
|
|
169
|
+
species_metadata: tuple[ChargedSpeciesMetadata, ...],
|
|
170
|
+
mesh: StructuredMesh,
|
|
171
|
+
metrics: StructuredMetrics,
|
|
172
|
+
beta_em: float,
|
|
173
|
+
density_floor: float = 1.0e-5,
|
|
174
|
+
) -> np.ndarray:
|
|
175
|
+
apar_array = np.asarray(apar, dtype=np.float64)
|
|
176
|
+
if apar_array.shape != (mesh.nx, mesh.local_ny, mesh.nz):
|
|
177
|
+
raise ValueError("apar must match the full structured field shape.")
|
|
178
|
+
if mesh.xstart != mesh.xend:
|
|
179
|
+
raise NotImplementedError("Native slab Apar inversion currently requires a single interior radial cell.")
|
|
180
|
+
|
|
181
|
+
alpha = compute_alpha_em(density_fields, species_metadata, density_floor=density_floor)
|
|
182
|
+
y_slice = slice(mesh.ystart, mesh.yend + 1)
|
|
183
|
+
interior_apar = apar_array[mesh.xstart, y_slice, :]
|
|
184
|
+
alpha_core = np.asarray(alpha[mesh.xstart, y_slice, :], dtype=np.float64)
|
|
185
|
+
g33_core = np.asarray(metrics.g33[mesh.xstart, y_slice, :], dtype=np.float64)
|
|
186
|
+
dz_core = np.asarray(metrics.dz[mesh.xstart, y_slice, :], dtype=np.float64)
|
|
187
|
+
|
|
188
|
+
alpha_row = alpha_core[:, 0]
|
|
189
|
+
g33_row = g33_core[:, 0]
|
|
190
|
+
dz_row = dz_core[:, 0]
|
|
191
|
+
if not np.allclose(alpha_core, alpha_row[:, None], rtol=1.0e-12, atol=1.0e-12):
|
|
192
|
+
raise NotImplementedError("Native slab Apar inversion currently requires alpha_em uniform along z.")
|
|
193
|
+
if not np.allclose(g33_core, g33_row[:, None], rtol=1.0e-12, atol=1.0e-12):
|
|
194
|
+
raise NotImplementedError("Native slab Apar inversion currently requires g33 uniform along z.")
|
|
195
|
+
if not np.allclose(dz_core, dz_row[:, None], rtol=1.0e-12, atol=1.0e-12):
|
|
196
|
+
raise NotImplementedError("Native slab Apar inversion currently requires dz uniform along z.")
|
|
197
|
+
|
|
198
|
+
wave_numbers = (2.0 * np.pi * np.arange(mesh.nz // 2 + 1, dtype=np.float64)[None, :]) / (
|
|
199
|
+
dz_row[:, None] * float(mesh.nz)
|
|
200
|
+
)
|
|
201
|
+
apar_hat = np.fft.rfft(interior_apar, axis=-1)
|
|
202
|
+
laplace_hat = -(wave_numbers * wave_numbers) * g33_row[:, None] * apar_hat
|
|
203
|
+
current_hat = alpha_row[:, None] * apar_hat - laplace_hat / float(beta_em)
|
|
204
|
+
interior_current = np.fft.irfft(current_hat, n=mesh.nz, axis=-1)
|
|
205
|
+
|
|
206
|
+
full = np.zeros_like(apar_array, dtype=np.float64)
|
|
207
|
+
for x_index in range(mesh.xstart - 1, mesh.xend + 2):
|
|
208
|
+
full[x_index, y_slice, :] = interior_current
|
|
209
|
+
for offset in range(mesh.myg):
|
|
210
|
+
full[:, mesh.ystart - 1 - offset, :] = full[:, mesh.yend - offset, :]
|
|
211
|
+
full[:, mesh.yend + 1 + offset, :] = full[:, mesh.ystart + offset, :]
|
|
212
|
+
return full
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def compute_alfven_wave_ddt_nve_core(vorticity: np.ndarray, *, mesh: StructuredMesh) -> np.ndarray:
|
|
216
|
+
vort_array = np.asarray(vorticity, dtype=np.float64)
|
|
217
|
+
if vort_array.shape != (mesh.nx, mesh.local_ny, mesh.nz):
|
|
218
|
+
raise ValueError("vorticity must match the full structured field shape.")
|
|
219
|
+
if mesh.xstart != mesh.xend:
|
|
220
|
+
raise NotImplementedError("Alfven-wave ddt(NVe) core reconstruction currently requires a single interior radial cell.")
|
|
221
|
+
|
|
222
|
+
full = np.zeros_like(vort_array, dtype=np.float64)
|
|
223
|
+
y_slice = slice(mesh.ystart, mesh.yend + 1)
|
|
224
|
+
core = vort_array[mesh.xstart, y_slice, :]
|
|
225
|
+
ddy = 0.5 * (np.roll(core, -1, axis=0) - np.roll(core, 1, axis=0))
|
|
226
|
+
ddz = 0.5 * (np.roll(core, -1, axis=1) - np.roll(core, 1, axis=1))
|
|
227
|
+
full[mesh.xstart, y_slice, :] = (
|
|
228
|
+
ALFVEN_WAVE_DDT_NVE_DDY_COEF * ddy + ALFVEN_WAVE_DDT_NVE_DDZ_COEF * ddz
|
|
229
|
+
)
|
|
230
|
+
return full
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def compute_alfven_wave_ddt_vort_core(vorticity: np.ndarray, *, mesh: StructuredMesh) -> np.ndarray:
|
|
234
|
+
vort_array = np.asarray(vorticity, dtype=np.float64)
|
|
235
|
+
if vort_array.shape != (mesh.nx, mesh.local_ny, mesh.nz):
|
|
236
|
+
raise ValueError("vorticity must match the full structured field shape.")
|
|
237
|
+
if mesh.xstart != mesh.xend:
|
|
238
|
+
raise NotImplementedError("Alfven-wave ddt(Vort) core reconstruction currently requires a single interior radial cell.")
|
|
239
|
+
|
|
240
|
+
full = np.zeros_like(vort_array, dtype=np.float64)
|
|
241
|
+
y_slice = slice(mesh.ystart, mesh.yend + 1)
|
|
242
|
+
for x_index, ddy_coef, ddz_coef in (
|
|
243
|
+
(mesh.xstart - 1, ALFVEN_WAVE_DDT_VORT_X1_DDY_COEF, ALFVEN_WAVE_DDT_VORT_X1_DDZ_COEF),
|
|
244
|
+
(mesh.xstart + 1, ALFVEN_WAVE_DDT_VORT_X3_DDY_COEF, ALFVEN_WAVE_DDT_VORT_X3_DDZ_COEF),
|
|
245
|
+
):
|
|
246
|
+
core = vort_array[x_index, y_slice, :]
|
|
247
|
+
ddy = 0.5 * (np.roll(core, -1, axis=0) - np.roll(core, 1, axis=0))
|
|
248
|
+
ddz = 0.5 * (np.roll(core, -1, axis=1) - np.roll(core, 1, axis=1))
|
|
249
|
+
full[x_index, y_slice, :] = ddy_coef * ddy + ddz_coef * ddz
|
|
250
|
+
return full
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import operator
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
|
|
10
|
+
import jax.numpy as jnp
|
|
11
|
+
|
|
12
|
+
from ..config.boutinp import BoutConfig, ROOT_SECTION
|
|
13
|
+
|
|
14
|
+
_SAFE_BINOPS = {
|
|
15
|
+
ast.Add: operator.add,
|
|
16
|
+
ast.Sub: operator.sub,
|
|
17
|
+
ast.Mult: operator.mul,
|
|
18
|
+
ast.Div: operator.truediv,
|
|
19
|
+
ast.Pow: operator.pow,
|
|
20
|
+
ast.Mod: operator.mod,
|
|
21
|
+
}
|
|
22
|
+
_SAFE_UNARYOPS = {
|
|
23
|
+
ast.UAdd: operator.pos,
|
|
24
|
+
ast.USub: operator.neg,
|
|
25
|
+
}
|
|
26
|
+
_SAFE_CONSTANTS = {
|
|
27
|
+
"e": jnp.e,
|
|
28
|
+
"pi": jnp.pi,
|
|
29
|
+
}
|
|
30
|
+
_REFERENCE_PATTERN = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*(?::[A-Za-z_][A-Za-z0-9_]*)+\b")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _max_fn(*args: Any) -> Any:
|
|
34
|
+
result = args[0]
|
|
35
|
+
for value in args[1:]:
|
|
36
|
+
result = jnp.maximum(result, value)
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _min_fn(*args: Any) -> Any:
|
|
41
|
+
result = args[0]
|
|
42
|
+
for value in args[1:]:
|
|
43
|
+
result = jnp.minimum(result, value)
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _heaviside_fn(value: Any) -> Any:
|
|
48
|
+
return jnp.where(jnp.asarray(value) > 0.0, 1.0, 0.0)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_SAFE_FUNCTIONS = {
|
|
52
|
+
"H": _heaviside_fn,
|
|
53
|
+
"abs": jnp.abs,
|
|
54
|
+
"cos": jnp.cos,
|
|
55
|
+
"exp": jnp.exp,
|
|
56
|
+
"log": jnp.log,
|
|
57
|
+
"max": _max_fn,
|
|
58
|
+
"min": _min_fn,
|
|
59
|
+
"sin": jnp.sin,
|
|
60
|
+
"sqrt": jnp.sqrt,
|
|
61
|
+
"tan": jnp.tan,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ArrayExpressionEvaluator:
|
|
67
|
+
config: BoutConfig
|
|
68
|
+
local_values: Mapping[str, Any]
|
|
69
|
+
external_values: Mapping[str, Any] | None = None
|
|
70
|
+
_cache: dict[tuple[str, str], Any] = field(default_factory=dict, init=False)
|
|
71
|
+
|
|
72
|
+
def evaluate(self, expression: str, *, current_section: str = ROOT_SECTION) -> Any:
|
|
73
|
+
return self._evaluate_expression(expression, current_section=current_section, seen=set())
|
|
74
|
+
|
|
75
|
+
def resolve_option(self, section: str, key: str) -> Any:
|
|
76
|
+
return self._resolve_option(section, key, seen=set())
|
|
77
|
+
|
|
78
|
+
def _resolve_option(self, section: str, key: str, seen: set[tuple[str, str]]) -> Any:
|
|
79
|
+
cache_key = (section, key)
|
|
80
|
+
if cache_key in self._cache:
|
|
81
|
+
return self._cache[cache_key]
|
|
82
|
+
if cache_key in seen:
|
|
83
|
+
cycle = " -> ".join(f"{entry_section}:{entry_key}" for entry_section, entry_key in (*seen, cache_key))
|
|
84
|
+
raise ValueError(f"Cyclic array expression detected: {cycle}")
|
|
85
|
+
|
|
86
|
+
if not self.config.has_option(section, key):
|
|
87
|
+
raise KeyError(f"Missing option {section}:{key}")
|
|
88
|
+
|
|
89
|
+
seen.add(cache_key)
|
|
90
|
+
value = self.config.get(section, key)
|
|
91
|
+
if isinstance(value.parsed, bool | int | float):
|
|
92
|
+
result = value.parsed
|
|
93
|
+
elif isinstance(value.parsed, tuple):
|
|
94
|
+
raise TypeError(f"Option {section}:{key} is a list, not an array expression")
|
|
95
|
+
else:
|
|
96
|
+
result = self._evaluate_expression(value.raw, current_section=section, seen=seen)
|
|
97
|
+
self._cache[cache_key] = result
|
|
98
|
+
seen.remove(cache_key)
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
def _evaluate_expression(self, expression: str, *, current_section: str, seen: set[tuple[str, str]]) -> Any:
|
|
102
|
+
tree, references = _compile_expression(expression)
|
|
103
|
+
return self._eval_node(tree.body, current_section=current_section, references=references, seen=seen)
|
|
104
|
+
|
|
105
|
+
def _eval_node(
|
|
106
|
+
self,
|
|
107
|
+
node: ast.AST,
|
|
108
|
+
*,
|
|
109
|
+
current_section: str,
|
|
110
|
+
references: Mapping[str, str],
|
|
111
|
+
seen: set[tuple[str, str]],
|
|
112
|
+
) -> Any:
|
|
113
|
+
if isinstance(node, ast.Constant):
|
|
114
|
+
if isinstance(node.value, bool | int | float):
|
|
115
|
+
return node.value
|
|
116
|
+
raise TypeError(f"Unsupported constant value: {node.value!r}")
|
|
117
|
+
if isinstance(node, ast.Name):
|
|
118
|
+
return self._resolve_name(node.id, current_section=current_section, references=references, seen=seen)
|
|
119
|
+
if isinstance(node, ast.BinOp) and type(node.op) in _SAFE_BINOPS:
|
|
120
|
+
left = self._eval_node(node.left, current_section=current_section, references=references, seen=seen)
|
|
121
|
+
right = self._eval_node(node.right, current_section=current_section, references=references, seen=seen)
|
|
122
|
+
return _SAFE_BINOPS[type(node.op)](left, right)
|
|
123
|
+
if isinstance(node, ast.UnaryOp) and type(node.op) in _SAFE_UNARYOPS:
|
|
124
|
+
operand = self._eval_node(node.operand, current_section=current_section, references=references, seen=seen)
|
|
125
|
+
return _SAFE_UNARYOPS[type(node.op)](operand)
|
|
126
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
127
|
+
function_name = node.func.id
|
|
128
|
+
if function_name not in _SAFE_FUNCTIONS:
|
|
129
|
+
raise TypeError(f"Unsupported array expression function {function_name!r}")
|
|
130
|
+
args = [
|
|
131
|
+
self._eval_node(argument, current_section=current_section, references=references, seen=seen)
|
|
132
|
+
for argument in node.args
|
|
133
|
+
]
|
|
134
|
+
return _SAFE_FUNCTIONS[function_name](*args)
|
|
135
|
+
raise TypeError(f"Unsupported array expression node: {ast.dump(node, include_attributes=False)}")
|
|
136
|
+
|
|
137
|
+
def _resolve_name(
|
|
138
|
+
self,
|
|
139
|
+
name: str,
|
|
140
|
+
*,
|
|
141
|
+
current_section: str,
|
|
142
|
+
references: Mapping[str, str],
|
|
143
|
+
seen: set[tuple[str, str]],
|
|
144
|
+
) -> Any:
|
|
145
|
+
if name in references:
|
|
146
|
+
section_name, key = references[name].rsplit(":", 1)
|
|
147
|
+
return self._resolve_option(section_name, key, seen)
|
|
148
|
+
if self.external_values is not None and name in self.external_values:
|
|
149
|
+
return self.external_values[name]
|
|
150
|
+
if name in self.local_values:
|
|
151
|
+
return self.local_values[name]
|
|
152
|
+
if name in _SAFE_CONSTANTS:
|
|
153
|
+
return _SAFE_CONSTANTS[name]
|
|
154
|
+
if current_section != ROOT_SECTION and self.config.has_option(current_section, name):
|
|
155
|
+
return self._resolve_option(current_section, name, seen)
|
|
156
|
+
if self.config.has_option(ROOT_SECTION, name):
|
|
157
|
+
return self._resolve_option(ROOT_SECTION, name, seen)
|
|
158
|
+
raise KeyError(f"Unknown array expression symbol {name!r} while evaluating section {current_section!r}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@lru_cache(maxsize=1024)
|
|
162
|
+
def _compile_expression(expression: str) -> tuple[ast.Expression, Mapping[str, str]]:
|
|
163
|
+
sanitized = expression.replace("π", "pi").replace("^", "**")
|
|
164
|
+
sanitized = re.sub(r"(?<=\d)pi\b", "*pi", sanitized)
|
|
165
|
+
references: dict[str, str] = {}
|
|
166
|
+
|
|
167
|
+
def replace_reference(match: re.Match[str]) -> str:
|
|
168
|
+
token = f"__ref_{len(references)}"
|
|
169
|
+
references[token] = match.group(0)
|
|
170
|
+
return token
|
|
171
|
+
|
|
172
|
+
tree = ast.parse(_REFERENCE_PATTERN.sub(replace_reference, sanitized), mode="eval")
|
|
173
|
+
return tree, dict(references)
|