Skip to content

Commit c484b6d

Browse files
Track 2: substrate-aware forward-mode autograd
A dual number is a 2-element array [value, derivative]. No new Value variant — duals compose with existing array ops, matmul, dict, and HInt/HFloat substrate metadata. Pattern: x' = dual(x, 1.0) # lift input with seed y' = dual_mul(x', x') # forward-prop through f grad = dual_d(y') # df/dx Builtins: dual / dual_v / dual_d dual_add / dual_sub / dual_mul / dual_div / dual_neg dual_pow_int dual_exp / dual_sin / dual_cos dual_relu / dual_sigmoid / dual_tanh Mixing duals and scalar constants is natural: any plain scalar passed into a dual op is treated as (scalar, 0.0) by unpack_dual(). Tests: 17 cases covering analytic gradients of polynomials, products, quotients, transcendentals, both ReLU branches, sigmoid+tanh, and a two-level chain rule. Plus an end-to-end neuron with frozen weights and the quadratic-loss gradient w.r.t. a weight parameter — the shape you'd see in a real training loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent eb144b6 commit c484b6d

3 files changed

Lines changed: 385 additions & 0 deletions

File tree

examples/tests/test_autograd.omc

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# Track 2: substrate-aware forward-mode autograd via dual numbers.
2+
#
3+
# Dual: [value, derivative]. Lift x with dual(x, 1.0), forward-propagate
4+
# through dual_* ops, read df/dx from dual_d. Substrate metadata follows
5+
# the value through HInt/HFloat as usual.
6+
7+
fn assert_eq(actual, expected, msg) {
8+
if actual != expected {
9+
test_record_failure(msg + ": expected " + to_string(expected) + " got " + to_string(actual));
10+
}
11+
}
12+
13+
fn assert_true(cond, msg) {
14+
if !cond { test_record_failure(msg); }
15+
}
16+
17+
fn approx_eq(a, b, tol) {
18+
h d = a - b;
19+
if d < 0.0 { d = 0.0 - d; }
20+
return d <= tol;
21+
}
22+
23+
# ---- Constructor / accessors ----
24+
25+
fn test_dual_construct() {
26+
h x = dual(3.0, 1.0);
27+
assert_true(approx_eq(dual_v(x), 3.0, 0.001), "value");
28+
assert_true(approx_eq(dual_d(x), 1.0, 0.001), "derivative");
29+
}
30+
31+
# ---- f(x) = x ; f'(x) = 1 ----
32+
33+
fn test_identity_grad() {
34+
h x = dual(5.0, 1.0);
35+
assert_true(approx_eq(dual_d(x), 1.0, 0.001), "df/dx of x is 1");
36+
}
37+
38+
# ---- f(x) = x^2 ; f'(x) = 2x ; at x=3, f'=6 ----
39+
40+
fn test_square_grad() {
41+
h x = dual(3.0, 1.0);
42+
h y = dual_mul(x, x);
43+
assert_true(approx_eq(dual_v(y), 9.0, 0.001), "x^2 at 3 = 9");
44+
assert_true(approx_eq(dual_d(y), 6.0, 0.001), "d/dx x^2 at 3 = 6");
45+
}
46+
47+
# ---- f(x) = x^3 via dual_pow_int ; f'(x) = 3x^2 ; at x=2, f=8, f'=12 ---
48+
49+
fn test_cube_grad_pow() {
50+
h x = dual(2.0, 1.0);
51+
h y = dual_pow_int(x, 3);
52+
assert_true(approx_eq(dual_v(y), 8.0, 0.001), "x^3 at 2 = 8");
53+
assert_true(approx_eq(dual_d(y), 12.0, 0.001), "d/dx x^3 at 2 = 12");
54+
}
55+
56+
# ---- f(x) = a*x + b (a=2, b=5) ; f'(x) = 2 ----
57+
58+
fn test_affine_grad() {
59+
h x = dual(7.0, 1.0);
60+
# constant scalars are treated as duals with deriv=0
61+
h y = dual_add(dual_mul(2.0, x), 5.0);
62+
assert_true(approx_eq(dual_v(y), 19.0, 0.001), "2*7+5 = 19");
63+
assert_true(approx_eq(dual_d(y), 2.0, 0.001), "slope is 2");
64+
}
65+
66+
# ---- f(x) = (x+1) * (x-1) = x^2 - 1 ; f'(x) = 2x ; at x=4, f'=8 ----
67+
68+
fn test_product_rule() {
69+
h x = dual(4.0, 1.0);
70+
h y = dual_mul(dual_add(x, 1.0), dual_sub(x, 1.0));
71+
assert_true(approx_eq(dual_v(y), 15.0, 0.001), "(x+1)(x-1) at 4 = 15");
72+
assert_true(approx_eq(dual_d(y), 8.0, 0.001), "deriv at 4 = 8");
73+
}
74+
75+
# ---- f(x) = 1/x ; f'(x) = -1/x^2 ; at x=2, f'=-0.25 ----
76+
77+
fn test_reciprocal() {
78+
h x = dual(2.0, 1.0);
79+
h y = dual_div(1.0, x);
80+
assert_true(approx_eq(dual_v(y), 0.5, 0.001), "1/2 = 0.5");
81+
assert_true(approx_eq(dual_d(y), 0 - 0.25, 0.001), "d/dx 1/x at 2 = -0.25");
82+
}
83+
84+
# ---- f(x) = exp(x) ; f'(x) = exp(x) ; at x=0, both 1 ----
85+
86+
fn test_exp_grad() {
87+
h x = dual(0.0, 1.0);
88+
h y = dual_exp(x);
89+
assert_true(approx_eq(dual_v(y), 1.0, 0.001), "exp(0) = 1");
90+
assert_true(approx_eq(dual_d(y), 1.0, 0.001), "d/dx exp(0) = 1");
91+
}
92+
93+
# ---- f(x) = sin(x) at x=0 ; f=0, f'=cos(0)=1 ----
94+
95+
fn test_sin_grad() {
96+
h x = dual(0.0, 1.0);
97+
h y = dual_sin(x);
98+
assert_true(approx_eq(dual_v(y), 0.0, 0.001), "sin(0) = 0");
99+
assert_true(approx_eq(dual_d(y), 1.0, 0.001), "d/dx sin(0) = 1");
100+
}
101+
102+
# ---- ReLU branches ----
103+
104+
fn test_relu_positive() {
105+
h x = dual(3.5, 1.0);
106+
h y = dual_relu(x);
107+
assert_true(approx_eq(dual_v(y), 3.5, 0.001), "relu(3.5) = 3.5");
108+
assert_true(approx_eq(dual_d(y), 1.0, 0.001), "relu' on positive = 1");
109+
}
110+
111+
fn test_relu_negative() {
112+
h x = dual(0 - 2.0, 1.0);
113+
h y = dual_relu(x);
114+
assert_true(approx_eq(dual_v(y), 0.0, 0.001), "relu(-2) = 0");
115+
assert_true(approx_eq(dual_d(y), 0.0, 0.001), "relu' on negative = 0");
116+
}
117+
118+
# ---- Sigmoid at 0: value 0.5, deriv 0.25 ----
119+
120+
fn test_sigmoid_grad() {
121+
h x = dual(0.0, 1.0);
122+
h y = dual_sigmoid(x);
123+
assert_true(approx_eq(dual_v(y), 0.5, 0.001), "sigmoid(0) = 0.5");
124+
assert_true(approx_eq(dual_d(y), 0.25, 0.001), "sigmoid'(0) = 0.25");
125+
}
126+
127+
# ---- Tanh at 0: value 0, deriv 1 ----
128+
129+
fn test_tanh_grad() {
130+
h x = dual(0.0, 1.0);
131+
h y = dual_tanh(x);
132+
assert_true(approx_eq(dual_v(y), 0.0, 0.001), "tanh(0) = 0");
133+
assert_true(approx_eq(dual_d(y), 1.0, 0.001), "tanh'(0) = 1");
134+
}
135+
136+
# ---- Chain rule: f(x) = sigmoid(2x + 1) ; analytic grad at x=0 ----
137+
# y = sigmoid(2x + 1). At x=0: u=1, sigmoid(1) = 0.7310586,
138+
# sigmoid'(1) = 0.7310586*(1 - 0.7310586) = 0.196612.
139+
# dy/dx = sigmoid'(u) * du/dx = 0.196612 * 2 = 0.393224.
140+
141+
fn test_chain_rule_sigmoid() {
142+
h x = dual(0.0, 1.0);
143+
h u = dual_add(dual_mul(2.0, x), 1.0);
144+
h y = dual_sigmoid(u);
145+
assert_true(approx_eq(dual_v(y), 0.7310586, 0.001), "sigmoid(1) value");
146+
assert_true(approx_eq(dual_d(y), 0.393224, 0.001), "chain-rule deriv");
147+
}
148+
149+
# ---- Composition: a tiny "neuron" y = sigmoid(w*x + b) ----
150+
# At w=0.5, x=2.0, b=0.0: z = 1.0, y = sigmoid(1) = 0.7310586
151+
# Want dy/dx with w,b held constant. Lift only x:
152+
153+
fn test_neuron_dydx() {
154+
h w = 0.5;
155+
h b = 0.0;
156+
h x = dual(2.0, 1.0); # seed for d/dx
157+
h z = dual_add(dual_mul(w, x), b); # w*x + b ; dz/dx = w = 0.5
158+
h y = dual_sigmoid(z);
159+
# dy/dz = sigmoid(1)*(1-sigmoid(1)) ≈ 0.196612 ; dy/dx = 0.196612*0.5
160+
assert_true(approx_eq(dual_d(y), 0.098306, 0.001), "neuron dy/dx");
161+
}
162+
163+
# ---- Substrate-aware: gradients on Fibonacci-valued inputs ----
164+
# Take f(x) = x^2, evaluate at the Fibonacci attractor x=5.
165+
# Value 25 is non-attractor (closest is 21 or 34) so resonance < 1,
166+
# but the gradient computation itself is exact: f'(5) = 10.
167+
168+
fn test_grad_substrate_input() {
169+
h x = dual(5.0, 1.0);
170+
h y = dual_mul(x, x);
171+
assert_true(approx_eq(dual_v(y), 25.0, 0.001), "5^2 = 25");
172+
assert_true(approx_eq(dual_d(y), 10.0, 0.001), "f'(5) = 10");
173+
}
174+
175+
# ---- Quadratic loss: L = (y_hat - y)^2 ; dL/dy_hat = 2(y_hat - y) ---
176+
# y_hat = w*x. At w=3, x=2 (y_hat=6, y=5): L=1, dL/dw via chain = 2*1*2 = 4
177+
178+
fn test_loss_grad_w() {
179+
h w = dual(3.0, 1.0); # seed d/dw
180+
h x = 2.0;
181+
h y_target = 5.0;
182+
h y_hat = dual_mul(w, x); # 6 ; dy_hat/dw = 2
183+
h diff = dual_sub(y_hat, y_target); # 1 ; ddiff/dw = 2
184+
h L = dual_mul(diff, diff); # 1 ; dL/dw = 2*1*2 = 4
185+
assert_true(approx_eq(dual_v(L), 1.0, 0.001), "loss = 1");
186+
assert_true(approx_eq(dual_d(L), 4.0, 0.001), "dL/dw = 4");
187+
}

omnimcode-core/src/compiler.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@ impl Compiler {
254254
// 2D array primitives (Track 2 — 2026-05-16)
255255
| "arr_matmul" | "arr_transpose"
256256
| "arr_eye" | "arr_zeros_2d"
257+
// Forward-mode autograd duals (Track 2 — 2026-05-16)
258+
| "dual" | "dual_add" | "dual_sub"
259+
| "dual_mul" | "dual_div" | "dual_neg"
260+
| "dual_pow_int" | "dual_exp"
261+
| "dual_sin" | "dual_cos"
262+
| "dual_relu" | "dual_sigmoid" | "dual_tanh"
257263
// introspection
258264
| "defined_functions"
259265
// test runner: get_failures returns array of strings

0 commit comments

Comments
 (0)