""" Dempster's Court — The Belief Engine (M1) ========================================= Pure-Python Dempster-Shafer core. NO model, NO UI, NO arithmetic done by an LLM. This is the load-bearing, must-be-correct component. Everything is a deterministic function of mass functions over a frame (the "Dock" = set of suspects). A mass function is a dict: { frozenset(suspects): mass }, masses sum to ~1, no empty set. The full-frame subset frozenset(all suspects) represents ignorance (Theta). Implements: Dempster, Yager, PCR5, Cautious(Denoeux), discounting, Bel, Pl, conflict K. """ from itertools import combinations, product from functools import reduce # ----------------------------------------------------------------------------- helpers def all_subsets(frame): """All non-empty subsets of the frame, as frozensets.""" frame = list(frame) subs = [] for r in range(1, len(frame) + 1): for c in combinations(frame, r): subs.append(frozenset(c)) return subs def normalize(m, eps=1e-12): """Drop empty set, clamp negatives, renormalize to sum 1.""" m = {a: max(0.0, v) for a, v in m.items() if a and v > eps} s = sum(m.values()) if s <= 0: raise ValueError("mass function has no positive mass") return {a: v / s for a, v in m.items()} def make_nondogmatic(m, frame, eps=1e-6): """Ensure m(Theta) > 0 (required for the canonical decomposition).""" theta = frozenset(frame) m = dict(m) if m.get(theta, 0.0) <= 0: m[theta] = m.get(theta, 0.0) + eps return normalize(m) # ----------------------------------------------------------------------------- Bel / Pl / K def belief(m, A): A = frozenset(A) return sum(v for B, v in m.items() if B <= A) def plausibility(m, A): A = frozenset(A) return sum(v for B, v in m.items() if B & A) def conflict_K(m1, m2): return sum(v1 * v2 for (B, v1), (C, v2) in product(m1.items(), m2.items()) if not (B & C)) # ----------------------------------------------------------------------------- conjunctive core def conjunctive(m1, m2): """Unnormalized conjunctive combination; returns dict possibly with empty-set mass.""" out = {} for (B, v1), (C, v2) in product(m1.items(), m2.items()): A = B & C out[A] = out.get(A, 0.0) + v1 * v2 return out # may include frozenset() (the empty set) carrying conflict # ----------------------------------------------------------------------------- the four Methods def dempster(m1, m2): """The Zealot: normalize conflict away. Famous for manufacturing false certainty.""" q = conjunctive(m1, m2) K = q.pop(frozenset(), 0.0) if K >= 1.0 - 1e-15: raise ValueError("total conflict: Dempster undefined") return normalize({A: v / (1.0 - K) for A, v in q.items()}) def yager(m1, m2, frame): """The Agnostic: pour conflict into ignorance (Theta).""" q = conjunctive(m1, m2) empty = q.pop(frozenset(), 0.0) theta = frozenset(frame) q[theta] = q.get(theta, 0.0) + empty return normalize(q) def pcr5(m1, m2): """The Diplomat: redistribute each partial conflict back to its causes, proportionally.""" out = {} for (B, v1), (C, v2) in product(m1.items(), m2.items()): A = B & C if A: # no conflict: behaves conjunctively out[A] = out.get(A, 0.0) + v1 * v2 else: # conflict m1(B)*m2(C): split back to B and C denom = v1 + v2 if denom > 0: out[B] = out.get(B, 0.0) + (v1 * v1 * v2) / denom out[C] = out.get(C, 0.0) + (v2 * v2 * v1) / denom return normalize(out) # ---- Cautious rule (Denoeux): via canonical decomposition in commonality space ---- def commonality(m, subsets): """q(B) = sum of m(C) for all C >= B.""" return {B: sum(v for C, v in m.items() if B <= C) for B in subsets} def weights_from_mass(m, frame): """Canonical conjunctive weights w(A) for every proper subset A of the frame. ln w(A) = - sum_{B >= A} (-1)^(|B|-|A|) ln q(B). Requires q(B) > 0 (non-dogmatic).""" import math m = make_nondogmatic(m, frame) subsets = all_subsets(frame) q = commonality(m, subsets) theta = frozenset(frame) w = {} for A in subsets: if A == theta: continue # weights defined for proper subsets only acc = 0.0 for B in subsets: if A <= B: acc += ((-1) ** (len(B) - len(A))) * math.log(q[B]) w[A] = math.exp(-acc) return w def mass_from_weights(w, frame): """Recompose a mass from conjunctive weights via commonality products, then inverse Mobius. A simple BBA A^w has commonality q(B) = w if B is NOT subset of A, else 1. Conjunctive combination => product of commonalities.""" subsets = all_subsets(frame) # q(B) = product over proper subsets A of [ w(A) if not (B <= A) else 1 ] q = {} for B in subsets: prod = 1.0 for A, wA in w.items(): if not (B <= A): prod *= wA q[B] = prod # inverse Mobius: m(A) = sum_{B >= A} (-1)^(|B|-|A|) q(B) m = {} for A in subsets: acc = 0.0 for B in subsets: if A <= B: acc += ((-1) ** (len(B) - len(A))) * q[B] if acc > 1e-12: m[A] = acc return normalize(m) def cautious(m1, m2, frame): """The Skeptic: idempotent. w12(A) = min(w1(A), w2(A)). Immune to repeated evidence.""" w1 = weights_from_mass(m1, frame) w2 = weights_from_mass(m2, frame) w12 = {A: min(w1[A], w2[A]) for A in w1} return mass_from_weights(w12, frame) # ----------------------------------------------------------------------------- discounting def discount(m, alpha, frame): """Reliability discounting. alpha in [0,1]; alpha=0 keeps mass, alpha=1 -> full ignorance.""" theta = frozenset(frame) out = {A: (1 - alpha) * v for A, v in m.items() if A != theta} out[theta] = (1 - alpha) * m.get(theta, 0.0) + alpha return normalize(out) # ----------------------------------------------------------------------------- fuse a list def fuse(masses, method, frame): """Fold a list of depositions with the chosen Method (pairwise where binary).""" if not masses: raise ValueError("no depositions to fuse") if method == "dempster": return reduce(dempster, masses) if method == "yager": return reduce(lambda a, b: yager(a, b, frame), masses) if method == "pcr5": return reduce(pcr5, masses) if method == "cautious": return reduce(lambda a, b: cautious(a, b, frame), masses) raise ValueError(f"unknown method: {method}") def verdict(m, frame, threshold): """Decision rule: convict argmax-Belief suspect iff its Belief >= threshold.""" suspects = list(frame) bels = {s: belief(m, {s}) for s in suspects} top = max(bels, key=bels.get) if bels[top] >= threshold: return ("CONVICT", top, bels[top]) return ("COLD_CASE", None, bels[top]) def report(m, frame): """Suspicion (Bel) and Shadow (Pl) per suspect — what the Fusion Bench renders.""" return {s: {"suspicion": round(belief(m, {s}), 4), "shadow": round(plausibility(m, {s}), 4)} for s in frame}