Download match.py from graziul/differance-engine: direct link, hf CLI and curl.
- Browser
- Download file 41.4 kB
-
https://huggingface.co/graziul/differance-engine/resolve/main/match.py
- Command line
-
hf download hf://graziul/differance-engine/match.py
-
curl -L -o match.py https://huggingface.co/graziul/differance-engine/resolve/main/match.py
41.4 kB
| """ | |
| Compositional matching engine. | |
| Takes extraction output (concepts with operational signatures) and matches | |
| against the formalism knowledge base using three strategies: | |
| 1. DIRECT MATCH: signature matches a known formalism exactly → "≡ X" | |
| 2. COMPOSITIONAL MATCH: signature = compose(f₁, f₂, ...) → "≡ X ∘ Y" | |
| 3. ANALOGY MATCH: same meso/macro type as known formalism → "≈ X (Δ: ...)" | |
| Composition is type-checked: rules specify input/output signatures, and the | |
| engine verifies that each rule's input constraints are satisfied before | |
| applying it. The result is a valid composition tree, not just a bag of rules. | |
| Returns UNKNOWN when no match found. Returns CONFUSED when the concept's | |
| own operations are internally contradictory. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import re | |
| from collections.abc import Sequence | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| from typing import Any, Optional | |
| import yaml | |
| # --------------------------------------------------------------------------- | |
| # Type aliases (match the YAML schemas) | |
| # --------------------------------------------------------------------------- | |
| Operation = str # maximize | minimize | transform | project | decompose | sample | aggregate | match | propagate | |
| DomainType = str # vector | matrix | graph | distribution | sequence | manifold | scalar_field | set | function | latent | |
| CodomainType = str # vector | matrix | graph | distribution | sequence | scalar | embedding | assignment | latent | |
| ObjectiveFamily = str # divergence | likelihood | energy | correlation | information | none | adversarial | reconstruction | |
| MesoType = str # joint_embedding | spectral_method | energy_model | dynamical_system | ... | |
| MacroType = str # optimization | eigenvalue_problem | stochastic_process | statistical_inference | hamiltonian_system | none | |
| class Signature: | |
| """Typed operational signature extracted from a concept or formalism.""" | |
| operation: Operation | None = None | |
| domain: DomainType | None = None | |
| codomain: CodomainType | None = None | |
| objective_family: ObjectiveFamily | None = None | |
| def from_concept(cls, concept: dict) -> "Signature": | |
| """Infer a typed signature from the LLM's extracted concept fields.""" | |
| sig = cls() | |
| # Infer operation from the mathematical_operation string | |
| op_text = (concept.get("mathematical_operation") or "").lower() | |
| sig.operation = _infer_operation(op_text) | |
| sig.domain = _normalize_domain(concept.get("domain")) | |
| sig.codomain = _normalize_domain(concept.get("codomain")) | |
| sig.objective_family = _infer_objective(concept.get("objective") or "", op_text) | |
| return sig | |
| def from_formalism(cls, fm: dict) -> "Signature": | |
| """Extract signature from a formalism YAML entry.""" | |
| sig_raw = fm.get("signature", {}) | |
| return cls( | |
| operation=sig_raw.get("operation"), | |
| domain=sig_raw.get("domain"), | |
| codomain=sig_raw.get("codomain"), | |
| objective_family=sig_raw.get("objective_family"), | |
| ) | |
| def matches(self, other: "Signature", *, strict: bool = True) -> float: | |
| """Return a match score 0.0–1.0 between this and another signature. | |
| strict=True: all non-None fields must match exactly; score = proportion matched. | |
| strict=False: fuzzy — operation + codomain weighted higher. | |
| """ | |
| fields = [ | |
| ("operation", 0.35), | |
| ("domain", 0.15), | |
| ("codomain", 0.30), | |
| ("objective_family", 0.20), | |
| ] | |
| total = 0.0 | |
| matched = 0.0 | |
| pairs = [] | |
| for attr, weight in fields: | |
| mine = getattr(self, attr) | |
| theirs = getattr(other, attr) | |
| pairs.append((attr, mine, theirs, weight)) | |
| if strict: | |
| # Only score on fields where both sides have a value | |
| scorable = [(a, m, t, w) for a, m, t, w in pairs if m is not None and t is not None] | |
| if not scorable: | |
| return 0.0 | |
| total = sum(w for _, _, _, w in scorable) | |
| matched = sum(w for _, m, t, w in scorable if m == t) | |
| else: | |
| total = sum(weight for _, _, _, weight in pairs) | |
| for _, mine_val, theirs_val, weight in pairs: | |
| if mine_val is None or theirs_val is None: | |
| matched += weight * 0.5 # neutral on unknown | |
| elif mine_val == theirs_val: | |
| matched += weight | |
| # else: 0 on mismatch | |
| return matched / total if total > 0 else 0.0 | |
| class Formalism: | |
| """A known formalism from the KB.""" | |
| id: str | |
| name: str | |
| year: int | None | |
| origin: str | |
| signature: Signature | |
| meso_type: MesoType | None | |
| macro_type: MacroType | None | |
| canonical_reference: str | |
| status: str | |
| def from_yaml(cls, entry: dict) -> "Formalism": | |
| return cls( | |
| id=entry["id"], | |
| name=entry["name"], | |
| year=entry.get("year"), | |
| origin=entry.get("origin", ""), | |
| signature=Signature.from_formalism(entry), | |
| meso_type=entry.get("meso_type"), | |
| macro_type=entry.get("macro_type"), | |
| canonical_reference=entry.get("canonical_reference", ""), | |
| status=entry.get("status", "seed"), | |
| ) | |
| class CompositionRule: | |
| """A composition rule from the KB.""" | |
| id: str | |
| name: str | |
| description: str | |
| decomposes_to: list[str] # formalism_ids this rule expands to | |
| input_constraints: dict[str, Any] | |
| output_signature: dict[str, Any] | |
| preserves: list[str] | |
| introduces: list[str] | |
| examples: list[str] | |
| status: str | |
| def from_yaml(cls, entry: dict) -> "CompositionRule": | |
| return cls( | |
| id=entry["id"], | |
| name=entry.get("name", entry["id"]), | |
| description=entry.get("description", ""), | |
| decomposes_to=entry.get("decomposes_to", []), | |
| input_constraints=entry.get("input_constraints", {}), | |
| output_signature=entry.get("output_signature", {}), | |
| preserves=entry.get("preserves", []), | |
| introduces=entry.get("introduces", []), | |
| examples=entry.get("examples", []), | |
| status=entry.get("status", "seed"), | |
| ) | |
| def accepts(self, formalism: Formalism) -> bool: | |
| """Check whether this rule can be applied to the given formalism.""" | |
| constraints = self.input_constraints | |
| if not constraints: | |
| return True # universal rule | |
| # Check specific formalism IDs | |
| req_ids = constraints.get("formalism_ids") | |
| if req_ids is not None: | |
| if isinstance(req_ids, list) and req_ids and formalism.id not in req_ids: | |
| return False | |
| # Check meso types | |
| req_meso = constraints.get("meso_types") | |
| if req_meso is not None: | |
| if isinstance(req_meso, list) and req_meso: | |
| if formalism.meso_type not in req_meso: | |
| return False | |
| # Check macro types | |
| req_macro = constraints.get("macro_types") | |
| if req_macro is not None: | |
| if isinstance(req_macro, list) and req_macro: | |
| if formalism.macro_type not in req_macro: | |
| return False | |
| return True | |
| class CompositionNode: | |
| """A node in a composition tree: formalism + list of applied rules.""" | |
| formalism: Formalism | |
| rules: list[CompositionRule] = field(default_factory=list) | |
| def name(self) -> str: | |
| if not self.rules: | |
| return self.formalism.name | |
| rule_names = " ∘ ".join(r.name for r in self.rules) | |
| return f"{self.formalism.name} ∘ {rule_names}" | |
| def is_direct(self) -> bool: | |
| return len(self.rules) == 0 | |
| class MatchResult: | |
| """The result of matching a concept against the KB.""" | |
| concept_name: str | |
| result_type: str # "identity" | "compositional" | "analogy" | "unknown" | "confused" | |
| reduction: str # e.g., "CCA ∘ neuralize ∘ predict_in_codomain" | |
| reduction_expanded: str # e.g., "CCA ∘ Gradient Descent ∘ CCA" (rules expanded) | |
| canonical_analog: str # e.g., "Kernel CCA (Bach & Jordan, 2002)" | |
| genuine_delta: str # what's actually new, if anything | |
| micro: str # fine-grained: what operation happens at the lowest level | |
| meso: str # mid-level: what structural family this belongs to | |
| macro: str # top-level: what grand tradition this sits in | |
| confidence: float | |
| nodes: list[CompositionNode] = field(default_factory=list) | |
| match_scores: list[float] = field(default_factory=list) | |
| notes: list[str] = field(default_factory=list) | |
| def display(self) -> str: | |
| """Sous rature display: ~~AI Term~~ → Mathematical Operation""" | |
| if self.result_type == "identity": | |
| return f"~~{self.concept_name}~~ ≡ {self.reduction}" | |
| elif self.result_type == "compositional": | |
| base = f"~~{self.concept_name}~~ ≡ {self.reduction}" | |
| if self.reduction_expanded and self.reduction_expanded != self.reduction: | |
| base += f"\x00EXPAND\x00{self.reduction_expanded}\x00/EXPAND\x00" | |
| return base | |
| elif self.result_type == "analogy": | |
| return f"~~{self.concept_name}~~ ≈ {self.reduction} (Δ: {self.genuine_delta})" | |
| elif self.result_type == "confused": | |
| return f"~~{self.concept_name}~~ → CONFUSED: {self.notes[0] if self.notes else 'terminology overload'}" | |
| else: | |
| return f"~~{self.concept_name}~~ → UNKNOWN" | |
| # --------------------------------------------------------------------------- | |
| # KB loading | |
| # --------------------------------------------------------------------------- | |
| def _kb_dir() -> Path: | |
| return Path(__file__).resolve().parent / "kb" | |
| def load_formalisms(path: Path | None = None) -> list[Formalism]: | |
| """Load the formalism KB.""" | |
| if path is None: | |
| path = _kb_dir() / "formalisms.yaml" | |
| with open(path) as f: | |
| data = yaml.safe_load(f) | |
| return [Formalism.from_yaml(e) for e in data.get("formalisms", [])] | |
| def load_composition_rules(path: Path | None = None) -> list[CompositionRule]: | |
| """Load the composition rules KB.""" | |
| if path is None: | |
| path = _kb_dir() / "composition_rules.yaml" | |
| with open(path) as f: | |
| data = yaml.safe_load(f) | |
| return [CompositionRule.from_yaml(e) for e in data.get("composition_rules", [])] | |
| # --------------------------------------------------------------------------- | |
| # Signature inference helpers (parse LLM output into typed fields) | |
| # --------------------------------------------------------------------------- | |
| _OP_PATTERNS: list[tuple[str, str]] = [ | |
| (r"\b(minimi[zs]e|minimi[zs]ation|minimi[zs]ing)\b", "minimize"), | |
| (r"\b(maximi[zs]e|maximi[zs]ation|maximi[zs]ing)\b", "maximize"), | |
| (r"\b(project|projection|projecting)\b", "project"), | |
| (r"\b(decompose|decomposition|factorize|factorization|eigen)\b", "decompose"), | |
| (r"\b(sample|sampling|generate|generating|generative)\b", "sample"), | |
| (r"\b(aggregate|aggregation|weighted\s+sum|pooling)\b", "aggregate"), | |
| (r"\b(transform|transformations?|map|mapping)\b", "transform"), | |
| (r"\b(match|matching|align|alignment)\b", "match"), | |
| (r"\b(propagat|diffuse|random\s+walk)\b", "propagate"), | |
| ] | |
| def _infer_operation(text: str) -> Operation | None: | |
| text_lower = text.lower() | |
| for pattern, op in _OP_PATTERNS: | |
| if re.search(pattern, text_lower): | |
| return op | |
| return None | |
| _DOMAIN_MAP: dict[str, DomainType] = { | |
| "vector": "vector", "vectors": "vector", "embedding": "vector", | |
| "matrix": "matrix", "matrices": "matrix", | |
| "graph": "graph", | |
| "distribution": "distribution", "probability": "distribution", | |
| "sequence": "sequence", "token": "sequence", "time series": "sequence", | |
| "manifold": "manifold", | |
| "set": "set", | |
| "function": "function", "scalar field": "scalar_field", | |
| "latent": "latent", "latent space": "latent", | |
| } | |
| def _normalize_domain(text: str | None) -> DomainType | None: | |
| if not text: | |
| return None | |
| t = text.strip().lower() | |
| # Try exact match first | |
| for key, val in _DOMAIN_MAP.items(): | |
| if key in t: | |
| return val | |
| return t # pass through — might be a valid value we just don't have mapped | |
| _OBJ_PATTERNS: list[tuple[str, ObjectiveFamily]] = [ | |
| (r"\b(kl\b|kullback|divergence|kl\s*divergence)\b", "divergence"), | |
| (r"\b(likelihood|log\s*likelihood|mle|maximum\s*likelihood)\b", "likelihood"), | |
| (r"\b(energy|free\s*energy|hamiltonian)\b", "energy"), | |
| (r"\b(correlation|canonical\s*correlation|cca|cross.correlation)\b", "correlation"), | |
| (r"\b(mutual\s*information|mi\b|infonce|information\s*max)\b", "information"), | |
| (r"\b(adversarial|minimax|min.max|gan\b|discriminator)\b", "adversarial"), | |
| (r"\b(reconstruction|autoencod|encode.decode|mse\b|squared\s*error)\b", "reconstruction"), | |
| ] | |
| def _infer_objective(obj_text: str, op_text: str) -> ObjectiveFamily | None: | |
| combined = (obj_text + " " + op_text).lower() | |
| for pattern, obj in _OBJ_PATTERNS: | |
| if re.search(pattern, combined): | |
| return obj | |
| return None | |
| def _infer_meso_type(sig: Signature, concept: dict) -> MesoType | None: | |
| """Infer meso-type from signature and concept text.""" | |
| text = ( | |
| f"{concept.get('mathematical_operation', '')} " | |
| f"{concept.get('canonical_analog', '')}" | |
| ).lower() | |
| if any(w in text for w in ("kernel", "rkhs", "nyström", "nystrom")): | |
| return "kernel_method" | |
| if any(w in text for w in ("spectral", "eigen", "laplacian", "fourier")): | |
| return "spectral_method" | |
| if any(w in text for w in ("energy", "free energy", "boltzmann", "hamiltonian")): | |
| return "energy_model" | |
| if any(w in text for w in ("diffusion", "sde", "langevin", "score-based", "ddpm")): | |
| return "diffusion_process" | |
| if any(w in text for w in ("variational", "elbo", "vi ")): | |
| return "variational" | |
| if any(w in text for w in ("optimal transport", "wasserstein", "sinkhorn")): | |
| return "optimal_transport" | |
| if any(w in text for w in ("contrastive", "siamese", "infonce")): | |
| return "joint_embedding" | |
| if any(w in text for w in ("gan", "adversarial", "minimax", "generator")): | |
| return "game_theoretic" | |
| if any(w in text for w in ("mean field", "mean-field")): | |
| return "mean_field" | |
| if any(w in text for w in ("joint embedding", "multi.view", "multiview", "cca")): | |
| return "joint_embedding" | |
| if any(w in text for w in ("pca", "projection", "linear", "svd")): | |
| return "linear_projection" | |
| if any(w in text for w in ("spin", "ising", "hopfield")): | |
| return "spin_system" | |
| return None | |
| # --------------------------------------------------------------------------- | |
| # Matching engine | |
| # --------------------------------------------------------------------------- | |
| # Keyword → rule triggers for canonical analog path. | |
| # When the LLM says "this is essentially X," but the concept text | |
| # describes specific modifications, these keyword sets determine which | |
| # rules describe the paper's actual delta from the canonical analog. | |
| _RULE_KEYWORDS: dict[str, list[str]] = { | |
| "neuralize": [ | |
| "learned", "learnable", "deep", "encoder", "neural", "network", | |
| "parameterized", "differentiable", "end-to-end", "trained", "φ_θ", | |
| "f_θ", "g_θ", "dnn", "backprop", | |
| ], | |
| "predict_in_codomain": [ | |
| "predict", "predictive", "predicting", "prediction", | |
| "latent space", "embedding space", "representation space", | |
| "in latent", "in embedding", "codomain", | |
| "future embedding", "future representation", | |
| ], | |
| "contrastivize": [ | |
| "contrastive", "contrastively", "positive pair", "negative pair", | |
| "infonce", "noise contrastive", "nce", | |
| ], | |
| "diffuse": [ | |
| "diffusion", "denoising", "denoise", "score-based", | |
| "reverse process", "forward process", "sde", "ddpm", | |
| ], | |
| "adversarize": [ | |
| "adversarial", "gan", "discriminator", "generator", | |
| "minimax", "min-max", | |
| ], | |
| "variational_bound": [ | |
| "variational", "elbo", "vae", "auto-encoding", "autoencoding", | |
| "amortized inference", "inference network", | |
| ], | |
| "regularize": [ | |
| "regularize", "regularization", "l1 ", "l2 ", "weight decay", | |
| "dropout", "sparsity", | |
| ], | |
| "attention_wrap": [ | |
| "attention", "self-attention", "transformer", "attend", | |
| ], | |
| } | |
| class MatchEngine: | |
| """The compositional matching engine.""" | |
| formalisms: list[Formalism] | |
| rules: list[CompositionRule] | |
| config: dict = field(default_factory=dict) | |
| # Indexes for fast lookup | |
| _by_id: dict[str, Formalism] = field(default_factory=dict) | |
| _by_meso: dict[MesoType, list[Formalism]] = field(default_factory=dict) | |
| _by_macro: dict[MacroType, list[Formalism]] = field(default_factory=dict) | |
| _by_name: dict[str, Formalism] = field(default_factory=dict) # fuzzy name index | |
| _name_tokens: dict[str, list[Formalism]] = field(default_factory=dict) | |
| def __post_init__(self): | |
| self._build_indexes() | |
| def _build_indexes(self): | |
| for fm in self.formalisms: | |
| self._by_id[fm.id] = fm | |
| if fm.meso_type: | |
| self._by_meso.setdefault(fm.meso_type, []).append(fm) | |
| if fm.macro_type: | |
| self._by_macro.setdefault(fm.macro_type, []).append(fm) | |
| # Name index: lowercase the name and each token | |
| name_lower = fm.name.lower() | |
| self._by_name[name_lower] = fm | |
| for token in name_lower.replace("(", "").replace(")", "").replace("/", " ").split(): | |
| token = token.strip().rstrip(".,;:") | |
| if len(token) >= 3: | |
| self._name_tokens.setdefault(token, []).append(fm) | |
| # ---- Canonical analog resolution ---- | |
| def _resolve_canonical_analog(self, analog_text: str) -> Formalism | None: | |
| """Parse the LLM's canonical_analog field and find the matching formalism. | |
| Handles formats like: | |
| - "Kernel CCA (Bach & Jordan, 2002)" | |
| - "Kernel Canonical Correlation Analysis" | |
| - "CCA — Bach & Jordan 2002" | |
| """ | |
| if not analog_text: | |
| return None | |
| text_lower = analog_text.lower().strip() | |
| # 1. Exact name match | |
| if text_lower in self._by_name: | |
| return self._by_name[text_lower] | |
| # 2. Try stripping parenthetical citations | |
| no_parens = re.sub(r"\([^)]*\)", "", text_lower).strip() | |
| if no_parens in self._by_name: | |
| return self._by_name[no_parens] | |
| # 3. Token intersection scoring | |
| tokens = set(t.strip().rstrip(".,;:") for t in no_parens.replace("/", " ").split() if len(t.strip()) >= 3) | |
| if not tokens: | |
| return None | |
| scored: list[tuple[int, Formalism]] = [] | |
| for fm in self.formalisms: | |
| fm_tokens = set(t.strip().rstrip(".,;:") for t in fm.name.lower().replace("(", "").replace(")", "").replace("/", " ").split() if len(t.strip()) >= 3) | |
| intersection = tokens & fm_tokens | |
| if intersection: | |
| scored.append((len(intersection), fm)) | |
| if scored: | |
| scored.sort(key=lambda x: x[0], reverse=True) | |
| if scored[0][0] >= 2: | |
| return scored[0][1] | |
| # Single-token match only if the matched token is distinctive | |
| top_token = max(tokens, key=len) if tokens else "" | |
| for score, fm in scored: | |
| if score >= 1 and len(top_token) >= 4: # e.g., "canonical", "correlation" | |
| return fm | |
| return None | |
| # ---- Main entry point ---- | |
| def match_concept(self, concept: dict) -> MatchResult: | |
| """Match a single extracted concept against the KB. | |
| Returns a MatchResult with the best available decomposition. | |
| """ | |
| name = concept.get("name", "unknown") | |
| sig = Signature.from_concept(concept) | |
| meso = _infer_meso_type(sig, concept) | |
| # Route based on concept flags from extraction | |
| flags = concept.get("flags", []) or [] | |
| if "cannot_determine_from_abstract" in flags: | |
| return MatchResult( | |
| concept_name=name, | |
| result_type="unknown", | |
| reduction="cannot determine from abstract", | |
| reduction_expanded="cannot determine from abstract", | |
| canonical_analog="", | |
| genuine_delta="", | |
| micro=concept.get("mathematical_operation", ""), | |
| meso=meso or "unknown", | |
| macro="unknown", | |
| confidence=0.0, | |
| notes=["LLM extraction flagged: cannot determine from abstract"], | |
| ) | |
| if "terminology_overload" in flags or "claim_operation_mismatch" in flags: | |
| pass # Still attempt match but note flags | |
| # Strategy 0: Canonical analog from LLM extraction (highest-weight signal) | |
| canonical_analog_text = concept.get("canonical_analog", "") or "" | |
| base_fm = self._resolve_canonical_analog(canonical_analog_text) | |
| if base_fm is not None: | |
| # The LLM says this is essentially X. Now find rules that account | |
| # for what makes it "novel" beyond X. | |
| rules = self._find_rules_to_match(base_fm, sig, concept) | |
| if not rules: | |
| # No rules needed — the LLM-identified analog is the answer | |
| return MatchResult( | |
| concept_name=name, | |
| result_type="identity", | |
| reduction=base_fm.name, | |
| reduction_expanded=base_fm.name, | |
| canonical_analog=f"{base_fm.name} ({base_fm.canonical_reference})", | |
| genuine_delta="LLM-identified rebranding of known formalism", | |
| micro=f"{base_fm.signature.operation}({base_fm.signature.domain} → {base_fm.signature.codomain})", | |
| meso=base_fm.meso_type or "none", | |
| macro=base_fm.macro_type or "none", | |
| confidence=0.85, # LLM identification is high-confidence | |
| nodes=[CompositionNode(formalism=base_fm)], | |
| match_scores=[0.85], | |
| notes=["matched via LLM canonical_analog field"], | |
| ) | |
| else: | |
| # Rules account for the delta from the canonical analog | |
| rule_names = " ∘ ".join(r.name for r in rules) | |
| expanded_fms = self.expand_rules(rules) | |
| expanded = base_fm.name + " ∘ " + " ∘ ".join(expanded_fms) | |
| return MatchResult( | |
| concept_name=name, | |
| result_type="compositional", | |
| reduction=f"{base_fm.name} ∘ {rule_names}", | |
| reduction_expanded=expanded, | |
| canonical_analog=f"{base_fm.name} ({base_fm.canonical_reference})", | |
| genuine_delta=" ∘ ".join(r.name for r in rules), | |
| micro=f"{base_fm.signature.operation}({base_fm.signature.domain} → {base_fm.signature.codomain})", | |
| meso=base_fm.meso_type or "none", | |
| macro=base_fm.macro_type or "none", | |
| confidence=0.80, | |
| nodes=[CompositionNode(formalism=base_fm, rules=rules)], | |
| match_scores=[0.80], | |
| notes=["matched via LLM canonical_analog with rule delta"], | |
| ) | |
| # Strategy 1: Direct identity match (signature only) | |
| direct = self._match_direct(sig) | |
| if direct and direct[1] >= 0.85: | |
| fm, score = direct | |
| return MatchResult( | |
| concept_name=name, | |
| result_type="identity", | |
| reduction=fm.name, | |
| reduction_expanded=fm.name, | |
| canonical_analog=f"{fm.name} ({fm.canonical_reference})", | |
| genuine_delta="none — this is a direct rebranding", | |
| micro=f"exactly {fm.name}: {fm.signature.operation}({fm.signature.domain} → {fm.signature.codomain})", | |
| meso=fm.meso_type or "none", | |
| macro=fm.macro_type or "none", | |
| confidence=score, | |
| nodes=[CompositionNode(formalism=fm)], | |
| match_scores=[score], | |
| ) | |
| # Strategy 2: Compositional match | |
| comp = self._match_compositional(sig, meso) | |
| if comp and comp.confidence >= 0.5: | |
| return comp | |
| # Strategy 3: Analogy match | |
| analogy = self._match_analogy(sig, meso) | |
| if analogy and analogy.confidence >= 0.4: | |
| return analogy | |
| # Give up | |
| return MatchResult( | |
| concept_name=name, | |
| result_type="unknown", | |
| reduction="no match in KB", | |
| reduction_expanded="no match in KB", | |
| canonical_analog="", | |
| genuine_delta="", | |
| micro=concept.get("mathematical_operation", ""), | |
| meso=meso or "unknown", | |
| macro="unknown", | |
| confidence=0.0, | |
| notes=["concept signature does not match any formalism or valid composition"], | |
| ) | |
| # ---- Strategy 1: Direct match ---- | |
| def _match_direct(self, sig: Signature) -> tuple[Formalism, float] | None: | |
| best: tuple[Formalism, float] | None = None | |
| best_score = 0.0 | |
| for fm in self.formalisms: | |
| score = sig.matches(fm.signature, strict=True) | |
| if score > best_score: | |
| best_score = score | |
| best = (fm, score) | |
| if best and best_score >= 0.5: | |
| return best | |
| return None | |
| # ---- Rule expansion ---- | |
| def expand_rules(self, rules: list[CompositionRule]) -> list[str]: | |
| """Recursively expand composition rules into their constituent formalism names. | |
| Each rule's decomposes_to field lists formalism IDs it's composed of. | |
| This method looks up those formalisms by ID and returns their display names, | |
| giving the full decomposition chain beneath what appears as a single rule. | |
| """ | |
| names: list[str] = [] | |
| for rule in rules: | |
| expanded = False | |
| for fm_id in rule.decomposes_to: | |
| fm = self._by_id.get(fm_id) | |
| if fm: | |
| names.append(fm.name) | |
| expanded = True | |
| if not expanded: | |
| # Rule has no decomposition — use the rule name itself | |
| names.append(rule.name) | |
| return names | |
| # ---- Rule-to-target matching (for canonical analog case) ---- | |
| # _RULE_KEYWORDS is a module-level constant; see below. | |
| # Keyword → rule triggers for canonical analog path. | |
| def _find_rules_to_match( | |
| self, | |
| base_fm: Formalism, | |
| target_sig: Signature, | |
| concept: dict | None = None, | |
| ) -> list[CompositionRule]: | |
| """Find composition rules that describe the paper's delta from the | |
| canonical analog. Uses two strategies: | |
| 1. Signature-distance minimization (greedy, depth ≤ 2) | |
| 2. Keyword-triggered rules from concept text (when LLM has already | |
| identified the base formalism — the keywords describe what the | |
| paper actually changed) | |
| """ | |
| base_sig = base_fm.signature | |
| base_dist = 1.0 - base_sig.matches(target_sig, strict=False) | |
| # Strategy A: Signature-distance minimization | |
| best_rules: list[CompositionRule] = [] | |
| best_dist = base_dist | |
| for rule in self.rules: | |
| if not rule.accepts(base_fm): | |
| continue | |
| transformed = self._apply_rule_signature(base_sig, rule) | |
| dist = 1.0 - transformed.matches(target_sig, strict=False) | |
| if dist < best_dist: | |
| best_dist = dist | |
| best_rules = [rule] | |
| for rule1 in self.rules: | |
| if not rule1.accepts(base_fm): | |
| continue | |
| inter = self._apply_rule_signature(base_sig, rule1) | |
| for rule2 in self.rules: | |
| if rule2 is rule1: | |
| continue | |
| if not self._rule_accepts_signature(rule2, inter): | |
| continue | |
| transformed = self._apply_rule_signature(inter, rule2) | |
| dist = 1.0 - transformed.matches(target_sig, strict=False) | |
| if dist < best_dist: | |
| best_dist = dist | |
| best_rules = [rule1, rule2] | |
| sig_improved = best_dist < base_dist - 0.05 | |
| # Strategy B: Keyword-triggered rules from concept text | |
| if concept is not None: | |
| text = " ".join([ | |
| concept.get("mathematical_operation") or "", | |
| concept.get("objective") or "", | |
| concept.get("claimed_novelty_text") or "", | |
| concept.get("confidence_rationale") or "", | |
| ]).lower() | |
| keyword_rules: list[CompositionRule] = [] | |
| for rule in self.rules: | |
| if rule in best_rules: | |
| continue | |
| keywords = _RULE_KEYWORDS.get(rule.id, []) | |
| if any(kw in text for kw in keywords): | |
| if rule.accepts(base_fm) or not rule.input_constraints: | |
| keyword_rules.append(rule) | |
| # Merge: signature-driven rules first, then keyword rules | |
| # that don't duplicate. Prefer keyword rules when the LLM | |
| # has identified the base (they're semantically richer). | |
| if keyword_rules: | |
| if sig_improved: | |
| # Both strategies agree — merge, deduplicate | |
| merged = list(best_rules) | |
| for kr in keyword_rules: | |
| if kr not in merged: | |
| merged.append(kr) | |
| return merged | |
| else: | |
| # Only keyword strategy fires — use those | |
| return keyword_rules | |
| if sig_improved: | |
| return best_rules | |
| return [] | |
| # ---- Strategy 2: Compositional match ---- | |
| def _match_compositional(self, sig: Signature, meso: MesoType | None) -> MatchResult | None: | |
| """Try to decompose the concept as formalism + composition rules. | |
| For each formalism whose signature is close, see if applying | |
| available rules transforms it toward the concept's signature. | |
| """ | |
| candidates: list[tuple[Formalism, list[CompositionRule], float]] = [] | |
| # For each formalism that could be a base | |
| for fm in self.formalisms: | |
| for rule in self.rules: | |
| if not rule.accepts(fm): | |
| continue | |
| # Apply rule conceptually and score | |
| composed_sig = self._apply_rule_signature(fm.signature, rule) | |
| score = sig.matches(composed_sig, strict=True) | |
| if score >= 0.5: | |
| candidates.append((fm, [rule], score)) | |
| # Try two-rule compositions | |
| for rule2 in self.rules: | |
| if rule2 is rule: | |
| continue | |
| # Check if rule2 accepts the output type of rule1 | |
| intermediate = self._apply_rule_signature(fm.signature, rule) | |
| if not self._rule_accepts_signature(rule2, intermediate): | |
| continue | |
| composed2 = self._apply_rule_signature(intermediate, rule2) | |
| score2 = sig.matches(composed2, strict=True) | |
| if score2 >= 0.5: | |
| candidates.append((fm, [rule, rule2], score2)) | |
| if not candidates: | |
| return None | |
| # Pick best | |
| best_fm, best_rules, best_score = max(candidates, key=lambda c: c[2]) | |
| rule_names = " ∘ ".join(r.name for r in best_rules) | |
| reduction = f"{best_fm.name} ∘ {rule_names}" | |
| expanded_fms = self.expand_rules(best_rules) | |
| expanded = best_fm.name + " ∘ " + " ∘ ".join(expanded_fms) | |
| return MatchResult( | |
| concept_name="", | |
| result_type="compositional", | |
| reduction=reduction, | |
| reduction_expanded=expanded, | |
| canonical_analog=f"{best_fm.name} ({best_fm.canonical_reference})", | |
| genuine_delta=" ∘ ".join(r.name for r in best_rules), | |
| micro=f"{best_fm.signature.operation}({best_fm.signature.domain} → {best_fm.signature.codomain})", | |
| meso=best_fm.meso_type or "none", | |
| macro=best_fm.macro_type or "none", | |
| confidence=best_score, | |
| nodes=[CompositionNode(formalism=best_fm, rules=best_rules)], | |
| match_scores=[best_score], | |
| ) | |
| def _apply_rule_signature(self, sig: Signature, rule: CompositionRule) -> Signature: | |
| """Compute the approximate output signature after applying a rule. | |
| Rules modify operation/codomain/objective_family/meso/macro. | |
| Fields not mentioned in output_signature pass through unchanged. | |
| """ | |
| out = rule.output_signature | |
| return Signature( | |
| operation=out.get("operation", sig.operation), | |
| domain=sig.domain, # domain typically preserved | |
| codomain=out.get("codomain", sig.codomain), | |
| objective_family=out.get("objective_family", sig.objective_family), | |
| ) | |
| def _rule_accepts_signature(self, rule: CompositionRule, sig: Signature) -> bool: | |
| """Check if a rule can be applied to an intermediate signature. | |
| This is a looser check than rule.accepts(formalism) since we | |
| don't have a Formalism object — we check meso/macro constraints. | |
| """ | |
| constraints = rule.input_constraints | |
| if not constraints: | |
| return True | |
| req_ids = constraints.get("formalism_ids") | |
| if req_ids is not None and isinstance(req_ids, list) and req_ids: | |
| return False # specific formalism constraint can't be satisfied by signature alone | |
| # If rule requires specific meso_types and we can't determine them, be permissive | |
| req_meso = constraints.get("meso_types") | |
| if req_meso is not None and isinstance(req_meso, list) and req_meso: | |
| # Without a formalism we can't enforce meso_type constraints tightly | |
| # For intermediate nodes, be permissive | |
| pass | |
| req_macro = constraints.get("macro_types") | |
| if req_macro is not None and isinstance(req_macro, list) and req_macro: | |
| pass # same reasoning | |
| return True | |
| # ---- Strategy 3: Analogy match ---- | |
| def _match_analogy(self, sig: Signature, meso: MesoType | None) -> MatchResult | None: | |
| """Find formalisms with the same meso/macro type but different specifics.""" | |
| if not meso: | |
| return None | |
| candidates = self._by_meso.get(meso, []) | |
| if not candidates: | |
| return None | |
| # Find the best signature match among same-meso formalisms | |
| best_score = 0.0 | |
| best_fm: Formalism | None = None | |
| for fm in candidates: | |
| score = sig.matches(fm.signature, strict=False) | |
| if score > best_score: | |
| best_score = score | |
| best_fm = fm | |
| if best_fm is None or best_score < 0.3: | |
| return None | |
| # Compute the delta: what's different? | |
| deltas: list[str] = [] | |
| if sig.operation and sig.operation != best_fm.signature.operation: | |
| deltas.append(f"operation: {best_fm.signature.operation} → {sig.operation}") | |
| if sig.codomain and sig.codomain != best_fm.signature.codomain: | |
| deltas.append(f"codomain: {best_fm.signature.codomain} → {sig.codomain}") | |
| if sig.objective_family and sig.objective_family != best_fm.signature.objective_family: | |
| deltas.append(f"objective: {best_fm.signature.objective_family} → {sig.objective_family}") | |
| delta_str = "; ".join(deltas) if deltas else "minor variation" | |
| return MatchResult( | |
| concept_name="", | |
| result_type="analogy", | |
| reduction=f"{best_fm.name}", | |
| reduction_expanded=f"{best_fm.name}", | |
| canonical_analog=f"{best_fm.name} ({best_fm.canonical_reference})", | |
| genuine_delta=delta_str, | |
| micro=f"Shares meso-type '{meso}' with {best_fm.name}", | |
| meso=meso, | |
| macro=best_fm.macro_type or "none", | |
| confidence=best_score, | |
| nodes=[CompositionNode(formalism=best_fm)], | |
| match_scores=[best_score], | |
| ) | |
| # ---- Batch matching ---- | |
| def match_paper(self, extraction: dict) -> dict: | |
| """Match all concepts extracted from a paper. | |
| Returns the extraction dict augmented with match results. | |
| """ | |
| concepts = extraction.get("concepts", []) | |
| matched = [] | |
| for concept in concepts: | |
| result = self.match_concept(concept) | |
| matched.append(result) | |
| extraction["_matches"] = [self._result_to_dict(r) for r in matched] | |
| extraction["_match_summary"] = self._summarize(matched) | |
| return extraction | |
| def _result_to_dict(self, r: MatchResult) -> dict: | |
| return { | |
| "concept_name": r.concept_name, | |
| "result_type": r.result_type, | |
| "reduction": r.reduction, | |
| "reduction_expanded": r.reduction_expanded, | |
| "canonical_analog": r.canonical_analog, | |
| "genuine_delta": r.genuine_delta, | |
| "micro": r.micro, | |
| "meso": r.meso, | |
| "macro": r.macro, | |
| "confidence": r.confidence, | |
| "display": r.display, | |
| "notes": r.notes, | |
| } | |
| def _summarize(self, results: list[MatchResult]) -> dict: | |
| identity = sum(1 for r in results if r.result_type == "identity") | |
| compositional = sum(1 for r in results if r.result_type == "compositional") | |
| analogy = sum(1 for r in results if r.result_type == "analogy") | |
| unknown = sum(1 for r in results if r.result_type == "unknown") | |
| confused = sum(1 for r in results if r.result_type == "confused") | |
| total = len(results) | |
| return { | |
| "total_concepts": total, | |
| "identity_reductions": identity, | |
| "compositional_reductions": compositional, | |
| "analogy_matches": analogy, | |
| "unknown": unknown, | |
| "confused": confused, | |
| "reduction_rate": (identity + compositional) / total if total else 0, | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Convenience: load engine from defaults | |
| # --------------------------------------------------------------------------- | |
| def load_engine( | |
| formalism_path: Path | None = None, | |
| rules_path: Path | None = None, | |
| ) -> MatchEngine: | |
| """Load the match engine from the default KB files.""" | |
| formalisms = load_formalisms(formalism_path) | |
| rules = load_composition_rules(rules_path) | |
| return MatchEngine(formalisms=formalisms, rules=rules) | |
| # --------------------------------------------------------------------------- | |
| # Self-test: JEPA canonical decomposition | |
| # --------------------------------------------------------------------------- | |
| def _test_jepa(): | |
| """Verify the canonical JEPA decomposition: CCA ∘ neuralize ∘ predict_in_codomain.""" | |
| engine = load_engine() | |
| # Simulate an extraction result for JEPA | |
| jepa_concept = { | |
| "name": "Joint Embedding Predictive Architecture", | |
| "is_claimed_novel": True, | |
| "claimed_novelty_text": "predicts representations in latent space rather than raw inputs", | |
| "mathematical_operation": "maximize mutual information between joint embeddings of x and y, then predict future embedding from past embedding in the joint space", | |
| "domain": "vector", | |
| "codomain": "vector", | |
| "objective": "maximize I(Z_x; Z_y) — mutual information between embeddings, plus prediction error in latent space", | |
| "constraints": [], | |
| "canonical_analog": "Kernel CCA (Bach & Jordan 2002)", | |
| "deconstructive_move": "binary_overturn", | |
| "confidence": "high", | |
| "confidence_rationale": "abstract explicitly describes joint embedding + prediction in latent space", | |
| "flags": [], | |
| } | |
| result = engine.match_concept(jepa_concept) | |
| print("=== JEPA Canonical Decomposition Test ===") | |
| print(f"Concept: {result.concept_name}") | |
| print(f"Result type: {result.result_type}") | |
| print(f"Reduction: {result.reduction}") | |
| print(f"Display: {result.display}") | |
| print(f"Micro: {result.micro}") | |
| print(f"Meso: {result.meso}") | |
| print(f"Macro: {result.macro}") | |
| print(f"Confidence: {result.confidence}") | |
| print(f"Canonical analog: {result.canonical_analog}") | |
| print(f"Genuine delta: {result.genuine_delta}") | |
| if result.notes: | |
| print(f"Notes: {result.notes}") | |
| print() | |
| return result | |
| if __name__ == "__main__": | |
| _test_jepa() | |