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.
Files changed (62) hide show
  1. equiframe_benchmark/PROTOCOL_V2.md +172 -0
  2. equiframe_benchmark/__init__.py +284 -0
  3. equiframe_benchmark/__main__.py +3 -0
  4. equiframe_benchmark/_scale_workload.py +375 -0
  5. equiframe_benchmark/calibration_types.py +112 -0
  6. equiframe_benchmark/cli.py +196 -0
  7. equiframe_benchmark/commands.py +149 -0
  8. equiframe_benchmark/commands_scale.py +38 -0
  9. equiframe_benchmark/commands_step10.py +275 -0
  10. equiframe_benchmark/commands_v2.py +206 -0
  11. equiframe_benchmark/cost_table.py +62 -0
  12. equiframe_benchmark/costs.py +788 -0
  13. equiframe_benchmark/distribution.py +932 -0
  14. equiframe_benchmark/evaluation.py +614 -0
  15. equiframe_benchmark/evaluation_v2.py +428 -0
  16. equiframe_benchmark/fixtures.py +299 -0
  17. equiframe_benchmark/freeze.py +869 -0
  18. equiframe_benchmark/freeze_v2.py +412 -0
  19. equiframe_benchmark/integration.py +655 -0
  20. equiframe_benchmark/io.py +29 -0
  21. equiframe_benchmark/models.py +313 -0
  22. equiframe_benchmark/models_v2.py +13 -0
  23. equiframe_benchmark/mutations.py +34 -0
  24. equiframe_benchmark/oracle.py +264 -0
  25. equiframe_benchmark/pairwise.py +248 -0
  26. equiframe_benchmark/pilot.py +254 -0
  27. equiframe_benchmark/py.typed +0 -0
  28. equiframe_benchmark/reconciliation.py +294 -0
  29. equiframe_benchmark/reproduction.py +542 -0
  30. equiframe_benchmark/scale_io.py +19 -0
  31. equiframe_benchmark/scale_process.py +1077 -0
  32. equiframe_benchmark/scale_types.py +120 -0
  33. equiframe_benchmark/scaling.py +529 -0
  34. equiframe_benchmark/schemas/protocol-v2/CommonSupportV2-2.0.schema.json +327 -0
  35. equiframe_benchmark/schemas/protocol-v2/ContrastSpecV2-2.0.schema.json +37 -0
  36. equiframe_benchmark/schemas/protocol-v2/EvaluationConfigV2-2.0.schema.json +184 -0
  37. equiframe_benchmark/schemas/protocol-v2/EvaluationReportV2-2.0.schema.json +1237 -0
  38. equiframe_benchmark/schemas/protocol-v2/EvaluationStratumV2-2.0.schema.json +747 -0
  39. equiframe_benchmark/schemas/protocol-v2/FreezeDefinitionV2-2.0.schema.json +5951 -0
  40. equiframe_benchmark/schemas/protocol-v2/FreezeManifestV2-2.0.schema.json +6222 -0
  41. equiframe_benchmark/schemas/protocol-v2/PrincipalContrastV2-2.0.schema.json +578 -0
  42. equiframe_benchmark/schemas/protocol-v2/ProtocolReviewV2-2.0.schema.json +56 -0
  43. equiframe_benchmark/schemas/protocol-v2/SignedMetricV2-2.0.schema.json +51 -0
  44. equiframe_benchmark/schemas/protocol-v2/StudyCaseResultV2-2.0.schema.json +4881 -0
  45. equiframe_benchmark/schemas/protocol-v2/StudyResultV2-2.0.schema.json +8370 -0
  46. equiframe_benchmark/schemas/protocol-v2/TableExportV2-2.0.schema.json +75 -0
  47. equiframe_benchmark/selection.py +178 -0
  48. equiframe_benchmark/snapshot.py +24 -0
  49. equiframe_benchmark/splits.py +92 -0
  50. equiframe_benchmark/study.py +526 -0
  51. equiframe_benchmark/study_bundle.py +379 -0
  52. equiframe_benchmark/study_bundle_v2.py +388 -0
  53. equiframe_benchmark/study_v2.py +544 -0
  54. equiframe_benchmark/tables.py +347 -0
  55. equiframe_benchmark/tables_v2.py +340 -0
  56. equiframe_benchmark/training_types.py +52 -0
  57. equiframe_benchmark-0.12.4.dist-info/METADATA +343 -0
  58. equiframe_benchmark-0.12.4.dist-info/RECORD +62 -0
  59. equiframe_benchmark-0.12.4.dist-info/WHEEL +4 -0
  60. equiframe_benchmark-0.12.4.dist-info/entry_points.txt +3 -0
  61. equiframe_benchmark-0.12.4.dist-info/licenses/LICENSE +21 -0
  62. 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
+ )