// demo_neuralODE_theorem_382A_main.c (Zorro lite-C)
// Minimal Neural ODE example + Theorem 382A demo:
// - dh/dt = f_theta(t,h) where f_theta is a tiny "neural net" (hard-coded weights)
// - Integrate with composition: RK4 step then Heun RK2 step
// - Build stage-permuted equivalents of both methods
// - Show composed result is unchanged (Theorem 382A)
// Runs in main() (no bars/history). No malloc. No typedef unsigned.
#define MAXS 4
#define TRI_A 6 // for MAXS=4: 4*3/2 = 6
static var G_k[MAXS];
// ---- RK tableaus: Heun2 (M1) and RK4 (M2) + permuted versions (M1B, M2B)
static int M1_s = 0; static float M1_A[TRI_A]; static float M1_b[MAXS]; static float M1_c[MAXS];
static int M2_s = 0; static float M2_A[TRI_A]; static float M2_b[MAXS]; static float M2_c[MAXS];
static int M1B_s = 0; static float M1B_A[TRI_A]; static float M1B_b[MAXS]; static float M1B_c[MAXS];
static int M2B_s = 0; static float M2B_A[TRI_A]; static float M2B_b[MAXS]; static float M2B_c[MAXS];
// ----------------- Triangular indexing (explicit RK uses only i>j) -----------------
int Aidx(int i, int j)
{
return (i*(i-1))/2 + j;
}
var getA(float* Atri, int i, int j)
{
if(i <= j) return 0;
return (var)Atri[Aidx(i,j)];
}
void setA(float* Atri, int i, int j, var v)
{
if(i <= j) return;
Atri[Aidx(i,j)] = (float)v;
}
void zeroTableau(int* s, float* Atri, float* b, float* c)
{
int i;
*s = 0;
for(i=0;i<TRI_A;i++) Atri[i] = 0;
for(i=0;i<MAXS;i++) { b[i]=0; c[i]=0; }
}
// ----------------- "Neural network" for the Neural ODE -----------------
// Hidden state: h(t) (scalar for simplicity)
// dh/dt = f_theta(t,h)
//
// We avoid fancy ops and use a safe activation: softsign(z) = z / (1 + abs(z))
var softsign(var z)
{
return z / (1.0 + abs(z));
}
// Hard-coded weights (this stands in for a trained NN)
var f_theta(var t, var h)
{
// "features" could be more inputs; here we just use t and h
// layer1: z = w_h*h + w_t*t + b
var z = 1.20*h + 0.70*t - 0.30;
// activation
var a = softsign(z);
// output layer: dh/dt = w*a + b2
return 0.90*a + 0.10;
}
// ----------------- One explicit RK step for Neural ODE -----------------
var rk_step(int s, float* Atri, float* b, float* c, var t, var y, var h)
{
int i,j;
if(s < 1) return y;
if(s > MAXS) s = MAXS;
for(i=0;i<s;i++) G_k[i] = 0;
for(i=0;i<s;i++)
{
var yi = y;
for(j=0;j<i;j++)
yi += h * getA(Atri,i,j) * G_k[j];
G_k[i] = f_theta(t + (var)c[i]*h, yi);
}
var y1 = y;
for(i=0;i<s;i++)
y1 += h * (var)b[i] * G_k[i];
return y1;
}
// ----------------- Composition: do method2 step, then method1 step -----------------
// IMPORTANT for time-dependent ODE: the second step starts at t+h.
var compose_step(int s1,float* A1,float* b1,float* c1,
int s2,float* A2,float* b2,float* c2,
var t, var y, var h)
{
var y_mid = rk_step(s2,A2,b2,c2,t, y, h);
var y_out = rk_step(s1,A1,b1,c1,t+h, y_mid, h);
return y_out;
}
// ----------------- Stage permutation (build equivalent tableau) -----------------
void permute_method_into(int s, float* A, float* b, float* c,
int* p,
int* sOut, float* Aout, float* bout, float* cout)
{
int i,j;
*sOut = s;
if(*sOut < 0) *sOut = 0;
if(*sOut > MAXS) *sOut = MAXS;
for(i=0;i<TRI_A;i++) Aout[i] = 0;
for(i=0;i<MAXS;i++) { bout[i]=0; cout[i]=0; }
// permute b,c
for(i=0;i<*sOut;i++)
{
int pi = p[i];
if(pi < 0) pi = 0;
if(pi >= s) pi = s-1;
bout[i] = b[pi];
cout[i] = c[pi];
}
// permute A (lower triangle only)
for(i=0;i<*sOut;i++)
for(j=0;j<i;j++)
{
int pi = p[i];
int pj = p[j];
if(pi < 0) pi = 0;
if(pj < 0) pj = 0;
if(pi >= s) pi = s-1;
if(pj >= s) pj = s-1;
setA(Aout, i, j, getA(A, pi, pj));
}
}
// ----------------- Build Heun RK2 and classic RK4 -----------------
void make_heun_rk2()
{
zeroTableau(&M1_s, M1_A, M1_b, M1_c);
M1_s = 2;
M1_c[0] = 0.0; M1_c[1] = 1.0;
setA(M1_A, 1, 0, 1.0);
M1_b[0] = 0.5; M1_b[1] = 0.5;
}
void make_rk4()
{
zeroTableau(&M2_s, M2_A, M2_b, M2_c);
M2_s = 4;
M2_c[0]=0.0; M2_c[1]=0.5; M2_c[2]=0.5; M2_c[3]=1.0;
setA(M2_A, 1, 0, 0.5);
setA(M2_A, 2, 1, 0.5);
setA(M2_A, 3, 2, 1.0);
M2_b[0]=(float)(1./6.);
M2_b[1]=(float)(1./3.);
M2_b[2]=(float)(1./3.);
M2_b[3]=(float)(1./6.);
}
void buildAll()
{
make_heun_rk2();
make_rk4();
// Example stage permutations:
// Heun has 2 stages: swap them (1,0)
int p1[MAXS]; p1[0]=1; p1[1]=0; p1[2]=0; p1[3]=0;
// RK4 has 4 stages: swap middle two (0,2,1,3)
int p2[MAXS]; p2[0]=0; p2[1]=2; p2[2]=1; p2[3]=3;
permute_method_into(M1_s, M1_A, M1_b, M1_c, p1, &M1B_s, M1B_A, M1B_b, M1B_c);
permute_method_into(M2_s, M2_A, M2_b, M2_c, p2, &M2B_s, M2B_A, M2B_b, M2B_c);
}
// ----------------- Entry point -----------------
function main()
{
buildAll();
// Neural ODE initial condition (hidden state)
var t0 = 0.0;
var h0 = 0.25;
// One composed "macro step": RK4 step of size dt, then Heun step of size dt
// Total time advanced = 2*dt because we advance t by dt between the two steps.
var dt = 0.10;
// Original composition
var hA = compose_step(M1_s, M1_A, M1_b, M1_c,
M2_s, M2_A, M2_b, M2_c,
t0, h0, dt);
// Permuted-but-equivalent composition (Theorem 382A says this should match)
var hB = compose_step(M1B_s, M1B_A, M1B_b, M1B_c,
M2B_s, M2B_A, M2B_b, M2B_c,
t0, h0, dt);
printf("\nNeural ODE demo: dh/dt = f_theta(t,h)");
printf("\nStart: t=%.3f h=%.17g", t0, h0);
printf("\nStep: RK4(dt) then Heun(dt), dt=%.3f (total time +%.3f)", dt, 2*dt);
printf("\n\nhA = compose(original RK4, original Heun) = %.17g", hA);
printf("\nhB = compose(permuted RK4, permuted Heun) = %.17g", hB);
printf("\nabs diff = %.3e\n", abs(hA - hB));
quit("done");
}