rtsa 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rtsa/__init__.py +26 -0
- rtsa/__main__.py +5 -0
- rtsa/analysis/__init__.py +59 -0
- rtsa/analysis/benchmark.py +358 -0
- rtsa/analysis/fingerprint.py +325 -0
- rtsa/analysis/performance_correlation.py +382 -0
- rtsa/analysis/prune.py +657 -0
- rtsa/analysis/signal_adapters.py +146 -0
- rtsa/analysis/step_classifier.py +274 -0
- rtsa/analysis/step_clustering.py +193 -0
- rtsa/cli.py +434 -0
- rtsa/config/__init__.py +34 -0
- rtsa/core/__init__.py +31 -0
- rtsa/core/metrics.py +525 -0
- rtsa/core/motif_matcher.py +586 -0
- rtsa/core/ngs_validator.py +628 -0
- rtsa/core/robust_tsi.py +488 -0
- rtsa/core/types.py +443 -0
- rtsa/demo.py +99 -0
- rtsa/experiments/__init__.py +1 -0
- rtsa/experiments/annotate_steps.py +105 -0
- rtsa/experiments/calibrate_thresholds.py +331 -0
- rtsa/experiments/correlation_analysis.py +207 -0
- rtsa/experiments/data_analysis.py +242 -0
- rtsa/experiments/end_to_end_prune.py +391 -0
- rtsa/experiments/pilot.py +354 -0
- rtsa/experiments/run.py +456 -0
- rtsa/experiments/synthetic_redundant_cots.py +221 -0
- rtsa/extractors/__init__.py +30 -0
- rtsa/extractors/agreement.py +634 -0
- rtsa/extractors/analysis.py +139 -0
- rtsa/extractors/baselines.py +221 -0
- rtsa/extractors/experiments.py +264 -0
- rtsa/extractors/gcp_validator.py +498 -0
- rtsa/extractors/inter_annotator.py +134 -0
- rtsa/extractors/llm_extractor.py +380 -0
- rtsa/extractors/random_baseline.py +121 -0
- rtsa/extractors/rule_based.py +578 -0
- rtsa/extractors/syntax_based.py +448 -0
- rtsa/extractors/synthetic_validation.py +198 -0
- rtsa/utils/__init__.py +15 -0
- rtsa/utils/data_loader.py +287 -0
- rtsa/utils/gsm8k_loader.py +151 -0
- rtsa/utils/hf_adapter.py +260 -0
- rtsa/utils/math_loader.py +257 -0
- rtsa/utils/trace_exporters.py +197 -0
- rtsa/utils/visualize.py +95 -0
- rtsa-0.1.0.dist-info/METADATA +220 -0
- rtsa-0.1.0.dist-info/RECORD +53 -0
- rtsa-0.1.0.dist-info/WHEEL +5 -0
- rtsa-0.1.0.dist-info/entry_points.txt +2 -0
- rtsa-0.1.0.dist-info/licenses/LICENSE +21 -0
- rtsa-0.1.0.dist-info/top_level.txt +1 -0
rtsa/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RTSA: Reasoning Trace Structure Analysis Toolkit.
|
|
3
|
+
|
|
4
|
+
Quick start:
|
|
5
|
+
from rtsa.extractors import create_extractor_deepseek
|
|
6
|
+
ext = create_extractor_deepseek()
|
|
7
|
+
graph = ext.extract("Calculate 5+3=8. Check the result.")
|
|
8
|
+
print(graph.nodes)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
# Convenience re-exports when running from the project directory
|
|
14
|
+
from rtsa.core.types import ReasoningTraceGraph, GraphNode, NodeType, MotifEntry
|
|
15
|
+
from rtsa.core.metrics import compute_graph_features, compute_tsi, compute_pairwise_tsi
|
|
16
|
+
from rtsa.core.motif_matcher import MotifMatcher
|
|
17
|
+
from rtsa.core.ngs_validator import NGSValidator
|
|
18
|
+
from rtsa.core.robust_tsi import UnsupervisedTSI
|
|
19
|
+
from rtsa.extractors import create_extractor_deepseek, RuleBasedExtractor, SyntaxBasedExtractor
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ReasoningTraceGraph", "GraphNode", "NodeType", "MotifEntry",
|
|
23
|
+
"compute_graph_features", "compute_tsi", "compute_pairwise_tsi",
|
|
24
|
+
"MotifMatcher", "NGSValidator", "UnsupervisedTSI",
|
|
25
|
+
"create_extractor_deepseek", "RuleBasedExtractor", "SyntaxBasedExtractor",
|
|
26
|
+
]
|
rtsa/__main__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""RTSA analysis layer — value-added modules built on core graph primitives.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- prune : redundancy detection & CoT optimization
|
|
5
|
+
- fingerprint: LLM authorship attribution via graph structure
|
|
6
|
+
- benchmark : extractor reliability benchmarking (GCP + NGS + TSI)
|
|
7
|
+
"""
|
|
8
|
+
from rtsa.extractors.analysis import (
|
|
9
|
+
CostEstimator, CostBreakdown, estimate_project_cost,
|
|
10
|
+
kruskal_wallis_test, bootstrap_ci, cohens_d, partial_correlation,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .prune import (
|
|
14
|
+
PruneConfig,
|
|
15
|
+
RedundancyRegion,
|
|
16
|
+
PruningReport,
|
|
17
|
+
RedundancyAnalyzer,
|
|
18
|
+
prune_graph,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .fingerprint import (
|
|
22
|
+
ModelSignature,
|
|
23
|
+
FingerprintMatchResult,
|
|
24
|
+
ModelFingerprint,
|
|
25
|
+
enroll_model,
|
|
26
|
+
identify_author,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .benchmark import (
|
|
30
|
+
ExtractorScore,
|
|
31
|
+
ExtractorBenchmarkResult,
|
|
32
|
+
BenchmarkReport,
|
|
33
|
+
ExtractorBenchmark,
|
|
34
|
+
benchmark_extractors,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Re-exports from rtsa.extractors.analysis (legacy)
|
|
39
|
+
"CostEstimator", "CostBreakdown", "estimate_project_cost",
|
|
40
|
+
"kruskal_wallis_test", "bootstrap_ci", "cohens_d", "partial_correlation",
|
|
41
|
+
# Prune module
|
|
42
|
+
"PruneConfig",
|
|
43
|
+
"RedundancyRegion",
|
|
44
|
+
"PruningReport",
|
|
45
|
+
"RedundancyAnalyzer",
|
|
46
|
+
"prune_graph",
|
|
47
|
+
# Fingerprint module
|
|
48
|
+
"ModelSignature",
|
|
49
|
+
"FingerprintMatchResult",
|
|
50
|
+
"ModelFingerprint",
|
|
51
|
+
"enroll_model",
|
|
52
|
+
"identify_author",
|
|
53
|
+
# Benchmark module
|
|
54
|
+
"ExtractorScore",
|
|
55
|
+
"ExtractorBenchmarkResult",
|
|
56
|
+
"BenchmarkReport",
|
|
57
|
+
"ExtractorBenchmark",
|
|
58
|
+
"benchmark_extractors",
|
|
59
|
+
]
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extractor Reliability Benchmark — Direction A (unique differentiator).
|
|
3
|
+
|
|
4
|
+
Provides a unified benchmarking suite that calibrates and compares
|
|
5
|
+
reasoning-trace extractors across three dimensions:
|
|
6
|
+
|
|
7
|
+
1. Granularity Calibration Protocol (GCP) — precision at sentence level
|
|
8
|
+
2. NGS Structural Compliance — validity of produced graphs
|
|
9
|
+
3. Cross-Extractor Consistency — Topological Similarity Index (TSI)
|
|
10
|
+
|
|
11
|
+
This is the feature ReasoningFlow will never build (they don't evaluate
|
|
12
|
+
themselves), making it the ideal flagship command for RTSA-as-library.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from rtsa.core.types import NodeType, ReasoningTraceGraph
|
|
25
|
+
from rtsa.core.metrics import compute_tsi
|
|
26
|
+
from rtsa.core.ngs_validator import NGSValidator, NGSViolation
|
|
27
|
+
from rtsa.extractors.gcp_validator import GCPValidator, GCPResult, make_gcp_adapter
|
|
28
|
+
from rtsa.extractors.inter_annotator import InterAnnotatorAgreement
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Data structures
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ExtractorScore:
|
|
38
|
+
"""Scores for a single extractor on a single benchmark dimension."""
|
|
39
|
+
dimension: str
|
|
40
|
+
score: float
|
|
41
|
+
passed: bool
|
|
42
|
+
details: Dict = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ExtractorBenchmarkResult:
|
|
47
|
+
"""Complete benchmark result for one extractor."""
|
|
48
|
+
extractor_name: str
|
|
49
|
+
n_samples: int
|
|
50
|
+
gcp: Optional[GCPResult] = None
|
|
51
|
+
ngs_pass_rate: float = 0.0
|
|
52
|
+
ngs_violations: List[NGSViolation] = field(default_factory=list)
|
|
53
|
+
mean_tsi_vs_others: float = 0.0
|
|
54
|
+
overall_score: float = 0.0
|
|
55
|
+
passed: bool = False
|
|
56
|
+
plan_redundancy_score: float = 0.0 # reasonplan insight: wasteful planning structure
|
|
57
|
+
|
|
58
|
+
def summary(self) -> str:
|
|
59
|
+
lines = [
|
|
60
|
+
f"BenchmarkResult: {self.extractor_name}",
|
|
61
|
+
f" Samples: {self.n_samples}",
|
|
62
|
+
]
|
|
63
|
+
if self.gcp:
|
|
64
|
+
lines.append(
|
|
65
|
+
f" GCP: mean={self.gcp.mean_gcs:.3f} min={self.gcp.min_gcs:.3f} "
|
|
66
|
+
f"passed={self.gcp.passed}"
|
|
67
|
+
)
|
|
68
|
+
lines.append(f" NGS pass rate: {self.ngs_pass_rate:.1%}")
|
|
69
|
+
lines.append(f" Mean TSI vs peers: {self.mean_tsi_vs_others:.3f}")
|
|
70
|
+
lines.append(f" Overall score: {self.overall_score:.3f} passed={self.passed}")
|
|
71
|
+
if self.plan_redundancy_score > 0.05:
|
|
72
|
+
lines.append(
|
|
73
|
+
f" Plan redundancy: {self.plan_redundancy_score:.3f} "
|
|
74
|
+
f"(reasonplan insight: excessive planning structure hurts efficiency)"
|
|
75
|
+
)
|
|
76
|
+
return "\n".join(lines)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class BenchmarkReport:
|
|
81
|
+
"""Aggregate report across all extractors."""
|
|
82
|
+
results: Dict[str, ExtractorBenchmarkResult]
|
|
83
|
+
winner: Optional[str] = None
|
|
84
|
+
ranking: List[Tuple[str, float]] = field(default_factory=list)
|
|
85
|
+
|
|
86
|
+
def to_json(self, path: str) -> None:
|
|
87
|
+
serializable = {
|
|
88
|
+
"winner": self.winner,
|
|
89
|
+
"ranking": self.ranking,
|
|
90
|
+
"extractors": {
|
|
91
|
+
name: {
|
|
92
|
+
"n_samples": r.n_samples,
|
|
93
|
+
"gcp_mean": r.gcp.mean_gcs if r.gcp else None,
|
|
94
|
+
"gcp_passed": r.gcp.passed if r.gcp else None,
|
|
95
|
+
"ngs_pass_rate": r.ngs_pass_rate,
|
|
96
|
+
"mean_tsi": r.mean_tsi_vs_others,
|
|
97
|
+
"overall_score": r.overall_score,
|
|
98
|
+
"passed": r.passed,
|
|
99
|
+
}
|
|
100
|
+
for name, r in self.results.items()
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
104
|
+
json.dump(serializable, f, indent=2, ensure_ascii=False)
|
|
105
|
+
logger.info(f"Benchmark report saved to {path}")
|
|
106
|
+
|
|
107
|
+
def summary(self) -> str:
|
|
108
|
+
lines = [
|
|
109
|
+
"=" * 60,
|
|
110
|
+
"RTSA Extractor Reliability Benchmark Report",
|
|
111
|
+
"=" * 60,
|
|
112
|
+
]
|
|
113
|
+
for rank, (name, score) in enumerate(self.ranking, 1):
|
|
114
|
+
marker = " ★" if name == self.winner else ""
|
|
115
|
+
lines.append(f"#{rank} {name}: {score:.3f}{marker}")
|
|
116
|
+
lines.append("=" * 60)
|
|
117
|
+
return "\n".join(lines)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Benchmark engine
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
class ExtractorBenchmark:
|
|
125
|
+
"""Run the full RTSA extractor reliability benchmark.
|
|
126
|
+
|
|
127
|
+
Usage:
|
|
128
|
+
bench = ExtractorBenchmark()
|
|
129
|
+
report = bench.run({
|
|
130
|
+
"rbe": rule_based_extractor,
|
|
131
|
+
"sbe": syntax_based_extractor,
|
|
132
|
+
"llm": llm_extractor,
|
|
133
|
+
}, graphs={...})
|
|
134
|
+
print(report.summary())
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(
|
|
138
|
+
self,
|
|
139
|
+
gcp_validator: Optional[GCPValidator] = None,
|
|
140
|
+
ngs_validator: Optional[NGSValidator] = None,
|
|
141
|
+
):
|
|
142
|
+
self.gcp = gcp_validator or GCPValidator()
|
|
143
|
+
self.ngs = ngs_validator or NGSValidator(strict=True)
|
|
144
|
+
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
# Main entry point
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def run(
|
|
150
|
+
self,
|
|
151
|
+
extractors: Dict[str, Callable[[str], ReasoningTraceGraph]],
|
|
152
|
+
sample_texts: Optional[List[str]] = None,
|
|
153
|
+
graphs: Optional[Dict[str, List[ReasoningTraceGraph]]] = None,
|
|
154
|
+
run_gcp: bool = True,
|
|
155
|
+
run_ngs: bool = True,
|
|
156
|
+
run_tsi: bool = True,
|
|
157
|
+
) -> BenchmarkReport:
|
|
158
|
+
"""Run benchmark suite.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
extractors: mapping name → extractor function
|
|
162
|
+
sample_texts: texts for GCP calibration (sentence-level)
|
|
163
|
+
graphs: pre-extracted graphs per extractor for NGS/TSI
|
|
164
|
+
run_gcp: whether to run Granularity Calibration Protocol
|
|
165
|
+
run_ngs: whether to run NGS structural validation
|
|
166
|
+
run_tsi: whether to compute cross-extractor TSI
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
BenchmarkReport with aggregated scores and ranking
|
|
170
|
+
"""
|
|
171
|
+
results: Dict[str, ExtractorBenchmarkResult] = {}
|
|
172
|
+
|
|
173
|
+
for name, extractor in extractors.items():
|
|
174
|
+
logger.info(f"Benchmarking extractor: {name}")
|
|
175
|
+
result = ExtractorBenchmarkResult(extractor_name=name, n_samples=0)
|
|
176
|
+
|
|
177
|
+
# --- GCP (sentence-level calibration) ---
|
|
178
|
+
if run_gcp and sample_texts:
|
|
179
|
+
# Wrap extractor for GCP (sentence → List[NodeType])
|
|
180
|
+
def sentence_adapter(sentence: str) -> List[NodeType]:
|
|
181
|
+
# Try to extract a mini-graph from a single sentence
|
|
182
|
+
# and return its node-type sequence
|
|
183
|
+
try:
|
|
184
|
+
mini_graph = extractor(sentence)
|
|
185
|
+
return [n.type for n in sorted(mini_graph.nodes, key=lambda n: n.id)]
|
|
186
|
+
except Exception:
|
|
187
|
+
return []
|
|
188
|
+
|
|
189
|
+
gcp_result = self.gcp.calibrate_extractor(
|
|
190
|
+
make_gcp_adapter(sentence_adapter),
|
|
191
|
+
extractor_name=name,
|
|
192
|
+
)
|
|
193
|
+
result.gcp = gcp_result
|
|
194
|
+
|
|
195
|
+
# --- NGS (graph-level structural compliance) ---
|
|
196
|
+
if run_ngs and graphs and name in graphs:
|
|
197
|
+
extractor_graphs = graphs[name]
|
|
198
|
+
result.n_samples = len(extractor_graphs)
|
|
199
|
+
valid_count = 0
|
|
200
|
+
all_violations: List[NGSViolation] = []
|
|
201
|
+
for g in extractor_graphs:
|
|
202
|
+
is_valid, violations = self.ngs.validate(g)
|
|
203
|
+
if is_valid:
|
|
204
|
+
valid_count += 1
|
|
205
|
+
all_violations.extend(violations)
|
|
206
|
+
result.ngs_pass_rate = valid_count / max(len(extractor_graphs), 1)
|
|
207
|
+
result.ngs_violations = all_violations
|
|
208
|
+
result.plan_redundancy_score = self._compute_plan_redundancy(extractor_graphs)
|
|
209
|
+
|
|
210
|
+
results[name] = result
|
|
211
|
+
|
|
212
|
+
# --- TSI (cross-extractor consistency) ---
|
|
213
|
+
if run_tsi and graphs:
|
|
214
|
+
self._compute_tsi_matrix(results, graphs)
|
|
215
|
+
|
|
216
|
+
# --- Aggregate scoring ---
|
|
217
|
+
self._compute_overall_scores(results)
|
|
218
|
+
|
|
219
|
+
# --- Ranking ---
|
|
220
|
+
ranking = sorted(
|
|
221
|
+
((name, r.overall_score) for name, r in results.items()),
|
|
222
|
+
key=lambda x: x[1],
|
|
223
|
+
reverse=True,
|
|
224
|
+
)
|
|
225
|
+
winner = ranking[0][0] if ranking else None
|
|
226
|
+
|
|
227
|
+
report = BenchmarkReport(results=results, winner=winner, ranking=ranking)
|
|
228
|
+
logger.info(report.summary())
|
|
229
|
+
return report
|
|
230
|
+
|
|
231
|
+
# ------------------------------------------------------------------
|
|
232
|
+
# TSI computation
|
|
233
|
+
# ------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def _compute_tsi_matrix(
|
|
236
|
+
self,
|
|
237
|
+
results: Dict[str, ExtractorBenchmarkResult],
|
|
238
|
+
graphs: Dict[str, List[ReasoningTraceGraph]],
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Compute pairwise mean TSI between extractors."""
|
|
241
|
+
names = list(results.keys())
|
|
242
|
+
if len(names) < 2:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
# Build per-extractor graph lists (must be same-length for fair comparison)
|
|
246
|
+
lens = [len(graphs[n]) for n in names if n in graphs]
|
|
247
|
+
if not lens:
|
|
248
|
+
return
|
|
249
|
+
min_len = min(lens)
|
|
250
|
+
if min_len == 0:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
for name in names:
|
|
254
|
+
if name not in graphs:
|
|
255
|
+
continue
|
|
256
|
+
my_graphs = graphs[name][:min_len]
|
|
257
|
+
tsi_scores: List[float] = []
|
|
258
|
+
for other in names:
|
|
259
|
+
if other == name or other not in graphs:
|
|
260
|
+
continue
|
|
261
|
+
other_graphs = graphs[other][:min_len]
|
|
262
|
+
for g1, g2 in zip(my_graphs, other_graphs):
|
|
263
|
+
try:
|
|
264
|
+
tsi = compute_tsi(g1.to_networkx(), g2.to_networkx())
|
|
265
|
+
tsi_scores.append(tsi.tsi_value)
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
if tsi_scores:
|
|
269
|
+
results[name].mean_tsi_vs_others = float(np.mean(tsi_scores))
|
|
270
|
+
|
|
271
|
+
# ------------------------------------------------------------------
|
|
272
|
+
# Plan-redundancy insight (reasonplan negative result)
|
|
273
|
+
# ------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def _compute_plan_redundancy(graphs: List[ReasoningTraceGraph]) -> float:
|
|
277
|
+
"""Measure planning-structure bloat (Branch/Backtrack without productive children).
|
|
278
|
+
|
|
279
|
+
Inspired by reasonplan negative results: excessive planning overhead
|
|
280
|
+
in reasoning traces degrades effective output quality.
|
|
281
|
+
"""
|
|
282
|
+
if not graphs:
|
|
283
|
+
return 0.0
|
|
284
|
+
scores = []
|
|
285
|
+
for g in graphs:
|
|
286
|
+
n = len(g.nodes)
|
|
287
|
+
if n == 0:
|
|
288
|
+
continue
|
|
289
|
+
plan_nodes = [node for node in g.nodes if node.type.value in ("Branch", "Backtrack")]
|
|
290
|
+
if not plan_nodes:
|
|
291
|
+
scores.append(0.0)
|
|
292
|
+
continue
|
|
293
|
+
wasteful = 0
|
|
294
|
+
for node in plan_nodes:
|
|
295
|
+
children = [e[1] for e in g.edges if e[0] == node.id]
|
|
296
|
+
child_types = {c.type.value for c in g.nodes if c.id in children}
|
|
297
|
+
if not child_types.intersection({"Transform", "Retrieve", "Compare"}):
|
|
298
|
+
wasteful += 1
|
|
299
|
+
scores.append(wasteful / n)
|
|
300
|
+
return float(np.mean(scores))
|
|
301
|
+
|
|
302
|
+
# ------------------------------------------------------------------
|
|
303
|
+
# Overall scoring
|
|
304
|
+
# ------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _compute_overall_scores(
|
|
308
|
+
results: Dict[str, ExtractorBenchmarkResult],
|
|
309
|
+
) -> None:
|
|
310
|
+
"""Aggregate per-extractor scores into a single 0-1 score.
|
|
311
|
+
|
|
312
|
+
Weights:
|
|
313
|
+
GCP passed & mean → 40%
|
|
314
|
+
NGS pass rate → 35%
|
|
315
|
+
TSI consistency → 25%
|
|
316
|
+
"""
|
|
317
|
+
for r in results.values():
|
|
318
|
+
scores = []
|
|
319
|
+
weights = []
|
|
320
|
+
|
|
321
|
+
if r.gcp is not None:
|
|
322
|
+
gcp_score = r.gcp.mean_gcs if r.gcp.passed else r.gcp.mean_gcs * 0.5
|
|
323
|
+
scores.append(gcp_score)
|
|
324
|
+
weights.append(0.40)
|
|
325
|
+
|
|
326
|
+
if r.n_samples > 0:
|
|
327
|
+
scores.append(r.ngs_pass_rate)
|
|
328
|
+
weights.append(0.35)
|
|
329
|
+
|
|
330
|
+
if r.mean_tsi_vs_others > 0:
|
|
331
|
+
scores.append(r.mean_tsi_vs_others)
|
|
332
|
+
weights.append(0.25)
|
|
333
|
+
|
|
334
|
+
if scores:
|
|
335
|
+
r.overall_score = float(np.average(scores, weights=weights))
|
|
336
|
+
else:
|
|
337
|
+
r.overall_score = 0.0
|
|
338
|
+
|
|
339
|
+
# Pass threshold: overall >= 0.70 AND NGS pass rate >= 0.60
|
|
340
|
+
r.passed = r.overall_score >= 0.70 and r.ngs_pass_rate >= 0.60
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
# Convenience one-liner
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
def benchmark_extractors(
|
|
348
|
+
extractors: Dict[str, Callable[[str], ReasoningTraceGraph]],
|
|
349
|
+
sample_texts: Optional[List[str]] = None,
|
|
350
|
+
graphs: Optional[Dict[str, List[ReasoningTraceGraph]]] = None,
|
|
351
|
+
output_path: Optional[str] = None,
|
|
352
|
+
) -> BenchmarkReport:
|
|
353
|
+
"""Run full benchmark and optionally save report to JSON."""
|
|
354
|
+
bench = ExtractorBenchmark()
|
|
355
|
+
report = bench.run(extractors, sample_texts=sample_texts, graphs=graphs)
|
|
356
|
+
if output_path:
|
|
357
|
+
report.to_json(output_path)
|
|
358
|
+
return report
|