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.
Files changed (75) hide show
  1. conjecture_solver/__init__.py +10 -0
  2. conjecture_solver/__main__.py +4 -0
  3. conjecture_solver/action_handlers.py +459 -0
  4. conjecture_solver/adapters/__init__.py +7 -0
  5. conjecture_solver/adapters/base.py +96 -0
  6. conjecture_solver/adapters/fake.py +155 -0
  7. conjecture_solver/adapters/pic.py +196 -0
  8. conjecture_solver/adapters/warpx.py +1375 -0
  9. conjecture_solver/autonomous_research.py +1071 -0
  10. conjecture_solver/benchmarks/__init__.py +6 -0
  11. conjecture_solver/benchmarks/electrostatic_pic.py +547 -0
  12. conjecture_solver/benchmarks/kinetic_sufficiency.py +318 -0
  13. conjecture_solver/builtin_skills/warpx/SKILL.md +151 -0
  14. conjecture_solver/builtin_skills/warpx/agents/openai.yaml +4 -0
  15. conjecture_solver/builtin_skills/warpx/examples/implicit_em_smoke.py +74 -0
  16. conjecture_solver/builtin_skills/warpx/examples/minimal_smoke.py +54 -0
  17. conjecture_solver/builtin_skills/warpx/examples/openpmd_field_smoke.py +87 -0
  18. conjecture_solver/builtin_skills/warpx/manifest.json +11 -0
  19. conjecture_solver/builtin_skills/warpx/references/2d-xz-commissioning.md +145 -0
  20. conjecture_solver/builtin_skills/warpx/references/cpu-launch-tuning.md +54 -0
  21. conjecture_solver/builtin_skills/warpx/references/diagnostics.md +139 -0
  22. conjecture_solver/builtin_skills/warpx/references/gpu-launch-tuning.md +79 -0
  23. conjecture_solver/builtin_skills/warpx/references/local-cuda-deployment.md +87 -0
  24. conjecture_solver/builtin_skills/warpx/references/numerical-risks.md +32 -0
  25. conjecture_solver/builtin_skills/warpx/references/picmi-interface.md +118 -0
  26. conjecture_solver/builtin_skills/warpx/references/resource-scaling.md +24 -0
  27. conjecture_solver/builtin_skills/warpx/references/time-integration.md +58 -0
  28. conjecture_solver/builtin_skills/warpx/scripts/benchmark_cpu_threads.py +207 -0
  29. conjecture_solver/builtin_skills/warpx/scripts/benchmark_gpu.py +242 -0
  30. conjecture_solver/builtin_skills/warpx/scripts/bootstrap_local_cuda.sh +82 -0
  31. conjecture_solver/builtin_skills/warpx/scripts/probe_local_cuda.py +110 -0
  32. conjecture_solver/builtin_skills/warpx/scripts/reduced_energy_budget.py +195 -0
  33. conjecture_solver/builtin_skills/warpx/scripts/run_local_cuda.sh +39 -0
  34. conjecture_solver/campaign.py +406 -0
  35. conjecture_solver/cli.py +963 -0
  36. conjecture_solver/confirmation.py +287 -0
  37. conjecture_solver/control.py +234 -0
  38. conjecture_solver/deployment.py +874 -0
  39. conjecture_solver/discovery.py +68 -0
  40. conjecture_solver/domains/__init__.py +40 -0
  41. conjecture_solver/domains/base.py +157 -0
  42. conjecture_solver/domains/kinetic_sufficiency.py +521 -0
  43. conjecture_solver/ledger.py +238 -0
  44. conjecture_solver/lifecycle.py +131 -0
  45. conjecture_solver/literature.py +494 -0
  46. conjecture_solver/llm.py +228 -0
  47. conjecture_solver/models.py +279 -0
  48. conjecture_solver/mvp_agent.py +3212 -0
  49. conjecture_solver/mvp_claims.py +1279 -0
  50. conjecture_solver/mvp_control.py +322 -0
  51. conjecture_solver/mvp_guidance.py +172 -0
  52. conjecture_solver/mvp_launch.py +1296 -0
  53. conjecture_solver/mvp_monitor.py +1487 -0
  54. conjecture_solver/mvp_skills.py +538 -0
  55. conjecture_solver/orchestration.py +578 -0
  56. conjecture_solver/outbox.py +526 -0
  57. conjecture_solver/parameters.py +394 -0
  58. conjecture_solver/proposals.py +427 -0
  59. conjecture_solver/research_tools.py +278 -0
  60. conjecture_solver/schema_export.py +246 -0
  61. conjecture_solver/search.py +1047 -0
  62. conjecture_solver/semantics.py +146 -0
  63. conjecture_solver/tui/__init__.py +7 -0
  64. conjecture_solver/tui/app.py +202 -0
  65. conjecture_solver/tui/claim_views.py +213 -0
  66. conjecture_solver/tui/screens.py +1014 -0
  67. conjecture_solver/warpx_analysis.py +184 -0
  68. conjecture_solver/warpx_campaign.py +148 -0
  69. conjecture_solver/warpx_confirmation.py +617 -0
  70. simjecture-0.1.0.dist-info/METADATA +400 -0
  71. simjecture-0.1.0.dist-info/RECORD +75 -0
  72. simjecture-0.1.0.dist-info/WHEEL +4 -0
  73. simjecture-0.1.0.dist-info/entry_points.txt +4 -0
  74. simjecture-0.1.0.dist-info/licenses/LICENSE +202 -0
  75. 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