equiframe-benchmark 0.12.4__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.
- equiframe_benchmark/PROTOCOL_V2.md +172 -0
- equiframe_benchmark/__init__.py +284 -0
- equiframe_benchmark/__main__.py +3 -0
- equiframe_benchmark/_scale_workload.py +375 -0
- equiframe_benchmark/calibration_types.py +112 -0
- equiframe_benchmark/cli.py +196 -0
- equiframe_benchmark/commands.py +149 -0
- equiframe_benchmark/commands_scale.py +38 -0
- equiframe_benchmark/commands_step10.py +275 -0
- equiframe_benchmark/commands_v2.py +206 -0
- equiframe_benchmark/cost_table.py +62 -0
- equiframe_benchmark/costs.py +788 -0
- equiframe_benchmark/distribution.py +932 -0
- equiframe_benchmark/evaluation.py +614 -0
- equiframe_benchmark/evaluation_v2.py +428 -0
- equiframe_benchmark/fixtures.py +299 -0
- equiframe_benchmark/freeze.py +869 -0
- equiframe_benchmark/freeze_v2.py +412 -0
- equiframe_benchmark/integration.py +655 -0
- equiframe_benchmark/io.py +29 -0
- equiframe_benchmark/models.py +313 -0
- equiframe_benchmark/models_v2.py +13 -0
- equiframe_benchmark/mutations.py +34 -0
- equiframe_benchmark/oracle.py +264 -0
- equiframe_benchmark/pairwise.py +248 -0
- equiframe_benchmark/pilot.py +254 -0
- equiframe_benchmark/py.typed +0 -0
- equiframe_benchmark/reconciliation.py +294 -0
- equiframe_benchmark/reproduction.py +542 -0
- equiframe_benchmark/scale_io.py +19 -0
- equiframe_benchmark/scale_process.py +1077 -0
- equiframe_benchmark/scale_types.py +120 -0
- equiframe_benchmark/scaling.py +529 -0
- equiframe_benchmark/schemas/protocol-v2/CommonSupportV2-2.0.schema.json +327 -0
- equiframe_benchmark/schemas/protocol-v2/ContrastSpecV2-2.0.schema.json +37 -0
- equiframe_benchmark/schemas/protocol-v2/EvaluationConfigV2-2.0.schema.json +184 -0
- equiframe_benchmark/schemas/protocol-v2/EvaluationReportV2-2.0.schema.json +1237 -0
- equiframe_benchmark/schemas/protocol-v2/EvaluationStratumV2-2.0.schema.json +747 -0
- equiframe_benchmark/schemas/protocol-v2/FreezeDefinitionV2-2.0.schema.json +5951 -0
- equiframe_benchmark/schemas/protocol-v2/FreezeManifestV2-2.0.schema.json +6222 -0
- equiframe_benchmark/schemas/protocol-v2/PrincipalContrastV2-2.0.schema.json +578 -0
- equiframe_benchmark/schemas/protocol-v2/ProtocolReviewV2-2.0.schema.json +56 -0
- equiframe_benchmark/schemas/protocol-v2/SignedMetricV2-2.0.schema.json +51 -0
- equiframe_benchmark/schemas/protocol-v2/StudyCaseResultV2-2.0.schema.json +4881 -0
- equiframe_benchmark/schemas/protocol-v2/StudyResultV2-2.0.schema.json +8370 -0
- equiframe_benchmark/schemas/protocol-v2/TableExportV2-2.0.schema.json +75 -0
- equiframe_benchmark/selection.py +178 -0
- equiframe_benchmark/snapshot.py +24 -0
- equiframe_benchmark/splits.py +92 -0
- equiframe_benchmark/study.py +526 -0
- equiframe_benchmark/study_bundle.py +379 -0
- equiframe_benchmark/study_bundle_v2.py +388 -0
- equiframe_benchmark/study_v2.py +544 -0
- equiframe_benchmark/tables.py +347 -0
- equiframe_benchmark/tables_v2.py +340 -0
- equiframe_benchmark/training_types.py +52 -0
- equiframe_benchmark-0.12.4.dist-info/METADATA +343 -0
- equiframe_benchmark-0.12.4.dist-info/RECORD +62 -0
- equiframe_benchmark-0.12.4.dist-info/WHEEL +4 -0
- equiframe_benchmark-0.12.4.dist-info/entry_points.txt +3 -0
- equiframe_benchmark-0.12.4.dist-info/licenses/LICENSE +21 -0
- equiframe_benchmark-0.12.4.dist-info/licenses/THIRD_PARTY_NOTICES.md +8 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""Trusted isolated synthetic cost worker. No source/service/network access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Literal, TypeVar
|
|
13
|
+
|
|
14
|
+
import equiframe as ef
|
|
15
|
+
from equiframe.evidence import describe_file, inventory, load_json, publish_json, verify_files
|
|
16
|
+
|
|
17
|
+
from equiframe_benchmark.costs import _Monitor, _PsutilProbe
|
|
18
|
+
from equiframe_benchmark.models import digest
|
|
19
|
+
from equiframe_benchmark.scale_io import save_scale_document
|
|
20
|
+
from equiframe_benchmark.scale_types import (
|
|
21
|
+
SCALE_SAMPLE_INTERVAL_SECONDS,
|
|
22
|
+
BaselineCost,
|
|
23
|
+
ScaleSpec,
|
|
24
|
+
ScaleTrial,
|
|
25
|
+
ScaleWorkerCompletion,
|
|
26
|
+
StageCost,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_T = TypeVar("_T")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def make_contract(spec: ScaleSpec) -> ef.Contract:
|
|
33
|
+
columns = tuple(
|
|
34
|
+
ef.ColumnSpec(
|
|
35
|
+
name="id" if i == 0 else f"value_{i}",
|
|
36
|
+
logical_type=ef.StringType()
|
|
37
|
+
if i > 1 and i % 2 == 0
|
|
38
|
+
else ef.IntegerType(bits=64, signed=True),
|
|
39
|
+
nullable=i > 0 and spec.null_every is not None,
|
|
40
|
+
numeric_policy=None
|
|
41
|
+
if i > 1 and i % 2 == 0
|
|
42
|
+
else ef.NumericPolicy(comparison="exact", unit="count"),
|
|
43
|
+
null_policy=ef.NullPolicy(representations=("null",)),
|
|
44
|
+
)
|
|
45
|
+
for i in range(spec.width)
|
|
46
|
+
)
|
|
47
|
+
keys = (
|
|
48
|
+
(
|
|
49
|
+
ef.KeySpec(
|
|
50
|
+
name="id",
|
|
51
|
+
columns=("id",),
|
|
52
|
+
unique=True,
|
|
53
|
+
incomplete="reject",
|
|
54
|
+
scope=ef.TableKeyScope(),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
if spec.alignment == "keyed"
|
|
58
|
+
else ()
|
|
59
|
+
)
|
|
60
|
+
schema = ef.TableSchema(
|
|
61
|
+
columns=columns, keys=keys, multiplicity="preserve", order=ef.IgnoredOrder()
|
|
62
|
+
)
|
|
63
|
+
return ef.Contract(
|
|
64
|
+
schema_version="1.0",
|
|
65
|
+
contract_id="engineering.scale",
|
|
66
|
+
revision=1,
|
|
67
|
+
family="duplicates_order",
|
|
68
|
+
inputs=(ef.TableSpec(name="source", schema_spec=schema),),
|
|
69
|
+
output=ef.TableSpec(name="result", schema_spec=schema),
|
|
70
|
+
preconditions=(),
|
|
71
|
+
rules=(),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _schema(spec: ScaleSpec) -> Any:
|
|
76
|
+
pa = importlib.import_module("pyarrow")
|
|
77
|
+
return pa.schema(
|
|
78
|
+
[
|
|
79
|
+
pa.field(
|
|
80
|
+
"id" if i == 0 else f"value_{i}",
|
|
81
|
+
pa.string() if i > 1 and i % 2 == 0 else pa.int64(),
|
|
82
|
+
nullable=i > 0 and spec.null_every is not None,
|
|
83
|
+
)
|
|
84
|
+
for i in range(spec.width)
|
|
85
|
+
]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _value(spec: ScaleSpec, row: int, column: int, *, changed: bool) -> int | str | None:
|
|
90
|
+
identity = row % (spec.distinct_rows or spec.rows)
|
|
91
|
+
if column == 0:
|
|
92
|
+
return identity
|
|
93
|
+
# Alter the first payload column after NULL generation, so even NULL is a real change.
|
|
94
|
+
if changed and column == 1:
|
|
95
|
+
return -(identity + 1)
|
|
96
|
+
if spec.null_every is not None and identity % spec.null_every == 0:
|
|
97
|
+
return None
|
|
98
|
+
number = 0 if identity % 100 < spec.skew_percent else identity % spec.cardinality
|
|
99
|
+
if column > 1 and column % 2 == 0:
|
|
100
|
+
return ("x" * (1 + number % spec.text_max_length)) + str(number)
|
|
101
|
+
return number
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def capture(spec: ScaleSpec, directory: Path) -> tuple[Path, Path]:
|
|
105
|
+
"""Generate both complete sides in bounded batches directly into Arrow IPC files."""
|
|
106
|
+
pa = importlib.import_module("pyarrow")
|
|
107
|
+
schema = _schema(spec)
|
|
108
|
+
paths = directory / "candidate.arrow", directory / "expected.arrow"
|
|
109
|
+
for side, path in enumerate(paths):
|
|
110
|
+
with pa.OSFile(str(path), "wb") as sink, pa.ipc.new_file(sink, schema) as writer:
|
|
111
|
+
for start in range(0, spec.rows, spec.batch_rows):
|
|
112
|
+
rows = range(start, min(start + spec.batch_rows, spec.rows))
|
|
113
|
+
arrays = [
|
|
114
|
+
pa.array(
|
|
115
|
+
[
|
|
116
|
+
_value(
|
|
117
|
+
spec,
|
|
118
|
+
row,
|
|
119
|
+
column,
|
|
120
|
+
changed=side == 0
|
|
121
|
+
and (
|
|
122
|
+
(spec.difference_last and row == spec.rows - 1)
|
|
123
|
+
or (
|
|
124
|
+
spec.difference_every is not None
|
|
125
|
+
and row % spec.difference_every == 0
|
|
126
|
+
)
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
for row in rows
|
|
130
|
+
],
|
|
131
|
+
type=schema.field(column).type,
|
|
132
|
+
)
|
|
133
|
+
for column in range(spec.width)
|
|
134
|
+
]
|
|
135
|
+
writer.write_batch(pa.RecordBatch.from_arrays(arrays, schema=schema))
|
|
136
|
+
return paths
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _snapshot(paths: tuple[Path, Path], directory: Path) -> None:
|
|
140
|
+
pa = importlib.import_module("pyarrow")
|
|
141
|
+
records = []
|
|
142
|
+
for path in paths:
|
|
143
|
+
with pa.memory_map(str(path), "r") as source:
|
|
144
|
+
reader = pa.ipc.open_file(source)
|
|
145
|
+
rows = sum(reader.get_batch(i).num_rows for i in range(reader.num_record_batches))
|
|
146
|
+
records.append(
|
|
147
|
+
{"artifact": describe_file(path, directory).model_dump(mode="json"), "rows": rows}
|
|
148
|
+
)
|
|
149
|
+
publish_json({"inputs": records}, directory / "snapshot.json")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _baseline(spec: ScaleSpec, paths: tuple[Path, Path], method: str) -> BaselineCost:
|
|
153
|
+
name: Any = method
|
|
154
|
+
if method == "datacompy":
|
|
155
|
+
return BaselineCost(
|
|
156
|
+
method=name,
|
|
157
|
+
state="not_available",
|
|
158
|
+
wall_seconds=None,
|
|
159
|
+
reason=(
|
|
160
|
+
"DataComPy is not a pinned dependency of this candidate; "
|
|
161
|
+
"no untested semantic or version claims"
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
if method == "pandas_assert" and spec.alignment != "keyed":
|
|
165
|
+
return BaselineCost(
|
|
166
|
+
method=name,
|
|
167
|
+
state="not_applicable",
|
|
168
|
+
wall_seconds=None,
|
|
169
|
+
reason=(
|
|
170
|
+
"Native assertion baseline is restricted to declared unique keys; "
|
|
171
|
+
"no silent row sorting or multiset policy substitution"
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
start = time.perf_counter()
|
|
175
|
+
state: Literal["equivalent", "different"]
|
|
176
|
+
try:
|
|
177
|
+
pa = importlib.import_module("pyarrow")
|
|
178
|
+
engine = importlib.import_module("pandas" if method == "pandas_assert" else "duckdb")
|
|
179
|
+
tables = []
|
|
180
|
+
for path in paths:
|
|
181
|
+
with pa.memory_map(str(path), "r") as source:
|
|
182
|
+
tables.append(pa.ipc.open_file(source).read_all())
|
|
183
|
+
if not tables[0].schema.equals(tables[1].schema, check_metadata=True):
|
|
184
|
+
state = "different"
|
|
185
|
+
elif method == "pandas_assert":
|
|
186
|
+
frames = [
|
|
187
|
+
table.to_pandas(types_mapper=engine.ArrowDtype)
|
|
188
|
+
.sort_values("id")
|
|
189
|
+
.reset_index(drop=True)
|
|
190
|
+
for table in tables
|
|
191
|
+
]
|
|
192
|
+
try:
|
|
193
|
+
engine.testing.assert_frame_equal(
|
|
194
|
+
frames[0], frames[1], check_exact=True, check_dtype=True, check_names=True
|
|
195
|
+
)
|
|
196
|
+
state = "equivalent"
|
|
197
|
+
except AssertionError:
|
|
198
|
+
state = "different"
|
|
199
|
+
else:
|
|
200
|
+
with engine.connect(config={"threads": "1", "memory_limit": "256MB"}) as connection:
|
|
201
|
+
connection.register("lhs", tables[0])
|
|
202
|
+
connection.register("rhs", tables[1])
|
|
203
|
+
delta = connection.execute(
|
|
204
|
+
"SELECT EXISTS(SELECT * FROM (SELECT * FROM lhs EXCEPT ALL SELECT * FROM rhs) "
|
|
205
|
+
"UNION ALL SELECT * FROM (SELECT * FROM rhs EXCEPT ALL SELECT * FROM lhs))"
|
|
206
|
+
).fetchone()
|
|
207
|
+
state = "different" if delta[0] else "equivalent"
|
|
208
|
+
return BaselineCost(
|
|
209
|
+
method=name,
|
|
210
|
+
state=state,
|
|
211
|
+
wall_seconds=time.perf_counter() - start,
|
|
212
|
+
reason=(
|
|
213
|
+
"Exact integers/text/NULL and equal Arrow schemas; includes native loading/"
|
|
214
|
+
"conversion/query; excludes full differences, durable snapshots, reporting "
|
|
215
|
+
"and integrity verification"
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
except ImportError:
|
|
219
|
+
return BaselineCost(
|
|
220
|
+
method=name,
|
|
221
|
+
state="not_available",
|
|
222
|
+
wall_seconds=None,
|
|
223
|
+
reason="Optional native engine is not installed",
|
|
224
|
+
)
|
|
225
|
+
except Exception as error:
|
|
226
|
+
return BaselineCost(
|
|
227
|
+
method=name,
|
|
228
|
+
state="failed",
|
|
229
|
+
wall_seconds=time.perf_counter() - start,
|
|
230
|
+
reason=type(error).__name__,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def run_trial(spec: ScaleSpec, directory: Path, repetition: int) -> ScaleTrial:
|
|
235
|
+
directory.mkdir()
|
|
236
|
+
stages: list[StageCost] = []
|
|
237
|
+
monitor = _Monitor(_PsutilProbe, SCALE_SAMPLE_INTERVAL_SECONDS)
|
|
238
|
+
monitor.start()
|
|
239
|
+
start = time.perf_counter()
|
|
240
|
+
|
|
241
|
+
def measured(name: str, scope: str, action: Callable[[], _T]) -> _T:
|
|
242
|
+
began = time.perf_counter()
|
|
243
|
+
value = action()
|
|
244
|
+
stages.append(StageCost(name=name, wall_seconds=time.perf_counter() - began, scope=scope))
|
|
245
|
+
return value
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
paths = measured(
|
|
249
|
+
"capture",
|
|
250
|
+
"Synthetic generation and batch Arrow IPC writing; no external database",
|
|
251
|
+
lambda: capture(spec, directory),
|
|
252
|
+
)
|
|
253
|
+
measured(
|
|
254
|
+
"snapshot",
|
|
255
|
+
"Complete input hashing and batch row counting",
|
|
256
|
+
lambda: _snapshot(paths, directory),
|
|
257
|
+
)
|
|
258
|
+
transferred = directory / "left.arrow", directory / "right.arrow"
|
|
259
|
+
measured(
|
|
260
|
+
"transfer",
|
|
261
|
+
"Local file copies only, no network transfer",
|
|
262
|
+
lambda: tuple(shutil.copyfile(a, b) for a, b in zip(paths, transferred, strict=True)),
|
|
263
|
+
)
|
|
264
|
+
contract = measured(
|
|
265
|
+
"plan",
|
|
266
|
+
"Declare and validate the exact contract; "
|
|
267
|
+
"comparator internal planning remains in compare",
|
|
268
|
+
lambda: make_contract(spec),
|
|
269
|
+
)
|
|
270
|
+
result = measured(
|
|
271
|
+
"compare",
|
|
272
|
+
"Public compare end-to-end including its input snapshots, planning, "
|
|
273
|
+
"exact SQLite work, serialization and persistence",
|
|
274
|
+
lambda: ef.compare(
|
|
275
|
+
*transferred,
|
|
276
|
+
contract=contract,
|
|
277
|
+
reference="expected",
|
|
278
|
+
work_dir=directory / "comparison",
|
|
279
|
+
options=spec.comparison_options,
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
serialized = measured(
|
|
283
|
+
"serialize",
|
|
284
|
+
"Additional standalone serialization of the returned comparison document",
|
|
285
|
+
lambda: result.model_dump_json().encode("utf-8"),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def persist() -> None:
|
|
289
|
+
target = directory / "serialized-comparison.json"
|
|
290
|
+
partial = directory / "serialized-comparison.json.part"
|
|
291
|
+
with partial.open("xb") as output:
|
|
292
|
+
output.write(serialized)
|
|
293
|
+
output.flush()
|
|
294
|
+
os.fsync(output.fileno())
|
|
295
|
+
os.link(partial, target)
|
|
296
|
+
partial.unlink()
|
|
297
|
+
|
|
298
|
+
measured(
|
|
299
|
+
"persist",
|
|
300
|
+
"Additional standalone fsync plus exclusive atomic manifest publication",
|
|
301
|
+
persist,
|
|
302
|
+
)
|
|
303
|
+
measured(
|
|
304
|
+
"render",
|
|
305
|
+
"Privacy-filtered report construction and HTML/text/JSON publication "
|
|
306
|
+
"without reexecuting inputs",
|
|
307
|
+
lambda: ef.write_comparison_report(result, directory / "report"),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def verify() -> int:
|
|
311
|
+
restored = ef.read_comparison(directory / "serialized-comparison.json")
|
|
312
|
+
if restored != result:
|
|
313
|
+
raise ValueError("comparison roundtrip changed")
|
|
314
|
+
count = sum(1 for _ in ef.iter_differences(result.differences))
|
|
315
|
+
verify_files(directory, inventory(directory))
|
|
316
|
+
return count
|
|
317
|
+
|
|
318
|
+
count = measured(
|
|
319
|
+
"verify",
|
|
320
|
+
"Comparison reader, all difference records, and complete evidence SHA-256 reread",
|
|
321
|
+
verify,
|
|
322
|
+
)
|
|
323
|
+
elapsed = time.perf_counter() - start
|
|
324
|
+
finally:
|
|
325
|
+
resources = monitor.finish(time.perf_counter() - start)
|
|
326
|
+
# Baselines are separate services and outside EquiFrame pipeline duration/RSS samples.
|
|
327
|
+
baselines = (
|
|
328
|
+
tuple(
|
|
329
|
+
_baseline(spec, transferred, name)
|
|
330
|
+
for name in ("pandas_assert", "duckdb_sql", "datacompy")
|
|
331
|
+
)
|
|
332
|
+
if spec.run_baselines
|
|
333
|
+
else ()
|
|
334
|
+
)
|
|
335
|
+
evidence = inventory(directory)
|
|
336
|
+
return ScaleTrial(
|
|
337
|
+
case_sha256=digest(spec),
|
|
338
|
+
repetition=repetition,
|
|
339
|
+
cache_state="fresh_process_os_cache_uncontrolled"
|
|
340
|
+
if repetition == 0
|
|
341
|
+
else "warm_process_os_cache_uncontrolled",
|
|
342
|
+
stages=tuple(stages),
|
|
343
|
+
end_to_end_seconds=elapsed,
|
|
344
|
+
resources=resources,
|
|
345
|
+
comparison=result,
|
|
346
|
+
verified_difference_records=count,
|
|
347
|
+
evidence_bytes=sum(item.size_bytes for item in evidence),
|
|
348
|
+
evidence=evidence,
|
|
349
|
+
baselines=baselines,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def main() -> None:
|
|
354
|
+
from equiframe_benchmark.scaling import _environment
|
|
355
|
+
|
|
356
|
+
spec = load_json(ScaleSpec, Path(sys.argv[1]))
|
|
357
|
+
directory = Path(sys.argv[2])
|
|
358
|
+
for repetition in range(spec.repetitions):
|
|
359
|
+
trial = run_trial(spec, directory / f"trial-{repetition:03d}", repetition)
|
|
360
|
+
save_scale_document(trial, directory / f"trial-{repetition:03d}.json")
|
|
361
|
+
files = tuple(
|
|
362
|
+
item
|
|
363
|
+
for item in inventory(directory)
|
|
364
|
+
if item.path in {f"trial-{i:03d}.json" for i in range(spec.repetitions)}
|
|
365
|
+
)
|
|
366
|
+
save_scale_document(
|
|
367
|
+
ScaleWorkerCompletion(
|
|
368
|
+
environment=_environment(), case_sha256=digest(spec), trial_files=files
|
|
369
|
+
),
|
|
370
|
+
directory / "worker-completed.json",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
if __name__ == "__main__":
|
|
375
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Reviewed calibration lineage and group support stay outside detector features."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from equiframe import NormalCalibrationProfiles
|
|
6
|
+
from pydantic import model_validator
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from equiframe_benchmark.models import Digest, Document, Text, digest
|
|
10
|
+
from equiframe_benchmark.training_types import TrainingRecord, TrainingSelection
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CalibrationManifest(Document):
|
|
14
|
+
purpose: Literal["normal_calibration"] = "normal_calibration"
|
|
15
|
+
catalog_sha256: Digest
|
|
16
|
+
splits_sha256: Digest
|
|
17
|
+
records: tuple[TrainingRecord, ...]
|
|
18
|
+
|
|
19
|
+
@model_validator(mode="after")
|
|
20
|
+
def unique(self) -> Self:
|
|
21
|
+
if not self.records:
|
|
22
|
+
raise ValueError("nonempty normal calibration selection required")
|
|
23
|
+
for field in ("candidate_id", "execution_sha256", "profile_sha256"):
|
|
24
|
+
if len({getattr(r, field) for r in self.records}) != len(self.records):
|
|
25
|
+
raise ValueError("duplicate calibration evidence")
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CalibrationSelection(Document):
|
|
30
|
+
manifest: CalibrationManifest
|
|
31
|
+
calibration: NormalCalibrationProfiles
|
|
32
|
+
|
|
33
|
+
@model_validator(mode="after")
|
|
34
|
+
def consistent(self) -> Self:
|
|
35
|
+
if digest(self.manifest) != self.calibration.selection_manifest_sha256:
|
|
36
|
+
raise ValueError("calibration manifest mismatch")
|
|
37
|
+
if len(self.manifest.records) != len(self.calibration.profiles):
|
|
38
|
+
raise ValueError("calibration selection size mismatch")
|
|
39
|
+
if any(
|
|
40
|
+
record.vector_sha256 != digest(vector)
|
|
41
|
+
for record, vector in zip(self.manifest.records, self.calibration.profiles, strict=True)
|
|
42
|
+
):
|
|
43
|
+
raise ValueError("calibration vectors do not match recorded evidence")
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DetectorSelectionAudit(Document):
|
|
48
|
+
training_sha256: Digest
|
|
49
|
+
calibration_sha256: Digest
|
|
50
|
+
training_groups: int
|
|
51
|
+
calibration_groups: int
|
|
52
|
+
min_training_groups: int
|
|
53
|
+
min_calibration_groups: int
|
|
54
|
+
rationale: Text
|
|
55
|
+
state: Literal["eligible", "not_estimable"]
|
|
56
|
+
|
|
57
|
+
@model_validator(mode="after")
|
|
58
|
+
def consistent(self) -> Self:
|
|
59
|
+
if min(self.min_training_groups, self.min_calibration_groups) < 1:
|
|
60
|
+
raise ValueError("explicit positive group minima required")
|
|
61
|
+
if min(self.training_groups, self.calibration_groups) < 1:
|
|
62
|
+
raise ValueError("selections must contain groups")
|
|
63
|
+
eligible = (
|
|
64
|
+
self.training_groups >= self.min_training_groups
|
|
65
|
+
and self.calibration_groups >= self.min_calibration_groups
|
|
66
|
+
)
|
|
67
|
+
if (self.state == "eligible") != eligible:
|
|
68
|
+
raise ValueError("audit state contradicts group support")
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def audit_detector_selections(
|
|
73
|
+
training: TrainingSelection,
|
|
74
|
+
calibration: CalibrationSelection,
|
|
75
|
+
*,
|
|
76
|
+
min_training_groups: int,
|
|
77
|
+
min_calibration_groups: int,
|
|
78
|
+
rationale: str,
|
|
79
|
+
) -> DetectorSelectionAudit:
|
|
80
|
+
"""Check pair provenance/support before fitting; eligibility is not a power guarantee."""
|
|
81
|
+
training = TrainingSelection.model_validate(training)
|
|
82
|
+
calibration = CalibrationSelection.model_validate(calibration)
|
|
83
|
+
t, c = training.manifest, calibration.manifest
|
|
84
|
+
if t.catalog_sha256 != c.catalog_sha256 or t.splits_sha256 != c.splits_sha256:
|
|
85
|
+
raise ValueError("training and calibration require the same catalog and split revision")
|
|
86
|
+
if (
|
|
87
|
+
len(
|
|
88
|
+
{
|
|
89
|
+
p.spec_sha256
|
|
90
|
+
for p in (*training.training.profiles, *calibration.calibration.profiles)
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
!= 1
|
|
94
|
+
):
|
|
95
|
+
raise ValueError("a detector pair requires one family and feature revision")
|
|
96
|
+
for field in ("group_id", "scenario_id", "candidate_id", "execution_sha256", "profile_sha256"):
|
|
97
|
+
if {getattr(r, field) for r in t.records} & {getattr(r, field) for r in c.records}:
|
|
98
|
+
raise ValueError("training/calibration leakage in " + field)
|
|
99
|
+
train_groups = len({r.group_id for r in t.records})
|
|
100
|
+
calibration_groups = len({r.group_id for r in c.records})
|
|
101
|
+
return DetectorSelectionAudit(
|
|
102
|
+
training_sha256=digest(training),
|
|
103
|
+
calibration_sha256=digest(calibration),
|
|
104
|
+
training_groups=train_groups,
|
|
105
|
+
calibration_groups=calibration_groups,
|
|
106
|
+
min_training_groups=min_training_groups,
|
|
107
|
+
min_calibration_groups=min_calibration_groups,
|
|
108
|
+
rationale=rationale,
|
|
109
|
+
state="eligible"
|
|
110
|
+
if train_groups >= min_training_groups and calibration_groups >= min_calibration_groups
|
|
111
|
+
else "not_estimable",
|
|
112
|
+
)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Benchmark commands keep experimental labels outside the operational library."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from equiframe._cli_support import exit_with_failure
|
|
12
|
+
|
|
13
|
+
from equiframe_benchmark.commands import register as _register_delivery
|
|
14
|
+
from equiframe_benchmark.commands_scale import register as _register_scale
|
|
15
|
+
from equiframe_benchmark.commands_step10 import register as _register_step10
|
|
16
|
+
from equiframe_benchmark.commands_v2 import register_v2 as _register_v2
|
|
17
|
+
from equiframe_benchmark.io import load_document, save_document
|
|
18
|
+
from equiframe_benchmark.models import Catalog, SplitManifest
|
|
19
|
+
from equiframe_benchmark.splits import dependency_groups, make_splits, split_counts, validate_splits
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(no_args_is_help=True, add_completion=False, pretty_exceptions_enable=False)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _failure(error: Exception, *, input_validation: bool = True) -> None:
|
|
25
|
+
invalid = input_validation and isinstance(error, ValueError)
|
|
26
|
+
exit_with_failure(
|
|
27
|
+
error,
|
|
28
|
+
code="invalid_input" if invalid else "operation_failed",
|
|
29
|
+
exit_code=2 if invalid else 3,
|
|
30
|
+
input_validation=input_validation,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("validate")
|
|
35
|
+
def validate_command(catalog: Path, splits: Path | None = typer.Option(None)) -> None:
|
|
36
|
+
"""Validate a catalog and optional grouped partition manifest; never create labels."""
|
|
37
|
+
try:
|
|
38
|
+
document = load_document(Catalog, catalog)
|
|
39
|
+
summary: dict[str, object] = {
|
|
40
|
+
"catalog_id": document.catalog_id,
|
|
41
|
+
"synthetic": document.synthetic,
|
|
42
|
+
"scenarios": len(document.scenarios),
|
|
43
|
+
"groups": len(dependency_groups(document)),
|
|
44
|
+
"scientific_review_confirmed": False,
|
|
45
|
+
}
|
|
46
|
+
if splits:
|
|
47
|
+
manifest = load_document(SplitManifest, splits)
|
|
48
|
+
validate_splits(document, manifest)
|
|
49
|
+
summary["partitions"] = split_counts(document, manifest)
|
|
50
|
+
except Exception as error:
|
|
51
|
+
_failure(error)
|
|
52
|
+
return
|
|
53
|
+
typer.echo(json.dumps(summary, sort_keys=True))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command("split")
|
|
57
|
+
def split_command(
|
|
58
|
+
catalog: Path,
|
|
59
|
+
output: Path = typer.Option(...),
|
|
60
|
+
seed: int = typer.Option(...),
|
|
61
|
+
training: int = typer.Option(3),
|
|
62
|
+
calibration: int = typer.Option(1),
|
|
63
|
+
test: int = typer.Option(2),
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Create an exclusive grouped split with an explicit seed and integer weights."""
|
|
66
|
+
try:
|
|
67
|
+
document = load_document(Catalog, catalog)
|
|
68
|
+
manifest = make_splits(document, seed=seed, weights=(training, calibration, test))
|
|
69
|
+
validate_splits(document, manifest)
|
|
70
|
+
save_document(manifest, output)
|
|
71
|
+
except Exception as error:
|
|
72
|
+
_failure(error)
|
|
73
|
+
return
|
|
74
|
+
typer.echo(json.dumps(split_counts(document, manifest), sort_keys=True))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command("pilot")
|
|
78
|
+
def pilot_command(output: Path = typer.Option(Path("equiframe-benchmark-runs"))) -> None:
|
|
79
|
+
"""Run a new synthetic native pilot; human reviews remain pending."""
|
|
80
|
+
from equiframe_benchmark.pilot import run_pilot
|
|
81
|
+
|
|
82
|
+
directory = output.resolve() / ("pilot_" + uuid4().hex)
|
|
83
|
+
try:
|
|
84
|
+
summary = run_pilot(directory)
|
|
85
|
+
except Exception as error:
|
|
86
|
+
_failure(error)
|
|
87
|
+
return
|
|
88
|
+
typer.echo(json.dumps({"pilot_id": directory.name, **summary}, sort_keys=True))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _factory(reference: str) -> Callable[[], object]:
|
|
92
|
+
from typing import cast
|
|
93
|
+
|
|
94
|
+
if not re.fullmatch(r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*:[A-Za-z_]\w*", reference):
|
|
95
|
+
raise ValueError("explicit module:factory reference is required")
|
|
96
|
+
module, name = reference.split(":")
|
|
97
|
+
factory = getattr(importlib.import_module(module), name)
|
|
98
|
+
if not callable(factory):
|
|
99
|
+
raise ValueError("registered factory must be callable")
|
|
100
|
+
return cast(Callable[[], object], factory)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command("integrate")
|
|
104
|
+
def integrate_command(
|
|
105
|
+
spec: Path,
|
|
106
|
+
snapshot_id: str = typer.Option(...),
|
|
107
|
+
client: str = typer.Option(...),
|
|
108
|
+
registry: str = typer.Option(...),
|
|
109
|
+
output: Path = typer.Option(Path("equiframe-integration")),
|
|
110
|
+
expected: Path | None = typer.Option(None),
|
|
111
|
+
bundle: Path | None = typer.Option(None),
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Consume a pinned snapshot through explicitly supplied trusted client/code."""
|
|
114
|
+
from typing import cast
|
|
115
|
+
|
|
116
|
+
import equiframe as ef
|
|
117
|
+
from equiframe.evidence import load_json
|
|
118
|
+
|
|
119
|
+
from equiframe_benchmark.integration import IntegrationSpec, run_integration
|
|
120
|
+
from equiframe_benchmark.snapshot import SnapshotClient, SnapshotDataSource
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
configuration = load_json(IntegrationSpec, spec)
|
|
124
|
+
detector = load_json(ef.DetectorBundle, bundle) if bundle else None
|
|
125
|
+
registered = _factory(registry)()
|
|
126
|
+
if not isinstance(registered, ef.TransformationRegistry):
|
|
127
|
+
raise ValueError("registry factory must return a TransformationRegistry")
|
|
128
|
+
transport = _factory(client)()
|
|
129
|
+
if not all(
|
|
130
|
+
callable(getattr(transport, name, None))
|
|
131
|
+
for name in ("get_snapshot", "download_artifact")
|
|
132
|
+
):
|
|
133
|
+
raise ValueError("client factory must expose the snapshot client protocol")
|
|
134
|
+
source = SnapshotDataSource(
|
|
135
|
+
snapshot_id,
|
|
136
|
+
client=cast(SnapshotClient, transport),
|
|
137
|
+
expected_domain_schema_revision=configuration.domain_schema_revision,
|
|
138
|
+
table_names=tuple(
|
|
139
|
+
table.name for table in configuration.pipelines[0].execution.contract.inputs
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
result = run_integration(
|
|
143
|
+
configuration,
|
|
144
|
+
source=source,
|
|
145
|
+
registry=registered,
|
|
146
|
+
detector_bundle=detector,
|
|
147
|
+
expected=expected,
|
|
148
|
+
work_dir=output,
|
|
149
|
+
)
|
|
150
|
+
except Exception as error:
|
|
151
|
+
_failure(error)
|
|
152
|
+
return
|
|
153
|
+
typer.echo(
|
|
154
|
+
json.dumps(
|
|
155
|
+
{
|
|
156
|
+
"integration_id": result.integration_id,
|
|
157
|
+
"complete": result.complete,
|
|
158
|
+
"inputs_verified": result.inputs_verified,
|
|
159
|
+
"scientific_equivalence_confirmed": False,
|
|
160
|
+
},
|
|
161
|
+
sort_keys=True,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
raise typer.Exit(code=result.exit_code)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command("verify-integration")
|
|
168
|
+
def verify_integration_command(directory: Path) -> None:
|
|
169
|
+
"""Verify the existing receipt and evidence without contacting the service."""
|
|
170
|
+
from equiframe_benchmark.integration import read_integration
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
result = read_integration(directory)
|
|
174
|
+
except Exception as error:
|
|
175
|
+
_failure(error, input_validation=False)
|
|
176
|
+
return
|
|
177
|
+
typer.echo(
|
|
178
|
+
json.dumps(
|
|
179
|
+
{
|
|
180
|
+
"integration_id": result.integration_id,
|
|
181
|
+
"evidence_verified": True,
|
|
182
|
+
"complete": result.complete,
|
|
183
|
+
},
|
|
184
|
+
sort_keys=True,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
_register_scale(app)
|
|
190
|
+
_register_step10(app)
|
|
191
|
+
_register_v2(app)
|
|
192
|
+
_register_delivery(app)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def main() -> None:
|
|
196
|
+
app()
|