simjecture 0.1.0__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.
- conjecture_solver/__init__.py +10 -0
- conjecture_solver/__main__.py +4 -0
- conjecture_solver/action_handlers.py +459 -0
- conjecture_solver/adapters/__init__.py +7 -0
- conjecture_solver/adapters/base.py +96 -0
- conjecture_solver/adapters/fake.py +155 -0
- conjecture_solver/adapters/pic.py +196 -0
- conjecture_solver/adapters/warpx.py +1375 -0
- conjecture_solver/autonomous_research.py +1071 -0
- conjecture_solver/benchmarks/__init__.py +6 -0
- conjecture_solver/benchmarks/electrostatic_pic.py +547 -0
- conjecture_solver/benchmarks/kinetic_sufficiency.py +318 -0
- conjecture_solver/builtin_skills/warpx/SKILL.md +151 -0
- conjecture_solver/builtin_skills/warpx/agents/openai.yaml +4 -0
- conjecture_solver/builtin_skills/warpx/examples/implicit_em_smoke.py +74 -0
- conjecture_solver/builtin_skills/warpx/examples/minimal_smoke.py +54 -0
- conjecture_solver/builtin_skills/warpx/examples/openpmd_field_smoke.py +87 -0
- conjecture_solver/builtin_skills/warpx/manifest.json +11 -0
- conjecture_solver/builtin_skills/warpx/references/2d-xz-commissioning.md +145 -0
- conjecture_solver/builtin_skills/warpx/references/cpu-launch-tuning.md +54 -0
- conjecture_solver/builtin_skills/warpx/references/diagnostics.md +139 -0
- conjecture_solver/builtin_skills/warpx/references/gpu-launch-tuning.md +79 -0
- conjecture_solver/builtin_skills/warpx/references/local-cuda-deployment.md +87 -0
- conjecture_solver/builtin_skills/warpx/references/numerical-risks.md +32 -0
- conjecture_solver/builtin_skills/warpx/references/picmi-interface.md +118 -0
- conjecture_solver/builtin_skills/warpx/references/resource-scaling.md +24 -0
- conjecture_solver/builtin_skills/warpx/references/time-integration.md +58 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_cpu_threads.py +207 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_gpu.py +242 -0
- conjecture_solver/builtin_skills/warpx/scripts/bootstrap_local_cuda.sh +82 -0
- conjecture_solver/builtin_skills/warpx/scripts/probe_local_cuda.py +110 -0
- conjecture_solver/builtin_skills/warpx/scripts/reduced_energy_budget.py +195 -0
- conjecture_solver/builtin_skills/warpx/scripts/run_local_cuda.sh +39 -0
- conjecture_solver/campaign.py +406 -0
- conjecture_solver/cli.py +963 -0
- conjecture_solver/confirmation.py +287 -0
- conjecture_solver/control.py +234 -0
- conjecture_solver/deployment.py +874 -0
- conjecture_solver/discovery.py +68 -0
- conjecture_solver/domains/__init__.py +40 -0
- conjecture_solver/domains/base.py +157 -0
- conjecture_solver/domains/kinetic_sufficiency.py +521 -0
- conjecture_solver/ledger.py +238 -0
- conjecture_solver/lifecycle.py +131 -0
- conjecture_solver/literature.py +494 -0
- conjecture_solver/llm.py +228 -0
- conjecture_solver/models.py +279 -0
- conjecture_solver/mvp_agent.py +3212 -0
- conjecture_solver/mvp_claims.py +1279 -0
- conjecture_solver/mvp_control.py +322 -0
- conjecture_solver/mvp_guidance.py +172 -0
- conjecture_solver/mvp_launch.py +1296 -0
- conjecture_solver/mvp_monitor.py +1487 -0
- conjecture_solver/mvp_skills.py +538 -0
- conjecture_solver/orchestration.py +578 -0
- conjecture_solver/outbox.py +526 -0
- conjecture_solver/parameters.py +394 -0
- conjecture_solver/proposals.py +427 -0
- conjecture_solver/research_tools.py +278 -0
- conjecture_solver/schema_export.py +246 -0
- conjecture_solver/search.py +1047 -0
- conjecture_solver/semantics.py +146 -0
- conjecture_solver/tui/__init__.py +7 -0
- conjecture_solver/tui/app.py +202 -0
- conjecture_solver/tui/claim_views.py +213 -0
- conjecture_solver/tui/screens.py +1014 -0
- conjecture_solver/warpx_analysis.py +184 -0
- conjecture_solver/warpx_campaign.py +148 -0
- conjecture_solver/warpx_confirmation.py +617 -0
- simjecture-0.1.0.dist-info/METADATA +400 -0
- simjecture-0.1.0.dist-info/RECORD +75 -0
- simjecture-0.1.0.dist-info/WHEEL +4 -0
- simjecture-0.1.0.dist-info/entry_points.txt +4 -0
- simjecture-0.1.0.dist-info/licenses/LICENSE +202 -0
- simjecture-0.1.0.dist-info/licenses/NOTICE +6 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Matched-moment kinetic stability counterexample.
|
|
2
|
+
|
|
3
|
+
The normalization uses electron plasma frequency equal to one. Ions are an
|
|
4
|
+
immobile neutralizing background. For a Gaussian component with weight a,
|
|
5
|
+
drift u, and standard deviation sigma, its electrostatic susceptibility is
|
|
6
|
+
|
|
7
|
+
a / (k sigma)^2 * [1 + zeta Z(zeta)]
|
|
8
|
+
|
|
9
|
+
where zeta = (omega - k u) / (sqrt(2) k sigma) and Z is the plasma dispersion
|
|
10
|
+
function. The least-damped or fastest-growing root is the primary observable.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import warnings
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
from pydantic import Field, model_validator
|
|
19
|
+
from scipy.optimize import root
|
|
20
|
+
from scipy.special import wofz
|
|
21
|
+
|
|
22
|
+
from ..models import (
|
|
23
|
+
DomainSpec,
|
|
24
|
+
EvidenceContract,
|
|
25
|
+
HypothesisNode,
|
|
26
|
+
HypothesisOrigin,
|
|
27
|
+
MatchedPairFormalPredicate,
|
|
28
|
+
ObservableSpec,
|
|
29
|
+
PropositionClass,
|
|
30
|
+
StrictModel,
|
|
31
|
+
)
|
|
32
|
+
from ..semantics import MatchedObservation, SufficiencyWitness, evaluate_predictive_sufficiency
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GaussianComponent(StrictModel):
|
|
36
|
+
weight: float = Field(gt=0, le=1)
|
|
37
|
+
drift: float
|
|
38
|
+
sigma: float = Field(gt=0)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GaussianMixture(StrictModel):
|
|
42
|
+
components: tuple[GaussianComponent, ...] = Field(min_length=1)
|
|
43
|
+
|
|
44
|
+
@model_validator(mode="after")
|
|
45
|
+
def normalized_weights(self) -> GaussianMixture:
|
|
46
|
+
if abs(sum(component.weight for component in self.components) - 1.0) > 1e-12:
|
|
47
|
+
raise ValueError("component weights must sum to one")
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def density(self) -> float:
|
|
51
|
+
return sum(component.weight for component in self.components)
|
|
52
|
+
|
|
53
|
+
def mean(self) -> float:
|
|
54
|
+
return sum(component.weight * component.drift for component in self.components)
|
|
55
|
+
|
|
56
|
+
def second_moment(self) -> float:
|
|
57
|
+
return sum(
|
|
58
|
+
component.weight * (component.sigma**2 + component.drift**2)
|
|
59
|
+
for component in self.components
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def variance(self) -> float:
|
|
63
|
+
return self.second_moment() - self.mean() ** 2
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DistributionMoments(StrictModel):
|
|
67
|
+
density: float
|
|
68
|
+
mean_velocity: float
|
|
69
|
+
variance: float = Field(ge=0)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LinearMode(StrictModel):
|
|
73
|
+
frequency: float
|
|
74
|
+
growth_rate: float
|
|
75
|
+
dielectric_residual: float = Field(ge=0)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class KineticCaseResult(StrictModel):
|
|
79
|
+
name: str
|
|
80
|
+
distribution: GaussianMixture
|
|
81
|
+
moments: DistributionMoments
|
|
82
|
+
mode: LinearMode
|
|
83
|
+
classification: str
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class KineticSufficiencyResult(StrictModel):
|
|
87
|
+
schema_version: str = "0.1.0"
|
|
88
|
+
experiment_id: str = "planted_kinetic_sufficiency_v1"
|
|
89
|
+
hypothesis: HypothesisNode
|
|
90
|
+
observable: ObservableSpec
|
|
91
|
+
wavenumber: float = Field(gt=0)
|
|
92
|
+
maxwellian: KineticCaseResult
|
|
93
|
+
two_stream: KineticCaseResult
|
|
94
|
+
moments_match: bool
|
|
95
|
+
witness: SufficiencyWitness
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def plasma_dispersion(zeta: complex) -> complex:
|
|
99
|
+
return 1j * np.sqrt(np.pi) * wofz(zeta)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def dielectric(
|
|
103
|
+
omega: complex,
|
|
104
|
+
wavenumber: float,
|
|
105
|
+
distribution: GaussianMixture,
|
|
106
|
+
) -> complex:
|
|
107
|
+
value = 1.0 + 0.0j
|
|
108
|
+
with np.errstate(all="ignore"):
|
|
109
|
+
for component in distribution.components:
|
|
110
|
+
zeta = (omega - wavenumber * component.drift) / (
|
|
111
|
+
np.sqrt(2.0) * wavenumber * component.sigma
|
|
112
|
+
)
|
|
113
|
+
value += component.weight / (wavenumber * component.sigma) ** 2 * (
|
|
114
|
+
1.0 + zeta * plasma_dispersion(zeta)
|
|
115
|
+
)
|
|
116
|
+
return complex(value)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def solve_modes(
|
|
120
|
+
distribution: GaussianMixture,
|
|
121
|
+
*,
|
|
122
|
+
wavenumber: float,
|
|
123
|
+
residual_tolerance: float = 1e-8,
|
|
124
|
+
) -> tuple[LinearMode, ...]:
|
|
125
|
+
guesses = (
|
|
126
|
+
complex(real, imaginary)
|
|
127
|
+
for real in np.linspace(-2.0, 2.0, 9)
|
|
128
|
+
for imaginary in np.linspace(-0.8, 0.6, 8)
|
|
129
|
+
)
|
|
130
|
+
modes: list[LinearMode] = []
|
|
131
|
+
for guess in guesses:
|
|
132
|
+
with warnings.catch_warnings(), np.errstate(all="ignore"):
|
|
133
|
+
warnings.simplefilter("ignore", RuntimeWarning)
|
|
134
|
+
solution = root(
|
|
135
|
+
lambda pair: (
|
|
136
|
+
dielectric(
|
|
137
|
+
complex(float(pair[0]), float(pair[1])),
|
|
138
|
+
wavenumber,
|
|
139
|
+
distribution,
|
|
140
|
+
).real,
|
|
141
|
+
dielectric(
|
|
142
|
+
complex(float(pair[0]), float(pair[1])),
|
|
143
|
+
wavenumber,
|
|
144
|
+
distribution,
|
|
145
|
+
).imag,
|
|
146
|
+
),
|
|
147
|
+
(guess.real, guess.imag),
|
|
148
|
+
tol=1e-11,
|
|
149
|
+
)
|
|
150
|
+
if not solution.success or not np.all(np.isfinite(solution.x)):
|
|
151
|
+
continue
|
|
152
|
+
omega = complex(float(solution.x[0]), float(solution.x[1]))
|
|
153
|
+
residual = abs(dielectric(omega, wavenumber, distribution))
|
|
154
|
+
if residual > residual_tolerance or abs(omega) > 10:
|
|
155
|
+
continue
|
|
156
|
+
if any(
|
|
157
|
+
abs(omega - complex(mode.frequency, mode.growth_rate)) < 1e-6 for mode in modes
|
|
158
|
+
):
|
|
159
|
+
continue
|
|
160
|
+
modes.append(
|
|
161
|
+
LinearMode(
|
|
162
|
+
frequency=omega.real,
|
|
163
|
+
growth_rate=omega.imag,
|
|
164
|
+
dielectric_residual=float(residual),
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
if not modes:
|
|
168
|
+
raise RuntimeError("dispersion solver found no validated root")
|
|
169
|
+
return tuple(sorted(modes, key=lambda mode: mode.growth_rate, reverse=True))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def moments(distribution: GaussianMixture) -> DistributionMoments:
|
|
173
|
+
return DistributionMoments(
|
|
174
|
+
density=distribution.density(),
|
|
175
|
+
mean_velocity=distribution.mean(),
|
|
176
|
+
variance=distribution.variance(),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def classify_mode(mode: LinearMode, *, zero_tolerance: float = 1e-6) -> str:
|
|
181
|
+
if mode.growth_rate > zero_tolerance:
|
|
182
|
+
return "unstable"
|
|
183
|
+
if mode.growth_rate < -zero_tolerance:
|
|
184
|
+
return "damped"
|
|
185
|
+
return "neutral"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def build_problem() -> tuple[HypothesisNode, ObservableSpec]:
|
|
189
|
+
domain = DomainSpec(
|
|
190
|
+
id="domain_linear_1d_vlasov_poisson_k05",
|
|
191
|
+
description=(
|
|
192
|
+
"Linear electrostatic perturbations of smooth normalized electron "
|
|
193
|
+
"distributions with immobile ions at k=0.5"
|
|
194
|
+
),
|
|
195
|
+
model_family="linearized_1d_electrostatic_vlasov_poisson",
|
|
196
|
+
assumptions=(
|
|
197
|
+
"electron plasma frequency equals one",
|
|
198
|
+
"immobile neutralizing ion background",
|
|
199
|
+
"Gaussian-mixture equilibrium distributions",
|
|
200
|
+
),
|
|
201
|
+
fixed_parameters={"wavenumber": 0.5},
|
|
202
|
+
)
|
|
203
|
+
observable = ObservableSpec(
|
|
204
|
+
id="observable_dominant_growth_rate",
|
|
205
|
+
name="dominant linear growth rate",
|
|
206
|
+
semantic_kind="dominant_linear_growth_rate",
|
|
207
|
+
mathematical_definition="maximum imaginary part among validated dielectric roots",
|
|
208
|
+
estimator="analytic Gaussian-mixture dielectric root solver",
|
|
209
|
+
units="electron_plasma_frequency",
|
|
210
|
+
tolerance=0.02,
|
|
211
|
+
)
|
|
212
|
+
hypothesis = HypothesisNode(
|
|
213
|
+
id="hypothesis_low_moments_sufficient_for_stability",
|
|
214
|
+
statement=(
|
|
215
|
+
"Density, mean velocity, and variance are sufficient to determine "
|
|
216
|
+
"the dominant linear growth rate in the declared distribution family."
|
|
217
|
+
),
|
|
218
|
+
machine_predicate=(
|
|
219
|
+
"equal(n, mean_v, variance) implies abs(gamma_left-gamma_right) <= 0.02"
|
|
220
|
+
),
|
|
221
|
+
formal_predicate=MatchedPairFormalPredicate(
|
|
222
|
+
matched_coordinates=("density", "mean_velocity", "variance"),
|
|
223
|
+
outcome_observable_id=observable.id,
|
|
224
|
+
maximum_outcome_difference=observable.tolerance,
|
|
225
|
+
),
|
|
226
|
+
proposition_class=PropositionClass.PREDICTIVE_SUFFICIENCY,
|
|
227
|
+
domain=domain,
|
|
228
|
+
coordinates=("density", "mean_velocity", "variance"),
|
|
229
|
+
evidence_contract=EvidenceContract(
|
|
230
|
+
primary_observable_id=observable.id,
|
|
231
|
+
falsifying_witness=(
|
|
232
|
+
"a matched pair with equal declared moments and growth rates "
|
|
233
|
+
"separated by more than 0.02"
|
|
234
|
+
),
|
|
235
|
+
primary_tolerance=observable.tolerance,
|
|
236
|
+
),
|
|
237
|
+
origin=HypothesisOrigin.HUMAN,
|
|
238
|
+
)
|
|
239
|
+
return hypothesis, observable
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def run_kinetic_sufficiency_benchmark(
|
|
243
|
+
*,
|
|
244
|
+
wavenumber: float = 0.5,
|
|
245
|
+
) -> KineticSufficiencyResult:
|
|
246
|
+
if abs(wavenumber - 0.5) > 1e-12:
|
|
247
|
+
raise ValueError("version 1 of the planted benchmark is preregistered at k=0.5")
|
|
248
|
+
|
|
249
|
+
maxwellian_distribution = GaussianMixture(
|
|
250
|
+
components=(GaussianComponent(weight=1.0, drift=0.0, sigma=1.0),)
|
|
251
|
+
)
|
|
252
|
+
drift = 0.9
|
|
253
|
+
component_sigma = float(np.sqrt(1.0 - drift**2))
|
|
254
|
+
two_stream_distribution = GaussianMixture(
|
|
255
|
+
components=(
|
|
256
|
+
GaussianComponent(weight=0.5, drift=-drift, sigma=component_sigma),
|
|
257
|
+
GaussianComponent(weight=0.5, drift=drift, sigma=component_sigma),
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
maxwellian_moments = moments(maxwellian_distribution)
|
|
262
|
+
two_stream_moments = moments(two_stream_distribution)
|
|
263
|
+
maxwellian_mode = solve_modes(
|
|
264
|
+
maxwellian_distribution,
|
|
265
|
+
wavenumber=wavenumber,
|
|
266
|
+
)[0]
|
|
267
|
+
two_stream_mode = solve_modes(
|
|
268
|
+
two_stream_distribution,
|
|
269
|
+
wavenumber=wavenumber,
|
|
270
|
+
)[0]
|
|
271
|
+
hypothesis, observable = build_problem()
|
|
272
|
+
coordinate_names = hypothesis.coordinates
|
|
273
|
+
left_coordinates = {
|
|
274
|
+
coordinate: getattr(maxwellian_moments, coordinate) for coordinate in coordinate_names
|
|
275
|
+
}
|
|
276
|
+
right_coordinates = {
|
|
277
|
+
coordinate: getattr(two_stream_moments, coordinate) for coordinate in coordinate_names
|
|
278
|
+
}
|
|
279
|
+
witness = evaluate_predictive_sufficiency(
|
|
280
|
+
hypothesis,
|
|
281
|
+
MatchedObservation(
|
|
282
|
+
evidence_id="evidence_maxwellian_mode",
|
|
283
|
+
coordinates=left_coordinates,
|
|
284
|
+
outcome=maxwellian_mode.growth_rate,
|
|
285
|
+
outcome_uncertainty=maxwellian_mode.dielectric_residual,
|
|
286
|
+
),
|
|
287
|
+
MatchedObservation(
|
|
288
|
+
evidence_id="evidence_two_stream_mode",
|
|
289
|
+
coordinates=right_coordinates,
|
|
290
|
+
outcome=two_stream_mode.growth_rate,
|
|
291
|
+
outcome_uncertainty=two_stream_mode.dielectric_residual,
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
moments_match = all(
|
|
295
|
+
abs(left_coordinates[name] - right_coordinates[name]) <= 1e-10
|
|
296
|
+
for name in coordinate_names
|
|
297
|
+
)
|
|
298
|
+
return KineticSufficiencyResult(
|
|
299
|
+
hypothesis=hypothesis,
|
|
300
|
+
observable=observable,
|
|
301
|
+
wavenumber=wavenumber,
|
|
302
|
+
maxwellian=KineticCaseResult(
|
|
303
|
+
name="unit_variance_maxwellian",
|
|
304
|
+
distribution=maxwellian_distribution,
|
|
305
|
+
moments=maxwellian_moments,
|
|
306
|
+
mode=maxwellian_mode,
|
|
307
|
+
classification=classify_mode(maxwellian_mode),
|
|
308
|
+
),
|
|
309
|
+
two_stream=KineticCaseResult(
|
|
310
|
+
name="matched_moment_symmetric_two_stream",
|
|
311
|
+
distribution=two_stream_distribution,
|
|
312
|
+
moments=two_stream_moments,
|
|
313
|
+
mode=two_stream_mode,
|
|
314
|
+
classification=classify_mode(two_stream_mode),
|
|
315
|
+
),
|
|
316
|
+
moments_match=moments_match,
|
|
317
|
+
witness=witness,
|
|
318
|
+
)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: warpx
|
|
3
|
+
description: Author, commission, and diagnose simulations with the pinned WarpX 26.07 PICMI capability. Use for any hypothesis whose decisive evidence may require WarpX, especially multidimensional, collisional, electromagnetic, or fully kinetic particle-in-cell calculations.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# WarpX 26.07 PICMI skill
|
|
7
|
+
|
|
8
|
+
Use this skill when a hypothesis can be challenged with a particle-in-cell
|
|
9
|
+
calculation supported by the installed `warpx-cpu-26.07` or
|
|
10
|
+
`warpx-cuda-openpmd-26.07` capability. The skill
|
|
11
|
+
explains the instrument; it does not prescribe the scientific question,
|
|
12
|
+
parameters, diagnostics, or conclusion.
|
|
13
|
+
|
|
14
|
+
## Execution contract
|
|
15
|
+
|
|
16
|
+
- Author the PICMI Python program and any analysis programs in the writable
|
|
17
|
+
workspace using workspace-relative action paths such as `probe.py`, never
|
|
18
|
+
`/work/probe.py`. The sandbox maps the working directory to `/work` only at
|
|
19
|
+
execution time.
|
|
20
|
+
- Execute a PICMI program with `run_capability`, selecting
|
|
21
|
+
`warpx-cuda-openpmd-26.07` for local 2D GPU work or `warpx-cpu-26.07` for
|
|
22
|
+
the CPU fallback, and an argv whose first item is the workspace-relative
|
|
23
|
+
Python filename. Arguments are passed directly to the pinned Python
|
|
24
|
+
interpreter; there is no shell.
|
|
25
|
+
- The capability has no host home, network, provider credential, or writable
|
|
26
|
+
location outside `/work`.
|
|
27
|
+
- Before GPU production work, read `references/gpu-launch-tuning.md`. The CUDA
|
|
28
|
+
capability is non-MPI, 2D-only, and provides HDF5 openPMD diagnostics.
|
|
29
|
+
- Ordinary `run_python` does not contain `pywarpx`. Use `run_capability` for
|
|
30
|
+
programs that import `pywarpx`; use ordinary Python for independent analysis
|
|
31
|
+
when its installed packages suffice.
|
|
32
|
+
- Treat a zero exit status as execution success only. It is not scientific or
|
|
33
|
+
numerical qualification.
|
|
34
|
+
- The harness health-checks each exact capability build once and caches the result.
|
|
35
|
+
Do not repeat generic import, CUDA, or openPMD smokes inside each campaign when
|
|
36
|
+
`capability_preflight` is healthy.
|
|
37
|
+
- Use `stage=workbench` while authoring, debugging, or revising PICMI programs.
|
|
38
|
+
Workbench artifacts are provenance-recorded but permanently non-evidentiary and
|
|
39
|
+
need no claim or contract. Once the program is stable, freeze it and use
|
|
40
|
+
`stage=evidence` for prospective five-aspect qualification and scientific runs.
|
|
41
|
+
|
|
42
|
+
## Recommended use
|
|
43
|
+
|
|
44
|
+
1. Translate the active subhypothesis into observables and a falsifier.
|
|
45
|
+
2. Derive scientific parameters from the active hypothesis, explicit operator
|
|
46
|
+
input, or sources retrieved during the campaign. Preserve that provenance in
|
|
47
|
+
the workspace. Then choose a supported geometry, field model, boundaries,
|
|
48
|
+
species distributions, time-integration scheme, and runtime. For
|
|
49
|
+
electromagnetic calculations, read `references/time-integration.md` before
|
|
50
|
+
fixing the timestep.
|
|
51
|
+
3. Iterate on unknown interfaces and candidate programs in the workbench. Before
|
|
52
|
+
promoting a stable WarpX program, register a prospective evidence contract on
|
|
53
|
+
its instrument claim and transition to the complete physical commissioning
|
|
54
|
+
contract. Workbench success cannot be promoted retroactively; the evidence-stage
|
|
55
|
+
qualification must generate fresh artifacts from the frozen source.
|
|
56
|
+
4. Begin workbench development with an inexpensive anchor that exercises the intended model and
|
|
57
|
+
diagnostic path. Make the program emit a compact JSON commissioning summary
|
|
58
|
+
with qualification booleans below a top-level `checks` object, and register
|
|
59
|
+
exact `validation_checks` such as `checks.completed` for its named
|
|
60
|
+
booleans. Tag checks as `representation`, `physics_controls`, `boundaries`,
|
|
61
|
+
`diagnostics`, or `numerical_regime`; one qualifying contract must cover all
|
|
62
|
+
five. `physics_controls` verifies that the interactions, forcing, sources,
|
|
63
|
+
or constitutive controls required by the claim are present in the realized
|
|
64
|
+
run and have a measured effect where relevant. An
|
|
65
|
+
`interface` check is useful but cannot qualify a scientific run by itself.
|
|
66
|
+
Commission the mathematical representation, not only execution.
|
|
67
|
+
Keep the commissioned program source immutable and parameterize anchors,
|
|
68
|
+
sweeps, and controls through arguments or configuration. Changed source must
|
|
69
|
+
be recommissioned before it can generate scientific evidence.
|
|
70
|
+
5. Inspect the realized input and outputs. Challenge charge balance, current
|
|
71
|
+
and force balance, energy, boundaries, particle loading, interaction/source
|
|
72
|
+
activation, resolution, timestep, particle statistics, finite-domain
|
|
73
|
+
effects, and diagnostic definitions as relevant. Preserve quantitative
|
|
74
|
+
metrics behind every qualification boolean; do not make a boolean a mere
|
|
75
|
+
restatement of the requested inputs.
|
|
76
|
+
6. Establish a numerical practice before using production runs as evidence.
|
|
77
|
+
7. Preserve raw outputs and write analysis code in the workspace. Do not rely
|
|
78
|
+
only on terminal prose.
|
|
79
|
+
|
|
80
|
+
Read these resources as needed:
|
|
81
|
+
|
|
82
|
+
- `references/picmi-interface.md`: main Python construction sequence.
|
|
83
|
+
- `references/2d-xz-commissioning.md`: required reading before any 2D Cartesian
|
|
84
|
+
run; coordinate, field-source, equilibrium, and preflight invariants.
|
|
85
|
+
- `references/diagnostics.md`: field, particle, reduced, and openPMD outputs.
|
|
86
|
+
- `references/time-integration.md`: explicit and implicit PICMI evolve schemes,
|
|
87
|
+
timestep selection, and solver qualification.
|
|
88
|
+
- `references/numerical-risks.md`: common validity failures.
|
|
89
|
+
- `references/resource-scaling.md`: controlling experiment cost.
|
|
90
|
+
- `references/cpu-launch-tuning.md`: required before a long local-host CPU
|
|
91
|
+
campaign; detects MPI support, benchmarks OpenMP count/affinity and AMReX
|
|
92
|
+
block size, and records the Ryzen 9 9900X calibration.
|
|
93
|
+
- `references/gpu-launch-tuning.md`: required before a local-host CUDA run;
|
|
94
|
+
verifies the actual backend, supplies the WSL launch environment, benchmarks
|
|
95
|
+
GPU block sizes, and records RTX 4000 Ada CPU/GPU comparisons.
|
|
96
|
+
- `references/local-cuda-deployment.md`: required when installing or repairing
|
|
97
|
+
the CUDA/openPMD runtime on this or a new machine; use its pinned bootstrap,
|
|
98
|
+
post-install validation, and failure signatures.
|
|
99
|
+
- `examples/minimal_smoke.py`: a one-step neutral-plasma interface smoke test;
|
|
100
|
+
it is deliberately not a scientific template. Its JSON booleans are under
|
|
101
|
+
`checks`.
|
|
102
|
+
- `examples/implicit_em_smoke.py`: a one-step 2D theta-implicit electromagnetic
|
|
103
|
+
wiring and native-input smoke test; it is also permanently non-evidentiary.
|
|
104
|
+
Its JSON booleans are under `checks`. Run it in the workbench only when the
|
|
105
|
+
implicit interface itself is relevant; it does not prove loaded-plasma stability.
|
|
106
|
+
- Host repository demos and operator launch scripts outside this skill tree are
|
|
107
|
+
not skill resources: sandbox agents cannot `read_skill` or
|
|
108
|
+
`materialize_skill_resource` them.
|
|
109
|
+
- `examples/openpmd_field_smoke.py`: a one-step 2D field-diagnostic wiring and
|
|
110
|
+
openPMD/HDF5 readback smoke test; it is permanently non-evidentiary and is the
|
|
111
|
+
CPU and CUDA/openPMD capabilities' cached harness preflight. Campaign agents
|
|
112
|
+
normally need not repeat it. Its JSON booleans are under `checks`.
|
|
113
|
+
- `scripts/benchmark_cpu_threads.py`: run an isolated, validated OpenMP scaling
|
|
114
|
+
sweep for an operator-supplied PICMI program and exact argument list. Never
|
|
115
|
+
infer MPI support merely from the presence of `mpiexec`; the script queries
|
|
116
|
+
the loaded AMReX library.
|
|
117
|
+
- `scripts/run_local_cuda.sh`: operator-side launcher for the project-local 2D
|
|
118
|
+
CUDA runtime. Autonomous sandbox work uses the named CUDA capability instead.
|
|
119
|
+
Skill `scripts/` are host/operator tools: they are not exact-reuse sandbox
|
|
120
|
+
programs and typically cannot run inside the capability (for example they
|
|
121
|
+
may call `nvidia-smi`). Use `examples/*.py` for in-sandbox exact reuse.
|
|
122
|
+
- `scripts/bootstrap_local_cuda.sh`: create the pinned CUDA, non-MPI HDF5, and
|
|
123
|
+
openPMD environments and compile the 2D WarpX binding from an audited source
|
|
124
|
+
checkout.
|
|
125
|
+
- `scripts/probe_local_cuda.py`: prove CUDA-aware AMReX plus an openPMD HDF5
|
|
126
|
+
write/read round trip after deployment.
|
|
127
|
+
- `scripts/benchmark_gpu.py`: run a guarded, validated CUDA block-size sweep;
|
|
128
|
+
it passes the operator-supplied program arguments through unchanged except
|
|
129
|
+
for the explicitly named block-size option.
|
|
130
|
+
- `scripts/reduced_energy_budget.py`: materialize this deterministic sandbox-safe
|
|
131
|
+
reader for pinned single-level `FieldEnergy` plus `ParticleEnergy` diagnostics.
|
|
132
|
+
It selects exact total columns by realized header, refuses unknown schemas,
|
|
133
|
+
and prevents adding totals to their component/species columns.
|
|
134
|
+
|
|
135
|
+
Official WarpX 26.07 documentation:
|
|
136
|
+
|
|
137
|
+
- <https://warpx.readthedocs.io/en/latest/usage/python.html>
|
|
138
|
+
- <https://warpx.readthedocs.io/en/latest/usage/parameters.html>
|
|
139
|
+
- <https://warpx.readthedocs.io/en/latest/usage/workflows/analysis.html>
|
|
140
|
+
|
|
141
|
+
## Boundaries
|
|
142
|
+
|
|
143
|
+
Do not infer that WarpX is preferable merely because it is installed. A
|
|
144
|
+
smaller analytic or ordinary-Python calculation may be a better discriminator.
|
|
145
|
+
Do not claim that a runtime smoke test qualifies a physical regime. Do not
|
|
146
|
+
silently change an observable or numerical policy after seeing a result;
|
|
147
|
+
record the reason and treat the changed calculation as a new attempt.
|
|
148
|
+
Never obtain scientific parameters, expected outcomes, acceptance thresholds,
|
|
149
|
+
or benchmark definitions from this skill, its smoke examples, host-performance
|
|
150
|
+
records, or an earlier demonstration. Retrieve or derive them within the active
|
|
151
|
+
campaign and preserve their source.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""One-step WarpX theta-implicit EM interface smoke; never scientific evidence.
|
|
2
|
+
|
|
3
|
+
Uses a tiny 2D grid so the same program runs under both the multi-dimension
|
|
4
|
+
CPU capability and the 2D-only CUDA/openPMD capability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pywarpx import picmi
|
|
11
|
+
|
|
12
|
+
grid = picmi.Cartesian2DGrid(
|
|
13
|
+
number_of_cells=[8, 8],
|
|
14
|
+
lower_bound=[0.0, 0.0],
|
|
15
|
+
upper_bound=[1.0e-3, 1.0e-3],
|
|
16
|
+
lower_boundary_conditions=["periodic", "periodic"],
|
|
17
|
+
upper_boundary_conditions=["periodic", "periodic"],
|
|
18
|
+
lower_boundary_conditions_particles=["periodic", "periodic"],
|
|
19
|
+
upper_boundary_conditions_particles=["periodic", "periodic"],
|
|
20
|
+
warpx_max_grid_size=8,
|
|
21
|
+
)
|
|
22
|
+
solver = picmi.ElectromagneticSolver(grid=grid, method="Yee")
|
|
23
|
+
nonlinear_solver = picmi.PicardNonlinearSolver(
|
|
24
|
+
require_convergence=True,
|
|
25
|
+
max_iterations=20,
|
|
26
|
+
relative_tolerance=1.0e-8,
|
|
27
|
+
absolute_tolerance=0.0,
|
|
28
|
+
diagnostic_file="picard_iterations.txt",
|
|
29
|
+
diagnostic_interval="1",
|
|
30
|
+
)
|
|
31
|
+
evolve_scheme = picmi.ThetaImplicitEMEvolveScheme(
|
|
32
|
+
nonlinear_solver=nonlinear_solver,
|
|
33
|
+
theta=0.5,
|
|
34
|
+
)
|
|
35
|
+
simulation = picmi.Simulation(
|
|
36
|
+
solver=solver,
|
|
37
|
+
time_step_size=1.0e-12,
|
|
38
|
+
max_steps=1,
|
|
39
|
+
particle_shape=1,
|
|
40
|
+
verbose=1,
|
|
41
|
+
warpx_evolve_scheme=evolve_scheme,
|
|
42
|
+
warpx_random_seed=1,
|
|
43
|
+
warpx_serialize_initial_conditions=True,
|
|
44
|
+
warpx_amrex_the_arena_init_size=256 * 1024 * 1024,
|
|
45
|
+
warpx_used_inputs_file="implicit_used_inputs",
|
|
46
|
+
)
|
|
47
|
+
for particle_type, name, frozen in (
|
|
48
|
+
("electron", "electrons", False),
|
|
49
|
+
("proton", "ions", True),
|
|
50
|
+
):
|
|
51
|
+
species = picmi.Species(
|
|
52
|
+
particle_type=particle_type,
|
|
53
|
+
name=name,
|
|
54
|
+
initial_distribution=picmi.UniformDistribution(density=1.0e15),
|
|
55
|
+
warpx_do_not_push=frozen,
|
|
56
|
+
)
|
|
57
|
+
simulation.add_species(
|
|
58
|
+
species,
|
|
59
|
+
layout=picmi.GriddedLayout(grid=grid, n_macroparticle_per_cell=[1, 1]),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
simulation.step()
|
|
63
|
+
used_inputs = Path("implicit_used_inputs").read_text()
|
|
64
|
+
checks = {
|
|
65
|
+
"completed": True,
|
|
66
|
+
"theta_implicit_native": 'algo.evolve_scheme = "theta_implicit_em"'
|
|
67
|
+
in used_inputs,
|
|
68
|
+
"picard_native": 'implicit_evolve.nonlinear_solver = "picard"' in used_inputs,
|
|
69
|
+
"convergence_required_native": "picard.require_convergence = 1" in used_inputs,
|
|
70
|
+
"scientific_evidence_eligible": False,
|
|
71
|
+
}
|
|
72
|
+
Path("implicit_capability_smoke.json").write_text(
|
|
73
|
+
json.dumps({"checks": checks}) + "\n"
|
|
74
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""One-step WarpX/PICMI interface smoke; permanently non-evidentiary."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pywarpx import picmi
|
|
7
|
+
|
|
8
|
+
grid = picmi.Cartesian1DGrid(
|
|
9
|
+
number_of_cells=[8],
|
|
10
|
+
lower_bound=[0.0],
|
|
11
|
+
upper_bound=[1.0e-3],
|
|
12
|
+
lower_boundary_conditions=["periodic"],
|
|
13
|
+
upper_boundary_conditions=["periodic"],
|
|
14
|
+
lower_boundary_conditions_particles=["periodic"],
|
|
15
|
+
upper_boundary_conditions_particles=["periodic"],
|
|
16
|
+
warpx_max_grid_size=8,
|
|
17
|
+
)
|
|
18
|
+
solver = picmi.ElectrostaticSolver(grid=grid)
|
|
19
|
+
simulation = picmi.Simulation(
|
|
20
|
+
solver=solver,
|
|
21
|
+
time_step_size=1.0e-14,
|
|
22
|
+
max_steps=1,
|
|
23
|
+
particle_shape=1,
|
|
24
|
+
verbose=1,
|
|
25
|
+
warpx_random_seed=1,
|
|
26
|
+
warpx_serialize_initial_conditions=True,
|
|
27
|
+
warpx_used_inputs_file="warpx_used_inputs",
|
|
28
|
+
)
|
|
29
|
+
for particle_type, name, frozen in (
|
|
30
|
+
("electron", "electrons", False),
|
|
31
|
+
("proton", "ions", True),
|
|
32
|
+
):
|
|
33
|
+
species = picmi.Species(
|
|
34
|
+
particle_type=particle_type,
|
|
35
|
+
name=name,
|
|
36
|
+
initial_distribution=picmi.UniformDistribution(density=1.0e15),
|
|
37
|
+
warpx_do_not_push=frozen,
|
|
38
|
+
)
|
|
39
|
+
simulation.add_species(
|
|
40
|
+
species,
|
|
41
|
+
layout=picmi.GriddedLayout(grid=grid, n_macroparticle_per_cell=[1]),
|
|
42
|
+
)
|
|
43
|
+
simulation.step()
|
|
44
|
+
Path("capability_smoke.json").write_text(
|
|
45
|
+
json.dumps(
|
|
46
|
+
{
|
|
47
|
+
"checks": {
|
|
48
|
+
"completed": True,
|
|
49
|
+
"scientific_evidence_eligible": False,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
+ "\n"
|
|
54
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""One-step 2D openPMD/HDF5 field-diagnostic smoke; never scientific evidence."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import openpmd_api as io
|
|
7
|
+
from pywarpx import picmi
|
|
8
|
+
|
|
9
|
+
grid = picmi.Cartesian2DGrid(
|
|
10
|
+
number_of_cells=[8, 8],
|
|
11
|
+
lower_bound=[0.0, 0.0],
|
|
12
|
+
upper_bound=[1.0e-3, 1.0e-3],
|
|
13
|
+
lower_boundary_conditions=["periodic", "periodic"],
|
|
14
|
+
upper_boundary_conditions=["periodic", "periodic"],
|
|
15
|
+
lower_boundary_conditions_particles=["periodic", "periodic"],
|
|
16
|
+
upper_boundary_conditions_particles=["periodic", "periodic"],
|
|
17
|
+
warpx_max_grid_size=8,
|
|
18
|
+
)
|
|
19
|
+
solver = picmi.ElectromagneticSolver(grid=grid, method="Yee")
|
|
20
|
+
simulation = picmi.Simulation(
|
|
21
|
+
solver=solver,
|
|
22
|
+
time_step_size=1.0e-14,
|
|
23
|
+
max_steps=1,
|
|
24
|
+
particle_shape=1,
|
|
25
|
+
verbose=1,
|
|
26
|
+
warpx_random_seed=1,
|
|
27
|
+
warpx_serialize_initial_conditions=True,
|
|
28
|
+
warpx_amrex_the_arena_init_size=256 * 1024 * 1024,
|
|
29
|
+
warpx_used_inputs_file="openpmd_used_inputs",
|
|
30
|
+
)
|
|
31
|
+
for particle_type, name in (("electron", "electrons"), ("proton", "ions")):
|
|
32
|
+
species = picmi.Species(
|
|
33
|
+
particle_type=particle_type,
|
|
34
|
+
name=name,
|
|
35
|
+
initial_distribution=picmi.UniformDistribution(
|
|
36
|
+
density=1.0e15,
|
|
37
|
+
rms_velocity=[1.0e3, 1.0e3, 1.0e3],
|
|
38
|
+
),
|
|
39
|
+
warpx_do_not_push=True,
|
|
40
|
+
)
|
|
41
|
+
simulation.add_species(
|
|
42
|
+
species,
|
|
43
|
+
layout=picmi.GriddedLayout(
|
|
44
|
+
grid=grid,
|
|
45
|
+
n_macroparticle_per_cell=[1, 1],
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
simulation.add_diagnostic(
|
|
50
|
+
picmi.FieldDiagnostic(
|
|
51
|
+
name="fields",
|
|
52
|
+
grid=grid,
|
|
53
|
+
period=1,
|
|
54
|
+
data_list=["E", "B", "J", "rho"],
|
|
55
|
+
write_dir="openpmd_diags",
|
|
56
|
+
warpx_format="openpmd",
|
|
57
|
+
warpx_openpmd_backend="h5",
|
|
58
|
+
warpx_openpmd_encoding="f",
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
simulation.step()
|
|
62
|
+
|
|
63
|
+
used_inputs = Path("openpmd_used_inputs").read_text()
|
|
64
|
+
h5_files = sorted(Path("openpmd_diags").rglob("*.h5"))
|
|
65
|
+
iteration_count = 0
|
|
66
|
+
mesh_record_count = 0
|
|
67
|
+
if h5_files:
|
|
68
|
+
series = io.Series(str(h5_files[0]), io.Access.read_only)
|
|
69
|
+
# Iteration objects have .time and .meshes, not .iteration.
|
|
70
|
+
iteration_count = len(series.iterations)
|
|
71
|
+
mesh_record_count = sum(
|
|
72
|
+
len(series.iterations[index].meshes) for index in series.iterations
|
|
73
|
+
)
|
|
74
|
+
series.close()
|
|
75
|
+
|
|
76
|
+
checks = {
|
|
77
|
+
"completed": True,
|
|
78
|
+
"openpmd_format_native": 'fields.format = "openpmd"' in used_inputs,
|
|
79
|
+
"h5_backend_native": 'fields.openpmd_backend = "h5"' in used_inputs,
|
|
80
|
+
"h5_file_nonempty": bool(h5_files and h5_files[0].stat().st_size > 0),
|
|
81
|
+
"iteration_readable": iteration_count > 0,
|
|
82
|
+
"mesh_records_readable": mesh_record_count > 0,
|
|
83
|
+
"scientific_evidence_eligible": False,
|
|
84
|
+
}
|
|
85
|
+
Path("openpmd_capability_smoke.json").write_text(
|
|
86
|
+
json.dumps({"checks": checks}) + "\n"
|
|
87
|
+
)
|