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,617 @@
|
|
|
1
|
+
"""Independent fresh-seed confirmation using a physics-qualified WarpX profile."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from statistics import median
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import Field, model_validator
|
|
12
|
+
|
|
13
|
+
from .adapters.base import JobState, NormalizedResult, SimulatorAdapter
|
|
14
|
+
from .adapters.warpx import (
|
|
15
|
+
WARPX_ADAPTER_VERSION,
|
|
16
|
+
WarpXExecutionProfile,
|
|
17
|
+
WarpXNumericalConfig,
|
|
18
|
+
WarpXPairSummary,
|
|
19
|
+
WarpXPhysicalConfig,
|
|
20
|
+
WarpXPhysicsQualificationRecord,
|
|
21
|
+
build_warpx_experiment,
|
|
22
|
+
qualified_warpx_profile,
|
|
23
|
+
warpx_physics_qualification_hash,
|
|
24
|
+
)
|
|
25
|
+
from .control import CampaignControl
|
|
26
|
+
from .ledger import SQLiteEventLedger, StoredEvent
|
|
27
|
+
from .models import StrictModel
|
|
28
|
+
from .outbox import (
|
|
29
|
+
InjectedOutboxCrash,
|
|
30
|
+
JournaledSimulatorAdapter,
|
|
31
|
+
OutboxCrashPoint,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WarpXConfirmationDisposition(StrEnum):
|
|
36
|
+
CONFIRMED = "confirmed"
|
|
37
|
+
INCONCLUSIVE = "inconclusive"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class WarpXAttemptFailureKind(StrEnum):
|
|
41
|
+
INFRASTRUCTURE = "infrastructure"
|
|
42
|
+
NUMERICAL = "numerical"
|
|
43
|
+
VALIDITY = "validity"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class QualifiedWarpXInstrument(StrictModel):
|
|
47
|
+
"""A passing qualification bound to the exact executable adapter profile."""
|
|
48
|
+
|
|
49
|
+
schema_version: Literal["0.1.0"] = "0.1.0"
|
|
50
|
+
id: str = Field(min_length=1)
|
|
51
|
+
adapter_name: Literal["warpx_picmi"] = "warpx_picmi"
|
|
52
|
+
adapter_version: str = WARPX_ADAPTER_VERSION
|
|
53
|
+
qualification_hash: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
54
|
+
qualification: WarpXPhysicsQualificationRecord
|
|
55
|
+
execution_profile: WarpXExecutionProfile
|
|
56
|
+
|
|
57
|
+
@model_validator(mode="after")
|
|
58
|
+
def qualification_is_exact(self) -> QualifiedWarpXInstrument:
|
|
59
|
+
expected_hash = warpx_physics_qualification_hash(self.qualification)
|
|
60
|
+
if not self.qualification.passed or not self.qualification.authorizes_scientific_evidence:
|
|
61
|
+
raise ValueError("registered WarpX instrument requires a passing qualification")
|
|
62
|
+
if self.qualification_hash != expected_hash:
|
|
63
|
+
raise ValueError("registered instrument qualification hash does not match its record")
|
|
64
|
+
if self.execution_profile != qualified_warpx_profile(self.qualification):
|
|
65
|
+
raise ValueError("registered instrument profile does not match its qualification")
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def register_qualified_warpx_instrument(
|
|
70
|
+
qualification: WarpXPhysicsQualificationRecord,
|
|
71
|
+
) -> QualifiedWarpXInstrument:
|
|
72
|
+
qualification_hash = warpx_physics_qualification_hash(qualification)
|
|
73
|
+
return QualifiedWarpXInstrument(
|
|
74
|
+
id=f"qualified_warpx_{qualification_hash[:20]}",
|
|
75
|
+
qualification_hash=qualification_hash,
|
|
76
|
+
qualification=qualification,
|
|
77
|
+
execution_profile=qualified_warpx_profile(qualification),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class WarpXConfirmationResolution(StrictModel):
|
|
82
|
+
id: str = Field(min_length=1)
|
|
83
|
+
grid_cells: int = Field(ge=32)
|
|
84
|
+
electron_macroparticles_per_cell: int = Field(ge=16)
|
|
85
|
+
time_step_omega_pe: float = Field(gt=0)
|
|
86
|
+
diagnostic_interval_steps: int = Field(ge=1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class WarpXConfirmationDesign(StrictModel):
|
|
90
|
+
schema_version: Literal["0.1.0"] = "0.1.0"
|
|
91
|
+
qualification_hash: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
92
|
+
physical: WarpXPhysicalConfig
|
|
93
|
+
seeds: tuple[int, ...] = Field(default=(211, 307, 401), min_length=3)
|
|
94
|
+
resolutions: tuple[WarpXConfirmationResolution, ...] = Field(min_length=2)
|
|
95
|
+
selection_frozen_before_confirmation: Literal[True] = True
|
|
96
|
+
|
|
97
|
+
@model_validator(mode="after")
|
|
98
|
+
def matrix_is_unique(self) -> WarpXConfirmationDesign:
|
|
99
|
+
if len(set(self.seeds)) != len(self.seeds) or any(seed < 1 for seed in self.seeds):
|
|
100
|
+
raise ValueError("confirmation seeds must be unique positive integers")
|
|
101
|
+
if len({resolution.id for resolution in self.resolutions}) != len(self.resolutions):
|
|
102
|
+
raise ValueError("confirmation resolution identifiers must be unique")
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class WarpXConfirmationAttempt(StrictModel):
|
|
107
|
+
seed: int = Field(ge=1)
|
|
108
|
+
resolution_id: str = Field(min_length=1)
|
|
109
|
+
normalized_result: NormalizedResult
|
|
110
|
+
checks: dict[str, bool]
|
|
111
|
+
confirmed: bool
|
|
112
|
+
|
|
113
|
+
@model_validator(mode="after")
|
|
114
|
+
def result_matches_checks(self) -> WarpXConfirmationAttempt:
|
|
115
|
+
if not self.checks or self.confirmed != all(self.checks.values()):
|
|
116
|
+
raise ValueError("confirmation attempt status must equal all checks")
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class WarpXConfirmationFailure(StrictModel):
|
|
121
|
+
ordinal: int = Field(ge=1)
|
|
122
|
+
seed: int = Field(ge=1)
|
|
123
|
+
resolution_id: str = Field(min_length=1)
|
|
124
|
+
experiment_id: str = Field(min_length=1)
|
|
125
|
+
external_job_id: str | None = None
|
|
126
|
+
kind: WarpXAttemptFailureKind
|
|
127
|
+
detail: str = Field(min_length=1)
|
|
128
|
+
consumed_case_units: Literal[2] = 2
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class WarpXConfirmationReport(StrictModel):
|
|
132
|
+
schema_version: Literal["0.1.0"] = "0.1.0"
|
|
133
|
+
design: WarpXConfirmationDesign
|
|
134
|
+
attempts: tuple[WarpXConfirmationAttempt, ...] = ()
|
|
135
|
+
failures: tuple[WarpXConfirmationFailure, ...] = ()
|
|
136
|
+
median_reference_rates_by_resolution: dict[str, float]
|
|
137
|
+
median_candidate_rates_by_resolution: dict[str, float]
|
|
138
|
+
checks: dict[str, bool]
|
|
139
|
+
disposition: WarpXConfirmationDisposition
|
|
140
|
+
|
|
141
|
+
@model_validator(mode="after")
|
|
142
|
+
def disposition_matches_checks(self) -> WarpXConfirmationReport:
|
|
143
|
+
expected = (
|
|
144
|
+
WarpXConfirmationDisposition.CONFIRMED
|
|
145
|
+
if self.checks and all(self.checks.values())
|
|
146
|
+
else WarpXConfirmationDisposition.INCONCLUSIVE
|
|
147
|
+
)
|
|
148
|
+
if self.disposition is not expected:
|
|
149
|
+
raise ValueError("confirmation disposition must follow the report checks")
|
|
150
|
+
attempt_keys = {(attempt.seed, attempt.resolution_id) for attempt in self.attempts}
|
|
151
|
+
failure_keys = {(failure.seed, failure.resolution_id) for failure in self.failures}
|
|
152
|
+
if attempt_keys & failure_keys:
|
|
153
|
+
raise ValueError("a confirmation matrix point cannot both succeed and fail")
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def default_warpx_confirmation_design(
|
|
158
|
+
qualification: WarpXPhysicsQualificationRecord,
|
|
159
|
+
) -> WarpXConfirmationDesign:
|
|
160
|
+
if not qualification.passed:
|
|
161
|
+
raise ValueError("a failed WarpX qualification cannot define confirmation")
|
|
162
|
+
return WarpXConfirmationDesign(
|
|
163
|
+
qualification_hash=warpx_physics_qualification_hash(qualification),
|
|
164
|
+
physical=qualification.scope.physical,
|
|
165
|
+
resolutions=(
|
|
166
|
+
WarpXConfirmationResolution(
|
|
167
|
+
id="coarse",
|
|
168
|
+
grid_cells=64,
|
|
169
|
+
electron_macroparticles_per_cell=512,
|
|
170
|
+
time_step_omega_pe=0.05,
|
|
171
|
+
diagnostic_interval_steps=2,
|
|
172
|
+
),
|
|
173
|
+
WarpXConfirmationResolution(
|
|
174
|
+
id="refined",
|
|
175
|
+
grid_cells=128,
|
|
176
|
+
electron_macroparticles_per_cell=512,
|
|
177
|
+
time_step_omega_pe=0.025,
|
|
178
|
+
diagnostic_interval_steps=4,
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def build_warpx_confirmation_report(
|
|
185
|
+
*,
|
|
186
|
+
design: WarpXConfirmationDesign,
|
|
187
|
+
qualification: WarpXPhysicsQualificationRecord,
|
|
188
|
+
results: tuple[NormalizedResult, ...],
|
|
189
|
+
failures: tuple[WarpXConfirmationFailure, ...] = (),
|
|
190
|
+
) -> WarpXConfirmationReport:
|
|
191
|
+
qualification_hash = warpx_physics_qualification_hash(qualification)
|
|
192
|
+
if not qualification.passed or design.qualification_hash != qualification_hash:
|
|
193
|
+
raise ValueError("confirmation design does not name a passing qualification")
|
|
194
|
+
resolution_by_numerics = {
|
|
195
|
+
(
|
|
196
|
+
resolution.grid_cells,
|
|
197
|
+
resolution.electron_macroparticles_per_cell,
|
|
198
|
+
resolution.time_step_omega_pe,
|
|
199
|
+
resolution.diagnostic_interval_steps,
|
|
200
|
+
): resolution.id
|
|
201
|
+
for resolution in design.resolutions
|
|
202
|
+
}
|
|
203
|
+
calibration_packages = {point.run_package_hash for point in qualification.calibration_points}
|
|
204
|
+
calibration_manifests = {
|
|
205
|
+
digest
|
|
206
|
+
for point in qualification.calibration_points
|
|
207
|
+
for digest in (
|
|
208
|
+
point.reference_diagnostic_manifest_hash,
|
|
209
|
+
point.candidate_diagnostic_manifest_hash,
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
attempts: list[WarpXConfirmationAttempt] = []
|
|
213
|
+
for result in results:
|
|
214
|
+
diagnostics = result.diagnostics
|
|
215
|
+
numerical = WarpXNumericalConfig.model_validate(diagnostics["numerical"])
|
|
216
|
+
physical = WarpXPhysicalConfig.model_validate(diagnostics["physical"])
|
|
217
|
+
profile = WarpXExecutionProfile.model_validate(diagnostics["execution_profile"])
|
|
218
|
+
pair = WarpXPairSummary.model_validate(diagnostics["pair_summary"])
|
|
219
|
+
resolution_id = resolution_by_numerics.get(
|
|
220
|
+
(
|
|
221
|
+
numerical.grid_cells,
|
|
222
|
+
numerical.electron_macroparticles_per_cell,
|
|
223
|
+
numerical.time_step_omega_pe,
|
|
224
|
+
numerical.diagnostic_interval_steps,
|
|
225
|
+
),
|
|
226
|
+
"outside_design",
|
|
227
|
+
)
|
|
228
|
+
run_package_hash = str(diagnostics["run_package_hash"])
|
|
229
|
+
result_manifests = {
|
|
230
|
+
pair.reference.diagnostic_manifest_hash,
|
|
231
|
+
pair.candidate.diagnostic_manifest_hash,
|
|
232
|
+
}
|
|
233
|
+
checks = {
|
|
234
|
+
"configuration_matches_frozen_design": (
|
|
235
|
+
physical == design.physical
|
|
236
|
+
and numerical.random_seed in design.seeds
|
|
237
|
+
and resolution_id != "outside_design"
|
|
238
|
+
),
|
|
239
|
+
"configuration_is_in_qualified_scope": not qualification.scope.validation_errors(
|
|
240
|
+
physical,
|
|
241
|
+
numerical,
|
|
242
|
+
),
|
|
243
|
+
"qualified_profile_hash_matches": (
|
|
244
|
+
profile.qualified_for_scientific_evidence
|
|
245
|
+
and profile.qualification_hash == qualification_hash
|
|
246
|
+
),
|
|
247
|
+
"numerical_validity_passed": bool(
|
|
248
|
+
diagnostics["validity_gates"]["numerical_validity_passed"]
|
|
249
|
+
),
|
|
250
|
+
"reference_is_damped": pair.reference.classification == "damped",
|
|
251
|
+
"candidate_is_unstable": pair.candidate.classification == "unstable",
|
|
252
|
+
"effective_rates_are_separated": (
|
|
253
|
+
pair.candidate.effective_growth_rate_omega_pe
|
|
254
|
+
- pair.reference.effective_growth_rate_omega_pe
|
|
255
|
+
>= qualification.minimum_effective_rate_separation
|
|
256
|
+
),
|
|
257
|
+
"raw_witness_satisfies_predicate": bool(diagnostics["raw_witness_satisfies_predicate"]),
|
|
258
|
+
"result_is_scientific_evidence_eligible": bool(
|
|
259
|
+
diagnostics["scientific_evidence_eligible"]
|
|
260
|
+
),
|
|
261
|
+
"normalized_result_falsifies_hypothesis": bool(
|
|
262
|
+
result.observables["hypothesis_falsified"]
|
|
263
|
+
),
|
|
264
|
+
"package_was_not_used_for_calibration": run_package_hash not in calibration_packages,
|
|
265
|
+
"diagnostics_were_not_used_for_calibration": not bool(
|
|
266
|
+
result_manifests & calibration_manifests
|
|
267
|
+
),
|
|
268
|
+
}
|
|
269
|
+
attempts.append(
|
|
270
|
+
WarpXConfirmationAttempt(
|
|
271
|
+
seed=numerical.random_seed,
|
|
272
|
+
resolution_id=resolution_id,
|
|
273
|
+
normalized_result=result,
|
|
274
|
+
checks=checks,
|
|
275
|
+
confirmed=all(checks.values()),
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
expected_keys = {
|
|
280
|
+
(seed, resolution.id) for seed in design.seeds for resolution in design.resolutions
|
|
281
|
+
}
|
|
282
|
+
observed_keys = {(attempt.seed, attempt.resolution_id) for attempt in attempts}
|
|
283
|
+
failed_keys = {(failure.seed, failure.resolution_id) for failure in failures}
|
|
284
|
+
if any(key not in expected_keys for key in failed_keys):
|
|
285
|
+
raise ValueError("confirmation failure is outside the frozen design")
|
|
286
|
+
if len(failed_keys) != len(failures):
|
|
287
|
+
raise ValueError("confirmation failures must identify unique matrix points")
|
|
288
|
+
|
|
289
|
+
def medians(observable: str) -> dict[str, float]:
|
|
290
|
+
return {
|
|
291
|
+
resolution.id: float(
|
|
292
|
+
median(
|
|
293
|
+
float(attempt.normalized_result.observables[observable])
|
|
294
|
+
for attempt in attempts
|
|
295
|
+
if attempt.resolution_id == resolution.id
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
for resolution in design.resolutions
|
|
299
|
+
if any(attempt.resolution_id == resolution.id for attempt in attempts)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
reference_medians = medians("maxwellian_growth_rate")
|
|
303
|
+
candidate_medians = medians("two_stream_growth_rate")
|
|
304
|
+
|
|
305
|
+
def median_range(values: dict[str, float]) -> float:
|
|
306
|
+
return max(values.values()) - min(values.values()) if values else math.inf
|
|
307
|
+
|
|
308
|
+
checks = {
|
|
309
|
+
"confirmation_matrix_is_complete": (
|
|
310
|
+
observed_keys | failed_keys == expected_keys
|
|
311
|
+
and len(attempts) + len(failures) == len(expected_keys)
|
|
312
|
+
),
|
|
313
|
+
"no_execution_failures": not failures,
|
|
314
|
+
"every_attempt_confirmed": bool(attempts)
|
|
315
|
+
and all(attempt.confirmed for attempt in attempts),
|
|
316
|
+
"confirmation_packages_are_unique": (
|
|
317
|
+
len(
|
|
318
|
+
{
|
|
319
|
+
str(attempt.normalized_result.diagnostics["run_package_hash"])
|
|
320
|
+
for attempt in attempts
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
== len(attempts)
|
|
324
|
+
),
|
|
325
|
+
"reference_median_rate_is_resolution_converged": (
|
|
326
|
+
median_range(reference_medians) <= qualification.maximum_median_rate_shift
|
|
327
|
+
),
|
|
328
|
+
"candidate_median_rate_is_resolution_converged": (
|
|
329
|
+
median_range(candidate_medians) <= qualification.maximum_median_rate_shift
|
|
330
|
+
),
|
|
331
|
+
}
|
|
332
|
+
disposition = (
|
|
333
|
+
WarpXConfirmationDisposition.CONFIRMED
|
|
334
|
+
if all(checks.values())
|
|
335
|
+
else WarpXConfirmationDisposition.INCONCLUSIVE
|
|
336
|
+
)
|
|
337
|
+
return WarpXConfirmationReport(
|
|
338
|
+
design=design,
|
|
339
|
+
attempts=tuple(sorted(attempts, key=lambda attempt: (attempt.resolution_id, attempt.seed))),
|
|
340
|
+
failures=tuple(sorted(failures, key=lambda failure: (failure.resolution_id, failure.seed))),
|
|
341
|
+
median_reference_rates_by_resolution=reference_medians,
|
|
342
|
+
median_candidate_rates_by_resolution=candidate_medians,
|
|
343
|
+
checks=checks,
|
|
344
|
+
disposition=disposition,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class WarpXConfirmationCrashPoint(StrEnum):
|
|
349
|
+
AFTER_CONFIRMATION_STARTED = "after_confirmation_started"
|
|
350
|
+
AFTER_JOB_RECEIPT = "after_job_receipt"
|
|
351
|
+
AFTER_RESULT_RETRIEVED = "after_result_retrieved"
|
|
352
|
+
AFTER_MATRIX_POINT_COMMITTED = "after_matrix_point_committed"
|
|
353
|
+
AFTER_CONFIRMATION_COMPLETED = "after_confirmation_completed"
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class InjectedWarpXConfirmationCrash(RuntimeError):
|
|
357
|
+
def __init__(self, point: WarpXConfirmationCrashPoint, ordinal: int | None = None) -> None:
|
|
358
|
+
suffix = f" at matrix ordinal {ordinal}" if ordinal is not None else ""
|
|
359
|
+
super().__init__(f"injected WarpX confirmation crash at {point.value}{suffix}")
|
|
360
|
+
self.point = point
|
|
361
|
+
self.ordinal = ordinal
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@dataclass
|
|
365
|
+
class WarpXConfirmationProjection:
|
|
366
|
+
design: WarpXConfirmationDesign | None = None
|
|
367
|
+
attempts: dict[tuple[int, str], WarpXConfirmationAttempt] | None = None
|
|
368
|
+
failures: dict[tuple[int, str], WarpXConfirmationFailure] | None = None
|
|
369
|
+
report: WarpXConfirmationReport | None = None
|
|
370
|
+
|
|
371
|
+
def __post_init__(self) -> None:
|
|
372
|
+
if self.attempts is None:
|
|
373
|
+
self.attempts = {}
|
|
374
|
+
if self.failures is None:
|
|
375
|
+
self.failures = {}
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def replay(cls, events: tuple[StoredEvent, ...]) -> WarpXConfirmationProjection:
|
|
379
|
+
state = cls()
|
|
380
|
+
for event in events:
|
|
381
|
+
if event.event_type == "qualified_warpx_confirmation_started":
|
|
382
|
+
state.design = WarpXConfirmationDesign.model_validate(event.payload["design"])
|
|
383
|
+
elif event.event_type == "qualified_warpx_confirmation_attempt_completed":
|
|
384
|
+
attempt = WarpXConfirmationAttempt.model_validate(event.payload["attempt"])
|
|
385
|
+
assert state.attempts is not None
|
|
386
|
+
state.attempts[(attempt.seed, attempt.resolution_id)] = attempt
|
|
387
|
+
elif event.event_type == "qualified_warpx_confirmation_attempt_failed":
|
|
388
|
+
failure = WarpXConfirmationFailure.model_validate(event.payload["failure"])
|
|
389
|
+
assert state.failures is not None
|
|
390
|
+
state.failures[(failure.seed, failure.resolution_id)] = failure
|
|
391
|
+
elif event.event_type == "qualified_warpx_confirmation_completed":
|
|
392
|
+
state.report = WarpXConfirmationReport.model_validate(event.payload["report"])
|
|
393
|
+
return state
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class WarpXConfirmationRunner:
|
|
397
|
+
"""Restart-safe execution of every point in a frozen qualified matrix."""
|
|
398
|
+
|
|
399
|
+
def __init__(
|
|
400
|
+
self,
|
|
401
|
+
*,
|
|
402
|
+
campaign_id: str,
|
|
403
|
+
ledger: SQLiteEventLedger,
|
|
404
|
+
instrument: QualifiedWarpXInstrument,
|
|
405
|
+
adapter: SimulatorAdapter,
|
|
406
|
+
design: WarpXConfirmationDesign,
|
|
407
|
+
control: CampaignControl | None = None,
|
|
408
|
+
crash_at: WarpXConfirmationCrashPoint | None = None,
|
|
409
|
+
crash_ordinal: int | None = None,
|
|
410
|
+
outbox_crash_at: OutboxCrashPoint | None = None,
|
|
411
|
+
) -> None:
|
|
412
|
+
if design.qualification_hash != instrument.qualification_hash:
|
|
413
|
+
raise ValueError("confirmation design names a different qualified instrument")
|
|
414
|
+
if adapter.capabilities().adapter_name != instrument.adapter_name:
|
|
415
|
+
raise ValueError("confirmation adapter does not match the registered instrument")
|
|
416
|
+
self.campaign_id = campaign_id
|
|
417
|
+
self.ledger = ledger
|
|
418
|
+
self.instrument = instrument
|
|
419
|
+
self.adapter = JournaledSimulatorAdapter(
|
|
420
|
+
campaign_id=campaign_id,
|
|
421
|
+
ledger=ledger,
|
|
422
|
+
adapter=adapter,
|
|
423
|
+
crash_at=outbox_crash_at,
|
|
424
|
+
)
|
|
425
|
+
self.design = design
|
|
426
|
+
self.control = control
|
|
427
|
+
self.crash_at = crash_at
|
|
428
|
+
self.crash_ordinal = crash_ordinal
|
|
429
|
+
|
|
430
|
+
def _state(self) -> WarpXConfirmationProjection:
|
|
431
|
+
return WarpXConfirmationProjection.replay(self.ledger.load(self.campaign_id))
|
|
432
|
+
|
|
433
|
+
def _append(
|
|
434
|
+
self,
|
|
435
|
+
event_type: str,
|
|
436
|
+
aggregate_type: str,
|
|
437
|
+
aggregate_id: str,
|
|
438
|
+
payload: dict[str, object],
|
|
439
|
+
suffix: str,
|
|
440
|
+
) -> None:
|
|
441
|
+
self.ledger.append(
|
|
442
|
+
campaign_id=self.campaign_id,
|
|
443
|
+
event_type=event_type,
|
|
444
|
+
aggregate_type=aggregate_type,
|
|
445
|
+
aggregate_id=aggregate_id,
|
|
446
|
+
payload=payload,
|
|
447
|
+
idempotency_key=f"{self.campaign_id}:{suffix}",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def _crash(self, point: WarpXConfirmationCrashPoint, ordinal: int | None = None) -> None:
|
|
451
|
+
if self.crash_at is point and (self.crash_ordinal is None or self.crash_ordinal == ordinal):
|
|
452
|
+
raise InjectedWarpXConfirmationCrash(point, ordinal)
|
|
453
|
+
|
|
454
|
+
def _record_failure(self, failure: WarpXConfirmationFailure) -> None:
|
|
455
|
+
self._append(
|
|
456
|
+
"qualified_warpx_confirmation_attempt_failed",
|
|
457
|
+
"warpx_confirmation_attempt",
|
|
458
|
+
failure.experiment_id,
|
|
459
|
+
{"failure": failure.model_dump(mode="json")},
|
|
460
|
+
f"qualified-warpx-confirmation:{failure.resolution_id}:{failure.seed}:failed",
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def run(self) -> WarpXConfirmationReport:
|
|
464
|
+
state = self._state()
|
|
465
|
+
if state.design is not None and state.design != self.design:
|
|
466
|
+
raise ValueError("campaign already contains a different WarpX confirmation design")
|
|
467
|
+
if state.report is not None:
|
|
468
|
+
return state.report
|
|
469
|
+
if state.design is None:
|
|
470
|
+
self._append(
|
|
471
|
+
"qualified_warpx_confirmation_started",
|
|
472
|
+
"warpx_confirmation",
|
|
473
|
+
self.instrument.id,
|
|
474
|
+
{
|
|
475
|
+
"instrument_id": self.instrument.id,
|
|
476
|
+
"design": self.design.model_dump(mode="json"),
|
|
477
|
+
},
|
|
478
|
+
"qualified-warpx-confirmation:started",
|
|
479
|
+
)
|
|
480
|
+
self._crash(WarpXConfirmationCrashPoint.AFTER_CONFIRMATION_STARTED)
|
|
481
|
+
|
|
482
|
+
ordinal = 0
|
|
483
|
+
for resolution in self.design.resolutions:
|
|
484
|
+
for seed in self.design.seeds:
|
|
485
|
+
ordinal += 1
|
|
486
|
+
state = self._state()
|
|
487
|
+
assert state.attempts is not None and state.failures is not None
|
|
488
|
+
key = (seed, resolution.id)
|
|
489
|
+
if key in state.attempts or key in state.failures:
|
|
490
|
+
continue
|
|
491
|
+
if self.control is not None:
|
|
492
|
+
self.control.require_processing_authority()
|
|
493
|
+
numerical = WarpXNumericalConfig(
|
|
494
|
+
grid_cells=resolution.grid_cells,
|
|
495
|
+
electron_macroparticles_per_cell=(resolution.electron_macroparticles_per_cell),
|
|
496
|
+
ion_macroparticles_per_cell=16,
|
|
497
|
+
time_step_omega_pe=resolution.time_step_omega_pe,
|
|
498
|
+
final_time_omega_pe=20.0,
|
|
499
|
+
diagnostic_interval_steps=resolution.diagnostic_interval_steps,
|
|
500
|
+
random_seed=seed,
|
|
501
|
+
)
|
|
502
|
+
experiment = build_warpx_experiment(
|
|
503
|
+
physical=self.design.physical,
|
|
504
|
+
numerical=numerical,
|
|
505
|
+
experiment_id=(f"warpx_confirmation_{resolution.id}_seed_{seed}_v1"),
|
|
506
|
+
)
|
|
507
|
+
validation = self.adapter.validate(experiment)
|
|
508
|
+
if not validation.valid:
|
|
509
|
+
raise ValueError("; ".join(validation.errors))
|
|
510
|
+
run = self.adapter.compile_input(experiment)
|
|
511
|
+
try:
|
|
512
|
+
job = self.adapter.submit(
|
|
513
|
+
run,
|
|
514
|
+
idempotency_key=f"{experiment.id}:submit",
|
|
515
|
+
)
|
|
516
|
+
except InjectedOutboxCrash:
|
|
517
|
+
raise
|
|
518
|
+
except Exception as error:
|
|
519
|
+
self._record_failure(
|
|
520
|
+
WarpXConfirmationFailure(
|
|
521
|
+
ordinal=ordinal,
|
|
522
|
+
seed=seed,
|
|
523
|
+
resolution_id=resolution.id,
|
|
524
|
+
experiment_id=experiment.id,
|
|
525
|
+
kind=WarpXAttemptFailureKind.INFRASTRUCTURE,
|
|
526
|
+
detail=f"{type(error).__name__}: {error}",
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
self._crash(
|
|
530
|
+
WarpXConfirmationCrashPoint.AFTER_MATRIX_POINT_COMMITTED,
|
|
531
|
+
ordinal,
|
|
532
|
+
)
|
|
533
|
+
continue
|
|
534
|
+
self._crash(WarpXConfirmationCrashPoint.AFTER_JOB_RECEIPT, ordinal)
|
|
535
|
+
status = self.adapter.monitor(job)
|
|
536
|
+
if status.state is not JobState.COMPLETED:
|
|
537
|
+
kind = (
|
|
538
|
+
WarpXAttemptFailureKind.NUMERICAL
|
|
539
|
+
if status.state is JobState.FAILED
|
|
540
|
+
else WarpXAttemptFailureKind.INFRASTRUCTURE
|
|
541
|
+
)
|
|
542
|
+
self._record_failure(
|
|
543
|
+
WarpXConfirmationFailure(
|
|
544
|
+
ordinal=ordinal,
|
|
545
|
+
seed=seed,
|
|
546
|
+
resolution_id=resolution.id,
|
|
547
|
+
experiment_id=experiment.id,
|
|
548
|
+
external_job_id=job.job_id,
|
|
549
|
+
kind=kind,
|
|
550
|
+
detail=(
|
|
551
|
+
f"scheduler state {status.state.value}: "
|
|
552
|
+
f"{status.detail or 'no detail'}"
|
|
553
|
+
),
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
else:
|
|
557
|
+
try:
|
|
558
|
+
normalized = self.adapter.normalize(self.adapter.retrieve(job))
|
|
559
|
+
except Exception as error:
|
|
560
|
+
self._record_failure(
|
|
561
|
+
WarpXConfirmationFailure(
|
|
562
|
+
ordinal=ordinal,
|
|
563
|
+
seed=seed,
|
|
564
|
+
resolution_id=resolution.id,
|
|
565
|
+
experiment_id=experiment.id,
|
|
566
|
+
external_job_id=job.job_id,
|
|
567
|
+
kind=WarpXAttemptFailureKind.VALIDITY,
|
|
568
|
+
detail=f"{type(error).__name__}: {error}",
|
|
569
|
+
)
|
|
570
|
+
)
|
|
571
|
+
else:
|
|
572
|
+
self._crash(
|
|
573
|
+
WarpXConfirmationCrashPoint.AFTER_RESULT_RETRIEVED,
|
|
574
|
+
ordinal,
|
|
575
|
+
)
|
|
576
|
+
attempt_report = build_warpx_confirmation_report(
|
|
577
|
+
design=self.design,
|
|
578
|
+
qualification=self.instrument.qualification,
|
|
579
|
+
results=(normalized,),
|
|
580
|
+
).attempts[0]
|
|
581
|
+
self._append(
|
|
582
|
+
"qualified_warpx_confirmation_attempt_completed",
|
|
583
|
+
"warpx_confirmation_attempt",
|
|
584
|
+
experiment.id,
|
|
585
|
+
{"attempt": attempt_report.model_dump(mode="json")},
|
|
586
|
+
(f"qualified-warpx-confirmation:{resolution.id}:{seed}:completed"),
|
|
587
|
+
)
|
|
588
|
+
self._crash(
|
|
589
|
+
WarpXConfirmationCrashPoint.AFTER_MATRIX_POINT_COMMITTED,
|
|
590
|
+
ordinal,
|
|
591
|
+
)
|
|
592
|
+
if self.control is not None:
|
|
593
|
+
self.control.require_processing_authority()
|
|
594
|
+
|
|
595
|
+
state = self._state()
|
|
596
|
+
assert state.attempts is not None and state.failures is not None
|
|
597
|
+
report = build_warpx_confirmation_report(
|
|
598
|
+
design=self.design,
|
|
599
|
+
qualification=self.instrument.qualification,
|
|
600
|
+
results=tuple(
|
|
601
|
+
attempt.normalized_result
|
|
602
|
+
for attempt in sorted(
|
|
603
|
+
state.attempts.values(),
|
|
604
|
+
key=lambda item: (item.resolution_id, item.seed),
|
|
605
|
+
)
|
|
606
|
+
),
|
|
607
|
+
failures=tuple(state.failures.values()),
|
|
608
|
+
)
|
|
609
|
+
self._append(
|
|
610
|
+
"qualified_warpx_confirmation_completed",
|
|
611
|
+
"warpx_confirmation",
|
|
612
|
+
self.instrument.id,
|
|
613
|
+
{"report": report.model_dump(mode="json")},
|
|
614
|
+
"qualified-warpx-confirmation:completed",
|
|
615
|
+
)
|
|
616
|
+
self._crash(WarpXConfirmationCrashPoint.AFTER_CONFIRMATION_COMPLETED)
|
|
617
|
+
return self._state().report or report
|