Skip to main content

oxiphysics_gpu/neural_compute/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::collections::HashMap;
6use std::f64::consts::PI as PI_F64;
7
8use super::functions::scaled_dot_product_attention;
9
10/// Batch normalization layer (inference mode).
11///
12/// Normalizes input features using stored running mean and variance,
13/// then applies learned scale (gamma) and shift (beta).
14#[derive(Debug, Clone)]
15pub struct BatchNormLayer {
16    /// Running mean for each feature.
17    pub running_mean: Vec<f32>,
18    /// Running variance for each feature.
19    pub running_var: Vec<f32>,
20    /// Learned scale parameter (gamma).
21    pub gamma: Vec<f32>,
22    /// Learned shift parameter (beta).
23    pub beta: Vec<f32>,
24    /// Small constant for numerical stability.
25    pub epsilon: f32,
26    /// Number of features.
27    pub n_features: usize,
28}
29impl BatchNormLayer {
30    /// Create a new batch norm layer with identity transform (gamma=1, beta=0).
31    pub fn new(n_features: usize) -> Self {
32        Self {
33            running_mean: vec![0.0; n_features],
34            running_var: vec![1.0; n_features],
35            gamma: vec![1.0; n_features],
36            beta: vec![0.0; n_features],
37            epsilon: 1e-5,
38            n_features,
39        }
40    }
41    /// Apply batch normalization in inference mode.
42    ///
43    /// output\[i\] = gamma\[i\] * (input\[i\] - mean\[i\]) / sqrt(var\[i\] + eps) + beta\[i\]
44    pub fn forward(&self, input: &[f32]) -> Vec<f32> {
45        assert_eq!(input.len(), self.n_features);
46        (0..self.n_features)
47            .map(|i| {
48                let normalized =
49                    (input[i] - self.running_mean[i]) / (self.running_var[i] + self.epsilon).sqrt();
50                self.gamma[i] * normalized + self.beta[i]
51            })
52            .collect()
53    }
54    /// Set the running statistics.
55    pub fn set_stats(&mut self, mean: &[f32], var: &[f32]) {
56        assert_eq!(mean.len(), self.n_features);
57        assert_eq!(var.len(), self.n_features);
58        self.running_mean.copy_from_slice(mean);
59        self.running_var.copy_from_slice(var);
60    }
61    /// Set the affine parameters.
62    pub fn set_affine(&mut self, gamma: &[f32], beta: &[f32]) {
63        assert_eq!(gamma.len(), self.n_features);
64        assert_eq!(beta.len(), self.n_features);
65        self.gamma.copy_from_slice(gamma);
66        self.beta.copy_from_slice(beta);
67    }
68}
69impl BatchNormLayer {
70    /// Update running statistics from a mini-batch (training mode).
71    ///
72    /// Uses exponential moving average:
73    /// `running_mean = (1-momentum) * running_mean + momentum * batch_mean`
74    ///
75    /// # Panics
76    /// Panics if `batch` is empty or if any sample has the wrong feature count.
77    pub fn update_running_stats(&mut self, batch: &[Vec<f32>], momentum: f32) {
78        assert!(
79            !batch.is_empty(),
80            "update_running_stats: batch must not be empty"
81        );
82        let n = batch.len() as f32;
83        let mut batch_mean = vec![0.0_f32; self.n_features];
84        for sample in batch {
85            assert_eq!(sample.len(), self.n_features, "sample length mismatch");
86            for (k, &v) in sample.iter().enumerate() {
87                batch_mean[k] += v;
88            }
89        }
90        for m in &mut batch_mean {
91            *m /= n;
92        }
93        let mut batch_var = vec![0.0_f32; self.n_features];
94        for sample in batch {
95            for (k, &v) in sample.iter().enumerate() {
96                let d = v - batch_mean[k];
97                batch_var[k] += d * d;
98            }
99        }
100        for v in &mut batch_var {
101            *v /= n;
102        }
103        for k in 0..self.n_features {
104            self.running_mean[k] =
105                (1.0 - momentum) * self.running_mean[k] + momentum * batch_mean[k];
106            self.running_var[k] = (1.0 - momentum) * self.running_var[k] + momentum * batch_var[k];
107        }
108    }
109}
110/// A single-step Elman RNN cell:
111/// `h_t = activation(W_x * x_t + W_h * h_{t-1} + b)`.
112#[derive(Debug, Clone)]
113pub struct RnnCell {
114    /// Input-to-hidden weight matrix `[hidden_size × input_size]`.
115    pub w_x: Vec<f64>,
116    /// Hidden-to-hidden weight matrix `[hidden_size × hidden_size]`.
117    pub w_h: Vec<f64>,
118    /// Bias vector `[hidden_size]`.
119    pub b: Vec<f64>,
120    /// Input dimensionality.
121    pub input_size: usize,
122    /// Hidden state dimensionality.
123    pub hidden_size: usize,
124    /// Activation function.
125    pub activation: ExtActivation,
126}
127impl RnnCell {
128    /// Create a new RNN cell with zero weights.
129    pub fn new(input_size: usize, hidden_size: usize, activation: ExtActivation) -> Self {
130        Self {
131            w_x: vec![0.0_f64; hidden_size * input_size],
132            w_h: vec![0.0_f64; hidden_size * hidden_size],
133            b: vec![0.0_f64; hidden_size],
134            input_size,
135            hidden_size,
136            activation,
137        }
138    }
139    /// One forward step.
140    ///
141    /// Returns the new hidden state `h_t` of length `hidden_size`.
142    pub fn step(&self, x: &[f64], h_prev: &[f64]) -> Vec<f64> {
143        assert_eq!(x.len(), self.input_size);
144        assert_eq!(h_prev.len(), self.hidden_size);
145        (0..self.hidden_size)
146            .map(|o| {
147                let mut acc = self.b[o];
148                for (i, &xi) in x.iter().enumerate() {
149                    acc += self.w_x[o * self.input_size + i] * xi;
150                }
151                for (i, &hi) in h_prev.iter().enumerate() {
152                    acc += self.w_h[o * self.hidden_size + i] * hi;
153                }
154                self.activation.apply(acc)
155            })
156            .collect()
157    }
158    /// Run the RNN over a full sequence `[seq_len][input_size]`.
159    ///
160    /// Returns all hidden states `[seq_len][hidden_size]`.
161    pub fn forward_sequence(&self, sequence: &[Vec<f64>]) -> Vec<Vec<f64>> {
162        let mut h = vec![0.0_f64; self.hidden_size];
163        let mut hidden_states = Vec::with_capacity(sequence.len());
164        for x in sequence {
165            h = self.step(x, &h);
166            hidden_states.push(h.clone());
167        }
168        hidden_states
169    }
170}
171/// An inference pipeline that chains DenseLayer and BatchNormLayer operations.
172#[derive(Debug, Clone)]
173pub struct InferencePipeline {
174    /// Ordered list of operations.
175    pub ops: Vec<InferenceOp>,
176}
177impl InferencePipeline {
178    /// Create an empty pipeline.
179    pub fn new() -> Self {
180        Self { ops: Vec::new() }
181    }
182    /// Add an operation to the pipeline.
183    pub fn add_op(&mut self, op: InferenceOp) {
184        self.ops.push(op);
185    }
186    /// Run forward pass through all operations.
187    pub fn forward(&self, input: &[f32]) -> Vec<f32> {
188        let mut current = input.to_vec();
189        for op in &self.ops {
190            current = match op {
191                InferenceOp::Dense(layer) => layer.forward(&current),
192                InferenceOp::BatchNorm(bn) => bn.forward(&current),
193                InferenceOp::Activation(act) => current.iter().map(|&x| act.apply(x)).collect(),
194            };
195        }
196        current
197    }
198    /// Total number of trainable parameters.
199    pub fn total_parameters(&self) -> usize {
200        self.ops
201            .iter()
202            .map(|op| match op {
203                InferenceOp::Dense(layer) => layer.parameter_count(),
204                InferenceOp::BatchNorm(bn) => 2 * bn.n_features,
205                InferenceOp::Activation(_) => 0,
206            })
207            .sum()
208    }
209}
210/// Convenience builder for standard AANN architectures.
211pub struct NetworkBuilder;
212impl NetworkBuilder {
213    /// Build a simple element-specific network:
214    ///
215    /// `n_descriptors → hidden[0] (Tanh) → hidden[1] (Tanh) → … → 1 (Linear)`
216    ///
217    /// At least one hidden size must be provided.
218    pub fn simple_aann(
219        n_descriptors: usize,
220        hidden_sizes: &[usize],
221        _element: u8,
222    ) -> FeedForwardNet {
223        let mut net = FeedForwardNet::new();
224        let mut prev = n_descriptors;
225        for &h in hidden_sizes {
226            net.add_layer(DenseLayer::new(prev, h, ActivationFn::Tanh));
227            prev = h;
228        }
229        net.add_layer(DenseLayer::new(prev, 1, ActivationFn::Linear));
230        net
231    }
232}
233/// Position-wise feed-forward network used inside a transformer block.
234///
235/// FFN(x) = max(0, x W1 + b1) W2 + b2
236///
237/// Applied identically to each position in the sequence.
238#[derive(Debug, Clone)]
239pub struct TransformerFfn {
240    /// Input / output dimensionality.
241    pub d_model: usize,
242    /// Inner (hidden) dimensionality.
243    pub d_ff: usize,
244    /// W1: \[d_ff × d_model\]
245    pub w1: Vec<f64>,
246    /// b1: \[d_ff\]
247    pub b1: Vec<f64>,
248    /// W2: \[d_model × d_ff\]
249    pub w2: Vec<f64>,
250    /// b2: \[d_model\]
251    pub b2: Vec<f64>,
252}
253impl TransformerFfn {
254    /// Create with zero weights.
255    pub fn new(d_model: usize, d_ff: usize) -> Self {
256        Self {
257            d_model,
258            d_ff,
259            w1: vec![0.0_f64; d_ff * d_model],
260            b1: vec![0.0_f64; d_ff],
261            w2: vec![0.0_f64; d_model * d_ff],
262            b2: vec![0.0_f64; d_model],
263        }
264    }
265    /// Forward pass over a sequence `[seq_len × d_model]` (flat row-major).
266    pub fn forward(&self, x: &[f64], seq_len: usize) -> Vec<f64> {
267        let dm = self.d_model;
268        let df = self.d_ff;
269        let mut out = vec![0.0_f64; seq_len * dm];
270        for t in 0..seq_len {
271            let mut hidden = vec![0.0_f64; df];
272            for (j, h_j) in hidden.iter_mut().enumerate() {
273                let mut acc = self.b1[j];
274                for (i, &xi) in x[t * dm..t * dm + dm].iter().enumerate() {
275                    acc += xi * self.w1[j * dm + i];
276                }
277                *h_j = acc.max(0.0);
278            }
279            for (j, out_j) in out[t * dm..t * dm + dm].iter_mut().enumerate() {
280                let mut acc = self.b2[j];
281                for (i, &hi) in hidden.iter().enumerate() {
282                    acc += hi * self.w2[j * df + i];
283                }
284                *out_j = acc;
285            }
286        }
287        out
288    }
289}
290/// A 1-D convolutional layer operating on a sequence of feature vectors.
291///
292/// Applies a set of `out_channels` filters, each of length `kernel_size`
293/// spanning `in_channels` input channels, using causal (left) padding so the
294/// output length equals the input length.
295///
296/// Layout:
297/// - `weights[o][k][c]` = weight for output channel `o`, kernel position `k`,
298///   input channel `c`.
299/// - `biases[o]` = bias for output channel `o`.
300#[derive(Debug, Clone)]
301pub struct Conv1DLayer {
302    /// Number of input channels per time step.
303    pub in_channels: usize,
304    /// Number of output channels per time step.
305    pub out_channels: usize,
306    /// Kernel (filter) length along the time axis.
307    pub kernel_size: usize,
308    /// Filter weights: `weights[out_ch][kernel_pos][in_ch]`.
309    pub weights: Vec<Vec<Vec<f64>>>,
310    /// Bias per output channel.
311    pub biases: Vec<f64>,
312    /// Activation function applied after convolution.
313    pub activation: ExtActivation,
314}
315impl Conv1DLayer {
316    /// Create a new Conv1D layer with zero-initialised weights.
317    pub fn new(
318        in_channels: usize,
319        out_channels: usize,
320        kernel_size: usize,
321        activation: ExtActivation,
322    ) -> Self {
323        let weights = vec![vec![vec![0.0_f64; in_channels]; kernel_size]; out_channels];
324        let biases = vec![0.0_f64; out_channels];
325        Self {
326            in_channels,
327            out_channels,
328            kernel_size,
329            weights,
330            biases,
331            activation,
332        }
333    }
334    /// Forward pass.
335    ///
336    /// `input` has shape `[seq_len][in_channels]`.  Returns a tensor of shape
337    /// `[seq_len][out_channels]` using causal (left-zero) padding.
338    pub fn forward(&self, input: &[Vec<f64>]) -> Vec<Vec<f64>> {
339        let seq_len = input.len();
340        let mut output = vec![vec![0.0_f64; self.out_channels]; seq_len];
341        for (t, out_t) in output.iter_mut().enumerate() {
342            for (o, out_to) in out_t.iter_mut().enumerate() {
343                let mut acc = self.biases[o];
344                for k in 0..self.kernel_size {
345                    let src_t = t as isize - k as isize;
346                    if src_t < 0 {
347                        continue;
348                    }
349                    let src_t = src_t as usize;
350                    for (c, &inp) in input[src_t].iter().enumerate() {
351                        acc += self.weights[o][k][c] * inp;
352                    }
353                }
354                *out_to = self.activation.apply(acc);
355            }
356        }
357        output
358    }
359    /// Total number of trainable parameters.
360    pub fn num_params(&self) -> usize {
361        self.out_channels * self.kernel_size * self.in_channels + self.out_channels
362    }
363}
364/// A single operation in the inference pipeline.
365#[derive(Debug, Clone)]
366pub enum InferenceOp {
367    /// Dense (fully-connected) layer.
368    Dense(DenseLayer),
369    /// Batch normalization layer.
370    BatchNorm(BatchNormLayer),
371    /// Activation function (standalone).
372    Activation(ActivationFn),
373}
374/// Adam optimizer for a flat parameter vector.
375///
376/// Reference: Kingma & Ba (2015) "Adam: A Method for Stochastic Optimization".
377#[derive(Debug, Clone)]
378pub struct AdamOptimizer {
379    /// Learning rate α.
380    pub lr: f64,
381    /// Exponential decay rate for first moment estimates.
382    pub beta1: f64,
383    /// Exponential decay rate for second moment estimates.
384    pub beta2: f64,
385    /// Small constant for numerical stability.
386    pub epsilon: f64,
387    /// First moment vector (m).
388    pub m: Vec<f64>,
389    /// Second moment vector (v).
390    pub v: Vec<f64>,
391    /// Current step count (t).
392    pub step: u64,
393}
394impl AdamOptimizer {
395    /// Create a new Adam optimizer for a parameter vector of length `n_params`.
396    pub fn new(n_params: usize, lr: f64, beta1: f64, beta2: f64, epsilon: f64) -> Self {
397        Self {
398            lr,
399            beta1,
400            beta2,
401            epsilon,
402            m: vec![0.0; n_params],
403            v: vec![0.0; n_params],
404            step: 0,
405        }
406    }
407    /// Create an Adam optimizer with default hyperparameters (lr=1e-3, β1=0.9, β2=0.999, ε=1e-8).
408    pub fn default_params(n_params: usize) -> Self {
409        Self::new(n_params, 1e-3, 0.9, 0.999, 1e-8)
410    }
411    /// Apply one Adam update step to `params` using `grads`.
412    ///
413    /// Updates `params` in-place and increments the step counter.
414    pub fn step_update(&mut self, params: &mut [f64], grads: &[f64]) {
415        assert_eq!(
416            params.len(),
417            self.m.len(),
418            "AdamOptimizer::step_update: params/m length mismatch"
419        );
420        assert_eq!(
421            grads.len(),
422            self.m.len(),
423            "AdamOptimizer::step_update: grads/m length mismatch"
424        );
425        self.step += 1;
426        let t = self.step as f64;
427        let bias_corr1 = 1.0 - self.beta1.powf(t);
428        let bias_corr2 = 1.0 - self.beta2.powf(t);
429        for i in 0..params.len() {
430            self.m[i] = self.beta1 * self.m[i] + (1.0 - self.beta1) * grads[i];
431            self.v[i] = self.beta2 * self.v[i] + (1.0 - self.beta2) * grads[i] * grads[i];
432            let m_hat = self.m[i] / bias_corr1;
433            let v_hat = self.v[i] / bias_corr2;
434            params[i] -= self.lr * m_hat / (v_hat.sqrt() + self.epsilon);
435        }
436    }
437    /// Reset state (moments and step counter) to zero.
438    pub fn reset(&mut self) {
439        self.m.iter_mut().for_each(|x| *x = 0.0);
440        self.v.iter_mut().for_each(|x| *x = 0.0);
441        self.step = 0;
442    }
443}
444/// A single graph neural network layer implementing the sum-aggregation
445/// message passing update:
446///
447/// h_i^(l+1) = σ(W_self * h_i^(l) + W_neigh * Σ_{j ∈ N(i)} h_j^(l) + b)
448///
449/// All nodes share the same weight matrices.
450#[derive(Debug, Clone)]
451pub struct GnnLayer {
452    /// Input feature dimension.
453    pub in_dim: usize,
454    /// Output feature dimension.
455    pub out_dim: usize,
456    /// Self-loop weight matrix W_self \[out_dim × in_dim\].
457    pub w_self: Vec<f64>,
458    /// Neighbour aggregation weight matrix W_neigh \[out_dim × in_dim\].
459    pub w_neigh: Vec<f64>,
460    /// Bias vector \[out_dim\].
461    pub bias: Vec<f64>,
462    /// Activation function.
463    pub activation: ExtActivation,
464}
465impl GnnLayer {
466    /// Create a new GNN layer with zero-initialised weights.
467    pub fn new(in_dim: usize, out_dim: usize, activation: ExtActivation) -> Self {
468        Self {
469            in_dim,
470            out_dim,
471            w_self: vec![0.0_f64; out_dim * in_dim],
472            w_neigh: vec![0.0_f64; out_dim * in_dim],
473            bias: vec![0.0_f64; out_dim],
474            activation,
475        }
476    }
477    /// Forward pass.
478    ///
479    /// * `node_feats` – `[n_nodes × in_dim]` flat row-major node feature matrix.
480    /// * `adj`        – adjacency list: `adj[i]` contains the neighbour indices of node `i`.
481    ///
482    /// Returns `[n_nodes × out_dim]` flat row-major.
483    pub fn forward(&self, node_feats: &[f64], n_nodes: usize, adj: &[Vec<usize>]) -> Vec<f64> {
484        assert_eq!(node_feats.len(), n_nodes * self.in_dim);
485        assert_eq!(adj.len(), n_nodes);
486        let in_d = self.in_dim;
487        let out_d = self.out_dim;
488        let mut out = vec![0.0_f64; n_nodes * out_d];
489        for i in 0..n_nodes {
490            let h_self = &node_feats[i * in_d..(i + 1) * in_d];
491            let mut agg = vec![0.0_f64; in_d];
492            for &j in &adj[i] {
493                let h_j = &node_feats[j * in_d..(j + 1) * in_d];
494                for d in 0..in_d {
495                    agg[d] += h_j[d];
496                }
497            }
498            for o in 0..out_d {
499                let mut acc = self.bias[o];
500                for d in 0..in_d {
501                    acc += self.w_self[o * in_d + d] * h_self[d];
502                    acc += self.w_neigh[o * in_d + d] * agg[d];
503                }
504                out[i * out_d + o] = self.activation.apply(acc);
505            }
506        }
507        out
508    }
509    /// Total number of trainable parameters.
510    pub fn num_params(&self) -> usize {
511        2 * self.out_dim * self.in_dim + self.out_dim
512    }
513}
514/// A multi-layer message passing neural network stacking `GnnLayer`s.
515#[derive(Debug, Clone)]
516pub struct MessagePassingNet {
517    /// Ordered list of GNN layers.
518    pub layers: Vec<GnnLayer>,
519}
520impl MessagePassingNet {
521    /// Create an empty MPNN.
522    pub fn new() -> Self {
523        Self { layers: Vec::new() }
524    }
525    /// Add a GNN layer to the stack.
526    pub fn add_layer(&mut self, layer: GnnLayer) {
527        self.layers.push(layer);
528    }
529    /// Run all layers in sequence over a fixed graph.
530    ///
531    /// Returns the final node feature matrix `[n_nodes × last_out_dim]`.
532    pub fn forward(&self, node_feats: &[f64], n_nodes: usize, adj: &[Vec<usize>]) -> Vec<f64> {
533        let mut h = node_feats.to_vec();
534        for layer in &self.layers {
535            h = layer.forward(&h, n_nodes, adj);
536        }
537        h
538    }
539    /// Aggregate node features to a single graph-level representation (mean pooling).
540    pub fn global_mean_pool(&self, node_feats: &[f64], n_nodes: usize, out_dim: usize) -> Vec<f64> {
541        if n_nodes == 0 {
542            return vec![0.0_f64; out_dim];
543        }
544        let mut pooled = vec![0.0_f64; out_dim];
545        for i in 0..n_nodes {
546            for d in 0..out_dim {
547                pooled[d] += node_feats[i * out_dim + d];
548            }
549        }
550        let inv_n = 1.0 / n_nodes as f64;
551        for v in &mut pooled {
552            *v *= inv_n;
553        }
554        pooled
555    }
556}
557/// Accumulates gradients from multiple backward passes for mini-batch training.
558#[derive(Debug, Clone)]
559pub struct GradAccumulator {
560    /// Accumulated weight gradients.
561    pub grad_weights: Vec<f64>,
562    /// Accumulated bias gradients.
563    pub grad_biases: Vec<f64>,
564    /// Number of samples accumulated.
565    pub count: usize,
566}
567impl GradAccumulator {
568    /// Create a new accumulator sized for `n_weights` weights and `n_biases` biases.
569    pub fn new(n_weights: usize, n_biases: usize) -> Self {
570        Self {
571            grad_weights: vec![0.0; n_weights],
572            grad_biases: vec![0.0; n_biases],
573            count: 0,
574        }
575    }
576    /// Add a set of gradients (accumulate without dividing).
577    pub fn accumulate(&mut self, gw: &[f64], gb: &[f64]) {
578        assert_eq!(gw.len(), self.grad_weights.len());
579        assert_eq!(gb.len(), self.grad_biases.len());
580        for (acc, &g) in self.grad_weights.iter_mut().zip(gw.iter()) {
581            *acc += g;
582        }
583        for (acc, &g) in self.grad_biases.iter_mut().zip(gb.iter()) {
584            *acc += g;
585        }
586        self.count += 1;
587    }
588    /// Compute mean gradients (divide by count) and return them.
589    pub fn mean_grads(&self) -> (Vec<f64>, Vec<f64>) {
590        let n = self.count.max(1) as f64;
591        let gw: Vec<f64> = self.grad_weights.iter().map(|&g| g / n).collect();
592        let gb: Vec<f64> = self.grad_biases.iter().map(|&g| g / n).collect();
593        (gw, gb)
594    }
595    /// Zero all accumulated gradients and reset count.
596    pub fn zero(&mut self) {
597        self.grad_weights.iter_mut().for_each(|x| *x = 0.0);
598        self.grad_biases.iter_mut().for_each(|x| *x = 0.0);
599        self.count = 0;
600    }
601}
602/// Multi-head attention module.
603///
604/// Projects Q, K, V with learned linear projections, runs `n_heads`
605/// parallel attention heads, then concatenates and projects the output.
606///
607/// All weight matrices are stored flat row-major.
608#[derive(Debug, Clone)]
609pub struct MultiHeadAttention {
610    /// Model dimensionality.
611    pub d_model: usize,
612    /// Number of attention heads.
613    pub n_heads: usize,
614    /// Dimensionality per head: `d_model / n_heads`.
615    pub d_head: usize,
616    /// W_Q projection \[d_model × d_model\].
617    pub w_q: Vec<f64>,
618    /// W_K projection \[d_model × d_model\].
619    pub w_k: Vec<f64>,
620    /// W_V projection \[d_model × d_model\].
621    pub w_v: Vec<f64>,
622    /// W_O output projection \[d_model × d_model\].
623    pub w_o: Vec<f64>,
624    /// Output bias \[d_model\].
625    pub b_o: Vec<f64>,
626}
627impl MultiHeadAttention {
628    /// Create a new MHA module with zero-initialised projections.
629    pub fn new(d_model: usize, n_heads: usize) -> Self {
630        assert_eq!(d_model % n_heads, 0, "d_model must be divisible by n_heads");
631        let d_head = d_model / n_heads;
632        let dm2 = d_model * d_model;
633        Self {
634            d_model,
635            n_heads,
636            d_head,
637            w_q: vec![0.0_f64; dm2],
638            w_k: vec![0.0_f64; dm2],
639            w_v: vec![0.0_f64; dm2],
640            w_o: vec![0.0_f64; dm2],
641            b_o: vec![0.0_f64; d_model],
642        }
643    }
644    /// Initialise W_Q, W_K, W_V, W_O with identity-like weights for testing.
645    pub fn init_identity(&mut self) {
646        let dm = self.d_model;
647        for row in 0..dm {
648            self.w_q[row * dm + row] = 1.0;
649            self.w_k[row * dm + row] = 1.0;
650            self.w_v[row * dm + row] = 1.0;
651            self.w_o[row * dm + row] = 1.0;
652        }
653    }
654    /// Linear projection: `output = input @ W^T`  where W is `[out × in]`.
655    fn project(
656        input: &[f64],
657        w: &[f64],
658        seq_len: usize,
659        in_dim: usize,
660        out_dim: usize,
661    ) -> Vec<f64> {
662        let mut out = vec![0.0_f64; seq_len * out_dim];
663        for t in 0..seq_len {
664            for o in 0..out_dim {
665                let mut acc = 0.0_f64;
666                for i in 0..in_dim {
667                    acc += input[t * in_dim + i] * w[o * in_dim + i];
668                }
669                out[t * out_dim + o] = acc;
670            }
671        }
672        out
673    }
674    /// Forward pass.
675    ///
676    /// `x` has shape `[seq_len × d_model]` (flat row-major).
677    /// Returns output of shape `[seq_len × d_model]`.
678    pub fn forward(&self, x: &[f64], seq_len: usize) -> Vec<f64> {
679        let dm = self.d_model;
680        let dh = self.d_head;
681        let nh = self.n_heads;
682        let q_full = Self::project(x, &self.w_q, seq_len, dm, dm);
683        let k_full = Self::project(x, &self.w_k, seq_len, dm, dm);
684        let v_full = Self::project(x, &self.w_v, seq_len, dm, dm);
685        let mut concat = vec![0.0_f64; seq_len * dm];
686        for h in 0..nh {
687            let mut q_h = vec![0.0_f64; seq_len * dh];
688            let mut k_h = vec![0.0_f64; seq_len * dh];
689            let mut v_h = vec![0.0_f64; seq_len * dh];
690            for t in 0..seq_len {
691                for d in 0..dh {
692                    q_h[t * dh + d] = q_full[t * dm + h * dh + d];
693                    k_h[t * dh + d] = k_full[t * dm + h * dh + d];
694                    v_h[t * dh + d] = v_full[t * dm + h * dh + d];
695                }
696            }
697            let head_out =
698                scaled_dot_product_attention(&q_h, &k_h, &v_h, seq_len, seq_len, dh, dh, None);
699            for t in 0..seq_len {
700                for d in 0..dh {
701                    concat[t * dm + h * dh + d] = head_out[t * dh + d];
702                }
703            }
704        }
705        let projected = Self::project(&concat, &self.w_o, seq_len, dm, dm);
706        let mut output = projected;
707        for t in 0..seq_len {
708            for d in 0..dm {
709                output[t * dm + d] += self.b_o[d];
710            }
711        }
712        output
713    }
714    /// Total number of trainable parameters.
715    pub fn num_params(&self) -> usize {
716        4 * self.d_model * self.d_model + self.d_model
717    }
718}
719/// A single fully-connected layer with f64 weights.
720#[derive(Debug, Clone)]
721pub struct NeuralLayer {
722    /// Weight matrix: `weights[out][in]`.
723    pub weights: Vec<Vec<f64>>,
724    /// Bias vector of length `out_features`.
725    pub biases: Vec<f64>,
726    /// Activation function applied after the affine transform.
727    pub activation: ActivationFn64,
728}
729impl NeuralLayer {
730    /// Create a new layer with Xavier-uniform initialised weights.
731    ///
732    /// Xavier uniform: U(-limit, limit) where limit = sqrt(6 / (fan_in + fan_out)).
733    pub fn new_xavier(in_features: usize, out_features: usize, activation: ActivationFn64) -> Self {
734        let limit = (6.0_f64 / (in_features + out_features) as f64).sqrt();
735        let mut state: u64 = 0x123456789abcdef0;
736        let lcg_next = |s: &mut u64| -> f64 {
737            *s = s
738                .wrapping_mul(6364136223846793005)
739                .wrapping_add(1442695040888963407);
740            let bits = (*s >> 33) as f64;
741            bits / (u64::MAX as f64) * 2.0 * limit - limit
742        };
743        let weights: Vec<Vec<f64>> = (0..out_features)
744            .map(|_| (0..in_features).map(|_| lcg_next(&mut state)).collect())
745            .collect();
746        let biases = vec![0.0_f64; out_features];
747        Self {
748            weights,
749            biases,
750            activation,
751        }
752    }
753    /// Forward pass: activation(W * input + b).
754    pub fn forward(&self, input: &[f64]) -> Vec<f64> {
755        let out_features = self.weights.len();
756        let mut output = Vec::with_capacity(out_features);
757        for o in 0..out_features {
758            let mut acc = self.biases[o];
759            for (i, &x) in input.iter().enumerate() {
760                acc += self.weights[o][i] * x;
761            }
762            output.push(self.activation.apply(acc));
763        }
764        output
765    }
766}
767/// Attention-based graph readout that computes a weighted sum of node features.
768///
769/// For each node i, computes a scalar attention score a_i = sigmoid(w · h_i + b),
770/// then returns Σ_i a_i * h_i.
771#[derive(Debug, Clone)]
772pub struct AttentionReadout {
773    /// Feature dimensionality.
774    pub d_feat: usize,
775    /// Attention weight vector \[d_feat\].
776    pub w_attn: Vec<f64>,
777    /// Attention bias (scalar).
778    pub b_attn: f64,
779}
780impl AttentionReadout {
781    /// Create with zero attention weights.
782    pub fn new(d_feat: usize) -> Self {
783        Self {
784            d_feat,
785            w_attn: vec![0.0_f64; d_feat],
786            b_attn: 0.0,
787        }
788    }
789    /// Compute attention-weighted sum over nodes.
790    ///
791    /// `node_feats`: `[n_nodes × d_feat]` flat row-major.
792    pub fn forward(&self, node_feats: &[f64], n_nodes: usize) -> Vec<f64> {
793        let df = self.d_feat;
794        let mut out = vec![0.0_f64; df];
795        let mut attn_scores = Vec::with_capacity(n_nodes);
796        for i in 0..n_nodes {
797            let h = &node_feats[i * df..(i + 1) * df];
798            let raw: f64 = h
799                .iter()
800                .zip(self.w_attn.iter())
801                .map(|(&x, &w)| x * w)
802                .sum::<f64>()
803                + self.b_attn;
804            let score = 1.0 / (1.0 + (-raw).exp());
805            attn_scores.push(score);
806        }
807        for i in 0..n_nodes {
808            let h = &node_feats[i * df..(i + 1) * df];
809            for d in 0..df {
810                out[d] += attn_scores[i] * h[d];
811            }
812        }
813        out
814    }
815}
816/// A single transformer encoder block:
817/// x → MHA(LayerNorm(x)) + x → FFN(LayerNorm(·)) + ·
818#[derive(Debug, Clone)]
819pub struct TransformerBlock {
820    /// Multi-head self-attention module.
821    pub mha: MultiHeadAttention,
822    /// Feed-forward network.
823    pub ffn: TransformerFfn,
824    /// Layer norm before MHA.
825    pub ln1: LayerNorm,
826    /// Layer norm before FFN.
827    pub ln2: LayerNorm,
828    /// Model dimensionality.
829    pub d_model: usize,
830}
831impl TransformerBlock {
832    /// Create a new transformer block with zero weights.
833    pub fn new(d_model: usize, n_heads: usize, d_ff: usize) -> Self {
834        Self {
835            mha: MultiHeadAttention::new(d_model, n_heads),
836            ffn: TransformerFfn::new(d_model, d_ff),
837            ln1: LayerNorm::new(d_model),
838            ln2: LayerNorm::new(d_model),
839            d_model,
840        }
841    }
842    /// Forward pass with pre-norm style residual connections.
843    ///
844    /// `x` is flat row-major `[seq_len × d_model]`.
845    pub fn forward(&self, x: &[f64], seq_len: usize) -> Vec<f64> {
846        let dm = self.d_model;
847        let mut normed1 = vec![0.0_f64; seq_len * dm];
848        for t in 0..seq_len {
849            let row = &x[t * dm..(t + 1) * dm];
850            let n = self.ln1.forward(row);
851            normed1[t * dm..(t + 1) * dm].copy_from_slice(&n);
852        }
853        let attn_out = self.mha.forward(&normed1, seq_len);
854        let mut x1 = vec![0.0_f64; seq_len * dm];
855        for i in 0..x1.len() {
856            x1[i] = x[i] + attn_out[i];
857        }
858        let mut normed2 = vec![0.0_f64; seq_len * dm];
859        for t in 0..seq_len {
860            let row = &x1[t * dm..(t + 1) * dm];
861            let n = self.ln2.forward(row);
862            normed2[t * dm..(t + 1) * dm].copy_from_slice(&n);
863        }
864        let ffn_out = self.ffn.forward(&normed2, seq_len);
865        let mut x2 = vec![0.0_f64; seq_len * dm];
866        for i in 0..x2.len() {
867            x2[i] = x1[i] + ffn_out[i];
868        }
869        x2
870    }
871}
872/// Activation functions for neural network layers.
873#[derive(Debug, Clone, PartialEq)]
874pub enum ActivationFn {
875    /// Hyperbolic tangent.
876    Tanh,
877    /// Rectified linear unit.
878    Relu,
879    /// Logistic sigmoid.
880    Sigmoid,
881    /// Sigmoid-weighted linear unit.
882    Silu,
883    /// Gaussian error linear unit (approximation).
884    Gelu,
885    /// Identity / no activation.
886    Linear,
887}
888impl ActivationFn {
889    /// Evaluate the activation function at `x`.
890    pub fn apply(&self, x: f32) -> f32 {
891        match self {
892            ActivationFn::Tanh => x.tanh(),
893            ActivationFn::Relu => x.max(0.0),
894            ActivationFn::Sigmoid => 1.0 / (1.0 + (-x).exp()),
895            ActivationFn::Silu => x / (1.0 + (-x).exp()),
896            ActivationFn::Gelu => {
897                let cdf = 0.5
898                    * (1.0
899                        + (std::f32::consts::FRAC_2_SQRT_PI.sqrt() * (x + 0.044715 * x * x * x))
900                            .tanh());
901                x * cdf
902            }
903            ActivationFn::Linear => x,
904        }
905    }
906    /// Evaluate the derivative of the activation function at `x`.
907    pub fn derivative(&self, x: f32) -> f32 {
908        match self {
909            ActivationFn::Tanh => {
910                let t = x.tanh();
911                1.0 - t * t
912            }
913            ActivationFn::Relu => {
914                if x > 0.0 {
915                    1.0
916                } else {
917                    0.0
918                }
919            }
920            ActivationFn::Sigmoid => {
921                let s = 1.0 / (1.0 + (-x).exp());
922                s * (1.0 - s)
923            }
924            ActivationFn::Silu => {
925                let s = 1.0 / (1.0 + (-x).exp());
926                s + x * s * (1.0 - s)
927            }
928            ActivationFn::Gelu => {
929                let eps = 1e-5_f32;
930                (self.apply(x + eps) - self.apply(x - eps)) / (2.0 * eps)
931            }
932            ActivationFn::Linear => 1.0,
933        }
934    }
935}
936/// Layer normalisation (Ba et al., 2016).
937///
938/// Normalises the *entire* feature vector of a single sample to zero mean and
939/// unit variance, then applies learned scale (gamma) and shift (beta).
940#[derive(Debug, Clone)]
941pub struct LayerNormLayer {
942    /// Number of features (last dimension size).
943    pub n_features: usize,
944    /// Learned scale parameter (gamma), initialised to 1.
945    pub gamma: Vec<f64>,
946    /// Learned shift parameter (beta), initialised to 0.
947    pub beta: Vec<f64>,
948    /// Numerical stability constant.
949    pub epsilon: f64,
950}
951impl LayerNormLayer {
952    /// Create a new LayerNorm with identity transform (gamma=1, beta=0).
953    pub fn new(n_features: usize) -> Self {
954        Self {
955            n_features,
956            gamma: vec![1.0; n_features],
957            beta: vec![0.0; n_features],
958            epsilon: 1e-5,
959        }
960    }
961    /// Apply layer normalisation to one sample vector.
962    ///
963    /// output\[i\] = gamma\[i\] * (input\[i\] - mean) / sqrt(var + eps) + beta\[i\]
964    pub fn forward(&self, input: &[f64]) -> Vec<f64> {
965        assert_eq!(
966            input.len(),
967            self.n_features,
968            "LayerNorm: input size mismatch"
969        );
970        let n = self.n_features as f64;
971        let mean: f64 = input.iter().sum::<f64>() / n;
972        let var: f64 = input.iter().map(|&x| (x - mean) * (x - mean)).sum::<f64>() / n;
973        let std_inv = 1.0 / (var + self.epsilon).sqrt();
974        (0..self.n_features)
975            .map(|i| self.gamma[i] * (input[i] - mean) * std_inv + self.beta[i])
976            .collect()
977    }
978    /// Compute gradient of the layer norm output with respect to the input.
979    ///
980    /// Returns `(d_input, d_gamma, d_beta)` given upstream gradient `d_output`.
981    pub fn backward(&self, input: &[f64], d_output: &[f64]) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
982        assert_eq!(input.len(), self.n_features);
983        assert_eq!(d_output.len(), self.n_features);
984        let n = self.n_features as f64;
985        let mean: f64 = input.iter().sum::<f64>() / n;
986        let var: f64 = input.iter().map(|&x| (x - mean) * (x - mean)).sum::<f64>() / n;
987        let std_inv = 1.0 / (var + self.epsilon).sqrt();
988        let x_hat: Vec<f64> = input.iter().map(|&x| (x - mean) * std_inv).collect();
989        let d_gamma: Vec<f64> = (0..self.n_features)
990            .map(|i| d_output[i] * x_hat[i])
991            .collect();
992        let d_beta: Vec<f64> = d_output.to_vec();
993        let d_x_hat: Vec<f64> = (0..self.n_features)
994            .map(|i| d_output[i] * self.gamma[i])
995            .collect();
996        let sum_d_x_hat: f64 = d_x_hat.iter().sum();
997        let sum_d_x_hat_xhat: f64 = d_x_hat.iter().zip(x_hat.iter()).map(|(&a, &b)| a * b).sum();
998        let d_input: Vec<f64> = (0..self.n_features)
999            .map(|i| std_inv * (d_x_hat[i] - (sum_d_x_hat + x_hat[i] * sum_d_x_hat_xhat) / n))
1000            .collect();
1001        (d_input, d_gamma, d_beta)
1002    }
1003}
1004/// Feature-wise Z-score normalizer.
1005///
1006/// Stores per-feature mean and standard deviation fitted on a training corpus.
1007#[derive(Debug, Clone)]
1008pub struct DataNormalizer {
1009    /// Per-feature mean.
1010    pub mean: Vec<f32>,
1011    /// Per-feature standard deviation.
1012    pub std_dev: Vec<f32>,
1013}
1014impl DataNormalizer {
1015    /// Fit normalizer statistics from a collection of sample vectors.
1016    ///
1017    /// # Panics
1018    /// Panics if `data` is empty or if sample vectors have inconsistent lengths.
1019    pub fn fit(data: &[Vec<f32>]) -> Self {
1020        assert!(
1021            !data.is_empty(),
1022            "DataNormalizer::fit: data must be non-empty"
1023        );
1024        let n_features = data[0].len();
1025        let n = data.len() as f32;
1026        let mut mean = vec![0.0_f32; n_features];
1027        for sample in data {
1028            assert_eq!(
1029                sample.len(),
1030                n_features,
1031                "DataNormalizer::fit: inconsistent sample length"
1032            );
1033            for (k, &v) in sample.iter().enumerate() {
1034                mean[k] += v;
1035            }
1036        }
1037        for m in &mut mean {
1038            *m /= n;
1039        }
1040        let mut variance = vec![0.0_f32; n_features];
1041        for sample in data {
1042            for (k, &v) in sample.iter().enumerate() {
1043                let diff = v - mean[k];
1044                variance[k] += diff * diff;
1045            }
1046        }
1047        let std_dev: Vec<f32> = variance
1048            .iter()
1049            .map(|&v| {
1050                let s = (v / n).sqrt();
1051                if s < 1e-8 { 1.0 } else { s }
1052            })
1053            .collect();
1054        DataNormalizer { mean, std_dev }
1055    }
1056    /// Standardise a single sample: `(x - mean) / std`.
1057    pub fn transform(&self, x: &[f32]) -> Vec<f32> {
1058        x.iter()
1059            .zip(self.mean.iter())
1060            .zip(self.std_dev.iter())
1061            .map(|((&xi, &m), &s)| (xi - m) / s)
1062            .collect()
1063    }
1064    /// Invert standardisation: `x * std + mean`.
1065    pub fn inverse_transform(&self, x: &[f32]) -> Vec<f32> {
1066        x.iter()
1067            .zip(self.mean.iter())
1068            .zip(self.std_dev.iter())
1069            .map(|((&xi, &m), &s)| xi * s + m)
1070            .collect()
1071    }
1072}
1073/// Activation function for f64-precision neural network layers.
1074#[derive(Debug, Clone, PartialEq)]
1075pub enum ActivationFn64 {
1076    /// Rectified linear unit: max(0, x).
1077    Relu,
1078    /// Logistic sigmoid: 1 / (1 + exp(-x)).
1079    Sigmoid,
1080    /// Hyperbolic tangent.
1081    Tanh,
1082    /// Identity (no activation).
1083    Linear,
1084}
1085impl ActivationFn64 {
1086    /// Evaluate the activation at a single value.
1087    pub fn apply(&self, x: f64) -> f64 {
1088        match self {
1089            ActivationFn64::Relu => x.max(0.0),
1090            ActivationFn64::Sigmoid => 1.0 / (1.0 + (-x).exp()),
1091            ActivationFn64::Tanh => x.tanh(),
1092            ActivationFn64::Linear => x,
1093        }
1094    }
1095    /// Apply the activation in-place to every element of a vector.
1096    pub fn apply_batch(&self, v: &mut [f64]) {
1097        for x in v.iter_mut() {
1098            *x = self.apply(*x);
1099        }
1100    }
1101}
1102/// A sequential feed-forward neural network using f64 precision.
1103#[derive(Debug, Clone)]
1104pub struct NeuralNetwork {
1105    /// Ordered list of layers.
1106    pub layers: Vec<NeuralLayer>,
1107}
1108impl NeuralNetwork {
1109    /// Build a network with Xavier-initialised weights from a list of layer sizes.
1110    ///
1111    /// All hidden layers use `activation`; the final layer uses `ActivationFn64::Linear`.
1112    pub fn new(layer_sizes: &[usize], activation: ActivationFn64) -> Self {
1113        assert!(
1114            layer_sizes.len() >= 2,
1115            "need at least input and output size"
1116        );
1117        let mut layers = Vec::new();
1118        for i in 0..layer_sizes.len() - 1 {
1119            let act = if i == layer_sizes.len() - 2 {
1120                ActivationFn64::Linear
1121            } else {
1122                activation.clone()
1123            };
1124            layers.push(NeuralLayer::new_xavier(
1125                layer_sizes[i],
1126                layer_sizes[i + 1],
1127                act,
1128            ));
1129        }
1130        Self { layers }
1131    }
1132    /// Run a forward pass through all layers.
1133    pub fn forward(&self, input: &[f64]) -> Vec<f64> {
1134        let mut current: Vec<f64> = input.to_vec();
1135        for layer in &self.layers {
1136            current = layer.forward(&current);
1137        }
1138        current
1139    }
1140    /// Expected input dimension (size of the first layer's input).
1141    pub fn input_dim(&self) -> usize {
1142        self.layers
1143            .first()
1144            .map_or(0, |l| l.weights.first().map_or(0, |r| r.len()))
1145    }
1146    /// Expected output dimension (size of the last layer's output).
1147    pub fn output_dim(&self) -> usize {
1148        self.layers.last().map_or(0, |l| l.biases.len())
1149    }
1150}
1151/// A sequential feed-forward neural network.
1152#[derive(Debug, Clone)]
1153pub struct FeedForwardNet {
1154    /// Ordered list of dense layers.
1155    pub layers: Vec<DenseLayer>,
1156}
1157impl FeedForwardNet {
1158    /// Create an empty network.
1159    pub fn new() -> Self {
1160        FeedForwardNet { layers: Vec::new() }
1161    }
1162    /// Append a layer to the end of the network.
1163    pub fn add_layer(&mut self, layer: DenseLayer) {
1164        self.layers.push(layer);
1165    }
1166    /// Run a forward pass through all layers.
1167    pub fn forward(&self, input: &[f32]) -> Vec<f32> {
1168        let mut current: Vec<f32> = input.to_vec();
1169        for layer in &self.layers {
1170            current = layer.forward(&current);
1171        }
1172        current
1173    }
1174    /// Returns the expected input width (`None` if the network has no layers).
1175    pub fn input_size(&self) -> Option<usize> {
1176        self.layers.first().map(|l| l.in_features)
1177    }
1178    /// Returns the output width of the last layer (`None` if empty).
1179    pub fn output_size(&self) -> Option<usize> {
1180        self.layers.last().map(|l| l.out_features)
1181    }
1182    /// Sum of parameters across all layers.
1183    pub fn total_parameters(&self) -> usize {
1184        self.layers.iter().map(|l| l.parameter_count()).sum()
1185    }
1186}
1187impl FeedForwardNet {
1188    /// Compute the total gradient norm across all layers.
1189    ///
1190    /// `layer_grads[i]` is the concatenated `[grad_weights, grad_biases]` for
1191    /// layer `i`.  Returns the L2 norm of all gradients combined.
1192    pub fn compute_gradient_norm(&self, layer_grads: &[Vec<f32>]) -> f32 {
1193        let sum_sq: f32 = layer_grads
1194            .iter()
1195            .flat_map(|g| g.iter())
1196            .map(|&v| v * v)
1197            .sum();
1198        sum_sq.sqrt()
1199    }
1200    /// Clip per-layer gradient vectors in-place so their combined norm ≤ `max_norm`.
1201    /// Returns the pre-clip norm.
1202    pub fn clip_gradients(&self, layer_grads: &mut [Vec<f32>], max_norm: f32) -> f32 {
1203        let norm = self.compute_gradient_norm(layer_grads);
1204        if norm > max_norm && norm > 0.0 {
1205            let scale = max_norm / norm;
1206            for g in layer_grads.iter_mut() {
1207                for v in g.iter_mut() {
1208                    *v *= scale;
1209                }
1210            }
1211        }
1212        norm
1213    }
1214}
1215/// Layer normalisation applied to each time step independently.
1216///
1217/// Normalises a feature vector of length `n_features` to zero mean and unit
1218/// variance, then applies learnable scale (gamma) and bias (beta).
1219#[derive(Debug, Clone)]
1220pub struct LayerNorm {
1221    /// Number of features.
1222    pub n_features: usize,
1223    /// Learnable scale parameter.
1224    pub gamma: Vec<f64>,
1225    /// Learnable shift parameter.
1226    pub beta: Vec<f64>,
1227    /// Numerical stability constant.
1228    pub epsilon: f64,
1229}
1230impl LayerNorm {
1231    /// Create a new layer norm with identity initialisation (gamma=1, beta=0).
1232    pub fn new(n_features: usize) -> Self {
1233        Self {
1234            n_features,
1235            gamma: vec![1.0_f64; n_features],
1236            beta: vec![0.0_f64; n_features],
1237            epsilon: 1e-5,
1238        }
1239    }
1240    /// Normalise a single feature vector.
1241    pub fn forward(&self, x: &[f64]) -> Vec<f64> {
1242        assert_eq!(x.len(), self.n_features);
1243        let n = self.n_features as f64;
1244        let mean = x.iter().sum::<f64>() / n;
1245        let var = x.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / n;
1246        let std = (var + self.epsilon).sqrt();
1247        x.iter()
1248            .enumerate()
1249            .map(|(i, &v)| self.gamma[i] * (v - mean) / std + self.beta[i])
1250            .collect()
1251    }
1252}
1253/// A mock GPU buffer for batched neural network inference.
1254///
1255/// In a real GPU backend this would be a device-side buffer; here we
1256/// store a flat f64 array in host memory.
1257#[derive(Debug, Clone)]
1258pub struct GpuNeuralBuffer {
1259    /// Batch size (number of samples).
1260    pub batch_size: usize,
1261    /// Dimensionality of each input sample.
1262    pub input_dim: usize,
1263    /// Dimensionality of each output sample.
1264    pub output_dim: usize,
1265    /// Flat data storage: `batch_size * max(input_dim, output_dim)` elements.
1266    pub data: Vec<f64>,
1267}
1268impl GpuNeuralBuffer {
1269    /// Pack a slice of 3-D positions into a buffer suitable for network input.
1270    ///
1271    /// Each position `[x, y, z]` becomes three consecutive f64 values.
1272    pub fn pack_positions(positions: &[[f64; 3]]) -> Self {
1273        let batch_size = positions.len();
1274        let input_dim = 3;
1275        let output_dim = 3;
1276        let mut data = Vec::with_capacity(batch_size * input_dim);
1277        for p in positions {
1278            data.push(p[0]);
1279            data.push(p[1]);
1280            data.push(p[2]);
1281        }
1282        Self {
1283            batch_size,
1284            input_dim,
1285            output_dim,
1286            data,
1287        }
1288    }
1289    /// Unpack the buffer contents as a list of 3-D force vectors.
1290    ///
1291    /// Assumes `data` has been filled with `batch_size * 3` values by
1292    /// a prior inference step.
1293    pub fn unpack_forces(&self) -> Vec<[f64; 3]> {
1294        self.data.chunks(3).map(|c| [c[0], c[1], c[2]]).collect()
1295    }
1296}
1297/// Sinusoidal positional encoding (Vaswani et al., 2017).
1298///
1299/// For each position `pos` and dimension `i` in a `d_model`-dimensional
1300/// embedding space:
1301///   PE\[pos, 2i\]   = sin(pos / 10000^(2i/d_model))
1302///   PE\[pos, 2i+1\] = cos(pos / 10000^(2i/d_model))
1303#[derive(Debug, Clone)]
1304pub struct PositionalEncoding {
1305    /// Embedding dimensionality.
1306    pub d_model: usize,
1307    /// Maximum sequence length supported.
1308    pub max_len: usize,
1309    /// Pre-computed encoding table: `table[pos][dim]`.
1310    pub table: Vec<Vec<f64>>,
1311}
1312impl PositionalEncoding {
1313    /// Build the positional encoding table up to `max_len` positions.
1314    pub fn new(d_model: usize, max_len: usize) -> Self {
1315        let mut table = vec![vec![0.0_f64; d_model]; max_len];
1316        for (pos, row) in table.iter_mut().enumerate() {
1317            for i in 0..(d_model / 2) {
1318                let angle = (pos as f64) / (10000.0_f64).powf(2.0 * i as f64 / d_model as f64);
1319                row[2 * i] = angle.sin();
1320                if 2 * i + 1 < d_model {
1321                    row[2 * i + 1] = angle.cos();
1322                }
1323            }
1324        }
1325        Self {
1326            d_model,
1327            max_len,
1328            table,
1329        }
1330    }
1331    /// Add positional encoding to a sequence of embeddings in-place.
1332    ///
1333    /// `embeddings[t]` is a feature vector of length `d_model`.
1334    pub fn add_to_sequence(&self, embeddings: &mut [Vec<f64>]) {
1335        for (t, emb) in embeddings.iter_mut().enumerate() {
1336            if t >= self.max_len {
1337                break;
1338            }
1339            for d in 0..emb.len().min(self.d_model) {
1340                emb[d] += self.table[t][d];
1341            }
1342        }
1343    }
1344    /// Return the positional encoding vector for position `pos`.
1345    pub fn get(&self, pos: usize) -> &[f64] {
1346        &self.table[pos.min(self.max_len - 1)]
1347    }
1348}
1349/// A fully-connected layer with f64 weights supporting forward pass and
1350/// gradient computation for backpropagation.
1351#[derive(Debug, Clone)]
1352pub struct DenseLayer64 {
1353    /// Weight matrix in row-major layout: `weights[out * in_features + in]`.
1354    pub weights: Vec<f64>,
1355    /// Bias vector of length `out_features`.
1356    pub biases: Vec<f64>,
1357    /// Number of input features.
1358    pub in_features: usize,
1359    /// Number of output features.
1360    pub out_features: usize,
1361    /// Activation function.
1362    pub activation: ExtActivation,
1363    /// Pre-activation outputs from the last forward pass (z = W*x + b).
1364    pub last_pre_act: Vec<f64>,
1365    /// Post-activation outputs from the last forward pass.
1366    pub last_output: Vec<f64>,
1367    /// Last input fed to this layer.
1368    pub last_input: Vec<f64>,
1369}
1370impl DenseLayer64 {
1371    /// Create a new layer with zero-initialised weights and biases.
1372    pub fn new(in_features: usize, out_features: usize, activation: ExtActivation) -> Self {
1373        Self {
1374            weights: vec![0.0_f64; out_features * in_features],
1375            biases: vec![0.0_f64; out_features],
1376            in_features,
1377            out_features,
1378            activation,
1379            last_pre_act: Vec::new(),
1380            last_output: Vec::new(),
1381            last_input: Vec::new(),
1382        }
1383    }
1384    /// Forward pass: computes `activation(W * input + b)`.
1385    /// Caches `pre_act`, `output`, and `input` for backprop.
1386    pub fn forward(&mut self, input: &[f64]) -> Vec<f64> {
1387        assert_eq!(
1388            input.len(),
1389            self.in_features,
1390            "DenseLayer64::forward: input size mismatch"
1391        );
1392        self.last_input = input.to_vec();
1393        let pre_act: Vec<f64> = (0..self.out_features)
1394            .map(|o| {
1395                let row = o * self.in_features;
1396                let mut acc = self.biases[o];
1397                for (i, &inp) in input.iter().enumerate() {
1398                    acc += self.weights[row + i] * inp;
1399                }
1400                acc
1401            })
1402            .collect();
1403        let output: Vec<f64> = pre_act.iter().map(|&z| self.activation.apply(z)).collect();
1404        self.last_pre_act = pre_act;
1405        self.last_output = output.clone();
1406        output
1407    }
1408    /// Backward pass: computes gradients w.r.t. weights, biases, and input.
1409    ///
1410    /// `delta_out` is the gradient of the loss w.r.t. this layer's output
1411    /// (same shape as `last_output`).
1412    ///
1413    /// Returns `(grad_weights, grad_biases, delta_in)` where `delta_in` is the
1414    /// gradient passed to the previous layer.
1415    pub fn backward(&self, delta_out: &[f64]) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
1416        assert_eq!(
1417            delta_out.len(),
1418            self.out_features,
1419            "DenseLayer64::backward: delta_out size mismatch"
1420        );
1421        let delta_pre: Vec<f64> = delta_out
1422            .iter()
1423            .zip(self.last_pre_act.iter())
1424            .map(|(&d, &z)| d * self.activation.derivative(z))
1425            .collect();
1426        let mut grad_weights = vec![0.0_f64; self.out_features * self.in_features];
1427        for (o, &dp_o) in delta_pre.iter().enumerate() {
1428            let row = o * self.in_features;
1429            for (i, &li) in self.last_input.iter().enumerate() {
1430                grad_weights[row + i] = dp_o * li;
1431            }
1432        }
1433        let grad_biases = delta_pre.clone();
1434        let mut delta_in = vec![0.0_f64; self.in_features];
1435        for (o, &dp_o) in delta_pre.iter().enumerate() {
1436            let row = o * self.in_features;
1437            for (i, di) in delta_in.iter_mut().enumerate() {
1438                *di += self.weights[row + i] * dp_o;
1439            }
1440        }
1441        (grad_weights, grad_biases, delta_in)
1442    }
1443    /// Apply gradient updates using a simple SGD step.
1444    pub fn apply_sgd(&mut self, grad_weights: &[f64], grad_biases: &[f64], lr: f64) {
1445        for (w, &gw) in self.weights.iter_mut().zip(grad_weights.iter()) {
1446            *w -= lr * gw;
1447        }
1448        for (b, &gb) in self.biases.iter_mut().zip(grad_biases.iter()) {
1449            *b -= lr * gb;
1450        }
1451    }
1452    /// Total number of parameters.
1453    pub fn num_params(&self) -> usize {
1454        self.out_features * self.in_features + self.out_features
1455    }
1456}
1457/// Atomic neural network potential (NNP) with one sub-network per element.
1458///
1459/// Architecture follows Behler (2011): each atom contributes an atomic energy
1460/// predicted by an element-specific feed-forward network whose input is the
1461/// Behler-Parrinello descriptor vector.
1462#[derive(Debug)]
1463pub struct AtomicNeuralNetwork {
1464    /// Element-specific networks keyed by atomic number.
1465    pub networks: HashMap<u8, FeedForwardNet>,
1466    /// Symmetry-function descriptor shared by all elements.
1467    pub descriptor: BehlerParrinelloDescriptor,
1468}
1469impl AtomicNeuralNetwork {
1470    /// Create a new AANN with the given descriptor.
1471    pub fn new(descriptor: BehlerParrinelloDescriptor) -> Self {
1472        AtomicNeuralNetwork {
1473            networks: HashMap::new(),
1474            descriptor,
1475        }
1476    }
1477    /// Register a sub-network for the given atomic number.
1478    pub fn add_element_network(&mut self, atomic_number: u8, net: FeedForwardNet) {
1479        self.networks.insert(atomic_number, net);
1480    }
1481    /// Predict the atomic energy for one atom given its descriptor.
1482    ///
1483    /// Returns `None` if no network is registered for `atomic_number`.
1484    pub fn atomic_energy(&self, atomic_number: u8, descriptor: &[f32]) -> Option<f32> {
1485        self.networks
1486            .get(&atomic_number)
1487            .map(|net| net.forward(descriptor)[0])
1488    }
1489    /// Sum of atomic energies over all atoms.
1490    ///
1491    /// Atoms whose element has no registered network contribute 0.
1492    pub fn total_energy(&self, positions: &[[f64; 3]], atomic_numbers: &[u8]) -> f64 {
1493        assert_eq!(
1494            positions.len(),
1495            atomic_numbers.len(),
1496            "total_energy: positions and atomic_numbers must have the same length"
1497        );
1498        let mut e_total = 0.0_f64;
1499        for (i, &z) in atomic_numbers.iter().enumerate() {
1500            let desc_f64 = self.descriptor.descriptor_vector(positions, i);
1501            let desc_f32: Vec<f32> = desc_f64.iter().map(|&v| v as f32).collect();
1502            if let Some(e) = self.atomic_energy(z, &desc_f32) {
1503                e_total += e as f64;
1504            }
1505        }
1506        e_total
1507    }
1508}
1509/// A single fully-connected (dense) layer with an activation function.
1510///
1511/// Weights are stored in row-major order: `weights[out * in_features + in]`.
1512#[derive(Debug, Clone)]
1513pub struct DenseLayer {
1514    /// Weight matrix in row-major layout `[out_features × in_features]`.
1515    pub weights: Vec<f32>,
1516    /// Bias vector of length `out_features`.
1517    pub biases: Vec<f32>,
1518    /// Number of input features.
1519    pub in_features: usize,
1520    /// Number of output features.
1521    pub out_features: usize,
1522    /// Activation function applied after the affine transform.
1523    pub activation: ActivationFn,
1524}
1525impl DenseLayer {
1526    /// Create a new layer with zero-initialised weights and biases.
1527    pub fn new(in_features: usize, out_features: usize, activation: ActivationFn) -> Self {
1528        DenseLayer {
1529            weights: vec![0.0_f32; out_features * in_features],
1530            biases: vec![0.0_f32; out_features],
1531            in_features,
1532            out_features,
1533            activation,
1534        }
1535    }
1536    /// Compute `activation(W * input + b)`.
1537    ///
1538    /// # Panics
1539    /// Panics if `input.len() != self.in_features`.
1540    pub fn forward(&self, input: &[f32]) -> Vec<f32> {
1541        assert_eq!(
1542            input.len(),
1543            self.in_features,
1544            "DenseLayer::forward: input length {} != in_features {}",
1545            input.len(),
1546            self.in_features
1547        );
1548        (0..self.out_features)
1549            .map(|o| {
1550                let row_offset = o * self.in_features;
1551                let mut acc = self.biases[o];
1552                for (i, &inp) in input.iter().enumerate() {
1553                    acc += self.weights[row_offset + i] * inp;
1554                }
1555                self.activation.apply(acc)
1556            })
1557            .collect()
1558    }
1559    /// Replace the weight matrix (must have length `out_features * in_features`).
1560    ///
1561    /// # Panics
1562    /// Panics if `w.len()` does not match.
1563    pub fn set_weights(&mut self, w: &[f32]) {
1564        assert_eq!(
1565            w.len(),
1566            self.out_features * self.in_features,
1567            "set_weights: expected {} elements, got {}",
1568            self.out_features * self.in_features,
1569            w.len()
1570        );
1571        self.weights.copy_from_slice(w);
1572    }
1573    /// Replace the bias vector (must have length `out_features`).
1574    ///
1575    /// # Panics
1576    /// Panics if `b.len()` does not match.
1577    pub fn set_biases(&mut self, b: &[f32]) {
1578        assert_eq!(
1579            b.len(),
1580            self.out_features,
1581            "set_biases: expected {} elements, got {}",
1582            self.out_features,
1583            b.len()
1584        );
1585        self.biases.copy_from_slice(b);
1586    }
1587    /// Total number of trainable parameters (weights + biases).
1588    pub fn parameter_count(&self) -> usize {
1589        self.out_features * self.in_features + self.out_features
1590    }
1591}
1592/// Dropout regularisation layer.
1593///
1594/// During training, each neuron is set to zero with probability `rate`.
1595/// During inference (training=false) the layer passes inputs through unchanged
1596/// but scales by `1 - rate` to maintain expected magnitude.
1597#[derive(Debug, Clone)]
1598pub struct DropoutLayer {
1599    /// Probability of dropping a unit (0.0 = no dropout, 1.0 = all dropped).
1600    pub rate: f64,
1601    /// Whether the layer is in training mode.
1602    pub training: bool,
1603    /// Mask used in the last forward pass (1.0 = kept, 0.0 = dropped).
1604    pub last_mask: Vec<f64>,
1605    /// Seed for the deterministic LCG used to generate the mask.
1606    pub(super) seed: u64,
1607}
1608impl DropoutLayer {
1609    /// Create a new dropout layer.
1610    pub fn new(rate: f64, training: bool) -> Self {
1611        assert!(
1612            (0.0..=1.0).contains(&rate),
1613            "dropout rate must be in [0, 1]"
1614        );
1615        Self {
1616            rate,
1617            training,
1618            last_mask: Vec::new(),
1619            seed: 0xdeadbeefcafe1234,
1620        }
1621    }
1622    /// Set the seed for reproducible mask generation.
1623    pub fn set_seed(&mut self, seed: u64) {
1624        self.seed = seed;
1625    }
1626    /// Apply dropout to `input`.
1627    ///
1628    /// In training mode, randomly zeros elements with probability `rate`
1629    /// and scales the rest by `1 / (1 - rate)` (inverted dropout).
1630    /// In eval mode, passes through unchanged.
1631    pub fn forward(&mut self, input: &[f64]) -> Vec<f64> {
1632        if !self.training || self.rate == 0.0 {
1633            self.last_mask = vec![1.0; input.len()];
1634            return input.to_vec();
1635        }
1636        if self.rate == 1.0 {
1637            self.last_mask = vec![0.0; input.len()];
1638            return vec![0.0; input.len()];
1639        }
1640        let scale = 1.0 / (1.0 - self.rate);
1641        let mut mask = Vec::with_capacity(input.len());
1642        let mut output = Vec::with_capacity(input.len());
1643        for &x in input {
1644            self.seed = self
1645                .seed
1646                .wrapping_mul(6364136223846793005)
1647                .wrapping_add(1442695040888963407);
1648            let u = (self.seed >> 11) as f64 / (1u64 << 53) as f64;
1649            let m = if u >= self.rate { scale } else { 0.0 };
1650            mask.push(m);
1651            output.push(x * m);
1652        }
1653        self.last_mask = mask;
1654        output
1655    }
1656    /// Backward pass: applies the stored mask to the upstream gradient.
1657    pub fn backward(&self, delta_out: &[f64]) -> Vec<f64> {
1658        delta_out
1659            .iter()
1660            .zip(self.last_mask.iter())
1661            .map(|(&d, &m)| d * m)
1662            .collect()
1663    }
1664}
1665/// Behler-Parrinello symmetry functions for constructing atomic descriptors.
1666///
1667/// Reference: J. Behler and M. Parrinello, PRL 98, 146401 (2007).
1668#[derive(Debug, Clone)]
1669pub struct BehlerParrinelloDescriptor {
1670    /// Radial decay parameters η for G2 functions.
1671    pub eta: Vec<f64>,
1672    /// Shift parameters R_s for G2 functions.
1673    pub rs: Vec<f64>,
1674    /// Cutoff radius R_c in Ångström (or same units as positions).
1675    pub cutoff: f64,
1676}
1677impl BehlerParrinelloDescriptor {
1678    /// Smooth cutoff function.
1679    ///
1680    /// f_c(r) = 0.5 * (cos(Ï€ r / R_c) + 1)  for r < R_c, else 0.
1681    pub fn cutoff_fn(r: f64, rc: f64) -> f64 {
1682        if r < rc {
1683            0.5 * ((PI_F64 * r / rc).cos() + 1.0)
1684        } else {
1685            0.0
1686        }
1687    }
1688    /// G1 radial symmetry function: G1 = f_c(r).
1689    pub fn radial_g1(r: f64, rc: f64) -> f64 {
1690        Self::cutoff_fn(r, rc)
1691    }
1692    /// G2 radial symmetry function: G2 = exp(-η (r - R_s)²) * f_c(r).
1693    pub fn radial_g2(r: f64, eta: f64, rs: f64, rc: f64) -> f64 {
1694        let diff = r - rs;
1695        (-eta * diff * diff).exp() * Self::cutoff_fn(r, rc)
1696    }
1697    /// G4 angular symmetry function (two-body factor for a triplet i-j-k).
1698    ///
1699    /// G4 = 2^(1-ζ) * (1 + λ cos θ)^ζ * exp(-η (r_ij² + r_ik² + r_jk²)) * f_c(r_ij) f_c(r_ik) f_c(r_jk)
1700    pub fn angular_g4(
1701        r_ij: f64,
1702        r_ik: f64,
1703        r_jk: f64,
1704        cos_theta: f64,
1705        eta: f64,
1706        zeta: f64,
1707        lambda: f64,
1708        rc: f64,
1709    ) -> f64 {
1710        let angular = (1.0 + lambda * cos_theta).powf(zeta);
1711        let radial = (-eta * (r_ij * r_ij + r_ik * r_ik + r_jk * r_jk)).exp();
1712        let fc = Self::cutoff_fn(r_ij, rc) * Self::cutoff_fn(r_ik, rc) * Self::cutoff_fn(r_jk, rc);
1713        2.0_f64.powf(1.0 - zeta) * angular * radial * fc
1714    }
1715    /// Compute a single G2 descriptor value (convenience wrapper).
1716    pub fn compute(r_ij: f64, eta: f64, rs: f64, cutoff: f64) -> f64 {
1717        Self::radial_g2(r_ij, eta, rs, cutoff)
1718    }
1719    /// Build a full G2 descriptor vector for atom `center_idx`.
1720    ///
1721    /// For every (η, R_s) pair the function sums G2(r_ij, η, R_s, R_c) over all
1722    /// neighbours j ≠ center_idx that lie within the cutoff radius.
1723    pub fn descriptor_vector(&self, positions: &[[f64; 3]], center_idx: usize) -> Vec<f64> {
1724        let n_descriptors = self.eta.len();
1725        let mut desc = vec![0.0_f64; n_descriptors];
1726        let ci = positions[center_idx];
1727        for (j, pos_j) in positions.iter().enumerate() {
1728            if j == center_idx {
1729                continue;
1730            }
1731            let dx = pos_j[0] - ci[0];
1732            let dy = pos_j[1] - ci[1];
1733            let dz = pos_j[2] - ci[2];
1734            let r = (dx * dx + dy * dy + dz * dz).sqrt();
1735            if r >= self.cutoff {
1736                continue;
1737            }
1738            for (dk, (&eta_k, &rs_k)) in desc.iter_mut().zip(self.eta.iter().zip(self.rs.iter())) {
1739                *dk += Self::radial_g2(r, eta_k, rs_k, self.cutoff);
1740            }
1741        }
1742        desc
1743    }
1744}
1745/// Extended activation functions with additional variants for f64 paths.
1746#[derive(Debug, Clone, PartialEq)]
1747pub enum ExtActivation {
1748    /// Leaky ReLU: max(alpha * x, x) where alpha is the negative slope.
1749    LeakyRelu(f64),
1750    /// Swish: x * sigmoid(beta * x).  beta=1 recovers SiLU.
1751    Swish(f64),
1752    /// Standard ReLU.
1753    Relu,
1754    /// Logistic sigmoid.
1755    Sigmoid,
1756    /// Hyperbolic tangent.
1757    Tanh,
1758    /// Identity.
1759    Linear,
1760}
1761impl ExtActivation {
1762    /// Evaluate the activation function at `x`.
1763    pub fn apply(&self, x: f64) -> f64 {
1764        match self {
1765            ExtActivation::LeakyRelu(alpha) => {
1766                if x >= 0.0 {
1767                    x
1768                } else {
1769                    alpha * x
1770                }
1771            }
1772            ExtActivation::Swish(beta) => x / (1.0 + (-beta * x).exp()),
1773            ExtActivation::Relu => x.max(0.0),
1774            ExtActivation::Sigmoid => 1.0 / (1.0 + (-x).exp()),
1775            ExtActivation::Tanh => x.tanh(),
1776            ExtActivation::Linear => x,
1777        }
1778    }
1779    /// Evaluate the derivative of the activation function at `x`.
1780    pub fn derivative(&self, x: f64) -> f64 {
1781        match self {
1782            ExtActivation::LeakyRelu(alpha) => {
1783                if x >= 0.0 {
1784                    1.0
1785                } else {
1786                    *alpha
1787                }
1788            }
1789            ExtActivation::Swish(beta) => {
1790                let sig = 1.0 / (1.0 + (-beta * x).exp());
1791                sig + beta * x * sig * (1.0 - sig)
1792            }
1793            ExtActivation::Relu => {
1794                if x > 0.0 {
1795                    1.0
1796                } else {
1797                    0.0
1798                }
1799            }
1800            ExtActivation::Sigmoid => {
1801                let s = 1.0 / (1.0 + (-x).exp());
1802                s * (1.0 - s)
1803            }
1804            ExtActivation::Tanh => {
1805                let t = x.tanh();
1806                1.0 - t * t
1807            }
1808            ExtActivation::Linear => 1.0,
1809        }
1810    }
1811    /// Apply elementwise to a vector in-place.
1812    pub fn apply_vec(&self, v: &mut [f64]) {
1813        for x in v.iter_mut() {
1814            *x = self.apply(*x);
1815        }
1816    }
1817}