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,347 @@
|
|
|
1
|
+
"""Regenerate exact CSV tables from the bound evaluation, never from edited totals."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import hashlib
|
|
5
|
+
import io
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
from equiframe._path_compat import is_junction
|
|
10
|
+
from pydantic import Field, model_validator
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from equiframe_benchmark.evaluation import EvaluationReport, MetricValue
|
|
14
|
+
from equiframe_benchmark.io import load_document, save_document
|
|
15
|
+
from equiframe_benchmark.models import Digest, Document, Text, digest
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EvaluationTable(Document):
|
|
19
|
+
filename: Text
|
|
20
|
+
sha256: Digest
|
|
21
|
+
size_bytes: Annotated[int, Field(ge=0)]
|
|
22
|
+
rows: Annotated[int, Field(ge=0)]
|
|
23
|
+
|
|
24
|
+
@model_validator(mode="after")
|
|
25
|
+
def filename_only(self) -> Self:
|
|
26
|
+
if Path(self.filename).name != self.filename or not self.filename.endswith(".csv"):
|
|
27
|
+
raise ValueError("a table must have one local CSV filename")
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TableExport(Document):
|
|
32
|
+
evaluation_sha256: Digest
|
|
33
|
+
scientific: bool
|
|
34
|
+
tables: tuple[EvaluationTable, ...]
|
|
35
|
+
|
|
36
|
+
@model_validator(mode="after")
|
|
37
|
+
def unique(self) -> Self:
|
|
38
|
+
if len({t.filename for t in self.tables}) != len(self.tables):
|
|
39
|
+
raise ValueError("duplicate table filenames")
|
|
40
|
+
if tuple(t.filename for t in self.tables) != (
|
|
41
|
+
"coverage.csv",
|
|
42
|
+
"metrics.csv",
|
|
43
|
+
"average_precision.csv",
|
|
44
|
+
"incremental.csv",
|
|
45
|
+
"bootstrap.csv",
|
|
46
|
+
"cases.csv",
|
|
47
|
+
):
|
|
48
|
+
raise ValueError("the complete table inventory is required")
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _csv(header: tuple[str, ...], rows: list[list[object]]) -> bytes:
|
|
53
|
+
stream = io.StringIO(newline="")
|
|
54
|
+
writer = csv.writer(stream, lineterminator="\n")
|
|
55
|
+
writer.writerow(header)
|
|
56
|
+
writer.writerows(rows)
|
|
57
|
+
return stream.getvalue().encode("utf-8")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _value(metric: MetricValue) -> list[object]:
|
|
61
|
+
return [metric.numerator, metric.denominator, metric.value, metric.reason]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def table_payloads(report: EvaluationReport) -> dict[str, tuple[bytes, int]]:
|
|
65
|
+
"""Return deterministic bytes and data-row counts, including undefined/empty strata."""
|
|
66
|
+
return _validated_payloads(EvaluationReport.model_validate(report))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _validated_payloads(report: EvaluationReport) -> dict[str, tuple[bytes, int]]:
|
|
70
|
+
"""Format a report already revalidated at the caller's public boundary.
|
|
71
|
+
|
|
72
|
+
Keeping formatting separate avoids a second full deterministic bootstrap
|
|
73
|
+
during write/read, without caching or trusting a caller-supplied instance.
|
|
74
|
+
"""
|
|
75
|
+
coverage: list[list[object]] = []
|
|
76
|
+
metrics: list[list[object]] = []
|
|
77
|
+
ap: list[list[object]] = []
|
|
78
|
+
incremental: list[list[object]] = []
|
|
79
|
+
bootstrap: list[list[object]] = []
|
|
80
|
+
cases: list[list[object]] = []
|
|
81
|
+
scope_header = ("mode", "scope", "family", "migration")
|
|
82
|
+
ratio_header = ("numerator", "denominator", "value", "undefined_reason")
|
|
83
|
+
for stratum in report.strata:
|
|
84
|
+
scope: list[object] = [
|
|
85
|
+
report.config.mode,
|
|
86
|
+
stratum.kind,
|
|
87
|
+
stratum.family,
|
|
88
|
+
stratum.migration,
|
|
89
|
+
]
|
|
90
|
+
summary = stratum.coverage
|
|
91
|
+
coverage.append(
|
|
92
|
+
[
|
|
93
|
+
*scope,
|
|
94
|
+
summary.total_cases,
|
|
95
|
+
summary.oracle_confirmed,
|
|
96
|
+
summary.oracle_pending,
|
|
97
|
+
summary.oracle_disagreement,
|
|
98
|
+
summary.oracle_not_evaluable,
|
|
99
|
+
len(summary.failed_case_ids),
|
|
100
|
+
len(summary.principal_case_ids),
|
|
101
|
+
len(summary.excluded_case_ids),
|
|
102
|
+
*_value(summary.principal_coverage),
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
for method in stratum.methods:
|
|
106
|
+
for support_name, confusion in (
|
|
107
|
+
("principal", method.principal),
|
|
108
|
+
("own_support", method.own_support),
|
|
109
|
+
):
|
|
110
|
+
for metric_name in ("precision", "recall", "f1", "false_positive_rate"):
|
|
111
|
+
value = getattr(confusion, metric_name)
|
|
112
|
+
metrics.append(
|
|
113
|
+
[
|
|
114
|
+
*scope,
|
|
115
|
+
method.method,
|
|
116
|
+
support_name,
|
|
117
|
+
len(method.complete_case_ids),
|
|
118
|
+
len(method.own_case_ids),
|
|
119
|
+
len(method.partial_alert_case_ids),
|
|
120
|
+
len(method.unavailable_case_ids),
|
|
121
|
+
method.coverage.numerator,
|
|
122
|
+
method.coverage.denominator,
|
|
123
|
+
confusion.tp,
|
|
124
|
+
confusion.fp,
|
|
125
|
+
confusion.tn,
|
|
126
|
+
confusion.fn,
|
|
127
|
+
metric_name,
|
|
128
|
+
*_value(value),
|
|
129
|
+
]
|
|
130
|
+
)
|
|
131
|
+
for average in stratum.average_precision:
|
|
132
|
+
for support_name, value, ids in (
|
|
133
|
+
("principal", average.principal, average.principal_case_ids),
|
|
134
|
+
("own_support", average.own_support, average.own_case_ids),
|
|
135
|
+
):
|
|
136
|
+
ap.append([*scope, average.method, support_name, len(ids), *_value(value)])
|
|
137
|
+
gain = stratum.incremental
|
|
138
|
+
for name, ids, value in (
|
|
139
|
+
(
|
|
140
|
+
"additional_true_positives_H_vs_R",
|
|
141
|
+
gain.additional_true_positive_case_ids,
|
|
142
|
+
gain.additional_true_positive_rate,
|
|
143
|
+
),
|
|
144
|
+
(
|
|
145
|
+
"additional_false_positives_H_vs_R",
|
|
146
|
+
gain.additional_false_positive_case_ids,
|
|
147
|
+
gain.additional_false_positive_rate,
|
|
148
|
+
),
|
|
149
|
+
):
|
|
150
|
+
incremental.append([*scope, name, len(ids), *_value(value)])
|
|
151
|
+
interval = stratum.bootstrap
|
|
152
|
+
for estimate in interval.intervals:
|
|
153
|
+
bootstrap.append(
|
|
154
|
+
[
|
|
155
|
+
*scope,
|
|
156
|
+
estimate.metric,
|
|
157
|
+
len(interval.group_ids),
|
|
158
|
+
interval.attempted_replicates,
|
|
159
|
+
estimate.defined_replicates,
|
|
160
|
+
estimate.undefined_replicates,
|
|
161
|
+
report.config.bootstrap.seed,
|
|
162
|
+
report.config.bootstrap.confidence,
|
|
163
|
+
interval.lower_rank,
|
|
164
|
+
interval.upper_rank,
|
|
165
|
+
estimate.lower,
|
|
166
|
+
estimate.upper,
|
|
167
|
+
estimate.reason,
|
|
168
|
+
]
|
|
169
|
+
)
|
|
170
|
+
for record in report.records:
|
|
171
|
+
cases.append(
|
|
172
|
+
[
|
|
173
|
+
report.config.mode,
|
|
174
|
+
record.case_id,
|
|
175
|
+
record.scenario_id,
|
|
176
|
+
record.group_id,
|
|
177
|
+
record.family,
|
|
178
|
+
record.migration,
|
|
179
|
+
record.engine,
|
|
180
|
+
record.oracle_state,
|
|
181
|
+
record.oracle_label,
|
|
182
|
+
record.r.decision,
|
|
183
|
+
record.r.complete,
|
|
184
|
+
record.m.decision,
|
|
185
|
+
record.m.complete,
|
|
186
|
+
record.b.decision,
|
|
187
|
+
record.b.complete,
|
|
188
|
+
record.h.decision,
|
|
189
|
+
record.h.complete,
|
|
190
|
+
record.m_score,
|
|
191
|
+
record.b_score,
|
|
192
|
+
record.failure_reason,
|
|
193
|
+
]
|
|
194
|
+
)
|
|
195
|
+
definitions = (
|
|
196
|
+
(
|
|
197
|
+
"coverage.csv",
|
|
198
|
+
(
|
|
199
|
+
*scope_header,
|
|
200
|
+
"scheduled",
|
|
201
|
+
"confirmed",
|
|
202
|
+
"pending",
|
|
203
|
+
"disagreement",
|
|
204
|
+
"oracle_not_evaluable",
|
|
205
|
+
"failed",
|
|
206
|
+
"principal",
|
|
207
|
+
"excluded",
|
|
208
|
+
*ratio_header,
|
|
209
|
+
),
|
|
210
|
+
coverage,
|
|
211
|
+
),
|
|
212
|
+
(
|
|
213
|
+
"metrics.csv",
|
|
214
|
+
(
|
|
215
|
+
*scope_header,
|
|
216
|
+
"method",
|
|
217
|
+
"support",
|
|
218
|
+
"complete",
|
|
219
|
+
"own",
|
|
220
|
+
"partial_alert",
|
|
221
|
+
"not_evaluable",
|
|
222
|
+
"coverage_numerator",
|
|
223
|
+
"coverage_denominator",
|
|
224
|
+
"tp",
|
|
225
|
+
"fp",
|
|
226
|
+
"tn",
|
|
227
|
+
"fn",
|
|
228
|
+
"metric",
|
|
229
|
+
*ratio_header,
|
|
230
|
+
),
|
|
231
|
+
metrics,
|
|
232
|
+
),
|
|
233
|
+
(
|
|
234
|
+
"average_precision.csv",
|
|
235
|
+
(*scope_header, "method", "support", "cases", *ratio_header),
|
|
236
|
+
ap,
|
|
237
|
+
),
|
|
238
|
+
(
|
|
239
|
+
"incremental.csv",
|
|
240
|
+
(*scope_header, "statistic", "additional_cases", *ratio_header),
|
|
241
|
+
incremental,
|
|
242
|
+
),
|
|
243
|
+
(
|
|
244
|
+
"bootstrap.csv",
|
|
245
|
+
(
|
|
246
|
+
*scope_header,
|
|
247
|
+
"statistic",
|
|
248
|
+
"groups",
|
|
249
|
+
"attempted",
|
|
250
|
+
"defined",
|
|
251
|
+
"undefined",
|
|
252
|
+
"seed",
|
|
253
|
+
"confidence",
|
|
254
|
+
"lower_rank",
|
|
255
|
+
"upper_rank",
|
|
256
|
+
"lower",
|
|
257
|
+
"upper",
|
|
258
|
+
"reason",
|
|
259
|
+
),
|
|
260
|
+
bootstrap,
|
|
261
|
+
),
|
|
262
|
+
(
|
|
263
|
+
"cases.csv",
|
|
264
|
+
(
|
|
265
|
+
"mode",
|
|
266
|
+
"case_id",
|
|
267
|
+
"scenario_id",
|
|
268
|
+
"group_id",
|
|
269
|
+
"family",
|
|
270
|
+
"migration",
|
|
271
|
+
"engine",
|
|
272
|
+
"oracle_state",
|
|
273
|
+
"label",
|
|
274
|
+
"R",
|
|
275
|
+
"R_complete",
|
|
276
|
+
"M",
|
|
277
|
+
"M_complete",
|
|
278
|
+
"B",
|
|
279
|
+
"B_complete",
|
|
280
|
+
"H",
|
|
281
|
+
"H_complete",
|
|
282
|
+
"M_score",
|
|
283
|
+
"B_score",
|
|
284
|
+
"failure_reason",
|
|
285
|
+
),
|
|
286
|
+
cases,
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
return {name: (_csv(header, rows), len(rows)) for name, header, rows in definitions}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def write_tables(report: EvaluationReport, directory: Path) -> TableExport:
|
|
293
|
+
"""Publish in a new directory; an interruption cannot leave a valid final receipt."""
|
|
294
|
+
report = EvaluationReport.model_validate(report)
|
|
295
|
+
payloads = _validated_payloads(report)
|
|
296
|
+
directory = directory.resolve()
|
|
297
|
+
directory.mkdir(parents=True, exist_ok=False)
|
|
298
|
+
tables: list[EvaluationTable] = []
|
|
299
|
+
for filename, (data, rows) in payloads.items():
|
|
300
|
+
with (directory / filename).open("xb") as stream:
|
|
301
|
+
stream.write(data)
|
|
302
|
+
tables.append(
|
|
303
|
+
EvaluationTable(
|
|
304
|
+
filename=filename,
|
|
305
|
+
sha256=hashlib.sha256(data).hexdigest(),
|
|
306
|
+
size_bytes=len(data),
|
|
307
|
+
rows=rows,
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
result = TableExport(
|
|
311
|
+
evaluation_sha256=digest(report), scientific=report.scientific, tables=tuple(tables)
|
|
312
|
+
)
|
|
313
|
+
save_document(result, directory / "tables.json")
|
|
314
|
+
return result
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def read_tables(directory: Path, *, report: EvaluationReport) -> TableExport:
|
|
318
|
+
"""Verify every byte against regeneration and reject unlisted files or links."""
|
|
319
|
+
report = EvaluationReport.model_validate(report)
|
|
320
|
+
if directory.is_symlink() or is_junction(directory):
|
|
321
|
+
raise ValueError("the table directory cannot be a filesystem link")
|
|
322
|
+
directory = directory.resolve()
|
|
323
|
+
manifest = directory / "tables.json"
|
|
324
|
+
if manifest.is_symlink() or not manifest.is_file() or manifest.stat().st_size > 65536:
|
|
325
|
+
raise ValueError("table receipt must be a bounded regular file")
|
|
326
|
+
result = load_document(TableExport, manifest)
|
|
327
|
+
expected = _validated_payloads(report)
|
|
328
|
+
if result.evaluation_sha256 != digest(report) or result.scientific != report.scientific:
|
|
329
|
+
raise ValueError("tables belong to a different evaluation")
|
|
330
|
+
if {p.name for p in directory.iterdir()} != {*expected, "tables.json"}:
|
|
331
|
+
raise ValueError("table directory inventory is not closed")
|
|
332
|
+
for table in result.tables:
|
|
333
|
+
path = directory / table.filename
|
|
334
|
+
if path.is_symlink() or not path.is_file():
|
|
335
|
+
raise ValueError("tables must be regular local files")
|
|
336
|
+
payload, rows = expected[table.filename]
|
|
337
|
+
if path.stat().st_size != len(payload):
|
|
338
|
+
raise ValueError("table size differs from bound regeneration")
|
|
339
|
+
data = path.read_bytes()
|
|
340
|
+
if (
|
|
341
|
+
data != payload
|
|
342
|
+
or hashlib.sha256(data).hexdigest() != table.sha256
|
|
343
|
+
or len(data) != table.size_bytes
|
|
344
|
+
or rows != table.rows
|
|
345
|
+
):
|
|
346
|
+
raise ValueError("table bytes or row counts differ from bound regeneration")
|
|
347
|
+
return result
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Exact V2 paired tables, with exclusions, denominators and common-support appendix."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from equiframe._path_compat import is_junction
|
|
8
|
+
from pydantic import model_validator
|
|
9
|
+
from typing_extensions import Self
|
|
10
|
+
|
|
11
|
+
from equiframe_benchmark.evaluation_v2 import EvaluationReportV2, SignedMetricV2
|
|
12
|
+
from equiframe_benchmark.io import load_document, save_document
|
|
13
|
+
from equiframe_benchmark.models import Digest, digest
|
|
14
|
+
from equiframe_benchmark.models_v2 import DocumentV2 as Document
|
|
15
|
+
from equiframe_benchmark.tables import EvaluationTable, _csv
|
|
16
|
+
|
|
17
|
+
TABLES_V2 = (
|
|
18
|
+
"coverage.csv",
|
|
19
|
+
"metrics.csv",
|
|
20
|
+
"bootstrap.csv",
|
|
21
|
+
"exclusions.csv",
|
|
22
|
+
"common_metrics.csv",
|
|
23
|
+
"common_bootstrap.csv",
|
|
24
|
+
"cases.csv",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TableExportV2(Document):
|
|
29
|
+
schema_version: Literal["2.0"] = "2.0"
|
|
30
|
+
evaluation_sha256: Digest
|
|
31
|
+
scientific: bool
|
|
32
|
+
tables: tuple[EvaluationTable, ...]
|
|
33
|
+
|
|
34
|
+
@model_validator(mode="after")
|
|
35
|
+
def inventory(self) -> Self:
|
|
36
|
+
if tuple(table.filename for table in self.tables) != TABLES_V2:
|
|
37
|
+
raise ValueError("V2 requires its complete ordered seven-table inventory")
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _value(value: SignedMetricV2) -> list[object]:
|
|
42
|
+
return [value.numerator, value.denominator, value.value, value.reason]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _validated_payloads(report: EvaluationReportV2) -> dict[str, tuple[bytes, int]]:
|
|
46
|
+
rows: dict[str, list[list[object]]] = {name: [] for name in TABLES_V2}
|
|
47
|
+
scope_header = ("mode", "scope", "family", "migration")
|
|
48
|
+
pair_header = (*scope_header, "left_method", "right_method")
|
|
49
|
+
ratio = ("numerator", "denominator", "value", "undefined_reason")
|
|
50
|
+
for stratum in report.strata:
|
|
51
|
+
scope: list[object] = [
|
|
52
|
+
report.config.mode,
|
|
53
|
+
stratum.kind,
|
|
54
|
+
stratum.family,
|
|
55
|
+
stratum.migration,
|
|
56
|
+
]
|
|
57
|
+
for contrast in stratum.principal:
|
|
58
|
+
pair = contrast.comparison
|
|
59
|
+
identity = [*scope, pair.left_method, pair.right_method]
|
|
60
|
+
rows["coverage.csv"].append(
|
|
61
|
+
[
|
|
62
|
+
*identity,
|
|
63
|
+
stratum.total_cases,
|
|
64
|
+
stratum.oracle_confirmed,
|
|
65
|
+
len(pair.support_case_ids),
|
|
66
|
+
len(pair.group_ids),
|
|
67
|
+
pair.divergent_cases,
|
|
68
|
+
pair.normal_cases,
|
|
69
|
+
pair.coverage_confirmed.numerator,
|
|
70
|
+
pair.coverage_confirmed.denominator,
|
|
71
|
+
pair.coverage_confirmed.value,
|
|
72
|
+
pair.coverage_confirmed.reason,
|
|
73
|
+
pair.coverage_scheduled.numerator,
|
|
74
|
+
pair.coverage_scheduled.denominator,
|
|
75
|
+
pair.coverage_scheduled.value,
|
|
76
|
+
pair.coverage_scheduled.reason,
|
|
77
|
+
len(pair.exclusions),
|
|
78
|
+
len(pair.left_partial_alert_case_ids),
|
|
79
|
+
len(pair.right_partial_alert_case_ids),
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
for estimate in contrast.estimates:
|
|
83
|
+
rows["metrics.csv"].append(
|
|
84
|
+
[
|
|
85
|
+
*identity,
|
|
86
|
+
estimate.name,
|
|
87
|
+
*_value(estimate.estimate),
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
for interval in contrast.bootstrap.intervals:
|
|
91
|
+
rows["bootstrap.csv"].append(
|
|
92
|
+
[
|
|
93
|
+
*identity,
|
|
94
|
+
interval.metric,
|
|
95
|
+
len(contrast.bootstrap.group_ids),
|
|
96
|
+
contrast.bootstrap.attempted_replicates,
|
|
97
|
+
contrast.bootstrap.lower_rank,
|
|
98
|
+
contrast.bootstrap.upper_rank,
|
|
99
|
+
interval.lower,
|
|
100
|
+
interval.upper,
|
|
101
|
+
interval.defined_replicates,
|
|
102
|
+
interval.undefined_replicates,
|
|
103
|
+
interval.reason,
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
for exclusion in pair.exclusions:
|
|
107
|
+
for reason in exclusion.reasons:
|
|
108
|
+
rows["exclusions.csv"].append(
|
|
109
|
+
[
|
|
110
|
+
*identity,
|
|
111
|
+
exclusion.case_id,
|
|
112
|
+
exclusion.oracle_state,
|
|
113
|
+
reason,
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
for method in stratum.common.methods:
|
|
117
|
+
confusion = method.metrics
|
|
118
|
+
values = [
|
|
119
|
+
(metric, getattr(confusion, metric))
|
|
120
|
+
for metric in ("precision", "recall", "f1", "false_positive_rate")
|
|
121
|
+
]
|
|
122
|
+
if method.average_precision is not None:
|
|
123
|
+
values.append(("average_precision", method.average_precision))
|
|
124
|
+
for name, value in values:
|
|
125
|
+
rows["common_metrics.csv"].append(
|
|
126
|
+
[
|
|
127
|
+
*scope,
|
|
128
|
+
stratum.common.role,
|
|
129
|
+
len(stratum.common.case_ids),
|
|
130
|
+
method.method,
|
|
131
|
+
confusion.tp,
|
|
132
|
+
confusion.fp,
|
|
133
|
+
confusion.fn,
|
|
134
|
+
confusion.tn,
|
|
135
|
+
name,
|
|
136
|
+
value.numerator,
|
|
137
|
+
value.denominator,
|
|
138
|
+
value.value,
|
|
139
|
+
value.reason,
|
|
140
|
+
]
|
|
141
|
+
)
|
|
142
|
+
for interval in stratum.common.bootstrap.intervals:
|
|
143
|
+
bootstrap = stratum.common.bootstrap
|
|
144
|
+
rows["common_bootstrap.csv"].append(
|
|
145
|
+
[
|
|
146
|
+
*scope,
|
|
147
|
+
stratum.common.role,
|
|
148
|
+
interval.metric,
|
|
149
|
+
len(bootstrap.group_ids),
|
|
150
|
+
bootstrap.attempted_replicates,
|
|
151
|
+
bootstrap.lower_rank,
|
|
152
|
+
bootstrap.upper_rank,
|
|
153
|
+
interval.lower,
|
|
154
|
+
interval.upper,
|
|
155
|
+
interval.defined_replicates,
|
|
156
|
+
interval.undefined_replicates,
|
|
157
|
+
interval.reason,
|
|
158
|
+
]
|
|
159
|
+
)
|
|
160
|
+
for record in report.records:
|
|
161
|
+
decisions: list[object] = []
|
|
162
|
+
for component in (record.r, record.m, record.b, record.h):
|
|
163
|
+
decisions.extend((component.decision, component.complete, component.reason))
|
|
164
|
+
rows["cases.csv"].append(
|
|
165
|
+
[
|
|
166
|
+
record.case_id,
|
|
167
|
+
record.scenario_id,
|
|
168
|
+
record.group_id,
|
|
169
|
+
record.family,
|
|
170
|
+
record.migration,
|
|
171
|
+
record.engine,
|
|
172
|
+
record.oracle_state,
|
|
173
|
+
record.oracle_label,
|
|
174
|
+
*decisions,
|
|
175
|
+
record.m_score,
|
|
176
|
+
record.b_score,
|
|
177
|
+
record.candidate_sha256,
|
|
178
|
+
record.pipeline_sha256,
|
|
179
|
+
record.oracle_sha256,
|
|
180
|
+
record.detector_bundle_sha256,
|
|
181
|
+
record.freeze_sha256,
|
|
182
|
+
record.failure_reason,
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
headers = {
|
|
186
|
+
"coverage.csv": (
|
|
187
|
+
*pair_header,
|
|
188
|
+
"scheduled",
|
|
189
|
+
"confirmed",
|
|
190
|
+
"paired",
|
|
191
|
+
"groups",
|
|
192
|
+
"divergent",
|
|
193
|
+
"normal",
|
|
194
|
+
"confirmed_numerator",
|
|
195
|
+
"confirmed_denominator",
|
|
196
|
+
"confirmed_coverage",
|
|
197
|
+
"confirmed_undefined",
|
|
198
|
+
"scheduled_numerator",
|
|
199
|
+
"scheduled_denominator",
|
|
200
|
+
"scheduled_coverage",
|
|
201
|
+
"scheduled_undefined",
|
|
202
|
+
"excluded",
|
|
203
|
+
"left_partial_alerts",
|
|
204
|
+
"right_partial_alerts",
|
|
205
|
+
),
|
|
206
|
+
"metrics.csv": (*pair_header, "metric", *ratio),
|
|
207
|
+
"bootstrap.csv": (
|
|
208
|
+
*pair_header,
|
|
209
|
+
"metric",
|
|
210
|
+
"groups",
|
|
211
|
+
"attempted",
|
|
212
|
+
"lower_rank",
|
|
213
|
+
"upper_rank",
|
|
214
|
+
"lower",
|
|
215
|
+
"upper",
|
|
216
|
+
"defined",
|
|
217
|
+
"undefined",
|
|
218
|
+
"reason",
|
|
219
|
+
),
|
|
220
|
+
"exclusions.csv": (*pair_header, "case_id", "oracle_state", "reason"),
|
|
221
|
+
"common_metrics.csv": (
|
|
222
|
+
*scope_header,
|
|
223
|
+
"role",
|
|
224
|
+
"common_support",
|
|
225
|
+
"method",
|
|
226
|
+
"tp",
|
|
227
|
+
"fp",
|
|
228
|
+
"fn",
|
|
229
|
+
"tn",
|
|
230
|
+
"metric",
|
|
231
|
+
*ratio,
|
|
232
|
+
),
|
|
233
|
+
"common_bootstrap.csv": (
|
|
234
|
+
*scope_header,
|
|
235
|
+
"role",
|
|
236
|
+
"metric",
|
|
237
|
+
"groups",
|
|
238
|
+
"attempted",
|
|
239
|
+
"lower_rank",
|
|
240
|
+
"upper_rank",
|
|
241
|
+
"lower",
|
|
242
|
+
"upper",
|
|
243
|
+
"defined",
|
|
244
|
+
"undefined",
|
|
245
|
+
"reason",
|
|
246
|
+
),
|
|
247
|
+
"cases.csv": (
|
|
248
|
+
"case_id",
|
|
249
|
+
"scenario_id",
|
|
250
|
+
"group_id",
|
|
251
|
+
"family",
|
|
252
|
+
"migration",
|
|
253
|
+
"engine",
|
|
254
|
+
"oracle_state",
|
|
255
|
+
"oracle_label",
|
|
256
|
+
"R",
|
|
257
|
+
"R_complete",
|
|
258
|
+
"R_reason",
|
|
259
|
+
"M",
|
|
260
|
+
"M_complete",
|
|
261
|
+
"M_reason",
|
|
262
|
+
"B",
|
|
263
|
+
"B_complete",
|
|
264
|
+
"B_reason",
|
|
265
|
+
"H",
|
|
266
|
+
"H_complete",
|
|
267
|
+
"H_reason",
|
|
268
|
+
"M_score",
|
|
269
|
+
"B_score",
|
|
270
|
+
"candidate_sha256",
|
|
271
|
+
"pipeline_sha256",
|
|
272
|
+
"oracle_sha256",
|
|
273
|
+
"bundle_sha256",
|
|
274
|
+
"freeze_sha256",
|
|
275
|
+
"failure_reason",
|
|
276
|
+
),
|
|
277
|
+
}
|
|
278
|
+
return {name: (_csv(headers[name], rows[name]), len(rows[name])) for name in TABLES_V2}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def table_payloads_v2(report: EvaluationReportV2) -> dict[str, tuple[bytes, int]]:
|
|
282
|
+
return _validated_payloads(EvaluationReportV2.model_validate(report))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def write_tables_v2(report: EvaluationReportV2, directory: Path) -> TableExportV2:
|
|
286
|
+
checked = EvaluationReportV2.model_validate(report)
|
|
287
|
+
payloads = _validated_payloads(checked)
|
|
288
|
+
directory = directory.resolve()
|
|
289
|
+
directory.mkdir(parents=True, exist_ok=False)
|
|
290
|
+
tables = []
|
|
291
|
+
for name, (payload, rows) in payloads.items():
|
|
292
|
+
with (directory / name).open("xb") as output:
|
|
293
|
+
output.write(payload)
|
|
294
|
+
tables.append(
|
|
295
|
+
EvaluationTable(
|
|
296
|
+
filename=name,
|
|
297
|
+
sha256=hashlib.sha256(payload).hexdigest(),
|
|
298
|
+
size_bytes=len(payload),
|
|
299
|
+
rows=rows,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
receipt = TableExportV2(
|
|
303
|
+
evaluation_sha256=digest(checked),
|
|
304
|
+
scientific=checked.scientific,
|
|
305
|
+
tables=tuple(tables),
|
|
306
|
+
)
|
|
307
|
+
save_document(receipt, directory / "tables.json")
|
|
308
|
+
return receipt
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def read_tables_v2(directory: Path, *, report: EvaluationReportV2) -> TableExportV2:
|
|
312
|
+
checked = EvaluationReportV2.model_validate(report)
|
|
313
|
+
if directory.is_symlink() or is_junction(directory):
|
|
314
|
+
raise ValueError("V2 table directory must not be a filesystem link")
|
|
315
|
+
directory = directory.resolve()
|
|
316
|
+
path = directory / "tables.json"
|
|
317
|
+
if path.is_symlink() or not path.is_file() or path.stat().st_size > 65536:
|
|
318
|
+
raise ValueError("V2 table receipt must be a bounded regular file")
|
|
319
|
+
receipt = load_document(TableExportV2, path)
|
|
320
|
+
expected = _validated_payloads(checked)
|
|
321
|
+
if receipt.evaluation_sha256 != digest(checked) or receipt.scientific != checked.scientific:
|
|
322
|
+
raise ValueError("V2 tables belong to a different paired evaluation")
|
|
323
|
+
if {entry.name for entry in directory.iterdir()} != {*expected, "tables.json"}:
|
|
324
|
+
raise ValueError("V2 table inventory must be closed")
|
|
325
|
+
for table in receipt.tables:
|
|
326
|
+
target = directory / table.filename
|
|
327
|
+
if target.is_symlink() or is_junction(target) or not target.is_file():
|
|
328
|
+
raise ValueError("V2 tables must be regular local files")
|
|
329
|
+
payload, rows = expected[table.filename]
|
|
330
|
+
if target.stat().st_size != len(payload):
|
|
331
|
+
raise ValueError("V2 table size differs from regeneration")
|
|
332
|
+
data = target.read_bytes()
|
|
333
|
+
if (
|
|
334
|
+
data != payload
|
|
335
|
+
or hashlib.sha256(data).hexdigest() != table.sha256
|
|
336
|
+
or len(data) != table.size_bytes
|
|
337
|
+
or rows != table.rows
|
|
338
|
+
):
|
|
339
|
+
raise ValueError("V2 table bytes or counts differ from captured decisions")
|
|
340
|
+
return receipt
|