numerical.py python
192 lines 6.5 KB
Raw
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 12 days ago
1 """Sparse / block / full tensor diff for numerical arrays.
2
3 Diffs flat 1-D numerical arrays element-wise with an epsilon tolerance.
4 Floating-point values within ``schema.epsilon`` of each other are not
5 considered changed — this prevents noise from triggering spurious diffs in
6 simulation state, velocity curves, and weight matrices.
7
8 Three output modes (``schema.diff_mode``):
9
10 - ``"sparse"`` — one ``ReplaceOp`` per changed element. Best for data where
11 a small fraction of elements change (e.g. sparse gradient updates).
12 - ``"block"`` — groups adjacent changed elements into contiguous range ops.
13 Best for data where changes cluster (e.g. a section of a velocity curve
14 was edited).
15 - ``"full"`` — emits a single ``ReplaceOp`` for the entire array if any
16 element changed. Best for very large tensors where per-element ops are
17 prohibitively expensive, or when the domain only cares "did anything change?"
18
19 Public API
20 ----------
21 - :func:`diff` — ``list[float]`` × ``list[float]`` → ``StructuredDelta``.
22 """
23
24 from __future__ import annotations
25
26 import hashlib
27 import logging
28
29 from muse.core.schema import TensorSchema
30 from muse.domain import DomainOp, ReplaceOp, StructuredDelta
31
32 logger = logging.getLogger(__name__)
33
34
35 # ---------------------------------------------------------------------------
36 # Internal helpers
37 # ---------------------------------------------------------------------------
38
39
40 def _float_content_id(values: list[float]) -> str:
41 """Deterministic SHA-256 for a list of float values."""
42 payload = ",".join(f"{v:.10g}" for v in values)
43 return hashlib.sha256(payload.encode()).hexdigest()
44
45
46 def _single_content_id(value: float) -> str:
47 """Deterministic SHA-256 for a single float value."""
48 return hashlib.sha256(f"{value:.10g}".encode()).hexdigest()
49
50
51 # ---------------------------------------------------------------------------
52 # Top-level diff entry point
53 # ---------------------------------------------------------------------------
54
55
56 def diff(
57 schema: TensorSchema,
58 base: list[float],
59 target: list[float],
60 *,
61 domain: str,
62 address: str = "",
63 ) -> StructuredDelta:
64 """Diff two 1-D numerical arrays under the given ``TensorSchema``.
65
66 Length mismatches are treated as a full replacement. For equal-length
67 arrays, the ``diff_mode`` on the schema controls the output granularity.
68
69 Args:
70 schema: The ``TensorSchema`` declaring dtype, epsilon, and diff_mode.
71 base: Base (ancestor) array of float values.
72 target: Target (newer) array of float values.
73 domain: Domain tag for the returned ``StructuredDelta``.
74 address: Address prefix for generated op entries.
75
76 Returns:
77 A ``StructuredDelta`` with ``ReplaceOp`` entries for changed elements
78 and a human-readable summary.
79 """
80 eps = schema["epsilon"]
81 ops: list[DomainOp] = []
82
83 # Length mismatch → full replacement regardless of diff_mode
84 if len(base) != len(target):
85 old_cid = _float_content_id(base)
86 new_cid = _float_content_id(target)
87 ops = [
88 ReplaceOp(
89 op="replace",
90 address=address,
91 position=None,
92 old_content_id=old_cid,
93 new_content_id=new_cid,
94 old_summary=f"tensor[{len(base)}] (prev)",
95 new_summary=f"tensor[{len(target)}] (new)",
96 )
97 ]
98 return StructuredDelta(
99 domain=domain,
100 ops=ops,
101 summary=f"tensor length changed {len(base)}→{len(target)}",
102 )
103
104 # Identify changed indices. Strict `>` so that eps=0.0 means exact equality:
105 # identical values (|b-t|=0) are never flagged, while any actual difference is.
106 changed: list[int] = [
107 i for i, (b, t) in enumerate(zip(base, target)) if abs(b - t) > eps
108 ]
109
110 if not changed:
111 return StructuredDelta(
112 domain=domain, ops=[], summary="no numerical changes"
113 )
114
115 mode = schema["diff_mode"]
116
117 if mode == "full":
118 old_cid = _float_content_id(base)
119 new_cid = _float_content_id(target)
120 ops = [
121 ReplaceOp(
122 op="replace",
123 address=address,
124 position=None,
125 old_content_id=old_cid,
126 new_content_id=new_cid,
127 old_summary=f"tensor[{len(base)}] (prev)",
128 new_summary=f"tensor[{len(target)}] (new)",
129 )
130 ]
131 summary = f"{len(changed)} element{'s' if len(changed) != 1 else ''} changed"
132
133 elif mode == "sparse":
134 for i in changed:
135 ops.append(
136 ReplaceOp(
137 op="replace",
138 address=address,
139 position=i,
140 old_content_id=_single_content_id(base[i]),
141 new_content_id=_single_content_id(target[i]),
142 old_summary=f"[{i}]={base[i]:.6g}",
143 new_summary=f"[{i}]={target[i]:.6g}",
144 )
145 )
146 n = len(changed)
147 summary = f"{n} element{'s' if n != 1 else ''} changed"
148
149 else: # "block"
150 # Group adjacent changed indices into contiguous ranges
151 blocks: list[tuple[int, int]] = [] # (start, end) inclusive
152 run_start = changed[0]
153 run_end = changed[0]
154 for idx in changed[1:]:
155 if idx == run_end + 1:
156 run_end = idx
157 else:
158 blocks.append((run_start, run_end))
159 run_start = idx
160 run_end = idx
161 blocks.append((run_start, run_end))
162
163 for start, end in blocks:
164 block_base = base[start : end + 1]
165 block_target = target[start : end + 1]
166 label = f"[{start}]" if start == end else f"[{start}:{end+1}]"
167 ops.append(
168 ReplaceOp(
169 op="replace",
170 address=address,
171 position=start,
172 old_content_id=_float_content_id(block_base),
173 new_content_id=_float_content_id(block_target),
174 old_summary=f"{label} (prev)",
175 new_summary=f"{label} (new)",
176 )
177 )
178 n = len(changed)
179 summary = (
180 f"{n} element{'s' if n != 1 else ''} changed "
181 f"in {len(blocks)} block{'s' if len(blocks) != 1 else ''}"
182 )
183
184 logger.debug(
185 "numerical.diff %r mode=%r: %d changed of %d elements",
186 address,
187 mode,
188 len(changed),
189 len(base),
190 )
191
192 return StructuredDelta(domain=domain, ops=ops, summary=summary)
File History 1 commit
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 12 days ago