CharlesCNorton commited on
Commit
58a9bad
·
1 Parent(s): ca99a3e

Add gate-level calculator UI and CLI expr mode

Browse files

- Add calculator.py expression evaluator with JSON/strict output for float16 gate-only eval

- Add Gradio Space app.py + requirements.txt for proof-of-concept UI

- Update README with Space notes and expanded TODO/caveats

Files changed (4) hide show
  1. README.md +48 -2
  2. app.py +59 -0
  3. calculator.py +568 -0
  4. requirements.txt +3 -0
README.md CHANGED
@@ -183,6 +183,33 @@ python eval.py --coverage --inputs-coverage
183
 
184
  `--inputs-coverage` evaluates gates via their `.inputs` tensors using seeded external inputs and explicit overrides, and fails if inputs cannot be resolved. This is for coverage and routing sanity, not a correctness proof.
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  ## Development History
187
 
188
  Started as an 8-bit CPU project. Built boolean gates, then arithmetic (adders -> multipliers -> dividers), then CPU control logic. The CPU worked but the arithmetic core turned out to be the useful part, so it was extracted.
@@ -205,14 +232,33 @@ This began as an attempt to build a complete threshold-logic CPU. The CPU is in
205
  - Boolean, threshold, modular, pattern recognition, combinational
206
 
207
  **Next:**
208
- - TBD
 
 
209
 
210
  **Cleanup:**
211
  - None (8-bit arithmetic scaffolding removed)
212
 
213
  ## TODO (Unified)
214
 
215
- None.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
  ## License
218
 
 
183
 
184
  `--inputs-coverage` evaluates gates via their `.inputs` tensors using seeded external inputs and explicit overrides, and fails if inputs cannot be resolved. This is for coverage and routing sanity, not a correctness proof.
185
 
186
+ ## Python Calculator Interface (Gate-Level)
187
+
188
+ `calculator.py` provides a pure gate-level evaluator that uses only `.inputs` + `.weight`/`.bias` (no arithmetic shortcuts). This is intended as a rigorous proof-of-concept for using the weights as a calculator.
189
+ Expression mode routes even constants through a circuit (via `float16.add` with +0) to ensure gate-level evaluation.
190
+
191
+ Examples:
192
+
193
+ ```bash
194
+ python calculator.py float16.add 1.0 2.0
195
+ python calculator.py float16.sqrt 2.0
196
+ python calculator.py float16.add --inputs a=0x3c00 b=0x4000 --hex
197
+ python calculator.py --expr "1 + 1"
198
+ python calculator.py "sin(pi / 2)"
199
+ python calculator.py --expr "1 + 1" --json
200
+ python calculator.py "pi" --strict
201
+ ```
202
+
203
+ Programmatic use:
204
+
205
+ ```python
206
+ from calculator import ThresholdCalculator
207
+
208
+ calc = ThresholdCalculator("arithmetic.safetensors")
209
+ out, _ = calc.float16_binop("add", 1.0, 2.0)
210
+ print(out)
211
+ ```
212
+
213
  ## Development History
214
 
215
  Started as an 8-bit CPU project. Built boolean gates, then arithmetic (adders -> multipliers -> dividers), then CPU control logic. The CPU worked but the arithmetic core turned out to be the useful part, so it was extracted.
 
232
  - Boolean, threshold, modular, pattern recognition, combinational
233
 
234
  **Next:**
235
+ - Add a full scientific calculator function set (log10, asin/acos/atan, sinh/cosh, etc.) as gate-level circuits.
236
+ - Add 32-bit integer arithmetic circuits (add/sub/shift/compare, then mul/div).
237
+ - Add higher precision modes (float32 or fixed-point) for typical calculator accuracy.
238
 
239
  **Cleanup:**
240
  - None (8-bit arithmetic scaffolding removed)
241
 
242
  ## TODO (Unified)
243
 
244
+ - Scientific functions missing from current weights (log10, asin/acos/atan, sinh/cosh, floor/ceil/round, etc.).
245
+ - 32-bit integer circuits and an int32 calculator mode.
246
+ - Degree-mode trig and implicit multiplication parsing.
247
+ - Higher-precision arithmetic (float32 or fixed-point).
248
+ - Complex-number support (or explicit domain errors).
249
+
250
+ ## Hugging Face Space (Proof of Concept)
251
+
252
+ A minimal Space UI is included in `app.py`. It uses the gate-level evaluator only (no arithmetic shortcuts) and exposes a normal calculator-style expression input.
253
+
254
+ Example expressions:
255
+ - `1 + 1`
256
+ - `sin(pi / 2)`
257
+ - `exp(ln(2))`
258
+
259
+ Notes:
260
+ - All results are **float16** (IEEE-754 half) and may be rounded.
261
+ - `pow` uses the `exp(b*ln(a))` definition; negative bases yield NaN.
262
 
263
  ## License
264
 
app.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hugging Face Space: gate-level calculator for threshold-calculus.
4
+ """
5
+
6
+ import gradio as gr
7
+
8
+ from calculator import ThresholdCalculator, bits_to_int, float16_bits_to_float
9
+
10
+
11
+ calc = ThresholdCalculator("arithmetic.safetensors")
12
+
13
+
14
+ def eval_expression(expr: str):
15
+ expr = (expr or "").strip()
16
+ if not expr:
17
+ return "", "", "", ""
18
+ try:
19
+ result = calc.evaluate_expr(expr)
20
+ out_int = bits_to_int(result.bits)
21
+ out_float = float16_bits_to_float(out_int)
22
+ return (
23
+ f"{out_float}",
24
+ f"0x{out_int:04x}",
25
+ f"{result.gates_evaluated}",
26
+ f"{result.elapsed_s:.4f}s",
27
+ )
28
+ except Exception as exc:
29
+ return f"ERROR: {exc}", "", "", ""
30
+
31
+
32
+ with gr.Blocks(title="Threshold Calculus Calculator") as demo:
33
+ gr.Markdown(
34
+ "# Threshold Calculus Calculator\n"
35
+ "Pure gate-level evaluation of float16 arithmetic (IEEE-754 half). "
36
+ "All results are float16 and may be rounded."
37
+ )
38
+ expr = gr.Textbox(
39
+ label="Expression",
40
+ placeholder="e.g., 1 + 1, sin(pi / 2), exp(ln(2))",
41
+ )
42
+ run = gr.Button("Evaluate")
43
+ out_val = gr.Textbox(label="Float16 Result", interactive=False)
44
+ out_bits = gr.Textbox(label="Result Bits (hex)", interactive=False)
45
+ out_gates = gr.Textbox(label="Gates Evaluated", interactive=False)
46
+ out_time = gr.Textbox(label="Elapsed", interactive=False)
47
+
48
+ run.click(eval_expression, inputs=[expr], outputs=[out_val, out_bits, out_gates, out_time])
49
+ expr.submit(eval_expression, inputs=[expr], outputs=[out_val, out_bits, out_gates, out_time])
50
+
51
+ gr.Markdown(
52
+ "Notes:\n"
53
+ "- Functions: sin, cos, tan, tanh, sqrt, rsqrt, exp, ln, log2, abs, neg\n"
54
+ "- Constants: pi, e, inf, nan\n"
55
+ "- Pow uses exp(b*ln(a)); negative bases yield NaN."
56
+ )
57
+
58
+ if __name__ == "__main__":
59
+ demo.launch()
calculator.py ADDED
@@ -0,0 +1,568 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Threshold-calculus gate-level calculator.
4
+
5
+ Pure evaluation via .inputs + .weight/.bias only. No arithmetic shortcuts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import ast
12
+ import json
13
+ import math
14
+ import struct
15
+ import time
16
+ from dataclasses import dataclass, field
17
+ from typing import Dict, Iterable, List, Optional, Sequence, Tuple
18
+
19
+ import torch
20
+ from safetensors import safe_open
21
+
22
+
23
+ def int_to_bits(val: int, width: int) -> List[float]:
24
+ """Convert integer to LSB-first bit list of length width."""
25
+ return [float((val >> i) & 1) for i in range(width)]
26
+
27
+
28
+ def bits_to_int(bits: Sequence[float]) -> int:
29
+ """Convert LSB-first bit list to integer."""
30
+ return sum((1 << i) for i, b in enumerate(bits) if b >= 0.5)
31
+
32
+
33
+ def float_to_float16_bits(val: float) -> int:
34
+ """Convert float to IEEE-754 float16 bits (with canonical NaN)."""
35
+ try:
36
+ packed = struct.pack(">e", float(val))
37
+ return struct.unpack(">H", packed)[0]
38
+ except (OverflowError, struct.error):
39
+ if val == float("inf"):
40
+ return 0x7C00
41
+ if val == float("-inf"):
42
+ return 0xFC00
43
+ if val != val:
44
+ return 0x7E00
45
+ return 0x7BFF if val > 0 else 0xFBFF
46
+
47
+
48
+ def float16_bits_to_float(bits: int) -> float:
49
+ """Interpret 16-bit int as IEEE-754 float16."""
50
+ packed = struct.pack(">H", bits & 0xFFFF)
51
+ return struct.unpack(">e", packed)[0]
52
+
53
+
54
+ def parse_external_name(name: str) -> Tuple[Optional[str], Optional[int], Optional[str]]:
55
+ """
56
+ Parse an external signal name into (base, index, full_key).
57
+ Examples:
58
+ "$a" -> ("a", None, "$a")
59
+ "float16.add.$a[3]" -> ("a", 3, "float16.add.$a")
60
+ """
61
+ if "$" not in name:
62
+ return None, None, None
63
+ full_key = name.split("[", 1)[0]
64
+ base_part = name.split("$", 1)[1]
65
+ base = base_part.split("[", 1)[0]
66
+ idx = None
67
+ if "[" in base_part and "]" in base_part:
68
+ try:
69
+ idx = int(base_part.split("[", 1)[1].split("]", 1)[0])
70
+ except ValueError:
71
+ idx = None
72
+ return base, idx, full_key
73
+
74
+
75
+ def resolve_alias_target(name: str, gates: set) -> Optional[str]:
76
+ """Resolve common alias signal names to actual gate names."""
77
+ if name in gates:
78
+ return name
79
+ cand = name + ".layer2"
80
+ if cand in gates:
81
+ return cand
82
+ if name.endswith(".sum"):
83
+ cand = name[:-4] + ".xor2.layer2"
84
+ if cand in gates:
85
+ return cand
86
+ if name.endswith(".cout"):
87
+ for suffix in [".or_carry", ".carry_or"]:
88
+ cand = name[:-5] + suffix
89
+ if cand in gates:
90
+ return cand
91
+ return None
92
+
93
+
94
+ def normalize_expr(expr: str) -> str:
95
+ """Normalize user-facing calculator syntax to Python AST syntax."""
96
+ expr = expr.replace("π", "pi")
97
+ expr = expr.replace("×", "*").replace("÷", "/").replace("−", "-")
98
+ if "^" in expr:
99
+ expr = expr.replace("^", "**")
100
+ return expr
101
+
102
+
103
+ def looks_like_expression(text: str) -> bool:
104
+ tokens = ["+", "-", "*", "/", "(", ")", "^", "pi", "π"]
105
+ return any(tok in text for tok in tokens)
106
+
107
+
108
+ @dataclass
109
+ class EvalResult:
110
+ bits: List[float]
111
+ elapsed_s: float
112
+ gates_evaluated: int
113
+ non_gate_events: List[str] = field(default_factory=list)
114
+
115
+
116
+ class ThresholdCalculator:
117
+ def __init__(self, model_path: str = "./arithmetic.safetensors") -> None:
118
+ self.model_path = model_path
119
+ self.tensors: Dict[str, torch.Tensor] = {}
120
+ self.gates: List[str] = []
121
+ self.name_to_id: Dict[str, int] = {}
122
+ self.id_to_name: Dict[int, str] = {}
123
+ self._gate_inputs: Dict[str, torch.Tensor] = {}
124
+ self._gate_set: set = set()
125
+ self._alias_to_gate: Dict[int, int] = {}
126
+ self._gate_to_alias: Dict[int, List[int]] = {}
127
+ self._id_to_gate: Dict[int, str] = {}
128
+ self._topo_cache: Dict[Tuple[str, Tuple[str, ...]], List[str]] = {}
129
+ self._load()
130
+
131
+ def _load(self) -> None:
132
+ with safe_open(self.model_path, framework="pt") as f:
133
+ for name in f.keys():
134
+ self.tensors[name] = f.get_tensor(name)
135
+ metadata = f.metadata()
136
+ if metadata and "signal_registry" in metadata:
137
+ registry_raw = json.loads(metadata["signal_registry"])
138
+ self.id_to_name = {int(k): v for k, v in registry_raw.items()}
139
+ self.name_to_id = {v: int(k) for k, v in registry_raw.items()}
140
+ self.gates = sorted({k.rsplit(".", 1)[0] for k in self.tensors.keys() if k.endswith(".weight")})
141
+ self._gate_set = set(self.gates)
142
+ for gate in self.gates:
143
+ inputs_key = f"{gate}.inputs"
144
+ if inputs_key in self.tensors:
145
+ self._gate_inputs[gate] = self.tensors[inputs_key].to(dtype=torch.long)
146
+ self._build_alias_maps()
147
+ for gate in self.gates:
148
+ gid = self.name_to_id.get(gate)
149
+ if gid is not None:
150
+ self._id_to_gate[gid] = gate
151
+
152
+ def _build_alias_maps(self) -> None:
153
+ gates = set(self.gates)
154
+ alias_to_gate: Dict[int, int] = {}
155
+ gate_to_alias: Dict[int, List[int]] = {}
156
+ for name, sid in self.name_to_id.items():
157
+ if name in ("#0", "#1"):
158
+ continue
159
+ if name.startswith("$") or ".$" in name:
160
+ continue
161
+ if name in gates:
162
+ continue
163
+ target = resolve_alias_target(name, gates)
164
+ if not target:
165
+ continue
166
+ target_id = self.name_to_id.get(target)
167
+ if target_id is None:
168
+ continue
169
+ alias_to_gate[sid] = target_id
170
+ gate_to_alias.setdefault(target_id, []).append(sid)
171
+ self._alias_to_gate = alias_to_gate
172
+ self._gate_to_alias = gate_to_alias
173
+
174
+ def _signal_to_gate(self, sid: int) -> Optional[str]:
175
+ if sid in self._id_to_gate:
176
+ return self._id_to_gate[sid]
177
+ alias_target = self._alias_to_gate.get(sid)
178
+ if alias_target is not None:
179
+ return self._id_to_gate.get(alias_target)
180
+ return None
181
+
182
+ def _default_outputs(self, prefix: str, out_bits: int) -> List[str]:
183
+ if f"{prefix}.out0.weight" in self.tensors:
184
+ return [f"{prefix}.out{i}" for i in range(out_bits)]
185
+ if prefix in self._gate_set:
186
+ return [prefix]
187
+ raise RuntimeError(f"{prefix}: no outputs found")
188
+
189
+ def _collect_required_gates(self, output_gates: Sequence[str]) -> List[str]:
190
+ required: set = set()
191
+ stack = list(output_gates)
192
+ while stack:
193
+ gate = stack.pop()
194
+ if gate in required:
195
+ continue
196
+ if gate not in self._gate_inputs:
197
+ raise RuntimeError(f"{gate}: missing .inputs tensor")
198
+ required.add(gate)
199
+ input_ids = self._gate_inputs[gate]
200
+ for sid in input_ids.tolist():
201
+ dep_gate = self._signal_to_gate(int(sid))
202
+ if dep_gate is not None and dep_gate not in required:
203
+ stack.append(dep_gate)
204
+ return sorted(required)
205
+
206
+ def _topo_sort(self, gates: Sequence[str]) -> List[str]:
207
+ key = tuple(gates)
208
+ cache_key = ("__set__", key)
209
+ if cache_key in self._topo_cache:
210
+ return self._topo_cache[cache_key]
211
+ gate_set = set(gates)
212
+ deps: Dict[str, set] = {g: set() for g in gates}
213
+ rev: Dict[str, List[str]] = {g: [] for g in gates}
214
+ for gate in gates:
215
+ input_ids = self._gate_inputs[gate].tolist()
216
+ for sid in input_ids:
217
+ dep_gate = self._signal_to_gate(int(sid))
218
+ if dep_gate is not None and dep_gate in gate_set:
219
+ deps[gate].add(dep_gate)
220
+ rev[dep_gate].append(gate)
221
+ queue = sorted([g for g in gates if not deps[g]])
222
+ order: List[str] = []
223
+ while queue:
224
+ g = queue.pop(0)
225
+ order.append(g)
226
+ for child in rev[g]:
227
+ deps[child].remove(g)
228
+ if not deps[child]:
229
+ queue.append(child)
230
+ queue.sort()
231
+ if len(order) != len(gates):
232
+ raise RuntimeError("Dependency cycle or unresolved inputs in gate graph")
233
+ self._topo_cache[cache_key] = order
234
+ return order
235
+
236
+ def _required_externals(self, gates: Iterable[str]) -> List[int]:
237
+ externals: set = set()
238
+ for gate in gates:
239
+ for sid in self._gate_inputs[gate].tolist():
240
+ sid = int(sid)
241
+ name = self.id_to_name.get(sid, "")
242
+ if name.startswith("$") or ".$" in name:
243
+ externals.add(sid)
244
+ return sorted(externals)
245
+
246
+ def _normalize_inputs(self, required_externals: List[int], inputs: Dict[str, object]) -> Dict[int, float]:
247
+ exact: Dict[str, object] = {}
248
+ base_inputs: Dict[str, object] = {}
249
+ for key, val in inputs.items():
250
+ if "$" in key:
251
+ exact[key] = val
252
+ else:
253
+ base_inputs[key] = val
254
+
255
+ width_full: Dict[str, int] = {}
256
+ width_base: Dict[str, int] = {}
257
+ for sid in required_externals:
258
+ name = self.id_to_name.get(sid, "")
259
+ base, idx, full_key = parse_external_name(name)
260
+ if base is None or full_key is None:
261
+ continue
262
+ w = (idx + 1) if idx is not None else 1
263
+ width_full[full_key] = max(width_full.get(full_key, 1), w)
264
+ width_base[base] = max(width_base.get(base, 1), w)
265
+
266
+ def ensure_bit_list(val: object, width: int) -> List[float]:
267
+ if isinstance(val, (list, tuple)):
268
+ bits = [float(b) for b in val]
269
+ if len(bits) < width:
270
+ raise RuntimeError(f"input width {len(bits)} < required {width}")
271
+ return bits
272
+ if isinstance(val, int):
273
+ return int_to_bits(val, width)
274
+ if isinstance(val, float):
275
+ if width != 16:
276
+ raise RuntimeError("float inputs only supported for 16-bit values")
277
+ bits_int = float_to_float16_bits(val)
278
+ return int_to_bits(bits_int, width)
279
+ raise RuntimeError("inputs must be list/tuple, int, or float16-compatible float")
280
+
281
+ normalized: Dict[int, float] = {}
282
+ for sid in required_externals:
283
+ name = self.id_to_name.get(sid, "")
284
+ base, idx, full_key = parse_external_name(name)
285
+ if base is None or full_key is None:
286
+ continue
287
+ if full_key in exact:
288
+ bits = ensure_bit_list(exact[full_key], width_full[full_key])
289
+ elif base in base_inputs:
290
+ bits = ensure_bit_list(base_inputs[base], width_base[base])
291
+ else:
292
+ raise RuntimeError(f"missing external input for {name}")
293
+ use_idx = idx if idx is not None else 0
294
+ normalized[sid] = float(bits[use_idx])
295
+ return normalized
296
+
297
+ def evaluate_prefix(
298
+ self,
299
+ prefix: str,
300
+ inputs: Dict[str, object],
301
+ out_bits: int = 16,
302
+ outputs: Optional[List[str]] = None,
303
+ ) -> EvalResult:
304
+ output_gates = outputs if outputs is not None else self._default_outputs(prefix, out_bits)
305
+ required_gates = self._collect_required_gates(output_gates)
306
+ gate_order = self._topo_sort(required_gates)
307
+ required_externals = self._required_externals(required_gates)
308
+
309
+ num_signals = len(self.id_to_name)
310
+ signals = torch.full((num_signals,), float("nan"))
311
+ if "#0" in self.name_to_id:
312
+ signals[self.name_to_id["#0"]] = 0.0
313
+ if "#1" in self.name_to_id:
314
+ signals[self.name_to_id["#1"]] = 1.0
315
+
316
+ seeded = self._normalize_inputs(required_externals, inputs)
317
+ for sid, val in seeded.items():
318
+ signals[sid] = val
319
+
320
+ start = time.time()
321
+ evaluated = 0
322
+ for gate in gate_order:
323
+ input_ids = self._gate_inputs[gate]
324
+ input_vals = signals[input_ids]
325
+ if torch.isnan(input_vals).any():
326
+ raise RuntimeError(f"{gate}: unresolved inputs")
327
+ weight = self.tensors[f"{gate}.weight"].float()
328
+ bias = self.tensors.get(f"{gate}.bias", torch.tensor([0.0])).float().item()
329
+ total = torch.dot(weight, input_vals.float()).item() + bias
330
+ out = 1.0 if total >= 0 else 0.0
331
+ gate_id = self.name_to_id.get(gate)
332
+ if gate_id is not None:
333
+ signals[gate_id] = out
334
+ for alias_id in self._gate_to_alias.get(gate_id, []):
335
+ signals[alias_id] = out
336
+ evaluated += 1
337
+ elapsed = time.time() - start
338
+
339
+ bits: List[float] = []
340
+ for gate in output_gates:
341
+ gid = self.name_to_id.get(gate)
342
+ if gid is None or torch.isnan(signals[gid]):
343
+ raise RuntimeError(f"{prefix}: missing output {gate}")
344
+ bits.append(float(signals[gid]))
345
+ return EvalResult(bits=bits, elapsed_s=elapsed, gates_evaluated=evaluated)
346
+
347
+ # Float16 convenience wrappers (pure gate evaluation)
348
+ def float16_binop(self, op: str, a: float, b: float) -> Tuple[float, EvalResult]:
349
+ prefix = f"float16.{op}"
350
+ a_bits = int_to_bits(float_to_float16_bits(a), 16)
351
+ b_bits = int_to_bits(float_to_float16_bits(b), 16)
352
+ if op == "sub":
353
+ # float16.sub is defined as add with flipped sign bit on b
354
+ b_bits[15] = 1.0 - b_bits[15]
355
+ result = self.evaluate_prefix(prefix, {"a": a_bits, "b": b_bits}, out_bits=16)
356
+ out_int = bits_to_int(result.bits)
357
+ return float16_bits_to_float(out_int), result
358
+
359
+ def float16_unary(self, op: str, x: float) -> Tuple[float, EvalResult]:
360
+ prefix = f"float16.{op}"
361
+ x_bits = int_to_bits(float_to_float16_bits(x), 16)
362
+ # Unary LUT ops are wired through float16.lut.$x
363
+ result = self.evaluate_prefix(prefix, {"x": x_bits}, out_bits=16)
364
+ out_int = bits_to_int(result.bits)
365
+ return float16_bits_to_float(out_int), result
366
+
367
+ def float16_pow(self, a: float, b: float) -> Tuple[float, EvalResult]:
368
+ prefix = "float16.pow"
369
+ a_bits = int_to_bits(float_to_float16_bits(a), 16)
370
+ b_bits = int_to_bits(float_to_float16_bits(b), 16)
371
+ result = self.evaluate_prefix(prefix, {"a": a_bits, "b": b_bits}, out_bits=16)
372
+ out_int = bits_to_int(result.bits)
373
+ return float16_bits_to_float(out_int), result
374
+
375
+ def evaluate_expr(self, expr: str, force_gate_eval: bool = True) -> EvalResult:
376
+ """Evaluate a calculator expression using float16 circuits."""
377
+ expr = normalize_expr(expr)
378
+ tree = ast.parse(expr, mode="eval")
379
+
380
+ total_elapsed = 0.0
381
+ total_gates = 0
382
+ non_gate_events: List[str] = []
383
+
384
+ def run_prefix(prefix: str, inputs: Dict[str, object]) -> int:
385
+ nonlocal total_elapsed, total_gates
386
+ res = self.evaluate_prefix(prefix, inputs, out_bits=16)
387
+ total_elapsed += res.elapsed_s
388
+ total_gates += res.gates_evaluated
389
+ return bits_to_int(res.bits)
390
+
391
+ def eval_node(node: ast.AST) -> int:
392
+ if isinstance(node, ast.Expression):
393
+ return eval_node(node.body)
394
+ if isinstance(node, ast.Constant):
395
+ if isinstance(node.value, (int, float)):
396
+ return float_to_float16_bits(float(node.value))
397
+ raise RuntimeError("unsupported literal")
398
+ if isinstance(node, ast.Name):
399
+ name = node.id
400
+ if name == "pi":
401
+ return float_to_float16_bits(math.pi)
402
+ if name == "e":
403
+ return float_to_float16_bits(math.e)
404
+ if name == "inf":
405
+ return float_to_float16_bits(float("inf"))
406
+ if name == "nan":
407
+ return float_to_float16_bits(float("nan"))
408
+ raise RuntimeError(f"unknown identifier: {name}")
409
+ if isinstance(node, ast.UnaryOp):
410
+ if isinstance(node.op, ast.UAdd):
411
+ return eval_node(node.operand)
412
+ if isinstance(node.op, ast.USub):
413
+ x = eval_node(node.operand)
414
+ return run_prefix("float16.neg", {"x": x})
415
+ raise RuntimeError("unsupported unary operator")
416
+ if isinstance(node, ast.BinOp):
417
+ a = eval_node(node.left)
418
+ b = eval_node(node.right)
419
+ if isinstance(node.op, ast.Add):
420
+ return run_prefix("float16.add", {"a": a, "b": b})
421
+ if isinstance(node.op, ast.Sub):
422
+ b_flip = b ^ 0x8000
423
+ return run_prefix("float16.sub", {"a": a, "b": b_flip})
424
+ if isinstance(node.op, ast.Mult):
425
+ return run_prefix("float16.mul", {"a": a, "b": b})
426
+ if isinstance(node.op, ast.Div):
427
+ return run_prefix("float16.div", {"a": a, "b": b})
428
+ if isinstance(node.op, ast.Pow):
429
+ return run_prefix("float16.pow", {"a": a, "b": b})
430
+ raise RuntimeError("unsupported binary operator")
431
+ if isinstance(node, ast.Call):
432
+ if not isinstance(node.func, ast.Name):
433
+ raise RuntimeError("unsupported function")
434
+ fname = node.func.id
435
+ if len(node.args) != 1:
436
+ raise RuntimeError(f"{fname} expects one argument")
437
+ x = eval_node(node.args[0])
438
+ if fname == "sqrt":
439
+ return run_prefix("float16.sqrt", {"x": x})
440
+ if fname == "rsqrt":
441
+ return run_prefix("float16.rsqrt", {"x": x})
442
+ if fname == "exp":
443
+ return run_prefix("float16.exp", {"x": x})
444
+ if fname in ("ln", "log"):
445
+ return run_prefix("float16.ln", {"x": x})
446
+ if fname == "log2":
447
+ return run_prefix("float16.log2", {"x": x})
448
+ if fname == "sin":
449
+ return run_prefix("float16.sin", {"x": x})
450
+ if fname == "cos":
451
+ return run_prefix("float16.cos", {"x": x})
452
+ if fname == "tan":
453
+ return run_prefix("float16.tan", {"x": x})
454
+ if fname == "tanh":
455
+ return run_prefix("float16.tanh", {"x": x})
456
+ if fname == "abs":
457
+ return run_prefix("float16.abs", {"x": x})
458
+ if fname == "neg":
459
+ return run_prefix("float16.neg", {"x": x})
460
+ raise RuntimeError(f"unsupported function: {fname}")
461
+ raise RuntimeError("unsupported expression")
462
+
463
+ out_bits = eval_node(tree)
464
+ if total_gates == 0:
465
+ if force_gate_eval:
466
+ # Route constants through float16.add with +0 to ensure gate-level evaluation.
467
+ out_bits = run_prefix("float16.add", {"a": out_bits, "b": 0})
468
+ else:
469
+ non_gate_events.append("constant_expression_no_gates")
470
+ return EvalResult(
471
+ bits=int_to_bits(out_bits, 16),
472
+ elapsed_s=total_elapsed,
473
+ gates_evaluated=total_gates,
474
+ non_gate_events=non_gate_events,
475
+ )
476
+
477
+
478
+ def main() -> int:
479
+ parser = argparse.ArgumentParser(description="Gate-level calculator for threshold-calculus")
480
+ parser.add_argument("prefix", nargs="?", default="", help="Circuit prefix (e.g., float16.add) or expression")
481
+ parser.add_argument("values", nargs="*", help="Input values (float for float16, int otherwise)")
482
+ parser.add_argument("--model", default="./arithmetic.safetensors", help="Path to safetensors model")
483
+ parser.add_argument("--out-bits", type=int, default=16, help="Number of output bits")
484
+ parser.add_argument("--inputs", nargs="*", help="Explicit inputs as name=value (e.g., a=0x3c00)")
485
+ parser.add_argument("--hex", action="store_true", help="Parse numeric inputs as hex")
486
+ parser.add_argument("--expr", help="Evaluate expression using float16 circuits")
487
+ parser.add_argument("--json", action="store_true", help="Output JSON result")
488
+ parser.add_argument("--strict", action="store_true", help="Warn if any non-gate path is used")
489
+ args = parser.parse_args()
490
+
491
+ calc = ThresholdCalculator(args.model)
492
+
493
+ def emit_result(prefix: str, out_int: int, result: EvalResult, expr: Optional[str] = None) -> int:
494
+ out_float = float16_bits_to_float(out_int) if len(result.bits) == 16 else None
495
+ if args.strict and result.non_gate_events:
496
+ print(f"STRICT WARNING: non-gate path used: {result.non_gate_events}")
497
+ if args.json:
498
+ payload = {
499
+ "prefix": prefix,
500
+ "expr": expr,
501
+ "bits": f"0x{out_int:04x}",
502
+ "float16": out_float,
503
+ "gates": result.gates_evaluated,
504
+ "elapsed_s": result.elapsed_s,
505
+ "non_gate_events": result.non_gate_events,
506
+ }
507
+ print(json.dumps(payload))
508
+ else:
509
+ if expr:
510
+ print(f"expr={expr}")
511
+ print(f"bits=0x{out_int:04x} float16={out_float}")
512
+ print(f"gates={result.gates_evaluated} elapsed_s={result.elapsed_s:.4f}")
513
+ return 0
514
+
515
+ if args.expr or (args.prefix and not args.values and not args.inputs and looks_like_expression(args.prefix)):
516
+ expr = args.expr if args.expr else args.prefix
517
+ result = calc.evaluate_expr(expr)
518
+ out_int = bits_to_int(result.bits)
519
+ return emit_result("expr", out_int, result, expr=expr)
520
+
521
+ if not args.prefix:
522
+ raise RuntimeError("Provide a circuit prefix or use --expr")
523
+
524
+ if args.inputs:
525
+ inputs: Dict[str, object] = {}
526
+ for item in args.inputs:
527
+ if "=" not in item:
528
+ raise RuntimeError("inputs must be name=value")
529
+ key, val = item.split("=", 1)
530
+ if args.hex or val.startswith("0x"):
531
+ inputs[key] = int(val, 16)
532
+ else:
533
+ try:
534
+ inputs[key] = int(val)
535
+ except ValueError:
536
+ inputs[key] = float(val)
537
+ result = calc.evaluate_prefix(args.prefix, inputs, out_bits=args.out_bits)
538
+ out_int = bits_to_int(result.bits)
539
+ print(f"bits={result.bits}")
540
+ print(f"int=0x{out_int:0{(args.out_bits + 3) // 4}x}")
541
+ if args.out_bits == 16:
542
+ pass
543
+ return emit_result(args.prefix, out_int, result)
544
+
545
+ # Convenience mode for float16 binary/unary
546
+ prefix = args.prefix
547
+ if prefix.startswith("float16."):
548
+ op = prefix.split(".", 1)[1]
549
+ if op == "pow":
550
+ if len(args.values) != 2:
551
+ raise RuntimeError("float16.pow requires two values")
552
+ out, result = calc.float16_pow(float(args.values[0]), float(args.values[1]))
553
+ elif op in ("add", "sub", "mul", "div"):
554
+ if len(args.values) != 2:
555
+ raise RuntimeError(f"{prefix} requires two values")
556
+ out, result = calc.float16_binop(op, float(args.values[0]), float(args.values[1]))
557
+ else:
558
+ if len(args.values) != 1:
559
+ raise RuntimeError(f"{prefix} requires one value")
560
+ out, result = calc.float16_unary(op, float(args.values[0]))
561
+ out_bits = bits_to_int(result.bits)
562
+ return emit_result(prefix, out_bits, result)
563
+
564
+ raise RuntimeError("Provide --inputs for non-float16 circuits")
565
+
566
+
567
+ if __name__ == "__main__":
568
+ raise SystemExit(main())
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio
2
+ torch
3
+ safetensors