Spaces:
Paused
Paused
Download src/research/evidence_engine.py from ThomasHeisig/Brain-5D-Space: direct link, hf CLI and curl.
- Browser
- Download file 18.4 kB
-
https://huggingface.co/spaces/ThomasHeisig/Brain-5D-Space/resolve/main/src/research/evidence_engine.py
- Command line
-
hf download hf://spaces/ThomasHeisig/Brain-5D-Space/src/research/evidence_engine.py
-
curl -L -o evidence_engine.py https://huggingface.co/spaces/ThomasHeisig/Brain-5D-Space/resolve/main/src/research/evidence_engine.py
18.4 kB
| """ | |
| Evidence Engine — Evaluates experimental results and updates evidence status. | |
| After each experiment, the evidence engine: | |
| 1. Creates evidence entries from results | |
| 2. Links evidence to claims and hypotheses | |
| 3. Re-evaluates claim/hypothesis status based on accumulated evidence | |
| 4. Generates automated follow-up questions | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Any, cast | |
| from .registry import ( | |
| REGISTRY_DIR, | |
| REPO_ROOT, | |
| Claim, | |
| Hypothesis, | |
| ResearchRegistry, | |
| ) | |
| EXPERIMENTS_DIR = REPO_ROOT / "research" / "experiments" | |
| EVIDENCE_DIR = REGISTRY_DIR / "evidence" | |
| EVIDENCE_DIR.mkdir(exist_ok=True) | |
| def _next_evidence_id() -> str: | |
| """Generate the next evidence ID: EVID-{YYYY}-{NN}.""" | |
| year = datetime.now().year | |
| existing = list(EVIDENCE_DIR.glob(f"EVID-{year}-*.json")) | |
| n = len(existing) + 1 | |
| return f"EVID-{year}-{n:02d}" | |
| _INVALID_EVIDENCE_STATUSES = frozenset( | |
| { | |
| "template", | |
| "not_started", | |
| "running", | |
| "failed", | |
| "invalid", | |
| } | |
| ) | |
| """Experiment statuses that cannot produce scientific evidence.""" | |
| _VALID_EVIDENCE_MODES = frozenset( | |
| { | |
| "deterministic_verification", | |
| "stochastic_experiment", | |
| "observational_experiment", | |
| } | |
| ) | |
| def _json_digest(value: object) -> str: | |
| return hashlib.sha256( | |
| json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") | |
| ).hexdigest() | |
| def _check_experiment_valid(experiment_id: str) -> dict[str, Any] | None: | |
| """Check whether an experiment manifest is valid scientific evidence. | |
| Returns the manifest dict if valid, None if invalid. | |
| The EvidenceEngine MUST NOT accept: | |
| - template (template/example experiments) | |
| - not_started (not yet executed) | |
| - running (incomplete execution) | |
| - failed (execution failure/crash) | |
| - invalid (scientifically unusable run) | |
| Only ``completed`` experiments with ``validity.valid == True`` may | |
| generate scientific evidence. | |
| """ | |
| manifest_path = EXPERIMENTS_DIR / experiment_id / "manifest.json" | |
| if not manifest_path.exists(): | |
| return None | |
| try: | |
| manifest: dict[str, Any] = json.loads(manifest_path.read_text(encoding="utf-8")) | |
| except (json.JSONDecodeError, OSError): | |
| return None | |
| status = manifest.get("experiment_status", "not_started") | |
| if status != "completed" or status in _INVALID_EVIDENCE_STATUSES: | |
| return None | |
| validity_raw = manifest.get("validity", {}) | |
| if not isinstance(validity_raw, dict): | |
| return None | |
| validity = cast(dict[str, object], validity_raw) | |
| valid = validity.get("valid") | |
| if valid is not True: | |
| return None | |
| runtime_error_count = validity.get("runtime_error_count") | |
| fatal_error_count = validity.get("fatal_error_count") | |
| if any( | |
| not isinstance(count, int) or isinstance(count, bool) or count < 0 | |
| for count in (runtime_error_count, fatal_error_count) | |
| ): | |
| return None | |
| if runtime_error_count != 0 or fatal_error_count != 0: | |
| return None | |
| causal_taint = manifest.get("causal_taint", "PURE") | |
| if causal_taint not in {"PURE", "OBSERVED", "PROPOSED", "AI_INFLUENCED"}: | |
| return None | |
| if causal_taint != "PURE": | |
| treatment_raw = manifest.get("ai_treatment") | |
| if not isinstance(treatment_raw, dict): | |
| return None | |
| treatment = cast(dict[str, object], treatment_raw) | |
| protocol_id = treatment.get("protocol_id") | |
| if ( | |
| treatment.get("registered") is not True | |
| or not isinstance(protocol_id, str) | |
| or not protocol_id.strip() | |
| or treatment.get("mode", "confirmatory") | |
| not in {"exploratory", "confirmatory"} | |
| ): | |
| return None | |
| git_raw = manifest.get("git", {}) | |
| if not isinstance(git_raw, dict): | |
| return None | |
| git = cast(dict[str, Any], git_raw) | |
| if git.get("dirty") is not False: | |
| return None | |
| return manifest | |
| class EvidenceEngine: | |
| """Evaluates experimental evidence and updates claim/hypothesis status. | |
| The engine enforces scientific validity: | |
| - Only ``completed`` experiments with ``validity.valid == True`` | |
| can produce evidence. | |
| - Invalid, failed, or template experiments are silently rejected. | |
| """ | |
| def __init__(self, registry: ResearchRegistry): | |
| self.registry = registry | |
| def record_human_review( | |
| self, | |
| experiment_id: str, | |
| *, | |
| reviewer: str, | |
| decision: str, | |
| comments: str, | |
| ) -> Path: | |
| """Write the mandatory human review artifact for an experiment.""" | |
| if not experiment_id or "/" in experiment_id or "\\" in experiment_id: | |
| raise ValueError("Invalid experiment id") | |
| if not reviewer.strip() or not comments.strip(): | |
| raise ValueError("Human review requires reviewer and comments") | |
| if decision not in {"supports", "refutes", "inconclusive"}: | |
| raise ValueError("Human review decision is invalid") | |
| experiment_dir = EXPERIMENTS_DIR / experiment_id | |
| if not (experiment_dir / "manifest.json").is_file(): | |
| raise ValueError("Experiment manifest does not exist") | |
| review_path = experiment_dir / "human_review.json" | |
| if review_path.exists(): | |
| raise FileExistsError(f"Human review already exists: {experiment_id}") | |
| review = { | |
| "experiment_id": experiment_id, | |
| "reviewer": reviewer.strip(), | |
| "decision": decision, | |
| "comments": comments.strip(), | |
| "reviewed_at": datetime.now().isoformat(), | |
| } | |
| review_path.write_text( | |
| json.dumps(review, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" | |
| ) | |
| return review_path | |
| def promote_validated_experiment( | |
| self, | |
| experiment_id: str, | |
| claim_id: str, | |
| hypothesis_id: str, | |
| result_summary: str, | |
| *, | |
| evidence_mode: str = "observational_experiment", | |
| effect_size: dict[str, Any] | None = None, | |
| verification: dict[str, Any] | None = None, | |
| limitations: str | None = None, | |
| ) -> str: | |
| """Promote DATA to EVID only after source freeze and human review.""" | |
| manifest = _check_experiment_valid(experiment_id) | |
| if manifest is None: | |
| raise ValueError("Experiment is not eligible for validated promotion") | |
| provenance_raw = manifest.get("provenance_digests") | |
| source_freeze_sha = manifest.get("source_freeze_sha") | |
| if not isinstance(provenance_raw, dict) or not isinstance( | |
| source_freeze_sha, str | |
| ): | |
| raise ValueError("Validated promotion requires source-freeze digests") | |
| provenance = cast(dict[str, object], provenance_raw) | |
| if _json_digest(provenance) != source_freeze_sha: | |
| raise ValueError("Source-freeze digest does not match provenance digests") | |
| review_path = EXPERIMENTS_DIR / experiment_id / "human_review.json" | |
| try: | |
| review_raw: object = json.loads(review_path.read_text(encoding="utf-8")) | |
| except (OSError, json.JSONDecodeError) as exc: | |
| raise ValueError("Validated promotion requires a human review") from exc | |
| if not isinstance(review_raw, dict): | |
| raise ValueError("Human review artifact is invalid") | |
| review = cast(dict[str, object], review_raw) | |
| if review.get("experiment_id") != experiment_id: | |
| raise ValueError("Human review experiment identity does not match") | |
| if ( | |
| not isinstance(review.get("reviewer"), str) | |
| or not str(review["reviewer"]).strip() | |
| ): | |
| raise ValueError("Human review requires a reviewer") | |
| if ( | |
| not isinstance(review.get("comments"), str) | |
| or not str(review["comments"]).strip() | |
| ): | |
| raise ValueError("Human review requires comments") | |
| decision = review.get("decision") | |
| if not isinstance(decision, str) or decision not in { | |
| "supports", | |
| "refutes", | |
| "inconclusive", | |
| }: | |
| raise ValueError("Human review decision is invalid") | |
| merged_verification = dict(verification or {}) | |
| merged_verification["source_freeze_sha"] = source_freeze_sha | |
| merged_verification["human_review"] = review | |
| return self.evaluate_experiment( | |
| experiment_id=experiment_id, | |
| claim_id=claim_id, | |
| hypothesis_id=hypothesis_id, | |
| result_summary=result_summary, | |
| effect_size=effect_size, | |
| evidence_mode=evidence_mode, | |
| verification=merged_verification, | |
| status=decision, | |
| limitations=limitations, | |
| ) | |
| def evaluate_experiment( | |
| self, | |
| experiment_id: str, | |
| claim_id: str, | |
| hypothesis_id: str, | |
| result_summary: str, | |
| effect_size: dict[str, Any] | None = None, | |
| statistical_significance: dict[str, Any] | None = None, | |
| evidence_mode: str = "observational_experiment", | |
| verification: dict[str, Any] | None = None, | |
| status: str = "inconclusive", | |
| limitations: str | None = None, | |
| ) -> str: | |
| """ | |
| Evaluate an experiment and create an evidence entry. | |
| Args: | |
| experiment_id: ID of the experiment to evaluate. | |
| claim_id: ID of the claim to link evidence to. | |
| hypothesis_id: ID of the hypothesis to link evidence to. | |
| result_summary: Summary of the experimental result. | |
| effect_size: Optional effect size metrics. | |
| statistical_significance: Optional statistical significance data. | |
| status: Evidence status (supports | refutes | inconclusive). | |
| limitations: Optional limitations text. | |
| Returns: | |
| The evidence ID. | |
| Raises: | |
| ValueError: If the experiment is not scientifically valid | |
| (template, not_started, running, failed, or invalid). | |
| """ | |
| if evidence_mode not in _VALID_EVIDENCE_MODES: | |
| raise ValueError(f"Unknown evidence mode: {evidence_mode!r}") | |
| if evidence_mode == "deterministic_verification" and statistical_significance: | |
| raise ValueError( | |
| "Deterministic verification must not claim statistical significance." | |
| ) | |
| # Phase 1: Reject scientifically invalid experiments | |
| manifest = _check_experiment_valid(experiment_id) | |
| if manifest is None: | |
| raise ValueError( | |
| f"Cannot create evidence from experiment {experiment_id}: " | |
| "experiment is not scientifically valid. " | |
| "Only 'completed' experiments with valid execution may " | |
| "produce evidence." | |
| ) | |
| evidence_id = _next_evidence_id() | |
| # Include experiment validity info in evidence record | |
| validity = manifest.get("validity", {}) | |
| evidence: dict[str, Any] = { | |
| "evidence_id": evidence_id, | |
| "experiment_id": experiment_id, | |
| "claim_id": claim_id, | |
| "hypothesis_id": hypothesis_id, | |
| "result_summary": result_summary, | |
| "effect_size": effect_size or {}, | |
| "statistical_significance": statistical_significance or {}, | |
| "evidence_mode": evidence_mode, | |
| "verification": verification or {}, | |
| "status": status, # supports | refutes | inconclusive | pending | |
| "limitations": limitations or "", | |
| "artifacts": {"figures": [], "data_files": []}, | |
| "generated": datetime.now().isoformat(), | |
| "experiment_validity": { | |
| "valid": validity.get("valid", True), | |
| "runtime_error_count": validity.get("runtime_error_count", 0), | |
| "fatal_error_count": validity.get("fatal_error_count", 0), | |
| }, | |
| } | |
| # Save evidence | |
| evidence_path = EVIDENCE_DIR / f"{evidence_id}.json" | |
| with open(evidence_path, "w", encoding="utf-8") as f: | |
| json.dump(evidence, f, indent=2, ensure_ascii=False) | |
| # Update claim | |
| claim = self.registry.claims.get(claim_id) | |
| if claim: | |
| claim.evidence.append(evidence_id) | |
| claim.experiments.append(experiment_id) | |
| self._update_claim_status(claim) | |
| self.registry.save_claims() | |
| # Update hypothesis | |
| hypothesis = self.registry.hypotheses.get(hypothesis_id) | |
| if hypothesis: | |
| hypothesis.evidence.append(evidence_id) | |
| self._update_hypothesis_status(hypothesis) | |
| self.registry.save_hypotheses() | |
| self.update_research_question(claim_id, evidence_id) | |
| return evidence_id | |
| def _update_claim_status(self, claim: Claim) -> None: | |
| """Re-evaluate claim status based on all accumulated evidence.""" | |
| if not claim.evidence: | |
| claim.status = "untested" | |
| claim.confidence = "none" | |
| return | |
| supporting = 0 | |
| refuting = 0 | |
| inconclusive = 0 | |
| for evid_id in claim.evidence: | |
| evid = self._load_evidence(evid_id) | |
| if evid: | |
| s = evid.get("status", "inconclusive") | |
| if s == "supports": | |
| supporting += 1 | |
| elif s == "refutes": | |
| refuting += 1 | |
| else: | |
| inconclusive += 1 | |
| total = supporting + refuting + inconclusive | |
| # Check minimum runs requirement | |
| if total < claim.minimum_runs: | |
| claim.status = "inconclusive" | |
| claim.confidence = "low" | |
| return | |
| # Decision logic | |
| if supporting >= 2 * refuting and supporting >= 3 and supporting > total * 0.7: | |
| claim.status = "supported" | |
| claim.confidence = "high" if supporting >= 10 else "medium" | |
| elif refuting >= 2 * supporting and refuting >= 3: | |
| claim.status = "refuted" | |
| claim.confidence = "high" if refuting >= 10 else "medium" | |
| elif supporting > refuting: | |
| claim.status = "inconclusive" | |
| claim.confidence = "low" | |
| else: | |
| claim.status = "inconclusive" | |
| claim.confidence = "none" | |
| def _update_hypothesis_status(self, hypothesis: Hypothesis) -> None: | |
| """Simple hypothesis status based on linked evidence.""" | |
| if not hypothesis.evidence: | |
| hypothesis.status = "untested" | |
| return | |
| supporting = 0 | |
| refuting = 0 | |
| for evid_id in hypothesis.evidence: | |
| evid = self._load_evidence(evid_id) | |
| if evid: | |
| s = evid.get("status", "inconclusive") | |
| if s == "supports": | |
| supporting += 1 | |
| elif s == "refutes": | |
| refuting += 1 | |
| if supporting > refuting and supporting >= 2: | |
| hypothesis.status = "supported" | |
| elif refuting > supporting and refuting >= 2: | |
| hypothesis.status = "refuted" | |
| else: | |
| hypothesis.status = "inconclusive" | |
| def update_research_question(self, claim_id: str, evidence_id: str) -> None: | |
| """Reflect claim maturity in its question without authoring an answer. | |
| ``ready_for_answer`` is deliberately a review state. A human researcher | |
| must still evaluate methods and limitations before setting an answer to | |
| ``provisionally_answered`` or ``answered``. | |
| """ | |
| claim = self.registry.claims.get(claim_id) | |
| if claim is None: | |
| return | |
| question = self.registry.questions.get(claim.research_question) | |
| if question is None or question.status in {"answered", "superseded"}: | |
| return | |
| if evidence_id not in question.evidence: | |
| question.evidence.append(evidence_id) | |
| related_claims = self.registry.claims_for_question(question.id) | |
| if related_claims and all( | |
| item.status in {"supported", "refuted"} for item in related_claims | |
| ): | |
| question.status = "ready_for_answer" | |
| elif any(item.status == "inconclusive" for item in related_claims): | |
| question.status = "inconclusive" | |
| else: | |
| question.status = "in_progress" | |
| self.registry.save_questions() | |
| def _load_evidence(self, evidence_id: str) -> dict[str, Any] | None: | |
| path = EVIDENCE_DIR / f"{evidence_id}.json" | |
| if not path.exists(): | |
| return None | |
| with open(path, encoding="utf-8") as f: | |
| data: Any = json.load(f) | |
| return cast("dict[str, Any]", data) if isinstance(data, dict) else None | |
| def generate_followup_questions(self, experiment_id: str) -> list[dict[str, str]]: | |
| """ | |
| Automatically generate follow-up research questions based on | |
| experimental outcomes. Returns candidate questions for human review. | |
| """ | |
| manifest_path = EXPERIMENTS_DIR / experiment_id / "manifest.json" | |
| if not manifest_path.exists(): | |
| return [] | |
| with open(manifest_path, encoding="utf-8") as f: | |
| manifest = json.load(f) | |
| sim = manifest.get("simulation", {}) | |
| questions: list[dict[str, str]] = [] | |
| # Pattern: STDP + Homeostasis → check interaction | |
| if sim.get("learning") and sim.get("homeostasis"): | |
| questions.append( | |
| { | |
| "id": "RQ-AUTO-CANDIDATE", | |
| "question": "Ist die beobachtete Stabilität hauptsächlich durch Homeostase statt STDP verursacht?", | |
| "source": "auto_generated", | |
| "trigger": "STDP + Homeostasis co-occurrence", | |
| } | |
| ) | |
| # Pattern: Single dimension → suggest ablation | |
| dims = sim.get("dimensions", []) | |
| if len(dims) == 5: | |
| questions.append( | |
| { | |
| "id": "RQ-AUTO-CANDIDATE", | |
| "question": "Wäre der Effekt in niedrigeren Dimensionen (1D/2D/3D) derselbe?", | |
| "source": "auto_generated", | |
| "trigger": "5D experiment without lower-dimensional control", | |
| } | |
| ) | |
| return questions | |