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,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
|