consequa-x 0.1.0a2__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.
- consequa_x/__init__.py +22 -0
- consequa_x/__main__.py +3 -0
- consequa_x/adapters/__init__.py +5 -0
- consequa_x/adapters/ollama.py +350 -0
- consequa_x/attribution.py +407 -0
- consequa_x/benchmark/__init__.py +1 -0
- consequa_x/benchmark/assessment_metrics.py +54 -0
- consequa_x/benchmark/calibration.py +787 -0
- consequa_x/benchmark/calibration_export.py +139 -0
- consequa_x/benchmark/calibration_mechanisms.py +266 -0
- consequa_x/benchmark/decision_coalition.py +254 -0
- consequa_x/benchmark/decision_contrastive.py +448 -0
- consequa_x/benchmark/decision_replay.py +329 -0
- consequa_x/benchmark/decision_shapley.py +628 -0
- consequa_x/benchmark/evaluation.py +187 -0
- consequa_x/benchmark/interaction_export.py +220 -0
- consequa_x/benchmark/interaction_mechanisms.py +192 -0
- consequa_x/benchmark/interval_comparison.py +276 -0
- consequa_x/benchmark/intervals.py +164 -0
- consequa_x/benchmark/payments.py +426 -0
- consequa_x/benchmark/policy_experiment.py +298 -0
- consequa_x/benchmark/policy_runner.py +234 -0
- consequa_x/benchmark/runner.py +241 -0
- consequa_x/benchmark/study.py +183 -0
- consequa_x/benchmark/targets.py +149 -0
- consequa_x/benchmark/templates.py +102 -0
- consequa_x/cli.py +1191 -0
- consequa_x/explanation.py +130 -0
- consequa_x/graph.py +142 -0
- consequa_x/models.py +532 -0
- consequa_x/policies.py +248 -0
- consequa_x/py.typed +0 -0
- consequa_x/schema.py +159 -0
- consequa_x/schemas/trace-v0.1.json +543 -0
- consequa_x/verification.py +192 -0
- consequa_x-0.1.0a2.dist-info/METADATA +303 -0
- consequa_x-0.1.0a2.dist-info/RECORD +41 -0
- consequa_x-0.1.0a2.dist-info/WHEEL +5 -0
- consequa_x-0.1.0a2.dist-info/entry_points.txt +2 -0
- consequa_x-0.1.0a2.dist-info/licenses/LICENSE +21 -0
- consequa_x-0.1.0a2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""Decision-level suffix replay against one observed factual outcome.
|
|
2
|
+
|
|
3
|
+
This independently written CAR-style binary adaptation keeps its estimates
|
|
4
|
+
separate from paired context leave-one-out effects. It uses the captured
|
|
5
|
+
factual outcome once, fresh continuation seeds, and changed-minus-factual bad
|
|
6
|
+
probability. Missing evidence refuses inference; it never becomes a good draw.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from copy import deepcopy
|
|
12
|
+
from dataclasses import dataclass, replace
|
|
13
|
+
import math
|
|
14
|
+
import random
|
|
15
|
+
from typing import Iterable, Literal
|
|
16
|
+
|
|
17
|
+
from ..attribution import REPLAY_TARGETS, ReplayTarget
|
|
18
|
+
from ..models import EventType, Trace, to_dict
|
|
19
|
+
from ..policies import PolicyError
|
|
20
|
+
from .decision_replay import DecisionPrefixReplay, replay_decision_prefix
|
|
21
|
+
from .evaluation import BudgetedPolicy, CallBudgetExceeded
|
|
22
|
+
from .intervals import binomial_interval
|
|
23
|
+
from .policy_runner import MAX_TRIAL_SEED
|
|
24
|
+
from .runner import Episode
|
|
25
|
+
from .targets import episode_outcome
|
|
26
|
+
from .templates import DEFAULT_REFUND_TEMPLATE, RefundTaskTemplate
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
REFERENCE_REVISION = "db90fa28b97164c35e7c524e4597b8cbdb3035af"
|
|
30
|
+
CONTRASTIVE_INTERVAL_METHODS = ("percentile_bootstrap", "wilson", "clopper_pearson")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class DecisionContrastiveBranch:
|
|
35
|
+
"""An attempted suffix sample; the full plan also retains unstarted slots."""
|
|
36
|
+
|
|
37
|
+
index: int
|
|
38
|
+
decision_index: int
|
|
39
|
+
continuation_seed: int
|
|
40
|
+
replay: DecisionPrefixReplay
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class DecisionContrastiveEstimate:
|
|
45
|
+
decision_index: int
|
|
46
|
+
event_id: str
|
|
47
|
+
target: ReplayTarget
|
|
48
|
+
status: Literal["estimated", "refused"]
|
|
49
|
+
factual_bad: bool | None
|
|
50
|
+
changed_bad_rate: float | None
|
|
51
|
+
effect: float | None
|
|
52
|
+
confidence_interval: tuple[float, float] | None
|
|
53
|
+
known_samples: int
|
|
54
|
+
unknown_samples: int
|
|
55
|
+
failed_samples: int
|
|
56
|
+
unstarted_samples: int
|
|
57
|
+
seeds: tuple[int, ...]
|
|
58
|
+
significant_rescue: bool | None
|
|
59
|
+
reason: str | None = None
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def interval_degenerate(self) -> bool | None:
|
|
63
|
+
if self.confidence_interval is None:
|
|
64
|
+
return None
|
|
65
|
+
return self.confidence_interval[0] == self.confidence_interval[1]
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> dict:
|
|
68
|
+
result = to_dict(self)
|
|
69
|
+
result["interval_degenerate"] = self.interval_degenerate
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class DecisionContrastiveLocus:
|
|
75
|
+
target: ReplayTarget
|
|
76
|
+
status: Literal["identified", "no_rescue", "refused"]
|
|
77
|
+
decision_index: int | None = None
|
|
78
|
+
event_id: str | None = None
|
|
79
|
+
reason: str | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class DecisionContrastiveResult:
|
|
84
|
+
factual: Episode
|
|
85
|
+
branches: tuple[DecisionContrastiveBranch, ...]
|
|
86
|
+
results: tuple[DecisionContrastiveEstimate, ...]
|
|
87
|
+
loci: tuple[DecisionContrastiveLocus, ...]
|
|
88
|
+
policy_metadata: dict
|
|
89
|
+
targets: tuple[ReplayTarget, ...]
|
|
90
|
+
samples_per_decision: int
|
|
91
|
+
planned_seeds: tuple[tuple[int, ...], ...]
|
|
92
|
+
planned_branches: int
|
|
93
|
+
calls: int
|
|
94
|
+
initial_calls: int
|
|
95
|
+
max_calls: int
|
|
96
|
+
budget_exhausted: bool
|
|
97
|
+
template: RefundTaskTemplate
|
|
98
|
+
confidence: float
|
|
99
|
+
bootstrap_samples: int
|
|
100
|
+
bootstrap_seed: int
|
|
101
|
+
interval_method: str = "percentile_bootstrap"
|
|
102
|
+
|
|
103
|
+
def method_record(self) -> dict:
|
|
104
|
+
bootstrap = self.interval_method == "percentile_bootstrap"
|
|
105
|
+
return {
|
|
106
|
+
"name": "car_style_binary_decision_contrastive",
|
|
107
|
+
"reference_revision": REFERENCE_REVISION,
|
|
108
|
+
"baseline": "one_observed_factual_outcome",
|
|
109
|
+
"bad_encoding": "not target_value; unknown remains unknown",
|
|
110
|
+
"effect_sign": "changed_bad_rate_minus_factual_bad",
|
|
111
|
+
"intervention": "hold decisions before position; resample position and suffix",
|
|
112
|
+
"decision_order": "ascending factual decision positions",
|
|
113
|
+
"continuation_seed_schedule": "seed + decision_index * samples + sample_index",
|
|
114
|
+
"interval_method": ("conditional_percentile_bootstrap" if bootstrap else self.interval_method),
|
|
115
|
+
"confidence": self.confidence,
|
|
116
|
+
"bootstrap_samples": self.bootstrap_samples if bootstrap else None,
|
|
117
|
+
"bootstrap_seed": self.bootstrap_seed if bootstrap else None,
|
|
118
|
+
"bootstrap_rng": "python.random.Random (MT19937), reset per estimate" if bootstrap else None,
|
|
119
|
+
"bootstrap_baseline": "constant observed label; no RNG draws for baseline",
|
|
120
|
+
"quantile_method": "linear interpolation between sorted bootstrap effects" if bootstrap else None,
|
|
121
|
+
"interval_target": "known changed bad-outcome probability minus constant factual label",
|
|
122
|
+
"interval_assumptions": "independent known binary draws from the declared continuation law",
|
|
123
|
+
"binomial_coverage": ("equal-tailed exact binomial, conservative under its assumptions"
|
|
124
|
+
if self.interval_method == "clopper_pearson" else
|
|
125
|
+
"score approximation" if self.interval_method == "wilson" else None),
|
|
126
|
+
"multiplicity_adjustment": "none",
|
|
127
|
+
"rescue_rule": "effect < 0 and confidence_interval upper bound < 0",
|
|
128
|
+
"locus_rule": "latest rescuing position; every position must be estimable",
|
|
129
|
+
"missingness_policy": "refuse whole affected estimate; no drops or redraws",
|
|
130
|
+
"budget_unit": "actual attempted policy calls, including provider failures",
|
|
131
|
+
"minimum_samples_per_decision": 2,
|
|
132
|
+
"deviations": [
|
|
133
|
+
"Python RNG and a constant baseline differ from upstream NumPy seed draws.",
|
|
134
|
+
"Minimum two samples and strict finite input validation.",
|
|
135
|
+
"Call budget retains partial failure and unstarted receipts instead of aborting.",
|
|
136
|
+
"Unknown evidence refuses estimates; incomplete positions refuse the final locus.",
|
|
137
|
+
],
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def summary(self) -> dict:
|
|
141
|
+
completed = sum(branch.replay.episode is not None for branch in self.branches)
|
|
142
|
+
return {
|
|
143
|
+
"factual_decision_count": len(self.planned_seeds),
|
|
144
|
+
"samples_per_decision": self.samples_per_decision,
|
|
145
|
+
"planned_branches": self.planned_branches,
|
|
146
|
+
"attempted_branches": len(self.branches),
|
|
147
|
+
"completed_branches": completed,
|
|
148
|
+
"failed_branches": len(self.branches) - completed,
|
|
149
|
+
"not_started_branches": self.planned_branches - len(self.branches),
|
|
150
|
+
"policy_calls": self.calls,
|
|
151
|
+
"shared_budget_calls_before": self.initial_calls,
|
|
152
|
+
"shared_budget_calls_after": self.initial_calls + self.calls,
|
|
153
|
+
"max_calls": self.max_calls,
|
|
154
|
+
"budget_exhausted": self.budget_exhausted,
|
|
155
|
+
"estimates": {
|
|
156
|
+
target: {status: sum(result.target == target and result.status == status
|
|
157
|
+
for result in self.results)
|
|
158
|
+
for status in ("estimated", "refused")}
|
|
159
|
+
for target in self.targets
|
|
160
|
+
},
|
|
161
|
+
"loci": to_dict(self.loci),
|
|
162
|
+
"method": self.method_record(),
|
|
163
|
+
"statistical_limits": [
|
|
164
|
+
"Intervals condition on one observed factual episode and world.",
|
|
165
|
+
"There is no multiplicity correction across positions or targets.",
|
|
166
|
+
("Homogeneous binary samples can yield zero-width bootstrap intervals; "
|
|
167
|
+
"this does not prove zero population variance or certain rescue."
|
|
168
|
+
if self.interval_method == "percentile_bootstrap" else
|
|
169
|
+
"Binomial intervals do not collapse at homogeneous observations; "
|
|
170
|
+
"coverage statements require their independent binomial assumptions."),
|
|
171
|
+
"Requested seeds do not establish independent samples or hidden-state equality.",
|
|
172
|
+
"The selected locus is a nominal statistical rule, not proof of unique causation.",
|
|
173
|
+
],
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _linear_quantile(ordered: list[float], probability: float) -> float:
|
|
178
|
+
position = (len(ordered) - 1) * probability
|
|
179
|
+
lower = math.floor(position)
|
|
180
|
+
upper = math.ceil(position)
|
|
181
|
+
fraction = position - lower
|
|
182
|
+
return ordered[lower] * (1 - fraction) + ordered[upper] * fraction
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _bootstrap_interval(values: tuple[bool, ...], factual_bad: bool, *,
|
|
186
|
+
confidence: float, bootstrap_samples: int,
|
|
187
|
+
bootstrap_seed: int) -> tuple[float, float]:
|
|
188
|
+
# This is deliberately a separate estimator from paired Wilson differences.
|
|
189
|
+
# One observed factual label stays constant in every bootstrap replicate.
|
|
190
|
+
rng = random.Random(bootstrap_seed)
|
|
191
|
+
count = len(values)
|
|
192
|
+
if all(value == values[0] for value in values):
|
|
193
|
+
point = int(values[0]) - int(factual_bad)
|
|
194
|
+
return float(point), float(point)
|
|
195
|
+
effects = sorted(
|
|
196
|
+
sum(values[rng.randrange(count)] for _ in range(count)) / count - int(factual_bad)
|
|
197
|
+
for _ in range(bootstrap_samples)
|
|
198
|
+
)
|
|
199
|
+
tail = (1 - confidence) / 2
|
|
200
|
+
return _linear_quantile(effects, tail), _linear_quantile(effects, 1 - tail)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _selected_targets(targets: Iterable[ReplayTarget]) -> tuple[ReplayTarget, ...]:
|
|
204
|
+
if isinstance(targets, str):
|
|
205
|
+
raise ValueError("targets must be a nonempty iterable of distinct replay targets")
|
|
206
|
+
selected = tuple(targets)
|
|
207
|
+
if (not selected or any(not isinstance(target, str) or target not in REPLAY_TARGETS
|
|
208
|
+
for target in selected) or len(set(selected)) != len(selected)):
|
|
209
|
+
raise ValueError("targets must be a nonempty iterable of distinct replay targets")
|
|
210
|
+
return selected
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _interval_method(method):
|
|
214
|
+
if type(method) is not str or method not in CONTRASTIVE_INTERVAL_METHODS:
|
|
215
|
+
raise ValueError("interval_method must be percentile_bootstrap, wilson or clopper_pearson")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _changed_interval(values, factual_bad, *, confidence, interval_method,
|
|
219
|
+
bootstrap_samples, bootstrap_seed):
|
|
220
|
+
if interval_method == "percentile_bootstrap":
|
|
221
|
+
return _bootstrap_interval(tuple(values), factual_bad, confidence=confidence,
|
|
222
|
+
bootstrap_samples=bootstrap_samples, bootstrap_seed=bootstrap_seed)
|
|
223
|
+
lower, upper = binomial_interval(sum(values), len(values), confidence=confidence,
|
|
224
|
+
method=interval_method)
|
|
225
|
+
return lower - int(factual_bad), upper - int(factual_bad)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _loci(results, targets):
|
|
229
|
+
loci = []
|
|
230
|
+
for target in targets:
|
|
231
|
+
candidates = tuple(result for result in results if result.target == target)
|
|
232
|
+
refused = sum(result.status == "refused" for result in candidates)
|
|
233
|
+
if refused:
|
|
234
|
+
loci.append(DecisionContrastiveLocus(
|
|
235
|
+
target, "refused", reason=f"{refused} factual decision positions lack complete "
|
|
236
|
+
"estimates; a latest rescue locus cannot be declared."))
|
|
237
|
+
else:
|
|
238
|
+
rescues = tuple(result for result in candidates if result.significant_rescue)
|
|
239
|
+
if rescues:
|
|
240
|
+
latest = max(rescues, key=lambda result: result.decision_index)
|
|
241
|
+
loci.append(DecisionContrastiveLocus(target, "identified", latest.decision_index,
|
|
242
|
+
latest.event_id))
|
|
243
|
+
else:
|
|
244
|
+
loci.append(DecisionContrastiveLocus(
|
|
245
|
+
target, "no_rescue", reason="No position satisfies the declared negative "
|
|
246
|
+
"effect and interval criterion."))
|
|
247
|
+
return tuple(loci)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def with_contrastive_interval(result: DecisionContrastiveResult, *,
|
|
251
|
+
interval_method: str) -> DecisionContrastiveResult:
|
|
252
|
+
"""Compare intervals on the same completed samples, without new policy calls.
|
|
253
|
+
|
|
254
|
+
Original refusals stay refused. Evidence is recomputed through the allowed
|
|
255
|
+
outcome adapter; changed factual labels or sample means reject the rescore.
|
|
256
|
+
"""
|
|
257
|
+
if not isinstance(result, DecisionContrastiveResult):
|
|
258
|
+
raise TypeError("result must be a DecisionContrastiveResult")
|
|
259
|
+
_interval_method(interval_method)
|
|
260
|
+
factual = episode_outcome(result.factual)
|
|
261
|
+
cache = {(branch.decision_index, branch.continuation_seed):
|
|
262
|
+
episode_outcome(branch.replay.episode) if branch.replay.episode is not None else None
|
|
263
|
+
for branch in result.branches}
|
|
264
|
+
estimates = []
|
|
265
|
+
for item in result.results:
|
|
266
|
+
if item.status == "refused":
|
|
267
|
+
estimates.append(item)
|
|
268
|
+
continue
|
|
269
|
+
factual_value = factual.target_value(item.target)
|
|
270
|
+
changed = []
|
|
271
|
+
for seed in item.seeds:
|
|
272
|
+
outcome = cache.get((item.decision_index, seed))
|
|
273
|
+
value = outcome.target_value(item.target) if outcome is not None else None
|
|
274
|
+
if value is None:
|
|
275
|
+
raise ValueError("Previously estimated sample evidence became unavailable")
|
|
276
|
+
changed.append(not value)
|
|
277
|
+
if (factual_value is None or (not factual_value) != item.factual_bad
|
|
278
|
+
or len(changed) != result.samples_per_decision
|
|
279
|
+
or sum(changed) / len(changed) != item.changed_bad_rate
|
|
280
|
+
or item.changed_bad_rate - int(item.factual_bad) != item.effect):
|
|
281
|
+
raise ValueError("Factual label or sampled mean changed after the original estimate")
|
|
282
|
+
interval = _changed_interval(changed, item.factual_bad, confidence=result.confidence,
|
|
283
|
+
interval_method=interval_method,
|
|
284
|
+
bootstrap_samples=result.bootstrap_samples,
|
|
285
|
+
bootstrap_seed=result.bootstrap_seed)
|
|
286
|
+
estimates.append(replace(item, confidence_interval=interval,
|
|
287
|
+
significant_rescue=item.effect < 0 and interval[1] < 0))
|
|
288
|
+
return replace(result, results=tuple(estimates), loci=_loci(estimates, result.targets),
|
|
289
|
+
interval_method=interval_method)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def contrastive_decisions(
|
|
293
|
+
factual: Episode,
|
|
294
|
+
continuation_policy: BudgetedPolicy,
|
|
295
|
+
*,
|
|
296
|
+
samples: int = 16,
|
|
297
|
+
seed: int = 100,
|
|
298
|
+
targets: Iterable[ReplayTarget] = ("consequence",),
|
|
299
|
+
template: RefundTaskTemplate = DEFAULT_REFUND_TEMPLATE,
|
|
300
|
+
confidence: float = 0.95,
|
|
301
|
+
bootstrap_samples: int = 2000,
|
|
302
|
+
bootstrap_seed: int = 0,
|
|
303
|
+
interval_method: str = "clopper_pearson",
|
|
304
|
+
) -> DecisionContrastiveResult:
|
|
305
|
+
"""Sample every factual decision's suffix and estimate binary bad outcomes.
|
|
306
|
+
|
|
307
|
+
The supplied budget can already include the factual recording or other
|
|
308
|
+
work. ``calls`` is only this operation's continuation-call delta. Earlier
|
|
309
|
+
decisions are held with exact input checks; each sample gets a distinct
|
|
310
|
+
policy seed schedule while the factual simulator world stays fixed.
|
|
311
|
+
|
|
312
|
+
The complete factual capture, selected targets, schedule, template and
|
|
313
|
+
declared policy identity are validated before new provider calls. Every
|
|
314
|
+
target reuses one collected branch cache. Any missing outcome refuses that
|
|
315
|
+
candidate's whole estimate, and any refused candidate prevents a final
|
|
316
|
+
locus for its target. No sample is dropped, replaced, or redrawn.
|
|
317
|
+
"""
|
|
318
|
+
if not isinstance(factual, Episode):
|
|
319
|
+
raise TypeError("factual must be an Episode")
|
|
320
|
+
if not isinstance(factual.trace, Trace):
|
|
321
|
+
raise TypeError("factual.trace must be a Trace")
|
|
322
|
+
if not isinstance(continuation_policy, BudgetedPolicy):
|
|
323
|
+
raise TypeError("continuation_policy must be a BudgetedPolicy")
|
|
324
|
+
_interval_method(interval_method)
|
|
325
|
+
if type(samples) is not int or samples < 2:
|
|
326
|
+
raise ValueError("samples must be an integer of at least 2")
|
|
327
|
+
if type(seed) is not int or not 0 <= seed <= MAX_TRIAL_SEED:
|
|
328
|
+
raise ValueError(f"seed must be an integer in 0..{MAX_TRIAL_SEED}")
|
|
329
|
+
if type(bootstrap_samples) is not int or bootstrap_samples < 2:
|
|
330
|
+
raise ValueError("bootstrap_samples must be an integer of at least 2")
|
|
331
|
+
if type(bootstrap_seed) is not int or bootstrap_seed < 0:
|
|
332
|
+
raise ValueError("bootstrap_seed must be a nonnegative integer")
|
|
333
|
+
if type(confidence) not in {int, float} or not math.isfinite(confidence) or not 0 < confidence < 1:
|
|
334
|
+
raise ValueError("confidence must be finite and strictly between 0 and 1")
|
|
335
|
+
if not isinstance(template, RefundTaskTemplate):
|
|
336
|
+
raise TypeError("template must be a RefundTaskTemplate")
|
|
337
|
+
if (type(continuation_policy.max_calls) is not int or continuation_policy.max_calls < 1
|
|
338
|
+
or type(continuation_policy.calls) is not int
|
|
339
|
+
or not 0 <= continuation_policy.calls <= continuation_policy.max_calls):
|
|
340
|
+
raise ValueError("BudgetedPolicy must have a valid call counter and positive limit")
|
|
341
|
+
selected = _selected_targets(targets)
|
|
342
|
+
events = tuple(event for event in factual.trace.events if event.type is EventType.AGENT_DECISION)
|
|
343
|
+
if not events:
|
|
344
|
+
raise ValueError("factual trace must contain captured policy decisions")
|
|
345
|
+
planned_branches = len(events) * samples
|
|
346
|
+
if seed + planned_branches - 1 > MAX_TRIAL_SEED:
|
|
347
|
+
raise ValueError(f"Continuation seed schedule must stay in 0..{MAX_TRIAL_SEED}")
|
|
348
|
+
# Full held replay validates every capture without invoking the provider,
|
|
349
|
+
# even if the shared call budget is already exhausted.
|
|
350
|
+
probe = replay_decision_prefix(factual, continuation_policy, hold_count=len(events),
|
|
351
|
+
continuation_seed=seed, template=template)
|
|
352
|
+
if probe.episode is None:
|
|
353
|
+
raise ValueError("Full factual reconstruction failed: " + (probe.error or "unknown error"))
|
|
354
|
+
factual_policy_seed = events[0].data["model_seed"] // 6
|
|
355
|
+
if seed <= factual_policy_seed <= seed + planned_branches - 1:
|
|
356
|
+
raise ValueError("Continuation seed schedule overlaps the factual policy phase seeds")
|
|
357
|
+
planned_seeds = tuple(tuple(seed + position * samples + offset for offset in range(samples))
|
|
358
|
+
for position in range(len(events)))
|
|
359
|
+
metadata = deepcopy(probe.policy_metadata)
|
|
360
|
+
factual_outcome = episode_outcome(factual)
|
|
361
|
+
initial_calls = continuation_policy.calls
|
|
362
|
+
branches = []
|
|
363
|
+
exhausted = False
|
|
364
|
+
for position, seeds in enumerate(planned_seeds):
|
|
365
|
+
for continuation_seed in seeds:
|
|
366
|
+
if continuation_policy.calls >= continuation_policy.max_calls:
|
|
367
|
+
exhausted = True
|
|
368
|
+
break
|
|
369
|
+
before = continuation_policy.calls
|
|
370
|
+
try:
|
|
371
|
+
replay = replay_decision_prefix(
|
|
372
|
+
factual, continuation_policy, hold_count=position,
|
|
373
|
+
continuation_seed=continuation_seed, template=template,
|
|
374
|
+
)
|
|
375
|
+
except (PolicyError, ValueError, TypeError) as error:
|
|
376
|
+
# Preflight was valid. Later identity/capture drift is a failed
|
|
377
|
+
# planned draw, not a reason to lose already collected receipts.
|
|
378
|
+
replay = DecisionPrefixReplay(
|
|
379
|
+
position, continuation_seed, factual.seed, len(events), deepcopy(metadata),
|
|
380
|
+
(), continuation_policy.calls - before,
|
|
381
|
+
error_type=type(error).__name__, error=str(error),
|
|
382
|
+
budget_exhausted=isinstance(error, CallBudgetExceeded),
|
|
383
|
+
)
|
|
384
|
+
branches.append(DecisionContrastiveBranch(len(branches), position,
|
|
385
|
+
continuation_seed, replay))
|
|
386
|
+
if replay.budget_exhausted:
|
|
387
|
+
exhausted = True
|
|
388
|
+
break
|
|
389
|
+
if exhausted:
|
|
390
|
+
break
|
|
391
|
+
|
|
392
|
+
# Compute allowed target evidence once per completed world. Evaluator truth
|
|
393
|
+
# and agent assessment are not inputs to the binary outcome mapping.
|
|
394
|
+
cache = {(branch.decision_index, branch.continuation_seed):
|
|
395
|
+
(branch, episode_outcome(branch.replay.episode)
|
|
396
|
+
if branch.replay.episode is not None else None)
|
|
397
|
+
for branch in branches}
|
|
398
|
+
results = []
|
|
399
|
+
for position, seeds in enumerate(planned_seeds):
|
|
400
|
+
for target in selected:
|
|
401
|
+
factual_value = factual_outcome.target_value(target)
|
|
402
|
+
factual_bad = None if factual_value is None else not factual_value
|
|
403
|
+
counts = {key: 0 for key in ("known", "unknown", "failed", "unstarted")}
|
|
404
|
+
changed = []
|
|
405
|
+
for continuation_seed in seeds:
|
|
406
|
+
item = cache.get((position, continuation_seed))
|
|
407
|
+
if item is None:
|
|
408
|
+
counts["unstarted"] += 1
|
|
409
|
+
elif item[0].replay.episode is None:
|
|
410
|
+
counts["failed"] += 1
|
|
411
|
+
else:
|
|
412
|
+
value = item[1].target_value(target)
|
|
413
|
+
if value is None:
|
|
414
|
+
counts["unknown"] += 1
|
|
415
|
+
else:
|
|
416
|
+
counts["known"] += 1
|
|
417
|
+
changed.append(not value)
|
|
418
|
+
point = rate = interval = rescue = None
|
|
419
|
+
reason = None
|
|
420
|
+
status = "refused"
|
|
421
|
+
if factual_bad is not None and counts["known"] == samples:
|
|
422
|
+
rate = sum(changed) / samples
|
|
423
|
+
point = rate - int(factual_bad)
|
|
424
|
+
interval = _changed_interval(
|
|
425
|
+
tuple(changed), factual_bad, confidence=confidence, interval_method=interval_method,
|
|
426
|
+
bootstrap_samples=bootstrap_samples, bootstrap_seed=bootstrap_seed,
|
|
427
|
+
)
|
|
428
|
+
rescue = point < 0 and interval[1] < 0
|
|
429
|
+
status = "estimated"
|
|
430
|
+
else:
|
|
431
|
+
reason = (
|
|
432
|
+
f"Cannot estimate {target}: factual target is "
|
|
433
|
+
f"{'unknown' if factual_bad is None else 'known'}; "
|
|
434
|
+
f"{counts['known']} of {samples} suffix samples known, "
|
|
435
|
+
f"{counts['unknown']} unknown, {counts['failed']} failed, "
|
|
436
|
+
f"{counts['unstarted']} unstarted. No draws are dropped or redrawn."
|
|
437
|
+
)
|
|
438
|
+
results.append(DecisionContrastiveEstimate(
|
|
439
|
+
position, events[position].id, target, status, factual_bad, rate, point, interval,
|
|
440
|
+
counts["known"], counts["unknown"], counts["failed"], counts["unstarted"],
|
|
441
|
+
seeds, rescue, reason,
|
|
442
|
+
))
|
|
443
|
+
return DecisionContrastiveResult(
|
|
444
|
+
factual, tuple(branches), tuple(results), _loci(results, selected), metadata, selected, samples,
|
|
445
|
+
planned_seeds, planned_branches, continuation_policy.calls - initial_calls, initial_calls,
|
|
446
|
+
continuation_policy.max_calls, exhausted, template, float(confidence), bootstrap_samples,
|
|
447
|
+
bootstrap_seed, interval_method,
|
|
448
|
+
)
|