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,620 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from matplotlib import colors
|
|
9
|
+
from matplotlib import pyplot as plt
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from ..geometry import (
|
|
13
|
+
build_essos_vmec_scaled_qa_coordinates,
|
|
14
|
+
load_essos_coil_field_axis,
|
|
15
|
+
load_essos_vmec_field_axis,
|
|
16
|
+
resolve_essos_landreman_qa_json,
|
|
17
|
+
resolve_essos_landreman_qa_wout,
|
|
18
|
+
trace_essos_coil_initial_conditions,
|
|
19
|
+
trace_essos_vmec_initial_conditions,
|
|
20
|
+
)
|
|
21
|
+
from .publication_plotting import save_publication_figure, style_axis
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class EssosVmecFieldlineSurfaceArtifacts:
|
|
26
|
+
report_json_path: Path
|
|
27
|
+
arrays_npz_path: Path
|
|
28
|
+
plot_png_path: Path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class EssosVmecFieldlineSurfaceResult:
|
|
33
|
+
report: dict[str, Any]
|
|
34
|
+
arrays: dict[str, np.ndarray]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_essos_vmec_fieldline_surface_package(
|
|
38
|
+
*,
|
|
39
|
+
output_root: str | Path,
|
|
40
|
+
coil_json_path: str | Path | None = None,
|
|
41
|
+
vmec_wout_path: str | Path | None = None,
|
|
42
|
+
essos_root: str | Path | None = None,
|
|
43
|
+
case_label: str = "essos_vmec_fieldline_surface_campaign",
|
|
44
|
+
rho_min: float = 0.20,
|
|
45
|
+
rho_max: float = 0.92,
|
|
46
|
+
n_surfaces: int = 7,
|
|
47
|
+
ntheta_surface: int = 320,
|
|
48
|
+
maxtime: float = 900.0,
|
|
49
|
+
times_to_trace: int = 4200,
|
|
50
|
+
trace_tolerance: float = 1.0e-8,
|
|
51
|
+
sections: tuple[float, ...] = (0.0, float(np.pi / 2.0), float(np.pi), float(3.0 * np.pi / 2.0)),
|
|
52
|
+
field_source: str = "coil",
|
|
53
|
+
) -> EssosVmecFieldlineSurfaceArtifacts:
|
|
54
|
+
"""Trace external field lines and compare Poincare hits with scaled VMEC surfaces."""
|
|
55
|
+
|
|
56
|
+
root = Path(output_root)
|
|
57
|
+
data_dir = root / "data"
|
|
58
|
+
images_dir = root / "images"
|
|
59
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
images_dir.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
result = build_essos_vmec_fieldline_surface_campaign(
|
|
63
|
+
coil_json_path=coil_json_path,
|
|
64
|
+
vmec_wout_path=vmec_wout_path,
|
|
65
|
+
essos_root=essos_root,
|
|
66
|
+
rho_min=rho_min,
|
|
67
|
+
rho_max=rho_max,
|
|
68
|
+
n_surfaces=n_surfaces,
|
|
69
|
+
ntheta_surface=ntheta_surface,
|
|
70
|
+
maxtime=maxtime,
|
|
71
|
+
times_to_trace=times_to_trace,
|
|
72
|
+
trace_tolerance=trace_tolerance,
|
|
73
|
+
sections=sections,
|
|
74
|
+
field_source=field_source,
|
|
75
|
+
)
|
|
76
|
+
report_json_path = data_dir / f"{case_label}.json"
|
|
77
|
+
report_json_path.write_text(json.dumps(result.report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
78
|
+
arrays_npz_path = data_dir / f"{case_label}.npz"
|
|
79
|
+
np.savez_compressed(arrays_npz_path, **result.arrays)
|
|
80
|
+
plot_png_path = images_dir / f"{case_label}.png"
|
|
81
|
+
save_essos_vmec_fieldline_surface_plot(result.report, result.arrays, plot_png_path)
|
|
82
|
+
return EssosVmecFieldlineSurfaceArtifacts(
|
|
83
|
+
report_json_path=report_json_path,
|
|
84
|
+
arrays_npz_path=arrays_npz_path,
|
|
85
|
+
plot_png_path=plot_png_path,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_essos_vmec_fieldline_surface_campaign(
|
|
90
|
+
*,
|
|
91
|
+
coil_json_path: str | Path | None = None,
|
|
92
|
+
vmec_wout_path: str | Path | None = None,
|
|
93
|
+
essos_root: str | Path | None = None,
|
|
94
|
+
rho_min: float = 0.20,
|
|
95
|
+
rho_max: float = 0.92,
|
|
96
|
+
n_surfaces: int = 7,
|
|
97
|
+
ntheta_surface: int = 320,
|
|
98
|
+
maxtime: float = 900.0,
|
|
99
|
+
times_to_trace: int = 4200,
|
|
100
|
+
trace_tolerance: float = 1.0e-8,
|
|
101
|
+
sections: tuple[float, ...] = (0.0, float(np.pi / 2.0), float(np.pi), float(3.0 * np.pi / 2.0)),
|
|
102
|
+
field_source: str = "coil",
|
|
103
|
+
) -> EssosVmecFieldlineSurfaceResult:
|
|
104
|
+
"""Build the independent field-line/VMEC-surface registration gate."""
|
|
105
|
+
|
|
106
|
+
if n_surfaces < 2:
|
|
107
|
+
raise ValueError("field-line surface validation requires at least two surfaces")
|
|
108
|
+
if ntheta_surface < 16:
|
|
109
|
+
raise ValueError("field-line surface validation requires ntheta_surface >= 16")
|
|
110
|
+
if not sections:
|
|
111
|
+
raise ValueError("at least one Poincare section is required")
|
|
112
|
+
|
|
113
|
+
field_source = field_source.lower().replace("-", "_")
|
|
114
|
+
if field_source not in {"coil", "vmec"}:
|
|
115
|
+
raise ValueError("field_source must be either 'coil' or 'vmec'")
|
|
116
|
+
|
|
117
|
+
resolved_coil_json = resolve_essos_landreman_qa_json(coil_json_path, essos_root=essos_root)
|
|
118
|
+
resolved_wout = resolve_essos_landreman_qa_wout(vmec_wout_path, essos_root=essos_root)
|
|
119
|
+
axis_major_radius, axis_vertical = load_essos_coil_field_axis(coil_json_path=resolved_coil_json, essos_root=essos_root)
|
|
120
|
+
vmec_axis_major_radius, vmec_axis_vertical = load_essos_vmec_field_axis(vmec_wout_path=resolved_wout, essos_root=essos_root)
|
|
121
|
+
sections_array = np.mod(np.asarray(sections, dtype=np.float64), 2.0 * np.pi)
|
|
122
|
+
|
|
123
|
+
seed_coordinates = build_essos_vmec_scaled_qa_coordinates(
|
|
124
|
+
resolved_wout,
|
|
125
|
+
nx=int(n_surfaces),
|
|
126
|
+
ny=1,
|
|
127
|
+
nz=max(int(ntheta_surface), 32),
|
|
128
|
+
rho_min=float(rho_min),
|
|
129
|
+
rho_max=float(rho_max),
|
|
130
|
+
axis_major_radius=axis_major_radius,
|
|
131
|
+
axis_vertical=axis_vertical,
|
|
132
|
+
)
|
|
133
|
+
surface_nphi = max(32, 4 * len(sections_array))
|
|
134
|
+
surface_coordinates_full = build_essos_vmec_scaled_qa_coordinates(
|
|
135
|
+
resolved_wout,
|
|
136
|
+
nx=int(n_surfaces),
|
|
137
|
+
ny=surface_nphi,
|
|
138
|
+
nz=int(ntheta_surface),
|
|
139
|
+
rho_min=float(rho_min),
|
|
140
|
+
rho_max=float(rho_max),
|
|
141
|
+
axis_major_radius=axis_major_radius,
|
|
142
|
+
axis_vertical=axis_vertical,
|
|
143
|
+
)
|
|
144
|
+
section_indices = np.mod(
|
|
145
|
+
np.rint(sections_array / (2.0 * np.pi) * float(surface_nphi)).astype(int),
|
|
146
|
+
surface_nphi,
|
|
147
|
+
)
|
|
148
|
+
surface_major = np.asarray(surface_coordinates_full["major"][:, section_indices, :], dtype=np.float64)
|
|
149
|
+
surface_vertical = np.asarray(surface_coordinates_full["vertical"][:, section_indices, :], dtype=np.float64)
|
|
150
|
+
surface_phi = np.asarray(surface_coordinates_full["phi"][:, section_indices, :], dtype=np.float64)
|
|
151
|
+
surface_theta = np.asarray(surface_coordinates_full["theta"][:, section_indices, :], dtype=np.float64)
|
|
152
|
+
initial_xyz = np.stack(
|
|
153
|
+
[
|
|
154
|
+
seed_coordinates["x"][:, 0, 0],
|
|
155
|
+
seed_coordinates["y"][:, 0, 0],
|
|
156
|
+
seed_coordinates["z"][:, 0, 0],
|
|
157
|
+
],
|
|
158
|
+
axis=-1,
|
|
159
|
+
)
|
|
160
|
+
current_sign = 1.0
|
|
161
|
+
trajectories_stp = np.empty((0, 0, 3), dtype=np.float64)
|
|
162
|
+
fieldline_s_drift_max = 0.0
|
|
163
|
+
if field_source == "vmec":
|
|
164
|
+
initial_stp = np.stack(
|
|
165
|
+
[
|
|
166
|
+
np.asarray(seed_coordinates["s_1d"], dtype=np.float64),
|
|
167
|
+
np.zeros(int(n_surfaces), dtype=np.float64),
|
|
168
|
+
np.zeros(int(n_surfaces), dtype=np.float64),
|
|
169
|
+
],
|
|
170
|
+
axis=-1,
|
|
171
|
+
)
|
|
172
|
+
trajectories_stp = trace_essos_vmec_initial_conditions(
|
|
173
|
+
initial_stp,
|
|
174
|
+
vmec_wout_path=resolved_wout,
|
|
175
|
+
essos_root=essos_root,
|
|
176
|
+
maxtime=maxtime,
|
|
177
|
+
times_to_trace=times_to_trace,
|
|
178
|
+
trace_tolerance=trace_tolerance,
|
|
179
|
+
)
|
|
180
|
+
trajectories = _vmec_coordinate_hits_to_xyz_placeholder(trajectories_stp, initial_xyz)
|
|
181
|
+
hits = _extract_vmec_coordinate_poincare_hits(
|
|
182
|
+
trajectories_stp,
|
|
183
|
+
sections_array,
|
|
184
|
+
surface_major=surface_major,
|
|
185
|
+
surface_vertical=surface_vertical,
|
|
186
|
+
surface_theta=surface_theta,
|
|
187
|
+
)
|
|
188
|
+
fieldline_s_drift_max = float(np.max(np.abs(trajectories_stp[:, :, 0] - initial_stp[:, None, 0])))
|
|
189
|
+
else:
|
|
190
|
+
trajectories = trace_essos_coil_initial_conditions(
|
|
191
|
+
initial_xyz,
|
|
192
|
+
coil_json_path=resolved_coil_json,
|
|
193
|
+
essos_root=essos_root,
|
|
194
|
+
current_sign=1.0,
|
|
195
|
+
maxtime=maxtime,
|
|
196
|
+
times_to_trace=times_to_trace,
|
|
197
|
+
trace_tolerance=trace_tolerance,
|
|
198
|
+
)
|
|
199
|
+
if _median_toroidal_advance(trajectories) < 0.0:
|
|
200
|
+
current_sign = -1.0
|
|
201
|
+
trajectories = trace_essos_coil_initial_conditions(
|
|
202
|
+
initial_xyz,
|
|
203
|
+
coil_json_path=resolved_coil_json,
|
|
204
|
+
essos_root=essos_root,
|
|
205
|
+
current_sign=current_sign,
|
|
206
|
+
maxtime=maxtime,
|
|
207
|
+
times_to_trace=times_to_trace,
|
|
208
|
+
trace_tolerance=trace_tolerance,
|
|
209
|
+
)
|
|
210
|
+
hits = _extract_poincare_hits(trajectories, sections_array)
|
|
211
|
+
same_surface_distance, nearest_surface_distance = _distance_to_reference_surfaces(
|
|
212
|
+
hits,
|
|
213
|
+
surface_major=surface_major,
|
|
214
|
+
surface_vertical=surface_vertical,
|
|
215
|
+
)
|
|
216
|
+
reference_extent = _reference_minor_extent(
|
|
217
|
+
surface_major[-1],
|
|
218
|
+
surface_vertical[-1],
|
|
219
|
+
)
|
|
220
|
+
normalized_same = same_surface_distance / max(reference_extent, 1.0e-30)
|
|
221
|
+
normalized_nearest = nearest_surface_distance / max(reference_extent, 1.0e-30)
|
|
222
|
+
finite = (
|
|
223
|
+
np.all(np.isfinite(trajectories))
|
|
224
|
+
and np.all(np.isfinite(trajectories_stp))
|
|
225
|
+
and np.all(np.isfinite(same_surface_distance))
|
|
226
|
+
and np.all(np.isfinite(nearest_surface_distance))
|
|
227
|
+
)
|
|
228
|
+
point_count = int(hits["r"].size)
|
|
229
|
+
same_p95 = _percentile_or_inf(normalized_same, 95.0)
|
|
230
|
+
nearest_p95 = _percentile_or_inf(normalized_nearest, 95.0)
|
|
231
|
+
nonaxisymmetric_rms = float(surface_coordinates_full["metadata"]["surface_nonaxisymmetric_major_rms"])
|
|
232
|
+
if field_source == "vmec":
|
|
233
|
+
source = "ESSOS VMEC field-line tracing compared with Landreman-Paul QA VMEC Fourier surfaces"
|
|
234
|
+
claim_scope = (
|
|
235
|
+
"surface-preserving VMEC-field Poincare diagnostic for the Landreman-Paul QA "
|
|
236
|
+
"Fourier boundary used by the movie renderer; distances are evaluated on the "
|
|
237
|
+
"scaled physics-grid copy"
|
|
238
|
+
)
|
|
239
|
+
same_threshold = 5.0e-2
|
|
240
|
+
nearest_threshold = 5.0e-2
|
|
241
|
+
s_drift_threshold = 1.0e-7
|
|
242
|
+
else:
|
|
243
|
+
source = "ESSOS coil field-line tracing compared with scaled Landreman-Paul QA VMEC surfaces"
|
|
244
|
+
claim_scope = (
|
|
245
|
+
"independent coil-field Poincare diagnostic for VMEC geometry registration; "
|
|
246
|
+
"the strict closed-surface match flag is reported separately because the "
|
|
247
|
+
"imported coil field can leave the scaled VMEC seed shell"
|
|
248
|
+
)
|
|
249
|
+
same_threshold = 0.35
|
|
250
|
+
nearest_threshold = 0.20
|
|
251
|
+
s_drift_threshold = float("inf")
|
|
252
|
+
report: dict[str, Any] = {
|
|
253
|
+
"case": "essos_vmec_fieldline_surface_campaign",
|
|
254
|
+
"source": source,
|
|
255
|
+
"claim_scope": claim_scope,
|
|
256
|
+
"field_source": field_source,
|
|
257
|
+
"coil_json_file": resolved_coil_json.name,
|
|
258
|
+
"vmec_wout_file": resolved_wout.name,
|
|
259
|
+
"coordinate_model": "scaled_vmec_fourier_flux_surfaces",
|
|
260
|
+
"n_surfaces": int(n_surfaces),
|
|
261
|
+
"ntheta_surface": int(ntheta_surface),
|
|
262
|
+
"sections": [float(value) for value in sections_array],
|
|
263
|
+
"maxtime": float(maxtime),
|
|
264
|
+
"times_to_trace": int(times_to_trace),
|
|
265
|
+
"trace_tolerance": float(trace_tolerance),
|
|
266
|
+
"current_sign": float(current_sign),
|
|
267
|
+
"poincare_point_count": point_count,
|
|
268
|
+
"points_per_surface_mean": float(point_count / max(int(n_surfaces), 1)),
|
|
269
|
+
"axis_major_radius": float(axis_major_radius),
|
|
270
|
+
"axis_vertical": float(axis_vertical),
|
|
271
|
+
"vmec_raw_axis_major_radius": float(vmec_axis_major_radius),
|
|
272
|
+
"vmec_raw_axis_vertical": float(vmec_axis_vertical),
|
|
273
|
+
"rho_min": float(rho_min),
|
|
274
|
+
"rho_max": float(rho_max),
|
|
275
|
+
"fieldline_s_drift_max": fieldline_s_drift_max,
|
|
276
|
+
"reference_minor_extent": float(reference_extent),
|
|
277
|
+
"surface_nonaxisymmetric_major_rms": nonaxisymmetric_rms,
|
|
278
|
+
"same_surface_distance_median": _median_or_inf(same_surface_distance),
|
|
279
|
+
"same_surface_distance_p95": _percentile_or_inf(same_surface_distance, 95.0),
|
|
280
|
+
"same_surface_distance_max": _max_or_inf(same_surface_distance),
|
|
281
|
+
"same_surface_distance_normalized_median": _median_or_inf(normalized_same),
|
|
282
|
+
"same_surface_distance_normalized_p95": same_p95,
|
|
283
|
+
"nearest_surface_distance_normalized_p95": nearest_p95,
|
|
284
|
+
"nearest_surface_distance_normalized_max": _max_or_inf(normalized_nearest),
|
|
285
|
+
"fieldline_radial_span_mean": _line_span_mean(hits["r"], hits["line_index"], int(n_surfaces)),
|
|
286
|
+
"fieldline_vertical_span_mean": _line_span_mean(hits["z"], hits["line_index"], int(n_surfaces)),
|
|
287
|
+
}
|
|
288
|
+
report["fieldline_surface_match_passed"] = bool(
|
|
289
|
+
finite
|
|
290
|
+
and point_count >= max(2 * int(n_surfaces), 1)
|
|
291
|
+
and nonaxisymmetric_rms > 5.0e-2
|
|
292
|
+
and same_p95 < same_threshold
|
|
293
|
+
and nearest_p95 < nearest_threshold
|
|
294
|
+
and fieldline_s_drift_max < s_drift_threshold
|
|
295
|
+
)
|
|
296
|
+
report["passed"] = bool(finite and point_count >= max(2 * int(n_surfaces), 1) and nonaxisymmetric_rms > 5.0e-2)
|
|
297
|
+
arrays = {
|
|
298
|
+
"initial_xyz": initial_xyz.astype(np.float32),
|
|
299
|
+
"trajectories_xyz": trajectories.astype(np.float32),
|
|
300
|
+
"trajectories_stp": trajectories_stp.astype(np.float32),
|
|
301
|
+
"surface_major": surface_major.astype(np.float32),
|
|
302
|
+
"surface_vertical": surface_vertical.astype(np.float32),
|
|
303
|
+
"surface_phi": surface_phi.astype(np.float32),
|
|
304
|
+
"surface_theta": surface_theta.astype(np.float32),
|
|
305
|
+
"rho_values": np.asarray(seed_coordinates["rho_1d"], dtype=np.float32),
|
|
306
|
+
"sections": sections_array.astype(np.float32),
|
|
307
|
+
"poincare_r": hits["r"].astype(np.float32),
|
|
308
|
+
"poincare_z": hits["z"].astype(np.float32),
|
|
309
|
+
"poincare_time_index": hits["time_index"].astype(np.float32),
|
|
310
|
+
"poincare_section_index": hits["section_index"].astype(np.int32),
|
|
311
|
+
"poincare_line_index": hits["line_index"].astype(np.int32),
|
|
312
|
+
"same_surface_distance": same_surface_distance.astype(np.float32),
|
|
313
|
+
"nearest_surface_distance": nearest_surface_distance.astype(np.float32),
|
|
314
|
+
"same_surface_distance_normalized": normalized_same.astype(np.float32),
|
|
315
|
+
"nearest_surface_distance_normalized": normalized_nearest.astype(np.float32),
|
|
316
|
+
}
|
|
317
|
+
return EssosVmecFieldlineSurfaceResult(report=report, arrays=arrays)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def save_essos_vmec_fieldline_surface_plot(
|
|
321
|
+
report: dict[str, Any],
|
|
322
|
+
arrays: dict[str, np.ndarray],
|
|
323
|
+
path: str | Path,
|
|
324
|
+
) -> Path:
|
|
325
|
+
"""Save the field-line/surface registration figure used to QA the movie geometry."""
|
|
326
|
+
|
|
327
|
+
surface_major = np.asarray(arrays["surface_major"], dtype=np.float64)
|
|
328
|
+
surface_vertical = np.asarray(arrays["surface_vertical"], dtype=np.float64)
|
|
329
|
+
sections = np.asarray(arrays["sections"], dtype=np.float64)
|
|
330
|
+
hit_r = np.asarray(arrays["poincare_r"], dtype=np.float64)
|
|
331
|
+
hit_z = np.asarray(arrays["poincare_z"], dtype=np.float64)
|
|
332
|
+
hit_section = np.asarray(arrays["poincare_section_index"], dtype=np.int32)
|
|
333
|
+
hit_line = np.asarray(arrays["poincare_line_index"], dtype=np.int32)
|
|
334
|
+
normalized_same = np.asarray(arrays["same_surface_distance_normalized"], dtype=np.float64)
|
|
335
|
+
|
|
336
|
+
panel_count = min(len(sections), 4)
|
|
337
|
+
fig, axes = plt.subplots(2, 3, figsize=(14.8, 8.4), constrained_layout=True)
|
|
338
|
+
flat_axes = axes.ravel()
|
|
339
|
+
cmap = plt.get_cmap("viridis")
|
|
340
|
+
norm = colors.Normalize(vmin=0, vmax=max(surface_major.shape[0] - 1, 1))
|
|
341
|
+
for section_index in range(panel_count):
|
|
342
|
+
axis = flat_axes[section_index]
|
|
343
|
+
for surface_index in range(surface_major.shape[0]):
|
|
344
|
+
color = "0.82" if surface_index < surface_major.shape[0] - 1 else "black"
|
|
345
|
+
width = 0.9 if surface_index < surface_major.shape[0] - 1 else 1.5
|
|
346
|
+
axis.plot(
|
|
347
|
+
surface_major[surface_index, section_index, :],
|
|
348
|
+
surface_vertical[surface_index, section_index, :],
|
|
349
|
+
color=color,
|
|
350
|
+
lw=width,
|
|
351
|
+
alpha=0.92,
|
|
352
|
+
)
|
|
353
|
+
mask = hit_section == section_index
|
|
354
|
+
if np.any(mask):
|
|
355
|
+
axis.scatter(
|
|
356
|
+
hit_r[mask],
|
|
357
|
+
hit_z[mask],
|
|
358
|
+
c=hit_line[mask],
|
|
359
|
+
s=5.0,
|
|
360
|
+
cmap=cmap,
|
|
361
|
+
norm=norm,
|
|
362
|
+
linewidths=0.0,
|
|
363
|
+
alpha=0.78,
|
|
364
|
+
)
|
|
365
|
+
axis.set_aspect("equal", adjustable="box")
|
|
366
|
+
style_axis(
|
|
367
|
+
axis,
|
|
368
|
+
title=rf"Poincare section $\phi={sections[section_index]:.2f}$",
|
|
369
|
+
xlabel="R",
|
|
370
|
+
ylabel="Z",
|
|
371
|
+
grid="both",
|
|
372
|
+
)
|
|
373
|
+
for empty_index in range(panel_count, 4):
|
|
374
|
+
flat_axes[empty_index].axis("off")
|
|
375
|
+
|
|
376
|
+
hist_axis = flat_axes[4]
|
|
377
|
+
hist_axis.hist(normalized_same[np.isfinite(normalized_same)], bins=24, color="#0A6E7D", alpha=0.82)
|
|
378
|
+
hist_axis.axvline(report["same_surface_distance_normalized_p95"], color="#CA6702", lw=2.0, label="95th percentile")
|
|
379
|
+
style_axis(
|
|
380
|
+
hist_axis,
|
|
381
|
+
title="distance to seeded VMEC surface",
|
|
382
|
+
xlabel="distance / outer-surface minor extent",
|
|
383
|
+
ylabel="count",
|
|
384
|
+
grid="y",
|
|
385
|
+
)
|
|
386
|
+
hist_axis.legend(frameon=False, fontsize=9)
|
|
387
|
+
|
|
388
|
+
text_axis = flat_axes[5]
|
|
389
|
+
text_axis.axis("off")
|
|
390
|
+
text_axis.text(
|
|
391
|
+
0.02,
|
|
392
|
+
0.95,
|
|
393
|
+
"\n".join(
|
|
394
|
+
[
|
|
395
|
+
"Independent geometry registration",
|
|
396
|
+
f"points: {report['poincare_point_count']}",
|
|
397
|
+
f"non-axisymmetric RMS: {report['surface_nonaxisymmetric_major_rms']:.3f}",
|
|
398
|
+
f"same-surface p95: {report['same_surface_distance_normalized_p95']:.3f}",
|
|
399
|
+
f"nearest-surface p95: {report['nearest_surface_distance_normalized_p95']:.3f}",
|
|
400
|
+
f"radial span mean: {report['fieldline_radial_span_mean']:.3e}",
|
|
401
|
+
f"vertical span mean: {report['fieldline_vertical_span_mean']:.3e}",
|
|
402
|
+
f"passed: {report['passed']}",
|
|
403
|
+
]
|
|
404
|
+
),
|
|
405
|
+
transform=text_axis.transAxes,
|
|
406
|
+
va="top",
|
|
407
|
+
fontsize=11,
|
|
408
|
+
bbox={"facecolor": "white", "edgecolor": "0.82", "alpha": 0.96},
|
|
409
|
+
)
|
|
410
|
+
title_prefix = "VMEC-field" if report.get("field_source") == "vmec" else "QA-coil"
|
|
411
|
+
fig.suptitle(
|
|
412
|
+
f"{title_prefix} field-line Poincare hits over scaled Landreman-Paul VMEC surfaces",
|
|
413
|
+
fontsize=15,
|
|
414
|
+
fontweight="semibold",
|
|
415
|
+
)
|
|
416
|
+
save_publication_figure(fig, path)
|
|
417
|
+
return Path(path)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _extract_poincare_hits(trajectories_xyz: np.ndarray, sections: np.ndarray) -> dict[str, np.ndarray]:
|
|
421
|
+
r_values: list[float] = []
|
|
422
|
+
z_values: list[float] = []
|
|
423
|
+
time_values: list[float] = []
|
|
424
|
+
section_values: list[int] = []
|
|
425
|
+
line_values: list[int] = []
|
|
426
|
+
period = 2.0 * np.pi
|
|
427
|
+
for line_index, trajectory in enumerate(np.asarray(trajectories_xyz, dtype=np.float64)):
|
|
428
|
+
major = np.sqrt(trajectory[:, 0] ** 2 + trajectory[:, 1] ** 2)
|
|
429
|
+
vertical = trajectory[:, 2]
|
|
430
|
+
phi = np.unwrap(np.arctan2(trajectory[:, 1], trajectory[:, 0]))
|
|
431
|
+
if not np.all(np.isfinite(phi)) or phi.size < 3:
|
|
432
|
+
continue
|
|
433
|
+
if phi[-1] < phi[0]:
|
|
434
|
+
phi = phi[::-1]
|
|
435
|
+
major = major[::-1]
|
|
436
|
+
vertical = vertical[::-1]
|
|
437
|
+
phi_min = float(phi[0])
|
|
438
|
+
phi_max = float(phi[-1])
|
|
439
|
+
for section_index, section in enumerate(sections):
|
|
440
|
+
k_min = int(np.floor((phi_min - float(section)) / period)) - 1
|
|
441
|
+
k_max = int(np.ceil((phi_max - float(section)) / period)) + 1
|
|
442
|
+
for k_value in range(k_min, k_max + 1):
|
|
443
|
+
target = float(section) + period * float(k_value)
|
|
444
|
+
if target <= phi_min + 1.0e-10 or target >= phi_max - 1.0e-10:
|
|
445
|
+
continue
|
|
446
|
+
r_values.append(float(np.interp(target, phi, major)))
|
|
447
|
+
z_values.append(float(np.interp(target, phi, vertical)))
|
|
448
|
+
time_values.append(float(np.interp(target, phi, np.arange(phi.size, dtype=np.float64))))
|
|
449
|
+
section_values.append(int(section_index))
|
|
450
|
+
line_values.append(int(line_index))
|
|
451
|
+
return {
|
|
452
|
+
"r": np.asarray(r_values, dtype=np.float64),
|
|
453
|
+
"z": np.asarray(z_values, dtype=np.float64),
|
|
454
|
+
"time_index": np.asarray(time_values, dtype=np.float64),
|
|
455
|
+
"section_index": np.asarray(section_values, dtype=np.int32),
|
|
456
|
+
"line_index": np.asarray(line_values, dtype=np.int32),
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _vmec_coordinate_hits_to_xyz_placeholder(trajectories_stp: np.ndarray, initial_xyz: np.ndarray) -> np.ndarray:
|
|
461
|
+
"""Return finite Cartesian placeholders for VMEC-coordinate field-line traces.
|
|
462
|
+
|
|
463
|
+
The validation plot and distance metrics for VMEC-coordinate traces are
|
|
464
|
+
computed directly from the VMEC coordinates and the sampled surface table.
|
|
465
|
+
A finite Cartesian array is still stored so the artifact schema remains
|
|
466
|
+
compatible with the coil-trace package.
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
stp = np.asarray(trajectories_stp, dtype=np.float64)
|
|
470
|
+
seeds = np.asarray(initial_xyz, dtype=np.float64)
|
|
471
|
+
return np.broadcast_to(seeds[:, None, :], stp.shape).copy()
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _extract_vmec_coordinate_poincare_hits(
|
|
475
|
+
trajectories_stp: np.ndarray,
|
|
476
|
+
sections: np.ndarray,
|
|
477
|
+
*,
|
|
478
|
+
surface_major: np.ndarray,
|
|
479
|
+
surface_vertical: np.ndarray,
|
|
480
|
+
surface_theta: np.ndarray,
|
|
481
|
+
) -> dict[str, np.ndarray]:
|
|
482
|
+
r_values: list[float] = []
|
|
483
|
+
z_values: list[float] = []
|
|
484
|
+
time_values: list[float] = []
|
|
485
|
+
section_values: list[int] = []
|
|
486
|
+
line_values: list[int] = []
|
|
487
|
+
theta_values: list[float] = []
|
|
488
|
+
s_values: list[float] = []
|
|
489
|
+
period = 2.0 * np.pi
|
|
490
|
+
for line_index, trajectory in enumerate(np.asarray(trajectories_stp, dtype=np.float64)):
|
|
491
|
+
if trajectory.shape[0] < 3:
|
|
492
|
+
continue
|
|
493
|
+
s_coord = trajectory[:, 0]
|
|
494
|
+
theta = trajectory[:, 1]
|
|
495
|
+
phi = np.unwrap(trajectory[:, 2])
|
|
496
|
+
if not np.all(np.isfinite(phi)):
|
|
497
|
+
continue
|
|
498
|
+
sample_index = np.arange(phi.size, dtype=np.float64)
|
|
499
|
+
if phi[-1] < phi[0]:
|
|
500
|
+
s_coord = s_coord[::-1]
|
|
501
|
+
theta = theta[::-1]
|
|
502
|
+
phi = phi[::-1]
|
|
503
|
+
sample_index = sample_index[::-1]
|
|
504
|
+
phi_min = float(phi[0])
|
|
505
|
+
phi_max = float(phi[-1])
|
|
506
|
+
for section_index, section in enumerate(sections):
|
|
507
|
+
k_min = int(np.floor((phi_min - float(section)) / period)) - 1
|
|
508
|
+
k_max = int(np.ceil((phi_max - float(section)) / period)) + 1
|
|
509
|
+
for k_value in range(k_min, k_max + 1):
|
|
510
|
+
target = float(section) + period * float(k_value)
|
|
511
|
+
if target <= phi_min + 1.0e-10 or target >= phi_max - 1.0e-10:
|
|
512
|
+
continue
|
|
513
|
+
theta_value = float(np.interp(target, phi, theta))
|
|
514
|
+
r_value, z_value = _interpolate_surface_section_at_theta(
|
|
515
|
+
theta_value,
|
|
516
|
+
surface_major[int(line_index), int(section_index), :],
|
|
517
|
+
surface_vertical[int(line_index), int(section_index), :],
|
|
518
|
+
surface_theta[int(line_index), int(section_index), :],
|
|
519
|
+
)
|
|
520
|
+
r_values.append(r_value)
|
|
521
|
+
z_values.append(z_value)
|
|
522
|
+
time_values.append(float(np.interp(target, phi, sample_index)))
|
|
523
|
+
section_values.append(int(section_index))
|
|
524
|
+
line_values.append(int(line_index))
|
|
525
|
+
theta_values.append(theta_value)
|
|
526
|
+
s_values.append(float(np.interp(target, phi, s_coord)))
|
|
527
|
+
return {
|
|
528
|
+
"r": np.asarray(r_values, dtype=np.float64),
|
|
529
|
+
"z": np.asarray(z_values, dtype=np.float64),
|
|
530
|
+
"time_index": np.asarray(time_values, dtype=np.float64),
|
|
531
|
+
"section_index": np.asarray(section_values, dtype=np.int32),
|
|
532
|
+
"line_index": np.asarray(line_values, dtype=np.int32),
|
|
533
|
+
"theta": np.asarray(theta_values, dtype=np.float64),
|
|
534
|
+
"s": np.asarray(s_values, dtype=np.float64),
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _interpolate_surface_section_at_theta(
|
|
539
|
+
theta_value: float,
|
|
540
|
+
major: np.ndarray,
|
|
541
|
+
vertical: np.ndarray,
|
|
542
|
+
theta: np.ndarray,
|
|
543
|
+
) -> tuple[float, float]:
|
|
544
|
+
theta_mod = float(np.mod(theta_value, 2.0 * np.pi))
|
|
545
|
+
theta_curve = np.mod(np.asarray(theta, dtype=np.float64), 2.0 * np.pi)
|
|
546
|
+
order = np.argsort(theta_curve)
|
|
547
|
+
theta_sorted = theta_curve[order]
|
|
548
|
+
major_sorted = np.asarray(major, dtype=np.float64)[order]
|
|
549
|
+
vertical_sorted = np.asarray(vertical, dtype=np.float64)[order]
|
|
550
|
+
theta_ext = np.concatenate([theta_sorted, theta_sorted[:1] + 2.0 * np.pi])
|
|
551
|
+
major_ext = np.concatenate([major_sorted, major_sorted[:1]])
|
|
552
|
+
vertical_ext = np.concatenate([vertical_sorted, vertical_sorted[:1]])
|
|
553
|
+
return float(np.interp(theta_mod, theta_ext, major_ext)), float(np.interp(theta_mod, theta_ext, vertical_ext))
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _distance_to_reference_surfaces(
|
|
557
|
+
hits: dict[str, np.ndarray],
|
|
558
|
+
*,
|
|
559
|
+
surface_major: np.ndarray,
|
|
560
|
+
surface_vertical: np.ndarray,
|
|
561
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
562
|
+
same_distance = np.empty(hits["r"].shape, dtype=np.float64)
|
|
563
|
+
nearest_distance = np.empty_like(same_distance)
|
|
564
|
+
for index, (r_value, z_value, section_index, line_index) in enumerate(
|
|
565
|
+
zip(hits["r"], hits["z"], hits["section_index"], hits["line_index"], strict=True)
|
|
566
|
+
):
|
|
567
|
+
section = int(np.clip(section_index, 0, surface_major.shape[1] - 1))
|
|
568
|
+
line = int(np.clip(line_index, 0, surface_major.shape[0] - 1))
|
|
569
|
+
same_curve_distance = np.sqrt(
|
|
570
|
+
(surface_major[line, section, :] - float(r_value)) ** 2
|
|
571
|
+
+ (surface_vertical[line, section, :] - float(z_value)) ** 2
|
|
572
|
+
)
|
|
573
|
+
all_curve_distance = np.sqrt(
|
|
574
|
+
(surface_major[:, section, :] - float(r_value)) ** 2
|
|
575
|
+
+ (surface_vertical[:, section, :] - float(z_value)) ** 2
|
|
576
|
+
)
|
|
577
|
+
same_distance[index] = float(np.min(same_curve_distance))
|
|
578
|
+
nearest_distance[index] = float(np.min(all_curve_distance))
|
|
579
|
+
return same_distance, nearest_distance
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _reference_minor_extent(surface_major: np.ndarray, surface_vertical: np.ndarray) -> float:
|
|
583
|
+
center_major = np.mean(surface_major, axis=1, keepdims=True)
|
|
584
|
+
radius = np.sqrt((surface_major - center_major) ** 2 + surface_vertical * surface_vertical)
|
|
585
|
+
return float(np.sqrt(np.mean(radius * radius)))
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _median_toroidal_advance(trajectories_xyz: np.ndarray) -> float:
|
|
589
|
+
phi = np.unwrap(np.arctan2(trajectories_xyz[:, :, 1], trajectories_xyz[:, :, 0]), axis=1)
|
|
590
|
+
return float(np.nanmedian(phi[:, -1] - phi[:, 0]))
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _line_span_mean(values: np.ndarray, line_indices: np.ndarray, n_lines: int) -> float:
|
|
594
|
+
spans: list[float] = []
|
|
595
|
+
values_array = np.asarray(values, dtype=np.float64)
|
|
596
|
+
line_array = np.asarray(line_indices, dtype=np.int32)
|
|
597
|
+
for line_index in range(int(n_lines)):
|
|
598
|
+
selected = values_array[line_array == line_index]
|
|
599
|
+
selected = selected[np.isfinite(selected)]
|
|
600
|
+
if selected.size:
|
|
601
|
+
spans.append(float(np.ptp(selected)))
|
|
602
|
+
return float(np.mean(spans)) if spans else 0.0
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _median_or_inf(values: np.ndarray) -> float:
|
|
606
|
+
finite = np.asarray(values, dtype=np.float64)
|
|
607
|
+
finite = finite[np.isfinite(finite)]
|
|
608
|
+
return float(np.median(finite)) if finite.size else float("inf")
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _percentile_or_inf(values: np.ndarray, percentile: float) -> float:
|
|
612
|
+
finite = np.asarray(values, dtype=np.float64)
|
|
613
|
+
finite = finite[np.isfinite(finite)]
|
|
614
|
+
return float(np.percentile(finite, percentile)) if finite.size else float("inf")
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _max_or_inf(values: np.ndarray) -> float:
|
|
618
|
+
finite = np.asarray(values, dtype=np.float64)
|
|
619
|
+
finite = finite[np.isfinite(finite)]
|
|
620
|
+
return float(np.max(finite)) if finite.size else float("inf")
|