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,184 @@
1
+ """Pure numerical estimators used by the WarpX openPMD postprocessor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from collections.abc import Iterable
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ import numpy as np
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class WindowedGrowth:
16
+ effective_growth_rate: float
17
+ early_rms_amplitude: float
18
+ late_rms_amplitude: float
19
+ amplitude_ratio: float
20
+ early_sample_count: int
21
+ late_sample_count: int
22
+ initial_amplitude: float
23
+ final_amplitude: float
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class WeightedMoments:
28
+ total_weight: float
29
+ mean_velocity: float
30
+ variance: float
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class GaussAssessment:
35
+ relative_residual: float
36
+ relative_charge_imbalance: float
37
+
38
+
39
+ def fit_windowed_effective_growth(
40
+ times_omega_pe: np.ndarray,
41
+ complex_mode: np.ndarray,
42
+ *,
43
+ early_window: tuple[float, float],
44
+ late_window: tuple[float, float],
45
+ minimum_samples: int,
46
+ ) -> WindowedGrowth:
47
+ """Measure preregistered early/late RMS change of a spatial Fourier mode.
48
+
49
+ The two-window estimator intentionally matches the independent quiet-start
50
+ PIC benchmark. It does not fit a late stochastic resurgence after a
51
+ damped signal has reached the finite-particle noise floor.
52
+ """
53
+
54
+ times = np.asarray(times_omega_pe, dtype=float)
55
+ mode = np.asarray(complex_mode, dtype=complex)
56
+ if times.ndim != 1 or mode.ndim != 1 or len(times) != len(mode):
57
+ raise ValueError("mode times and values must be equal-length one-dimensional arrays")
58
+ if len(times) < minimum_samples or not np.all(np.diff(times) > 0):
59
+ raise ValueError("mode series is too short or is not strictly increasing")
60
+ if not np.all(np.isfinite(times)) or not np.all(np.isfinite(mode)):
61
+ raise ValueError("mode series contains non-finite values")
62
+ early_start, early_end = early_window
63
+ late_start, late_end = late_window
64
+ if not 0 <= early_start < early_end < late_start < late_end <= times[-1] + 1.0e-10:
65
+ raise ValueError("growth windows must be ordered inside the diagnostic trace")
66
+ tolerance = 1.0e-10 * max(1.0, abs(late_end))
67
+ early_selected = (times >= early_start - tolerance) & (times <= early_end + tolerance)
68
+ late_selected = (times >= late_start - tolerance) & (times <= late_end + tolerance)
69
+ early_count = int(np.count_nonzero(early_selected))
70
+ late_count = int(np.count_nonzero(late_selected))
71
+ if min(early_count, late_count) < minimum_samples:
72
+ raise ValueError("a growth window contains too few diagnostic samples")
73
+ amplitudes = np.abs(mode)
74
+ early_rms = float(np.sqrt(np.mean(amplitudes[early_selected] ** 2)))
75
+ late_rms = float(np.sqrt(np.mean(amplitudes[late_selected] ** 2)))
76
+ if early_rms <= 0 or late_rms <= 0:
77
+ raise ValueError("growth-window RMS amplitude is zero")
78
+ ratio = late_rms / early_rms
79
+ time_separation = 0.5 * (late_start + late_end - early_start - early_end)
80
+ return WindowedGrowth(
81
+ effective_growth_rate=float(np.log(ratio) / time_separation),
82
+ early_rms_amplitude=early_rms,
83
+ late_rms_amplitude=late_rms,
84
+ amplitude_ratio=ratio,
85
+ early_sample_count=early_count,
86
+ late_sample_count=late_count,
87
+ initial_amplitude=float(abs(mode[0])),
88
+ final_amplitude=float(abs(mode[-1])),
89
+ )
90
+
91
+
92
+ def periodic_centered_gauss_assessment(
93
+ electric_field: np.ndarray,
94
+ charge_density: np.ndarray,
95
+ *,
96
+ cell_size: float,
97
+ vacuum_permittivity: float,
98
+ ) -> GaussAssessment:
99
+ """Assess the cell-centered periodic finite-difference Gauss law."""
100
+
101
+ field = np.asarray(electric_field, dtype=float)
102
+ charge = np.asarray(charge_density, dtype=float)
103
+ if field.ndim != 1 or charge.shape != field.shape or len(field) < 4:
104
+ raise ValueError("Gauss-law arrays must have one equal, nontrivial shape")
105
+ if cell_size <= 0 or vacuum_permittivity <= 0:
106
+ raise ValueError("Gauss-law normalization values must be positive")
107
+ if not np.all(np.isfinite(field)) or not np.all(np.isfinite(charge)):
108
+ raise ValueError("Gauss-law arrays contain non-finite values")
109
+ divergence = (np.roll(field, -1) - np.roll(field, 1)) / (2.0 * cell_size)
110
+ source = charge / vacuum_permittivity
111
+ mean_source = float(np.mean(source))
112
+ centered_source = source - mean_source
113
+ centered_divergence = divergence - float(np.mean(divergence))
114
+ scale = max(
115
+ float(np.sqrt(np.mean(centered_source**2))),
116
+ float(np.sqrt(np.mean(centered_divergence**2))),
117
+ )
118
+ if scale == 0:
119
+ residual = 0.0
120
+ imbalance = 0.0 if mean_source == 0 else float("inf")
121
+ else:
122
+ residual = float(np.sqrt(np.mean((centered_divergence - centered_source) ** 2)) / scale)
123
+ imbalance = abs(mean_source) / scale
124
+ return GaussAssessment(
125
+ relative_residual=residual,
126
+ relative_charge_imbalance=imbalance,
127
+ )
128
+
129
+
130
+ def weighted_velocity_moments(
131
+ momentum: np.ndarray,
132
+ mass: np.ndarray,
133
+ weights: np.ndarray,
134
+ *,
135
+ speed_of_light: float,
136
+ ) -> WeightedMoments:
137
+ """Convert SI momentum to velocity and compute weighted moments."""
138
+
139
+ momentum = np.asarray(momentum, dtype=float)
140
+ mass = np.asarray(mass, dtype=float)
141
+ weights = np.asarray(weights, dtype=float)
142
+ if momentum.shape != mass.shape or momentum.shape != weights.shape or momentum.ndim != 1:
143
+ raise ValueError("particle momentum, mass, and weight arrays must have one equal shape")
144
+ if speed_of_light <= 0 or np.any(mass <= 0) or np.any(weights < 0):
145
+ raise ValueError("particle mass, weight, and light speed are outside their domains")
146
+ if not all(np.all(np.isfinite(values)) for values in (momentum, mass, weights)):
147
+ raise ValueError("particle arrays contain non-finite values")
148
+ total_weight = float(np.sum(weights))
149
+ if total_weight <= 0:
150
+ raise ValueError("particle weights sum to zero")
151
+ proper_velocity = momentum / mass
152
+ velocity = proper_velocity / np.sqrt(1.0 + (proper_velocity / speed_of_light) ** 2)
153
+ mean = float(np.sum(weights * velocity) / total_weight)
154
+ variance = float(np.sum(weights * (velocity - mean) ** 2) / total_weight)
155
+ return WeightedMoments(
156
+ total_weight=total_weight,
157
+ mean_velocity=mean,
158
+ variance=variance,
159
+ )
160
+
161
+
162
+ def relative_range(values: np.ndarray) -> float:
163
+ values = np.asarray(values, dtype=float)
164
+ if values.ndim != 1 or not len(values) or not np.all(np.isfinite(values)):
165
+ raise ValueError("relative-range input must be a finite one-dimensional series")
166
+ scale = abs(float(values[0]))
167
+ if scale == 0:
168
+ raise ValueError("relative range is undefined for a zero initial value")
169
+ return float((np.max(values) - np.min(values)) / scale)
170
+
171
+
172
+ def diagnostic_manifest_hash(paths: Iterable[Path], *, root: Path) -> str:
173
+ entries: list[dict[str, str | int]] = []
174
+ for path in sorted((Path(path) for path in paths), key=lambda item: str(item)):
175
+ content = path.read_bytes()
176
+ entries.append(
177
+ {
178
+ "path": str(path.relative_to(root)),
179
+ "size": len(content),
180
+ "sha256": hashlib.sha256(content).hexdigest(),
181
+ }
182
+ )
183
+ canonical = json.dumps(entries, sort_keys=True, separators=(",", ":"))
184
+ return hashlib.sha256(canonical.encode()).hexdigest()
@@ -0,0 +1,148 @@
1
+ """Portable, self-verifying export for a qualified WarpX campaign."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Literal
11
+
12
+ from pydantic import Field, model_validator
13
+
14
+ from .benchmarks.kinetic_sufficiency import build_problem
15
+ from .ledger import SQLiteEventLedger
16
+ from .models import Claim, ClaimDisposition, HypothesisNode, RunEvidence, StrictModel
17
+ from .orchestration import MultiActionCampaignReport, OrchestrationDisposition
18
+ from .search import BlindedSearchReport
19
+ from .warpx_confirmation import (
20
+ QualifiedWarpXInstrument,
21
+ WarpXConfirmationDisposition,
22
+ WarpXConfirmationReport,
23
+ )
24
+
25
+
26
+ def _canonical_json(value: object) -> str:
27
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
28
+
29
+
30
+ class QualifiedWarpXCampaignPackage(StrictModel):
31
+ schema_version: Literal["0.1.0"] = "0.1.0"
32
+ campaign_id: str = Field(min_length=1)
33
+ hypothesis: HypothesisNode
34
+ instrument: QualifiedWarpXInstrument
35
+ search_report: BlindedSearchReport
36
+ confirmation_report: WarpXConfirmationReport
37
+ evidence: tuple[RunEvidence, ...] = ()
38
+ claim: Claim
39
+ campaign_report: MultiActionCampaignReport
40
+ provenance_event_hashes: tuple[str, ...] = Field(min_length=1)
41
+ generated_at: datetime
42
+ package_hash: str = Field(pattern=r"^[0-9a-f]{64}$")
43
+
44
+ @model_validator(mode="after")
45
+ def scientific_records_are_consistent(self) -> QualifiedWarpXCampaignPackage:
46
+ if self.campaign_report.campaign_id != self.campaign_id:
47
+ raise ValueError("campaign package contains a report for another campaign")
48
+ if self.campaign_report.disposition is not OrchestrationDisposition.COMPLETED:
49
+ raise ValueError("only a completed campaign can be exported")
50
+ if self.instrument.qualification_hash != self.confirmation_report.design.qualification_hash:
51
+ raise ValueError("confirmation does not use the packaged qualified instrument")
52
+ if (
53
+ self.search_report.confirmation_candidate
54
+ != self.confirmation_report.design.physical.candidate
55
+ ):
56
+ raise ValueError("confirmation candidate differs from the frozen search winner")
57
+ if self.claim.hypothesis_id != self.hypothesis.id:
58
+ raise ValueError("claim refers to another hypothesis")
59
+ evidence_ids = {item.id for item in self.evidence}
60
+ if self.claim.disposition is ClaimDisposition.REFUTED_WITHIN_MODEL:
61
+ if self.confirmation_report.disposition is not WarpXConfirmationDisposition.CONFIRMED:
62
+ raise ValueError("an unconfirmed matrix cannot support a refutation claim")
63
+ if not evidence_ids or set(self.claim.evidence_ids) != evidence_ids:
64
+ raise ValueError("refutation claim must name every packaged confirmation evidence")
65
+ if not all(item.eligible for item in self.evidence):
66
+ raise ValueError("refutation claim cannot cite ineligible evidence")
67
+ return self
68
+
69
+ def calculated_hash(self) -> str:
70
+ content = self.model_dump(mode="json", exclude={"package_hash"})
71
+ return hashlib.sha256(_canonical_json(content).encode()).hexdigest()
72
+
73
+ def verify_hash(self) -> bool:
74
+ return self.package_hash == self.calculated_hash()
75
+
76
+ @classmethod
77
+ def create(cls, **values: object) -> QualifiedWarpXCampaignPackage:
78
+ unhashed = cls.model_construct(package_hash="0" * 64, **values)
79
+ return cls.model_validate(
80
+ {
81
+ **unhashed.model_dump(mode="json", exclude={"package_hash"}),
82
+ "package_hash": unhashed.calculated_hash(),
83
+ }
84
+ )
85
+
86
+ def write(self, output_directory: str | Path) -> Path:
87
+ output = Path(output_directory)
88
+ output.mkdir(parents=True, exist_ok=True)
89
+ path = output / "qualified_warpx_campaign_package.json"
90
+ temporary = output / ".qualified_warpx_campaign_package.json.tmp"
91
+ temporary.write_text(self.model_dump_json(indent=2) + "\n")
92
+ os.replace(temporary, path)
93
+ return path
94
+
95
+ @classmethod
96
+ def read_verified(cls, path: str | Path) -> QualifiedWarpXCampaignPackage:
97
+ package = cls.model_validate_json(Path(path).read_text())
98
+ if not package.verify_hash():
99
+ raise ValueError("qualified WarpX campaign package hash does not match its contents")
100
+ return package
101
+
102
+
103
+ def build_qualified_warpx_campaign_package(
104
+ *,
105
+ campaign_id: str,
106
+ ledger: SQLiteEventLedger,
107
+ instrument: QualifiedWarpXInstrument,
108
+ campaign_report: MultiActionCampaignReport,
109
+ ) -> QualifiedWarpXCampaignPackage:
110
+ if campaign_report.campaign_id != campaign_id:
111
+ raise ValueError("campaign report targets a different campaign")
112
+ discovery_state = next(
113
+ state
114
+ for state in campaign_report.action_states
115
+ if "search_report" in (state.execution.output if state.execution else {})
116
+ )
117
+ confirmation_state = next(
118
+ state
119
+ for state in campaign_report.action_states
120
+ if "confirmation_report" in (state.execution.output if state.execution else {})
121
+ and "qualification_hash" in (state.execution.output if state.execution else {})
122
+ )
123
+ assert discovery_state.execution is not None
124
+ assert confirmation_state.execution is not None
125
+ output = confirmation_state.execution.output
126
+ search_report = BlindedSearchReport.model_validate(
127
+ discovery_state.execution.output["search_report"]
128
+ )
129
+ confirmation_report = WarpXConfirmationReport.model_validate(output["confirmation_report"])
130
+ evidence = tuple(RunEvidence.model_validate(item) for item in output["evidence"])
131
+ claim = Claim.model_validate(output["claim"])
132
+ events = ledger.load(campaign_id)
133
+ completed = next(
134
+ event for event in reversed(events) if event.event_type == "multi_action_campaign_completed"
135
+ )
136
+ hypothesis, _ = build_problem()
137
+ return QualifiedWarpXCampaignPackage.create(
138
+ campaign_id=campaign_id,
139
+ hypothesis=hypothesis,
140
+ instrument=instrument,
141
+ search_report=search_report,
142
+ confirmation_report=confirmation_report,
143
+ evidence=evidence,
144
+ claim=claim,
145
+ campaign_report=campaign_report,
146
+ provenance_event_hashes=tuple(event.event_hash for event in events),
147
+ generated_at=datetime.fromisoformat(completed.created_at),
148
+ )