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,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
|
+
)
|