/*
* Symbolic Morphology Engine — Latin verb morphology from raw letters to Boolean grammar
* Copyright (C) 2026 Ahmad Ali Parr
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
namespace Sovereign.Engine;
///
/// LEARNING LAYER — Differentiable computation graph.
///
/// Architecture:
/// 1. Character embedding table (learnable)
/// 2. Mean pooling over character positions (handles variable-length input)
/// 3. Hidden layer: z1 = W1·h + b1, a1 = tanh(z1)
/// 4. Output layer: z2 = W2·a1 + b2, ŷ = sigmoid(z2)
/// 5. Binary cross-entropy loss
/// 6. Manual chain-rule backpropagation
/// 7. Gradient descent parameter update
///
/// Every derivative is computed by hand. No autodiff framework.
///
public sealed class Engine
{
// ─── Hyperparameters ───────────────────────────────────────────
public readonly int EmbedDim;
public readonly int HiddenDim;
public readonly int OutputDim;
public readonly double LearningRate;
// ─── Learnable parameters ──────────────────────────────────────
// Embedding table: maps each uppercase letter to a vector
public readonly double[,] Embeddings; // [128, embedDim] (ASCII-indexed)
// Hidden layer: z1 = W1·h + b1
public readonly double[,] W1; // [hiddenDim, embedDim]
public readonly double[] B1; // [hiddenDim]
// Output layer: z2 = W2·a1 + b2
public readonly double[,] W2; // [outputDim, hiddenDim]
public readonly double[] B2; // [outputDim]
// ─── Cached forward state (for one sample) ─────────────────────
double[]? _charIndices;
double[,]? _embVecs;
double[]? _pooled;
double[]? _z1;
double[]? _a1;
double[]? _z2;
double[]? _yPred;
readonly Random _rng;
public Engine(int embedDim, int hiddenDim, int outputDim, double learningRate, int seed = 42)
{
EmbedDim = embedDim;
HiddenDim = hiddenDim;
OutputDim = outputDim;
LearningRate = learningRate;
_rng = new Random(seed);
Embeddings = new double[128, embedDim];
W1 = new double[hiddenDim, embedDim];
B1 = new double[hiddenDim];
W2 = new double[outputDim, hiddenDim];
B2 = new double[outputDim];
InitializeParameters();
}
// ─── Xavier/Glorot initialization ──────────────────────────────
// Scale = sqrt(2 / (fan_in + fan_out)) for tanh, sqrt(1/fan_in) for sigmoid
void InitializeParameters()
{
double embScale = Math.Sqrt(2.0 / (1 + EmbedDim));
for (int c = 0; c < 128; c++)
for (int d = 0; d < EmbedDim; d++)
Embeddings[c, d] = SampleNormal() * embScale;
double w1Scale = Math.Sqrt(2.0 / (EmbedDim + HiddenDim));
for (int i = 0; i < HiddenDim; i++)
for (int j = 0; j < EmbedDim; j++)
W1[i, j] = SampleNormal() * w1Scale;
double w2Scale = Math.Sqrt(2.0 / (HiddenDim + OutputDim));
for (int i = 0; i < OutputDim; i++)
for (int j = 0; j < HiddenDim; j++)
W2[i, j] = SampleNormal() * w2Scale;
}
double SampleNormal()
{
// Box-Muller transform: generate standard normal from uniform
double u1 = 1.0 - _rng.NextDouble(); // avoid log(0)
double u2 = _rng.NextDouble();
return Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Cos(2.0 * Math.PI * u2);
}
// ═══════════════════════════════════════════════════════════════
// FORWARD PASS
// ═══════════════════════════════════════════════════════════════
//
// For input word "AMO" (characters A, M, O):
//
// Step 1 — Embedding lookup:
// e_A = Embeddings['A'], e_M = Embeddings['M'], e_O = Embeddings['O']
//
// Step 2 — Mean pooling (variable-length → fixed-size):
// h = (e_A + e_M + e_O) / 3
//
// Step 3 — Hidden layer:
// z1 = W1·h + b1
// a1 = tanh(z1)
//
// Step 4 — Output layer:
// z2 = W2·a1 + b2
// ŷ = sigmoid(z2)
//
// ═══════════════════════════════════════════════════════════════
public double[] Forward(string word)
{
int len = word.Length;
_charIndices = new double[len];
_embVecs = new double[len, EmbedDim];
// Step 1: Character embedding lookup
for (int t = 0; t < len; t++)
{
int c = char.ToUpperInvariant(word[t]);
_charIndices[t] = c;
for (int d = 0; d < EmbedDim; d++)
_embVecs[t, d] = Embeddings[c, d];
}
// Step 2: Mean pooling — h = (1/N) Σ e_t
_pooled = new double[EmbedDim];
for (int d = 0; d < EmbedDim; d++)
{
double sum = 0;
for (int t = 0; t < len; t++)
sum += _embVecs[t, d];
_pooled[d] = sum / len;
}
// Step 3: Hidden layer — z1 = W1·h + b1, a1 = tanh(z1)
_z1 = new double[HiddenDim];
_a1 = new double[HiddenDim];
for (int i = 0; i < HiddenDim; i++)
{
double z = B1[i];
for (int j = 0; j < EmbedDim; j++)
z += W1[i, j] * _pooled[j];
_z1[i] = z;
_a1[i] = Math.Tanh(z);
}
// Step 4: Output layer — z2 = W2·a1 + b2, ŷ = sigmoid(z2)
_z2 = new double[OutputDim];
_yPred = new double[OutputDim];
for (int i = 0; i < OutputDim; i++)
{
double z = B2[i];
for (int j = 0; j < HiddenDim; j++)
z += W2[i, j] * _a1[j];
_z2[i] = z;
_yPred[i] = Sigmoid(z);
}
return _yPred;
}
// ═══════════════════════════════════════════════════════════════
// LOSS — Binary Cross-Entropy
// ═══════════════════════════════════════════════════════════════
//
// L = -(1/D) Σ_i [ y_i·log(ŷ_i) + (1-y_i)·log(1-ŷ_i) ]
//
// where D = number of output features (23)
//
// ═══════════════════════════════════════════════════════════════
public double ComputeLoss(double[] target)
{
double loss = 0;
const double eps = 1e-12;
for (int i = 0; i < OutputDim; i++)
{
double y = Math.Clamp(_yPred![i], eps, 1.0 - eps);
loss -= target[i] * Math.Log(y) + (1.0 - target[i]) * Math.Log(1.0 - y);
}
return loss / OutputDim;
}
// ═══════════════════════════════════════════════════════════════
// BACKWARD PASS — Classical Chain Rule
// ═══════════════════════════════════════════════════════════════
//
// For every operation in the forward graph, we compute the local
// derivative and propagate gradients backward:
//
// gradient_in = gradient_out × local_derivative
//
// At branching points (where a value feeds multiple paths):
//
// gradient = sum(all incoming gradient contributions)
//
// The full chain for this network:
//
// ∂L/∂z2 = (ŷ - y) / D [BCE + sigmoid]
// ∂L/∂W2 = (∂L/∂z2) · a1ᵀ [linear layer]
// ∂L/∂b2 = ∂L/∂z2 [bias]
// ∂L/∂a1 = W2ᵀ · (∂L/∂z2) [linear layer]
// ∂L/∂z1 = (∂L/∂a1) ⊙ (1 - a1²) [tanh activation]
// ∂L/∂W1 = (∂L/∂z1) · hᵀ [linear layer]
// ∂L/∂b1 = ∂L/∂z1 [bias]
// ∂L/∂h = W1ᵀ · (∂L/∂z1) [linear layer]
// ∂L/∂e_t = (1/N) · ∂L/∂h [mean pooling]
//
// ═══════════════════════════════════════════════════════════════
public (double[] GradW1, double[] GradB1, double[] GradW2, double[] GradB2, double GradNorm) Backward(double[] target)
{
int len = _charIndices!.Length;
double invD = 1.0 / OutputDim;
double invN = 1.0 / len;
// ── dL/dz2 = (ŷ - y) / D ──────────────────────────────────
// Local derivative of BCE w.r.t. z2 (through sigmoid):
// dL/dŷ = -(y/ŷ) + (1-y)/(1-ŷ)
// dŷ/dz2 = ŷ(1-ŷ)
// dL/dz2 = dL/dŷ × dŷ/dz2 = (ŷ - y) / D
var dL_dz2 = new double[OutputDim];
for (int i = 0; i < OutputDim; i++)
dL_dz2[i] = (_yPred![i] - target[i]) * invD;
// ── dL/dW2[i,j] = dL/dz2[i] × a1[j] ──────────────────────
// z2[i] = Σ_j W2[i,j]·a1[j] + b2[i]
// Local derivative: ∂z2[i]/∂W2[i,j] = a1[j]
var gradW2 = new double[OutputDim * HiddenDim];
for (int i = 0; i < OutputDim; i++)
for (int j = 0; j < HiddenDim; j++)
gradW2[i * HiddenDim + j] = dL_dz2[i] * _a1![j];
// ── dL/db2[i] = dL/dz2[i] ─────────────────────────────────
// ∂z2[i]/∂b2[i] = 1
var gradB2 = new double[OutputDim];
Array.Copy(dL_dz2, gradB2, OutputDim);
// ── dL/da1[j] = Σ_i W2[i,j] × dL/dz2[i] ──────────────────
// Branching point: a1 feeds all output dimensions.
// gradient = sum of all incoming contributions
var dL_da1 = new double[HiddenDim];
for (int j = 0; j < HiddenDim; j++)
{
double sum = 0;
for (int i = 0; i < OutputDim; i++)
sum += W2[i, j] * dL_dz2[i];
dL_da1[j] = sum;
}
// ── dL/dz1[j] = dL/da1[j] × (1 - a1[j]²) ────────────────
// Local derivative of tanh: d(tanh(z))/dz = 1 - tanh(z)²
var dL_dz1 = new double[HiddenDim];
for (int j = 0; j < HiddenDim; j++)
dL_dz1[j] = dL_da1[j] * (1.0 - _a1![j] * _a1[j]);
// ── dL/dW1[i,j] = dL/dz1[i] × h[j] ───────────────────────
var gradW1 = new double[HiddenDim * EmbedDim];
for (int i = 0; i < HiddenDim; i++)
for (int j = 0; j < EmbedDim; j++)
gradW1[i * EmbedDim + j] = dL_dz1[i] * _pooled![j];
// ── dL/db1[i] = dL/dz1[i] ─────────────────────────────────
var gradB1 = new double[HiddenDim];
Array.Copy(dL_dz1, gradB1, HiddenDim);
// ── dL/dh[j] = Σ_i W1[i,j] × dL/dz1[i] ──────────────────
// Another branching point: h feeds all hidden dimensions.
var dL_dh = new double[EmbedDim];
for (int j = 0; j < EmbedDim; j++)
{
double sum = 0;
for (int i = 0; i < HiddenDim; i++)
sum += W1[i, j] * dL_dz1[i];
dL_dh[j] = sum;
}
// ── dL/de_t[d] = (1/N) × dL/dh[d] ────────────────────────
// Mean pooling: h[d] = (1/N) Σ_t e_t[d]
// Local derivative: ∂h[d]/∂e_t[d] = 1/N for each t
var gradEmbed = new double[len, EmbedDim];
for (int t = 0; t < len; t++)
for (int d = 0; d < EmbedDim; d++)
gradEmbed[t, d] = invN * dL_dh[d];
// Accumulate embedding gradients into the table
for (int t = 0; t < len; t++)
{
int c = (int)_charIndices![t];
for (int d = 0; d < EmbedDim; d++)
Embeddings[c, d] -= LearningRate * gradEmbed[t, d];
}
// Compute gradient norm (for monitoring convergence)
double gradNorm = 0;
for (int i = 0; i < gradW1.Length; i++) gradNorm += gradW1[i] * gradW1[i];
for (int i = 0; i < gradB1.Length; i++) gradNorm += gradB1[i] * gradB1[i];
for (int i = 0; i < gradW2.Length; i++) gradNorm += gradW2[i] * gradW2[i];
for (int i = 0; i < gradB2.Length; i++) gradNorm += gradB2[i] * gradB2[i];
gradNorm = Math.Sqrt(gradNorm);
return (gradW1, gradB1, gradW2, gradB2, gradNorm);
}
// ═══════════════════════════════════════════════════════════════
// GRADIENT DESCENT STEP
// ═══════════════════════════════════════════════════════════════
//
// θ := θ - η × ∇θL
//
// Embeddings already updated in Backward (for efficiency).
// Here we update W1, b1, W2, b2.
//
// ═══════════════════════════════════════════════════════════════
public void GradientDescentStep(double[] gradW1, double[] gradB1, double[] gradW2, double[] gradB2)
{
for (int i = 0; i < HiddenDim; i++)
{
B1[i] -= LearningRate * gradB1[i];
for (int j = 0; j < EmbedDim; j++)
W1[i, j] -= LearningRate * gradW1[i * EmbedDim + j];
}
for (int i = 0; i < OutputDim; i++)
{
B2[i] -= LearningRate * gradB2[i];
for (int j = 0; j < HiddenDim; j++)
W2[i, j] -= LearningRate * gradW2[i * HiddenDim + j];
}
}
// ─── Activation functions ──────────────────────────────────────
static double Sigmoid(double x)
{
// Numerically stable sigmoid
if (x >= 0)
{
double e = Math.Exp(-x);
return 1.0 / (1.0 + e);
}
else
{
double e = Math.Exp(x);
return e / (1.0 + e);
}
}
}