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.
Files changed (41) hide show
  1. consequa_x/__init__.py +22 -0
  2. consequa_x/__main__.py +3 -0
  3. consequa_x/adapters/__init__.py +5 -0
  4. consequa_x/adapters/ollama.py +350 -0
  5. consequa_x/attribution.py +407 -0
  6. consequa_x/benchmark/__init__.py +1 -0
  7. consequa_x/benchmark/assessment_metrics.py +54 -0
  8. consequa_x/benchmark/calibration.py +787 -0
  9. consequa_x/benchmark/calibration_export.py +139 -0
  10. consequa_x/benchmark/calibration_mechanisms.py +266 -0
  11. consequa_x/benchmark/decision_coalition.py +254 -0
  12. consequa_x/benchmark/decision_contrastive.py +448 -0
  13. consequa_x/benchmark/decision_replay.py +329 -0
  14. consequa_x/benchmark/decision_shapley.py +628 -0
  15. consequa_x/benchmark/evaluation.py +187 -0
  16. consequa_x/benchmark/interaction_export.py +220 -0
  17. consequa_x/benchmark/interaction_mechanisms.py +192 -0
  18. consequa_x/benchmark/interval_comparison.py +276 -0
  19. consequa_x/benchmark/intervals.py +164 -0
  20. consequa_x/benchmark/payments.py +426 -0
  21. consequa_x/benchmark/policy_experiment.py +298 -0
  22. consequa_x/benchmark/policy_runner.py +234 -0
  23. consequa_x/benchmark/runner.py +241 -0
  24. consequa_x/benchmark/study.py +183 -0
  25. consequa_x/benchmark/targets.py +149 -0
  26. consequa_x/benchmark/templates.py +102 -0
  27. consequa_x/cli.py +1191 -0
  28. consequa_x/explanation.py +130 -0
  29. consequa_x/graph.py +142 -0
  30. consequa_x/models.py +532 -0
  31. consequa_x/policies.py +248 -0
  32. consequa_x/py.typed +0 -0
  33. consequa_x/schema.py +159 -0
  34. consequa_x/schemas/trace-v0.1.json +543 -0
  35. consequa_x/verification.py +192 -0
  36. consequa_x-0.1.0a2.dist-info/METADATA +303 -0
  37. consequa_x-0.1.0a2.dist-info/RECORD +41 -0
  38. consequa_x-0.1.0a2.dist-info/WHEEL +5 -0
  39. consequa_x-0.1.0a2.dist-info/entry_points.txt +2 -0
  40. consequa_x-0.1.0a2.dist-info/licenses/LICENSE +21 -0
  41. 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
+ )