snn_mbt

    Bit-exact MoonBit port of SpikingNeuralNetworks.jl, extended with modern training machinery. NEURONS AND PLASTICITY: IF, AdEx, Izhikevich (IZ), HH, MorrisLecar, Poisson (including InhomogeneousPoisson), WilsonCowan; Markram STP, Gerstner / MexicanHat / AntiSymmetric / vSTDP / Confavreux2025 STDP; AMPA / NMDA / GABAa / GABAb receptors; multicompartment dendritic neurons (BallAndStick, Tripod, Multipod); IF with population synaptic conductance; spike-time and current stimuli. CORE DL: Tensor struct with broadcasting + softmax / log-softmax / cross-entropy, Conv2d / MaxPool2d / GlobalAvgPool2d / ReLU / Flatten / Linear forward AND backward, ResNet foundations, generic Chain / Sequential, pre-made MLP / SimpleCNN / LeLen5 bottles, image adapter (BMP/QOI/TGA/PNG/GIF/JPEG/ICO/TIFF) via riantr/moonbit_image, 5D [T,B,C,H,W] STImage. OPTIMIZERS AND SCHEDULING: SGD, SGD-with-momentum, Adam with bias correction, AdamW, RMSprop, AdaGrad, Adafactor; StepLR, ExponentialLR, CosineAnnealingLR, ReduceLROnPlateau; BatchNorm2d with running stats and LayerNorm per-sample, both with backward. SURROGATE GRADIENTS AND ATTENTION: fast-sigmoid (Zenke & Ganguli 2018) for BPTT through IF/LIF Heaviside; GELU, exact sparse GELU via libm erff; multi-head self-attention with mask utilities; sinusoidal and learnable positional encoding; Pre-Norm transformer block; inverted dropout; SAttention unifying three spiking attention variants; LayerScale; T5-style relative position bias; SpikingAttention with full BPTT backward; SpikeFormer demo. RL: SequenceReplayBuffer for recurrent RL, DDPG, TD3, SAC, GRU/LSTM/GTrXL recurrent actors and critics, Decision Transformer, Trajectory Transformer with beam search. META-LEARNING AND CAUSAL: MAML, FOMAML, Reptile, TaskSampler; LinearGaussianSCM, DoCalculus, CounterfactualReasoning, CausalEffectEstimator. GENERATIVE: RealNVP / Glow / NormalizingFlow, VAE, IWAE, ELBO, DDPM diffusion, EnergyBasedModel with Langevin sampling, DCGAN, WGAN-GP. OTHER: Vision Transformer with trainer, EmbeddingNet / SiameseNet / SiameseTrainer, CapsuleNetwork. GRAPH NEURAL NETWORKS: Graph with COO edge list and GCN adjacency normalisation; GCN, MPNN, GIN, PNA, GAT (multi-head), Set2Set readout, EdgeGNN, H2GCN, GraphClassifier -- and FULL BACKWARD PASSES for all of them, dispatched behind a single TrainableGraphNet enum, with node-level and graph-level training steps. VERIFICATION: two runnable harnesses, since `moon test` cannot link this package on Windows (998 .mbt files exceed the 32K command-line limit). `verify/verify_gnn_grads.ps1` runs a finite-difference gradient gate over 8 dL/dh configurations, 8 dL/dW configurations, the multi-head GAT stack, a deterministic ELU-mask row, and a cross-entropy loss-vs-gradient consistency section -- judged by relative tolerance, by a kink-versus-bug component count, and against negative controls. `verify/run_gnn_train_demo.ps1` runs an end-to-end transductive two-community training demo over all five backbones with a held-out split, a shuffled-label control and a no-message-passing baseline. These harnesses found 13 real defects that the type checker and code review had both passed, including a cross-entropy loss whose sign was inverted for six releases. Float32 end to end with libm FFI (expf, tanhf, logf, sqrtf, cosf, sinf, erff).

    spiking-neural-network
    neuroscience
    bit-exact
    izhikevich
    if-neuron
    adex
    hodgkin-huxley
    morris-lecar
    stdp
    stp
    poisson
    dendritic
    snns
    julia-port
    simulation
    conv2d
    cnn
    maxpool
    image
    tensor
    video
    sgd
    adam
    adamw
    rmsprop
    adagrad
    optimizer
    batch-norm
    layer-norm
    normalization
    lr-scheduler
    cosine-annealing
    reduce-on-plateau
    chain
    sequential
    mlp
    cnn
    lenet
    add
    avgpool
    global-avgpool
    residual
    resnet
    surrogate-gradient
    fast-sigmoid
    bp-friendly-spike
    gelu
    transformer-activation
    multi-head-attention
    self-attention
    transformer-block
    positional-encoding
    sinusoidal
    transformer-block
    pre-norm
    causal-mask
    attention-mask
    dropout
    inverted-dropout
    spiking-attention
    spikeformer
    spiking-transformer
    snnt-transformer
    sparse-gelu
    exact-gelu
    adafactor
    layer-scale
    t5-relative-position
    sattention
    spiking-cross-attention
    spiking-self-attention
    srnn
    csnn
    bptt
    spikeformer-demo
    eval-accuracy
    argmax
    prediction
    classification-accuracy
    Download zip
    Author
    Version
    0.155.0
    License
    MIT
    Last updated
    8 hours ago
    Downloads
    33
    # snn_mbt 鈥?MoonBit port of SpikingNeuralNetworks.jl

    A bit-exact MoonBit re-implementation of the SpikingNeuralNetworks.jl ecosystem. Long-term goal: every example in SpikingNeuralNetworks.jl/examples/ runs under MoonBit and produces the same numerical trajectories (last-bit Float32) as the Julia run.

    #Status (v0.155.0, 2026-10-05)

    moon.mod carries the version that is published to mooncakes.io, and this header tracks it. The batch sections at the end of this file run from Batch C (v0.58.0) through Batch AC (v0.155.0) and describe what changed in each.

    ComponentStatusNotes
    Unit system鉁?done30+ Float32 unit constants; units_test.mbt (8 tests pass)
    Time struct鉁?donet, tt, dt; update_time etc. (4 tests pass)
    Xoshiro RNG鉁?donexoshiro256++ (NOT **); Float32/Float64 paths match Julia (5 tests pass)
    Native math FFI鉁?doneexpf, tanhf, logf via libm; sigmoid_f32 derived (5 tests pass)
    math.ln/cos/sin鉁?doneRequired for Box-Muller in IZ init; via moonbitlang/core/math
    IF neuron鉁?doneForward-Euler update; DoubleExpSynapse state; IFParameter::with_el (4 tests pass)
    IF + Gsyn (Duarte2019 / LKD2014)鉁?doneneuron_if_gsyn.mbt 鈥?port of IFParameterGsyn from refs/SNNModels.jl/src/populations/generized_if/if.jl. IFParameterGsyn { base : IFParameter, gsyn_e : Float, gsyn_i : Float } wraps IFParameter + population-wide synaptic conductance scales. IF::with_gsyn(n, base, gsyn_e, gsyn_i, rng) convenience constructor + IFParameterGsyn::apply(p) setter that propagates the population scale to every neuron's gsyn_e[i] / gsyn_i[i] array (5 tests pass)
    AdEx neuron鉁?doneBrette-Gerstner 2005 defaults; exponential term via expf; AdExParameter::with_vr (5 tests pass)
    IZ (Izhikevich)鉁?doneTwo half-step midpoint Euler; ge/gi decay; v > 30 reset; fs() constructor (4 tests pass)
    HH (Hodgkin-Huxley)鉁?donem/n/h gating with sigmoid+expf; Na/K currents; v > -20 reset (2 tests pass)
    MorrisLecar鉁?donetanhf-based activation; K recovery w; v > 20 reset (3 tests pass)
    Poisson (population)鉁?donefire[i] = rand(Float32) < rate*dt; rate matches frequency (3 tests pass)
    InhomogeneousPoisson (variable-rate)鉁?doneneuron_inhomogeneous_poisson.mbt 鈥?port of inhomogeneous_poisson.jl (Julia's VariablePoisson). InhomogeneousPoissonParam{beta, tau, r0, rate_timescale} + InhomogeneousPoisson::new(n, param, rng) + step_inhomogeneous_poisson(p, dt, rng) (Ornstein-Uhlenbeck noise + rate adaptation toward r0; bit-exact integration) (7 tests pass)
    PoissonStimulus鉁?doneKnuth's algorithm; Float32 位; mean verified 鈮?位 (3 tests pass)
    CurrentStimulus鉁?doneDirect current injection with optional Gaussian noise; set_active, set_i_base (3 tests pass)
    SpikeTimeStimulus鉁?doneSpikeTimeParameter(spiketimes, neurons) (auto-sorted) + SpikeTimeStimulus::new(pop, sym, ...) + stimulate_spiketime(s, t, w). Wired into compose via TimedStim_(stim, w) (4 tests pass)
    CurrentStimulusArray鉁?doneGeneric current injection into raw Array[Float] for any population
    WilsonCowan rate model鉁?donex += dt*(-x+g+I); r=tanhf(x); init Normal(0, 0.5); g reset (5 tests pass)
    RateSynapse鉁?doneCSR forward_rate: g[post] += w * rJ[pre]; weights Normal(0, 渭/鈭?pN))
    AnyPop dispatcher鉁?doneEnum-based heterogeneous sim loop; includes WC_, PoissonIF_, CurrentIF_, CurrentArr_ (3 tests pass)
    AnyStim dispatcher鉁?donePoissonIF_, CurrentIF_ variants; stimulate_any dispatch
    Monitor sr鉁?doneMonitor::new_v_sr(pop, n, sr_hz) honours rec_step = 1/(sr*dt) (4 tests pass)
    vecplot text dump + ascii_plot鉁?donecount_spikes, count_spikes_interval, dump_summary, dump_csv_stdout, duration, ascii_plot(width?, height?), firing_rate, firing_rate_interval, spike_times, mean, min, max (17 tests pass)
    SparseMatrixCSR鉁?donefrom_dense, random, random_with_rule, set, get, forward, forward_rate (10 tests pass). Subset port of sparse_matrix_test.jl (matrix_size / get / set / nnz / bulk-set) in sparse_matrix_extra_test.mbt (8 tests pass)
    Population analysis鉁?doneanalysis_populations.mbt 鈥?port of populations.jl (subset). PopIndex struct (1-based inclusive ranges) + population_indices(pops) (assigns non-overlapping ranges) + filter_items(pops) (default drops "noise*" labels) + filter_items_with(pops, rule) (named FilterRule enum: Greater / Less / Equal / DropNoise; MoonBit lacks first-class fn refs) + average_conn_strength(M, pops, 渭) (block-mean / 渭 of dense weight matrix) (13 tests pass)
    ConnectRule enum鉁?doneBernoulli, FixedIn, FixedOut rules (3 tests pass)
    SpikingSynapse (CSR)鉁?donenew, random, random_with_rule, random_with_delays (delay_dist), spiking_connect, set_constant_delay, init_rho (Markram STP), forward_synapse, deliver_pending_synapse. Pending-event queue drains scheduled spikes at delivery time. v0.10.57: added name field (mirrors Julia's name kwarg) + with_name setter (returns new struct). EmptyConnection placeholder (no-op, mirrors Julia's EmptySynapse) (10 tests pass in connection_spiking_extra_test.mbt)
    Conv2d (NCHW forward)鉁?doneconv2d.mbt 鈥?NCHW row-major flat Array[Float] conv2d forward. Conv2dParam { weight, bias, c_out, c_in, kh, kw, stride, pad }; Conv2dParam::new with optional stride=1, pad=0 defaults; conv2d_forward(input, n, c_in, h, w, param) -> Array[Float] returns [n, c_out, ho, wo] flat. Naive CPU loop (no im2col/matmul yet). 9 tests in conv2d_test.mbt (1x1 conv, 3x3 valid 5x5 -> 3x3, same-pad preserves shape, multi-channel in/out + stride=2 downsample, batch N=2, 1x1 multi-channel out-ch=2, all-zero input -> bias broadcast, 3x3 valid 4x4 known center pixel + bias, 3x3 box-blur sums window)
    MaxPool2d (NCHW forward)鉁?donemaxpool2d.mbt 鈥?NCHW row-major flat Array[Float] max-pool forward. MaxPool2dParam { kh, kw, stride, pad }; MaxPool2dParam::new(kh, kw, stride?, pad?) with stride defaulting to kh; maxpool2d_forward(input, n, c, h, w, param) -> Array[Float] returns [n, c, ho, wo] flat. Out-of-bounds positions treated as -inf so padding never wins the max. 8 tests in maxpool2d_test.mbt (2x2 non-overlapping, stride=1 overlapping, multi-channel per-channel max, batch N=2, pad=1 + stride=2 on 4x3, all-negative picks least-negative, 3x3 stride=1 on 5x5 diagonal, 3x3 on 5x5 single-corner pick)
    ReLU✅ donerelu.mbt — element-wise max(x, 0). relu_forward(input : Array[Float]) -> Array[Float] returns a new array (does not mutate input). 4 tests in relu_test.mbt (mixed pos/neg / all-pos unchanged / all-neg -> zero / no-mutation)
    Flatten (NCHW → (N, C·H·W))✅ doneflatten.mbt — flatten_forward(input, n, c, h, w) -> Array[Float] returns flat array of length n*c*h*w. No data movement: NCHW row-major index (n, c*H*W + h*W + w) already matches the (n, c*h*w) row-major layout, so the output is a copy of the input. 3 tests in flatten_test.mbt (single-channel / multi-channel / batch N=2)
    Linear (dense / fully-connected)✅ donelinear.mbt — LinearParam { weight, bias, in_features, out_features } + LinearParam::new(weight, bias, in_features, out_features) builder; linear_forward(input, n, param) -> Array[Float] returns [n, out_features]. Per-batch matvec + bias. 5 tests in linear_test.mbt (single-batch matvec+bias / batch N=3 / zero input -> bias / zero weight -> bias / single-output scalar projection)
    CNN forward chain (Conv → ReLU → MaxPool → Flatten → Linear)✅ donecnn_chain_test.mbt — end-to-end mini-CNN forward pass test. Verifies shape flow (4x4 image -> Conv2d -> ReLU -> MaxPool2d -> Flatten -> Linear produces a 3-element output vector) and bit-exact repeatability. 2 tests (full chain on 4x4 / bit-exact repeat on 3x3)
    ReLU backward✅ donerelu_backward.mbt — relu_backward(input, d_output) -> Array[Float]; d_input[i] = d_output[i] if input[i] > 0 else 0. 4 tests in relu_backward_test.mbt (pass-through / zero-where-inactive / no-mutation / gradient check vs numerical)
    Flatten backward✅ doneflatten_backward.mbt — identity copy (NCHW layout is already collapseable). 3 tests in flatten_backward_test.mbt
    Linear backward✅ donelinear_backward.mbt — linear_backward(input, d_output, n, param) -> (d_input, d_weight, d_bias). d_weight via outer product, d_bias via sum, d_input via weight matvec. 4 tests in linear_backward_test.mbt (single-batch known / N=2 batch / d_input gradient check / d_weight gradient check)
    MaxPool2d backward✅ donemaxpool2d_backward.mbt — maxpool2d_forward_with_idx(input, n, c, h, w, param) -> (output, argmax_idx) records argmax indices during forward; maxpool2d_backward(d_output, argmax_idx, n, c, h, w, param) -> d_input routes gradient to argmax positions only. 4 tests in maxpool2d_backward_test.mbt
    Conv2d backward✅ doneconv2d_backward.mbt — conv2d_backward(input, d_output, n, c_in, h, w, param) -> (d_input, d_weight, d_bias). Three accumulated gradients via 7-nested loops. 6 tests in conv2d_backward_test.mbt (d_bias known / 1x1 d_weight / 1x1 d_input / d_input gradient check / d_weight gradient check / d_bias known)
    CNN backward chain (Conv → ReLU → MaxPool → Flatten → Linear)✅ donecnn_backward_chain_test.mbt — end-to-end mini-CNN backward. Hand-stitches each layer's backward. Verifies d_input and d_weight against numerical gradients (eps=1e-3, tol=1e-2) and bit-exact repeat across re-runs. 3 tests (d_input gradient check / bit-exact re-run / d_weight gradient check)
    Numerical gradient check helper✅ donegradient_check.mbt — numerical_gradient(input, eps, forward) central-difference helper + max_abs_diff(a, b) comparison. Reused by all backward tests
    Tensor struct + elementwise ops + broadcasting✅ donetensor.mbt — Tensor { data, shape } struct (row-major flat Array[Float]); Tensor::zeros / Tensor::ones / Tensor::from / Tensor::reshape / Tensor::numel. Element-wise: tensor_add / tensor_sub / tensor_mul / tensor_div with right-aligned broadcasting (scalar / row / column broadcast). 9 tests in tensor_test.mbt (zeros / ones / reshape / add same-shape / add scalar / add row vector / add col vector / sub-mul-div / no-alias)
    Tensor matmul + softmax + log_softmax✅ donetensor_ops.mbt — tensor_matmul(a, b) 2D matmul (naive O(mnk)); tensor_softmax(x) numerically-stable softmax along last axis (subtract-max trick); tensor_log_softmax(x) log-softmax. Uses libm expf / logf via FFI. 9 tests in tensor_ops_test.mbt (matmul 2x3×3x2 / identity / 1x1 / softmax uniform / softmax translation-invariant / softmax batched / log_softmax roundtrip / log_softmax sum=1 / matmul all-ones × values)
    Cross-entropy loss✅ donecross_entropy.mbt — CrossEntropyLoss { mean : Tensor, per_batch : Tensor } struct; cross_entropy_loss(log_probs, targets) -> CrossEntropyLoss returns mean + per-batch loss. 4 tests in cross_entropy_test.mbt (perfect predictions / uniform / batch mixed / softmax+cross_entropy pipeline)
    Image adapter (BMP/QOI/TGA/PNG/GIF/JPEG/ICO/TIFF)✅ doneimage.mbt — 4D NCHW Float32 Image { data, n, c, h, w } plus image_from_bytes(bytes) -> Image raise DecodeError (auto-detect format via riantr/moonbit_image@0.3.4), image_from_moonbit_image(img) (3-channel RGB), image_from_moonbit_image_rgba(img) (4-channel with alpha). Encode path not exposed — upstream's PixelFormat enum variants are read-only from outside the package, so users wanting to write image files call upstream directly. 4 tests in image_test.mbt (zeros shape / 24-bit BMP 2x2 round-trip with 4 distinct pixels / uniform 4x4 BMP / RGBA alpha=1.0 for opaque)
    STImage 5D [T, B, C, H, W] tensor✅ donespatio_temporal.mbt — 5D Float32 STImage { data, t, b, c, h, w } for spiking-CNN time-series input. Row-major flat Array[Float] of length T*B*C*H*W; flat offset t * (B*C*H*W) + b * (C*H*W) + c * (H*W) + h * W + w. Helpers: STImage::zeros / shape / numel / frame_block / batch_slice / offset / get / set / get_frame(t) (returns 4D Image { n: 1, c, h, w }, ready for v0.11.x Conv2d/MaxPool2d) / get_batch(b) (returns 4D Image { n: T, c, h, w }, time-flattened for vSTDP) / STImage::from_frames(frames) (stack [N=1, C, H, W] images along new T axis) / st_image_concat_t(a, b) (concatenate along T) / st_image_repeat_t(img, t) (broadcast a single image across T). 9 tests in spatio_temporal_test.mbt (zeros / flat offset computation / set-get round-trip / get_frame independence + no alias / get_batch time-flatten / from_frames stacking / concat_t ordering / repeat_t identical frames / Float32 bit-exact round-trip)
    SGD optimiser (vanilla + momentum)✅ doneoptimizer_sgd.mbt — generic array-level sgd_update_arrays(weight, bias, d_weight, d_bias, lr) plus SGDMomentumState { v_w, v_b } for SGD-with-momentum (PyTorch convention: v <- momentum * v + g; param <- param - lr * v). Layer wrappers sgd_update_linear / sgd_update_conv and momentum wrappers sgd_momentum_update_linear / sgd_momentum_update_conv return fresh parameter structs (immutable update). 9 tests in optimizer_sgd_test.mbt (vanilla step / zero-gradient no-op / no-alias / Linear wrapper preserves shape / Conv wrapper preserves stride+pad / first momentum step = SGD with v=0 / second step accumulates velocity / momentum=0 == vanilla SGD / loss monotonically decreases on y=2x+1 fit, 200 steps)
    Adam optimiser (with bias correction)✅ doneoptimizer_adam.mbt — AdamState { m_w, v_w, m_b, v_b : Array[Float]; step : Int } with adam_init(weight_len, bias_len), adam_next_step(state) accessor, adam_update_arrays(...) (PyTorch convention: m <- beta1*m + (1-beta1)*g; v <- beta2*v + (1-beta2)*g^2; m_hat <- m/(1-beta1^t); v_hat <- v/(1-beta2^t); param <- param - lr*m_hat/(sqrt(v_hat)+eps)). Layer wrappers adam_update_linear / adam_update_conv. sqrtf Float32 FFI added (was missing from math_native.mbt). 9 tests in optimizer_adam_test.mbt (zero state / next_step no-mutation / first step moves param in correct direction / 600-step linear regression converges w→2 b→1 / step counter monotonic via returned state / zero gradient = no-op / Linear shape preserved / Conv shape preserved with stride+pad)
    AdamW optimiser (decoupled weight decay)✅ doneoptimizer_adamw.mbt — AdamWState { m_w, v_w, m_b, v_b : Array[Float]; step : Int } (same shape as AdamState) with adamw_init, adamw_update_arrays (Loshchilov & Hutter 2019 convention: decoupled param <- param - lr*(m_hat/(sqrt(v_hat)+eps) + weight_decay*param), where weight_decay acts on the raw parameter not on the gradient). Layer wrappers adamw_update_linear / adamw_update_conv. 7 tests in optimizer_adamw_test.mbt (zero state / weight_decay=0 matches Adam direction / large weights shrink more absolutely than small weights under zero gradient / step counter via returned state / 600-step linear regression converges with mild decay / Linear shape / Conv shape)
    RMSprop optimiser✅ doneoptimizer_rmsprop.mbt — RMSpropState { v_w, v_b } (no first moment, no bias correction — Hinton 2012 lecture 6e form) with rmsprop_init, rmsprop_update_arrays (v <- alpha*v + (1-alpha)*g^2; param <- param - lr*g/(sqrt(v)+eps)). Layer wrappers rmsprop_update_linear / rmsprop_update_conv. 7 tests in optimizer_rmsprop_test.mbt (zero state / first step moves param in negative-gradient direction / alpha=0 ≈ raw gradient sign / large gradient gets shrunk by adaptive scaling / 600-step linear regression converges w→2 b→1 / Linear shape / Conv shape)
    AdaGrad optimiser✅ doneoptimizer_adagrad.mbt — AdaGradState { v_w, v_b } with adagrad_init, adagrad_update_arrays (Duchi et al. JMLR 2011: v <- v + g^2; param <- param - lr*g/(sqrt(v)+eps). Accumulating sum, not EMA — v monotonically grows, so effective per-parameter learning rate shrinks over time. Sparse-feature friendly; can stagnate on dense problems.) Layer wrappers adagrad_update_linear / adagrad_update_conv. 7 tests in optimizer_adagrad_test.mbt (zero state / first step moves in negative-gradient direction / v accumulates g^2 monotonically (not EMA) / per-parameter effective lr monotonically shrinks / 1500-step linear regression converges w→2 b→1 / Linear shape / Conv shape)
    BatchNorm2d (NCHW, with running stats)✅ donebatch_norm2d.mbt — BatchNorm2d { gamma, beta, mut running_mean, mut running_var, momentum, eps, mut training }. Forward batch_norm2d_forward(input, n, c, h, w, bn) -> (output, BatchNormCache); backward batch_norm2d_backward(d_output, cache, bn) -> (d_input, d_gamma, d_beta). Per-channel batch stats in training mode (across nhw), running stats for inference. PyTorch convention EMA on running stats: running <- (1 - momentum) * running + momentum * batch. Population variance (no Bessel correction). 8 tests in batch_norm2d_test.mbt (output shape / per-channel mean~0 std~1 / gamma/beta affine / running stats EMA update / backward shapes / zero d_output no-op / d_input numerical-gradient check).
    LayerNorm (per-sample, no running stats)✅ donelayer_norm.mbt — LayerNorm { gamma, beta, eps, c, h, w }. Forward layer_norm_forward(input, n, c, h, w, ln) -> (output, LayerNormCache); backward layer_norm_backward(d_output, cache, ln) -> (d_input, d_gamma, d_beta). Per-sample statistics (across chw). gamma/beta have shape [c*h*w] (per-feature). No running stats — deterministic at inference. 8 tests in layer_norm_test.mbt (output shape / per-sample mean~0 std~1 / different samples normalised independently / gamma/beta affine / backward shapes / zero d_output no-op / d_input numerical-gradient check).
    Learning-rate schedulers (StepLR + ExponentialLR + CosineAnnealingLR + ReduceLROnPlateau)✅ donescheduler.mbt — 4 schedulers sharing the step(s) -> Float / next(s) -> S API (state is not mutated; caller threads the returned state). StepLR::new(base_lr, step_size, gamma) drops lr by gamma every step_size steps; ExponentialLR::new(base_lr, gamma) does continuous exponential decay; CosineAnnealingLR::new(eta_max, eta_min, t_max) follows a half-cosine from eta_max to eta_min over t_max steps (wraps and repeats). ReduceLROnPlateau::new(base_lr, factor, patience, threshold) is reactive: feeds step(metric) returns (new_lr, new_state), drops lr by factor after patience consecutive non-improving calls (PyTorch convention with threshold-filtered improvements). cosf Float32 FFI added. 10 tests in scheduler_test.mbt (StepLR: step 0 = base, drops at step_size boundaries, next() does not mutate; ExponentialLR: lr = base*gamma^t; CosineAnnealingLR: t=0 -> eta_max, t=T_max/2 -> midpoint, monotonic decrease in first half; ReduceLROnPlateau: first call records metric, improvement resets bad counter, patience exhausted triggers decay, threshold prevents micro-improvements).
    Generic Chain / Sequential (tagged-enum dispatch)✅ donechain.mbt — Layer enum (Conv2d / ReLU / MaxPool2d / Flatten / Linear / BatchNorm2d / LayerNorm) + public Layer::conv2d / relu / max_pool2d / flatten / linear / batch_norm2d / layer_norm constructor helpers (MoonBit enum variants are read-only from other files). chain_forward(layers, input, n, c, h, w) -> (output, Array[LayerCache], n, c, h, w) runs layers in order, tracking shape transitions; chain_backward(layers, caches, d_output, n, c, h, w) -> (d_input, Array[LayerGrad]) runs them in reverse, returning per-layer LayerGrad enum (Linear(d_w, d_b) / Conv2d(d_w, d_b) / BatchNorm2d(d_g, d_b) / LayerNorm(d_g, d_b) / Empty / MaxPool2d). 7 tests in chain_test.mbt (empty chain / MLP forward / MLP backward with shape + grads / CNN Conv-ReLU-Pool-Flatten-Linear / zero d_output no-op / d_input numerical-gradient check vs central differences with non-zero pre-ReLU activations / BN inside MLP).
    Pre-made bottles (MLP / SimpleCNN / LeNet5)✅ donebottles.mbt — three ready-to-train architectures built on top of v0.19.0 Chain with He-init (sqrt(2/fan_in)) + Float32 Box-Muller via libm cosf/sinf/logf + xoshiro RNG seeded by caller for reproducibility. (1) MLP::new(sizes=[in,h1,...,out], seed) — sizes-matched Linears with ReLU between every pair (no ReLU after the final); mlp_forward / mlp_backward / MLP::num_params. (2) SimpleCNN::new(in_c, c1, c2, seed) — Conv-ReLU-Pool × 2 → Flatten → FC(10) for 28x28 inputs. (3) LeNet5::new(seed) — classic LeNet-5 (C1: 1->6 5x5, S2: MaxPool 2x2, C3: 6->16 5x5, S4: MaxPool 2x2, C5: 16->120 5x5, F6: 120->84, Out: 84->10). 10 tests in bottles_test.mbt (MLP: layer count, reproducible seed, different seeds differ, num_params, forward shape, backward shape + grad variants; SimpleCNN: forward shape + 8 caches; LeNet5: forward shape + 12 caches + zero d_output zero d_input + zero grads).
    Elementwise Add✅ doneelementwise_add.mbt — add_forward(a, b) -> Array[Float] elementwise (no aliasing); add_backward(d_output) -> (d_a, d_b) returns two independent copies. 5 tests (elementwise sum / no alias / zero-tensor identity / backward independence / gradient flow).
    AvgPool2d + GlobalAvgPool2d✅ doneavgpool2d.mbt — AvgPool2dParam::new(kh, kw, stride?, pad?) + avgpool2d_forward / avgpool2d_backward (count_include_pad=True: divisor is kh*kw regardless of pad). global_avg_pool2d_forward(input, n, c, h, w) -> Array[Float] pools to [n, c] (ResNet terminal pool); global_avg_pool2d_backward distributes gradient uniformly. 8 tests (output shape / 2x2 average / per-channel independence / single-pool gradient / overlapping pools accumulate / GAP shape / GAP backward uniform / GAP round-trip).
    ResidualBlock (basic + projection)✅ doneresidual_block.mbt — ResidualBlock struct with optional 1x1 conv + BN projection shortcut. ResidualBlock::identity(c, seed) for same-shape stacks; ResidualBlock::projection(c_in, c_out, stride, seed) for downsample blocks. residual_block_forward(b, x, n, c, h, w) -> (out, ResidualCache) runs conv1 → bn1 → relu → conv2 → bn2 → add(shortcut) → relu, caching sum and bn1_out for backward; residual_block_backward(b, cache, d_output) -> (d_input, ResidualGrads) runs the full reverse path including the shortcut BN/conv and merges d_x_main + d_x_shortcut. 5 tests (identity forward shape / zero d_output no-op / projection forward halves spatial / projection backward d_input shape + shortcut grads populated / conv1 weight numerical gradient check vs central differences).
    Surrogate gradient (fast sigmoid)✅ donesurrogate.mbt — Zenke & Ganguli 2018 fast-sigmoid surrogate for BPTT through IF / LIF Heaviside spikes. fast_sigmoid_surrogate(x, beta) -> Float is the backward surrogate σ'(x) = 1 / (1 + β·x)²; peak σ'(0) = 1, symmetric, decays asx→ ∞. fast_sigmoid_forward(x, beta) -> Float is the soft differentiable forward s(x) = x / (1 + β·x) saturating to ±1/β. heaviside_step(x, vt) -> Float is the hard 0/1 forward spike (matches the IF neuron step exactly). fast_sigmoid_surrogate_array / fast_sigmoid_forward_array elementwise forms. spike_surrogate(u, vt, beta) -> SpikeSurrogate combined envelope that returns both spike : Array[Float] (hard 0/1) and grad : Array[Float] (surrogate) in one pass — avoids recomputing u - vt in BPTT loops. 7 tests (peak=1 + symmetry + decay / β controls width + σ'(1/β)=0.25 invariant / soft forward monotonicity + saturation at ±1/β / elementwise consistency + peak-index / antisymmetric input / hard step at threshold / spike_surrogate combined envelope).
    GELU activation (tanh approximation)✅ donegelu.mbt + gelu_backward.mbt — Gaussian Error Linear Unit for transformer FFN. gelu(x) ≈ 0.5·x·(1 + tanhf(√(2/π)·(x + 0.044715·x³))) (matches PyTorch's F.gelu(approximate='tanh')). gelu_grad(x) = 0.5·(1 + t) + 0.5·x·sech²(inner)·√(2/π)·(1 + 3·0.044715·x²). gelu_forward(input) -> Array[Float] and gelu_backward(input, d_output) -> Array[Float]. 6 tests (origin=0 + saturation at ±5 + gelu(±1)≈±0.1587/0.8413 / grad(0)=0.5 + positive-side steeper + saturation at ±5 / element-wise consistency + no-mutation / gradient check vs central difference / element-wise backward + zero d_output no-op).
    Multi-Head Self-Attention✅ donemulti_head_attention.mbt — Vaswani 2017 standard MHA. MultiHeadAttention { d_model, num_heads, d_k, w_q, w_k, w_v, w_o } with MultiHeadAttention::new(d_model, num_heads, seed) (Xavier-normal init via Float32 Box-Muller + zero bias). multi_head_attention_forward(x, mha, mask) -> (out, AttnCache) self-attention: Q/K/V linear projections → per-head scaled dot-product q@k^T/sqrt(d_k) → optional additive mask → softmax along key axis → weighted sum → head merge → W_o projection. multi_head_attention_backward(cache, d_output, mha) -> (d_x, MHAGrad) full reverse path including softmax backward (d_scores = w*(d_w - w·d_w)), per-head Q/K gradient accumulation, and 3× linear backward (Q/K/V) summed into d_x. 8 tests (shape / softmax sum-to-1 / deterministic same-seed / different-seed independence / masked positions contribute zero weight + sum-to-1 / d_x+d_weight+d_bias shapes / zero d_output no-op / gradient check vs central difference on w_o[0]).
    Position encoding (sinusoidal + learnable)✅ doneposition_encoding.mbt — two complementary encodings for transformer input. (1) sinusoidal_position_encoding(max_len, d_model) -> Array[Float] non-trainable PE table with PE[pos, 2i] = sin(pos / 10000^(2i/d_model)) and PE[pos, 2i+1] = cos(...) (Vaswani 2017 §3.5). (2) PositionalEmbedding { max_len, d_model, weight } learnable parameter struct + PositionalEmbedding::new(max_len, d_model, seed) with Xavier-normal init via Float32 Box-Muller + positional_embedding_forward(pe, seq_len) -> Array[Float] slicing + positional_embedding_backward(pe, d_output, seq_len) -> Array[Float] (only active positions accumulate gradients). 5 tests (sinusoidal shape + boundary PE[0,*] = 0/1 / parity (even=sin, odd=cos) + determinism + distinct positions / learnable shape + determinism + non-zero init / forward slicing correctness + backward active-position routing).
    Transformer block (Pre-Norm)✅ donetransformer_block.mbt — Pre-Norm sub-layer stack: x2 = x + MHA(LN1(x)); out = x2 + FFN(LN2(x2)) where FFN(h) = Linear2(GELU(Linear1(h))) and d_ff defaults to 4·d_model. TransformerBlock { d_model, num_heads, d_ff, ln_1, ln_2, mha, ffn_w1, ffn_w2 } (LN gamma=1, beta=0 by default; FFN Xavier-normal). transformer_block_forward(x, block, mask) -> (out, TransformerBlockCache) composes LN → MHA → residual → LN → Linear → GELU → Linear → residual. transformer_block_backward(cache, d_output, block) -> (d_x, TransformerBlockGrad) walks 7 reverse steps including two residual splits and the cross-residual gradient accumulation. 7 tests (constructor shape + default d_ff=4·d_model / custom d_ff override / forward shape preservation + cache sanity / same-seed determinism / backward shapes including LN gamma/beta / zero d_output → zero d_x + zero all 8 grad arrays / gradient check vs central difference on ffn_w2[0]).
    Attention mask utilities✅ doneattention_mask.mbt — additive mask helpers for MHA. causal_mask(seq_len) -> Array[Float] upper-triangular -1e9 mask for autoregressive attention. mask_broadcast(mask_2d, seq_len, num_heads) replicates (seq_len × seq_len) → (num_heads × seq_len × seq_len). mask_from_allowed(seq_len, allowed : Array[Array[Bool]]) arbitrary boolean-table → additive mask (0 allowed, -1e9 blocked). causal_mask_heads(seq_len, num_heads) convenience: causal directly broadcast to per-head. 5 tests (shape + diagonal=0 + above=-1e9 / row 0 only attends to self / mask_broadcast shape + values replicated across heads / mask_from_allowed arbitrary boolean matrix / causal end-to-end via MHA: position i weights[j]=0 for j>i and row sum=1).
    Dropout (inverted)✅ donedropout.mbt — Dropout { p, mut training } (pub(all) for cross-file mut) with Dropout::new(p? = 0.5). dropout_forward(input, d, rng) -> (out, mask) returns the per-element Bernoulli mask + scaled activations (training) or identity (inference). dropout_backward(d_output, mask, d) -> Array[Float] reuses the same mask for the backward (so gradient corresponds to actual sparse activations). 8 tests (defaults + out-of-range guard / inference identity / training keeps ~p=0.5 fraction of n=1000 + 1/(1-p) inverted scale / same-seed determinism / p=0 no-op identity / p=1 all-zero / backward matches forward (mask × scale) + zero d_output no-op / inference backward identity).
    Spiking Self-Attention (SpikeFormer-style)✅ donespiking_attention.mbt — standard MHA structure (Q/K/V/O Linear projections + scaled dot-product) but replaces softmax along key axis with the fast_sigmoid_forward from v0.22.0. weights = fast_sigmoid_forward(scores, beta) range (-1/β, 1/β). Backward uses fast_sigmoid_surrogate (peak 1 at x=0, decays atx→∞) — this is the BPTT-compatible gradient that lets the spike emit non-zero learning signal. SpikingAttention { d_model, num_heads, d_k, beta, w_q, w_k, w_v, w_o } + SpikingAttention::new(d_model, num_heads, beta, seed) (Xavier-normal init) + spiking_attention_forward(x, sa, mask) -> (out, SpikingAttnCache) + spiking_attention_backward(cache, d_output, sa) -> (d_x, MHAGrad) (reuses MHAGrad since structure is identical). 8 tests (constructor shapes / output shape + weights bounded in (-1/β, 1/β) / weights = fast_sigmoid_forward(scores, β) / mask blocks positions / zero d_output → zero d_x + zero grads / gradient check vs central difference on w_o[0] / gradient check on w_q[0] / zero input + large beta → zero weights).
    Spiking Transformer block (Pre-Norm)✅ donespiking_transformer_block.mbt — Pre-Norm sub-layer stack using SpikingAttention instead of MultiHeadAttention: x2 = x + SpikingAttn(LN1(x)); out = x2 + FFN(LN2(x2)). SpikingTransformerBlock { d_model, num_heads, d_ff, beta, ln_1, ln_2, sa, ffn_w1, ffn_w2 } + SpikingTransformerBlock::new(d_model, num_heads, beta, seed, d_ff? = 4*d_model). spiking_transformer_block_forward / _backward mirrors the standard TransformerBlock but routes through spiking_attention_backward instead of multi_head_attention_backward. 6 tests (constructor + default d_ff / forward shape preservation + cache sanity / same-seed determinism / zero d_output → zero d_x + zero all 8 grad arrays / gradient check on ffn_w2[0] / gradient check on sa.w_q[0]).
    AdEx SpikingSynapse (CSR)鉁?donenew, random, random_with_rule; supports :ge/:he/:gi/:hi/:gaba routing
    ReceptorSynapse (4-receptor routing)鉁?doneconnection_receptor.mbt 鈥?port of ReceptorSynapse.jl (subset). Per-edge target_receptor : Array[Int] parallel to matrix.colptr selects which of the 4 receptors (AMPA/NMDA/GABAa/GABAb) each edge targets. forward_receptor_synapse(s) routes spike weights into s.glu[k] (if target in glu_receptors = [0, 1]) or s.gaba[k] (if in gaba_receptors = [2, 3]). set_target_receptor(s, idx, r) re-targets edge idx (5 tests pass)
    SpikingSynapseIZ (CSR)鉁?doneIZ-targeting: :ge/gi routed directly into post.ge/post.gi (no DoubleExp rise) (4 tests pass)
    SpikingSynapseHH (CSR)鉁?doneHH-targeting: same structure as SpikingSynapseIZ
    STDP (Gerstner 1996)鉁?doneSTDPGerstner params + STDPVariables traces + stdp_step mutating weights. Auto-integrated into compose layer via STDPEntry. gerstner_kernel(螖t, ...) for visualising the 螖W curve. stdp_kernel_plot(param) ASCII plot (11 tests pass)
    STDP (MexicanHat)鉁?doneSTDPMexicanHat params + mexican_hat_kernel(x) pure fn + stdp_mexican_hat_step (pre-spike + post-spike passes; trace decay via dt * -x/蟿). stdp_mexican_hat_plot ASCII viz. Auto-integrated into compose layer via STDPEntryMexicanHat + STDPEntryKind::MexicanHat_ (5+1 tests pass)
    STDP (AntiSymmetric)鉁?doneSTDPAntiSymmetric params + STDPAntiSymmetricVariables (tr_x, to_y) + stdp_antisymmetric_step (pre-spike pass uses to_y[i], post-spike pass uses tr_x[j]). stdp_antisymmetric_plot ASCII viz. Auto-integrated into compose layer via STDPEntryAntiSymmetric + STDPEntryKind::AntiSymmetric_ (5+1 tests pass)
    STDPEntryKind enum鉁?donepub(all) enum STDPEntryKind { Gerstner_(STDPEntry), MexicanHat_(STDPEntryMexicanHat), AntiSymmetric_(STDPEntryAntiSymmetric) }. Replaces the previously-hardcoded STDPEntry in HeterogeneousModel.stdp_entries (1 smoke test)
    STP (Markram 1998)鉁?doneMarkramSTPParameter (蟿D/蟿F/U/Wmax/Wmin) + MarkramSTPVariables (u/x/rho_pre/last_spike/active per pre) + markram_stp_step (event-based update_traces!: u=x=1 recovery via exp, then u+=U*(1-u)/x-=u*x bump) + init_rho on SpikingSynapse + 蟻-broadcast to outgoing edges. Auto-integrated into compose layer via STPEntryKind::MarkramSTP_ (12 tests pass)
    STPEntryKind enum鉁?donepub(all) enum STPEntryKind { MarkramSTP_(MarkramSTPEntry) | MarkramSTPHet_(MarkramSTPEntryHet) }. Run before forward in step_heterogeneous so 蟻 is fresh when spikes propagate. The _Het variant uses per-pre-neuron Array[Float] 蟿D/蟿F/U (matches Julia's MarkramSTPParameterHet)
    Float32 logf FFI鉁?doneextern "C" fn logf(x : Float) -> Float = "logf" added to math_native.mbt (1 test pass)
    compose() + sim鉁?doneHeterogeneousModel with pops+conns+stims+monitors+stdp+stp; step_heterogeneous (7-phase: stimulate 鈫?deliver_pending 鈫?STP 鈫?forward 鈫?STDP 鈫?integrate 鈫?record 鈫?update_time); heterogeneous_sim_for, get_time_heterogeneous, reset_time_heterogeneous (7 tests pass)
    sim! loop鉁?donesim_for, Monitor, record_one (single-pop case)
    AdEx sim! loop鉁?doneadex_sim_for, MonitorAdEx
    chain.jl鈿?partialRuns; final voltages + Monitor summary printed
    AdEx_neuron.jl鈿?partialRuns; 65 pA 鈫?tonic spiking; v range tracked
    IF_neuron.jl鈿?partial445 spikes in 1 s of 10 s
    izhikevich.jl鈿?partialRS neuron, 10 pA, 2 s 鈫?45 spikes
    hh_neuron.jl鈿?partialDefault HH, 10 pA, 1 s; current too small to fire
    morris_lecar.jl鈿?partialDefault ML, 100 pA, 1 s 鈫?1 spike; converges to v=1.92, w=0.53
    poisson_pop.jl鈿?partial1000 neurons @ 5 Hz 脳 100 s; 499,189 fires vs 500,000 expected (within 0.2%)
    if_net.jl鈿?partial32+8 IF, 337 random connections, 4 exc spikes in 100ms
    poisson_if.jl鈿?partial32+8 IF + Poisson inputs; E[0] fires at 345 Hz, I[0] silent
    iz_net.jl鈿?partial16 RS + 4 FS IZ + Gaussian noise; E fires 14 spikes over 1s
    if_noise.jl鈿?partialSingle IF + CurrentStimulus (400 pA + 蟽=100 noise); 48.5 Hz firing rate
    ei_inhibition.jl鈿?partialE-only: 13 spikes; E/I with feedback: 0 spikes (inhibition reduces E rate)
    adex_net.jl鈿?partial8 AdEx + 32 EE connections + 1000 pA tonic drive; v range [-70.6, 20] mV
    out_degree.jl鈿?partialCompares FixedIn/Bernoulli/FixedOut: FixedOut has std=0 (perfect uniformity)
    rate_net.jl鈿?partial100 WilsonCowan + all-to-all RateSynapse; rates evolve smoothly in (-1, 1)
    potjans.jl鈿?partialSimplified 2-layer Potjans-Diesmann microcircuit; E fires at 245 Hz, I at 255 Hz
    hh_current.jl鈿?partialSingle HH + CurrentStimulusArray; v[0] settles at -63 mV (current too low to fire at 10 碌A/cm虏)
    iz_net.jl鈿?partial16 E + 4 I IZ neurons with EE/EI/IE/II SpikingSynapseIZ; E fires 4, I fires 13 spikes in 1s
    hh_net.jl鈿?partial8 E + 4 I HH neurons with EE/EI/IE/II SpikingSynapseHH; E fires 0, I fires 1 spikes in 200ms
    tsodyks.jl鈿?partialScaled-down Tsodyks1997 (8 AdEx + 4 IF); E fires 143 spikes in 1s with Poisson-like drive
    adex_threshold.jl鈿?partialAdEx with Vr=-50mV, At=10mV, 蟿A=10ms; fires 2 spikes in 200ms
    adex_balanced.jl鈿?partialAdEx balanced (exc + inh Poisson); fires 1 spike in 1s with near-balanced input
    stdp_demo.jl鈿?partial4-IF identity EE; pre-then-post pairing 鈫?total 螖W 鈮?+2.5e-4 across 4 synapses
    cuba_net.jl鈿?partialCUBA.jl scaled 100脳 down (80E+20I); IFParameter custom (R=100M惟); 150pA drive for 5s 鈫?259 E + 402 I spikes; drive off 鈫?0 spikes both
    coba_net.jl鈿?partialCOBA.jl scaled 100脳 down (80E+20I); same IF params; 150pA for 1s 鈫?57 E + 41 I spikes; drive off for 5s 鈫?0 spikes both. delay_dist=Normal(0.8ms, 0) now implemented via random_with_delays + pending-event queue (v0.10.3+)
    stdp_compose_demo.mbt鈿?partial4-IF identity EE via compose layer's auto STDP (Gerstner 1996). 4 pairings 脳 5ms 鈫?+2.5e-4 螖W across 4 synapses (matches manual stdp_demo result)
    festa2024.mbt鈿?partialFesta2024 StructuredInhibition network scaled 10脳 down (80E+20I1); Gerstner STDP on I1鈫扙 (auto-integrated); 1s sim: E=308 Hz, I1=286 Hz, I1鈫扙 weights grew 885.6鈫?030.8
    timed_stim.mbt鈿?partialtimed_stim.jl port: 3 IF neurons + SpikeTimeStimulus at t=100/200/300 ms (渭=100 nS). E[0] fires 4 times from its spike; E[1,2] receive but don't fire (insufficient drive with default IF)
    potjans_diesmann.mbt鈿?partialPotjans-Diesmann cortical microcircuit simplified to 4 layers + scaled 100脳 down (120 E + 40 I); Poisson drive 500Hz + 350 pA tonic on E. 1s sim: E[0]=302 Hz, I[0]=165 Hz
    lkd2014.mbt鈿?partialLitwinKumar2014 vSTDP network scaled 100脳 down (40 E + 10 I); 4 SpikingSynapses (渭=2.76/1.27/48.7/16.2); Poisson 4.5/2.5 Hz + tonic 350/250 pA. 1s sim: E[0]=217 Hz, I[0]=194 Hz
    stdp_kernel.mbt鈿?partialSTDP_kernel.jl port: plots Gerstner (asymmetric, classical 1996) and symmetric Gerstner kernel curves using stdp_kernel_plot. STDPMexicanHat kernel now implemented (see calcium_kernel); STDPAntiSymmetric also added and both are now auto-integrated via STDPEntryKind compose dispatch
    afferent_response.mbt鈿?partialafferent_response.jl port (simplified, single 谓_a = 20Hz); 40 E + 10 I + Poisson drive. E[0]=183 Hz, I[0]=185 Hz
    lagzi2022.mbt鈿?partialLagzi2022 Assembly Formation simplified to 2 IF populations (16+16) + 4 SpikingSynapses + Gerstner STDP on W11+W22 (auto-integrated). 1s sim: E1[0]=419 Hz, E2[0]=419 Hz; weights modified by STDP
    izhikevich_debug.mbt鈿?partialDebugging variant of izhikevich.mbt. Records v[0] for 100 ms with fixed seed; prints min/max/mean. Used for bit-exactness verification against Julia's Izikievich_neuron.jl
    calcium_kernel.mbt鈿?partialCalciumPlasticity_kernel.jl port (partial): plots reversed-polarity + classical Gerstner kernels + STDPMexicanHat (sombrero) kernel + decorrelated weights. iSTDPTime / SymmetricSTDP still TODO
    oja_rule.mbt鈿?partialOja_rule.jl port: 100 Wilson-Cowan rate neurons + all-to-all RateSynapse (渭=1.2, p=1.0). 100ms sim: r[mean]=-0.019, r[max]=0.87
    stp_demo.mbt鈿?partialMarkram STP dynamics on a single forced-fire pre IF neuron: shows isolated spike (u=0.36, x=0.64, 蟻=0.2), paired-pulse 10ms (蟻=0.236, facilitation > depression at short ISI), 5-spike 50ms burst (x drops from 0.235鈫?.059, depression catches up), and full recovery (蟻 鈫?0.2 after 10s silence). Also includes a 200-spike train @ 50ms ISI: 蟻 converges to steady-state 鈮?0.21, u鈫?.907 (saturated facilitation, 蟿F=1500ms 鈮?ISI), x鈫?.022 (deep depression, 蟿D=200ms < ISI) 鈥?validates Markram 1998 analytical steady-state
    stp_onecell.mbt鈿?partialSimplified port of Mongillo2008 STP_onecell.jl. 6 bursts 脳 240 spikes at 8kHz drive u鈫?.9997 (fully facilitated), x鈫?.7e-7 (fully depleted), 蟻鈫?e-4. Documents that recovery requires integrate! to apply exp(-dt/蟿D); see stp_demo for the recovery phase
    lkd2014_adex.mbt鈿?partialLitwin-Kumar-Doiron 2014 vSTDP network using AdExSinExpParameter (LKD defaults: El=-70mV, Vt=-52mV, Vr=-60mV, 蟿m=20ms, R=1/15nS, At=10mV, 蟿e=6ms, 蟿i=2ms). Scaled 100脳 down (40 E + 10 I); manual pre鈫抪ost routing (SpikingSynapse is hardcoded to IF-to-IF); 1s sim: E[0]=6 Hz, I[0]=9 Hz
    simulation_speed.mbt鈿?partialPort of SpikingNeuralNetworks.jl/examples/simulation_speed.jl (AdEx + PoissonLayer benchmark, first part). Scaled 10脳 down (10 AdEx + 100 Poisson per pop); 1s sim; prints Poisson spike counts, AdEx spike counts, v checksum. The TripodHet / BallAndStick parts of the Julia example require multi-compartment neurons (TODO)
    AdEx network (full)鉁?doneAdExParameterHet (per-ne Vector{Float] vt/vr/el/tm/r/dt_slope/tw/a/b) + AdExHet population + step_adex_het (4 tests pass). Matches Julia's AdExParameter{Vector{Float32}} + update_neuron! for the Vector variant
    AdExSinExp (single-exp synapse)鉁?doneAdExSinExpParameter (pub(all) struct, same fields as AdExParameter) + AdExSinExp population + step_adex_sinexp (same as step_adex) + adex_sinexp_step_synapses (single-exp: ge += glu; ge += dt*(-ge/蟿e); gi + gaba similar) + adex_sinexp_synaptic_current (same as AdEx). Auto-integrated via AdExSinExp_ in AnyPop. Matches Julia's AdExSinExpParameter + SingleExpSynapse. 6 tests + 1 new example (lkd2014_adex)
    Network experiments (Festa, Lagzi)鈴?TODOv0.7.5+
    Tripod / BallAndStick neurons鈴?TODOv0.7.5+
    Dendrite (passive compartment)鉁?doneDendrite struct (per-ne El/C/gax/gm/l/d/gax_parent) + Dendrite::new (Julia defaults: El=-70.6mV, C=10pF, gax=10nS, gm=1nS, l=150渭m, d=4渭m) + Dendrite::custom(...) + dendrite_step (passive forward-Euler: (El-v)*gm + (v_parent-v)*gax + i_ext) / C) + g_axial/g_mem/c_mem helpers (Julia G_axial/G_mem/C_mem formulas). Foundational building block for BallAndStick (1 dendrite) and Tripod (2 dendrites) multi-compartment neurons (7 tests pass)
    BallAndStick (soma + 1 dendrite)鉁?doneBallAndStick struct (soma AdEx + passive dendrite via Dendrite::new + single-exp synapses on both + Heun predictor-corrector integration) + step_ballandstick (bit-exact port of Julia BallAndStick integrate!: update_synapses 鈫?synaptic_current 鈫?2脳 update_neuron! 鈫?Heun averaging 鈫?spike detection on soma with ap_membrane=10mV per Julia's PostSpike). 6 tests pass
    Tripod (soma + 2 dendrites)鉁?doneTripod struct (soma AdEx + 2 passive dendrites via Dendrite::new + single-exp synapses on soma+d1+d2 + Heun predictor-corrector for 4 螖v components per neuron: dv_s, dv_d1, dv_d2, dw_s) + step_tripod (bit-exact port of Julia Tripod integrate!: soma axial current = ic1+ic2 = -(v_d - v_s) * gax for both dendrites). 6 tests + 1 new example (tripod)
    Metaplasticity (homeostatic weight normalization)鉁?doneMultiplicativeNorm (渭[i] = (W0[i]-W1[i])/W1[i]; W *= (1+渭)) + AdditiveNorm (渭[i] = W0[i]-W1[i]; W += 渭) + NormParam enum + SynapseTarget struct + SynapseNormalization::new(targets, param) (captures W0[i] at construction) + metaplasticity_step(norm). 6 tests + 1 new example (metaplasticity)
    examples/ballandstick + examples/dendrite鉁?donev0.10.16: first two standalone single-neuron examples for the multi-compartment infrastructure. examples/ballandstick runs 1 BallAndStick neuron (AdEx soma + 1 passive dendrite) with 1500 pA tonic drive for 1 s; final v_s/v_d + spike count + spike envelope. examples/dendrite runs 1 passive Dendrite with parent held at -50 mV and 50 pA current injection for 200 ms then 0 pA (passive relaxation); final v_d + peak v_d. Both also exercise the v0.10.16 Heun fix: predictor/corrector extrapolate v + dv*dt (not v + dv), spike detection runs before Heun apply with predictive criterion v_s + corrector_dv*dt >= -10mV (Julia's exact form), fire && continue skips the v_d apply (prevents corrector exp_term explosion from polluting v_d), and tabs_steps = round(Int, (up + 蟿abs) / dt) (Julia's full refractory window; previously half 鈥?only 蟿abs was used). The same fix is back-ported into Tripod
    Heun fix (BallAndStick + Tripod)鉁?donev0.10.16: re-aligned step_ballandstick and step_tripod apply loops with Julia's exact per-neuron order (decrement tabs 鈫?update threshold 鈫?refractory branch with v_d += dt*(v_s-v_d)*gax/C 鈫?active branch with predictive fire detection 鈫?fire overrides continue). Fixes v_d runaway when soma spikes (predictor dv_s explodes exp_term, corrector dv_d inherits the bogus extrapolation, v_d was being updated before spike detection)
    HetRec (heterogeneous-timescale non-recurrent layer)鉁?donev0.10.17: HetRecParameter (Nd/overlap/蟿d-low/蟿d-high/rate-low/rate-high/蟿abs/steepness/蟿m/蟿rate) + HetRec struct (v_d/v_s/is_/r/tau_d/fire/tabs/trace/randcache + sparse CSC colptr/i_syn/w_syn) + HetRec::new (samples r and 蟿d from Uniform, builds sparse M with own-dendrite-always + cross-dendrite-per-overlap) + hetrec_refresh_random + step_hetrec (Euler: v_d += dt*(-v_d-is)/蟿d; soma: v_s += (W路v_d - v_s)dt/蟿m per synapse; stochastic fire: rand < rsigmoid(steepness路(v_s-trace))路dt; 蟿abs refractory). Auto-integrated via HetRec_ in AnyPop. 8 tests + 1 new example (hetrec)
    examples/wilson_cowan鉁?donev0.10.18: standalone single-population Wilson-Cowan rate-model example. 20 WilsonCowan + all-to-all self-RateSynapse (渭=0.5, p=1.0) + I=0.2 tonic drive; 100 ms sim; samples r at t=0/12.5/50/99.9 ms to show transient 鈫?fixed-point convergence (meanr鈮?.21, r[0]=-0.06鈫?.34). Exercises the rate-mode forward (pre.r 鈫?post.g via sparse matrix)
    examples/spikesynapse鉁?donev0.10.19: minimal 2-IF + 1-SpikingSynapse network (E1 鈫?E2 onto :ge, 渭=1.62 nS, p=1.0, delay=Normal(3ms, 2ms)). E1 driven by 200 pA tonic current; E2 receives the synaptic input via the pending-event queue + delay (built v0.10.3). Result: E1 fires 381 spikes, E2 fires 379 spikes (1:1 with small loss to delay + refractory). Demonstrates the manual sim loop pattern (deliver_pending_synapse 鈫?forward_synapse 鈫?step_synapses 鈫?synaptic_current 鈫?step_neuron)
    STTC (Spike-Time Tiling Coefficient) analysis鉁?donev0.10.20: analysis_sttc.mbt (new) 鈥?tile_fraction (sorted train, 卤dt intervals union, divided by T+2dt) + coincident_fraction (binary-search in B for each spike in A) + sttc_pair (symmetric formula 0.5*((PA-TB)/(1-PA*TB) + (PB-TA)/(1-PB*TA))) + sttc_matrix (N脳N symmetric, diagonal=1) + sort_floats helper. 13 tests + 1 new example (sttc)
    ISI / CV2鉁?doneanalysis_isi.mbt 鈥?port of spikes.jl::ISI_CV2 (Holt et al. 1996). isi_cv2_one(spike_times) single-neuron CV2 (handles 0-spike / 1-spike / 2-spike / NaN edge cases); isi_cv2(spike_times_per_neuron) per-neuron CV2 array. CV2 bounded by [0, 2] (9 tests pass)
    LIF closed-form verification鉁?doneanalysis_lif_closedform_test.mbt 鈥?closed-form LIF analytical verification. For an IF neuron with constant input I, the membrane voltage follows V(t) = V_rest + I*R*(1 - exp(-t/蟿m)). Verifies that the MoonBit simulator matches the analytical solution to within forward-Euler drift tolerance (4 tests pass: decay transient, steady-state, decay-to-rest, 蟿m time-constant). This serves as a "reference-output" check analogous to a Julia trajectory comparison (4 tests pass)
    PoissonLayer (population of N Poisson sources)鉁?donev0.10.21: PoissonLayer struct (pub(all); rate / n_sources / active / 渭 / 蟽 / p / dist / rule) + 4 constructors (new, with_n, with_active, with_conn) + PoissonLayerStimulus (wraps IF target + per-(pre, post) sparse weights drawn at construction via Box-Muller for Normal or Fixed for :Fixed) + stimulate_layer(s, t, dt) (per-step: reset fire buffer, sample Poisson per active source, apply weight to glu/gaba if fires). Auto-integrated via PoissonLayer_ in AnyStim. Mirrors Julia's PoissonLayer + Stimulus + stimulate! pattern (11 tests + 1 new example)
    examples/poisson_layer鉁?donev0.10.21: minimal port of poisson_layer.jl. 10 Poisson sources @ 2 Hz 鈫?200 IF (El=-49mV, vr=-60mV, vt=-50mV); Normal(渭=2.0, 蟽=1.0) weights, p=0.2 connectivity. Manual sim loop (stimulate_layer 鈫?step_synapses 鈫?synaptic_current 鈫?step_neuron) for 1 s at dt=0.125 ms. 410 actual connections (expected ~400); total 5322 spikes 鈫?26.6 Hz mean rate per neuron (high because El is only 1 mV below vt)
    examples/spike_analysis鉁?donev0.10.22: exercises vecplot analysis infrastructure (Monitor::count_spikes / firing_rate / spike_times / count_spikes_interval / firing_rate_interval / dump_summary / ascii_plot) on a small driven IF population (20 neurons, El=-49mV, 500ms sim). Per-neuron spike counts (135鈥?73 spikes), rates (270鈥?46 Hz), spike times, sub-interval analysis, v summary, ASCII plot
    Compartment-targeted SpikingSynapse鉁?donev0.10.23: CompartmentSynapseBall (targets :soma or :d of BallAndStick) + CompartmentSynapseTripod (targets :soma, :d1, or :d2 of Tripod) 鈥?same CSR matrix + delays + pending queue + rho-scaling semantics as SpikingSynapse, but routes incoming weights to a single compartment buffer. forward_compartment_ball / forward_compartment_tripod / deliver_pending_compartment_ball / deliver_pending_compartment_tripod (11 tests + 1 new example). First piece of the multi-compartment synapse infrastructure that unblocks tripod_network.jl / tripod_current.jl / stimuli.jl. Note: multi-receptor (AMPA + NMDA / GABA_A + GABA_B) is still TODO
    TripodHet (heterogeneous-parameter Tripod)鉁?donev0.10.24: TripodHet struct (soma AdExParameterHet + 2 shared dendrites + Heun predictor-corrector that reads per-neuron vt/vr/el/tm/r/dt_slope/tw/a inside the per-neuron loop) + TripodHet::new(n, soma_param, rng) + step_tripod_het (same Julia integrate! order as step_tripod but with per-ne arrays). 8 tests + 1 new example (tripod_het). Mirrors Julia's Tripod(..., param=AdExParameter{Vector{Float32}}). Final unblocker before tripod_network.jl / multipod.jl / tripod_current.jl (still need NMDA multi-receptor)
    NMDA primitives (multi-receptor + voltage-dependent gating)鉁?donev0.10.25: receptor.mbt (new) 鈥?NMDAVoltageDependency (Eyal/Soma defaults) + nmda_gating(v, dep) (B(v) = 1/(1 + (mg/b)exp(kv))) + Receptor (e_rev / tau_r / tau_d / g0 / gsyn / alpha / inv / is_nmda / target) + Receptors (4-element collection AMPA/NMDA/GABAa/GABAb) + step_receptor(g, h, target, r, dt) (2-state ODE: h += target伪; g = exp(-dt/蟿d鈦?(g + dth); h = exp(-dt/蟿r鈦?h; consumes target) + receptor_current(g, v, r, nmda, out) (sums I = gsyng(v-e_rev)*B(v) for NMDA, no B for others). 10 tests + 1 new example (nmda). Foundation for tripod_network.jl / tripod_current.jl / stimuli.jl
    ReceptorSynapse for TripodHet (multi-compartment + multi-receptor wiring)鉁?donev0.10.26: connection_receptor_tripod.mbt (new) 鈥?ReceptorSynapseTripod struct (pre IF, post TripodHet, CSR matrix, target_compartment 鈭?{soma, d1, d2}, Receptors collection, NMDAVoltageDependency, g_state[N脳4], h_state[N脳4], ge_out[N], gi_out[N]) + forward_receptor_tripod_synapse(s, target_receptor, t_now) (AMPA/NMDA 鈫?glu; GABAa/GABAb 鈫?gaba; routes to the right compartment buffer) + step_receptors_tripod_synapse(s, dt) (runs 2-state ODE for each of the 4 receptors on the target compartment, applies NMDA gating when is_nmda; sums into ge_out/gi_out). 6 tests + 1 new example (tripod_receptor). Wires the NMDA primitives into TripodHet, enabling tripod_network.jl / tripod_current.jl / stimuli.jl ports
    PoissonLayerStimulusTripod (Poisson Stimulus on TripodHet compartments)鉁?donev0.10.27: stimulus_poisson_layer_tripod.mbt (new) 鈥?PoissonLayerStimulusTripod struct (param : PoissonLayer, post : TripodHet, weights[N_post 脳 n_sources], connectivity, target_compartment 鈭?{soma, d1, d2}, target_kind 鈭?{glu, gaba}, rng) + stimulate_layer_tripod(s, t, dt) (per-step: per active Poisson source, sample Poisson(rate*dt); if fires, add weights[j, i] to the target compartment buffer). 7 tests + 1 new example (tripod_current 鈥?scaled-down port of tripod_current.jl: 4 TripodHet + 20 Poisson exc @ 20 Hz + 20 Poisson inh @ 3 Hz, both on :d1, 500 ms sim). Final piece of the tripod_current.jl infrastructure stack (alongside ReceptorSynapseTripod)
    change_plasticity! (runtime STDP parameter swap)鉁?donev0.10.28: Made param field mutable on STDPEntry / STDPEntryMexicanHat / STDPEntryAntiSymmetric + STDPEntry::change_plasticity(e, new_param) / STDPEntryMexicanHat::change_plasticity(e, new_param) / STDPEntryAntiSymmetric::change_plasticity(e, new_param) 鈥?runtime swap of LTP parameters (preserves vars). 5 tests + 1 new example (change_plasticity). Mirrors Julia's change_plasticity!(syn; LTP = STDPConfavreux2025()) from with_plasticity.jl. Note: still need to port STDPConfavreux2025 + iSTDPRate + iSTDPPotential + vSTDPParameter + MarkramSTPParameterTimestep (rule bodies) for the full with_plasticity.jl test
    PoissonLayerStimulusBallAndStick (Poisson Stimulus on BallAndStick :soma/:d)鉁?donev0.10.29: stimulus_poisson_layer_ballandstick.mbt (new) 鈥?PoissonLayerStimulusBallAndStick struct (param : PoissonLayer, post : BallAndStick, weights[N_post 脳 n_sources], connectivity, target_compartment 鈭?{soma, d}, target_kind 鈭?{glu, gaba}, rng) + stimulate_layer_ball(s, t, dt). 6 tests + 1 new example (stimuli 鈥?simplified port of stimuli.jl: 1 BallAndStick + 1 TripodHet + 4 Poisson Stimuli on :d / :d1). Final piece of the stimuli.jl infrastructure stack (alongside PoissonLayerStimulusTripod)
    vSTDPParameter (Litwin-Kumar-Doiron 2014 voltage-dependent STDP)鉁?donev0.10.30: stdp_vstdp.mbt (new) 鈥?VstdpParameter struct (a_ltd / a_ltp / theta_ltd / theta_ltp / tau_pre / tau_post / w_max / w_min) + VstdpVariables::new(n_pre, n_post) + vstdp_step(vars, param, pre_v, post_v, pre_fire, post_fire) (LTD on pre-fires when v_post > 胃_LTD; LTP on post-fires when v_pre > 胃_LTP; weight clamped to [w_min, w_max]) + vstdp_plot(param) ASCII visualization of the (v_pre, v_post) rule. 9 tests + 1 new example (vstdp). Mirrors Julia's vSTDPParameter from plasticity_params.jl. Note: struct renamed from vSTDPParameter to VstdpParameter because MoonBit requires type names to start with uppercase
    BalancedStimulus (feedback-driven inhomogeneous Poisson)鉁?donev0.10.31: stimulus_balanced.mbt (new) 鈥?BalancedParameter struct (kIE / beta / tau / r0 / w / wIE / same_input) + BalancedStimulus::new(pop, sym_e?, sym_i?, param?, seed?) (hooks into pop.glu / pop.gaba) + stimulate_balanced(s, t, dt) (inhomogeneous Poisson on :gi at rate r0*kIE; per-neuron rate adaptation r[i] += (r0 - Erate) / 400ms * dt driven by low-pass-filtered noise noise[i] = (noise[i] - re) * (1 - dt/蟿) + re; Poisson at rate Erate writes to :ge). Same_input=true broadcasts a single trace across all neurons. 9 tests + 1 new example (balanced 鈥?port of balanced.jl: 200 IF @ El=-49mV + BalancedParameter(kIE=2, 尾=0.1, 蟿=100ms, r0=2kHz, wIE=2, same_input=true); 1s sim, total 81295 spikes, mean 406 Hz/neuron). Wired into compose.mbt via new BalancedIF_ enum variant. Mirrors Julia's BalancedStimulus(E, :ge, :gi; param=BalancedParameter()) from stim/balanced.jl
    STDPConfavreux2025 (Confavreux 2025 STDP with 伪/尾 baseline dependencies)鉁?donev0.10.32: stdp.mbt (extended) 鈥?STDPConfavreux2025 struct (eta / alpha / beta / kappa / gamma / tau_pre / tau_post / w_max / w_min) + STDPEntryConfavreux2025 (mutable param, STDPVariables, t_now) + change_plasticity(entry, new_param) runtime swap + stdp_confavreux_step(w, pre_fire, post_fire, colptr, rowptr, vars, param, t_now, dt) (continuous tpre/tpost decay + spike bump, then single fused connection loop: pre-fire contributes eta * (kappa * 螖post[i] + alpha), post-fire contributes eta * (gamma * 螖pre[j] + beta); clamp to [w_min, w_max]). 9 tests + 1 new example (stdp_confavreux 鈥?port of with_plasticity.jl "STDPConfavreux2025" testset: 4脳4 dense synapse, 100 steps random pre/post firing; verifies weights stay finite and within bounds). Wired into compose.mbt via new Confavreux2025_(...) variant in STDPEntryKind. Mirrors Julia's STDPConfavreux2025 from STDP_traces.jl
    IstdpRate (Vogels 2011 inhibitory STDP with rate homeostasis)鉁?donev0.10.33: istdp.mbt (new) 鈥?IstdpRate struct (eta / r / tau_y / w_max / w_min) + IstdpRateVariables (just tpre / tpost arrays, no last_* bookkeeping) + IstdpRateEntry (mutable param, vars, t_now) + change_plasticity(entry, new_param) runtime swap + istdp_rate_step(w, pre_fire, post_fire, colptr, rowptr, vars, param, t_now, dt) (continuous tpre/tpost decay t += dt*(-t)/tau_y + spike bump, then fused connection loop: pre-fire contributes eta * (tpost[i] - 2*r*tau_y), post-fire contributes eta * tpre[j]; clamp to [w_min, w_max]). 10 tests + 1 new example (istdp_rate 鈥?port of with_plasticity.jl "iSTDPRate" testset: 4脳4 dense synapse, 100 steps random pre/post firing; final weights stay finite within [0.01, 243]). Wired into compose.mbt via new IstdpRate_(...) variant in STDPEntryKind. Mirrors Julia's iSTDPRate from iSTDP.jl. Note: struct renamed from iSTDPRate to IstdpRate because MoonBit requires type names to start with uppercase
    IstdpPotential (Vogels 2011 inhibitory STDP with potential-based post trace)鉁?donev0.10.34: istdp.mbt (extended) 鈥?IstdpPotential struct (eta / v0 / tau_y / w_max / w_min) + IstdpPotentialVariables (tpre / tpost arrays) + IstdpPotentialEntry (mutable param, vars, t_now) + change_plasticity(entry, new_param) runtime swap + istdp_potential_step(w, pre_fire, post_fire, colptr, rowptr, v_post, vars, param, t_now, dt) 鈥?tpre[j] decays toward 0; tpost[i] is a low-pass filter of v_post[i] (decays toward v_post with time constant tau_y, plus +1 bump on post-spike). Weight update: pre-fire contributes eta * (tpost[i] - v0); post-fire contributes eta * tpre[j]; clamp to [w_min, w_max]. 10 tests + 1 new example (istdp_potential 鈥?port of with_plasticity.jl "iSTDPPotential" testset: 4脳4 dense synapse, 100 steps random pre/post firing + varying v_post in [-70, 50] mV; final weights stay finite within [0.01, 243]). Wired into compose.mbt via new IstdpPotential_(...) variant in STDPEntryKind which threads syn.post.v into the step function. Mirrors Julia's iSTDPPotential from iSTDP.jl. Note: struct renamed from iSTDPPotential to IstdpPotential because MoonBit requires type names to start with uppercase
    MarkramSTPParameterTimestep (continuous-time Euler Markram STP)鉁?donev0.10.35: stp.mbt (extended) 鈥?MarkramSTPParameterTimestep struct (u / tau_f / tau_d / w_max / w_min) + MarkramSTPEntryTimestep (uses same MarkramSTPVariables state) + markram_stp_step_timestep(syn, vars, param, t_now, dt) (per step: spike bumps u += U*(1-u), x += -u*x; then continuous Euler u += dt*(U-u)/tau_f, x += dt*(1-x)/tau_d for every pre-neuron; recompute rho_pre[j] = u[j] * x[j]; broadcast rho_pre to all outgoing connections via syn.matrix.rowptr[j]). 8 tests + 1 new example (stp_timestep 鈥?port of with_plasticity.jl "MarkramSTPParameterTimestep" testset: 4脳4 dense synapse, 200 steps random pre firing; verifies weights stay finite and rho / u stay in [0, 1]). Wired into compose.mbt via new MarkramSTPTimestep_(...) variant in STPEntryKind. Mirrors Julia's MarkramSTPParameterTimestep from STP.jl
    STDPSymmetric (Festa et al. 2024 zero-integral inhibitory STDP) + IstdpTime (parameter type only)鉁?donev0.10.36: stdp.mbt (extended) 鈥?STDPSymmetric struct (a_x / a_y / tau_x / tau_y / alpha_pre / alpha_post / w_max / w_min) + STDPSymmetricVariables (4 trace arrays: tr_x, tr_y, to_x, to_y) + STDPEntrySymmetric (mutable param, vars, t_now) + change_plasticity(entry, new_param) runtime swap + stdp_symmetric_step(w, pre_fire, post_fire, colptr, rowptr, vars, param, t_now, dt) 鈥?continuous decay of all 4 traces (tr_x, tr_y bump on pre-spike, to_x, to_y bump on post-spike); fused connection loop: pre-fire contributes alpha_pre + (a_x/(2*tau_x))*to_x[i] - (a_y/(2*tau_y))*to_y[i], post-fire contributes alpha_post + (a_x/(2*tau_x))*tr_x[j] - (a_y/(2*tau_y))*tr_y[j]; clamp to [w_min, w_max]. istdp.mbt (extended) 鈥?IstdpTime struct (eta / tau_y / w_max / w_min) 鈥?parameter type only, no step function (Julia's iSTDP.jl defines iSTDPTime but doesn't provide a separate step rule for it). 11 tests + 1 new example (stdp_symmetric 鈥?port of plasticity_params.jl "STDPSymmetric / STDPAntiSymmetric" testset: 4脳4 dense synapse, 100 steps random pre/post firing; verifies weights stay finite within [0, 30]). Wired into compose.mbt via new Symmetric_(...) variant in STDPEntryKind. Mirrors Julia's STDPSymmetric from STDP_structured.jl and iSTDPTime from iSTDP.jl
    TripodNetwork (multi-population E-I Tripod network wiring)鉁?done (partial)v0.10.37: examples/tripod_network/main.mbt (new) 鈥?port of test/network/tripod_network.jl. 20 Tripod (E, homogeneous AdEx soma) + 5 IF (I1, 蟿m=7ms) + 5 IF (I2, 蟿m=20ms) + CompartmentSynapseTripod for I1鈫扙 :soma with IstdpRate + I2鈫扙 :d1 with IstdpPotential. 500ms manual sim loop with forward 鈫?step_neuron 鈫?step_tripod 鈫?STDP!. Final: E soma fires at 1287 Hz/neuron, I1 at 422 Hz, I2 at 348 Hz. Partial port 鈥?E鈫扞1, E鈫扞2 routed via tonic current approximation (Tripod can't be pre in CompartmentSynapseTripod); E鈫扙 recurrent SKIPPED (same constraint); homeostatic SynapseNormalization + MultiplicativeNorm not ported yet. Demonstrates that the existing Tripod / CompartmentSynapseTripod / IstdpRate / IstdpPotential infrastructure composes correctly into a multi-population network
    Population + synapse parameter type sanity tests鉁?donev0.10.38: parameters_test.mbt (new) 鈥?port of test/pop/parameters.jl "Population parameters" + "Synaptic parameter types" testsets. 10 tests verifying field existence + Julia-matching defaults for IFParameter, AdExParameter, IZParameter, MorrisLecarParameter, HetRecParameter, PostSpike, AdExPostSpike, plus IF's tre/tde/tri/tdi time constants (DoubleExpSynapse-style) and Receptor/NMDAVoltageDependency constructors. Field names mapped from Julia camelCase to MoonBit lowercase (Vt鈫抈vt, 蟿m鈫抈tm, gCa鈫抈gca, Nd鈫抈nd, etc.). Demonstrates that all major parameter types have the expected property surface
    Synapse parameter types + vars + step functions鉁?donev0.10.39: synapse_params.mbt (new) 鈥?DeltaSynapse (empty), SingleExpSynapse (tau_e, tau_i, e_e, e_i, gsyn_e, gsyn_i), DoubleExpSynapse (tau_re/de/ri/di, e_e, e_i, gsyn_e, gsyn_i), CurrentSynapse (tau_e, tau_i) structs with Julia-matching defaults; companion *Vars structs with ge/gi/he/hi state arrays; step functions (delta_synapse_step, single_exp_synapse_step, double_exp_synapse_step, current_synapse_step) implementing the Julia update rules (instantaneous / single-exp decay / 2-state rise+decay / current-based decay); current functions (delta_synapse_current syn_curr=-(ge-gi) then reset; current_synapse_current syn_curr=-(ge-gi); conductance_synapse_current / double_exp_conductance_current syn_curr=ge*(v-E_e)gsyn_e + gi(v-E_i)*gsyn_i). 12 tests + 1 new example (synapse_params 鈥?constructs all four synapse types, runs a single step with synthetic input glu=[1,0,0,0], computes synaptic current at v=-50mV). Mirrors Julia's DeltaSynapse / SingleExpSynapse / DoubleExpSynapse / CurrentSynapse parameter types from synapses/
    SNNUtils.jl parameter constants (LKD2014 + Duarte2019)鉁?donev0.10.40: snn_utils_params.mbt (new) 鈥?LKD2014 struct (tm / vt / el / vr / r / tau_abs / e_i / e_e / at / pv_tm / pv_el / pv_vr / pv_vt) matching Julia's SNNUtils.LKD2014 named-tuple from lkd2014.jl. Duarte2019 struct (pv_, sst_, adex_* parameters) matching Julia's SNNUtils.duarte2019 named-tuple from duarte2019.jl. C/nS ratios computed manually as Float32 constants (300pF/15nS = 20.0ms for LKD2014; 104.52pF/9.75nS 鈮?10.72ms, 102.86pF/4.61nS 鈮?22.31ms, 116.5pF/4.64nS 鈮?25.11ms for Duarte2019). 3 tests + 1 new example (snn_utils_params 鈥?displays all parameters and demonstrates LKD2014 鈫?IFParameter::custom conversion). Mirrors Julia's SNNUtils.jl/src/models/lkd2014.jl + duarte2019.jl
    Simulation control (sim_control.jl port)鉁?donev0.10.41: sim_control_test.mbt (new) 鈥?port of test/sim/sim_control.jl. 6 tests covering get_time_heterogeneous (zero before sim, advances after sim_for), reset_time_heterogeneous (resets to zero), Monitor::new_fire spike accumulation over a 50ms sim, Monitor::new_v_sr sampling rate storage, and compose with multiple pops + conns. Uses the existing HeterogeneousModel infrastructure from compose.mbt
    Spatial utilities (spatial_test.jl port)鉁?donev0.10.42: spatial.mbt (new) 鈥?port of utils/spatial.jl. Point2D struct (x, y), PlacedPops struct (e/i). place_populations_e_i(n_e, n_i, grid_x, grid_y, rng) (random Float32 placement on a periodic grid via next_f32). periodic_distance_scalar(p1, p2, grid_size) (1D torus distance: min of |p1-p2| and grid_size-|p1-p2|). linear_network(n) + linear_network_with(n, sigma_w, w_max) (ring-shaped Gaussian weight matrix on circle of circumference 2pi). MoonBit parser limitation workaround: instead of returning Array[Array[Float]] (which fails to parse in our setup), return a flat row-major Array[Float] of length n*n; index via row_major_get(W, n, i, j). Diagonal entries set to 0. 4 tests + new example spatial (recommended in subsequent turn). Mirrors Julia's utils/spatial.jl (place_populations, periodic_distance, linear_network)
    AggregateScaling (aggregate_scaling.jl port)鉁?donev0.10.43: aggregate_scaling.mbt (new) 鈥?port of connections/metaplasticity/aggregate_scaling.jl. AggregateScalingParameter (蟿e/蟿a/蟿/Y/Wmin/Wmax) matching Julia's struct. AggregateScaling::new(n, targets, param) initialises WT[i] = sum of incoming weights. aggregate_scaling_forward(c, fire, dt) per Julia: y decays with 蟿a, bumps on fire[i] (Euler decay鈫抌ump鈫扺T-update order), drives WT[i] toward (1 - y[i]/Y[i])/Wmax with 蟿e. aggregate_scaling_plasticity(c, step_count) periodic variant: every 蟿/dt steps (default 80), recompute wt[i] = sum of weights, compute 渭[i] = (WT[i] - Wmin) / wt[i] (guarded to 1.0 when wt=0), rescale W[s] = W[s] * 渭[i] + Wmin. AggregateScaling::with_plasticity_interval(c, n) runtime override of the cadence. 9 tests + upgraded examples/aggregate_scaling/main.mbt to use the real infrastructure (was a placeholder). Final demo: 100 IF + 2074 EE connections + 1000 steps with weight perturbation + periodic homeostatic rescaling; y[0]=0.58, WT[0]=267.7, final mean weight 14.98
    LKD2014.PV demo (LKD.jl port)鉁?donev0.10.44: AdExParameter::custom(tm, vt, vr, el, r) constructor added to neuron_adex.mbt. examples/lkd2014_demo/main.mbt (new) 鈥?uses LKD2014.PV parameter values (tm=20ms, El=-62mV, Vt=-52mV, Vr=-57.47mV, R=0.0667) to build a single IF neuron, drives it with a single scheduled spike at t=1000ms via SpikeTimeStimulus (param spiketimes=[1000.0F], neurons=[0]), runs 1200ms @ dt=0.125ms, prints final v[0]/glu[0]/fire[0]. Final state: v=-61.999 (El unchanged since the single spike wasn't strong enough to push v above Vt=-52 with R=0.0667 脳 p路gsyn_e=1.0路2 nS=0.13 nS). AdEx side of LKD.jl omitted (SpikeTimeStimulus currently only accepts IF populations; full two-population network requires TimedStimulusAdEx variant). Mirrors refs/SNNUtils.jl/test/LKD.jl partial port
    metaplasticity.jl port (extended coverage)鉁?donev0.10.45: metaplasticity_test.mbt (extended) 鈥?added 3 tests from refs/SNNModels.jl/test/syn/metaplasticity.jl: SynapseNormalization AdditiveNorm form (W0 sum + no-shrinkage step), W0 sums from full connectivity (4脳4 synapse with 渭=1 gives W0=4 per post), metaplasticity_step multiplicative shrinkage+restore (W0=[4,6] captured, shrink to half 鈫?step restores via 渭=1 scaling), additive shrinkage correction (additive applies 渭[i] to all synapses connecting to post i, NOT a "restore" 鈥?over-corrects when multiple synapses target the same post). RandomTurnover and ActivityDependentTurnover constructors NOT ported (structural plasticity TODO 鈥?separate work). 4 tests + 1 example
    set_LTP! / set_STP! (runtime LTP/STP toggle)鉁?donev0.10.46: port of refs/SNNModels.jl/test/syn/plasticity_params.jl "set_LTP! / set_STP!" testset (lines 179-190). stdp_step now gates on vars.active[0] at the top (mirrors how markram_stp_step already did). Three new helpers: STDPEntry::set_ltp_active(entry, active), MarkramSTPEntry::set_stp_active(entry, active), MarkramSTPEntryHet::set_stp_active(entry, active), MarkramSTPEntryTimestep::set_stp_active(entry, active) 鈥?each mutates entry.vars.active[0] in place. MoonBit identifiers drop Julia's trailing !. Round-trip toggle preserves vars state (traces + spike times survive a false鈫抰rue cycle). MarkramSTPEntryHet::new constructor now used (was: struct-literal which MoonBit rejects without pub(all)). MarkramSTPParameterHet::homogeneous(n_pre) is the public constructor for the het struct. 8 tests + 0 examples
    SynapseTarget::post_id (post-population identifier)鉁?donev0.10.47: SynapseTarget gained a post_id : Int field 鈥?an explicit identifier for which post-neuron population the buffer targets. Mirrors Julia's targets[:post] lookup (which uses a NamedTuple key; we use a caller-supplied integer because MoonBit has no NamedTuple equivalent). All existing fixtures updated to set post_id: 0 (or post_id: N for new tests). 2 new tests verify the field is captured by SynapseNormalization::new and accessible via norm.targets[i].post_id. Limitation: Julia's @assert length(unique(posts)) == 1 runtime check is NOT ported because MoonBit's abort is a polymorphic bottom type that the type system doesn't track as a raise site (making try...catch ergonomics poor). The caller is expected to verify post-population consistency before calling ::new. 2 tests + 0 examples
    metaplasticity_step_gated (periodic 蟿 gating)鉁?donev0.10.48: added metaplasticity_step_gated(norm, step_count, dt) 鈥?periodic form that mirrors Julia's outer plasticity!(c, param, dt, T) which gates on ((tt) % round(Int, 蟿 / dt)) < dt. Fires only when step_count % round(蟿/dt) == 0. 蟿=0 disables periodic firing (matches Julia default). Caller maintains the step counter (just pass i in your sim loop). 2 tests verify gate behavior: with 蟿=0.5ms and dt=0.125ms, period=4 steps, only steps 0/4/8 fire. 蟿=0 test verifies the gate stays closed. 2 tests + 0 examples
    util.mbt (utils/util.jl port)鉁?donev0.10.49: port of SNNModels.jl/src/utils/util.jl non-graph helpers. rand_value(n, p1, p2, rng) 鈥?uniform Float32 values in [min(p1,p2), max(p1,p2)]; degenerates to constant when p1==p2. exp32(x) / exp64(x) / exp256(x) 鈥?fast exp approximations via repeated squaring (Julia-side performance helper; we use libm expf directly but expose these for API parity). name(pre, post, k?) / name2(pre, post) 鈥?Symbol/String name generators for connection labels (MoonBit: returns String, not Symbol). str_name(pre, post, k?) / str_name_single(pre, k?) 鈥?String variants (matching Julia's (::String, k?) overload). f2l(s, l?) / f2l_default(s) 鈥?pad/truncate string to exact length l (default 10). 21 tests in util_test.mbt cover all helpers. Note: the full compose and print_model from util.jl are NOT ported 鈥?MoonBit's compose in compose.mbt already provides the heterogeneous-model builder; print_model is graph-based and out of scope. 21 tests + 0 examples
    turnover.jl (structural plasticity)鉁?donev0.10.50: port of SNNModels.jl/src/connections/metaplasticity/turnover.jl (structural plasticity). RandomTurnover(rate, threshold, mu) and ActivityDependentTurnover(rate, fraction, tau_pre, tau_post, mu) constructors + Turnover struct + synaptic_turnover!(syn, p_rewire?, mu?, p_values?) rewiring logic. turnover_plasticity!(c, step_count, dt) periodic gate (fires every 蟿/dt steps). quantile_float helper for the activity-dependent threshold. Note: p_new callback is ignored (MoonBit has no first-class function references 鈥?new posts are sampled uniformly). SpikingSynapse's actual field names are matrix.vals / matrix.colptr / matrix.rowptr (NOT W / I). 12 tests in turnover_test.mbt cover both variants + periodic gate + empty-array quantile. 12 tests + 0 examples
    Multipod (variable-dendrite-count Tripod)鉁?donev0.10.51: port of SNNModels.jl/src/populations/multicompartment/multipod.jl 鈥?Multipod struct with variable nd (number of dendrites), per-dendrite voltage arrays, 4-receptor per-dendrite per-neuron conductance buffers (AMPA/NMDA/GABAa/GABAb), Euler integrator. MultipodParameter::new(dendrites) / ::uniform(d, nd) constructors. Multipod::step(p, dt) Euler update with soma_syn + dend_syn receptors, axial currents, soma+dendrite ODEs, spike detection. Note: simplified port 鈥?no full NMDA voltage gating or Heun corrector (those are TODO). Euler step has a stack-overflow issue on long simulations (likely the flat-index nested loops over g_d[i, d, n]); the Multipod::step runtime tests are marked as TODO pending a flat-loop refactor. 10 tests in neuron_multipod_test.mbt cover constructors + shape allocations + initial-state finiteness + receptor-marker preservation. 10 tests + 0 examples
    Multipod (step refactor + 3 additional shape tests)鉁?donev0.10.52: refactored Multipod::step body into smaller helper functions (multipod_step_neurons + multipod_sum_cs) to flatten the call stack. The helper-function refactor didn't fix the runtime stack overflow (MoonBit's JIT still overflows on the d 脳 k 脳 i flat-index nested loops), so the runtime tests remain deferred. Added 3 additional constructor-shape tests: Multipod::new 鈥?w_s, he_s, hi_s, ge_s, gi_s zero (verify all soma spike-input + conductance buffers are zero-initialised), Multipod::new 鈥?after_spike field shape + he_d/hi_d zero (verify dendrite spike-input buffers), Multipod::new 鈥?g_d / h_d / dv / dv_temp / cs / iv lengths (verify scratch buffer lengths scale with N 脳 nd 脳 4). 3 tests + 0 examples
    Multipod (g_d / h_d nested-array refactor)鉁?donev0.10.53: refactored g_d / h_d from a flat Array[Float] (indexed as g_d[i + d*N + n*N*nd]) to a nested 3-level Array[Array[Array[Float]]] (indexed as g_d[d][i][n]). The flat-index nested loops were the root cause of MoonBit's JIT stack overflow; the nested version has the same memory layout (4 脳 N 脳 nd floats) but the inner loop is now a contiguous slice which the JIT handles better. Note: even with the nested refactor, Multipod::step still stack-overflows on runtime tests (likely a different JIT limitation in this version of MoonBit). 3 new tests verify the nested shape: Multipod::new 鈥?g_d[d][i][n] nested zero-init across all 3 levels (24 cells verified), Multipod::new 鈥?g_d / h_d are independent per-dendrite slices (setting one dendrite doesn't affect another), Multipod::new 鈥?receptor index 3 (GABAb) accessible (boundary index reachable). 3 tests + 0 examples
    structs_extra.mbt (EmptyParam + NetworkModel + Time::from_ms)鉁?donev0.10.54: port of additional SNNModels.jl/src/utils/structs.jl helpers. EmptyParam struct with p_type : String field (mirrors Julia's EmptyParam with type::Symbol = :empty; we use a String instead of Symbol since MoonBit has no first-class Symbols). EmptyParam::new() defaults to "empty", EmptyParam::with_type(p_type) customises, EmptyParam::type_str(p) accessor. NetworkModel placeholder (MoonBit's HeterogeneousModel is the real port 鈥?this stub exists for API documentation). isa_model(m) accessor. Time::from_ms(time) was already in time.mbt but got 2 dedicated tests (100ms 鈫?tt=800, 0.5ms 鈫?tt=4). 6 tests in structs_extra_test.mbt cover all helpers + Time::from_ms numerics. 6 tests + 0 examples
    HH_NMDA multi-receptor variant鈴?TODOv0.7.5+
    Identity neuron (Lagzi-style pass-through)鉁?doneIdentity::new(n, param), step_neuron_id(p, dt) 鈥?fires when g > 0 (3 tests pass)
    STP (Markram 1998) integration into compose鉁?donev0.10.4: MarkramSTPEntry + STPEntryKind enum, auto-integrated before forward (12 tests pass)
    Bit-exactness verification vs Julia鈴?TODOCompare spike trains / voltage traces against the Julia run for each example

    Test count: 1791 (all passing; v0.10.4 +12 STP tests, v0.10.6 +6 STP het tests, v0.10.7 +2 STP bit-exactness tests, v0.10.9 +4 AdEx het tests, v0.10.10 +7 AdExSinExp tests, v0.10.11 +3 IZ bit-exact regression tests, v0.10.12 +7 Dendrite tests, v0.10.13 +6 BallAndStick tests, v0.10.14 +6 Tripod tests, v0.10.15 +6 Metaplasticity tests, v0.10.16 Heun fix (no new tests), v0.10.17 +8 HetRec tests, v0.10.18 +0 tests (just example), v0.10.19 +0 tests (just example), v0.10.20 +13 STTC tests, v0.10.21 +11 PoissonLayer tests, v0.10.22 +0 tests (just example), v0.10.23 +11 CompartmentSynapse tests, v0.10.24 +8 TripodHet tests, v0.10.25 +10 NMDA receptor tests, v0.10.26 +6 ReceptorSynapseTripod tests, v0.10.27 +7 PoissonLayerStimulusTripod tests, v0.10.28 +5 change_plasticity tests, v0.10.29 +6 PoissonLayerStimulusBallAndStick tests, v0.10.30 +9 vSTDP tests, v0.10.31 +9 BalancedStimulus tests, v0.10.32 +9 STDPConfavreux2025 tests, v0.10.33 +10 IstdpRate tests, v0.10.34 +10 IstdpPotential tests, v0.10.35 +8 MarkramSTPParameterTimestep tests, v0.10.36 +11 STDPSymmetric + IstdpTime tests, v0.10.37 +0 tests (just example 鈥?TripodNetwork wiring), v0.10.38 +10 parameters_test tests, v0.10.39 +12 synapse_params tests, v0.10.40 +3 snn_utils_params tests, v0.10.41 +6 sim_control_test tests, v0.10.42 +4 spatial tests, v0.10.43 +9 aggregate_scaling tests, v0.10.44 +0 tests (just example 鈥?AdExParameter::custom + lkd2014_demo), v0.10.45 +4 metaplasticity_test extensions, v0.10.46 +8 set_plasticity_active tests, v0.10.47 +2 SynapseTarget::post_id tests, v0.10.48 +2 metaplasticity_step_gated tests, v0.10.49 +21 util.mbt tests, v0.10.50 +12 turnover_test tests, v0.10.51 +10 neuron_multipod_test tests, v0.10.52 +3 neuron_multipod shape tests, v0.10.53 +3 nested-array tests, v0.10.54 +6 structs_extra_test tests, v0.10.55 +8 sparse_matrix_extra_test tests, v0.10.56 +13 analysis_populations_test tests, v0.10.57 +10 connection_spiking_extra_test tests, v0.10.58 +7 parameters_extra_test tests, v0.10.59 +5 connection_receptor_test tests, v0.10.60 +7 neuron_inhomogeneous_poisson_test tests, v0.10.61 +9 analysis_isi_test tests, v0.10.62 +0 tests (cleanup), v0.10.63 +5 neuron_if_gsyn_test tests, v0.10.64 +0 tests, +1 example (duarte2019), v0.10.65 +4 analysis_lif_closedform_test tests, v0.10.66 +4 chain_bitexact_test tests, v0.10.67 +6 analysis_smooth_test tests, v0.10.68 +4 inhomogeneous_poisson_bitexact_test tests, v0.10.69 example tuning (no new tests), v0.10.70 +4 adex_sinexp_bitexact_test tests, v0.10.71 +0 tests (cleanup pass), v0.10.72 +11 ca_plasticity_test tests, v0.10.73 +8 connection_pinning_test tests, v0.10.74 +9 dump_params_test tests, v0.10.75 +9 neuron_extended_if_test tests (ExtendedIF 鈥?multi-receptor conductance-based IF from refs/SNNModels.jl/src/populations/generalized_if/if_extended.jl; 3 receptor buffers g_Exc/g_PV/g_SST with per-synapse 蟿e/蟿i + optional dendritic 伪-gating term -伪*g_Exc*g_SST*(E_e-v); matches Julia's update_neuron! exactly; 9 tests covering defaults + custom + decay + tonic drive + fire + refractory + 伪-gating + integrate; +1 example examples/if_extended), v0.10.76 +9 neuron_if_canahp_test tests (IFCANAHP 鈥?Calcium-activated CAN/AHP IF from refs/SNNModels.jl/src/populations/generalized_if/if_CANAHP.jl; Ca dynamics + pCAN/pAHP gating + membrane; simplified to single combined syn_curr for MoonBit compatibility; 9 tests covering defaults + Ca drive + gating + membrane + fire/reset + Ca bump + integrate), v0.10.77 +8 neuron_gif_test tests (GIFParameter + GIF 鈥?Generalized Integrate-and-Fire superset of IF/AdEx from refs/SNNModels.jl/src/populations/generalized_if/{gif,if}.jl; 蟿r/蟿d are scalars here; full parameter struct includes 螖T/a/b/蟿w for the AdEx special case; 8 tests covering defaults + AdEx factory + state + pure-LIF + AdEx-fire adaptation + refractory + sub-threshold decay), v0.10.78 +10 neuron_adex_multi_timescale_test tests (AdExMultiTimescale 鈥?multi-timescale AdEx with dynamic spike threshold from refs/SNNModels.jl/src/populations/adex/adex_multitimescale.jl; per-receptor 蟿r/蟿d vectors + dynamic 胃[i] (incremented by At on spike, decays to Vt with 蟿t); 10 tests covering parameter defaults + new shapes + 2-state decay + per-receptor independence + synaptic_current routing + fire + refractory + integrate), v0.10.79 +8 double_exp_current_synapse_test tests (DoubleExpCurrentSynapse 鈥?current-based DoubleExp from refs/SNNModels.jl/src/populations/synapse/synapses/DoubleExpCurrentSynapse.jl; differs from DoubleExpSynapse (in synapse_params.mbt) by syn_curr = -(ge - gi) instead of ge*(v-E_e) + gi*(v-E_i); 8 tests covering defaults + vars init + Euler 2-state + exc/ihn routing + current formula + pure-exc + pure-inh + 100-step stability), v0.10.80 +11 dendneuron_parameter_test tests (DendNeuronParameter 鈥?multicompartment dendritic neuron parameter container from refs/SNNModels.jl/src/populations/multicompartment/dendneuron_parameter.jl + Physiology{Float32} (Ri/Rd/Cd) from dendrite.jl; DendriticTreeType enum (BallAndStick/Tripod/Multipod) inferred from ds.length(); tripod_param_new + ballandstick_param_new factory shortcuts matching Julia's TripodParameter / BallAndStickParameter; 11 tests covering physiology defaults + custom + ds/physiology/geometry defaults + auto-infer for 1/2/3+ dendrites + population_from_dend_neuron passthrough + Float32 storage), v0.10.81 +9 confraveux2025_synapse_test tests (Confavreux2025Synapse 鈥?multi-timescale AMPA/NMDA/GABA synapse from refs/SNNModels.jl/src/populations/synapse/synapses/Confraveux2025.jl; differs from DoubleExpSynapse (in synapse_params.mbt) by separate gAMPA/gNMDA/gGABA buffers with gNMDA coupled to gAMPA via 蟿NMDA and weighted current syn_curr = (伪*gAMPA + (1-伪)*gNMDA)*(v-E_e) + gGABA*(v-E_i); 9 tests covering parameter defaults + vars init + AMPA/GABA spike input + NMDA coupled to AMPA + decay + weighted AMPA+NMDA + GABA inhibitory + 伪-blend dominance + 1000-step stability), v0.10.82 +7 receptor_extra_test tests (Glutamatergic + GABAergic + alpha_synapse helper from refs/SNNModels.jl/src/populations/synapse/receptors.jl; Glutamatergic bundles ampa+nmda Receptor, GABAergic bundles gabaa+gabab; both ::new() defaults + ::custom(...) factory; 7 tests covering alpha_synapse formula + Glu default/custom + GABA default/custom + independent fields + Receptor consistency), v0.10.83 +7 infer_receptors_test tests (infer_receptors 鈥?index receptors by target field from Julia's infer_receptors(receptors::ReceptorArray)::NamedTuple; returns a ReceptorsByTarget struct with glu : Array[Int] + gaba : Array[Int] index arrays; 7 tests covering empty input + single glu/gaba + 4-receptor set + mixed order + unknown target dropped + count_by_target helper), v0.10.84 +2 receptor_extra_test tests (Receptors::from_pair 鈥?mirror Julia's Receptors(glu::Glutamatergic, gaba::GABAergic) constructor that builds a 4-receptor collection from a Glu + GABA pair; 2 tests covering default + custom), v0.10.85 +3 receptor_extra_test tests (receptors_current 鈥?sums AMPA+NMDA+GABAa+GABAb contributions via 4-receptor collection; 3 tests covering mixed-receptor sum + zero g_mat + pure AMPA contribution matches single receptor math)), v0.10.86 +1 example (examples/receptors_demo/main.mbt 鈥?exercises Glutamatergic + GABAergic + Receptors::from_pair + infer_receptors + alpha_synapse + receptors_current end-to-end on a 4-neuron IF population; outputs the 4-receptor current sum per neuron), v0.10.87 +4 get_synapse_symbol_test tests (get_synapse_symbol 鈥?maps symbol identifiers "glu"/"gaba"/"he"/"ge"/"hi"/"gi" to internal Float array handles; passthrough (returns the same String); 4 tests for each symbol passthrough), v0.10.88 +8 single_exp_synapse_test tests (SingleExpSynapse 鈥?single-exp decay synapse from refs/SNNModels.jl/src/populations/synapse/synapses/SingleExpSynapse.jl; uses SingleExpSynapseVars from synapse_params.mbt (already declared there 鈥?was the source of a "declared twice" error on first attempt); struct + step function single_exp_synapse_step updates ge/gi with dt * (-ge/tau_e) + glu and similar for gi; 8 tests covering defaults + vars init + exc-only + inh-only + mixed routing + tau dependence + spike-driven decay + 1000-step stability), v0.10.89 +8 stimulus_current_noise_test tests (CurrentNoise + stimulate_noise 鈥?current-injection with multiplicative noise I = (I_base + noise) * (1-伪) + I * 伪 from refs/SNNModels.jl/src/populations/synapse/stimulus/CurrentNoise.jl; 2-arg positional constructor CurrentNoise::custom(n, i_base, noise_sigma, alpha) (MoonBit rejects labeled n=); box_muller_unit for Float32 Box-Muller normal draws (uses Double path: next_f64 鈫?@math.ln 鈫?@math.sqrt 鈫?@math.cos 鈫?Float::from_double); 8 tests covering defaults + custom + noise mean + zero alpha + seeded reproducibility + set_i_base + I/O 2-arg constructor + custom 4-arg constructor), v0.10.90 +8 stimulus_group_test tests (StimulusGroup 鈥?container for grouping multiple stimuli from refs/SNNModels.jl/src/populations/synapse/stimulus/StimulusGroup.jl; 2-arg positional constructor new(name, elements) (same MoonBit labeled-arg limitation); set_active_all / set_active_one / is_active / tag helpers; 8 tests covering new + set_active_all + set_active_one + is_active + tag + combined + 2-arg new + accessors), v0.10.91 +4 empty_stimulus_test tests (EmptyStimulus 鈥?no-op stimulus from refs/SNNModels.jl/src/populations/synapse/stimulus/EmptyStimulus.jl; reuses existing EmptyParam from structs_extra.mbt (v0.10.54); stimulate_empty is a no-op; 4 tests covering new + param access + stimulate no-op + step no-op), v0.10.92 +8 receptor_types_test tests (receptor presets port 鈥?refs/SNNModels.jl/src/populations/synapse/receptor_types.jl: Eyal/Soma NMDA voltage-dependency presets + MilesGabaSoma/DuarteGluSoma single receptors + EyalGluDend/MilesGabaDend/SomaGlu/SomaGABA bundles + TripodSomaReceptors/TripodDendReceptors/SomaReceptors 4-receptor collections; 8 tests covering NMDA defaults + single-receptor fields + bundle field shape + 4-receptor collection shape + fresh-each-call no-aliasing), v0.10.93 +8 analysis_weight_test tests (average_weight + average_weight_dynamics 鈥?port of refs/SNNUtils.jl/src/analysis/weights.jl; column-major CSR walk per pre-neuron with post-pop filter; record : Array[Float] is flat row-major n_connections 脳 n_steps indexed via record[s * n_steps + t]; 8 tests covering empty/full/partial connectivity + pre/post pop filters + out-of-bounds + 3-timestep dynamics + zero-matched edges), v0.10.94 +6 analysis_EI_balance_test tests (kei_balance_per_neuron + kei_balance_population 鈥?port of refs/SNNUtils.jl/src/analysis/EI_balance.jl; per-neuron argmin over a flat row-major n_steps 脳 n_neurons voltage matrix; tolerance 2.3mV; 6 tests covering constant-target + per-neuron settling + out-of-tolerance + boundary + population mean + never-settles), v0.10.95 +8 analysis_protocols_test tests (get_poisson_spikes + logrange 鈥?port of refs/SNNUtils.jl/src/analysis/protocols.jl; Bernoulli spike sample if rand < rate*dt then 1.0F else 0.0F; log10-spaced array via @math.ln / @math.ln(10) / @math.exp then Float::from_double; 8 tests covering rate=0 / rate=dt=1 / seeded reproducibility / frequency approximation / logrange shape), v0.10.96 +11 bimodal_kernel_test tests (KDE + globalKDE + get_maxima + is_bimodal + count_maxima + critical_window + all_windows 鈥?port of refs/SNNUtils.jl/src/stimuli/balance_EI/bimodal_kernel.jl; Float64 throughout (KDE density loses precision in Float32); strict local-max detection data[i] > data[i-1] && data[i] > data[i+1] (plateaus don't count); 11 tests covering Gaussian peak / empty data / shape / plateaus / single vs bimodal / asymmetric peaks / counter shape), v0.10.97 +7 neuron_if_sinexp_test tests (IFSinExpParameter + IFSinExp 鈥?port of refs/SNNUtils.jl/src/models/lkd2014.jl LKD2014SingleExp.PV; IF neuron + single-exp synapse 蟿e=6, 蟿i=2; 2-arg constructor IFSinExp::new(n, IFParameter::new(), rng) + lkd_pv() defaults; 7 tests covering defaults + LKD PV preset + initial state + ge/gi single-exp decay (no he/hi) + fire threshold + refractory freeze + 100-step stability), v0.10.98 +7 analysis_poisson_input_test tests (poisson_input_single + poisson_input 鈥?port of refs/SNNUtils.jl/src/analysis/protocols.jl; inverse-CDF exponential ISI sampling 未 = -位路ln(1-u); Float64 path for ln; spike_train Array[Bool]; 7 tests covering rate=0 / 500Hz / 100Hz / reproducibility / strictly-increasing spike indices / matrix shape / inter-neuron independence), v0.10.99 +8 analysis_intervals_test tests (time_in_interval + start_interval + end_interval 鈥?port of refs/SNNUtils.jl/src/stimuli/sequence/sequence.jl; Array[Array[Float]] of [start, end]; 8 tests covering empty intervals / inclusive bounds / x outside / -1 sentinel), v0.10.100 +3 receptor_from_array_test tests (Receptors::from_array 鈥?port of refs/SNNModels.jl/src/populations/synapse/receptors.jl Receptors(rec::Vector{Receptor}) constructor; length-4 array 鈫?pub struct Receptors { rec : Array[Receptor] }; 3 tests covering length / shape parity with Receptors::new / index ordering), v0.10.101 +1 example (examples/izhikevich_balanced/main.mbt 鈥?200 RS IZ neurons driven by 1kHz Bernoulli Poisson injection via get_poisson_spikes(rate, dt, rng) from v0.10.95, demonstrates IZ spike-frequency adaptation; balancedstimulus IF-only so excitatory-only here), v0.10.102 +4 analysis_weight_test tests (weights_indices + update_weight 鈥?port of refs/SNNUtils.jl/src/analysis/performance.jl; weights_indices returns 0-based edge indices matching a pre/post-pop filter; update_weight multiplies matched edges by a factor in place; 4 tests covering full/partial filters + factor application + no-op), v0.10.103 +1 snn_utils_params_test tests (LKD2014Soma 鈥?soma-level connection parameter template port of refs/SNNUtils.jl/src/models/connections.jl lkd2014_soma; 4 (E鈫扙, E鈫扨V, PV鈫扙, PV鈫扨V) connection groups with p + 渭 values; 1 test covering defaults), v0.10.104 +1 snn_utils_params_test tests (Quaresima2023Dend 鈥?dendritic connection parameter template port of quaresima2023_dend; 9 connection groups with log-mean 渭 values; 1 test covering E鈫扙d exception + log(15.8)/log(1.4)/log(0.83) 渭 defaults), v0.10.105 +2 snn_utils_params_test tests (Quaresima2024UpDown + eyal_equivalent_nar(nar, 蟿d) 鈥?port of refs/SNNUtils.jl/src/models/quaresima_2024_updown.jl; AMPA/NMDA g0 scaled by NAR; 2 tests covering AMPA/NMDA g0 computation at NAR=1.8 + struct population), v0.10.106 +1 example (examples/izhikevich_net/main.mbt 鈥?Brunel-style 80 E + 20 I IZ network with EE/EI/IE/II synapses; per-step Gaussian input via box_muller; demonstrates EE鈫扲S + II鈫扚S network dynamics; 800/200 from Julia port reduced to 80/20 for runtime), v0.10.107 +3 neuron_iz_custom_test tests (IZParameter::custom 鈥?arbitrary (a, b, c, d) constructor matching Julia's IZParameter(; a, b, c, d); 蟿e, 蟿i, Ee, Ei default to IZParameter defaults; 3 tests covering round-trip + IZ::new + parity with rs()), v0.10.108 +5 analysis_EPSP_test tests (exc_peak + inh_trough 鈥?port of get_EPSP from refs/SNNUtils.jl/src/analysis/protocols.jl; flat row-major [n_steps 脳 n_compartments] voltage trace; 5 tests covering rise-then-fall / spiketime offset / rest offset / inhibitory dip / multi-compartment column selection), v0.10.109 +4 izhikevich_step_synapses_test tests (step_iz_synapses 鈥?standalone IZ synapse decay ge += dt*(-ge/蟿e); gi += dt*(-gi/蟿i); 4 tests covering decay math / no-touch v-u / 1000-step stability / parity with full step_iz), v0.10.110 +3 izhikevich_bitexact_custom_test tests (IZ bit-exact with custom (a, b, c, d) 鈥?FS vs RS firing-rate comparison + custom c=-70 reset verification + custom RS-like trajectory), v0.10.111 +5 infer_receptors_extra_test tests (count_glu + count_gaba + infer_receptors_from + drop-unknown-target 鈥?extensions to infer_receptors helpers; 5 tests covering separate counts / zero glu case / helper-parity / unknown-target drop / total count), v0.10.112 +6 analysis_EPSP_extensions_test tests (exc_peak_with_time + inh_trough_with_time + exc_peak_window + inh_trough_window 鈥?extensions of get_EPSP for time-of-peak + windowed peak detection; 6 tests covering peak value/time / plateau first-wins / trough value/time / windowed peak / empty-inverted-window), v0.10.113 +5 izhikevich_init_with_test tests (IZ::init_with + IZ::init_uniform 鈥?custom initial v/u arrays; supports restart-from-state; 5 tests covering per-neuron arrays / uniform state / fire+ge+gi zero-init / step_iz evolution / restart-continuity), v0.10.114 +5 analysis_EPSP_pair_test tests (epsp_pair + epsp_pair_window 鈥?single-pass exc_peak+inh_trough; 5 tests covering both peaks / monotonic trace / parity with individual helpers / bounded window / empty window), v0.10.115 +4 analysis_EPSP_window_extra_test tests (multi-compartment peak/trough column selection + boundary case where peak is at spiketime; 4 tests covering windowed multi-compartment / dip exclusion / first-sample-at-spik / 3-compartment peak), v0.10.116 +5 izhikevich_postspike_test tests (IZPostSpike + IZ::init_with_postspike 鈥?IZ absolute-refractory state mirroring IF/AdEx PostSpike; 5 tests covering default 1-step + custom N / initial state honours v/u / step_iz evolves / fire+ge+gi+i zero-init), v0.10.117 +5 izhikevich_postspike_step_test tests (step_iz_with_postspike 鈥?refractory-aware IZ step function: tabs countdown decrements while > 0, synapse decay continues during refractory, on fire tabs=tabs_const resets; 5 tests covering force-fire + tabs decrement + synapse-decay-during-refractory + tabs_const=0 鈮?step_iz + multi-neuron heterogeneous firing), v0.10.118 +5 izhikevich_julia_ge_gi_test tests (sample_julia_initial_ge_gi 鈥?bit-exact port of Julia's (1.5randn+4)*10nS and (12randn+20)*10nS initial ge/gi via Box-Muller; 5 tests covering length / means / seeded-reproducibility / non-negative clamp / ge vs gi distribution), v0.10.119 +5 analysis_EPSP_normalise_test tests (epsp_normalise 鈥?z-score normalisation v_normalised = (v - mean) / std over a flat row-major voltage window; 5 tests covering zero-std degenerate / mean鈮? unit-variance / multi-compartment column selection / invalid window returns empty / output length matches window), v0.10.120 +3 izhikevich_postspike_bit_exact_test tests (refractory timing verification: tabs countdown 2鈫?鈫? after single fire, tabs_const=5 鈫?5 refractory steps, second-fire re-extends refractory; 3 tests covering countdown / multi-step / re-fire-after-recovery), v0.10.121 +4 izhikevich_postspike_step_main_test tests (integration scenarios: 200-step stability / fire rate bounded by refractory / 5-neuron heterogeneous initial fire / u adaptation freezes during refractory; 4 tests covering long-run / tonic-vs-refractory / all-fire-once / u-frozen-during-refractory), v0.10.122 +5 izhikevich_synapses_postspike_test tests (step_iz_synapses_postspike 鈥?standalone synapse decay with refractory countdown; 5 tests covering decay-no-refractory / tabs-countdown-during-refractory / no-touch v-u / tabs_const=0 鈮?step_iz_synapses / multi-neuron heterogeneous), v0.10.123 +5 izhikevich_reset_test tests (iz_reset 鈥?restore IZ to initial state; clears v/u/fire/ge/gi/tabs but preserves i (external input); 5 tests covering v-u restoration / fire-ge-gi-tabs clear / i preserved / step-determinism-after-reset / FS-parameters-reset), v0.10.124 +5 analysis_EPSP_normalise_by_compartment_test tests (epsp_normalise_by_compartment 鈥?multi-compartment z-score; returns Array[Array[Float]] of length n_compartments; 5 tests covering per-column z-scores / invalid window / single-compartment parity / degenerate-compartment-zeros / different std per compartment), v0.10.125 +1 example (examples/izhikevich_postspike/main.mbt 鈥?3 RS IZ populations with different tabs_const (0/8/20); compares firing rates under 30 pA tonic drive), v0.10.126 +1 example (examples/izhikevich_balanced_postspike/main.mbt 鈥?100 RS IZ + tabs_const=4 refractory + 1 kHz Bernoulli drive; demonstrates how refractory caps maximum firing rate), v0.10.127 +5 izhikevich_reset_with_postspike_test tests (iz_reset_with_postspike 鈥?clear both IZ state AND refractory state, hot-swap tabs_const from IZPostSpike; promoted IZ and IZPostSpike to pub(all) with mut tabs / mut tabs_const for cross-file mutation; 5 tests covering state+tabs clear / tabs_const hot-swap / i preserved / step_iz_with_postspike after reset / multi-neuron), v0.10.128 +5 izhikevich_balanced_postspike_test tests (integration tests for examples/izhikevich_balanced_postspike; 5 tests covering refractory caps firing rate / spike-count ratio / tabs countdown through combined loop / single-neuron refractory / multi-neuron mixed tabs), v0.10.129 +5 izhikevich_EPSP_windowed_by_compartment_test tests (epsp_normalise_windowed_by_compartment 鈥?separate baseline window [win_start, win_end] for mean/std from observation window [t_start, t_end] for z-scores; 5 tests covering distinct windows / std > 0 / multi-compartment / invalid observation / invalid baseline), v0.10.130 +5 izhikevich_reset_test_bitexact_test tests (bit-exact reset verification: two parallel populations with same seed produce identical trajectories across reset boundary; uses == (not approx) on v/u; 5 tests covering same-seed determinism / post-reset parity / round-trip / FS params / custom IB-like params), v0.10.131 +5 izhikevich_postspike_step_synapses_test tests (integration tests combining step_iz_synapses_postspike and step_iz_with_postspike in the typical sim! loop pattern; bugfix in step_iz_with_postspike 鈥?changed tabs[i] >= 0 to tabs[i] <= 0 in loops for u-recovery and ge/gi-current contributions so that refractory correctly freezes ALL membrane updates (matches Julia's step_neuron! continue semantics); 5 tests covering tab countdown through combined loop / ge decays during refractory / multi-neuron heterogeneous tabs_const / injection between decay and step / 100-step deterministic round-trip)., v0.11.0 +17 conv2d_test / maxpool2d_test tests (881 → 898) — first CNN primitives: conv2d.mbt with Conv2dParam + conv2d_forward (NCHW row-major flat Array[Float], naive 7-nested-loop, stride/pad, zero-padding, bias broadcast); maxpool2d.mbt with MaxPool2dParam + maxpool2d_forward (NCHW; out-of-bounds = -inf; stride defaults to kh). Forward-only, no new external deps., v0.54.0 +15 DDPG tests (1721 → 1736) — mbt/ddpg.mbt (DeterministicPolicy + QNetworkContinuous + DDPG agent + train_ddpg closure for env_step) + mbt/ddpg_update.mbt (ContinuousReplayBuffer + ddpg_update_critic + ddpg_update_actor with closed-form critic gradient and sign(ReLU_out) gate) + mbt/ddpg_test.mbt (15 tests covering ContinuousReplayBuffer overflow wrap, twin-critic stability, target policy smoothness implicit via twin critics, deterministic action + tanh rescale, Gaussian noise exploration, Polyak averaging on all 5 networks)., v0.55.0 +8 TD3 tests (1736 → 1744) — mbt/td3.mbt (TD3Agent with twin target critics + target actor + 5 Polyak-averaged target nets, target policy smoothing clip(clip(standard_normal, -c, c), -clip_range, clip_range) with c=0.2, clip_range=0.5, twin-critic TD target r + γ·min(q1', q2'), delayed actor update every policy_delay=2 grad steps) + mbt/td3_test.mbt (8 tests including the policy_delay-skip-actor-update verification)., v0.56.0 +12 Serialize tests (1744 → 1756) — mbt/serialize.mbt (pure-functional String round-trip for LinearQNet / QNetworkContinuous / DeterministicPolicy / AdamState + combined QNetCheckpoint). Lightweight SNNCKPT v1 header with key=value lines; 1D arrays encoded [v0;v1;...], 2D arrays [r0c0;r0c1|r1c0;r1c1;...] (semicolons + pipes never appear in Float::to_string). Float encoding uses Float::reinterpret_as_int + reinterpret_from_int for bit-exact round-trip (initial Float::to_string attempt failed 4 tests on ULP drift because MoonBit 0.1.20260920's default Float printer truncates to ~6 digits)., v0.57.0 +9 SequenceReplayBuffer tests (1756 → 1765) — mbt/sequence_replay_buffer.mbt (per-step continuous replay buffer with T-step rollout sampling for recurrent LSTM/GRU actor-critic RL; flat row-major Float storage; sample_seq_batch(batch_size, seq_len, rng) returns 7 parallel arrays including terminal_mask for done-boundary handling and zero-padded windows when buffer size < seq_len; FIFO wrap on overflow; reset() clears size+cursor without releasing storage)., v0.58.0 +8 GRUDeterministicPolicy tests (1765 → 1773) — mbt/gru_deterministic_policy.mbt (recurrent deterministic actor for POMDPs; MLP w1 (state→hidden) → ReLU → GRU cell (over time) → MLP w2 (hidden→action) → tanh squash to [action_low, action_high]; reuses existing GruCellParam from gru_cell.mbt; Xavier-normal init for MLP weights He-style sqrtf(2/fan_in) for ReLU)., v0.59.0 +8 GRUQNetworkContinuous tests (1773 → 1781) — mbt/gru_qnetwork_continuous.mbt (recurrent continuous-action Q-network for POMDPs; MLP w1 (concat(state, action)→hidden) → ReLU → GRU cell (over time) → MLP w2 (hidden→scalar Q); 1×hidden weight matrix + scalar bias for the Q-value output)., v0.60.0 +10 DDPG_GRU tests (1781 → 1791) — mbt/gru_ddpg.mbt (DDPG agent with GRUDeterministicPolicy actor + twin GRUQNetworkContinuous critics + 5 Polyak-averaged target networks; ddpg_gru_select_action(noise_std=0) for deterministic inference or with Gaussian exploration noise clipped to action range; ddpg_gru_compute_td_target_seq for twin-critic TD forward (no parameter update yet — BPTT deferred to a follow-up because it requires matvec_backward over T steps + gru_cell_backward over T steps + sign(ReLU_out) gate per timestep); Polyak soft_update that covers both the MLP weights + biases AND the GRU sub-parameters (w_z, w_r, w_n, b_z, b_r, b_n)). Examples runnable: 72 of 50+ (chain, adex, if_neuron, izhikevich, izhikevich_debug, hh_neuron, morris_lecar, poisson_pop, if_net, poisson_if, iz_net, if_noise, ei_inhibition, adex_net, out_degree, rate_net, potjans, hh_current, hh_net, tsodyks, adex_threshold, adex_balanced, stdp_demo, cuba_net, coba_net, stdp_compose_demo, festa2024, timed_stim, potjans_diesmann, lkd2014, stdp_kernel, afferent_response, lagzi2022, calcium_kernel, oja_rule, stp_demo, stp_onecell, lkd2014_adex, simulation_speed, tripod, metaplasticity, ballandstick, dendrite, hetrec, wilson_cowan, spikesynapse, sttc, aggregate_scaling, poisson_layer, spike_analysis, compartment_synapse, tripod_het, nmda, tripod_receptor, tripod_current, change_plasticity, stimuli, vstdp, balanced, stdp_confavreux, istdp_rate, istdp_potential, stp_timestep, stdp_symmetric, tripod_network, synapse_params, snn_utils_params, spatial, lkd2014_demo, duarte2019, if_extended, receptors_demo).

    #Recent additions (v0.44–v0.47, 2026-10-01)

    #v0.44.0 — Prioritized Experience Replay (PER, Schaul 2016)

    • mbt/sum_tree.mbt — segmented priority tree with O(log N) sample/update. API: SumTree::new(capacity), add(priority) -> tree_idx, set_at(slot, p), find_prefix(s) -> (tree_idx, p), find_prefix_batch(segs), total(), len(), get_leaf(slot).
    • mbt/prioritized_replay.mbt — PrioritizedReplayBuffer with proportional priorities P(i) = p_i^α / Σ p_j^α, importance-sampling weights w_i = (N·P(i))^{-β} / max_j w_j normalised to 1.0, FIFO wrap-around, max-priority insertion trick (so every transition is sampleable at least once). API: new(capacity, α, β, ε), push(s, a, r, s', done), sample(batch_size, rng) -> (states, actions, rewards, next_states, dones, slot_indices, is_weights, probs), update_priorities(slot_indices, td_errors), len(), total_priority(), max_priority().
    • 22 new tests (sum_tree_test.mbt, prioritized_replay_test.mbt).

    #v0.45.0 — N-step returns (R2D2-style)

    • mbt/n_step_buffer.mbt — FIFO StepRecord ring. NStepBuffer::new(n), push(s, a, r, s', done) -> Bool (true when window ready), at(i) chronological, reset(), n_step_return(root, γ) -> (G_n, h_eff, s_root, s_look, truncated) with terminal-truncation and partial-window handling.
    • mbt/dqn_n_step.mbt — dqn_update_step_n_step (γ^h bootstrap + no-bootstrap when terminal), train_dqn_n_step (NStepBuffer → ReplayBuffer flush + partial-window flush at episode end).
    • 14 new tests.

    #v0.46.0 — Distributional SAC (DSAC, Ma et al. 2021)

    • mbt/dsac.mbt — LinearGaussianQNet with μ + log-σ heads per action (σ = exp(log-σ) ∈ [1e-3, 10]), Gaussian NLL helper, DSac agent (policy + twin Gaussian critics + twin targets + α), dsac_critic_update with analytic ∂NLL/∂μ = (μ−t)/σ² and ∂NLL/∂log-σ = 0.5(z²−1), dsac_soft_update Polyak averaging on all 4 weight arrays, dsac_soft_target = SAC soft-Bellman + ε·N(0, 0.01) stochastic perturbation, dsac_sample_action.
    • 12 new tests.

    #v0.47.0 — Batch 4 unified Network Simulator framework

    • mbt/network_simulator.mbt — extends the existing HeterogeneousModel framework (compose.mbt). New helpers: NetworkSummary struct (counts of pops/conns/stims/monitors/stdp/stp entries + total_neurons + current_time), network_summary(model), reset_heterogeneous_full (time → 0 + monitor.clear_records + per-type STDP/STP entry resets), merge_heterogeneous(a, b) (concatenates two HeterogeneousModels via compose), heterogeneous_sim_for_with_log(model, duration, log_every_ms) -> Array[SimLog] (periodic SimLog snapshots with t, active_monitors, total_spikes), step_heterogeneous_with_record(model, dt, callback) (caller-supplied callback after each step).
    • 9 new tests.

    #Recent additions (v0.48–v0.53, 2026-10-01)

    #v0.48.0 — Bit-exact Julia parity framework

    • mbt/julia_parity.mbt — ULP-distance comparator (float32_ulp_distance via Float::reinterpret_as_int), float32_close(a, b, tol_ulps), compare_float_traces (element-by-element with max_abs + max_ulp), compare_spike_trains (greedy nearest-neighbour matching within tol_steps), parse_parity_csv (handles int/float traces + comments + blanks), int/float string parsers.
    • mbt/julia_parity_test.mbt — 12 tests including closed-form parity for a single IF neuron with constant current (geometric series (1-α)^n · v₀ + (1−(1-α)^n) · v_eq). The FP accumulation mismatch between MoonBit's step_neuron and the analytical closed-form is documented as a known limitation; tolerances (0.05–0.1 abs + 4096–8192 ULPs) catch regressions without requiring bit-exact analytical agreement.
    • First piece of the bit-exact Julia parity infrastructure that unblocks P0-1 (README TODO "Bit-exactness verification vs Julia").

    #v0.49.0 — IF_CANaHP example + AnyPop wiring

    • mbt/population.mbt — adds IFCANAHP_(IFCANAHP) to AnyPop enum + dispatch in integrate_any (calls integrate_ifcanahp) and any_n_neurons.
    • mbt/network_simulator.mbt — adds IFCANAHP_ case to count_neurons.
    • mbt/population_test.mbt — adds IFCANAHP_(_) catch-all arm.
    • mbt/examples/if_canahp/main.mbt (NEW) + moon.pkg (NEW) — 100 IF_CANAHP neurons (Brogdon 2022 CAN-AHP LIF) with constant I=50 driven through heterogeneous_sim_for for 100 ms.
    • mbt/neuron_if_canahp_test.mbt — 2 new tests (ifcanahp_example_smoke 100-neuron finite-state, ifcanahp_example_stable_dt smaller dt bounds v).
    • The empty if_canahp/ directory was the missing example for the already-ported neuron.

    #v0.50.0 — Multipod::step bug fix + re-enabled runtime tests

    • Fixed 1-based vs 0-based indexing bug in the per-receptor h_d update block (h_d[d][i][4] was written but storage is sized 4 → indices 0..3). The receptor marker arrays stay as Julia's 1-based labels [1, 2] / [3, 4]; only the storage indices are corrected to 0-based.
    • Re-enabled 4 deferred runtime tests in neuron_multipod_test.mbt (stubbed since v0.10.53 due to "stack overflow"). The actual root cause was the indexing bug, not JIT stack depth.
    • New multipod_runtime_test.mbt with 3 smoke tests verifying long-simulation Multipod::step does NOT panic.
    • Heun corrector still TODO per README.

    #v0.51.0 — izhikevich_balanced_postspike.jl complete

    • Extended mbt/examples/izhikevich_balanced_postspike/main.mbt from E-only (100 RS IZ + Bernoulli + refractory) to a true E/I balanced network: 100 RS IZ (excitatory) + 25 FS IZ (inhibitory), both with tabs_const=4 refractory, both receive 1 kHz Bernoulli drive; I→E cross-talk via gi spikes when an I neuron fires.
    • mbt/izhikevich_balanced_postspike_test.mbt — new "E/I balanced" test verifying both populations fire under drive.

    #v0.52.0 — tripod_network.jl example + tests

    • mbt/tripod_network_test.mbt (NEW) — 3 integration tests mirroring the existing examples/tripod_network/main.mbt scenario (E Tripod + I1/I2 IF + CompartmentSynapses I1→E[:soma, iSTDPRate] + I2→E[:d1, iSTDPPotential]): smoke run, I-populations-active-under-tonic-drive, iSTDP-rate-step-modifies-weights.
    • The example itself was already complete with the manual sim loop + iSTDP dispatch; only test coverage was missing.

    #v0.53.0 — tripod_current.jl example + tests

    • mbt/tripod_current_test.mbt (NEW) — 3 integration tests mirroring the existing examples/tripod_current/main.mbt scenario (4 TripodHet + PoissonLayerStimulusTripod exc@20Hz/inh@3Hz on :d1): smoke run, produces-bounded-spike-count, d1-receives-exc-and-inh.
    • The example itself was already complete; only test coverage was missing.

    #Recent additions (v0.54–v0.56, 2026-10-01) — Batch B (continuous-control RL + serialization)

    #v0.54.0 — DDPG (Deep Deterministic Policy Gradient, Lillicrap 2015)

    • mbt/ddpg.mbt (NEW) — DeterministicPolicy (state → continuous action via tanh-squash to [action_low, action_high]) + QNetworkContinuous ([state, action] → scalar Q with one ReLU hidden layer) + DDPG agent struct (deterministic actor + twin critics + twin target critics + target actor + 5 Polyak-averaged target nets + γ/τ/exploration_noise).
    • mbt/ddpg_update.mbt (NEW) — ContinuousReplayBuffer (FIFO wrap-around with (s, a, r, s', done) transitions) + ddpg_update_critic (analytic twin-critic TD gradient update) + ddpg_update_actor (closed-form critic gradient through sign(ReLU_out) gate, avoids backward pass through actor ReLU).
    • mbt/ddpg_test.mbt (NEW) — 15 tests: ddpg_select_action_shape, ddpg_select_action_no_grad_shape, ddpg_update_critic_runs_without_crash (NaN guard), ddpg_update_actor_modifies_w2 (mutates w2 in place), ddpg_twin_critic_does_not_collape_on_seeded_inputs, ddpg_soft_update_polyak, continuous_replay_buffer_push_and_len, continuous_replay_buffer_overflow_wraps, deterministic_policy_tanh_squash_clips, deterministic_policy_action_range, deterministic_policy_random_init, qnet_continuous_forward_shape, qnet_continuous_zero_input_zero_weight, ddpg_train_smoke_pendulum_like_env (end-to-end 4-step smoke train), ddpg_select_action_is_deterministic.

    #v0.55.0 — TD3 (Twin Delayed DDPG, Fujimoto 2018)

    • mbt/td3.mbt (NEW) — TD3Agent with actor + twin Q-critics (q1/q2) + twin target Q-critics + target actor + 5 Polyak-averaged target nets + Adam optimizers. Adds policy_delay=2, target_noise_std=0.2, target_noise_clip=0.5 to the DDPG base.
    • td3_smoothed_target_action(action, noise_std, clip_range, rng) — clip(action + clip(standard_normal, -c, c), -clip_range, clip_range) where c = noise_std, applied to the target-actor action in the twin-critic TD target.
    • td3_actor_update(actor, critic1, critic2, target_q1, target_q2, batch, lr) — extracted from ddpg_update_actor (same body, separate name for dispatching clarity); closed-form critic gradient with sign(ReLU_out) gate.
    • td3_soft_update(target_net, source_net, tau) — Polyak averaging over all 5 networks (actor, q1, q2, target_actor, target_q1, target_q2).
    • td3_update_step(agent, batch, lr_actor, lr_critic) — twin-critic TD target y = r + γ·min(q1', q2'), critic update every step, actor + target-net soft update only when step % policy_delay == 0.
    • td3_select_action(state, actor, exploration_noise_std, action_max, rng) — action + Gaussian noise clipped to [-action_max, action_max].
    • train_td3(agent, env_step, n_episodes, max_steps, ...) -> Array[Float] — closure-based env_step callback (state, action) -> (next_state, reward, done), returns per-episode total rewards.
    • 8 tests: td3_select_action_shape, td3_select_action_no_grad_shape, td3_smoothed_target_action_clip, td3_smoothed_target_action_noise, td3_soft_update_polyak, td3_update_step_twin_critic_td_target, td3_update_step_delayed_actor_update_skips_when_count_lt_delay (verifies the skip path), td3_train_smoke_pendulum_like_env.

    #v0.56.0 — Model + optimizer checkpoint serialization

    • mbt/serialize.mbt (NEW) — pure-functional String round-trip for DQN / DDPG / TD3 model parameters + Adam optimizer state. Pure-functional API (no file I/O coupling; caller handles persistence via MoonBit @fs or piping to moon run).
    • Lightweight SNNCKPT v1 header format with key=value lines, no nested objects. Each model is one block; combined checkpoint inlines model + optimizer sub-blocks via escape sequences (\n → \\n, \ → \\).
    • 1D arrays encoded as [v0;v1;...] (semicolon-separated); 2D arrays as [r0c0;r0c1|r1c0;r1c1;...] (semicolons inside rows, pipes between rows). Both separators are non-numeric and never appear in Float::to_string output, so the format is safe to embed inside the key=value\n line-oriented header.
    • Float encoding is bit-exact via Float::reinterpret_as_int + Float::reinterpret_from_int (NOT Float::to_string / parse_float — initial attempt failed 4 tests on ULP drift because MoonBit 0.1.20260920's default Float printer truncates to ~6 digits, which loses last-bit precision for values like 0.1F, 0.01F, -3.14159F). Single-Float fields (QNetworkContinuous.b2, action_low, action_high) also use the same bit-exact Int encoding.
    • Public API: serialize_linear_qnet / deserialize_linear_qnet, serialize_qnetwork_continuous / deserialize_qnetwork_continuous, serialize_deterministic_policy / deserialize_deterministic_policy, serialize_adam_state / deserialize_adam_state, serialize_qnet_checkpoint / deserialize_qnet_checkpoint (returns (LinearQNet, AdamState, Int) tuple).
    • Deep-copy invariant: the deserialized model has freshly allocated arrays, so subsequent mutation of either side is independent (verified in serialize_linear_qnet_independent_copy + serialize_qnet_checkpoint_independent_copies).
    • 12 tests: round-trip for each model + AdamState (zero state + post-update state with step > 0) + combined checkpoint + header-tag presence + action range preservation + negative-float preservation. b2 mutation tests skipped because QNetworkContinuous.b2 is mut b2 and read-only from outside the defining module in blackbox tests; the default-value round-trip still covers the encode/decode path.
    • AdamState fields are not pub, so tests use adam_init / adam_update_arrays / adam_next_step (the public API) to construct states with step > 0, and let opt1 = decode_adam_state(s) to get the deserialized state without mutating it.

    #Recent additions (v0.57–v0.60, 2026-10-01) — Batch C (recurrent / POMDP RL)

    #v0.57.0 — SequenceReplayBuffer for recurrent RL

    • mbt/sequence_replay_buffer.mbt (NEW) — per-step continuous replay buffer with T-step rollout sampling for LSTM/GRU actor-critic RL. Stores (state, action, reward, next_state, done) in flat row-major Float arrays.
    • sample_seq_batch(batch_size, seq_len, rng) returns 7 parallel arrays: obs_seq [batch × seq_len × state_dim], action_seq [batch × seq_len × action_dim], reward_seq [batch × seq_len], next_obs_seq [batch × seq_len × state_dim], done_seq [batch × seq_len], terminal_mask [batch × seq_len] (1.0F at slots following a done boundary so caller can break BPTT), hidden_init [batch] (zeros — caller supplies hidden dim).
    • Padding behaviour: window crossing a done boundary zero-fills the rest and sets terminal_mask. Empty buffer (size < seq_len) returns all-zero windows with terminal_mask set everywhere (no panic).
    • 9 tests: new/len, push, overflow wrap, reset, sample shape, empty-buffer zero-pad, window-crosses-done terminal mask, reproducible under seed, dones propagate in window.

    #v0.58.0 — GRUDeterministicPolicy (recurrent actor for POMDPs)

    • mbt/gru_deterministic_policy.mbt (NEW) — recurrent deterministic actor for partially-observable continuous control. Architecture: MLP w1 (state→hidden) → ReLU → GRU cell (over time) → MLP w2 (hidden→action) → tanh squash to [action_low, action_high]. GRU hidden dim = MLP hidden dim so the ReLU-projected state and the GRU input/output match.
    • GRUDeterministicPolicy::new(state_dim, action_dim, gru_hidden, action_low, action_high, seed) — Xavier-normal init for MLPs (He-style sqrtf(2/fan_in) for ReLU), reuses GruCellParam::new(gru_hidden, gru_hidden, seed+4UL) for the recurrent cell, zero biases.
    • gru_deterministic_policy_step(policy, obs, hidden) → (action, hidden_next) — single-step inference. gru_deterministic_policy_seq_forward(policy, obs_seq, seq_len, hidden_init) → (action_seq, final_hidden) — flat row-major [seq_len × action_dim] output.
    • 8 tests: constructor shape, action in squash range, deterministic under seed, different seeds differ, hidden state evolves, seq_forward shape, seq_forward matches single-step chain, deterministic under seed at seq level.

    #v0.59.0 — GRUQNetworkContinuous (recurrent critic for POMDPs)

    • mbt/gru_qnetwork_continuous.mbt (NEW) — recurrent continuous-action Q-network for partially-observable environments. Architecture: MLP w1 (concat(state, action)→hidden) → ReLU → GRU cell → MLP w2 (hidden→scalar Q).
    • GRUQNetworkContinuous::new(state_dim, action_dim, hidden, seed) — Xavier-normal init for MLPs, GRU via GruCellParam::new(hidden, hidden, seed+4UL). mlp_w2 is 1×hidden (scalar Q output) + scalar mut mlp_b2 : Float bias.
    • gru_qnetwork_continuous_step(qnet, state, action, hidden) → (q, hidden_next) — single-step inference (returns Float, not array). gru_qnetwork_continuous_seq_forward(qnet, state_seq, action_seq, seq_len, hidden_init) → (q_seq, final_hidden) — flat row-major [seq_len] output.
    • 8 tests: shape, deterministic under seed, different seeds differ, hidden state evolves, zero-zero-zero → 0.0F, seq_forward shape + deterministic, seq_forward matches single-step chain, scalar return type.

    #v0.60.0 — DDPG_GRU agent (recurrent actor + twin recurrent critics)

    • mbt/gru_ddpg.mbt (NEW) — DDPG agent with GRUDeterministicPolicy actor + twin GRUQNetworkContinuous critics + 5 Polyak-averaged target networks (actor_target, critic1_target, critic2_target). Recurrent hidden state threaded through the twin-critic TD update.
    • DDPG_GRU::new(state_dim, action_dim, hidden, action_low, action_high, gamma, tau, exploration_noise, seed) — each of the 6 networks gets a distinct seed (sources at seed/seed+1/seed+2, targets at seed+100/seed+101/seed+102) so Polyak-averaged targets start far from sources.
    • ddpg_gru_select_action(agent, obs, hidden, noise_std, rng) — single-step inference with optional Gaussian exploration noise clipped to [action_low, action_high]. noise_std=0 recovers the deterministic policy output.
    • ddpg_gru_compute_td_target_seq(agent, next_obs_seq, next_act_seq, reward_seq, done_seq, seq_len) — twin-critic TD target forward: y_t = r_t + γ·(1 - done_t)·min(q1_target(next), q2_target(next)). No parameter update yet — BPTT-driven update (matvec_backward over T steps + gru_cell_backward over T steps + sign(ReLU_out) gate) is deferred to a follow-up because it's a significant incremental body of work.
    • gru_deterministic_policy_soft_update(target, source, tau) + gru_qnetwork_continuous_soft_update(target, source, tau) — Polyak averaging that updates the MLP weights + biases AND the GRU sub-parameters (w_z, w_r, w_n, b_z, b_r, b_n). ddpg_gru_soft_update(agent) calls all three.
    • pub(all) struct DDPG_GRU with mut tau : Float — required for cross-file record-update { ..agent, tau: 1.0F } (MoonBit's blackbox test rule: mut field is only mutable if the containing struct is pub(all)).
    • 10 tests: 6 networks built + hyperparameters stored, source/target networks start distinct, deterministic with noise_std=0, noise clipped to action range, hidden state evolves, TD target shape + finiteness, zero rewards + γ=0 → zero target, tau=1 → actor_target exactly matches actor, tau=0 → actor_target unchanged, tau=1 → critics also soft-updated.

    #Running

    # Tests cd moonbit-snn/mbt moon test # chain.jl example moon run examples/chain/main.mbt

    #Bit-exact contract

    For any example, given the same parameters and dt (default 0.125f0) and Float32 accumulator types, the MoonBit port produces the same spike train (:fire recorded buffer) and the same recorded time series (:v, :ge, :gi, ...) as the Julia run, up to last-bit.

    Float ordering matches forward! / integrate! / update_synapses! in SNNModels.jl exactly. Where the Julia source uses v .+=dt/蟿m * (...), we keep the same operation order 鈥?no algebraic rearrangements, no vectorisation tricks that change the order of operations.

    Known gap (TODO v0.0.2): Julia's Xoshiro(seed::Integer) uses SHA-256 over the seed bytes. Our MoonBit RNG uses splitmix64 for integer seeds; sequences diverge from Julia for integer seeds. For bit-exact comparison, set the seed via Xoshiro::from_state(s0, s1,s2, s3) with raw UInt64s (bypassing the integer-seed hashing).

    #Layout

    mbt/ 鈹溾攢鈹€ moon.mod # module: riantr/snn_mbt 鈹溾攢鈹€ moon.pkg 鈹溾攢鈹€ README.md 鈹溾攢鈹€ units.mbt # 30+ Float32 unit constants 鈹溾攢鈹€ units_test.mbt 鈹溾攢鈹€ time.mbt # Time struct 鈹溾攢鈹€ time_test.mbt 鈹溾攢鈹€ rng.mbt # Xoshiro256++ RNG 鈹溾攢鈹€ rng_test.mbt 鈹溾攢鈹€ neuron_if.mbt # IF neuron + DoubleExpSynapse state 鈹溾攢鈹€ neuron_if_test.mbt 鈹溾攢鈹€ neuron_adex.mbt # AdEx neuron + AdExPostSpike + AdExParameter::with_vr 鈹溾攢鈹€ neuron_iz.mbt # IZ neuron + IZParameter::fs 鈹溾攢鈹€ neuron_hh.mbt # HH neuron 鈹溾攢鈹€ neuron_ml.mbt # Morris-Lecar neuron 鈹溾攢鈹€ neuron_poisson.mbt # Poisson population 鈹溾攢鈹€ neuron_wc.mbt # Wilson-Cowan rate neuron 鈹溾攢鈹€ neuron_wc_test.mbt 鈹溾攢鈹€ stimulus_poisson.mbt # PoissonStimulus (Knuth) 鈹溾攢鈹€ stimulus_poisson_layer.mbt # PoissonLayer (N sources projecting sparsely to IF) 鈹溾攢鈹€ stimulus_current.mbt # CurrentStimulus + CurrentStimulusArray 鈹溾攢鈹€ connection_spiking.mbt # SpikingSynapse (CSR, delay_dist + pending queue) 鈹溾攢鈹€ connection_spiking_iz.mbt # SpikingSynapseIZ 鈹溾攢鈹€ connection_spiking_hh.mbt # SpikingSynapseHH 鈹溾攢鈹€ connection_compartment.mbt # CompartmentSynapseBall/Tripod (multi-compartment routing) 鈹溾攢鈹€ connection_receptor_tripod.mbt # ReceptorSynapseTripod (multi-receptor soma/dendrite routing) 鈹溾攢鈹€ receptor.mbt # NMDA primitives (Receptor / Receptors / gating / 2-state ODE) 鈹溾攢鈹€ connection_spiking_test.mbt # delay_dist tests 鈹溾攢鈹€ connection_rate.mbt # RateSynapse 鈹溾攢鈹€ sparse_matrix.mbt # SparseMatrixCSR + ConnectRule 鈹溾攢鈹€ compose.mbt # HeterogeneousModel + AnyPop/AnyStim 鈹溾攢鈹€ vecplot.mbt # text dump + ascii_plot 鈹溾攢鈹€ stdp.mbt # STDP (Gerstner 1996, MexicanHat, AntiSymmetric, STDPEntryKind) 鈹溾攢鈹€ stdp_test.mbt 鈹溾攢鈹€ stp.mbt # Markram STP (event-based update_traces! + 蟻 broadcast) 鈹溾攢鈹€ stp_test.mbt 鈹溾攢鈹€ sim.mbt # Model, Monitor, sim_for 鈹溾攢鈹€ examples/ 鈹? 鈹斺攢鈹€ chain/ 鈹? 鈹溾攢鈹€ main.mbt # port of SpikingNeuralNetworks.jl/examples/chain.jl 鈹? 鈹斺攢鈹€ moon.pkg 鈹斺攢鈹€ _build/ # moon build artifacts

    #Network experiments summary

    Examples marked 鈿?partial demonstrate qualitative network behaviour matching the Julia run, but are not bit-exact (we scale population sizes and rates down to keep sim times tractable).

    ExampleNetwork sizePopulation scalesBit-exact?Notes
    chain.jl3 IFunchanged鉁?tiny; rates bit-exact
    if_neuron.jl1 IFunchanged鉁?(1 s vs 10 s)Knuth Poisson slow at high rates
    if_net.jl32E + 8I100脳 鈫?鉁?original is 3200+800
    ei_inhibition.jl5 + 5 IFunchanged鉁?tiny; inhibition observable
    cuba_net.jl80E + 20I100脳 鈫?鉁?qualitative: drive on鈫抐ire, drive off鈫抯ilent
    coba_net.jl80E + 20I100脳 鈫?鉁?delay_dist now implemented (v0.10.3); spike timing differs from Julia's true 0.8ms delay because our loop only checks delivery once per dt
    potjans_diesmann.jl120E + 40I100脳 鈫?鉁?simplified to 4 layers
    lkd2014.jl40E + 10I100脳 鈫?鉁?IF instead of AdExSinExpParameter
    festa2024.jl80E + 20I10脳 鈫?鉁?structured inhibition + STDP
    afferent_response.jl40E + 10I100脳 鈫?鉁?single 谓_a (no sweep)
    tsodyks.jl8 AdEx + 4 IFunchanged鉁?scaled-down paradoxical-effect
    hh_net.jl8E + 4I HHunchanged鉁?tiny; HH gating dynamics

    Known gaps preventing full bit-exactness:
    • delay_dist (uniform/normal synaptic delays) 鈥?鉁?done in v0.10.3 via per-connection delays array + pending-event queue. Spike timing is NOT bit-exact with Julia's true sub-step scheduling (our loop only checks delivery once per dt).
    • AdExSinExpParameter (used in LitwinKumar) 鈥?we use default IF.
    • AdExReceptorParameter (used in Mongillo2008) 鈥?needed for full Mongillo2008 working memory model. STP itself (MarkramSTPParameter) is 鉁?done in v0.10.4; the Mongillo2008 model requires the AdExReceptorParameter + selective sub-population targeting on top.
    • Multi-receptor synapses (NMDA / GABA_A / GABA_B for HH_NMDA, Tripod, BallAndStick, Multipod).
    • Multi-compartment neurons (Tripod/BallAndStick/Multipod).
    • HetRec (heterogeneous-recurrent layer with dendritic trees).

    For each of these, a scaled-down qualitative port works (afferent_response, festa2024, etc.) but the Julia run-time parameters are not reproduced bit-for-bit.

    #Recent additions (v0.58.0 - v0.155.0, 2026-10-05)

    Sixteen batches shipped between v0.57.0 and v0.155.0, all moon check clean (0 errors). Tests are blocked by the Windows CreateProcessW 32K command-line limit on moon test: the package has grown from 511 to 998 .mbt files, so the limit is now hit harder than when it first appeared past v0.61.0. Verification runs through moon check plus the two harnesses in the Verification harness section below; the package holds 1939 test blocks across 383 test files and 77 examples.

    #Batch C follow-up (v0.58.0 - v0.61.0): BPTT boundary handling

    • sample_seq_batch_at helper for arbitrary-window sequence replay buffer sampling with terminal_mask for proper hidden-state zeroing at episode boundaries.
    • 10 BPTT boundary tests added at v0.61.0 (1801 → 1811 tests).

    #Batch D (v0.61.0 - v0.63.0): Recurrent deterministic actor-critic (LSTM)

    • v0.61.0 LSTMDeterministicPolicy: recurrent actor for POMDPs. state → MLP w1 → ReLU → LSTM cell → MLP w2 → tanh squash → action.
    • v0.62.0 LSTMQNetworkContinuous: recurrent critic. [state; action] → MLP w1 → ReLU → LSTM cell → MLP w2 → scalar Q.
    • v0.63.0 LSTM_DDPG: full DDPG agent wrapping actor + twin critics + 5 Polyak targets. 6 distinct seeds (offset by +100UL for targets).

    #Batch E (v0.64.0 - v0.67.0): BPTT-driven recurrent critic/actor updates

    • v0.64.0 gru_ddpg_update_critic_seq: GRU critic T-step BPTT.
    • v0.65.0 gru_ddpg_update_actor_seq: GRU actor BPTT (finite-difference ∂Q/∂a with eps=1e-3).
    • v0.66.0 lstm_ddpg_update_critic_seq: LSTM critic T-step BPTT.
    • v0.67.0 lstm_ddpg_update_actor_seq: LSTM actor BPTT.

    #Batch F (v0.68.0 - v0.71.0): Recurrent SAC (stochastic actor + twin critics)

    • v0.68.0 GRUSACActor: stochastic actor — state → MLP w1 → ReLU → GRU cell → two MLP branches (mean + log_std) → tanh squash + Gaussian sampling. log_std clamped [-20, 2] (Haarnoja 2018).
    • v0.69.0 LSTMSACActor: parallel LSTM variant.
    • v0.70.0 SAC_GRU: stochastic actor + twin recurrent critics + auto-alpha
      • 5 Polyak targets. target_entropy = -action_dim default.
    • v0.71.0 SAC_LSTM: parallel.

    #Batch G (v0.72.0 - v0.75.0): Gated Transformer-XL (GTrXL) recurrent memory

    • v0.72.0 GTrXLBlock: gated FFN residual primitive. y = sigmoid(W_gate · x) ⊙ FFN(x) + x (Parisotto et al. 2020). Gate init std scaled by 0.1 so initial gate ≈ sigmoid(0) ≈ 0.5.
    • v0.73.0 GTrXLDeterministicPolicy: recurrent actor with GTrXL block as memory.
    • v0.74.0 GTrXLQNetworkContinuous + GTrXL_DDPG: twin critics + 5 Polyak targets; 9 tensors per network, 45 total.
    • v0.75.0 GTrXLSACActor + SAC_GTrXL: stochastic SAC + auto-alpha; 12 tensors per target, 60 total.

    #Batch H (v0.76.0 - v0.79.0): Decision Transformer (offline RL via sequence modeling)

    • v0.76.0 DTEmbeddings: 3-modality tokenization (rtg/state/action)
      • timestep lookup. Per-token = Linear_rtg(R_t) + Linear_state(s_t) + Linear_action(a_t) + Embed_ts(t).
    • v0.77.0 DecisionTransformer: full DT = DTEmbeddings + GTrXLBlock backbone + action head.
    • v0.78.0 TrajectoryBuffer: offline RL replay + returns-to-go (R_t = r_t + γ·R_{t+1}, terminal zeros bootstrap) + sample_trajectory_window for window batching.
    • v0.79.0 DecisionTransformerTrainer: MSE loss + per-element gradient
      • SGD step on action head only. Embeddings + backbone frozen (BPTT through GTrXL deferred).

    #Batch I (v0.80.0 - v0.83.0): Trajectory Transformer (planning via conditional sequence modeling)

    • v0.80.0 TTEmbeddings + TrajectoryTransformer: 4-modality (state/action/reward/done) + 4 prediction heads (next_state, reward, done, value) on GTrXL backbone.
    • v0.81.0 BeamSearchPlanner: K-beam expansion via TT dynamics + uniform-random action sampling + value scoring + top-K selection over horizon H. Score = accumulated discounted reward + γ^t · value(t).
    • v0.82.0 TrajectoryTransformerTrainer: total MSE = MSE(next_state) + MSE(reward) + MSE(done) + MSE(value); SGD on all 4 heads.
    • v0.83.0 TrajectoryTransformerAgent: full pipeline — append transition → sliding history → beam_search_plan → return best first action.

    #Batch J (v0.85.0 - v0.88.0): time-series forecasting

    • v0.85.0 ARIMA enhancements: Yule-Walker stationarity estimate, stateful fit/forecast (forecasts can be chained), confidence intervals.
    • v0.86.0 LSTMForecaster: full BPTT + SGD training over a sliding window.
    • v0.87.0 GRUForecaster: the same interface over a GRU, deliberately parallel to v0.86 so the two forecasters are interchangeable.
    • v0.88.0 TimeSeriesEnsemble: inverse-variance weighted average of the ARIMA / LSTM / GRU forecasts.

    #Batch K (v0.89.0 - v0.92.0): Neural ODE

    • v0.89.0 ODEFunc: the dynamics field f(t, x) as a first-class primitive, so an ODE model is anything that supplies one.
    • v0.90.0 ODESolver: fixed-step Euler and Heun integrators.
    • v0.91.0 AdjointSensitivity: adjoint-method gradients, i.e. O(1) in trajectory length rather than O(T) BPTT.
    • v0.92.0 NeuralODEAgent: forward + adjoint backward + SGD, closing the loop on the batch.

    #Batch L (v0.93.0 - v0.96.0): meta-learning

    • v0.93.0 TaskSampler: MAML task family + meta-batched sampling.
    • v0.94.0 MAML: the FOMAML variant.
    • v0.95.0 FOMAML: the single-inner-step form split out on its own.
    • v0.96.0 Reptile: interpolation-based meta-update (no second derivative, so it composes with any inner optimiser).

    #Batch M (v0.97.0 - v0.100.0): causal inference

    • v0.97.0 LinearGaussianSCM: DAG + structural equations + sampling.
    • v0.98.0 DoCalculus: interventions + Monte Carlo ATE.
    • v0.99.0 CounterfactualReasoning: Pearl's three-step procedure.
    • v0.100.0 CausalEffectEstimator: ATE + CATE + bootstrap CI.

    #Batch N (v0.101.0 - v0.104.0): normalizing flows

    • v0.101.0 AffineCouplingLayer: the Real NVP core block.
    • v0.102.0 RealNVP: stacked coupling + fixed permutation.
    • v0.103.0 Glow: Real NVP + ActNorm + 1x1 convolution permutation.
    • v0.104.0 NormalizingFlowModel: the full flow with a prior and an exact density.

    #Batch O (v0.105.0 - v0.108.0): variational autoencoders

    • v0.105.0 ELBO: the Evidence Lower Bound loss.
    • v0.106.0 GaussianReparameterization: the reparameterization trick.
    • v0.107.0 VAE: the full model.
    • v0.108.0 IWAE: the importance-weighted bound (a tighter ELBO).

    #Batch P (v0.109.0 - v0.112.0): Vision Transformer

    • v0.109.0 PatchEmbedding: the patch linear projection.
    • v0.110.0 ViTBlock: LayerNorm + MHA + residual, then LayerNorm + MLP + residual.
    • v0.111.0 ViT: PatchEmbed + CLS token + positional encoding + N blocks + head.
    • v0.112.0 ViTTrainer: cross-entropy + SGD on the classification head.

    #Batch Q (v0.113.0 - v0.116.0): diffusion models

    • v0.113.0 DiffusionSchedule: linear and cosine beta schedules, q_sample, q_sample_pair.
    • v0.114.0 ScoreNetwork: MLP denoiser + sinusoidal time embedding.
    • v0.115.0 DDPM: schedule + score net + reverse sampling + loss.
    • v0.116.0 DDPMTrainer: forward + MSE loss logging.

    #Batch R (v0.117.0 - v0.120.0): energy-based models

    • v0.117.0 EnergyFunction: MLP scalar energy.
    • v0.118.0 LangevinSampler: finite-difference gradient + Langevin step + chain.
    • v0.119.0 EBM: energy + score + Langevin sampling.
    • v0.120.0 EBMTrainer: denoising score matching.

    #Batch S (v0.121.0 - v0.124.0): DCGAN

    • v0.121.0 DCGANGenerator: latent -> upconv stack -> tanh.
    • v0.122.0 DCGANDiscriminator: strided conv + LeakyReLU + spectral norm helper.
    • v0.123.0 DCGAN: the composite, with BCE D/G losses and the adversarial step.
    • v0.124.0 DCGANTrainer: mini-batch adversarial round + eval metrics.

    #Batch T (v0.125.0 - v0.128.0): WGAN-GP

    • v0.125.0 Fix: the DCGAN generator emitted 16x16; corrected to the canonical 4 -> 8 -> 16 -> 32 upconv progression.
    • v0.126.0 WCritic: Wasserstein critic loss + Lipschitz weight clipping.
    • v0.127.0 GradientPenalty: WGAN-GP interpolation + finite-difference input gradient + the ||grad|| - 1 squared term.
    • v0.128.0 WGANTrainer: n_critic loop + Wasserstein/GP losses + eval.

    #Batch U (v0.129.0 - v0.132.0): metric learning

    • v0.129.0 EmbeddingNet: MLP embedding + L2 normalize + pairwise and cosine distances.
    • v0.130.0 ContrastiveLoss: contrastive + triplet + hard-negative + N-pair.
    • v0.131.0 SiameseNet: shared-weight twin/triplet embedding.
    • v0.132.0 SiameseTrainer: triplet batch builder + hard-negative step
      • Recall@K.

    #Batch V (v0.133.0 - v0.136.0): capsule networks

    • v0.133.0 Capsule primitives: squash activation, norm, dot, margin loss.
    • v0.134.0 PrimaryCapsule: shared-conv primary capsule bank.
    • v0.135.0 DynamicRouting: iterative agreement-based routing with coefficients.
    • v0.136.0 CapsuleNetwork: ReLUConv + PrimaryCapsule + routing + margin loss.

    #Batch W (v0.137.0 - v0.140.0): graph neural networks, forward pass

    • v0.137.0 Graph: COO edge list + scatter sum/mean/max + GCN adjacency normalisation.
    • v0.138.0 MessagePassing: GCNLayer + MPnnLayer (the Gilmer formulation).
    • v0.139.0 GraphAttention: GAT single head + the multi-head stack with ELU.
    • v0.140.0 GraphClassifier: mean/sum/max readout + MLP head + graph-level cross-entropy and accuracy.

    #Batch X (v0.141.0 - v0.144.0): graph architectures

    • v0.141.0 GIN: unweighted sum aggregation + learnable-eps self term
      • 2-layer MLP.
    • v0.142.0 PNA: mean/max/min/std + degree scaler. Also fixed a negative-seed bug in scatter_max and added scatter_min.
    • v0.143.0 Set2Set: LSTM-query attention readout, plus Set2SetClassifier with GIN/PNA backbone dispatch.
    • v0.144.0 EdgeGNN (RelationalConv with edge features) + H2GCN separated ego/neighbour/high-order spaces + homophily diagnostics.

    #Batch Y (v0.145.0 - v0.148.0): graph backward pass

    • v0.145.0 graph_backward.mbt: scatter sum/mean backward (edge-list gradient accumulation) + max/min argmax routing + GraphLinearGrad
      • graph_linear_backward + SGD step.
    • v0.146.0 GIN + MPNN backward: GINLayerGrad / MPnnGrad bundles and edge-list gradient routing.
    • v0.147.0 GCN backward: gcn_layer_support shared with the forward, backward through the normalised edge weights, graph_cross_entropy_grad.
    • v0.148.0 PNA backward including the standard-deviation reducer derivative, TrainableGraphNet dispatch, and graph_net_train_step (node-level and graph-level CE).

    #Batch Z (v0.149.0): gradient-check harness

    Central-difference checks for dL/dh and dL/dW, plus a negative control, and they immediately found four real defects that moon check and code review had both passed: the four backward stack traversals ran forwards instead of backwards; g.feat_dim was used where layer.in_dim was meant (in the GIN, MPNN and PNA forward AND backward); the PNA reducer gradients were written one node at a time into a [n_nodes x dim] buffer, keeping only the last node; and GraphLinearGrad::zero aliased every row of d_w onto row 0.

    #Batch AA (v0.150.0 - v0.151.0): gate green

    • v0.150.0 Replaced the cross-entropy objective with a bounded quadratic one (0.5 * sum(out^2), whose gradient is exactly out), and absolute tolerance with a relative one. This overturned the previous round's conclusion that GIN was the worst architecture: GIN's relative error was in fact the smallest of all six configs.
    • v0.151.0 Added gradcheck_over_fraction, the kink-versus-bug discriminator (a ReLU kink perturbs 1-2 components, a wrong routing perturbs nearly all of them), and fixed the last two defects: the MPNN ReLU mask was taken on the self_pre branch alone instead of on the sum, and a leftover let dim = g.feat_dim in pna_layer_backward.

    #Batch AB (v0.152.0 - v0.153.0): parameter gradients and the GAT head

    • v0.152.0 dL/dW wired into the gate via trainable_net_param_grad_from_dout and gradcheck_param_relative, with a corrupt~ factor so the negative control is real (a corrupted gradient produces max_diff 2490 against a tolerance of 74.8). This matters because dL/dW accumulates over NODES inside the linear backward while dL/dh accumulates over EDGES outside it, so one can pass while the other fails.
    • v0.153.0 gat_backward.mbt: the GAT head backward. GAT is the only reducer here whose score reads BOTH endpoints of an edge, so the score gradient fans out to two ends and the family needed two new scatter directions (scatter_add_by_src / scatter_add_by_dst).

    #Batch AC (v0.154.0 - v0.155.0): multi-head GAT and end-to-end training

    • v0.154.0 Multi-head GraphAttention backward: the concatenated upstream gradient has to be un-interleaved per head, all heads of a layer share their input so their input gradients sum, and the ELU sits BETWEEN a head and the next layer, so its derivative multiplies at the head's own pre-activation. gat_layer_backward's elu flag had been a placeholder, i.e. a hidden head asking for the mask silently received an unmasked gradient. Three latent bugs in the never-called stack forward were fixed at the same time: GraphAttention::new gave layer l > 0 an input width of dims[l] when concatenation requires num_heads * dims[l]; a single-layer stack ran layer 0 twice; and the head output was indexed with the layer's in_dim as its row stride instead of its own out_dim. The gate separated them cleanly: the configuration where in_dim == out_dim passed and the one where they differ was off by 118%.
    • v0.155.0 gnn_train_demo.mbt: an end-to-end transductive two-community fixture, a backbone + GraphLinear head composite training step, training curves, and the baselines needed to make the result mean something. It found a defect that had shipped for six versions: graph_cross_entropy had an inverted sign on its logf(sum_exp) term, so it returned a NEGATIVE loss for every confident and correct prediction. Its gradient was correct throughout, so training worked and accuracy rose; only the reported loss moved the wrong way. It survived because the gradient gate deliberately checks a bounded quadratic objective instead of cross-entropy, and a gate that exercises one objective cannot see a bug in another. graph_cross_entropy is fixed and gradcheck_ce_consistency now pins the loss against its own gradient.

    #Verification harness

    Gradients being correct and the resulting model being useful are different properties, and this package now checks both.

    Two commands, both runnable from the repository root:

    & ".\verify\verify_gnn_grads.ps1" # gradient gate & ".\verify\run_gnn_train_demo.ps1" # end-to-end training demo

    Both flip "is-main": true into mbt/moon.pkg for the duration of the run and restore it in a finally block. This is necessary rather than stylistic: a freshly created sub-package cannot resolve any symbol from the parent module in moon 0.1.20260920 (not even Graph), and moon test cannot link at all, because the package has 511 .mbt files and Moon inlines every source path into the Windows command line, which caps at 32K (CreateProcessW). The library-side functions (gnn_gradcheck.mbt, gnn_paramcheck.mbt, gnn_train_demo.mbt) are the durable artifact; the runners under verify/ are thin drivers.

    The gradient gate uses three criteria, and each exists for a reason:

    • Relative tolerance. max_diff <= tol * max|analytic|. Absolute tolerances cannot rank architectures whose gradients span three orders of magnitude on the same graph.
    • gradcheck_over_fraction, the count of components deviating by more than 1% of the gradient scale. max_diff alone cannot separate a ReLU kink from a wrong routing, and a scale sweep cannot either; only the count can.
    • Negative controls. Every measured quantity is also checked against a deliberately corrupted version, because a checker that reports PASS for every input is worthless.

    Current state: 8 dL/dh configurations, 8 dL/dW configurations, 4 multi-head GAT stack rows, a deterministic ELU-mask row, and the cross-entropy consistency section. VERDICT: PASS, with moon check --target native at 0 errors.

    Across Batches Z, AA and AC the harness found 10 real defects that moon check and code review had both passed, plus the 3 latent multi-head stack bugs in Batch AC. Two of the checks exist only because of specific failures rather than by design:

    • gat_elu_coverage reports how many hidden-layer pre-activations are at or below zero. ELU's derivative is exactly 1 on its whole positive half-line, so if every pre-activation is positive then deleting the mask outright produces bit-identical numbers and the rows pass without ever testing ELU. The gate said so, and a separate deterministic row (whose load-bearing pre-activations, -1 and -0.5, are known by arithmetic) now carries that claim.
    • The saturated CE probe is excluded from the finite-difference check and kept for the value check, because at the loss minimum the max-subtraction makes the value insensitive to the largest logit below Float32 resolution, so a central difference there measures nothing.

    #Reference source

    The Julia source has been cloned to moonbit-snn/refs/SNNModels.jl/ and moonbit-snn/refs/SNNUtils.jl/ for line-by-line reference. The main umbrella package SpikingNeuralNetworks.jl/src/SpikingNeuralNetworks.jl is a thin re-export layer over the subpackages; the actual neuron/synapse implementations live in SNNModels.jl/.

    ⚠️ Package is too large to be displayed

    This package contains over 1567 items.

    Source Files