gabriel / muse public
numerical.py python
185 lines 6.5 KB
Raw
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be Merge branch 'fix/hub-user-read-update-path' into dev Human 9 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 import logging
25
26 from muse.core.types import blob_id
27 from muse.core.schema import TensorSchema
28 from muse.domain import DomainOp, ReplaceOp, StructuredDelta
29
30 logger = logging.getLogger(__name__)
31
32 # ---------------------------------------------------------------------------
33 # Internal helpers
34 # ---------------------------------------------------------------------------
35
36 def _float_content_id(values: list[float]) -> str:
37 """Deterministic ``sha256:``-prefixed ID for a list of float values."""
38 payload = ",".join(f"{v:.10g}" for v in values)
39 return blob_id(payload.encode())
40
41 def _single_content_id(value: float) -> str:
42 """Deterministic ``sha256:``-prefixed ID for a single float value."""
43 return blob_id(f"{value:.10g}".encode())
44
45 # ---------------------------------------------------------------------------
46 # Top-level diff entry point
47 # ---------------------------------------------------------------------------
48
49 def diff(
50 schema: TensorSchema,
51 base: list[float],
52 target: list[float],
53 *,
54 domain: str,
55 address: str = "",
56 ) -> StructuredDelta:
57 """Diff two 1-D numerical arrays under the given ``TensorSchema``.
58
59 Length mismatches are treated as a full replacement. For equal-length
60 arrays, the ``diff_mode`` on the schema controls the output granularity.
61
62 Args:
63 schema: The ``TensorSchema`` declaring dtype, epsilon, and diff_mode.
64 base: Base (ancestor) array of float values.
65 target: Target (newer) array of float values.
66 domain: Domain tag for the returned ``StructuredDelta``.
67 address: Address prefix for generated op entries.
68
69 Returns:
70 A ``StructuredDelta`` with ``ReplaceOp`` entries for changed elements
71 and a human-readable summary.
72 """
73 eps = schema["epsilon"]
74 ops: list[DomainOp] = []
75
76 # Length mismatch → full replacement regardless of diff_mode
77 if len(base) != len(target):
78 old_cid = _float_content_id(base)
79 new_cid = _float_content_id(target)
80 ops = [
81 ReplaceOp(
82 op="replace",
83 address=address,
84 position=None,
85 old_content_id=old_cid,
86 new_content_id=new_cid,
87 old_summary=f"tensor[{len(base)}] (prev)",
88 new_summary=f"tensor[{len(target)}] (new)",
89 )
90 ]
91 return StructuredDelta(
92 domain=domain,
93 ops=ops,
94 summary=f"tensor length changed {len(base)}→{len(target)}",
95 )
96
97 # Identify changed indices. Strict `>` so that eps=0.0 means exact equality:
98 # identical values (|b-t|=0) are never flagged, while any actual difference is.
99 changed: list[int] = [
100 i for i, (b, t) in enumerate(zip(base, target)) if abs(b - t) > eps
101 ]
102
103 if not changed:
104 return StructuredDelta(
105 domain=domain, ops=[], summary="no numerical changes"
106 )
107
108 mode = schema["diff_mode"]
109
110 if mode == "full":
111 old_cid = _float_content_id(base)
112 new_cid = _float_content_id(target)
113 ops = [
114 ReplaceOp(
115 op="replace",
116 address=address,
117 position=None,
118 old_content_id=old_cid,
119 new_content_id=new_cid,
120 old_summary=f"tensor[{len(base)}] (prev)",
121 new_summary=f"tensor[{len(target)}] (new)",
122 )
123 ]
124 summary = f"{len(changed)} element{'s' if len(changed) != 1 else ''} changed"
125
126 elif mode == "sparse":
127 for i in changed:
128 ops.append(
129 ReplaceOp(
130 op="replace",
131 address=address,
132 position=i,
133 old_content_id=_single_content_id(base[i]),
134 new_content_id=_single_content_id(target[i]),
135 old_summary=f"[{i}]={base[i]:.6g}",
136 new_summary=f"[{i}]={target[i]:.6g}",
137 )
138 )
139 n = len(changed)
140 summary = f"{n} element{'s' if n != 1 else ''} changed"
141
142 else: # "block"
143 # Group adjacent changed indices into contiguous ranges
144 blocks: list[tuple[int, int]] = [] # (start, end) inclusive
145 run_start = changed[0]
146 run_end = changed[0]
147 for idx in changed[1:]:
148 if idx == run_end + 1:
149 run_end = idx
150 else:
151 blocks.append((run_start, run_end))
152 run_start = idx
153 run_end = idx
154 blocks.append((run_start, run_end))
155
156 for start, end in blocks:
157 block_base = base[start : end + 1]
158 block_target = target[start : end + 1]
159 label = f"[{start}]" if start == end else f"[{start}:{end+1}]"
160 ops.append(
161 ReplaceOp(
162 op="replace",
163 address=address,
164 position=start,
165 old_content_id=_float_content_id(block_base),
166 new_content_id=_float_content_id(block_target),
167 old_summary=f"{label} (prev)",
168 new_summary=f"{label} (new)",
169 )
170 )
171 n = len(changed)
172 summary = (
173 f"{n} element{'s' if n != 1 else ''} changed "
174 f"in {len(blocks)} block{'s' if len(blocks) != 1 else ''}"
175 )
176
177 logger.debug(
178 "numerical.diff %r mode=%r: %d changed of %d elements",
179 address,
180 mode,
181 len(changed),
182 len(base),
183 )
184
185 return StructuredDelta(domain=domain, ops=ops, summary=summary)
File History 11 commits
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be Merge branch 'fix/hub-user-read-update-path' into dev Human 9 days ago
sha256:b7be56ec091919a612cffe7f3c8b600209d5155517443fdac0e16954c8c7d81f Merge 'task/git-export-ignored-file-deletion' into 'dev' — … Human 12 days ago
sha256:8de4334a98c945aace420969d389ad678aa926d4ab4e886b2ac4c4241cb3bf2b revert: keep pyproject.toml in canonical PEP 440 form Sonnet 4.6 patch 15 days ago
sha256:50bb615c573aa4b928fca75b60f82ba2a659910066507cec6a95c412ae22ccb9 docs: add issue docs for push have-negotiation bug (#55) an… Sonnet 4.6 18 days ago
sha256:d90d175cded68aae1d4ffcf4858917854195d0cd8ce1fe73cee4dbc02541cb74 chore: trigger push to surface null-OID paths Sonnet 4.6 24 days ago
sha256:e452ad9a6ace6ccc6d875a35e06caf9da5576a970c1c36133b69a891ce5fefa8 chore: prebuild timing test Sonnet 4.6 34 days ago
sha256:0008ab6695e3e064b3e236b24fd19e538fef6a588eb0d211622f4466d919c0b1 merge: pull staging/dev — advance to 0.2.0rc12 Sonnet 4.6 patch 36 days ago
sha256:9c33d61749fff814c5226d5386aa2af7064c2c02788594a25fdd709358132eea fix: _PROPOSAL_PREFIX_RESOLVE_LIMIT 200 → 100 to match hub … Sonnet 4.6 47 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 50 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 56 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 57 days ago