evalrx 0.1.2__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.
- evalrx/__init__.py +139 -0
- evalrx/agent_assets/__init__.py +2 -0
- evalrx/agent_assets/skills/README.md +28 -0
- evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
- evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
- evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
- evalrx/agent_assets/skills/nature-figure/README.md +412 -0
- evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
- evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
- evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
- evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
- evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
- evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
- evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
- evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
- evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
- evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
- evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
- evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
- evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
- evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
- evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
- evalrx/agent_assets/skills.py +27 -0
- evalrx/agent_runtime/__init__.py +78 -0
- evalrx/agent_runtime/_docker_runner.py +89 -0
- evalrx/agent_runtime/cli_runtime.py +103 -0
- evalrx/agent_runtime/cli_transcript.py +138 -0
- evalrx/agent_runtime/cli_types.py +68 -0
- evalrx/agent_runtime/codegen/__init__.py +5 -0
- evalrx/agent_runtime/codegen/runner.py +94 -0
- evalrx/agent_runtime/experiment_harness.py +117 -0
- evalrx/agent_runtime/factory.py +102 -0
- evalrx/agent_runtime/json_shape.py +44 -0
- evalrx/agent_runtime/judges/__init__.py +28 -0
- evalrx/agent_runtime/judges/agy.py +179 -0
- evalrx/agent_runtime/judges/autodetect.py +135 -0
- evalrx/agent_runtime/judges/claude.py +159 -0
- evalrx/agent_runtime/judges/codex.py +120 -0
- evalrx/agent_runtime/providers/__init__.py +21 -0
- evalrx/agent_runtime/providers/antigravity.py +31 -0
- evalrx/agent_runtime/providers/base.py +145 -0
- evalrx/agent_runtime/providers/claude_code.py +49 -0
- evalrx/agent_runtime/providers/codex.py +37 -0
- evalrx/agent_runtime/providers/gemini_cli.py +26 -0
- evalrx/agent_runtime/providers/kimi_cli.py +27 -0
- evalrx/agent_runtime/providers/opencode.py +27 -0
- evalrx/agent_runtime/providers/registry.py +58 -0
- evalrx/agent_runtime/sandbox.py +517 -0
- evalrx/agent_runtime/skill_audit.py +143 -0
- evalrx/agent_runtime/skills/__init__.py +19 -0
- evalrx/agent_runtime/skills/installer.py +68 -0
- evalrx/agent_runtime/skills/prompt_policy.py +86 -0
- evalrx/agent_runtime/skills/resolver.py +19 -0
- evalrx/analysis/__init__.py +132 -0
- evalrx/analysis/adjudicate.py +154 -0
- evalrx/analysis/analysis_module.py +361 -0
- evalrx/analysis/api.py +171 -0
- evalrx/analysis/case_studio.py +651 -0
- evalrx/analysis/cli.py +114 -0
- evalrx/analysis/dashboard.py +350 -0
- evalrx/analysis/eval_case_matrix.py +118 -0
- evalrx/analysis/eval_viz_theme.py +833 -0
- evalrx/analysis/explore_run.py +333 -0
- evalrx/analysis/explorer.py +1276 -0
- evalrx/analysis/failure_modes.py +607 -0
- evalrx/analysis/fused_pipeline.py +489 -0
- evalrx/analysis/holdout.py +300 -0
- evalrx/analysis/hypothesis_agent.py +230 -0
- evalrx/analysis/narration.py +177 -0
- evalrx/analysis/operationalize.py +442 -0
- evalrx/analysis/plain_language.py +42 -0
- evalrx/analysis/planner.py +283 -0
- evalrx/analysis/probe_search.py +203 -0
- evalrx/analysis/profile.py +268 -0
- evalrx/analysis/prompts/__init__.py +0 -0
- evalrx/analysis/prompts/explorer.py +417 -0
- evalrx/analysis/prompts/failure_modes.py +33 -0
- evalrx/analysis/prompts/holdout.py +27 -0
- evalrx/analysis/prompts/hypothesis_agent.py +78 -0
- evalrx/analysis/prompts/run_codebase.py +47 -0
- evalrx/analysis/prompts/stats_agent.py +72 -0
- evalrx/analysis/prompts/stats_tool_generator.py +43 -0
- evalrx/analysis/result_marker.py +47 -0
- evalrx/analysis/run_codebase.py +242 -0
- evalrx/analysis/run_view.py +205 -0
- evalrx/analysis/stage_views.py +93 -0
- evalrx/analysis/stats_agent.py +944 -0
- evalrx/analysis/stats_tool_agent.py +261 -0
- evalrx/analysis/stats_tool_generator.py +415 -0
- evalrx/analysis/stats_tools.py +1153 -0
- evalrx/analysis/trajectory_records.py +193 -0
- evalrx/analysis/workbench.py +431 -0
- evalrx/analyzers/__init__.py +42 -0
- evalrx/analyzers/agent/__init__.py +25 -0
- evalrx/analyzers/agent/counterfactual.py +84 -0
- evalrx/analyzers/agent/first_error_judge.py +96 -0
- evalrx/analyzers/agent/ignored_obs.py +81 -0
- evalrx/analyzers/agent/loop_detect.py +79 -0
- evalrx/analyzers/agent/reliability.py +165 -0
- evalrx/analyzers/agent/tool_shap.py +225 -0
- evalrx/analyzers/agent/trajectory_rubric.py +168 -0
- evalrx/analyzers/attention/__init__.py +19 -0
- evalrx/analyzers/attention/relative_attn.py +610 -0
- evalrx/analyzers/attention/rollout.py +73 -0
- evalrx/analyzers/attention/sink.py +56 -0
- evalrx/analyzers/attention/summary.py +190 -0
- evalrx/analyzers/attribution/__init__.py +6 -0
- evalrx/analyzers/attribution/generic_attn.py +31 -0
- evalrx/analyzers/attribution/gradcam.py +30 -0
- evalrx/analyzers/base.py +12 -0
- evalrx/analyzers/geometry/__init__.py +6 -0
- evalrx/analyzers/geometry/cka.py +70 -0
- evalrx/analyzers/geometry/linear_probe.py +157 -0
- evalrx/analyzers/hallucination/__init__.py +9 -0
- evalrx/analyzers/hallucination/chair.py +78 -0
- evalrx/analyzers/hallucination/opera.py +29 -0
- evalrx/analyzers/hallucination/pope.py +119 -0
- evalrx/analyzers/hallucination/selfcheck.py +155 -0
- evalrx/analyzers/hallucination/vcd.py +29 -0
- evalrx/analyzers/lens/__init__.py +7 -0
- evalrx/analyzers/lens/layer_contrast.py +133 -0
- evalrx/analyzers/lens/logit_lens.py +138 -0
- evalrx/analyzers/lens/tuned_lens.py +30 -0
- evalrx/analyzers/patching/__init__.py +5 -0
- evalrx/analyzers/patching/causal_trace.py +30 -0
- evalrx/analyzers/perturbation/__init__.py +23 -0
- evalrx/analyzers/perturbation/_shapley.py +54 -0
- evalrx/analyzers/perturbation/context_shap.py +174 -0
- evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
- evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
- evalrx/analyzers/perturbation/mm_shap.py +146 -0
- evalrx/analyzers/perturbation/modality_ablation.py +196 -0
- evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
- evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
- evalrx/analyzers/perturbation/rise.py +94 -0
- evalrx/analyzers/perturbation/vl_shap.py +102 -0
- evalrx/analyzers/reasoning/__init__.py +33 -0
- evalrx/analyzers/reasoning/_text.py +328 -0
- evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
- evalrx/analyzers/reasoning/arith_audit.py +226 -0
- evalrx/analyzers/reasoning/contamination.py +214 -0
- evalrx/analyzers/reasoning/knowledge_split.py +253 -0
- evalrx/analyzers/reasoning/self_repair.py +246 -0
- evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
- evalrx/analyzers/reasoning/termination_audit.py +258 -0
- evalrx/analyzers/uncertainty/__init__.py +18 -0
- evalrx/analyzers/uncertainty/calibration.py +174 -0
- evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
- evalrx/analyzers/uncertainty/entropy.py +90 -0
- evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
- evalrx/analyzers/uncertainty/self_consistency.py +204 -0
- evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
- evalrx/cli.py +411 -0
- evalrx/config.py +77 -0
- evalrx/contract/__init__.py +179 -0
- evalrx/contract/common.py +452 -0
- evalrx/contract/emit.py +948 -0
- evalrx/contract/export.py +237 -0
- evalrx/contract/m1.py +325 -0
- evalrx/contract/m2.py +317 -0
- evalrx/contract/m3.py +165 -0
- evalrx/contract/m4.py +130 -0
- evalrx/contract/m5.py +292 -0
- evalrx/contract/methodology.py +76 -0
- evalrx/contract/pre_m1.py +58 -0
- evalrx/contract/typescript.py +140 -0
- evalrx/core/__init__.py +85 -0
- evalrx/core/analyzer.py +174 -0
- evalrx/core/capability.py +54 -0
- evalrx/core/case.py +443 -0
- evalrx/core/experiment.py +106 -0
- evalrx/core/model.py +198 -0
- evalrx/core/pipeline.py +42 -0
- evalrx/core/registry.py +142 -0
- evalrx/core/result.py +64 -0
- evalrx/core/spec.py +173 -0
- evalrx/core/tokentype.py +165 -0
- evalrx/core/tool.py +92 -0
- evalrx/datasets/__init__.py +41 -0
- evalrx/datasets/base.py +68 -0
- evalrx/datasets/gui_os.py +52 -0
- evalrx/datasets/llm_qa.py +57 -0
- evalrx/datasets/pure_qa.py +12 -0
- evalrx/datasets/vlm_qa.py +695 -0
- evalrx/datasets/web_search_qa.py +52 -0
- evalrx/eval_agent/__init__.py +341 -0
- evalrx/eval_agent/_tools.py +81 -0
- evalrx/eval_agent/ab_runner.py +50 -0
- evalrx/eval_agent/agentic/__init__.py +43 -0
- evalrx/eval_agent/agentic/actions.py +216 -0
- evalrx/eval_agent/agentic/board.py +107 -0
- evalrx/eval_agent/agentic/loop.py +190 -0
- evalrx/eval_agent/agentic/tools.py +538 -0
- evalrx/eval_agent/checkpoint.py +57 -0
- evalrx/eval_agent/cli_agent.py +59 -0
- evalrx/eval_agent/cli_skills.py +5 -0
- evalrx/eval_agent/evolution.py +396 -0
- evalrx/eval_agent/git_manager.py +215 -0
- evalrx/eval_agent/hypothesis.py +172 -0
- evalrx/eval_agent/label_quarantine.py +209 -0
- evalrx/eval_agent/legacy.py +530 -0
- evalrx/eval_agent/log_schema.py +497 -0
- evalrx/eval_agent/loop.py +2159 -0
- evalrx/eval_agent/loop_reports.py +116 -0
- evalrx/eval_agent/model_instrumentation.py +282 -0
- evalrx/eval_agent/narration.py +193 -0
- evalrx/eval_agent/nl_runner.py +460 -0
- evalrx/eval_agent/orchestrator.py +61 -0
- evalrx/eval_agent/preregister.py +93 -0
- evalrx/eval_agent/prompts/__init__.py +1 -0
- evalrx/eval_agent/prompts/agentic.py +46 -0
- evalrx/eval_agent/prompts/case_discovery.py +25 -0
- evalrx/eval_agent/prompts/diagnosis.py +125 -0
- evalrx/eval_agent/prompts/experiment_writer.py +265 -0
- evalrx/eval_agent/prompts/explore_step.py +37 -0
- evalrx/eval_agent/prompts/fix_agent.py +257 -0
- evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
- evalrx/eval_agent/prompts/nl_runner.py +38 -0
- evalrx/eval_agent/prompts/probe_agent.py +25 -0
- evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
- evalrx/eval_agent/prompts/probe_generator.py +35 -0
- evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
- evalrx/eval_agent/report.py +58 -0
- evalrx/eval_agent/run_context.py +354 -0
- evalrx/eval_agent/run_log.schema.json +1215 -0
- evalrx/eval_agent/run_logger_v2.py +1764 -0
- evalrx/eval_agent/run_metadata.py +208 -0
- evalrx/eval_agent/stages/__init__.py +56 -0
- evalrx/eval_agent/stages/case_discovery.py +293 -0
- evalrx/eval_agent/stages/diagnosis.py +1017 -0
- evalrx/eval_agent/stages/experiment_writer.py +1634 -0
- evalrx/eval_agent/stages/fix_agent.py +3916 -0
- evalrx/eval_agent/stages/fix_internals.py +499 -0
- evalrx/eval_agent/stages/fix_pipeline.py +725 -0
- evalrx/eval_agent/stages/fix_tiers.py +187 -0
- evalrx/eval_agent/stages/fix_tools.py +1034 -0
- evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
- evalrx/eval_agent/stages/probe.py +439 -0
- evalrx/eval_agent/stages/probe_agent.py +1079 -0
- evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
- evalrx/eval_agent/stages/probe_generator.py +326 -0
- evalrx/eval_agent/stages/probe_search_agent.py +106 -0
- evalrx/eval_agent/stages/protocol.py +112 -0
- evalrx/eval_agent/stages/repair_catalog.py +273 -0
- evalrx/eval_agent/stages/surgery.py +524 -0
- evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
- evalrx/eval_agent/store.py +231 -0
- evalrx/logging_utils.py +112 -0
- evalrx/models/__init__.py +161 -0
- evalrx/models/_discover.py +101 -0
- evalrx/models/agent.py +380 -0
- evalrx/models/backends/__init__.py +58 -0
- evalrx/models/backends/api.py +169 -0
- evalrx/models/backends/base.py +57 -0
- evalrx/models/backends/gemini_compat.py +579 -0
- evalrx/models/backends/hf_local.py +2074 -0
- evalrx/models/backends/openai_compat.py +301 -0
- evalrx/models/backends/vllm_offline.py +116 -0
- evalrx/models/base.py +24 -0
- evalrx/models/blackbox/__init__.py +4 -0
- evalrx/models/blackbox/agent.py +31 -0
- evalrx/models/blackbox/base.py +29 -0
- evalrx/models/blackbox/gemini.py +279 -0
- evalrx/models/blackbox/llm_api.py +17 -0
- evalrx/models/blackbox/vlm_api.py +17 -0
- evalrx/models/compose.py +66 -0
- evalrx/models/inference.py +88 -0
- evalrx/models/paper_methods/__init__.py +8 -0
- evalrx/models/paper_methods/aad.py +53 -0
- evalrx/models/paper_methods/ifcd.py +204 -0
- evalrx/models/paper_methods/pai.py +164 -0
- evalrx/models/paper_methods/tcd.py +202 -0
- evalrx/models/paper_methods/vcd.py +45 -0
- evalrx/models/paper_methods/vicrop.py +137 -0
- evalrx/models/toolcodec.py +143 -0
- evalrx/models/tools/__init__.py +20 -0
- evalrx/models/tools/perception.py +300 -0
- evalrx/models/tools/visual.py +174 -0
- evalrx/models/whitebox/__init__.py +26 -0
- evalrx/models/whitebox/agent.py +31 -0
- evalrx/models/whitebox/base.py +24 -0
- evalrx/models/whitebox/qwen.py +61 -0
- evalrx/models/whitebox/qwen2_5_omni.py +29 -0
- evalrx/models/whitebox/qwen2_audio.py +25 -0
- evalrx/models/whitebox/qwen_omni.py +53 -0
- evalrx/models/whitebox/qwen_vl.py +62 -0
- evalrx/observability/__init__.py +21 -0
- evalrx/observability/envelope.py +122 -0
- evalrx/observability/outbox.py +111 -0
- evalrx/observability/tracer.py +882 -0
- evalrx/reporting/__init__.py +28 -0
- evalrx/reporting/case_study.py +947 -0
- evalrx/reporting/compiler.py +587 -0
- evalrx/reporting/dynamic.py +1882 -0
- evalrx/reporting/html_report.py +2225 -0
- evalrx/reporting/langfuse_exporter.py +38 -0
- evalrx/reporting/langfuse_source.py +155 -0
- evalrx/reporting/model.py +151 -0
- evalrx/reporting/run_events.py +184 -0
- evalrx/reporting/server.py +557 -0
- evalrx/reporting/stages.py +58 -0
- evalrx/reporting/static_export.py +142 -0
- evalrx/reporting/web_dist/index.html +146 -0
- evalrx/specs.py +727 -0
- evalrx/stats/__init__.py +47 -0
- evalrx/stats/api.py +192 -0
- evalrx/stats/bootstrap.py +86 -0
- evalrx/stats/ebh.py +27 -0
- evalrx/stats/evalue.py +98 -0
- evalrx/stats/friedman.py +138 -0
- evalrx/stats/mcnemar.py +40 -0
- evalrx/stats/multiplicity.py +159 -0
- evalrx/stats/subset_sampling.py +55 -0
- evalrx/term_links.py +43 -0
- evalrx/viz/__init__.py +7 -0
- evalrx/viz/labels.py +77 -0
- evalrx/viz/prompts.py +39 -0
- evalrx/viz/renderer.py +590 -0
- evalrx/viz/schema.py +36 -0
- evalrx/viz/style.py +134 -0
- evalrx-0.1.2.dist-info/METADATA +532 -0
- evalrx-0.1.2.dist-info/RECORD +339 -0
- evalrx-0.1.2.dist-info/WHEEL +5 -0
- evalrx-0.1.2.dist-info/entry_points.txt +3 -0
- evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
- evalrx-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Generic planning utilities for M2 analysis.
|
|
2
|
+
|
|
3
|
+
The planner consumes a profile of the available data and emits a deterministic
|
|
4
|
+
tool plan. It is intentionally conservative: discovery may inspect everything,
|
|
5
|
+
but confirmatory plans should make tested families explicit and avoid silently
|
|
6
|
+
dropping columns because of their original order.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from evalrx.analysis.profile import DatasetProfile, profile_stats_input
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class AnalysisPlanItem:
|
|
23
|
+
"""One planned statistical analysis."""
|
|
24
|
+
|
|
25
|
+
tool: str
|
|
26
|
+
config: dict[str, Any] = field(default_factory=dict)
|
|
27
|
+
rationale: str = ""
|
|
28
|
+
estimand: str = ""
|
|
29
|
+
family: str = "confirmatory"
|
|
30
|
+
priority: float = 0.0
|
|
31
|
+
|
|
32
|
+
def as_legacy_tuple(self) -> tuple[str, dict[str, Any], str]:
|
|
33
|
+
return self.tool, self.config, self.rationale
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict[str, Any]:
|
|
36
|
+
return {
|
|
37
|
+
"tool": self.tool,
|
|
38
|
+
"config": self.config,
|
|
39
|
+
"rationale": self.rationale,
|
|
40
|
+
"estimand": self.estimand,
|
|
41
|
+
"family": self.family,
|
|
42
|
+
"priority": self.priority,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _signal_priority(inp: Any, signal: str, profile: DatasetProfile) -> float:
|
|
47
|
+
col = profile.columns.get(signal)
|
|
48
|
+
sigmap = (getattr(inp, "per_case", {}) or {}).get(signal, {})
|
|
49
|
+
labels = getattr(inp, "labels", {}) or {}
|
|
50
|
+
if not sigmap:
|
|
51
|
+
return 0.0
|
|
52
|
+
n_labeled = max(1, len(labels))
|
|
53
|
+
coverage = min(1.0, len(sigmap) / n_labeled)
|
|
54
|
+
values = [float(v) for v in sigmap.values()]
|
|
55
|
+
unique = len(set(values))
|
|
56
|
+
variance = 0.0
|
|
57
|
+
if values:
|
|
58
|
+
mean = sum(values) / len(values)
|
|
59
|
+
variance = sum((v - mean) ** 2 for v in values) / len(values)
|
|
60
|
+
kind_bonus = 0.2 if col and col.is_binary else 0.1 if col and col.dtype == "numeric" else 0.0
|
|
61
|
+
sparse_binary_bonus = 0.15 if col and col.is_binary and coverage < 0.95 else 0.0
|
|
62
|
+
constant_penalty = -1.0 if unique <= 1 else 0.0
|
|
63
|
+
return coverage + min(1.0, variance) + kind_bonus + sparse_binary_bonus + constant_penalty
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
#: Agreement above which a BINARY signal is the label wearing another name.
|
|
67
|
+
#: Not 1.0: one flipped case out of dozens is still a restatement, not a finding.
|
|
68
|
+
_LABEL_RESTATEMENT_AGREEMENT = 0.95
|
|
69
|
+
#: Below this many shared cases, perfect agreement is cheap and means nothing.
|
|
70
|
+
_MIN_CASES_TO_JUDGE_RESTATEMENT = 10
|
|
71
|
+
#: Names of fields an analyzer produces BY GRADING — i.e. by asking the same
|
|
72
|
+
#: question the label answers. Matched on the last dotted segment.
|
|
73
|
+
_CORRECTNESS_NAME = re.compile(
|
|
74
|
+
r"(?:^|_)(?:correct|incorrect|label|labelled|passed|is_pass)(?:_|$)"
|
|
75
|
+
r"|_match$|^match$"
|
|
76
|
+
)
|
|
77
|
+
# These answer-audit fields are defined using the gold and/or the stored
|
|
78
|
+
# PASS/FAIL label. A sparse flag can have low whole-batch agreement while
|
|
79
|
+
# still making ``P(FAIL | flag)=1`` by construction, so an association test is
|
|
80
|
+
# circular. They remain useful descriptive sanity signals, never candidate
|
|
81
|
+
# discriminators for M2/M4.
|
|
82
|
+
_LABEL_DERIVED_SUFFIXES = frozenset({
|
|
83
|
+
"answer_extraction_audit.extraction_suspect",
|
|
84
|
+
"answer_extraction_audit.extraction_point_miss",
|
|
85
|
+
"answer_extraction_audit.gold_in_output",
|
|
86
|
+
"answer_extraction_audit.gold_in_answer_region",
|
|
87
|
+
"answer_extraction_audit.strict_match",
|
|
88
|
+
"answer_extraction_audit.label_disagrees",
|
|
89
|
+
"answer_extraction_audit.labelled_fail",
|
|
90
|
+
# gold x pred conjunctions: P(FAIL | flag) = 1 by construction. Sparse
|
|
91
|
+
# enough to slip past label_leak_score, named nothing like "correct", so
|
|
92
|
+
# they sat in the VLM family as guaranteed BH survivors. The marginals
|
|
93
|
+
# (pope.answered_yes / pope.gold_yes) stay testable.
|
|
94
|
+
"pope.false_positive",
|
|
95
|
+
"pope.false_negative",
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def restates_label(inp: Any, signal: str) -> bool:
|
|
100
|
+
"""True when *signal* reproduces the PASS/FAIL label, or its complement.
|
|
101
|
+
|
|
102
|
+
Such a signal always rejects H0, always survives FDR, and carries no
|
|
103
|
+
information — "does the label predict the label". Left in the pool it also
|
|
104
|
+
CROWDS OUT the real ones, because the survivor list is what M4 draws on to
|
|
105
|
+
verify a hypothesis. Measured on qwen3.5-2b / bbh_causal_judgement: of 10
|
|
106
|
+
rejecting signals, 6 survived FDR and every one of those 6 was the label
|
|
107
|
+
(``labelled_fail``, ``strict_match``, ``calibration.correct``,
|
|
108
|
+
``self_repair.baseline_correct`` / ``revised_correct``), while the two
|
|
109
|
+
genuinely informative extraction flags did not survive. M2's own narrative
|
|
110
|
+
called them out as "the label copied under another name"; M4 then verified a
|
|
111
|
+
hypothesis about label REPRODUCIBILITY using ``labelled_fail``, at
|
|
112
|
+
confidence 0.73.
|
|
113
|
+
|
|
114
|
+
Requires BOTH a correctness-shaped NAME and label agreement in the DATA,
|
|
115
|
+
because neither alone is safe and each fixes the other's failure:
|
|
116
|
+
|
|
117
|
+
* Data alone suppresses real findings. A composite discovered over
|
|
118
|
+
independent measurements — ``(obj_size < 40) and (attention < 0.3)`` —
|
|
119
|
+
can separate the labels perfectly, and that is the most valuable result
|
|
120
|
+
the loop can produce, not a tautology. It is statistically identical to
|
|
121
|
+
``calibration.correct``; only its provenance differs.
|
|
122
|
+
* Name alone suppresses honest fields. A column called ``correct`` that
|
|
123
|
+
tracks something other than this batch's label is a legitimate signal.
|
|
124
|
+
|
|
125
|
+
Two further things this deliberately does NOT do:
|
|
126
|
+
|
|
127
|
+
* It does not judge arbitrary signals by the conditional rate. A sparse,
|
|
128
|
+
independently measured flag may legitimately have
|
|
129
|
+
``P(FAIL | signal)=1``. The explicit answer-audit exceptions above are
|
|
130
|
+
removed by provenance because their definitions use gold/labels, not
|
|
131
|
+
because of their observed conditional rate.
|
|
132
|
+
* It does not touch continuous signals. Restatement is an identity claim,
|
|
133
|
+
and a continuous measure binarised at some threshold can drift into high
|
|
134
|
+
agreement without being the label at all.
|
|
135
|
+
"""
|
|
136
|
+
labels = getattr(inp, "labels", {}) or {}
|
|
137
|
+
if str(signal).lower() in _LABEL_DERIVED_SUFFIXES:
|
|
138
|
+
return bool(labels)
|
|
139
|
+
if not _CORRECTNESS_NAME.search(str(signal).rsplit(".", 1)[-1].lower()):
|
|
140
|
+
return False
|
|
141
|
+
sigmap = (getattr(inp, "per_case", {}) or {}).get(signal, {})
|
|
142
|
+
shared = [key for key in sigmap if key in labels]
|
|
143
|
+
if len(shared) < _MIN_CASES_TO_JUDGE_RESTATEMENT:
|
|
144
|
+
return False
|
|
145
|
+
try:
|
|
146
|
+
values = {float(sigmap[key]) for key in shared}
|
|
147
|
+
except (TypeError, ValueError):
|
|
148
|
+
return False
|
|
149
|
+
if not values <= {0.0, 1.0} or len(values) < 2:
|
|
150
|
+
return False
|
|
151
|
+
agree = sum(1 for key in shared
|
|
152
|
+
if bool(float(sigmap[key])) == bool(labels[key]))
|
|
153
|
+
rate = agree / len(shared)
|
|
154
|
+
return (rate >= _LABEL_RESTATEMENT_AGREEMENT
|
|
155
|
+
or rate <= 1.0 - _LABEL_RESTATEMENT_AGREEMENT)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def label_restating_signals(inp: Any) -> list[str]:
|
|
159
|
+
"""The signals :func:`ranked_signal_names` drops, so callers can report them.
|
|
160
|
+
|
|
161
|
+
Dropped rather than tested-and-flagged on purpose: the harm is not that the
|
|
162
|
+
result is wrong, it is that the result is RIGHT and meaningless, and every
|
|
163
|
+
such test consumes a slot in the FDR family that a real signal needed.
|
|
164
|
+
"""
|
|
165
|
+
return sorted(
|
|
166
|
+
signal for signal in (getattr(inp, "per_case", {}) or {})
|
|
167
|
+
if restates_label(inp, signal)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def ranked_signal_names(inp: Any, *, max_signals: int | None = None) -> list[str]:
|
|
172
|
+
"""Rank per-case signals by testability rather than insertion order."""
|
|
173
|
+
profile = profile_stats_input(inp)
|
|
174
|
+
scored = [
|
|
175
|
+
(_signal_priority(inp, signal, profile), signal)
|
|
176
|
+
for signal in (getattr(inp, "per_case", {}) or {})
|
|
177
|
+
if not restates_label(inp, signal)
|
|
178
|
+
]
|
|
179
|
+
scored.sort(key=lambda item: (-item[0], item[1]))
|
|
180
|
+
names = [name for score, name in scored if score > 0]
|
|
181
|
+
if max_signals is not None:
|
|
182
|
+
names = names[:max(0, int(max_signals))]
|
|
183
|
+
return names
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def plan_stats_input(
|
|
187
|
+
inp: Any,
|
|
188
|
+
*,
|
|
189
|
+
max_signals: int | None = None,
|
|
190
|
+
) -> list[AnalysisPlanItem]:
|
|
191
|
+
"""Create a deterministic confirmatory plan for a ``StatsInput`` object."""
|
|
192
|
+
labels = getattr(inp, "labels", {}) or {}
|
|
193
|
+
groups = getattr(inp, "groups", None) or {}
|
|
194
|
+
vectors = getattr(inp, "per_case_vectors", {}) or {}
|
|
195
|
+
n_fail = sum(1 for value in labels.values() if value)
|
|
196
|
+
n_pass = len(labels) - n_fail
|
|
197
|
+
plan: list[AnalysisPlanItem] = []
|
|
198
|
+
|
|
199
|
+
if n_pass > 0 and n_fail > 0 and getattr(inp, "per_case", None):
|
|
200
|
+
# Dropped, not silently: a suppressed signal that turns out to be real
|
|
201
|
+
# has to leave a trace somewhere, and this is the only place that knows.
|
|
202
|
+
dropped = label_restating_signals(inp)
|
|
203
|
+
if dropped:
|
|
204
|
+
logger.info(
|
|
205
|
+
"planner: dropped %d signal(s) that restate the PASS/FAIL label "
|
|
206
|
+
"and would crowd the FDR family: %s",
|
|
207
|
+
len(dropped), ", ".join(dropped))
|
|
208
|
+
ranked = ranked_signal_names(inp, max_signals=max_signals)
|
|
209
|
+
for key in ranked:
|
|
210
|
+
plan.append(AnalysisPlanItem(
|
|
211
|
+
tool="signal_label_assoc",
|
|
212
|
+
config={"signal": key},
|
|
213
|
+
rationale=f"test whether per-case signal '{key}' predicts FAIL",
|
|
214
|
+
estimand=f"P(FAIL | {key}=present/high) - P(FAIL | {key}=absent/low)",
|
|
215
|
+
priority=1.0,
|
|
216
|
+
))
|
|
217
|
+
continuous = [
|
|
218
|
+
key for key in ranked
|
|
219
|
+
if key in (getattr(inp, "per_case", {}) or {})
|
|
220
|
+
and len({float(v) for v in inp.per_case[key].values()}) > 2
|
|
221
|
+
]
|
|
222
|
+
for key in continuous:
|
|
223
|
+
plan.append(AnalysisPlanItem(
|
|
224
|
+
tool="rank_corr",
|
|
225
|
+
config={"signal": key},
|
|
226
|
+
rationale=f"monotonic association between continuous '{key}' and FAIL",
|
|
227
|
+
estimand=f"Kendall tau({key}, FAIL)",
|
|
228
|
+
family="descriptive",
|
|
229
|
+
priority=0.5,
|
|
230
|
+
))
|
|
231
|
+
|
|
232
|
+
if n_pass > 0 and n_fail > 0 and vectors:
|
|
233
|
+
vector_names = list(vectors)
|
|
234
|
+
if max_signals is not None:
|
|
235
|
+
vector_names = vector_names[:max(0, int(max_signals))]
|
|
236
|
+
for key in vector_names:
|
|
237
|
+
plan.append(AnalysisPlanItem(
|
|
238
|
+
tool="attention_decoding",
|
|
239
|
+
config={"signal": key},
|
|
240
|
+
rationale=f"omnibus: do FAIL/PASS per-case maps '{key}' differ?",
|
|
241
|
+
estimand=f"distributional difference in vector signal {key}",
|
|
242
|
+
priority=0.8,
|
|
243
|
+
))
|
|
244
|
+
|
|
245
|
+
if labels:
|
|
246
|
+
plan.append(AnalysisPlanItem(
|
|
247
|
+
tool="single_rate_evalue",
|
|
248
|
+
config={},
|
|
249
|
+
rationale="describe the overall FAIL rate against an explicit baseline if provided",
|
|
250
|
+
estimand="P(FAIL) - p0",
|
|
251
|
+
family="descriptive",
|
|
252
|
+
priority=0.1,
|
|
253
|
+
))
|
|
254
|
+
|
|
255
|
+
n_groups = len(groups)
|
|
256
|
+
if n_groups >= 3:
|
|
257
|
+
plan.append(AnalysisPlanItem(
|
|
258
|
+
tool="friedman_nemenyi",
|
|
259
|
+
config={},
|
|
260
|
+
rationale="rank 3+ strategies on shared cases",
|
|
261
|
+
estimand="strategy rank differences",
|
|
262
|
+
priority=0.9,
|
|
263
|
+
))
|
|
264
|
+
names = list(groups)
|
|
265
|
+
base = names[0]
|
|
266
|
+
for variant in names[1:]:
|
|
267
|
+
plan.append(AnalysisPlanItem(
|
|
268
|
+
tool="mcnemar_evalue",
|
|
269
|
+
config={"strategies": [base, variant]},
|
|
270
|
+
rationale=f"paired contrast: does '{variant}' repair '{base}' failures?",
|
|
271
|
+
estimand=f"{variant} success - {base} success on paired cases",
|
|
272
|
+
priority=1.0,
|
|
273
|
+
))
|
|
274
|
+
elif n_groups == 2:
|
|
275
|
+
plan.append(AnalysisPlanItem(
|
|
276
|
+
tool="mcnemar_evalue",
|
|
277
|
+
config={},
|
|
278
|
+
rationale="paired two-strategy comparison",
|
|
279
|
+
estimand="strategy success difference on paired cases",
|
|
280
|
+
priority=1.0,
|
|
281
|
+
))
|
|
282
|
+
|
|
283
|
+
return plan
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Hierarchical MCTS probing search (ProbeLLM-style adaptive test-case discovery).
|
|
2
|
+
|
|
3
|
+
Standalone tree-search skeleton: it knows nothing about models, judges, or
|
|
4
|
+
datasets — those are injected as three callables (``generate_macro``,
|
|
5
|
+
``generate_micro``, ``verify``). This mirrors the paper's two-level design
|
|
6
|
+
(arXiv 2602.12966, "ProbeLLM"): a global choice between Macro (broad
|
|
7
|
+
coverage — diversify topics) and Micro (local refinement — densify evidence
|
|
8
|
+
around a seed) regimes, each running its own UCB-guided MCTS to pick which
|
|
9
|
+
node to expand next, and a shared node/backup bookkeeping (Eqs. 5-10 in the
|
|
10
|
+
paper).
|
|
11
|
+
|
|
12
|
+
Failure cases discovered here are plain ``FailureCase`` objects — the result
|
|
13
|
+
feeds directly into :func:`evalrx.analysis.failure_modes.cluster_failures`
|
|
14
|
+
for failure-mode synthesis.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import math
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any, Callable
|
|
22
|
+
|
|
23
|
+
from evalrx.core.case import CaseBatch, FailureCase, Label
|
|
24
|
+
|
|
25
|
+
Regime = str # "macro" | "micro"
|
|
26
|
+
|
|
27
|
+
GenerateMacroFn = Callable[["ProbeNode", "list[ProbeNode]"], "FailureCase | None"]
|
|
28
|
+
GenerateMicroFn = Callable[["ProbeNode"], "FailureCase | None"]
|
|
29
|
+
VerifyFn = Callable[[FailureCase], FailureCase]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ProbeNode:
|
|
34
|
+
"""One evaluated test case in the search tree (paper Eq.5: (x, y*, y, fail)).
|
|
35
|
+
|
|
36
|
+
``N``/``E`` are *this node's* expansion stats (paper Eq.6): how many times
|
|
37
|
+
a child was proposed+evaluated from here, and how many of those children
|
|
38
|
+
were failures. They are updated on nodes along the *selection path*
|
|
39
|
+
(root..this node), not on the freshly created child itself — a brand-new
|
|
40
|
+
node always starts at N=E=0 until it is later expanded.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
case: FailureCase
|
|
44
|
+
regime: Regime
|
|
45
|
+
parent: "ProbeNode | None" = None
|
|
46
|
+
children: list["ProbeNode"] = field(default_factory=list)
|
|
47
|
+
N: int = 0
|
|
48
|
+
E: int = 0
|
|
49
|
+
|
|
50
|
+
def p_hat(self) -> float:
|
|
51
|
+
"""Empirical failure rate of expansions from this node (Eq.6)."""
|
|
52
|
+
return self.E / max(1, self.N)
|
|
53
|
+
|
|
54
|
+
def is_fail(self) -> bool:
|
|
55
|
+
return self.case.label == Label.FAIL
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ProbeSearchResult:
|
|
60
|
+
"""Output of one :meth:`ProbeSearch.run` call."""
|
|
61
|
+
|
|
62
|
+
macro_root: ProbeNode
|
|
63
|
+
micro_root: ProbeNode
|
|
64
|
+
n_simulations: int
|
|
65
|
+
n_macro: int
|
|
66
|
+
n_micro: int
|
|
67
|
+
failure_cases: CaseBatch
|
|
68
|
+
all_cases: CaseBatch
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def error_rate(self) -> float:
|
|
72
|
+
return len(self.failure_cases) / self.n_simulations if self.n_simulations else 0.0
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> dict[str, Any]:
|
|
75
|
+
return {
|
|
76
|
+
"n_simulations": self.n_simulations,
|
|
77
|
+
"n_macro": self.n_macro,
|
|
78
|
+
"n_micro": self.n_micro,
|
|
79
|
+
"error_rate": self.error_rate,
|
|
80
|
+
"n_failures": len(self.failure_cases),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _ucb(node: ProbeNode, parent_n: int, beta: float) -> float:
|
|
85
|
+
"""UCB(v|u) = p_hat(v) + beta*sqrt(log(max(1,N(u))) / max(1,N(v))) (Eq.7)."""
|
|
86
|
+
return node.p_hat() + beta * math.sqrt(math.log(max(1, parent_n)) / max(1, node.N))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _select_expandable(root: ProbeNode, *, beta: float, w_max: int) -> ProbeNode:
|
|
90
|
+
"""Descend via UCB while the current node is already at max width,
|
|
91
|
+
stopping at the first node with room for a new child (paper Eq.7-8, the
|
|
92
|
+
"continue this selection step until reaching an expandable node")."""
|
|
93
|
+
u = root
|
|
94
|
+
while u.children and len(u.children) >= w_max:
|
|
95
|
+
u = max(u.children, key=lambda v: _ucb(v, u.N, beta))
|
|
96
|
+
return u
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _choose_regime(macro_root: ProbeNode, micro_root: ProbeNode, beta: float) -> Regime:
|
|
100
|
+
"""Top-level Macro/Micro choice, via the *same* UCB rule applied to the
|
|
101
|
+
two regime roots — the global root's two children in the paper's
|
|
102
|
+
hierarchy (Fig.2/Sec.3.2), rather than a fixed or alternating split."""
|
|
103
|
+
total_n = macro_root.N + micro_root.N
|
|
104
|
+
ucb_macro = _ucb(macro_root, total_n, beta)
|
|
105
|
+
ucb_micro = _ucb(micro_root, total_n, beta)
|
|
106
|
+
return "macro" if ucb_macro >= ucb_micro else "micro"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ProbeSearch:
|
|
110
|
+
"""Hierarchical MCTS driver.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
generate_macro: ``(node, explored_macro_nodes) -> FailureCase | None``.
|
|
114
|
+
Proposes one new, *unevaluated* candidate case aimed
|
|
115
|
+
at broad topical coverage; receives every macro-tree
|
|
116
|
+
node explored so far so it can pick its own
|
|
117
|
+
diversity/frontier heuristic (e.g. embed + cluster
|
|
118
|
+
medoids, paper Eq.11) — this module stays agnostic
|
|
119
|
+
to how that is done.
|
|
120
|
+
generate_micro: ``(node) -> FailureCase | None``. Proposes one local
|
|
121
|
+
perturbation of ``node.case`` (paper's controlled
|
|
122
|
+
entity/attribute substitutions).
|
|
123
|
+
verify: ``(case) -> case``. Runs the target model + scorer,
|
|
124
|
+
returning the same case with ``.observed``/``.label``
|
|
125
|
+
populated (paper Eq.1's verifier V). A thin wrapper
|
|
126
|
+
around ``CaseDiscoveryAgent.discover`` in practice.
|
|
127
|
+
seeds: Initial seed cases (from an existing benchmark) used
|
|
128
|
+
to anchor both regime roots.
|
|
129
|
+
budget: Total number of simulations (T_max in Eq.4).
|
|
130
|
+
beta: UCB exploration constant (Eq.7).
|
|
131
|
+
w_max: Max children per node before progressive widening
|
|
132
|
+
forces a deeper UCB descent instead of a new sibling.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
generate_macro: GenerateMacroFn,
|
|
139
|
+
generate_micro: GenerateMicroFn,
|
|
140
|
+
verify: VerifyFn,
|
|
141
|
+
seeds: "list[FailureCase] | CaseBatch",
|
|
142
|
+
budget: int = 20,
|
|
143
|
+
beta: float = 1.0,
|
|
144
|
+
w_max: int = 3,
|
|
145
|
+
) -> None:
|
|
146
|
+
seeds = list(seeds)
|
|
147
|
+
if not seeds:
|
|
148
|
+
raise ValueError("ProbeSearch requires at least one seed case")
|
|
149
|
+
self._generate_macro = generate_macro
|
|
150
|
+
self._generate_micro = generate_micro
|
|
151
|
+
self._verify = verify
|
|
152
|
+
self.budget = budget
|
|
153
|
+
self.beta = beta
|
|
154
|
+
self.w_max = max(1, w_max)
|
|
155
|
+
|
|
156
|
+
self.macro_root = ProbeNode(case=seeds[0], regime="macro")
|
|
157
|
+
self.micro_root = ProbeNode(case=seeds[0], regime="micro")
|
|
158
|
+
self._macro_nodes: list[ProbeNode] = [self.macro_root]
|
|
159
|
+
self._evaluated: list[FailureCase] = []
|
|
160
|
+
self._n_macro = 0
|
|
161
|
+
self._n_micro = 0
|
|
162
|
+
|
|
163
|
+
def run(self) -> ProbeSearchResult:
|
|
164
|
+
for _ in range(self.budget):
|
|
165
|
+
regime = _choose_regime(self.macro_root, self.micro_root, self.beta)
|
|
166
|
+
root = self.macro_root if regime == "macro" else self.micro_root
|
|
167
|
+
u = _select_expandable(root, beta=self.beta, w_max=self.w_max)
|
|
168
|
+
|
|
169
|
+
new_case = (
|
|
170
|
+
self._generate_macro(u, self._macro_nodes)
|
|
171
|
+
if regime == "macro"
|
|
172
|
+
else self._generate_micro(u)
|
|
173
|
+
)
|
|
174
|
+
if new_case is None:
|
|
175
|
+
continue # generator exhausted for this node; budget step still spent
|
|
176
|
+
|
|
177
|
+
evaluated = self._verify(new_case)
|
|
178
|
+
child = ProbeNode(case=evaluated, regime=regime, parent=u)
|
|
179
|
+
u.children.append(child)
|
|
180
|
+
if regime == "macro":
|
|
181
|
+
self._macro_nodes.append(child)
|
|
182
|
+
self._n_macro += 1
|
|
183
|
+
else:
|
|
184
|
+
self._n_micro += 1
|
|
185
|
+
self._evaluated.append(evaluated)
|
|
186
|
+
|
|
187
|
+
fail = 1 if evaluated.label == Label.FAIL else 0
|
|
188
|
+
node: "ProbeNode | None" = u
|
|
189
|
+
while node is not None:
|
|
190
|
+
node.N += 1
|
|
191
|
+
node.E += fail
|
|
192
|
+
node = node.parent
|
|
193
|
+
|
|
194
|
+
failures = CaseBatch([c for c in self._evaluated if c.label == Label.FAIL])
|
|
195
|
+
return ProbeSearchResult(
|
|
196
|
+
macro_root=self.macro_root,
|
|
197
|
+
micro_root=self.micro_root,
|
|
198
|
+
n_simulations=len(self._evaluated),
|
|
199
|
+
n_macro=self._n_macro,
|
|
200
|
+
n_micro=self._n_micro,
|
|
201
|
+
failure_cases=failures,
|
|
202
|
+
all_cases=CaseBatch(self._evaluated),
|
|
203
|
+
)
|