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,428 @@
|
|
|
1
|
+
"""Version 2: principal paired supports; the four-method intersection is additional.
|
|
2
|
+
|
|
3
|
+
V1 documents and fingerprints are deliberately unchanged. This module consumes
|
|
4
|
+
captured decisions only: no training, labels, scores or native runs are generated.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import random
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from fractions import Fraction
|
|
14
|
+
from math import ceil
|
|
15
|
+
from typing import Annotated, Literal, cast
|
|
16
|
+
|
|
17
|
+
from equiframe import Family
|
|
18
|
+
from pydantic import Field, model_validator
|
|
19
|
+
from typing_extensions import Self
|
|
20
|
+
|
|
21
|
+
from equiframe_benchmark.evaluation import (
|
|
22
|
+
METHODS,
|
|
23
|
+
BootstrapPlan,
|
|
24
|
+
ConfusionMetrics,
|
|
25
|
+
EvaluationConfig,
|
|
26
|
+
EvaluationRecord,
|
|
27
|
+
GroupBootstrapResult,
|
|
28
|
+
IntervalEstimate,
|
|
29
|
+
Method,
|
|
30
|
+
MetricValue,
|
|
31
|
+
_ap,
|
|
32
|
+
_bootstrap,
|
|
33
|
+
_component,
|
|
34
|
+
_confusion,
|
|
35
|
+
_principal,
|
|
36
|
+
_records_digest,
|
|
37
|
+
_validate_inputs,
|
|
38
|
+
)
|
|
39
|
+
from equiframe_benchmark.models import Digest, Text, digest
|
|
40
|
+
from equiframe_benchmark.models_v2 import DocumentV2 as Document
|
|
41
|
+
from equiframe_benchmark.pairwise import PairwiseComparison, StratumKind, _direction, _pair
|
|
42
|
+
|
|
43
|
+
PRIMARY_FAMILIES: tuple[Family, ...] = ("nulls_types", "joins", "aggregations")
|
|
44
|
+
MigrationPairV2 = Literal["pandas-polars", "pandas-duckdb", "polars-duckdb"]
|
|
45
|
+
PRIMARY_MIGRATIONS: tuple[MigrationPairV2, ...] = (
|
|
46
|
+
"pandas-polars",
|
|
47
|
+
"pandas-duckdb",
|
|
48
|
+
"polars-duckdb",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ContrastSpecV2(Document):
|
|
53
|
+
schema_version: Literal["2.0"] = "2.0"
|
|
54
|
+
left: Method
|
|
55
|
+
right: Method
|
|
56
|
+
|
|
57
|
+
@model_validator(mode="after")
|
|
58
|
+
def distinct(self) -> Self:
|
|
59
|
+
if self.left == self.right:
|
|
60
|
+
raise ValueError("a paired contrast requires two distinct methods")
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class EvaluationConfigV2(Document):
|
|
65
|
+
schema_version: Literal["2.0"] = "2.0"
|
|
66
|
+
protocol: Literal["paired-principal-2.0"] = "paired-principal-2.0"
|
|
67
|
+
mode: Literal["pilot", "final"]
|
|
68
|
+
freeze_sha256: Digest | None
|
|
69
|
+
bootstrap: BootstrapPlan
|
|
70
|
+
contrasts: Annotated[tuple[ContrastSpecV2, ...], Field(min_length=1, max_length=6)]
|
|
71
|
+
expected_families: tuple[Family, ...] = PRIMARY_FAMILIES
|
|
72
|
+
expected_migrations: tuple[MigrationPairV2, ...] = PRIMARY_MIGRATIONS
|
|
73
|
+
|
|
74
|
+
@model_validator(mode="after")
|
|
75
|
+
def binding(self) -> Self:
|
|
76
|
+
if (self.mode == "final") != (self.freeze_sha256 is not None):
|
|
77
|
+
raise ValueError("final V2 evaluation requires its own frozen manifest")
|
|
78
|
+
if self.expected_families != PRIMARY_FAMILIES:
|
|
79
|
+
raise ValueError("V2 requires the three primary families in canonical order")
|
|
80
|
+
if self.expected_migrations != PRIMARY_MIGRATIONS:
|
|
81
|
+
raise ValueError("V2 requires all three engine pairs in canonical order")
|
|
82
|
+
pairs = [frozenset((pair.left, pair.right)) for pair in self.contrasts]
|
|
83
|
+
if len(set(pairs)) != len(pairs):
|
|
84
|
+
raise ValueError("a method contrast cannot be repeated or reversed")
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def evaluation_protocol_sha256_v2(config: EvaluationConfigV2) -> str:
|
|
89
|
+
checked = EvaluationConfigV2.model_validate(config)
|
|
90
|
+
payload = {
|
|
91
|
+
"schema_version": "2.0",
|
|
92
|
+
"protocol": checked.protocol,
|
|
93
|
+
"bootstrap": checked.bootstrap.model_dump(mode="json"),
|
|
94
|
+
"contrasts": [pair.model_dump(mode="json") for pair in checked.contrasts],
|
|
95
|
+
"expected_families": checked.expected_families,
|
|
96
|
+
"expected_migrations": checked.expected_migrations,
|
|
97
|
+
"principal": "confirmed_O_and_both_contrasted_methods_complete",
|
|
98
|
+
"additional": "confirmed_O_and_complete_R_M_B_H",
|
|
99
|
+
"uncertainty": "same_resampled_whole_groups_for_both_methods_and_differences",
|
|
100
|
+
"undefined": "explicit_denominators_no_undefined_replicate_omission",
|
|
101
|
+
"average_precision": "tied_thresholds_two_classes_homogeneous_family_and_bundle",
|
|
102
|
+
}
|
|
103
|
+
return hashlib.sha256(
|
|
104
|
+
json.dumps(payload, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
|
|
105
|
+
).hexdigest()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SignedMetricV2(Document):
|
|
109
|
+
schema_version: Literal["2.0"] = "2.0"
|
|
110
|
+
numerator: int
|
|
111
|
+
denominator: Annotated[int, Field(ge=0)]
|
|
112
|
+
value: float | None
|
|
113
|
+
reason: Text | None
|
|
114
|
+
|
|
115
|
+
@model_validator(mode="after")
|
|
116
|
+
def arithmetic(self) -> Self:
|
|
117
|
+
if self.denominator == 0:
|
|
118
|
+
if self.numerator != 0 or self.value is not None or self.reason is None:
|
|
119
|
+
raise ValueError("undefined contrast requires zero counts and a reason")
|
|
120
|
+
elif self.reason is not None or self.value != float(
|
|
121
|
+
Fraction(self.numerator, self.denominator)
|
|
122
|
+
):
|
|
123
|
+
raise ValueError("contrast value must match its exact rational value")
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class PairedEstimateV2(Document):
|
|
128
|
+
schema_version: Literal["2.0"] = "2.0"
|
|
129
|
+
name: Text
|
|
130
|
+
estimate: SignedMetricV2
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class PrincipalContrastV2(Document):
|
|
134
|
+
schema_version: Literal["2.0"] = "2.0"
|
|
135
|
+
comparison: PairwiseComparison
|
|
136
|
+
estimates: tuple[PairedEstimateV2, ...]
|
|
137
|
+
bootstrap: GroupBootstrapResult
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class CommonMethodV2(Document):
|
|
141
|
+
schema_version: Literal["2.0"] = "2.0"
|
|
142
|
+
method: Method
|
|
143
|
+
metrics: ConfusionMetrics
|
|
144
|
+
average_precision: MetricValue | None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class CommonSupportV2(Document):
|
|
148
|
+
schema_version: Literal["2.0"] = "2.0"
|
|
149
|
+
role: Literal["additional_common_R_M_B_H"] = "additional_common_R_M_B_H"
|
|
150
|
+
case_ids: tuple[Text, ...]
|
|
151
|
+
methods: tuple[CommonMethodV2, ...]
|
|
152
|
+
bootstrap: GroupBootstrapResult
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class EvaluationStratumV2(Document):
|
|
156
|
+
schema_version: Literal["2.0"] = "2.0"
|
|
157
|
+
kind: StratumKind
|
|
158
|
+
family: Family | None
|
|
159
|
+
migration: MigrationPairV2 | None
|
|
160
|
+
total_cases: Annotated[int, Field(ge=0)]
|
|
161
|
+
oracle_confirmed: Annotated[int, Field(ge=0)]
|
|
162
|
+
principal: tuple[PrincipalContrastV2, ...]
|
|
163
|
+
common: CommonSupportV2
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _signed(value: MetricValue) -> SignedMetricV2:
|
|
167
|
+
return SignedMetricV2(
|
|
168
|
+
numerator=value.numerator,
|
|
169
|
+
denominator=value.denominator,
|
|
170
|
+
value=value.value,
|
|
171
|
+
reason=value.reason,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _difference(left: MetricValue, right: MetricValue) -> SignedMetricV2:
|
|
176
|
+
if left.value is None or right.value is None:
|
|
177
|
+
return SignedMetricV2(
|
|
178
|
+
numerator=0, denominator=0, value=None, reason="undefined_contrasted_metric"
|
|
179
|
+
)
|
|
180
|
+
difference = Fraction(left.numerator, left.denominator) - Fraction(
|
|
181
|
+
right.numerator, right.denominator
|
|
182
|
+
)
|
|
183
|
+
return SignedMetricV2(
|
|
184
|
+
numerator=difference.numerator,
|
|
185
|
+
denominator=difference.denominator,
|
|
186
|
+
value=float(difference),
|
|
187
|
+
reason=None,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _estimates_v2(
|
|
192
|
+
records: tuple[EvaluationRecord, ...],
|
|
193
|
+
contrast: ContrastSpecV2,
|
|
194
|
+
) -> dict[str, SignedMetricV2]:
|
|
195
|
+
left = _confusion(records, contrast.left)
|
|
196
|
+
right = _confusion(records, contrast.right)
|
|
197
|
+
result: dict[str, SignedMetricV2] = {}
|
|
198
|
+
for metric in ("precision", "recall", "f1", "false_positive_rate"):
|
|
199
|
+
a, b = getattr(left, metric), getattr(right, metric)
|
|
200
|
+
result[f"{contrast.left}.{metric}"] = _signed(a)
|
|
201
|
+
result[f"{contrast.right}.{metric}"] = _signed(b)
|
|
202
|
+
result[f"difference.{contrast.left}-{contrast.right}.{metric}"] = _difference(a, b)
|
|
203
|
+
for method in (contrast.left, contrast.right):
|
|
204
|
+
if method in ("M", "B"):
|
|
205
|
+
result[f"{method}.average_precision"] = _signed(_ap(records, method))
|
|
206
|
+
for direction in (
|
|
207
|
+
_direction(records, contrast.left, contrast.right),
|
|
208
|
+
_direction(records, contrast.right, contrast.left),
|
|
209
|
+
):
|
|
210
|
+
prefix = f"{direction.method}_only_vs_{direction.reference}"
|
|
211
|
+
result[prefix + ".true_positive_rate"] = _signed(direction.additional_true_positive_rate)
|
|
212
|
+
result[prefix + ".false_positive_rate"] = _signed(direction.additional_false_positive_rate)
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _paired_bootstrap(
|
|
217
|
+
records: tuple[EvaluationRecord, ...],
|
|
218
|
+
contrast: ContrastSpecV2,
|
|
219
|
+
plan: BootstrapPlan,
|
|
220
|
+
) -> GroupBootstrapResult:
|
|
221
|
+
groups: dict[str, list[EvaluationRecord]] = defaultdict(list)
|
|
222
|
+
for record in records:
|
|
223
|
+
groups[record.group_id].append(record)
|
|
224
|
+
identifiers = tuple(sorted(groups))
|
|
225
|
+
points = _estimates_v2(records, contrast)
|
|
226
|
+
reason = (
|
|
227
|
+
"independence_not_supported"
|
|
228
|
+
if not plan.independence_supported
|
|
229
|
+
else "insufficient_groups"
|
|
230
|
+
if len(identifiers) < plan.min_groups
|
|
231
|
+
else None
|
|
232
|
+
)
|
|
233
|
+
if reason is not None:
|
|
234
|
+
return GroupBootstrapResult(
|
|
235
|
+
group_ids=identifiers,
|
|
236
|
+
attempted_replicates=0,
|
|
237
|
+
lower_rank=None,
|
|
238
|
+
upper_rank=None,
|
|
239
|
+
intervals=tuple(
|
|
240
|
+
IntervalEstimate(
|
|
241
|
+
metric=name,
|
|
242
|
+
lower=None,
|
|
243
|
+
upper=None,
|
|
244
|
+
defined_replicates=0,
|
|
245
|
+
undefined_replicates=0,
|
|
246
|
+
reason=reason,
|
|
247
|
+
)
|
|
248
|
+
for name in points
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
rng = random.Random(plan.seed)
|
|
252
|
+
samples: dict[str, list[Fraction]] = {name: [] for name in points}
|
|
253
|
+
undefined = dict.fromkeys(points, 0)
|
|
254
|
+
for _ in range(plan.repetitions):
|
|
255
|
+
# One draw of complete dependency groups serves both contrasted methods.
|
|
256
|
+
selected = tuple(
|
|
257
|
+
record
|
|
258
|
+
for _ in identifiers
|
|
259
|
+
for record in groups[identifiers[rng.randrange(len(identifiers))]]
|
|
260
|
+
)
|
|
261
|
+
for name, estimate in _estimates_v2(selected, contrast).items():
|
|
262
|
+
if estimate.value is None:
|
|
263
|
+
undefined[name] += 1
|
|
264
|
+
else:
|
|
265
|
+
samples[name].append(Fraction(estimate.numerator, estimate.denominator))
|
|
266
|
+
tail = (1 - Fraction(plan.confidence)) / 2
|
|
267
|
+
lower = max(1, ceil(tail * plan.repetitions))
|
|
268
|
+
upper = ceil((1 - tail) * plan.repetitions)
|
|
269
|
+
intervals: list[IntervalEstimate] = []
|
|
270
|
+
for name, point in points.items():
|
|
271
|
+
values = sorted(samples[name])
|
|
272
|
+
failure = (
|
|
273
|
+
point.reason
|
|
274
|
+
if point.value is None
|
|
275
|
+
else "undefined_bootstrap_replicates"
|
|
276
|
+
if undefined[name]
|
|
277
|
+
else None
|
|
278
|
+
)
|
|
279
|
+
intervals.append(
|
|
280
|
+
IntervalEstimate(
|
|
281
|
+
metric=name,
|
|
282
|
+
lower=float(values[lower - 1]) if failure is None else None,
|
|
283
|
+
upper=float(values[upper - 1]) if failure is None else None,
|
|
284
|
+
defined_replicates=len(values),
|
|
285
|
+
undefined_replicates=undefined[name],
|
|
286
|
+
reason=failure,
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
return GroupBootstrapResult(
|
|
290
|
+
group_ids=identifiers,
|
|
291
|
+
attempted_replicates=plan.repetitions,
|
|
292
|
+
lower_rank=lower,
|
|
293
|
+
upper_rank=upper,
|
|
294
|
+
intervals=tuple(intervals),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _strata_v2(
|
|
299
|
+
records: tuple[EvaluationRecord, ...],
|
|
300
|
+
config: EvaluationConfigV2,
|
|
301
|
+
) -> tuple[EvaluationStratumV2, ...]:
|
|
302
|
+
scopes: list[tuple[StratumKind, Family | None, MigrationPairV2 | None]] = [
|
|
303
|
+
("global", None, None)
|
|
304
|
+
]
|
|
305
|
+
scopes.extend(("family", family, None) for family in config.expected_families)
|
|
306
|
+
scopes.extend(("migration", None, migration) for migration in config.expected_migrations)
|
|
307
|
+
scopes.extend(
|
|
308
|
+
("family_migration", family, migration)
|
|
309
|
+
for family in config.expected_families
|
|
310
|
+
for migration in config.expected_migrations
|
|
311
|
+
)
|
|
312
|
+
result = []
|
|
313
|
+
cache: dict[tuple[str, ...], tuple[tuple[PrincipalContrastV2, ...], CommonSupportV2]] = {}
|
|
314
|
+
for kind, family, migration in scopes:
|
|
315
|
+
selected = tuple(
|
|
316
|
+
record
|
|
317
|
+
for record in records
|
|
318
|
+
if (family is None or record.family == family)
|
|
319
|
+
and (migration is None or record.migration == migration)
|
|
320
|
+
)
|
|
321
|
+
key = tuple(record.case_id for record in selected)
|
|
322
|
+
if key not in cache:
|
|
323
|
+
principal = []
|
|
324
|
+
for contrast in config.contrasts:
|
|
325
|
+
paired = tuple(
|
|
326
|
+
record
|
|
327
|
+
for record in selected
|
|
328
|
+
if record.oracle_state == "confirmed"
|
|
329
|
+
and _component(record, contrast.left).complete
|
|
330
|
+
and _component(record, contrast.right).complete
|
|
331
|
+
)
|
|
332
|
+
principal.append(
|
|
333
|
+
PrincipalContrastV2(
|
|
334
|
+
comparison=_pair(selected, contrast.left, contrast.right),
|
|
335
|
+
estimates=tuple(
|
|
336
|
+
PairedEstimateV2(name=name, estimate=value)
|
|
337
|
+
for name, value in _estimates_v2(paired, contrast).items()
|
|
338
|
+
),
|
|
339
|
+
bootstrap=_paired_bootstrap(paired, contrast, config.bootstrap),
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
common = tuple(record for record in selected if _principal(record))
|
|
343
|
+
additional = CommonSupportV2(
|
|
344
|
+
case_ids=tuple(record.case_id for record in common),
|
|
345
|
+
methods=tuple(
|
|
346
|
+
CommonMethodV2(
|
|
347
|
+
method=method,
|
|
348
|
+
metrics=_confusion(common, method),
|
|
349
|
+
average_precision=(_ap(common, method) if method in ("M", "B") else None),
|
|
350
|
+
)
|
|
351
|
+
for method in METHODS
|
|
352
|
+
),
|
|
353
|
+
bootstrap=_bootstrap(common, config.bootstrap),
|
|
354
|
+
)
|
|
355
|
+
cache[key] = (tuple(principal), additional)
|
|
356
|
+
contrasts, common_support = cache[key]
|
|
357
|
+
result.append(
|
|
358
|
+
EvaluationStratumV2(
|
|
359
|
+
kind=kind,
|
|
360
|
+
family=family,
|
|
361
|
+
migration=migration,
|
|
362
|
+
total_cases=len(selected),
|
|
363
|
+
oracle_confirmed=sum(record.oracle_state == "confirmed" for record in selected),
|
|
364
|
+
principal=contrasts,
|
|
365
|
+
common=common_support,
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
return tuple(result)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class EvaluationReportV2(Document):
|
|
372
|
+
schema_version: Literal["2.0"] = "2.0"
|
|
373
|
+
analysis_role: Literal["principal_paired"] = "principal_paired"
|
|
374
|
+
scientific: bool
|
|
375
|
+
config_sha256: Digest
|
|
376
|
+
protocol_sha256: Digest
|
|
377
|
+
records_sha256: Digest
|
|
378
|
+
config: EvaluationConfigV2
|
|
379
|
+
records: tuple[EvaluationRecord, ...]
|
|
380
|
+
strata: tuple[EvaluationStratumV2, ...]
|
|
381
|
+
|
|
382
|
+
@model_validator(mode="after")
|
|
383
|
+
def recalculate(self) -> Self:
|
|
384
|
+
_validate_v2(self.records, self.config)
|
|
385
|
+
if self.records != tuple(sorted(self.records, key=lambda record: record.case_id)):
|
|
386
|
+
raise ValueError("V2 records require canonical case order")
|
|
387
|
+
if (
|
|
388
|
+
self.scientific != (self.config.mode == "final")
|
|
389
|
+
or self.config_sha256 != digest(self.config)
|
|
390
|
+
or self.protocol_sha256 != evaluation_protocol_sha256_v2(self.config)
|
|
391
|
+
or self.records_sha256 != _records_digest(self.records)
|
|
392
|
+
or self.strata != _strata_v2(self.records, self.config)
|
|
393
|
+
):
|
|
394
|
+
raise ValueError("V2 report differs from captured records and frozen paired protocol")
|
|
395
|
+
return self
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _validate_v2(records: tuple[EvaluationRecord, ...], config: EvaluationConfigV2) -> None:
|
|
399
|
+
# Shared validation reads identity/scope fields only; no V1 config is created.
|
|
400
|
+
_validate_inputs(records, cast(EvaluationConfig, config))
|
|
401
|
+
for record in records:
|
|
402
|
+
if record.engine not in record.migration.split("-"):
|
|
403
|
+
raise ValueError("native engine must belong to its declared V2 migration pair")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def evaluate_records_v2(
|
|
407
|
+
records: tuple[EvaluationRecord, ...],
|
|
408
|
+
config: EvaluationConfigV2,
|
|
409
|
+
) -> EvaluationReportV2:
|
|
410
|
+
checked_config = EvaluationConfigV2.model_validate(config)
|
|
411
|
+
if not isinstance(records, tuple):
|
|
412
|
+
raise ValueError("V2 records require an explicit tuple")
|
|
413
|
+
checked = tuple(
|
|
414
|
+
sorted(
|
|
415
|
+
(EvaluationRecord.model_validate(record) for record in records),
|
|
416
|
+
key=lambda record: record.case_id,
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
_validate_v2(checked, checked_config)
|
|
420
|
+
return EvaluationReportV2(
|
|
421
|
+
scientific=checked_config.mode == "final",
|
|
422
|
+
config_sha256=digest(checked_config),
|
|
423
|
+
protocol_sha256=evaluation_protocol_sha256_v2(checked_config),
|
|
424
|
+
records_sha256=_records_digest(checked),
|
|
425
|
+
config=checked_config,
|
|
426
|
+
records=checked,
|
|
427
|
+
strata=_strata_v2(checked, checked_config),
|
|
428
|
+
)
|