Matthew Ford
feat: low-evidence gating in substitution_v11 + wisemoor description provenance
cc86172 | from __future__ import annotations | |
| import hashlib | |
| import json | |
| import math | |
| from collections import Counter | |
| from pathlib import Path | |
| from typing import Any, Iterable | |
| import numpy as np | |
| PIMT_V11 = "v11" | |
| ANSWER_SHAPES = {"single_material", "any_of", "accord"} | |
| # Minimum mutually-observed scoring components for a row to count as a | |
| # trustworthy retrieval. Below this the rank is driven mostly by priors / | |
| # file order, so it is reported as low-evidence, not a model miss or hit. | |
| MIN_OBSERVED_COMPONENTS = 2 | |
| def load_jsonl(path: str | Path) -> list[dict[str, Any]]: | |
| return [json.loads(line) for line in Path(path).open(encoding="utf-8") if line.strip()] | |
| def write_jsonl(path: str | Path, rows: Iterable[dict[str, Any]]) -> None: | |
| path = Path(path) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text( | |
| "".join(json.dumps(row, sort_keys=True, ensure_ascii=False) + "\n" for row in rows), | |
| encoding="utf-8", | |
| ) | |
| def wilson_interval(successes: int, total: int, z: float = 1.959963984540054) -> dict[str, Any]: | |
| """Return count-first 95% Wilson score reporting for a binomial proportion.""" | |
| if total < 0 or successes < 0 or successes > total: | |
| raise ValueError("Wilson counts must satisfy 0 <= successes <= total") | |
| if total == 0: | |
| return {"successes": successes, "total": total, "wilson_95": None} | |
| proportion = successes / total | |
| z2 = z * z | |
| denominator = 1.0 + z2 / total | |
| center = (proportion + z2 / (2.0 * total)) / denominator | |
| half_width = z * math.sqrt( | |
| (proportion * (1.0 - proportion) + z2 / (4.0 * total)) / total | |
| ) / denominator | |
| return { | |
| "successes": successes, | |
| "total": total, | |
| "wilson_95": [max(0.0, center - half_width), min(1.0, center + half_width)], | |
| } | |
| def explicit_eval_row(row: dict[str, Any]) -> dict[str, Any]: | |
| """Upgrade a legacy one-substitute row to the explicit v11.1 answer schema.""" | |
| if "answer" in row: | |
| return row | |
| return { | |
| **row, | |
| "answer": { | |
| "shape": "single_material", | |
| "materials": [{"name": row.get("substitute"), "cas": row.get("substitute_cas")}], | |
| }, | |
| } | |
| def _answer(row: dict[str, Any]) -> tuple[str, list[dict[str, Any]]]: | |
| answer = explicit_eval_row(row)["answer"] | |
| shape = answer.get("shape") | |
| materials = answer.get("materials") or [] | |
| if shape not in ANSWER_SHAPES: | |
| raise ValueError(f"unsupported answer shape for {row.get('pair_id')}: {shape!r}") | |
| if not materials or any(not (item.get("cas") or item.get("name")) for item in materials): | |
| raise ValueError(f"answer materials missing for {row.get('pair_id')}") | |
| if shape == "single_material" and len(materials) != 1: | |
| raise ValueError(f"single_material answer must contain exactly one material: {row.get('pair_id')}") | |
| return str(shape), materials | |
| def normalize_name(value: str | None) -> str: | |
| if not value: | |
| return "" | |
| keep = [] | |
| for ch in value.lower(): | |
| keep.append(ch if ch.isalnum() else " ") | |
| return " ".join("".join(keep).split()) | |
| def profile_key(profile: dict[str, Any]) -> str: | |
| return str(profile.get("cas") or profile.get("profile_id") or normalize_name(profile.get("name"))) | |
| def descriptor_distribution_from_codes(codes: Iterable[str]) -> dict[str, float]: | |
| counts = Counter(code for code in codes if code) | |
| total = sum(counts.values()) | |
| if not total: | |
| return {} | |
| return {code: count / total for code, count in sorted(counts.items())} | |
| def cosine_similarity(left: dict[str, float], right: dict[str, float]) -> float: | |
| keys = sorted(set(left) | set(right)) | |
| if not keys: | |
| return 0.0 | |
| a = np.asarray([left.get(key, 0.0) for key in keys], dtype=float) | |
| b = np.asarray([right.get(key, 0.0) for key in keys], dtype=float) | |
| denom = float(np.linalg.norm(a) * np.linalg.norm(b)) | |
| if denom <= 0: | |
| return 0.0 | |
| return float(np.dot(a, b) / denom) | |
| def numeric_proximity(a: Any, b: Any, scale: float) -> float | None: | |
| try: | |
| fa = float(a) | |
| fb = float(b) | |
| except (TypeError, ValueError): | |
| return None | |
| if not (math.isfinite(fa) and math.isfinite(fb)): | |
| return None | |
| return float(math.exp(-abs(fa - fb) / scale)) | |
| def note_role(profile: dict[str, Any]) -> str: | |
| role = profile.get("poucher_tier") | |
| if role in {"top", "heart", "mid", "base"}: | |
| return "heart" if role == "mid" else str(role) | |
| log_vp = profile.get("log10_vapor_pressure_pa") | |
| if log_vp is None: | |
| return "unknown" | |
| try: | |
| value = float(log_vp) | |
| except (TypeError, ValueError): | |
| return "unknown" | |
| if value >= 2.0: | |
| return "top" | |
| if value <= 0.0: | |
| return "base" | |
| return "heart" | |
| def role_score(left: dict[str, Any], right: dict[str, Any]) -> float | None: | |
| l_role = note_role(left) | |
| r_role = note_role(right) | |
| if "unknown" in {l_role, r_role}: | |
| return None | |
| return 1.0 if l_role == r_role else 0.25 | |
| def profile_lookup(profiles: Iterable[dict[str, Any]]) -> dict[str, dict[str, Any]]: | |
| out: dict[str, dict[str, Any]] = {} | |
| for profile in profiles: | |
| keys = {profile_key(profile), normalize_name(profile.get("name"))} | |
| cas = profile.get("cas") | |
| if cas: | |
| keys.add(str(cas)) | |
| for alias in profile.get("aliases") or []: | |
| keys.add(normalize_name(alias)) | |
| for key in keys: | |
| if key: | |
| out[key] = profile | |
| return out | |
| MODE_WEIGHTS: dict[str, dict[str, float]] = { | |
| "odor_match": {"odor": 0.70, "substantivity": 0.10, "volatility": 0.10, "role": 0.10}, | |
| "function_match": {"odor": 0.25, "substantivity": 0.25, "volatility": 0.25, "role": 0.25}, | |
| "evaporation_match": {"odor": 0.10, "substantivity": 0.35, "volatility": 0.40, "role": 0.15}, | |
| "regulatory_match": {"odor": 0.30, "substantivity": 0.20, "volatility": 0.20, "role": 0.15, "constraints": 0.15}, | |
| "cost_match": {"odor": 0.35, "substantivity": 0.20, "volatility": 0.20, "role": 0.15, "constraints": 0.10}, | |
| "accord_rebuild": {"odor": 0.35, "substantivity": 0.20, "volatility": 0.20, "role": 0.25}, | |
| } | |
| MODE_REQUIRED_FIELDS = { | |
| "regulatory_match": ("regulatory_status", "ifra_restrictions", "allergens"), | |
| "cost_match": ("cost_tier", "price", "availability_tier"), | |
| } | |
| def mode_supported(mode: str, target: dict[str, Any], profiles: Iterable[dict[str, Any]]) -> bool: | |
| """Whether a mode has at least target and catalogue evidence for its advertised axis.""" | |
| fields = MODE_REQUIRED_FIELDS.get(mode) | |
| if not fields: | |
| return True | |
| return any(target.get(field) is not None for field in fields) and any( | |
| any(profile.get(field) is not None for field in fields) for profile in profiles | |
| ) | |
| def mode_axis_similarity(mode: str, target: dict[str, Any], candidate: dict[str, Any]) -> float | None: | |
| """Compare observed mode-specific fields; never manufacture a neutral value.""" | |
| if mode == "cost_match": | |
| numeric = numeric_proximity(target.get("price"), candidate.get("price"), 1.0) | |
| if numeric is not None: | |
| return numeric | |
| for field in ("cost_tier", "availability_tier"): | |
| if target.get(field) is not None and candidate.get(field) is not None: | |
| return 1.0 if target[field] == candidate[field] else 0.0 | |
| if mode == "regulatory_match": | |
| scores = [] | |
| if target.get("regulatory_status") is not None and candidate.get("regulatory_status") is not None: | |
| scores.append(1.0 if target["regulatory_status"] == candidate["regulatory_status"] else 0.0) | |
| for field in ("ifra_restrictions", "allergens"): | |
| left, right = set(target.get(field) or []), set(candidate.get(field) or []) | |
| if left or right: | |
| scores.append(len(left & right) / len(left | right)) | |
| if scores: | |
| return sum(scores) / len(scores) | |
| return None | |
| def score_candidate( | |
| target: dict[str, Any], | |
| candidate: dict[str, Any], | |
| mode: str = "function_match", | |
| constraints: dict[str, Any] | None = None, | |
| ) -> dict[str, Any]: | |
| constraints = constraints or {} | |
| banned = {normalize_name(v) for v in constraints.get("banned_materials", [])} | |
| banned.update(str(v) for v in constraints.get("banned_cas", [])) | |
| c_key = profile_key(candidate) | |
| if c_key == profile_key(target): | |
| return {"score": -1.0, "reasons": {"self": True}} | |
| if c_key in banned or normalize_name(candidate.get("name")) in banned: | |
| return {"score": -1.0, "reasons": {"banned": True}} | |
| target_odor = target.get("descriptor_distribution") or {} | |
| candidate_odor = candidate.get("descriptor_distribution") or {} | |
| components: dict[str, float | None] = { | |
| "odor": cosine_similarity(target_odor, candidate_odor) if target_odor and candidate_odor else None, | |
| "substantivity": numeric_proximity( | |
| target.get("substantivity_log10_predicted"), | |
| candidate.get("substantivity_log10_predicted"), | |
| 0.45, | |
| ), | |
| "volatility": numeric_proximity(target.get("log10_vapor_pressure_pa"), candidate.get("log10_vapor_pressure_pa"), 1.0), | |
| "role": role_score(target, candidate), | |
| "constraints": mode_axis_similarity(mode, target, candidate), | |
| } | |
| weights = MODE_WEIGHTS.get(mode, MODE_WEIGHTS["function_match"]) | |
| observed = {name: value for name, value in components.items() if value is not None and name in weights} | |
| observed_weight = sum(weights[name] for name in observed) | |
| score = sum(float(value) * weights[name] for name, value in observed.items()) / observed_weight if observed_weight else 0.0 | |
| return { | |
| "score": float(score), | |
| "reasons": components, | |
| "evidence_coverage": observed_weight / sum(weights.values()), | |
| "observed_components": sorted(observed), | |
| } | |
| def substitute( | |
| target: str, | |
| profiles: list[dict[str, Any]], | |
| constraints: dict[str, Any] | None = None, | |
| mode: str = "function_match", | |
| top_k: int = 5, | |
| ) -> list[dict[str, Any]]: | |
| lookup = profile_lookup(profiles) | |
| target_profile = lookup.get(str(target)) or lookup.get(normalize_name(target)) | |
| if target_profile is None: | |
| raise KeyError(f"target material not found in v11 profile table: {target}") | |
| ranked = [] | |
| for candidate in profiles: | |
| scored = score_candidate(target_profile, candidate, mode=mode, constraints=constraints) | |
| if scored["score"] < 0: | |
| continue | |
| ranked.append( | |
| { | |
| "candidate": candidate.get("name"), | |
| "candidate_cas": candidate.get("cas"), | |
| "score": scored["score"], | |
| "components": scored["reasons"], | |
| "evidence_coverage": scored["evidence_coverage"], | |
| "observed_components": scored["observed_components"], | |
| "note_role": note_role(candidate), | |
| } | |
| ) | |
| ranked.sort( | |
| key=lambda row: ( | |
| -row["score"], | |
| -row["evidence_coverage"], | |
| str(row.get("candidate_cas") or ""), | |
| normalize_name(row.get("candidate")), | |
| ) | |
| ) | |
| return ranked[:top_k] | |
| def evaluate_material(material: str, profiles: list[dict[str, Any]]) -> dict[str, Any]: | |
| lookup = profile_lookup(profiles) | |
| profile = lookup.get(str(material)) or lookup.get(normalize_name(material)) | |
| if profile is None: | |
| raise KeyError(f"material not found in v11 profile table: {material}") | |
| return { | |
| "pimt_version": PIMT_V11, | |
| "type": "material", | |
| "material": profile.get("name"), | |
| "cas": profile.get("cas"), | |
| "odor_family_fit": profile.get("descriptor_distribution") or {}, | |
| "predicted_substantivity_log10": profile.get("substantivity_log10_predicted"), | |
| "volatility": { | |
| "vapor_pressure_pa": profile.get("vapor_pressure_pa"), | |
| "log10_vapor_pressure_pa": profile.get("log10_vapor_pressure_pa"), | |
| "boiling_point_k": profile.get("boiling_point_k"), | |
| "note_role": note_role(profile), | |
| }, | |
| "substitution_risks": profile.get("substitution_risks") or [], | |
| "enrichment_pending": profile.get("enrichment_pending") or [], | |
| } | |
| def evaluate_formula(formula: list[dict[str, Any]], profiles: list[dict[str, Any]]) -> dict[str, Any]: | |
| lookup = profile_lookup(profiles) | |
| materials = [] | |
| missing = [] | |
| total_weight = 0.0 | |
| role_weights = Counter() | |
| for item in formula: | |
| name = item.get("cas") or item.get("name") | |
| profile = lookup.get(str(name)) or lookup.get(normalize_name(str(name))) | |
| if profile is None: | |
| missing.append(name) | |
| continue | |
| weight = float(item.get("weight_fraction", item.get("percent", 0.0)) or 0.0) | |
| total_weight += weight | |
| role_weights[note_role(profile)] += weight | |
| materials.append(evaluate_material(str(name), profiles) | {"input_weight": weight}) | |
| return { | |
| "pimt_version": PIMT_V11, | |
| "type": "formula", | |
| "n_materials": len(materials), | |
| "missing_materials": missing, | |
| "total_weight": total_weight, | |
| "top_heart_base_balance": dict(role_weights), | |
| "materials": materials, | |
| "substitution_risks": ["safety/use-level flags enrichment-pending; no safety claims emitted"], | |
| } | |
| def evaluate_retriever( | |
| eval_rows: list[dict[str, Any]], | |
| profiles: list[dict[str, Any]], | |
| mode: str | None = None, | |
| top_k: int = 5, | |
| ranking_strategy: str = "model", | |
| ) -> dict[str, Any]: | |
| if ranking_strategy not in {"model", "random"}: | |
| raise ValueError(f"unknown ranking strategy: {ranking_strategy}") | |
| lookup = profile_lookup(profiles) | |
| evaluated = [] | |
| excluded = [] | |
| mode_counts = Counter() | |
| for legacy_row in eval_rows: | |
| row = explicit_eval_row(legacy_row) | |
| shape, answer_materials = _answer(row) | |
| target_key = row.get("target_cas") or row.get("target") | |
| if not (lookup.get(str(target_key)) or lookup.get(normalize_name(str(target_key)))): | |
| excluded.append({"pair_id": row["pair_id"], "reason": "target_missing_profile"}) | |
| continue | |
| resolved_answers = [] | |
| missing_answers = [] | |
| for item in answer_materials: | |
| keys = [item.get("cas"), item.get("name")] | |
| profile = next( | |
| (lookup.get(str(key)) or lookup.get(normalize_name(str(key))) for key in keys if key), | |
| None, | |
| ) | |
| if profile is None: | |
| missing_answers.append(item) | |
| else: | |
| resolved_answers.append(profile) | |
| insufficient = not resolved_answers if shape == "any_of" else bool(missing_answers) | |
| if insufficient: | |
| excluded.append( | |
| { | |
| "pair_id": row["pair_id"], | |
| "reason": "answer_material_missing_profile", | |
| "missing_answer_materials": missing_answers, | |
| } | |
| ) | |
| continue | |
| row_mode = mode or row.get("mode") | |
| if row_mode not in MODE_WEIGHTS: | |
| raise ValueError(f"unsupported retrieval mode for {row['pair_id']}: {row_mode!r}") | |
| target_profile = lookup.get(str(target_key)) or lookup.get(normalize_name(str(target_key))) | |
| target_profile_key = profile_key(target_profile) | |
| answer_keys = {profile_key(profile) for profile in resolved_answers} | |
| if target_profile_key in answer_keys: | |
| excluded.append({"pair_id": row["pair_id"], "reason": "answer_resolves_to_target"}) | |
| continue | |
| if not mode_supported(row_mode, target_profile, profiles): | |
| excluded.append({"pair_id": row["pair_id"], "reason": "mode_features_unsupported", "mode": row_mode}) | |
| continue | |
| mode_counts[row_mode] += 1 | |
| if ranking_strategy == "model": | |
| ranked = substitute(str(target_key), profiles, mode=row_mode, top_k=top_k) | |
| else: | |
| candidates = [profile for profile in profiles if profile_key(profile) != target_profile_key] | |
| candidates.sort( | |
| key=lambda profile: hashlib.sha256( | |
| f"{row['pair_id']}|{profile_key(profile)}".encode("utf-8") | |
| ).digest() | |
| ) | |
| ranked = [ | |
| {"candidate": profile.get("name"), "candidate_cas": profile.get("cas")} | |
| for profile in candidates[:top_k] | |
| ] | |
| ranked_keys = { | |
| profile_key(profile) | |
| for result in ranked | |
| for profile in [ | |
| lookup.get(str(result.get("candidate_cas"))) | |
| or lookup.get(normalize_name(result.get("candidate"))) | |
| ] | |
| if profile | |
| } | |
| matched_keys = ranked_keys & answer_keys | |
| hit = answer_keys <= ranked_keys if shape == "accord" else bool(matched_keys) | |
| substitute_profile = resolved_answers[0] | |
| # Evidence floor: count mutually-observed scoring components between the | |
| # target and the documented answer. Rows under the floor are reported as | |
| # low-evidence so their rank is not read as a trustworthy model outcome. | |
| answer_scored = score_candidate(target_profile, substitute_profile, mode=row_mode) if target_profile is not None else {} | |
| answer_observed_components = len(answer_scored.get("observed_components") or []) | |
| low_evidence = answer_observed_components < MIN_OBSERVED_COMPONENTS | |
| evaluated.append( | |
| { | |
| "pair_id": row["pair_id"], | |
| "grade": row["grade"], | |
| "mode": row_mode, | |
| "answer_shape": shape, | |
| "hit_top_k": bool(hit), | |
| "top_k": ranked, | |
| "documented_answer": row["answer"], | |
| "answer_components_retrieved": len(matched_keys), | |
| "answer_components_total": len(answer_keys), | |
| "answer_observed_components": answer_observed_components, | |
| "answer_evidence_coverage": answer_scored.get("evidence_coverage"), | |
| "low_evidence": low_evidence, | |
| "unavailable_optional_answers": missing_answers if shape == "any_of" else [], | |
| "substantivity_delta_abs": ( | |
| abs( | |
| float(target_profile.get("substantivity_log10_predicted")) | |
| - float(substitute_profile.get("substantivity_log10_predicted")) | |
| ) | |
| if target_profile | |
| and substitute_profile | |
| and target_profile.get("substantivity_log10_predicted") is not None | |
| and substitute_profile.get("substantivity_log10_predicted") is not None | |
| else None | |
| ), | |
| } | |
| ) | |
| n = len(evaluated) | |
| n_hit = sum(1 for row in evaluated if row["hit_top_k"]) | |
| n_missed = n - n_hit | |
| by_mode_top_k_hit = {} | |
| for row_mode in sorted(mode_counts): | |
| mode_rows = [row for row in evaluated if row["mode"] == row_mode] | |
| by_mode_top_k_hit[row_mode] = wilson_interval( | |
| sum(1 for row in mode_rows if row["hit_top_k"]), | |
| len(mode_rows), | |
| ) | |
| subst_deltas = [row["substantivity_delta_abs"] for row in evaluated if row["substantivity_delta_abs"] is not None] | |
| n_low_evidence = sum(1 for row in evaluated if row.get("low_evidence")) | |
| # Trustworthy subset: rows meeting the evidence floor. Hits are also reported | |
| # restricted to this subset so the headline is not carried by prior-only ranks. | |
| trustworthy = [row for row in evaluated if not row.get("low_evidence")] | |
| n_trustworthy = len(trustworthy) | |
| n_trustworthy_hit = sum(1 for row in trustworthy if row["hit_top_k"]) | |
| return { | |
| "pimt_version": PIMT_V11, | |
| "mode": mode or "per-row", | |
| "mode_counts": dict(sorted(mode_counts.items())), | |
| "ranking_strategy": ranking_strategy, | |
| "top_k": top_k, | |
| "n_eval_rows": len(eval_rows), | |
| "n_evaluable": n, | |
| "n_excluded": len(excluded), | |
| "n_hit": n_hit, | |
| "n_missed": n_missed, | |
| "n_low_evidence": n_low_evidence, | |
| "n_trustworthy": n_trustworthy, | |
| "n_trustworthy_hit": n_trustworthy_hit, | |
| "top_k_hit": wilson_interval(n_hit, n), | |
| "top_k_hit_trustworthy_only": wilson_interval(n_trustworthy_hit, n_trustworthy) if n_trustworthy else None, | |
| "by_mode_top_k_hit": by_mode_top_k_hit, | |
| "mean_substantivity_delta_abs": float(np.mean(subst_deltas)) if subst_deltas else None, | |
| "evaluated": evaluated, | |
| "excluded": excluded, | |
| } | |