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,406 @@
1
+ """Crash-recoverable runner for the first end-to-end scientific campaign."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from dataclasses import dataclass
7
+ from datetime import UTC, datetime
8
+ from enum import StrEnum
9
+
10
+ from .adapters.base import (
11
+ JobReference,
12
+ JobState,
13
+ NormalizedResult,
14
+ RunPackage,
15
+ SimulatorAdapter,
16
+ )
17
+ from .benchmarks.kinetic_sufficiency import build_problem
18
+ from .control import CampaignControl
19
+ from .discovery import DiscoveryPackage
20
+ from .ledger import SQLiteEventLedger, StoredEvent
21
+ from .models import (
22
+ AttemptOutcome,
23
+ AttemptRecord,
24
+ Claim,
25
+ ClaimDisposition,
26
+ EvidenceRole,
27
+ ExperimentSpec,
28
+ HypothesisNode,
29
+ RunEvidence,
30
+ )
31
+
32
+
33
+ class CrashPoint(StrEnum):
34
+ AFTER_CAMPAIGN_CREATED = "after_campaign_created"
35
+ AFTER_ACTION_PLANNED = "after_action_planned"
36
+ AFTER_ATTEMPT_RECORDED = "after_attempt_recorded"
37
+ AFTER_SUBMIT_BEFORE_COMMIT = "after_submit_before_commit"
38
+ AFTER_JOB_COMMITTED = "after_job_committed"
39
+ AFTER_RETRIEVE_BEFORE_COMMIT = "after_retrieve_before_commit"
40
+ AFTER_RESULT_COMMITTED = "after_result_committed"
41
+ AFTER_ATTEMPT_COMPLETED = "after_attempt_completed"
42
+ AFTER_EVIDENCE_COMMIT = "after_evidence_commit"
43
+ AFTER_CLAIM_COMMIT = "after_claim_commit"
44
+ AFTER_CAMPAIGN_COMPLETED = "after_campaign_completed"
45
+
46
+
47
+ class InjectedCrash(RuntimeError):
48
+ def __init__(self, point: CrashPoint) -> None:
49
+ super().__init__(f"injected crash at {point.value}")
50
+ self.point = point
51
+
52
+
53
+ @dataclass
54
+ class CampaignProjection:
55
+ hypothesis: HypothesisNode | None = None
56
+ experiment: ExperimentSpec | None = None
57
+ run_package: RunPackage | None = None
58
+ attempt: AttemptRecord | None = None
59
+ job: JobReference | None = None
60
+ normalized_result: NormalizedResult | None = None
61
+ evidence: RunEvidence | None = None
62
+ claim: Claim | None = None
63
+ completed_event: StoredEvent | None = None
64
+
65
+ @classmethod
66
+ def replay(cls, events: tuple[StoredEvent, ...]) -> CampaignProjection:
67
+ state = cls()
68
+ for event in events:
69
+ payload = event.payload
70
+ if event.event_type == "campaign_created":
71
+ state.hypothesis = HypothesisNode.model_validate(payload["hypothesis"])
72
+ state.experiment = ExperimentSpec.model_validate(payload["experiment"])
73
+ elif event.event_type == "action_planned":
74
+ state.run_package = RunPackage.model_validate(payload["run_package"])
75
+ elif event.event_type in {"attempt_recorded", "attempt_completed"}:
76
+ state.attempt = AttemptRecord.model_validate(payload["attempt"])
77
+ elif event.event_type == "job_submitted":
78
+ state.job = JobReference.model_validate(payload["job"])
79
+ elif event.event_type == "result_retrieved":
80
+ state.normalized_result = NormalizedResult.model_validate(
81
+ payload["normalized_result"]
82
+ )
83
+ elif event.event_type == "evidence_ingested":
84
+ state.evidence = RunEvidence.model_validate(payload["evidence"])
85
+ elif event.event_type == "claim_evaluated":
86
+ state.claim = Claim.model_validate(payload["claim"])
87
+ elif event.event_type == "campaign_completed":
88
+ state.completed_event = event
89
+ return state
90
+
91
+
92
+ def planted_experiment() -> ExperimentSpec:
93
+ return ExperimentSpec(
94
+ id="experiment_kinetic_sufficiency_v1",
95
+ hypothesis_ids=("hypothesis_low_moments_sufficient_for_stability",),
96
+ action_type="kinetic_sufficiency",
97
+ physical_parameters={"wavenumber": 0.5},
98
+ required_diagnostics=("dominant_linear_mode", "distribution_moments"),
99
+ predictions={
100
+ "sufficiency": "matched distributions have equal growth rate",
101
+ "counterexample": "matched distributions differ in growth rate",
102
+ },
103
+ falsification_condition="a matched pair differs by more than 0.02",
104
+ )
105
+
106
+
107
+ def planted_campaign_problem() -> tuple[HypothesisNode, ExperimentSpec]:
108
+ hypothesis, _ = build_problem()
109
+ return hypothesis, planted_experiment()
110
+
111
+
112
+ class CampaignRunner:
113
+ """Resume one campaign by replaying its ledger before each side effect."""
114
+
115
+ def __init__(
116
+ self,
117
+ *,
118
+ campaign_id: str,
119
+ ledger: SQLiteEventLedger,
120
+ adapter: SimulatorAdapter,
121
+ hypothesis: HypothesisNode,
122
+ experiment: ExperimentSpec,
123
+ crash_at: CrashPoint | None = None,
124
+ control: CampaignControl | None = None,
125
+ ) -> None:
126
+ self.campaign_id = campaign_id
127
+ self.ledger = ledger
128
+ self.adapter = adapter
129
+ self.hypothesis = hypothesis
130
+ self.experiment = experiment
131
+ self.crash_at = crash_at
132
+ if control is not None and control.campaign_id != campaign_id:
133
+ raise ValueError("control plane targets a different campaign")
134
+ self.control = control
135
+
136
+ def _crash(self, point: CrashPoint) -> None:
137
+ if self.crash_at is point:
138
+ raise InjectedCrash(point)
139
+
140
+ def _events(self) -> tuple[StoredEvent, ...]:
141
+ return self.ledger.load(self.campaign_id)
142
+
143
+ def _state(self) -> CampaignProjection:
144
+ return CampaignProjection.replay(self._events())
145
+
146
+ def _append(
147
+ self,
148
+ event_type: str,
149
+ aggregate_type: str,
150
+ aggregate_id: str,
151
+ payload: dict[str, object],
152
+ ) -> StoredEvent:
153
+ result = self.ledger.append(
154
+ campaign_id=self.campaign_id,
155
+ event_type=event_type,
156
+ aggregate_type=aggregate_type,
157
+ aggregate_id=aggregate_id,
158
+ payload=payload,
159
+ idempotency_key=f"{self.campaign_id}:{event_type}",
160
+ )
161
+ return result.event
162
+
163
+ def _validate_identity(self, state: CampaignProjection) -> None:
164
+ if state.experiment is not None and state.experiment != self.experiment:
165
+ raise ValueError("campaign already exists with a different experiment")
166
+ if state.hypothesis is not None and state.hypothesis != self.hypothesis:
167
+ raise ValueError("campaign already exists with a different hypothesis")
168
+
169
+ def _package(self, state: CampaignProjection) -> DiscoveryPackage:
170
+ if not all(
171
+ (
172
+ state.hypothesis,
173
+ state.experiment,
174
+ state.attempt,
175
+ state.normalized_result,
176
+ state.evidence,
177
+ state.claim,
178
+ state.completed_event,
179
+ )
180
+ ):
181
+ raise RuntimeError("cannot package an incomplete campaign")
182
+ assert state.hypothesis is not None
183
+ assert state.experiment is not None
184
+ assert state.attempt is not None
185
+ assert state.normalized_result is not None
186
+ assert state.evidence is not None
187
+ assert state.claim is not None
188
+ assert state.completed_event is not None
189
+ return DiscoveryPackage.create(
190
+ campaign_id=self.campaign_id,
191
+ hypothesis=state.hypothesis,
192
+ experiment=state.experiment,
193
+ attempt=state.attempt,
194
+ evidence=state.evidence,
195
+ claim=state.claim,
196
+ normalized_result=state.normalized_result,
197
+ provenance_event_hashes=tuple(event.event_hash for event in self._events()),
198
+ generated_at=datetime.fromisoformat(state.completed_event.created_at),
199
+ )
200
+
201
+ def run(self) -> DiscoveryPackage:
202
+ state = self._state()
203
+ self._validate_identity(state)
204
+ if state.completed_event is not None:
205
+ return self._package(state)
206
+
207
+ if state.experiment is None:
208
+ self._append(
209
+ "campaign_created",
210
+ "campaign",
211
+ self.campaign_id,
212
+ {
213
+ "hypothesis": self.hypothesis.model_dump(mode="json"),
214
+ "experiment": self.experiment.model_dump(mode="json"),
215
+ },
216
+ )
217
+ self._crash(CrashPoint.AFTER_CAMPAIGN_CREATED)
218
+ state = self._state()
219
+
220
+ if state.run_package is None:
221
+ validation = self.adapter.validate(self.experiment)
222
+ if not validation.valid:
223
+ raise ValueError("; ".join(validation.errors))
224
+ run_package = self.adapter.compile_input(self.experiment)
225
+ self._append(
226
+ "action_planned",
227
+ "experiment",
228
+ self.experiment.id,
229
+ {
230
+ "validation": validation.model_dump(mode="json"),
231
+ "run_package": run_package.model_dump(mode="json"),
232
+ },
233
+ )
234
+ self._crash(CrashPoint.AFTER_ACTION_PLANNED)
235
+ state = self._state()
236
+ assert state.run_package is not None
237
+
238
+ if self.control is not None and state.job is None:
239
+ self.control.require_action_authority(self.experiment.id)
240
+
241
+ attempt_id = f"attempt_{hashlib.sha256(self.campaign_id.encode()).hexdigest()[:16]}"
242
+ submission_key = f"{self.campaign_id}:submit:{state.run_package.package_hash}"
243
+ if state.attempt is None:
244
+ attempt = AttemptRecord(
245
+ id=attempt_id,
246
+ experiment_id=self.experiment.id,
247
+ idempotency_key=submission_key,
248
+ )
249
+ self._append(
250
+ "attempt_recorded",
251
+ "attempt",
252
+ attempt.id,
253
+ {"attempt": attempt.model_dump(mode="json")},
254
+ )
255
+ self._crash(CrashPoint.AFTER_ATTEMPT_RECORDED)
256
+ state = self._state()
257
+ assert state.attempt is not None
258
+
259
+ # Re-submission is intentional: a real scheduler must treat this as an
260
+ # idempotent attach, including after the process died before commit.
261
+ attached_job = self.adapter.submit(
262
+ state.run_package,
263
+ idempotency_key=state.attempt.idempotency_key,
264
+ )
265
+ if state.job is not None and attached_job != state.job:
266
+ raise RuntimeError("adapter violated the idempotent submission contract")
267
+ if state.job is None:
268
+ self._crash(CrashPoint.AFTER_SUBMIT_BEFORE_COMMIT)
269
+ self._append(
270
+ "job_submitted",
271
+ "attempt",
272
+ state.attempt.id,
273
+ {"job": attached_job.model_dump(mode="json")},
274
+ )
275
+ self._crash(CrashPoint.AFTER_JOB_COMMITTED)
276
+ state = self._state()
277
+ assert state.job is not None
278
+
279
+ if state.normalized_result is None:
280
+ status = self.adapter.monitor(state.job)
281
+ if status.state is not JobState.COMPLETED:
282
+ raise RuntimeError(f"job is not complete: {status.state.value}")
283
+ raw_result = self.adapter.retrieve(state.job)
284
+ normalized = self.adapter.normalize(raw_result)
285
+ self._crash(CrashPoint.AFTER_RETRIEVE_BEFORE_COMMIT)
286
+ self._append(
287
+ "result_retrieved",
288
+ "attempt",
289
+ state.attempt.id,
290
+ {
291
+ "raw_artifact_hashes": list(raw_result.artifact_hashes),
292
+ "normalized_result": normalized.model_dump(mode="json"),
293
+ },
294
+ )
295
+ self._crash(CrashPoint.AFTER_RESULT_COMMITTED)
296
+ state = self._state()
297
+ assert state.normalized_result is not None
298
+
299
+ if state.attempt.outcome is None:
300
+ completed = AttemptRecord.model_validate(
301
+ {
302
+ **state.attempt.model_dump(mode="json"),
303
+ "outcome": AttemptOutcome.SUCCESS.value,
304
+ "completed_at": datetime.now(UTC).isoformat(),
305
+ "external_job_id": state.job.job_id,
306
+ }
307
+ )
308
+ self._append(
309
+ "attempt_completed",
310
+ "attempt",
311
+ completed.id,
312
+ {"attempt": completed.model_dump(mode="json")},
313
+ )
314
+ self._crash(CrashPoint.AFTER_ATTEMPT_COMPLETED)
315
+ state = self._state()
316
+ assert state.attempt is not None
317
+
318
+ if self.control is not None:
319
+ self.control.require_processing_authority()
320
+
321
+ if state.evidence is None:
322
+ observables = state.normalized_result.observables
323
+ scientific_eligibility = bool(
324
+ state.normalized_result.diagnostics.get(
325
+ "scientific_evidence_eligible",
326
+ True,
327
+ )
328
+ )
329
+ evidence = RunEvidence(
330
+ id=f"evidence_{attempt_id.removeprefix('attempt_')}",
331
+ source_attempt_id=attempt_id,
332
+ role=EvidenceRole.DISCOVERY,
333
+ eligible=scientific_eligibility,
334
+ eligibility_reason=(
335
+ "successful run passed adapter and result validity gates"
336
+ if scientific_eligibility
337
+ else "adapter marked the execution profile or numerical result ineligible"
338
+ ),
339
+ observable_values={
340
+ "maxwellian_growth_rate": float(observables["maxwellian_growth_rate"]),
341
+ "two_stream_growth_rate": float(observables["two_stream_growth_rate"]),
342
+ "outcome_separation": abs(
343
+ float(observables["two_stream_growth_rate"])
344
+ - float(observables["maxwellian_growth_rate"])
345
+ ),
346
+ },
347
+ independence_group=(
348
+ f"{state.run_package.adapter_name}:{state.run_package.package_hash}"
349
+ ),
350
+ artifact_hashes=state.normalized_result.artifact_hashes,
351
+ )
352
+ self._append(
353
+ "evidence_ingested",
354
+ "evidence",
355
+ evidence.id,
356
+ {"evidence": evidence.model_dump(mode="json")},
357
+ )
358
+ self._crash(CrashPoint.AFTER_EVIDENCE_COMMIT)
359
+ state = self._state()
360
+ assert state.evidence is not None
361
+
362
+ if state.claim is None:
363
+ falsified = state.evidence.eligible and bool(
364
+ state.normalized_result.observables["hypothesis_falsified"]
365
+ )
366
+ disposition = (
367
+ ClaimDisposition.REFUTED_WITHIN_MODEL
368
+ if falsified
369
+ else ClaimDisposition.UNRESOLVED
370
+ )
371
+ claim = Claim(
372
+ id=f"claim_{hashlib.sha256(self.campaign_id.encode()).hexdigest()[:16]}",
373
+ hypothesis_id=self.hypothesis.id,
374
+ statement=(
375
+ "The low-order-moment predictive-sufficiency hypothesis is "
376
+ "refuted within the declared kinetic model."
377
+ if falsified
378
+ else "The declared test did not refute the hypothesis."
379
+ ),
380
+ disposition=disposition,
381
+ scope=self.hypothesis.domain.description,
382
+ evidence_ids=(state.evidence.id,),
383
+ limitations=(
384
+ "discovery evidence; independent confirmation has not been run",
385
+ "scope is restricted to the preregistered model family and k=0.5",
386
+ ),
387
+ )
388
+ self._append(
389
+ "claim_evaluated",
390
+ "claim",
391
+ claim.id,
392
+ {"claim": claim.model_dump(mode="json")},
393
+ )
394
+ self._crash(CrashPoint.AFTER_CLAIM_COMMIT)
395
+
396
+ state = self._state()
397
+ assert state.claim is not None
398
+ self._append(
399
+ "campaign_completed",
400
+ "campaign",
401
+ self.campaign_id,
402
+ {"claim_id": state.claim.id},
403
+ )
404
+ self._crash(CrashPoint.AFTER_CAMPAIGN_COMPLETED)
405
+ final_state = self._state()
406
+ return self._package(final_state)