drbx 2.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. drbx/__init__.py +20 -0
  2. drbx/__main__.py +5 -0
  3. drbx/cli.py +586 -0
  4. drbx/config/__init__.py +20 -0
  5. drbx/config/boutinp.py +556 -0
  6. drbx/config/model.py +34 -0
  7. drbx/config/normalization.py +66 -0
  8. drbx/data/atomic_rates/__init__.py +1 -0
  9. drbx/data/atomic_rates/iz_AMJUEL_H.x_2.1.5.json +213 -0
  10. drbx/data/atomic_rates/iz_AMJUEL_H.x_2.3.9a.json +213 -0
  11. drbx/data/atomic_rates/rec_AMJUEL_H.x_2.1.8.json +213 -0
  12. drbx/data/atomic_rates/rec_AMJUEL_H.x_2.3.13a.json +213 -0
  13. drbx/geometry/__init__.py +207 -0
  14. drbx/geometry/embedding.py +56 -0
  15. drbx/geometry/essos_import.py +1385 -0
  16. drbx/geometry/fci_geometry.py +4622 -0
  17. drbx/geometry/fci_maps.py +85 -0
  18. drbx/geometry/island_divertor.py +291 -0
  19. drbx/geometry/metric_tensor.py +99 -0
  20. drbx/geometry/open_slab.py +150 -0
  21. drbx/geometry/rotating_ellipse.py +253 -0
  22. drbx/geometry/shifted_torus.py +225 -0
  23. drbx/geometry/stellarator.py +287 -0
  24. drbx/geometry/vmec_extender_import.py +499 -0
  25. drbx/geometry/vmec_jax_import.py +306 -0
  26. drbx/linear/__init__.py +37 -0
  27. drbx/linear/dispersion.py +138 -0
  28. drbx/linear/eigen.py +91 -0
  29. drbx/native/__init__.py +224 -0
  30. drbx/native/array_backend.py +64 -0
  31. drbx/native/deck_runner.py +779 -0
  32. drbx/native/electromagnetic.py +250 -0
  33. drbx/native/expression.py +173 -0
  34. drbx/native/fci.py +295 -0
  35. drbx/native/fci_2_field_rhs.py +182 -0
  36. drbx/native/fci_4_field_rhs.py +1267 -0
  37. drbx/native/fci_boundaries.py +2494 -0
  38. drbx/native/fci_differentiable_case.py +304 -0
  39. drbx/native/fci_drb_EB_rhs.py +1243 -0
  40. drbx/native/fci_drb_rhs.py +190 -0
  41. drbx/native/fci_halo.py +1575 -0
  42. drbx/native/fci_helpers.py +350 -0
  43. drbx/native/fci_model.py +294 -0
  44. drbx/native/fci_neutral.py +139 -0
  45. drbx/native/fci_operators.py +4081 -0
  46. drbx/native/fci_sharding.py +597 -0
  47. drbx/native/fci_sheath_recycling.py +206 -0
  48. drbx/native/fci_time_integrator.py +96 -0
  49. drbx/native/fci_vorticity.py +198 -0
  50. drbx/native/fluid_1d.py +330 -0
  51. drbx/native/hasegawa_wakatani.py +196 -0
  52. drbx/native/limiters.py +57 -0
  53. drbx/native/mesh.py +238 -0
  54. drbx/native/metrics.py +234 -0
  55. drbx/native/neutrals/__init__.py +58 -0
  56. drbx/native/neutrals/atomic_rates.py +134 -0
  57. drbx/native/neutrals/detachment_sol_model.py +221 -0
  58. drbx/native/neutrals/reactions.py +164 -0
  59. drbx/native/neutrals/recycling_sol_model.py +197 -0
  60. drbx/native/sol_flux_tube.py +133 -0
  61. drbx/native/stellarator_turbulence.py +343 -0
  62. drbx/native/transport.py +134 -0
  63. drbx/native/units.py +32 -0
  64. drbx/native/vorticity.py +252 -0
  65. drbx/runtime/__init__.py +53 -0
  66. drbx/runtime/artifacts.py +161 -0
  67. drbx/runtime/memory.py +144 -0
  68. drbx/runtime/output.py +374 -0
  69. drbx/runtime/paths.py +9 -0
  70. drbx/runtime/performance.py +161 -0
  71. drbx/runtime/run_config.py +184 -0
  72. drbx/runtime/scheduler.py +99 -0
  73. drbx/runtime/state.py +40 -0
  74. drbx/validation/__init__.py +424 -0
  75. drbx/validation/autodiff_diffusion.py +329 -0
  76. drbx/validation/autodiff_diffusion_uncertainty.py +235 -0
  77. drbx/validation/diverted_tokamak_movie.py +558 -0
  78. drbx/validation/essos_fieldline_import_campaign.py +181 -0
  79. drbx/validation/essos_imported_artifact_audit.py +535 -0
  80. drbx/validation/essos_imported_drb_movie_campaign.py +2826 -0
  81. drbx/validation/essos_imported_fci_campaign.py +5241 -0
  82. drbx/validation/essos_imported_pytree_campaign.py +406 -0
  83. drbx/validation/essos_vmec_closed_field_campaign.py +314 -0
  84. drbx/validation/essos_vmec_closed_field_transient_campaign.py +629 -0
  85. drbx/validation/essos_vmec_fieldline_surface_campaign.py +620 -0
  86. drbx/validation/fluid_1d_mms_convergence.py +250 -0
  87. drbx/validation/geometry_lineouts.py +136 -0
  88. drbx/validation/geometry_slices.py +178 -0
  89. drbx/validation/publication_plotting.py +91 -0
  90. drbx/validation/stellarator_drb_pytree_campaign.py +621 -0
  91. drbx/validation/stellarator_fci_geometry_campaign.py +200 -0
  92. drbx/validation/stellarator_fci_operator_campaign.py +304 -0
  93. drbx/validation/stellarator_fci_suite_campaign.py +264 -0
  94. drbx/validation/stellarator_metric_mms_campaign.py +289 -0
  95. drbx/validation/stellarator_neutral_physics_campaign.py +255 -0
  96. drbx/validation/stellarator_sheath_recycling_campaign.py +331 -0
  97. drbx/validation/stellarator_sol_showcase.py +628 -0
  98. drbx/validation/stellarator_vorticity_campaign.py +304 -0
  99. drbx/validation/vmec_extender_edge_field_campaign.py +260 -0
  100. drbx/validation/vmec_extender_sol_smoke_campaign.py +365 -0
  101. drbx-2.0.0.dev0.dist-info/METADATA +380 -0
  102. drbx-2.0.0.dev0.dist-info/RECORD +106 -0
  103. drbx-2.0.0.dev0.dist-info/WHEEL +5 -0
  104. drbx-2.0.0.dev0.dist-info/entry_points.txt +2 -0
  105. drbx-2.0.0.dev0.dist-info/licenses/LICENSE +21 -0
  106. drbx-2.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,329 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ import jax
7
+ import jax.numpy as jnp
8
+ import numpy as np
9
+
10
+ from ..config.boutinp import parse_bout_input
11
+ from ..native.mesh import StructuredMesh, build_structured_mesh
12
+ from ..native.metrics import StructuredMetrics, build_structured_metrics
13
+ from ..native.transport import advance_anomalous_diffusion_history
14
+ from ..runtime.run_config import RunConfiguration
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class DiffusionAutodiffSetup:
19
+ config_text: str
20
+ run_config: RunConfiguration
21
+ mesh: StructuredMesh
22
+ metrics: StructuredMetrics
23
+ timestep: float
24
+ steps: int
25
+ density_boundary: str
26
+ pressure_boundary: str
27
+ background_density: float
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class StrongScalingPoint:
32
+ backend: str
33
+ device_count: int
34
+ elapsed_seconds: float
35
+ speedup: float
36
+ efficiency: float
37
+
38
+
39
+ def build_diffusion_autodiff_setup(
40
+ *,
41
+ nx: int = 128,
42
+ ny: int = 24,
43
+ timestep: float = 5.0,
44
+ steps: int = 6,
45
+ background_density: float = 1.0,
46
+ ) -> DiffusionAutodiffSetup:
47
+ config_text = f"""
48
+ nout = {steps}
49
+ timestep = {timestep}
50
+
51
+ [mesh]
52
+ nx = {nx}
53
+ ny = {ny}
54
+ nz = 1
55
+ dx = {1.0 / nx:.12f}
56
+ dy = {1.0 / ny:.12f}
57
+ dz = 1.0
58
+ J = 1
59
+ g11 = 1
60
+ g_22 = 1
61
+ g33 = 1
62
+
63
+ [solver]
64
+ type = native
65
+
66
+ [model]
67
+ components = h
68
+
69
+ [h]
70
+ type = evolve_density, evolve_pressure, anomalous_diffusion
71
+ AA = 1
72
+ charge = 1
73
+ anomalous_D = 1.0
74
+ thermal_conduction = false
75
+
76
+ [Nh]
77
+ bndry_all = neumann
78
+
79
+ [Ph]
80
+ bndry_all = neumann
81
+ """
82
+ config = parse_bout_input(config_text)
83
+ run_config = RunConfiguration.from_config(config)
84
+ mesh = build_structured_mesh(config, run_config)
85
+ metrics = build_structured_metrics(config, run_config, mesh)
86
+ return DiffusionAutodiffSetup(
87
+ config_text=config_text,
88
+ run_config=run_config,
89
+ mesh=mesh,
90
+ metrics=metrics,
91
+ timestep=timestep,
92
+ steps=steps,
93
+ density_boundary="neumann",
94
+ pressure_boundary="neumann",
95
+ background_density=background_density,
96
+ )
97
+
98
+
99
+ def theta_to_physical(theta: jnp.ndarray) -> dict[str, jnp.ndarray]:
100
+ theta = jnp.asarray(theta, dtype=jnp.float64)
101
+ diffusivity = 0.05 + jax.nn.softplus(theta[0])
102
+ amplitude = 0.02 + jax.nn.softplus(theta[1])
103
+ center = jax.nn.sigmoid(theta[2])
104
+ width = 0.04 + 0.26 * jax.nn.sigmoid(theta[3])
105
+ return {
106
+ "anomalous_D": diffusivity,
107
+ "amplitude": amplitude,
108
+ "center": center,
109
+ "width": width,
110
+ }
111
+
112
+
113
+ def physical_to_theta(
114
+ *,
115
+ anomalous_D: float,
116
+ amplitude: float,
117
+ center: float,
118
+ width: float,
119
+ ) -> jnp.ndarray:
120
+ center = float(np.clip(center, 1.0e-6, 1.0 - 1.0e-6))
121
+ width_scaled = (float(width) - 0.04) / 0.26
122
+ width_scaled = float(np.clip(width_scaled, 1.0e-6, 1.0 - 1.0e-6))
123
+
124
+ def inverse_softplus(value: float) -> float:
125
+ shifted = max(value - 0.05, 1.0e-6)
126
+ return float(np.log(np.expm1(shifted)))
127
+
128
+ def inverse_softplus_amp(value: float) -> float:
129
+ shifted = max(value - 0.02, 1.0e-6)
130
+ return float(np.log(np.expm1(shifted)))
131
+
132
+ return jnp.asarray(
133
+ [
134
+ inverse_softplus(anomalous_D),
135
+ inverse_softplus_amp(amplitude),
136
+ np.log(center / (1.0 - center)),
137
+ np.log(width_scaled / (1.0 - width_scaled)),
138
+ ],
139
+ dtype=jnp.float64,
140
+ )
141
+
142
+
143
+ def design_field(setup: DiffusionAutodiffSetup, theta: jnp.ndarray) -> jnp.ndarray:
144
+ physical = theta_to_physical(theta)
145
+ return design_field_from_physical(
146
+ setup,
147
+ anomalous_D=physical["anomalous_D"],
148
+ amplitude=physical["amplitude"],
149
+ center=physical["center"],
150
+ width=physical["width"],
151
+ )
152
+
153
+
154
+ def design_field_from_physical(
155
+ setup: DiffusionAutodiffSetup,
156
+ *,
157
+ anomalous_D: jnp.ndarray,
158
+ amplitude: jnp.ndarray,
159
+ center: jnp.ndarray,
160
+ width: jnp.ndarray,
161
+ ) -> jnp.ndarray:
162
+ x = jnp.linspace(0.0, 1.0, setup.mesh.nx, dtype=jnp.float64)[:, None, None]
163
+ y = jnp.linspace(0.0, 1.0, setup.mesh.local_ny, dtype=jnp.float64)[None, :, None]
164
+ profile = jnp.exp(-0.5 * ((x - center) / width) ** 2)
165
+ y_modulation = 1.0 + 0.15 * jnp.cos(2.0 * jnp.pi * y)
166
+ return setup.background_density + amplitude * profile * y_modulation
167
+
168
+
169
+ def simulate_density_history(setup: DiffusionAutodiffSetup, theta: jnp.ndarray) -> jnp.ndarray:
170
+ physical = theta_to_physical(theta)
171
+ return simulate_density_history_from_physical(
172
+ setup,
173
+ anomalous_D=physical["anomalous_D"],
174
+ amplitude=physical["amplitude"],
175
+ center=physical["center"],
176
+ width=physical["width"],
177
+ )
178
+
179
+
180
+ def simulate_density_history_from_physical(
181
+ setup: DiffusionAutodiffSetup,
182
+ *,
183
+ anomalous_D: jnp.ndarray,
184
+ amplitude: jnp.ndarray,
185
+ center: jnp.ndarray,
186
+ width: jnp.ndarray,
187
+ ) -> jnp.ndarray:
188
+ density0 = design_field_from_physical(
189
+ setup,
190
+ anomalous_D=anomalous_D,
191
+ amplitude=amplitude,
192
+ center=center,
193
+ width=width,
194
+ )
195
+ history = advance_anomalous_diffusion_history(
196
+ density0,
197
+ density0,
198
+ mesh=setup.mesh,
199
+ metrics=setup.metrics,
200
+ anomalous_D=anomalous_D,
201
+ density_boundary=setup.density_boundary,
202
+ pressure_boundary=setup.pressure_boundary,
203
+ timestep=setup.timestep,
204
+ steps=setup.steps,
205
+ )
206
+ return history.density_history
207
+
208
+
209
+ def active_density_slice(setup: DiffusionAutodiffSetup, field: jnp.ndarray) -> jnp.ndarray:
210
+ return field[setup.mesh.xstart : setup.mesh.xend + 1, setup.mesh.ystart : setup.mesh.yend + 1, 0]
211
+
212
+
213
+ def objective_for_parameter_vector(
214
+ theta: jnp.ndarray,
215
+ setup: DiffusionAutodiffSetup,
216
+ *,
217
+ target_final_density: jnp.ndarray | None = None,
218
+ objective_kind: str = "target_misfit",
219
+ ) -> jnp.ndarray:
220
+ history = simulate_density_history(setup, theta)
221
+ final_density = active_density_slice(setup, history[-1])
222
+ if objective_kind == "variance":
223
+ centered = final_density - jnp.mean(final_density)
224
+ return jnp.mean(centered * centered)
225
+ if target_final_density is None:
226
+ raise ValueError("target_final_density is required for target_misfit objectives")
227
+ target = jnp.asarray(target_final_density, dtype=jnp.float64)
228
+ mismatch = final_density - target
229
+ return 0.5 * jnp.mean(mismatch * mismatch)
230
+
231
+
232
+ def objective_for_physical_parameters(
233
+ parameters: jnp.ndarray,
234
+ setup: DiffusionAutodiffSetup,
235
+ *,
236
+ target_final_density: jnp.ndarray | None = None,
237
+ objective_kind: str = "target_misfit",
238
+ ) -> jnp.ndarray:
239
+ parameters = jnp.asarray(parameters, dtype=jnp.float64)
240
+ history = simulate_density_history_from_physical(
241
+ setup,
242
+ anomalous_D=parameters[0],
243
+ amplitude=parameters[1],
244
+ center=parameters[2],
245
+ width=parameters[3],
246
+ )
247
+ final_density = active_density_slice(setup, history[-1])
248
+ if objective_kind == "variance":
249
+ centered = final_density - jnp.mean(final_density)
250
+ return jnp.mean(centered * centered)
251
+ if target_final_density is None:
252
+ raise ValueError("target_final_density is required for target_misfit objectives")
253
+ target = jnp.asarray(target_final_density, dtype=jnp.float64)
254
+ mismatch = final_density - target
255
+ return 0.5 * jnp.mean(mismatch * mismatch)
256
+
257
+
258
+ def finite_difference_gradient(
259
+ objective_fn,
260
+ theta: jnp.ndarray,
261
+ *,
262
+ epsilon: float = 1.0e-4,
263
+ ) -> jnp.ndarray:
264
+ theta = jnp.asarray(theta, dtype=jnp.float64)
265
+ basis = jnp.eye(theta.size, dtype=jnp.float64)
266
+ values = []
267
+ for direction in basis:
268
+ forward = objective_fn(theta + epsilon * direction)
269
+ backward = objective_fn(theta - epsilon * direction)
270
+ values.append((forward - backward) / (2.0 * epsilon))
271
+ return jnp.asarray(values, dtype=jnp.float64)
272
+
273
+
274
+ def optimize_inverse_design(
275
+ objective_fn,
276
+ theta0: jnp.ndarray,
277
+ *,
278
+ iterations: int = 80,
279
+ learning_rate: float = 0.05,
280
+ beta1: float = 0.9,
281
+ beta2: float = 0.999,
282
+ epsilon: float = 1.0e-8,
283
+ ) -> dict[str, Any]:
284
+ theta = jnp.asarray(theta0, dtype=jnp.float64)
285
+ m = jnp.zeros_like(theta)
286
+ v = jnp.zeros_like(theta)
287
+ loss_history: list[float] = []
288
+ theta_history: list[np.ndarray] = [np.asarray(theta)]
289
+ value_and_grad_fn = jax.jit(jax.value_and_grad(objective_fn))
290
+ objective_only_fn = jax.jit(objective_fn)
291
+
292
+ for iteration in range(1, iterations + 1):
293
+ loss_value, gradient = value_and_grad_fn(theta)
294
+ m = beta1 * m + (1.0 - beta1) * gradient
295
+ v = beta2 * v + (1.0 - beta2) * (gradient * gradient)
296
+ m_hat = m / (1.0 - beta1**iteration)
297
+ v_hat = v / (1.0 - beta2**iteration)
298
+ theta = theta - learning_rate * m_hat / (jnp.sqrt(v_hat) + epsilon)
299
+ loss_history.append(float(loss_value))
300
+ theta_history.append(np.asarray(theta))
301
+
302
+ final_loss = float(objective_only_fn(theta))
303
+ return {
304
+ "theta": theta,
305
+ "loss_history": np.asarray(loss_history, dtype=np.float64),
306
+ "theta_history": np.asarray(theta_history, dtype=np.float64),
307
+ "final_loss": final_loss,
308
+ }
309
+
310
+
311
+ def compute_strong_scaling_points(points: list[tuple[int, float]], *, backend: str) -> list[StrongScalingPoint]:
312
+ if not points:
313
+ return []
314
+ ordered = sorted(points, key=lambda item: item[0])
315
+ baseline_devices, baseline_time = ordered[0]
316
+ result: list[StrongScalingPoint] = []
317
+ for device_count, elapsed_seconds in ordered:
318
+ speedup = baseline_time / elapsed_seconds
319
+ efficiency = speedup / float(device_count / baseline_devices)
320
+ result.append(
321
+ StrongScalingPoint(
322
+ backend=backend,
323
+ device_count=device_count,
324
+ elapsed_seconds=elapsed_seconds,
325
+ speedup=speedup,
326
+ efficiency=efficiency,
327
+ )
328
+ )
329
+ return result
@@ -0,0 +1,235 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ import jax
8
+ import jax.numpy as jnp
9
+ from matplotlib import pyplot as plt
10
+ import numpy as np
11
+
12
+ from .autodiff_diffusion import (
13
+ active_density_slice,
14
+ build_diffusion_autodiff_setup,
15
+ objective_for_physical_parameters,
16
+ simulate_density_history_from_physical,
17
+ )
18
+ from .publication_plotting import save_publication_figure, style_axis
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class AutodiffDiffusionUncertaintyArtifacts:
23
+ analysis_json_path: Path
24
+ arrays_npz_path: Path
25
+ plot_png_path: Path
26
+
27
+
28
+ def create_autodiff_diffusion_uncertainty_package(
29
+ *,
30
+ output_root: str | Path,
31
+ case_label: str = "autodiff_diffusion_uncertainty",
32
+ sample_count: int = 96,
33
+ random_seed: int = 7,
34
+ ) -> AutodiffDiffusionUncertaintyArtifacts:
35
+ root = Path(output_root)
36
+ data_dir = root / "data"
37
+ images_dir = root / "images"
38
+ data_dir.mkdir(parents=True, exist_ok=True)
39
+ images_dir.mkdir(parents=True, exist_ok=True)
40
+
41
+ report, arrays = build_autodiff_diffusion_uncertainty_report(
42
+ sample_count=sample_count,
43
+ random_seed=random_seed,
44
+ )
45
+ analysis_json_path = data_dir / f"{case_label}_analysis.json"
46
+ arrays_npz_path = data_dir / f"{case_label}_arrays.npz"
47
+ plot_png_path = images_dir / f"{case_label}.png"
48
+ analysis_json_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8")
49
+ np.savez_compressed(arrays_npz_path, **arrays)
50
+ save_autodiff_diffusion_uncertainty_plot(report, arrays, plot_png_path)
51
+ return AutodiffDiffusionUncertaintyArtifacts(
52
+ analysis_json_path=analysis_json_path,
53
+ arrays_npz_path=arrays_npz_path,
54
+ plot_png_path=plot_png_path,
55
+ )
56
+
57
+
58
+ def build_autodiff_diffusion_uncertainty_report(
59
+ *,
60
+ sample_count: int = 96,
61
+ random_seed: int = 7,
62
+ ) -> tuple[dict[str, object], dict[str, np.ndarray]]:
63
+ setup = build_diffusion_autodiff_setup(nx=160, ny=24, timestep=3.0, steps=8)
64
+
65
+ mean_parameters = jnp.asarray([0.34, 0.15, 0.48, 0.14], dtype=jnp.float64)
66
+ parameter_std = jnp.asarray([0.03, 0.025, 0.04, 0.02], dtype=jnp.float64)
67
+ covariance = jnp.diag(parameter_std**2)
68
+
69
+ def radial_profile(parameters: jnp.ndarray) -> jnp.ndarray:
70
+ history = simulate_density_history_from_physical(
71
+ setup,
72
+ anomalous_D=parameters[0],
73
+ amplitude=parameters[1],
74
+ center=parameters[2],
75
+ width=parameters[3],
76
+ )
77
+ final_density = active_density_slice(setup, history[-1])
78
+ return jnp.mean(final_density, axis=1)
79
+
80
+ def scalar_qoi(parameters: jnp.ndarray) -> jnp.ndarray:
81
+ return objective_for_physical_parameters(
82
+ parameters,
83
+ setup,
84
+ objective_kind="variance",
85
+ )
86
+
87
+ mean_profile = radial_profile(mean_parameters)
88
+ mean_qoi = scalar_qoi(mean_parameters)
89
+ profile_jacobian = jax.jacobian(radial_profile)(mean_parameters)
90
+ qoi_gradient = jax.grad(scalar_qoi)(mean_parameters)
91
+
92
+ linearized_profile_variance = jnp.einsum("ik,kl,il->i", profile_jacobian, covariance, profile_jacobian)
93
+ linearized_profile_sigma = jnp.sqrt(jnp.maximum(linearized_profile_variance, 0.0))
94
+ linearized_qoi_sigma = jnp.sqrt(jnp.maximum(qoi_gradient @ covariance @ qoi_gradient, 0.0))
95
+
96
+ rng = np.random.default_rng(random_seed)
97
+ raw_samples = rng.normal(
98
+ loc=np.asarray(mean_parameters, dtype=np.float64),
99
+ scale=np.asarray(parameter_std, dtype=np.float64),
100
+ size=(sample_count, mean_parameters.size),
101
+ )
102
+ sampled_parameters = np.asarray(raw_samples, dtype=np.float64)
103
+ sampled_parameters[:, 0] = np.clip(sampled_parameters[:, 0], 0.08, None)
104
+ sampled_parameters[:, 1] = np.clip(sampled_parameters[:, 1], 0.03, None)
105
+ sampled_parameters[:, 2] = np.clip(sampled_parameters[:, 2], 0.05, 0.95)
106
+ sampled_parameters[:, 3] = np.clip(sampled_parameters[:, 3], 0.05, 0.28)
107
+
108
+ batched_profile = jax.jit(jax.vmap(radial_profile))
109
+ batched_qoi = jax.jit(jax.vmap(scalar_qoi))
110
+ sampled_profiles = np.asarray(batched_profile(jnp.asarray(sampled_parameters, dtype=jnp.float64)), dtype=np.float64)
111
+ sampled_qoi = np.asarray(batched_qoi(jnp.asarray(sampled_parameters, dtype=jnp.float64)), dtype=np.float64)
112
+
113
+ monte_carlo_profile_mean = sampled_profiles.mean(axis=0)
114
+ monte_carlo_profile_sigma = sampled_profiles.std(axis=0, ddof=1)
115
+ monte_carlo_qoi_mean = float(sampled_qoi.mean())
116
+ monte_carlo_qoi_sigma = float(sampled_qoi.std(ddof=1))
117
+
118
+ profile_sigma_gap = float(np.max(np.abs(monte_carlo_profile_sigma - np.asarray(linearized_profile_sigma))))
119
+ qoi_sigma_relative_error = float(
120
+ abs(float(linearized_qoi_sigma) - monte_carlo_qoi_sigma) / max(monte_carlo_qoi_sigma, 1.0e-12)
121
+ )
122
+
123
+ report = {
124
+ "case": "autodiff_diffusion_uncertainty",
125
+ "sample_count": sample_count,
126
+ "random_seed": random_seed,
127
+ "mean_parameters": np.asarray(mean_parameters, dtype=np.float64).tolist(),
128
+ "parameter_std": np.asarray(parameter_std, dtype=np.float64).tolist(),
129
+ "scalar_qoi_name": "final_density_variance",
130
+ "scalar_qoi_at_mean": float(mean_qoi),
131
+ "linearized_qoi_sigma": float(linearized_qoi_sigma),
132
+ "monte_carlo_qoi_mean": monte_carlo_qoi_mean,
133
+ "monte_carlo_qoi_sigma": monte_carlo_qoi_sigma,
134
+ "qoi_sigma_relative_error": qoi_sigma_relative_error,
135
+ "profile_sigma_max_abs_gap": profile_sigma_gap,
136
+ "interpretation": [
137
+ "The scalar QoI uses the variance of the final active-domain density field on the compact native diffusion lane.",
138
+ "The profile uncertainty uses the radial mean of the final active-domain density field.",
139
+ "The comparison is between a first-order autodiff pushforward of the parameter covariance and a vectorized Monte Carlo estimate on the same native solve path.",
140
+ ],
141
+ }
142
+ arrays = {
143
+ "mean_parameters": np.asarray(mean_parameters, dtype=np.float64),
144
+ "parameter_std": np.asarray(parameter_std, dtype=np.float64),
145
+ "radial_coordinate": np.linspace(0.0, 1.0, mean_profile.shape[0], dtype=np.float64),
146
+ "mean_profile": np.asarray(mean_profile, dtype=np.float64),
147
+ "linearized_profile_sigma": np.asarray(linearized_profile_sigma, dtype=np.float64),
148
+ "monte_carlo_profile_mean": monte_carlo_profile_mean,
149
+ "monte_carlo_profile_sigma": monte_carlo_profile_sigma,
150
+ "sampled_qoi": sampled_qoi,
151
+ "sampled_parameters": sampled_parameters,
152
+ "qoi_gradient": np.asarray(qoi_gradient, dtype=np.float64),
153
+ }
154
+ return report, arrays
155
+
156
+
157
+ def save_autodiff_diffusion_uncertainty_plot(
158
+ report: dict[str, object],
159
+ arrays: dict[str, np.ndarray],
160
+ path: str | Path,
161
+ ) -> Path:
162
+ target = Path(path)
163
+ target.parent.mkdir(parents=True, exist_ok=True)
164
+
165
+ figure, axes = plt.subplots(1, 3, figsize=(16.2, 4.8), constrained_layout=True)
166
+
167
+ labels = ["D", "A", "center", "width"]
168
+ x = np.arange(len(labels))
169
+ axes[0].bar(x, arrays["mean_parameters"], color="#4c78a8", alpha=0.85)
170
+ axes[0].errorbar(
171
+ x,
172
+ arrays["mean_parameters"],
173
+ yerr=arrays["parameter_std"],
174
+ fmt="none",
175
+ ecolor="#111111",
176
+ elinewidth=2.0,
177
+ capsize=5,
178
+ )
179
+ axes[0].set_xticks(x, labels)
180
+ style_axis(axes[0], title="Input uncertainty model", ylabel="parameter value", grid="y")
181
+
182
+ sampled_qoi = arrays["sampled_qoi"]
183
+ axes[1].hist(sampled_qoi, bins=18, color="#72b7b2", alpha=0.8, density=True, label="Monte Carlo")
184
+ qoi_mean = float(report["scalar_qoi_at_mean"])
185
+ qoi_sigma = float(report["linearized_qoi_sigma"])
186
+ qoi_x = np.linspace(sampled_qoi.min(), sampled_qoi.max(), 240, dtype=np.float64)
187
+ normal_pdf = np.exp(-0.5 * ((qoi_x - qoi_mean) / max(qoi_sigma, 1.0e-12)) ** 2) / max(
188
+ qoi_sigma * np.sqrt(2.0 * np.pi),
189
+ 1.0e-12,
190
+ )
191
+ axes[1].plot(qoi_x, normal_pdf, color="#d1495b", linewidth=2.4, label="Linearized Gaussian")
192
+ axes[1].axvline(float(report["monte_carlo_qoi_mean"]), color="#111111", linestyle="--", linewidth=1.5)
193
+ style_axis(
194
+ axes[1],
195
+ title="Scalar uncertainty propagation",
196
+ xlabel="final-density variance QoI",
197
+ ylabel="density",
198
+ grid="both",
199
+ )
200
+ axes[1].legend(frameon=False)
201
+
202
+ radial = arrays["radial_coordinate"]
203
+ mean_profile = arrays["mean_profile"]
204
+ linear_sigma = arrays["linearized_profile_sigma"]
205
+ mc_mean = arrays["monte_carlo_profile_mean"]
206
+ mc_sigma = arrays["monte_carlo_profile_sigma"]
207
+ axes[2].plot(radial, mean_profile, color="#111111", linewidth=2.5, label="mean profile")
208
+ axes[2].fill_between(
209
+ radial,
210
+ mean_profile - 2.0 * linear_sigma,
211
+ mean_profile + 2.0 * linear_sigma,
212
+ color="#f58518",
213
+ alpha=0.28,
214
+ label="linearized 95% band",
215
+ )
216
+ axes[2].plot(radial, mc_mean, color="#2a9d8f", linewidth=2.0, linestyle="--", label="MC mean profile")
217
+ axes[2].fill_between(
218
+ radial,
219
+ mc_mean - 2.0 * mc_sigma,
220
+ mc_mean + 2.0 * mc_sigma,
221
+ color="#54a24b",
222
+ alpha=0.18,
223
+ label="Monte Carlo 95% band",
224
+ )
225
+ style_axis(
226
+ axes[2],
227
+ title="Profile uncertainty pushforward",
228
+ xlabel="normalized radial coordinate",
229
+ ylabel="radial mean density",
230
+ grid="both",
231
+ )
232
+ axes[2].legend(frameon=False, fontsize=9)
233
+
234
+ save_publication_figure(figure, target)
235
+ return target