Skip to content

Commit f57beb1

Browse files
Track 2: substrate-typed array library — vectorized arithmetic + resonance access
First piece of the substrate-typed-array track. The differentiator vs numpy: every element of an OMC int array carries φ-resonance and HIM (Harmonic Interference Metric) computed from the substrate at HInt construction time. The new builtins expose vectorized operations that PRESERVE this substrate-typing through element-wise math, plus two reduction-like operations that EXTRACT the substrate metadata as parallel arrays — things Python literally can't do because i64 doesn't carry resonance. NEW BUILTINS (9): Vectorized arithmetic (broadcast scalar ↔ array on either side): arr_add(a, b) — element-wise + arr_sub(a, b) — element-wise - arr_mul(a, b) — element-wise * arr_div_int(a, b) — element-wise / (zero divisor → 0, no NaN propagation; Singularity is at value level not array level) arr_neg(a) — unary - arr_scale(a, k) — explicit scalar mul (sugar for arr_mul) Substrate-aware (the differentiator from numpy): arr_resonance_vec(a) — per-element φ-resonance score [0..1] arr_him_vec(a) — per-element Harmonic Interference Metric arr_fold_all(a) — vectorized fold; snaps every element to its nearest Fibonacci attractor Helper added at module scope: elementwise_op(a: &Value, b: &Value, name, op: F) F: Fn(i64, i64) -> i64. Handles (array, array), (array, scalar), (scalar, array). Mismatched array lengths error explicitly — no implicit shape-1 expansion (keeps behavior obvious for a minimum-viable broadcast). Output Values are wrapped via HInt::new so per-element substrate-resonance gets recomputed from the arithmetic result. Tests (examples/tests/test_substrate_array.omc — 15 tests, all pass): - Element-wise: add / sub / mul / neg - Scalar broadcast: arr_scale, arr_add(arr, scalar), arr_sub(scalar, arr) - Division with zero-divisor handling - Per-element resonance: Fibonacci values resonance ~1.0, off-attractor < 1.0; HIM low for on-attractor - arr_fold_all snaps to attractors (post-fold every element is an exact Fibonacci value, verified via is_attractor) - Composition: ML-shaped pipeline (raw → scale → bias → fold → score by mean resonance) stays substrate-coherent - Composition with existing reductions: arr_add then arr_sum_int; arr_mul (square) then arr_sum_int (sum of squares) What this unlocks for the harmonic-LLM thesis: - Feature vectors with per-element substrate-resonance tracking - Substrate-aware activations: f(x) = arr_mul(arr_resonance_vec(x), x) weights every element by its own substrate coherence - Quantization-via-fold: arr_fold_all is a no-config substrate- aligned quantizer (compose with arr_resonance_vec to detect quantization error) What's NOT yet shipped (next session for Track 2): - Broadcasting beyond scalar↔array (no shape-1 expansion between differently-shaped arrays) - 2D arrays / matrices (matmul, transpose) - Substrate-aware autograd (forward-mode + reverse-mode) - End-to-end harmonic ML pipeline demo using these primitives Regression: 225 OMC tests + 15 new substrate-array = 240 OMC tests pass. All previous suites green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 159dca9 commit f57beb1

2 files changed

Lines changed: 365 additions & 0 deletions

File tree

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Track 2: substrate-typed array library — MVP tests.
2+
#
3+
# Vectorized arithmetic + per-element substrate metadata access:
4+
# arr_add / arr_sub / arr_mul / arr_div_int / arr_neg / arr_scale
5+
# arr_resonance_vec — every element's φ-resonance
6+
# arr_him_vec — every element's HIM score
7+
# arr_fold_all — element-wise snap to nearest Fibonacci attractor
8+
#
9+
# The defining feature vs numpy: every element of an OMC int array
10+
# carries substrate-resonance metadata. arr_resonance_vec / arr_him_vec
11+
# expose this; Python can't because i64 doesn't carry resonance.
12+
13+
fn assert_eq(actual, expected, msg) {
14+
if actual != expected {
15+
test_record_failure(msg + ": expected " + to_string(expected) + " got " + to_string(actual));
16+
}
17+
}
18+
19+
fn assert_true(cond, msg) {
20+
if !cond { test_record_failure(msg); }
21+
}
22+
23+
fn approx_eq(a, b, tol) {
24+
h d = a - b;
25+
if d < 0 { d = 0 - d; }
26+
return d <= tol;
27+
}
28+
29+
# ---- Element-wise arithmetic ----
30+
31+
fn test_arr_add() {
32+
h a = [1, 2, 3, 4];
33+
h b = [10, 20, 30, 40];
34+
h c = arr_add(a, b);
35+
assert_eq(arr_len(c), 4, "length preserved");
36+
assert_eq(arr_get(c, 0), 11, "1+10");
37+
assert_eq(arr_get(c, 3), 44, "4+40");
38+
}
39+
40+
fn test_arr_sub() {
41+
h c = arr_sub([10, 20, 30], [1, 2, 3]);
42+
assert_eq(arr_get(c, 0), 9, "10-1");
43+
assert_eq(arr_get(c, 2), 27, "30-3");
44+
}
45+
46+
fn test_arr_mul() {
47+
h c = arr_mul([1, 2, 3], [10, 10, 10]);
48+
assert_eq(arr_get(c, 0), 10, "1*10");
49+
assert_eq(arr_get(c, 2), 30, "3*10");
50+
}
51+
52+
fn test_arr_neg() {
53+
h c = arr_neg([1, 0 - 2, 3, 0 - 4]);
54+
assert_eq(arr_get(c, 0), 0 - 1, "neg 1");
55+
assert_eq(arr_get(c, 1), 2, "neg -2");
56+
}
57+
58+
# ---- Scalar broadcasting ----
59+
60+
fn test_arr_scale() {
61+
h c = arr_scale([1, 2, 3, 4], 5);
62+
assert_eq(arr_get(c, 0), 5, "1*5");
63+
assert_eq(arr_get(c, 3), 20, "4*5");
64+
}
65+
66+
fn test_arr_add_scalar() {
67+
# 2nd arg is a scalar — broadcasts to every element.
68+
h c = arr_add([10, 20, 30], 1);
69+
assert_eq(arr_get(c, 0), 11, "10+1");
70+
assert_eq(arr_get(c, 2), 31, "30+1");
71+
}
72+
73+
fn test_arr_scalar_lhs() {
74+
# 1st arg is a scalar — broadcasts to every element.
75+
h c = arr_sub(100, [1, 2, 3]);
76+
assert_eq(arr_get(c, 0), 99, "100-1");
77+
assert_eq(arr_get(c, 2), 97, "100-3");
78+
}
79+
80+
# ---- Division ----
81+
82+
fn test_arr_div_int() {
83+
h c = arr_div_int([10, 20, 30], [2, 5, 0]);
84+
assert_eq(arr_get(c, 0), 5, "10/2");
85+
assert_eq(arr_get(c, 1), 4, "20/5");
86+
# Div by zero → 0 (no NaN propagation through array)
87+
assert_eq(arr_get(c, 2), 0, "30/0 -> 0");
88+
}
89+
90+
# ---- Substrate-typed dtype: per-element resonance ----
91+
92+
fn test_arr_resonance_vec_attractors() {
93+
# Fibonacci numbers have resonance = 1.0
94+
h r = arr_resonance_vec([0, 1, 2, 3, 5, 8, 13, 21]);
95+
h i = 0;
96+
while i < arr_len(r) {
97+
assert_true(approx_eq(arr_get(r, i), 1.0, 0.001),
98+
"Fibonacci has resonance ~1.0");
99+
i = i + 1;
100+
}
101+
}
102+
103+
fn test_arr_resonance_vec_off_attractor() {
104+
# Off-attractor values have resonance < 1.0
105+
h r = arr_resonance_vec([7, 100, 99]);
106+
h i = 0;
107+
while i < arr_len(r) {
108+
assert_true(arr_get(r, i) < 1.0, "off-attractor resonance < 1");
109+
i = i + 1;
110+
}
111+
}
112+
113+
fn test_arr_him_vec() {
114+
# On-attractor values have low HIM (Harmonic Interference Metric).
115+
h h_attr = arr_him_vec([1, 2, 3, 5, 8, 13]);
116+
h i = 0;
117+
while i < arr_len(h_attr) {
118+
assert_true(arr_get(h_attr, i) < 0.5,
119+
"on-attractor HIM < 0.5");
120+
i = i + 1;
121+
}
122+
}
123+
124+
# ---- Substrate-aware quantization ----
125+
126+
fn test_arr_fold_all() {
127+
# 7 → 8 (nearest Fibonacci), 100 → 89, 50 → 55 (or 34, depends on
128+
# tiebreaker; just check the output is on an attractor).
129+
h folded = arr_fold_all([7, 100, 9, 22]);
130+
# All elements should be Fibonacci attractors after folding.
131+
h all_attractors = 1;
132+
h i = 0;
133+
while i < arr_len(folded) {
134+
if is_attractor(arr_get(folded, i)) == 0 {
135+
all_attractors = 0;
136+
}
137+
i = i + 1;
138+
}
139+
assert_eq(all_attractors, 1, "all folded elements on attractors");
140+
}
141+
142+
# ---- Composition: ML-shaped pipeline ----
143+
144+
fn test_pipeline_substrate_aware_features() {
145+
# Simulate a feature-engineering pipeline:
146+
# 1. Take raw counts
147+
# 2. Scale by a constant
148+
# 3. Add a per-sample bias
149+
# 4. Fold to substrate-aligned buckets
150+
# 5. Score by mean resonance — high resonance = substrate-coherent
151+
h raw = [3, 5, 8, 13, 21, 34]; # Fibonacci already
152+
h scaled = arr_scale(raw, 2); # [6, 10, 16, 26, 42, 68]
153+
h biased = arr_add(scaled, 1); # [7, 11, 17, 27, 43, 69]
154+
h folded = arr_fold_all(biased); # snap to nearest attractors
155+
h resonance = arr_resonance_vec(folded);
156+
# Sum of resonances; for fully substrate-aligned data, ~6.0
157+
h s = 0.0;
158+
h i = 0;
159+
while i < arr_len(resonance) {
160+
s = s + arr_get(resonance, i);
161+
i = i + 1;
162+
}
163+
assert_true(s > 5.5, "pipeline output stays substrate-coherent");
164+
}
165+
166+
# ---- Composition with existing reductions ----
167+
168+
fn test_arr_add_then_sum() {
169+
h c = arr_add([1, 2, 3], [10, 20, 30]);
170+
assert_eq(arr_sum_int(c), 66, "sum after element-wise add");
171+
}
172+
173+
fn test_arr_mul_then_dot() {
174+
# Square via element-wise mul, then dot-with-ones for sum-of-squares.
175+
h v = [3, 4, 5];
176+
h squared = arr_mul(v, v); # [9, 16, 25]
177+
assert_eq(arr_sum_int(squared), 50, "sum of squares 3,4,5");
178+
}

omnimcode-core/src/interpreter.rs

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2003,6 +2003,9 @@ impl Interpreter {
20032003
| "arr_argmax" | "arr_argmin" | "arr_cumsum" | "arr_diff" | "arr_range"
20042004
| "arr_unique_count" | "arr_partition_by"
20052005
| "arr_min_float" | "arr_max_float" | "arr_gcd" | "fnv1a_hash"
2006+
// Substrate-typed array library
2007+
| "arr_add" | "arr_sub" | "arr_mul" | "arr_div_int" | "arr_neg"
2008+
| "arr_scale" | "arr_resonance_vec" | "arr_him_vec" | "arr_fold_all"
20062009
| "arr_mean" | "arr_variance" | "arr_stddev" | "arr_median"
20072010
| "arr_harmonic_mean" | "arr_geometric_mean"
20082011
| "arr_sum_sq" | "arr_norm" | "arr_dot"
@@ -5385,6 +5388,143 @@ impl Interpreter {
53855388
Err("arr_dot: requires two arrays".to_string())
53865389
}
53875390
}
5391+
// ---- Substrate-typed array library (Track 2 MVP) -------
5392+
//
5393+
// Vectorized arithmetic + substrate-aware reductions on
5394+
// arrays of HInt. The dispatch boundary marshals int
5395+
// arrays through the L1.6 buffer; these handlers produce
5396+
// new arrays element-wise (so the substrate-resonance
5397+
// metadata on each output HInt is recomputed from the
5398+
// arithmetic result — no special tagging needed).
5399+
//
5400+
// Broadcasting: if the 2nd arg is a scalar (HInt / HFloat),
5401+
// it's repeated for every element of the 1st arg's array.
5402+
// Two arrays must match length (no implicit shape-1 broadcast).
5403+
"arr_add" => {
5404+
if args.len() < 2 {
5405+
return Err("arr_add requires (a, b)".to_string());
5406+
}
5407+
let a = self.eval_expr(&args[0])?;
5408+
let b = self.eval_expr(&args[1])?;
5409+
Ok(elementwise_op(&a, &b, "arr_add", |x, y| x.wrapping_add(y))?)
5410+
}
5411+
"arr_sub" => {
5412+
if args.len() < 2 {
5413+
return Err("arr_sub requires (a, b)".to_string());
5414+
}
5415+
let a = self.eval_expr(&args[0])?;
5416+
let b = self.eval_expr(&args[1])?;
5417+
Ok(elementwise_op(&a, &b, "arr_sub", |x, y| x.wrapping_sub(y))?)
5418+
}
5419+
"arr_mul" => {
5420+
if args.len() < 2 {
5421+
return Err("arr_mul requires (a, b)".to_string());
5422+
}
5423+
let a = self.eval_expr(&args[0])?;
5424+
let b = self.eval_expr(&args[1])?;
5425+
Ok(elementwise_op(&a, &b, "arr_mul", |x, y| x.wrapping_mul(y))?)
5426+
}
5427+
"arr_div_int" => {
5428+
// Integer division. Zero divisor produces 0 in that
5429+
// slot (matches harmonic_anomaly-style "no propagation
5430+
// of NaN through arrays" — Singularity is at the value
5431+
// level, not the array level).
5432+
if args.len() < 2 {
5433+
return Err("arr_div_int requires (a, b)".to_string());
5434+
}
5435+
let a = self.eval_expr(&args[0])?;
5436+
let b = self.eval_expr(&args[1])?;
5437+
Ok(elementwise_op(&a, &b, "arr_div_int",
5438+
|x, y| if y == 0 { 0 } else { x / y })?)
5439+
}
5440+
"arr_neg" => {
5441+
// Unary element-wise negation.
5442+
if args.is_empty() {
5443+
return Err("arr_neg requires (array)".to_string());
5444+
}
5445+
let a = self.eval_expr(&args[0])?;
5446+
if let Value::Array(arr) = a {
5447+
let out: Vec<Value> = arr.items.borrow().iter()
5448+
.map(|v| Value::HInt(HInt::new(v.to_int().wrapping_neg())))
5449+
.collect();
5450+
Ok(Value::Array(HArray::from_vec(out)))
5451+
} else {
5452+
Err("arr_neg: requires an array".to_string())
5453+
}
5454+
}
5455+
"arr_scale" => {
5456+
// arr_scale(arr, k) — explicit scalar multiply. Same as
5457+
// arr_mul(arr, k) when k is a scalar; provided as a
5458+
// named alias so callers can opt into the broadcast
5459+
// shape without it being inferred.
5460+
if args.len() < 2 {
5461+
return Err("arr_scale requires (array, scalar)".to_string());
5462+
}
5463+
let a = self.eval_expr(&args[0])?;
5464+
let k = self.eval_expr(&args[1])?;
5465+
Ok(elementwise_op(&a, &k, "arr_scale", |x, y| x.wrapping_mul(y))?)
5466+
}
5467+
// arr_resonance_vec(arr) -> array of f64 per-element
5468+
// resonance scores. The substrate-typed dtype's defining
5469+
// operation: each output element is HInt::compute_resonance
5470+
// of the corresponding input. Python literally can't do
5471+
// this — there's no φ-resonance attached to an i64.
5472+
"arr_resonance_vec" => {
5473+
if args.is_empty() {
5474+
return Err("arr_resonance_vec requires (array)".to_string());
5475+
}
5476+
let a = self.eval_expr(&args[0])?;
5477+
if let Value::Array(arr) = a {
5478+
let out: Vec<Value> = arr.items.borrow().iter()
5479+
.map(|v| Value::HFloat(HInt::compute_resonance(v.to_int())))
5480+
.collect();
5481+
Ok(Value::Array(HArray::from_vec(out)))
5482+
} else {
5483+
Err("arr_resonance_vec: requires an array".to_string())
5484+
}
5485+
}
5486+
// arr_him_vec(arr) -> array of f64 per-element HIM scores.
5487+
// Complement to arr_resonance_vec: HIM is the
5488+
// Harmonic-Interference-Metric — how off-attractor each
5489+
// value is. Together with resonance, these are the two
5490+
// substrate-typed metadata channels carried per-element.
5491+
"arr_him_vec" => {
5492+
if args.is_empty() {
5493+
return Err("arr_him_vec requires (array)".to_string());
5494+
}
5495+
let a = self.eval_expr(&args[0])?;
5496+
if let Value::Array(arr) = a {
5497+
let out: Vec<Value> = arr.items.borrow().iter()
5498+
.map(|v| {
5499+
let h = HInt::new(v.to_int());
5500+
Value::HFloat(h.him_score)
5501+
})
5502+
.collect();
5503+
Ok(Value::Array(HArray::from_vec(out)))
5504+
} else {
5505+
Err("arr_him_vec: requires an array".to_string())
5506+
}
5507+
}
5508+
// arr_fold_all(arr) -> new array with every element snapped
5509+
// to its nearest Fibonacci attractor. Vectorized fold.
5510+
// Substrate-canonical denoising / quantization primitive.
5511+
"arr_fold_all" => {
5512+
if args.is_empty() {
5513+
return Err("arr_fold_all requires (array)".to_string());
5514+
}
5515+
let a = self.eval_expr(&args[0])?;
5516+
if let Value::Array(arr) = a {
5517+
let out: Vec<Value> = arr.items.borrow().iter()
5518+
.map(|v| {
5519+
let folded = crate::phi_pi_fib::fold_to_nearest_attractor(v.to_int());
5520+
Value::HInt(HInt::new(folded))
5521+
})
5522+
.collect();
5523+
Ok(Value::Array(HArray::from_vec(out)))
5524+
} else {
5525+
Err("arr_fold_all: requires an array".to_string())
5526+
}
5527+
}
53885528
"arr_concat" => {
53895529
if args.len() < 2 {
53905530
return Err("arr_concat requires (array_a, array_b)".to_string());
@@ -7739,6 +7879,51 @@ fn values_equal(a: &Value, b: &Value) -> bool {
77397879

77407880
// Free function reused by quantize / quantization_ratio / mean_omni_weight.
77417881
// Snap |n| to the nearest Fibonacci attractor, preserving sign.
7882+
/// Track-2 substrate-typed-array helper: element-wise binary op
7883+
/// over (array, array) or (array, scalar). Scalar broadcasts to
7884+
/// every position of the array. Two-array length mismatch is an
7885+
/// error (no implicit shape-1 expansion — keeps behavior obvious).
7886+
/// `op` takes (i64, i64) and returns i64; the helper wraps the
7887+
/// result in HInt so per-element substrate resonance gets recomputed
7888+
/// from the arithmetic output.
7889+
pub(crate) fn elementwise_op<F: Fn(i64, i64) -> i64>(
7890+
a: &Value,
7891+
b: &Value,
7892+
name: &str,
7893+
op: F,
7894+
) -> Result<Value, String> {
7895+
match (a, b) {
7896+
(Value::Array(arr_a), Value::Array(arr_b)) => {
7897+
let ai = arr_a.items.borrow();
7898+
let bi = arr_b.items.borrow();
7899+
if ai.len() != bi.len() {
7900+
return Err(format!(
7901+
"{}: length mismatch ({} vs {})", name, ai.len(), bi.len()
7902+
));
7903+
}
7904+
let out: Vec<Value> = ai.iter().zip(bi.iter())
7905+
.map(|(x, y)| Value::HInt(HInt::new(op(x.to_int(), y.to_int()))))
7906+
.collect();
7907+
Ok(Value::Array(HArray::from_vec(out)))
7908+
}
7909+
(Value::Array(arr_a), scalar) => {
7910+
let sv = scalar.to_int();
7911+
let out: Vec<Value> = arr_a.items.borrow().iter()
7912+
.map(|x| Value::HInt(HInt::new(op(x.to_int(), sv))))
7913+
.collect();
7914+
Ok(Value::Array(HArray::from_vec(out)))
7915+
}
7916+
(scalar, Value::Array(arr_b)) => {
7917+
let sv = scalar.to_int();
7918+
let out: Vec<Value> = arr_b.items.borrow().iter()
7919+
.map(|y| Value::HInt(HInt::new(op(sv, y.to_int()))))
7920+
.collect();
7921+
Ok(Value::Array(HArray::from_vec(out)))
7922+
}
7923+
_ => Err(format!("{}: requires at least one array argument", name)),
7924+
}
7925+
}
7926+
77427927
/// Convert a `serde_json::Value` into an OMC `Value`. JSON object →
77437928
/// `Value::Dict`, JSON array → `Value::Array`, numbers split into
77447929
/// `HInt` (when representable as i64) vs `HFloat` (everything else).
@@ -8194,6 +8379,8 @@ pub(crate) const HEAL_BUILTIN_NAMES: &[&str] = &[
81948379
"arr_argmax", "arr_argmin", "arr_cumsum", "arr_diff", "arr_range",
81958380
"arr_unique_count", "arr_partition_by",
81968381
"arr_min_float", "arr_max_float", "arr_gcd", "fnv1a_hash",
8382+
"arr_add", "arr_sub", "arr_mul", "arr_div_int", "arr_neg",
8383+
"arr_scale", "arr_resonance_vec", "arr_him_vec", "arr_fold_all",
81978384
"arr_mean", "arr_variance", "arr_stddev", "arr_median",
81988385
"arr_harmonic_mean", "arr_geometric_mean",
81998386
"arr_sum_sq", "arr_norm", "arr_dot",

0 commit comments

Comments
 (0)