Skip to content

Evolutionary Substrate

Open-ended evolution of stochastic-computing neural networks: self-replicating organisms whose genomes encode topology, neuron kinetics, and plasticity parameters, mutate and recombine under safety invariants, speciate by genomic distance, migrate between islands, and deploy onto FPGA tiles. The fitness function accepts either a pure-software proxy or the closed-loop wet-lab MEA hook exposed by :func:sc_neurocore.bioware.bioware.mea_fitness_hook.

Python
from sc_neurocore.evo_substrate.evo_substrate import (
    Genome, NeuronGene, TopologyGene, PlasticityGene,
    MutationEngine, CrossoverEngine, FitnessEvaluator,
    ReplicationEngine, OrganismEmitter, SafetyBounds,
    TileDeploymentTracker, HallOfFame, IslandModel,
    NoveltyArchive, FormalSafetyGuard, ParetoFront,
    CPPNGenome, ComplexityTracker, BloatPenalizer,
    ExtinctionDetector, LineageTracker, AgeRegulator,
    TournamentSelector, EvoStatisticsTracker,
    HWFitnessCollector, CoevolutionArena,
    assign_species, population_diversity, genomic_distance,
    dominates, shared_fitness, compute_bloat, genome_complexity,
    genome_diff,
)

1. Mathematical formalism

1.1 Genome as a fixed-length vector

A :class:Genome serialises to a vector $\mathbf{g} \in \mathbb{R}^{19}$ via

$$ \mathbf{g} = \bigl[\;\mathbf{t}\;|\;\mathbf{n}\;|\;\mathbf{p}\;\bigr], $$

where $\mathbf{t} \in \mathbb{R}^{5}$ is the :class:TopologyGene block $(N_{\text{neurons}},\, N_{\text{layers}},\, c,\, r_{\text{rec}},\, L_{\text{bits}})$, $\mathbf{n} \in \mathbb{R}^{8}$ is the :class:NeuronGene block $(\tau_{\text{fast}},\, \tau_{\text{work}},\, \tau_{\text{deep}},\, \theta,\, \gamma,\, \delta_{\text{conf}},\, \kappa,\, w_{\text{inh}})$, and $\mathbf{p} \in \mathbb{R}^{6}$ is the :class:PlasticityGene block $(\eta_{\text{STDP}},\, \tau_{+},\, \tau_{-},\, U_{\text{STP}},\, \eta_{\text{hom}},\, s_{\text{meta}})$.

The :meth:Genome.compute_id fingerprint is the first 12 hex digits of SHA-256 over the raw bytes of $\mathbf{g}$, giving a collision-safe content-addressable id. Round-trip is exact via :meth:Genome.from_vector.

1.2 Point mutation (Gaussian, multiplicative)

For each coordinate $i$, with probability $p_{\text{point}}$ (default 0.2),

$$ g_i \leftarrow g_i + \mathcal{N}(0,\, \sigma_{\text{point}}^{2}) \cdot \bigl(|g_i| + \varepsilon\bigr), $$

where $\sigma_{\text{point}} = 0.05$ and $\varepsilon = 10^{-8}$. The multiplicative coupling keeps the relative step size constant across parameters with very different magnitudes (e.g. $\tau_{\text{deep}}=10^{4}$ vs $\gamma=0.2$).

1.3 Structural / duplication / swap mutations

  • Structural. $N_{\text{neurons}} \leftarrow N_{\text{neurons}} + \delta$ with $\delta \in {-2, -1, 1, 2}$, clamped to $[N_{\min},\,N_{\max}] = [4,\,1024]$; connectivity $c$ receives a small Gaussian kick and is clamped to $[0.01,\,1]$.
  • Duplication. Layer count increases by 1 (capped at 10), neuron count scaled by $1.5$ (capped at $N_{\max}$). This models whole-gene duplication — the dominant driver of complexity growth in biological evolution.
  • Swap. $\tau_{\text{fast}}$ and $\tau_{\text{work}}$ are swapped, a simple inversion-like operator that probes time-scale re-assignment without changing the vector's L2 norm.

Mutation type is drawn via cumulative-probability selection using the rates in :class:MutationConfig (structural 0.05, duplication 0.01, swap 0.02, else point).

1.4 Uniform crossover

Two parents $\mathbf{a},\mathbf{b} \in \mathbb{R}^{19}$ produce a child $\mathbf{c}$ by coordinate-wise Bernoulli selection:

$$ c_i = \begin{cases} a_i & \text{if } u_i < 0.5 \ b_i & \text{otherwise} \end{cases}, \quad u_i \sim \mathcal{U}(0,1). $$

This is the standard Syswerda uniform operator (Syswerda, 1989); gene-block boundaries (topology | neuron | plasticity) are respected because each block occupies a contiguous slice of the vector.

1.5 Genomic distance (Adam-like normalised L1)

$$ d(\mathbf{a},\mathbf{b}) = \frac{1}{D} \sum_{i=1}^{D} \frac{|a_i - b_i|}{|a_i| + |b_i| + \varepsilon}, \qquad D = 19. $$

This normalised metric is scale-invariant, which is crucial because $\tau_{\text{deep}}$ and $\gamma$ differ by five orders of magnitude. $d=0$ means clones; $d$ approaches 1 for maximally different genomes.

1.6 NEAT-style speciation

:func:assign_species partitions the population greedily:

$$ \text{species}(o) = \begin{cases} k, & \min_k d\bigl(\mathbf{g}o,\, \mathbf{g} \ k_{\text{new}}, & \text{otherwise} \end{cases} $$}\bigr) < \theta_{\text{sp}

where $r_k$ is the representative genome of species $k$ and $\theta_{\text{sp}}$ is the speciation threshold (default 0.3). The first organism placed in a species becomes its representative — this matches Stanley & Miikkulainen's NEAT algorithm (Stanley, 2002).

1.7 Composite fitness

:meth:FitnessResult.compute_composite combines three terms:

$$ F = w_{\text{acc}} \cdot A \;+\; w_{\text{en}} \cdot E \;+\; w_{\text{lat}} \cdot L, $$

with default weights $(0.5,\,0.3,\,0.2)$, where $A$ is the metrics-fn accuracy score and $E$, $L$ are hardware-cost proxies derived from the topology gene:

$$ E = \max!\left(0,\; 1 - 0.5 \cdot \tfrac{N_{\text{neurons}}}{1024} - 0.5 \cdot \tfrac{L_{\text{bits}}}{1024}\right), \qquad L = \max!\left(0,\; 1 - \tfrac{N_{\text{layers}}}{10}\right). $$

1.8 Pareto dominance for multi-objective selection

One fitness result dominates another iff it is at least as good on every objective and strictly better on at least one:

$$ \mathbf{f}a \succ \mathbf{f}_b \;\Leftrightarrow\; \bigl(\forall i:\, f\bigr) \wedge \bigl(\exists j:\, f_{a,j} > f_{b,j}\bigr). $$} \geq f_{b,i

:class:ParetoFront maintains the set of non-dominated organisms across generations, exposed through :func:dominates.

1.9 Bloat-aware fitness penalty

Complexity is measured by :func:genome_complexity$(g) = 0.7\,\tfrac{N_{\text{neurons}}}{N_{\max}} + 0.3\,\tfrac{N_{\text{layers}}}{10}$. :class:BloatPenalizer subtracts a parsimony term from the composite score:

$$ F_{\text{penalised}} = F - \lambda \cdot \max!\left(0,\; \mathrm{complexity}(g) - \mathrm{complexity}_{\text{baseline}}\right), $$

defaulting to $\lambda = 0.01$.

1.10 Fitness sharing (niche preservation)

:func:shared_fitness divides an organism's raw fitness by a niche count, yielding:

$$ F_{\text{shared}}(o_i) = \frac{F(o_i)} {\sum_j \max!\bigl(0,\;1 - d(g_i, g_j)/\sigma_{\text{share}}\bigr)}. $$

This is the Goldberg & Richardson (1987) fitness-sharing operator; it prevents a single dominant lineage from erasing weaker but diverse niches.

1.11 CPPN developmental encoding

Instead of storing weights directly, :class:CPPNGenome stores a small network of CPPN nodes with activations drawn from ${\sin,\, \tanh,\, \text{Gaussian},\, \text{sigmoid}}$. The connection weight between post-synaptic neuron at coordinate $\mathbf{x}$ and pre-synaptic neuron at coordinate $\mathbf{y}$ is obtained by a forward pass $w = \mathrm{CPPN}(\mathbf{x}, \mathbf{y})$. This matches Stanley's HyperNEAT formulation (Stanley et al., 2009) and exploits spatial symmetries — a mutation in one CPPN edge reshapes the entire weight matrix coherently.


2. Theory (why this particular design)

2.1 Genotype–phenotype map is lossy but hardware-closed

The 19-D genome does not encode specific weights — those are deterministic from weight_seed — nor specific spike trains. Instead, the genome encodes control points of the phenotype (time constants, connection probability, plasticity rates) that stay inside the envelope the hardware FPGA tile can realise. This is deliberate: the evolutionary search operates in a space where every point is constructible on the target substrate, so crossing a fitness gradient cannot yield an organism that fails to instantiate.

2.2 Why 19 coordinates, not 100s

Open-ended evolution typically gets more powerful with higher-dimensional genomes, but each added dimension multiplies the search volume. SC-NeuroCore fixes a small, physically motivated 19-D genome and lets :class:CPPNGenome provide the escape hatch for high-dimensional weight searches when needed. This follows the same principle as PicBreeder (Secretan, 2011) — small active genome, large effective phenotype via developmental indirection.

2.3 Formal safety as a hard filter

Every proposed genome passes through :class:FormalSafetyGuard before it enters the population. The guard checks three invariants:

  1. Time-constant positivity. $\tau_{\text{fast}},\tau_{\text{work}},\tau_{\text{deep}} > 0$ and $\tau_{\text{fast}} < \tau_{\text{work}} < \tau_{\text{deep}}$.
  2. Connectivity bounds. $c \in [0.01,\,1]$ and $N_{\text{neurons}} \in [4,\,1024]$.
  3. Lyapunov-bounded plasticity. $\eta_{\text{STDP}} \cdot \max(\tau_{+}, \tau_{-}) < C_{\text{lyap}}$ where $C_{\text{lyap}}$ is a pre-computed bound that keeps the STDP update map contractive under worst-case rate inputs.

Invariant 3 is checked by :meth:FormalSafetyGuard.check rather than proved on-the-fly; see docs/api/formal.md §6 for the matching Lean 4 theorem (axiomatised, with a Mathlib proof roadmap).

2.4 Extinction as a diversity reset

Real evolution periodically resets via mass-extinction events (Raup, 1991). :class:ExtinctionDetector mirrors this: when the best fitness has not improved for stagnation_gens=10 generations, a fraction (kill_fraction=0.9) of the population is culled and reseeded from the :class:HallOfFame. This is not just ergodicity theatre — it breaks local maxima that incremental mutation cannot escape.

2.5 Island model with periodic migration

:class:IslandModel runs N independent sub-populations with migration every $M$ generations. Each island has its own RNG seed and mutation pressure; diverse islands explore different basins. Migration copies the top-$k$ organisms between adjacent islands, propagating discoveries without erasing sub-population identity. This is the textbook Whitley distributed GA (Whitley, 1999) adapted to hardware-aware selection.


3. Position in the pipeline

Text Only
+------------------+    +-------------------+    +------------------+
|  ArcaneZenith    |    |    evo_substrate  |    |     bioware      |
|  cognitive core  |<---|  (this module)    |--->|  MEA closed-loop |
+------------------+    +-------------------+    +------------------+
                            ^      |     ^
                            |      |     |
                      seeds |      |     | deploys
                            |      v     |
                      +----------+ +------------+
                      | hdl_gen  | | FPGA tile  |
                      | verilog  | | allocation |
                      +----------+ +------------+
  • Upstream inputs. ArcaneZenith.step_from_genome seeds its time-constants from :class:NeuronGene; sc_scope and sc_doctor (see debug.md) observe phenotype behaviour.
  • Outputs. :class:OrganismEmitter converts winning genomes to NIR graph or Verilog for hdl_gen/verilog_generator.py; resource budget checks go through :class:SafetyBounds and :class:TileDeploymentTracker.
  • Closed loop. Fitness metrics come from either a software proxy or :func:bioware.mea_fitness_hook; the loop does not leave the substrate.

4. Features

  • Content-addressable genomes (SHA-256 ids, 12 hex chars).
  • 5 mutation operators (point, structural, duplication, swap, identity).
  • Uniform crossover with gene-block alignment.
  • Normalised L1 genomic distance (scale-invariant).
  • NEAT-style greedy speciation, fitness sharing, novelty archive.
  • Tournament + elitist + age-regulated selection (:class:TournamentSelector + :class:AgeRegulator).
  • Industrial-mode :class:ReplicationEngine with 9 co-operating guards.
  • Multi-objective Pareto front (:class:ParetoFront, :func:dominates).
  • Bloat penalty, complexity tracking, extinction detector, hall of fame.
  • CPPN developmental encoding (:class:CPPNGenome) for high-dim weight searches.
  • Island model (:class:IslandModel) with migration.
  • Hardware-side gating (:class:SafetyBounds, :class:ResourceBudget, :class:TileDeploymentTracker).
  • NIR + Verilog emission (:class:OrganismEmitter).
  • Co-evolution arena (:class:CoevolutionArena) for predator/prey or critic/actor dynamics.
  • Full lineage graph (:class:LineageTracker) — every child records its parent and mutation type, giving a reconstructable phylogeny.

5. Usage — end-to-end generation

Python
from sc_neurocore.evo_substrate.evo_substrate import (
    Genome, ReplicationEngine, MutationEngine, MutationConfig,
)

def metrics_fn(genome):
    # Plug your closed-loop MEA hook, or a software proxy:
    return {"accuracy": 0.5 + 0.01 * genome.topology.num_neurons / 32}

cfg = MutationConfig(
    point_rate=0.2,
    point_sigma=0.05,
    structural_rate=0.05,
    duplication_rate=0.01,
    swap_rate=0.02,
)
engine = ReplicationEngine(
    mutation_engine=MutationEngine(cfg, rng_seed=7),
    max_population=32,
    elitism=1,
    industrial_mode=True,
)

for i in range(16):
    g = Genome()
    g.compute_id()
    engine.seed(g)

engine.evaluate_all(metrics_fn)

for gen in range(20):
    stats = engine.evolve_generation(metrics_fn)
    print(
        f"gen {stats['generation']:>3}  "
        f"pop={stats['population_size']:>2}  "
        f"best={stats['best_fitness']:.3f}  "
        f"diversity={stats['diversity']:.3f}"
    )

Sample output from a real run (industrial_mode=True, 16-organism seed, 20 generations, rng_seed=7):

Text Only
gen   1  pop=16  best=0.042  diversity=0.006
gen   5  pop=16  best=0.044  diversity=0.008
gen  10  pop=16  best=0.044  diversity=0.008
gen  20  pop=16  best=0.044  diversity=0.007

Low best_fitness values reflect the toy metrics_fn above (returns 0.5 + 0.01 · num_neurons/32, penalised by hardware-cost and bloat terms); replace with a real evaluator to see non-trivial selection pressure. Population stays at 16 in this run because tournament selection + safety-guard rejection keep new organisms below the max_population=32 cap when the selected parents are similar.


6. API reference

6.1 Gene blocks

Class Fields (with defaults)
:class:TopologyGene num_neurons=16, num_layers=2, connectivity=0.3, recurrent_fraction=0.1, bitstream_length=256
:class:NeuronGene tau_fast=5, tau_work=200, tau_deep=10000, theta=1, gamma=0.2, delta_conf=0.3, kappa=5, w_inh=0.3
:class:PlasticityGene stdp_lr=0.01, stdp_tau_plus=20, stdp_tau_minus=20, stp_u_base=0.5, homeostatic_rate=0.001, meta_sensitivity=1

6.2 Mutation + crossover

Symbol Purpose
:class:MutationType enum: POINT, STRUCTURAL, DUPLICATION, SWAP, IDENTITY
:class:MutationConfig per-type rates, Gaussian σ, structural intensity bounds
:class:MutationEngine deterministic under rng_seed; mutate(g) returns (child, op)
:class:CrossoverEngine uniform crossover, gene-block-aligned
:func:genomic_distance scale-invariant L1
:func:assign_species NEAT-style speciation
:func:population_diversity mean pairwise distance

6.3 Fitness + selection

Symbol Purpose
:class:FitnessType ACCURACY, ENERGY, LATENCY, COMPOSITE
:class:FitnessResult (accuracy, energy_score, latency_score, composite)
:class:FitnessEvaluator scorer over population; accepts metrics_fn
:class:TournamentSelector $k$-way tournament with optional elitism
:class:AgeRegulator ages out organisms past max_age
:class:ParetoFront non-dominated front
:func:dominates Pareto relation $\succ$
:func:shared_fitness Goldberg–Richardson niching

6.4 Population control

Symbol Purpose
:class:IslandModel N sub-populations + periodic migration
:class:NoveltyArchive sparse archive of behaviourally distinct genomes
:class:HallOfFame top-K elites across generations
:class:BloatPenalizer parsimony penalty on composite fitness
:class:ComplexityTracker structural complexity over time
:class:ExtinctionDetector mass-extinction trigger on stagnation
:class:CoevolutionArena predator/prey or critic/actor co-evolution
:class:EvoStatisticsTracker per-generation :class:GenerationStats log

6.5 Safety + hardware

Symbol Purpose
:class:FormalSafetyGuard genome-side invariants (tau positivity, c bounds, Lyapunov plast.)
:class:SafetyBounds hardware-side limits (V, I, routing length)
:class:ResourceBudget tracks (power_mw, area_um2, latency_ns)
:class:TileAllocation which FPGA tile a genome occupies
:class:TileDeploymentTracker live map of tile occupancy; handles replication + extinction
:class:HWFitnessReport post-silicon metrics feedback
:class:HWFitnessCollector aggregates :class:HWFitnessReport into a fitness proxy

6.6 Indirect encoding (CPPN)

Symbol Purpose
:class:ActivationFunc SINE, TANH, GAUSSIAN, SIGMOID
:class:CPPNNode one activation node
:class:CPPNEdge one weighted edge
:class:CPPNGenome NEAT-like CPPN; expands to weight matrix via forward pass

6.7 Lineage + diff

Symbol Purpose
:class:LineageRecord (genome_id, parent_id, generation, mutation_type, fitness)
:class:LineageTracker records all records; walk ancestry via get_ancestors(genome_id)
:class:GenomeDiff (topology_delta, neuron_delta, plasticity_delta)
:func:genome_diff per-block L2 delta between two genomes
:func:genome_complexity scalar $0.7 N/N_{\max} + 0.3 L/10$

6.8 Emission

Symbol Purpose
:class:OrganismEmitter genome → NIR graph or Verilog
:class:GenomeSerializer JSON / binary round-trip

7. Verified benchmarks

Measured on Ubuntu 24.04 / CPython 3.12.3 / Intel i5-11600K @ 3.90 GHz, single-thread. All figures produced by benchmarks/bench_evo_substrate.py (committed) and reproducible with python benchmarks/bench_evo_substrate.py.

Operation Throughput Latency
MutationEngine.mutate 15 223 ops/s 65.69 µs
CrossoverEngine.crossover 29 631 ops/s 33.75 µs
genomic_distance (19-D) 92 891 ops/s 10.77 µs
FormalSafetyGuard.check 1 376 152 ops/s 0.73 µs
assign_species (n=64, θ=0.3) 1 430 ops/s 0.70 ms
ReplicationEngine.evolve_generation (pop=32, industrial_mode=True) 161 gen/s 6.20 ms

Raw JSON at benchmarks/results/bench_evo_substrate.json is written by the same script every run, so any doc regression (drift, rename, hidden simplification) can be caught by diffing the JSON rather than re-reading the markdown.

7.1 Determinism + reproducibility

All RNGs in the module are numpy.random.default_rng seeded through explicit constructor arguments (MutationEngine(rng_seed=…), CrossoverEngine(rng_seed=…), :class:ReplicationEngine's internal self.mutator.rng). Two consequences:

  1. A given (config, seeds, metrics_fn) triple is bit-reproducible: re-running the 20-generation demo above yields the same lineage tree, same :class:HallOfFame entries, and the same :class:ParetoFront.
  2. Islands in :class:IslandModel take independent seeds derived from a master seed, so experiments can be re-run with different master seeds to bound Monte-Carlo noise on any reported figure.

The lineage tracker (:class:LineageTracker) also lets you replay any subtree: given a surviving genome_id, :meth:get_ancestors returns the exact mutation chain from seed to present, which is what :class:OrganismEmitter serialises alongside the Verilog blob for audit trails on the FPGA tile side.

7.2 Multi-language kernel comparison

The four compute hot paths are also mirrored in Rust, Julia, Go, and Mojo for honest cross-language measurement. Python orchestration (ReplicationEngine, lineage, hall-of-fame, island model, safety guards — 40+ classes) stays authoritative in Python; only genomic_distance, crossover_uniform, point_mutation, and population_diversity are mirrored elsewhere.

Measured on 2026-04-20 via benchmarks/bench_evo_substrate_multilang.py (committed harness, JSON at benchmarks/results/bench_evo_substrate_multilang.json). Inputs are 19-D Float64 vectors, 100 000 iterations, warm cache.

Kernel (ns/call, dim=19) Rust Julia Go Mojo Python
genomic_distance 257.6 22.5 22.8 18.8 5 992.3
crossover_uniform 481.4 45.8 42.2 150.5 1 093.9
point_mutation 432.4 295.4 46.7 151.2 3 984.4

How to read this. The standalone numbers (Julia 22 ns, Go 23 ns, Mojo 19 ns on genomic_distance) confirm the kernel itself is cheap in every language — the hot loop is 19 float ops. The Rust number (258 ns) includes the PyO3 FFI boundary: NumPy array view materialisation + reference-count bump + tuple unpack. Rust called from another Rust binary runs in ~10 ns (see the Criterion benches in crates/evo_substrate_core/benches/evo_bench.rs).

From the Python orchestration's perspective (the caller that matters), Rust is the fastest accessible backend because Julia / Go / Mojo would need a subprocess round-trip per call (~ms, fatal for inner loops). The PyO3-dispatched Rust path gives 23× speedup over pure Python without any subprocess cost.

Fallback order. Python callers currently dispatch genomic_distance to Rust PyO3 when importable, else fall back to the NumPy reference (bit-exact). Julia / Go / Mojo versions are honest parity references for benchmarking, not called in the Python hot path.

7.3 Whole-process industrial runners (4-backend parity set)

Beyond the per-kernel dispatch surface above, each of the four compilers ships a whole-process evolve runner — the entire ReplicationEngine.evolve_generation() loop plus the eleven industrial guards (TournamentSelector, AgeRegulator, FormalSafetyGuard, BloatPenalizer, ExtinctionDetector, HallOfFame, ParetoFront, LineageTracker, MutationEngine × 4 variants, CrossoverEngine, parametric FitnessEvaluator). A Python orchestrator can invoke any backend with identical JSON config on stdin and receive an identical EvolveResult JSON on stdout.

Backend Entry point Source
Rust evo_substrate_core.py_evolve_run(config_json) -> str (PyO3) crates/evo_substrate_core/src/runner.rs (1 227 LOC)
Julia julia evo_runner.jl < cfg > result subprocess src/sc_neurocore/accel/julia/evo_substrate/evo_runner.jl (720 LOC)
Go ./evo_substrate_bench --runner < cfg > result subprocess src/sc_neurocore/accel/go/evo_substrate/runner.go (926 LOC)
Mojo pixi run mojo run kernels/evo_runner.mojo < cfg > result src/sc_neurocore/accel/mojo/kernels/evo_runner.mojo (803 LOC)

All four runners share a common XorShift64 PRNG (constants 13/7/17, 0xDEADBEEFCAFEBABE fallback for zero seeds) so the same seed produces byte-identical uniform sequences across languages.

7.3.1 Cross-backend parity — measured on fixed seed

Running the default config (seed=7, pop=16, gens=10, industrial_mode=True) against all four runners:

Backend gen 10 best Pareto size Lineage records Replications
Rust 0.69955078125 1 96 80
Julia 0.69955078125 1 96 80
Go 0.6992578125 1 96 80
Mojo 0.6999804687500001 3 96 80

Rust ↔ Julia are byte-exact identical on every field (genome_ids, lineage records, HoF entries, Pareto members, gen-by-gen stats).

Rust ↔ Go match on all structural counters and converge on the same Pareto size but drift at ~1e-3 on best_fitness — Go's math.Cos and math.Log differ from Rust's libm at ~1 ULP, and Box-Muller-based Gaussian mutation compounds that drift over ~80 mutations. A bit-exact polynomial cos/log is the path to close this; tracked as follow-up.

Rust ↔ Mojo structural parity (lineage, counters); numerics drift similarly to Go for the same libm reason plus Mojo 0.26 Python-interop noise in the SHA-256 hashing path. Mojo's Pareto size is larger (3 vs 1) because the compounding drift leaves a few more non-dominated organisms standing.

The cross-language parity suite in tests/test_evo_substrate/test_multilang_parity.py asserts the above four-way relationships; it skips gracefully on missing toolchains.

7.3.2 Whole-process timing (seed=7, pop=16, 10 gens, industrial)

Measured 2026-04-20 on i5-11600K / CPython 3.12.3:

Backend Wall clock Dispatch model
Rust (PyO3 in-process) 0.57 ms / run per-call from Python, warm
Rust (Criterion, pure Rust binary) ~5 µs / run in-process Rust, no FFI
Go (already-built binary, excl build) ~2 ms / run subprocess; add ~3 s for go build
Mojo (cold pixi + JIT compile) ~1.1 s / run subprocess; Mojo 0.26 JIT per invocation
Julia (cold Julia + JSON.jl precompile) ~3 s / run subprocess; amortises across long runs
Python (ReplicationEngine reference) 40.88 ms / run in-process, NumPy

Honest caveats on the "cold" column:

  • Go — the ./evo_substrate_bench binary must exist on disk. A fresh go build -o evo_substrate_bench . takes ~3 s the first time (compiler + module cache cold); the 2 ms figure above is the binary's own execution wall after build. The repo's .gitignore already skips the compiled binary, and the pytest + parity harness rebuild it on demand.
  • Mojo — the ~1.1 s includes pixi env activation (~200 ms), Mojo JIT compile (~800 ms), and Python interop bootstrap (~100 ms). Running the same .mojo file a second time in the SAME pixi env keeps the JIT result in the .pixi/envs/default mojo cache, so warm runs are ~700 ms. There is no truly "warm" Mojo because every subprocess restart re-pays the JIT cost.
  • Julia — ~3 s cold is dominated by JSON.jl + SHA.jl precompile (~2.5 s) plus Julia runtime startup (~500 ms). Inside an already-hot Julia session this drops to ~20 ms per evolve_run call. The Julia runner is the right choice when the surrounding experiment is also Julia (e.g. a DifferentialEquations.jl fitness function); as a per-call backend from Python, the subprocess overhead dominates.
  • Rust — the 0.57 ms is warm in-process via the PyO3 extension. First import incurs a ~10 ms module load, amortised across any non-trivial run.

7.3.3 When to pick which backend

  • Per-call from Python orchestration — Rust PyO3. The other three subprocess startup costs (2 ms – 3 s) make them unusable for the inner loop, which calls evolve_generation thousands of times.
  • Long experiments (1 000+ generations, 100+ pop) — any subprocess backend amortises; pick by what else your experiment touches. Julia if you reach into DiffEq / Plots; Go if you already have a Go service mesh; Mojo if you use other SIMD kernels in the same pixi env.
  • Audit / cross-check — run the same config through two backends and compare the JSON. Rust ↔ Julia is byte-exact; any mismatch indicates a regression in one of them.

7.3.4 Testing the 4-backend parity set

  • Rust: cargo test --release --features pyo3_bindings --manifest-path crates/evo_substrate_core/Cargo.toml — 17 unit tests.
  • Julia: julia src/sc_neurocore/accel/julia/evo_substrate/test_evo_runner.jl — 17 unit tests (PRNG, roundtrip, fitness, safety, determinism).
  • Go: go test -v ./src/sc_neurocore/accel/go/evo_substrate/... — 8 unit tests (PRNG, roundtrip, id-shape, fitness, safety, determinism).
  • Mojo: pytest tests/test_evo_substrate/test_mojo_runner.py — 7 side-validated unit tests driven from Python (Mojo 0.26 has no native unit-test harness).
  • Cross-language parity: pytest tests/test_evo_substrate/test_multilang_parity.py — 18 tests (schema, Rust↔Julia bit-exact, Rust↔Go tolerance, Rust↔Mojo structure, determinism).

Interpretation. Safety checks and distance computations are cheap enough (<1 µs and ~10 µs respectively) that they do not dominate the generation cost. Mutation and crossover are the slower inner ops (NumPy array creation per call); bulk speedup when moving to a Rust inner loop is achievable but not necessary for current population sizes — at 32 organisms × 20 generations, a full run completes in $\approx 0.12$ s.

Figures above are time.perf_counter deltas from benchmarks/bench_evo_substrate.py.


8. Citations

  1. Stanley K.O., Miikkulainen R. (2002). Evolving Neural Networks through Augmenting Topologies. Evolutionary Computation 10(2):99–127.
  2. Stanley K.O., D'Ambrosio D.B., Gauci J. (2009). A Hypercube-Based Encoding for Evolving Large-Scale Neural Networks. Artif. Life 15(2):185–212. (HyperNEAT / CPPN.)
  3. Syswerda G. (1989). Uniform Crossover in Genetic Algorithms. Proc. 3rd Int. Conf. on Genetic Algorithms, 2–9.
  4. Goldberg D.E., Richardson J. (1987). Genetic Algorithms with Sharing for Multimodal Function Optimization. ICGA-87, 41–49. (Fitness sharing.)
  5. Deb K., Pratap A., Agarwal S., Meyarivan T. (2002). A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II. IEEE TEC 6(2):182–197. (Pareto dominance.)
  6. Whitley D. (1999). An overview of evolutionary algorithms: practical issues and common pitfalls. Information and Software Technology 43(14):817–831. (Island model.)
  7. Secretan J. et al. (2011). Picbreeder: A Case Study in Collaborative Evolutionary Exploration of Design Space. Evolutionary Computation 19(3):373–403.
  8. Raup D.M. (1991). Extinction: Bad Genes or Bad Luck? W. W. Norton. (Mass-extinction dynamics.)
  9. Lehman J., Stanley K.O. (2011). Abandoning Objectives: Evolution Through the Search for Novelty Alone. Evolutionary Computation 19(2):189–223. (Novelty archive.)
  10. Šotek M. (2026). SC-NeuroCore: Self-replicating neuromorphic substrate. Internal report, ANULUM.

Reference

  • Source: src/sc_neurocore/evo_substrate/evo_substrate.py (1594 LOC).
  • Tests: tests/test_evo_substrate/test_evo_substrate.py (908 LOC).
  • Demo: examples/16_evo_substrate_demo.py.
  • Benchmark: benchmarks/bench_evo_substrate.py.

sc_neurocore.evo_substrate.evo_substrate

Open-ended evolution of SC neuromorphic networks at hardware speed.

Networks can emit mutated child networks (as NIR or Verilog) that run on separate FPGA tiles and compete/evolve under a fitness function.

Architecture:

Text Only
┌────────────────────────────────────────────┐
│             EvolutionArena                  │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐   │
│  │Organism 0│ │Organism 1│ │Organism N│   │
│  │(parent)  │ │(child)   │ │(mutant)  │   │
│  │  Genome  │ │  Genome  │ │  Genome  │   │
│  │  ├ topo  │ │  ├ topo  │ │  ├ topo  │   │
│  │  ├ neuro │ │  ├ neuro │ │  ├ neuro │   │
│  │  └ plast │ │  └ plast │ │  └ plast │   │
│  └────┬─────┘ └────┬─────┘ └────┬─────┘   │
│       │ fitness     │ fitness    │ fitness  │
│  ┌────▼─────────────▼───────────▼─────┐    │
│  │         FitnessEvaluator            │    │
│  └────────────────┬───────────────────┘    │
│                   │ selection               │
│  ┌────────────────▼───────────────────┐    │
│  │      ReplicationEngine             │    │
│  │  replicate() → mutate() → deploy() │    │
│  └────────────────────────────────────┘    │
└────────────────────────────────────────────┘

Compatible with: - neurons/models/arcane_neuron.py — base neuron model - meta_plasticity/ — mutable plasticity rules - hdl_gen/verilog_generator.py — HDL emission target - hypervisor/ — FPGA tile isolation for organisms - safety_cert/ — safety constraints on mutation space

TopologyGene dataclass

Encodes the network topology.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
@dataclass
class TopologyGene:
    """Encodes the network topology."""

    num_neurons: int = 16
    num_layers: int = 2
    connectivity: float = 0.3  # connection probability
    recurrent_fraction: float = 0.1
    bitstream_length: int = 256

    def to_vector(self) -> np.ndarray:
        return np.array(
            [
                self.num_neurons,
                self.num_layers,
                self.connectivity,
                self.recurrent_fraction,
                self.bitstream_length,
            ]
        )

    @classmethod
    def from_vector(cls, v: np.ndarray) -> TopologyGene:
        return cls(
            num_neurons=max(2, int(v[0])),
            num_layers=max(1, int(v[1])),
            connectivity=float(np.clip(v[2], 0.01, 1.0)),
            recurrent_fraction=float(np.clip(v[3], 0.0, 0.5)),
            bitstream_length=max(32, int(v[4])),
        )

NeuronGene dataclass

Encodes neuron-level parameters (ArcaneNeuron-compatible).

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
@dataclass
class NeuronGene:
    """Encodes neuron-level parameters (ArcaneNeuron-compatible)."""

    tau_fast: float = 5.0
    tau_work: float = 200.0
    tau_deep: float = 10000.0
    theta: float = 1.0
    gamma: float = 0.2
    delta_conf: float = 0.3
    kappa: float = 5.0
    w_inh: float = 0.3

    def to_vector(self) -> np.ndarray:
        return np.array(
            [
                self.tau_fast,
                self.tau_work,
                self.tau_deep,
                self.theta,
                self.gamma,
                self.delta_conf,
                self.kappa,
                self.w_inh,
            ]
        )

    @classmethod
    def from_vector(cls, v: np.ndarray) -> NeuronGene:
        return cls(
            tau_fast=max(0.5, float(v[0])),
            tau_work=max(1.0, float(v[1])),
            tau_deep=max(10.0, float(v[2])),
            theta=max(0.1, float(v[3])),
            gamma=float(np.clip(v[4], 0.0, 1.0)),
            delta_conf=float(np.clip(v[5], 0.0, 1.0)),
            kappa=max(0.1, float(v[6])),
            w_inh=float(np.clip(v[7], 0.0, 1.0)),
        )

PlasticityGene dataclass

Encodes plasticity rule parameters.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
@dataclass
class PlasticityGene:
    """Encodes plasticity rule parameters."""

    stdp_lr: float = 0.01
    stdp_tau_plus: float = 20.0
    stdp_tau_minus: float = 20.0
    stp_u_base: float = 0.5
    homeostatic_rate: float = 0.001
    meta_sensitivity: float = 1.0

    def to_vector(self) -> np.ndarray:
        return np.array(
            [
                self.stdp_lr,
                self.stdp_tau_plus,
                self.stdp_tau_minus,
                self.stp_u_base,
                self.homeostatic_rate,
                self.meta_sensitivity,
            ]
        )

    @classmethod
    def from_vector(cls, v: np.ndarray) -> PlasticityGene:
        return cls(
            stdp_lr=max(1e-6, float(v[0])),
            stdp_tau_plus=max(1.0, float(v[1])),
            stdp_tau_minus=max(1.0, float(v[2])),
            stp_u_base=float(np.clip(v[3], 0.01, 0.99)),
            homeostatic_rate=max(1e-6, float(v[4])),
            meta_sensitivity=max(0.1, float(v[5])),
        )

Genome dataclass

Complete genome for an evolving SC organism.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
@dataclass
class Genome:
    """Complete genome for an evolving SC organism."""

    genome_id: str = ""
    parent_id: str = ""
    generation: int = 0
    topology: TopologyGene = field(default_factory=TopologyGene)
    neuron: NeuronGene = field(default_factory=NeuronGene)
    plasticity: PlasticityGene = field(default_factory=PlasticityGene)
    weight_seed: int = 42
    identity_deep: float = 0.0

    def to_vector(self) -> np.ndarray:
        return np.concatenate(
            [
                self.topology.to_vector(),
                self.neuron.to_vector(),
                self.plasticity.to_vector(),
            ]
        )

    @classmethod
    def from_vector(cls, v: np.ndarray, gen: int = 0) -> Genome:
        return cls(
            generation=gen,
            topology=TopologyGene.from_vector(v[0:5]),
            neuron=NeuronGene.from_vector(v[5:13]),
            plasticity=PlasticityGene.from_vector(v[13:19]),
        )

    @property
    def vector_dim(self) -> int:
        return len(self.to_vector())

    def compute_id(self) -> str:
        h = hashlib.sha256(self.to_vector().tobytes())
        self.genome_id = h.hexdigest()[:12]
        return self.genome_id

MutationConfig dataclass

Controls mutation rates and magnitudes.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
236
237
238
239
240
241
242
243
244
245
246
@dataclass
class MutationConfig:
    """Controls mutation rates and magnitudes."""

    point_rate: float = 0.2
    point_sigma: float = 0.05
    structural_rate: float = 0.05
    duplication_rate: float = 0.01
    swap_rate: float = 0.02
    max_neurons: int = 1024
    min_neurons: int = 4

MutationEngine

Applies mutations to genomes.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
class MutationEngine:
    """Applies mutations to genomes."""

    def __init__(self, config: Optional[MutationConfig] = None, rng_seed: int = 42):
        self.config = config or MutationConfig()
        self.rng = np.random.default_rng(rng_seed)

    def mutate(self, genome: Genome) -> Tuple[Genome, MutationType]:
        """Apply a random mutation and return the mutated child."""
        child = copy.deepcopy(genome)
        child.parent_id = genome.genome_id
        child.generation = genome.generation + 1
        child.identity_deep = 0.0  # New organism starts fresh

        roll = self.rng.random()
        cumulative = 0.0

        cumulative += self.config.structural_rate
        if roll < cumulative:
            self._structural_mutation(child)
            child.compute_id()
            return child, MutationType.STRUCTURAL

        cumulative += self.config.duplication_rate
        if roll < cumulative:
            self._duplication_mutation(child)
            child.compute_id()
            return child, MutationType.DUPLICATION

        cumulative += self.config.swap_rate
        if roll < cumulative:
            self._swap_mutation(child)
            child.compute_id()
            return child, MutationType.SWAP

        # Default: point mutation
        self._point_mutation(child)
        child.compute_id()
        return child, MutationType.POINT

    def _point_mutation(self, genome: Genome) -> None:
        v = genome.to_vector()
        mask = self.rng.random(len(v)) < self.config.point_rate
        noise = self.rng.normal(0, self.config.point_sigma, size=len(v))
        v[mask] += noise[mask] * (np.abs(v[mask]) + 1e-8)
        rebuilt = Genome.from_vector(v, genome.generation)
        genome.topology = rebuilt.topology
        genome.neuron = rebuilt.neuron
        genome.plasticity = rebuilt.plasticity

    def _structural_mutation(self, genome: Genome) -> None:
        delta = self.rng.choice([-2, -1, 1, 2])
        genome.topology.num_neurons = int(
            np.clip(
                genome.topology.num_neurons + delta,
                self.config.min_neurons,
                self.config.max_neurons,
            )
        )
        genome.topology.connectivity += self.rng.normal(0, 0.05)
        genome.topology.connectivity = float(np.clip(genome.topology.connectivity, 0.01, 1.0))

    def _duplication_mutation(self, genome: Genome) -> None:
        genome.topology.num_layers = min(10, genome.topology.num_layers + 1)
        genome.topology.num_neurons = min(
            self.config.max_neurons,
            int(genome.topology.num_neurons * 1.5),
        )

    def _swap_mutation(self, genome: Genome) -> None:
        genome.neuron.tau_fast, genome.neuron.tau_work = (
            genome.neuron.tau_work,
            genome.neuron.tau_fast,
        )

mutate(genome)

Apply a random mutation and return the mutated child.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
def mutate(self, genome: Genome) -> Tuple[Genome, MutationType]:
    """Apply a random mutation and return the mutated child."""
    child = copy.deepcopy(genome)
    child.parent_id = genome.genome_id
    child.generation = genome.generation + 1
    child.identity_deep = 0.0  # New organism starts fresh

    roll = self.rng.random()
    cumulative = 0.0

    cumulative += self.config.structural_rate
    if roll < cumulative:
        self._structural_mutation(child)
        child.compute_id()
        return child, MutationType.STRUCTURAL

    cumulative += self.config.duplication_rate
    if roll < cumulative:
        self._duplication_mutation(child)
        child.compute_id()
        return child, MutationType.DUPLICATION

    cumulative += self.config.swap_rate
    if roll < cumulative:
        self._swap_mutation(child)
        child.compute_id()
        return child, MutationType.SWAP

    # Default: point mutation
    self._point_mutation(child)
    child.compute_id()
    return child, MutationType.POINT

CrossoverEngine

Uniform crossover between two parent genomes.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
class CrossoverEngine:
    """Uniform crossover between two parent genomes."""

    def __init__(self, rng_seed: int = 42):
        self.rng = np.random.default_rng(rng_seed)

    def crossover(self, parent_a: Genome, parent_b: Genome) -> Genome:
        """Uniform crossover: each gene drawn from either parent."""
        va = parent_a.to_vector()
        vb = parent_b.to_vector()
        mask = self.rng.random(len(va)) < 0.5
        child_v = np.where(mask, va, vb)
        child = Genome.from_vector(child_v, max(parent_a.generation, parent_b.generation) + 1)
        child.parent_id = f"{parent_a.genome_id}x{parent_b.genome_id}"
        child.compute_id()
        return child

crossover(parent_a, parent_b)

Uniform crossover: each gene drawn from either parent.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
334
335
336
337
338
339
340
341
342
343
def crossover(self, parent_a: Genome, parent_b: Genome) -> Genome:
    """Uniform crossover: each gene drawn from either parent."""
    va = parent_a.to_vector()
    vb = parent_b.to_vector()
    mask = self.rng.random(len(va)) < 0.5
    child_v = np.where(mask, va, vb)
    child = Genome.from_vector(child_v, max(parent_a.generation, parent_b.generation) + 1)
    child.parent_id = f"{parent_a.genome_id}x{parent_b.genome_id}"
    child.compute_id()
    return child

LineageRecord dataclass

One entry in the ancestry log.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
413
414
415
416
417
418
419
420
421
@dataclass
class LineageRecord:
    """One entry in the ancestry log."""

    genome_id: str
    parent_id: str
    generation: int
    mutation_type: str
    fitness: float = 0.0

LineageTracker

Tracks ancestry graph for all organisms.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
class LineageTracker:
    """Tracks ancestry graph for all organisms."""

    def __init__(self):
        self.records: List[LineageRecord] = []
        self._by_id: Dict[str, LineageRecord] = {}

    def record(self, organism: Organism, mutation_type: str = "seed") -> None:
        fit = organism.fitness.composite if organism.fitness else 0.0
        rec = LineageRecord(
            genome_id=organism.genome.genome_id,
            parent_id=organism.genome.parent_id,
            generation=organism.genome.generation,
            mutation_type=mutation_type,
            fitness=fit,
        )
        self.records.append(rec)
        self._by_id[rec.genome_id] = rec

    def get_ancestors(self, genome_id: str) -> List[LineageRecord]:
        """Walk the ancestry chain to the root."""
        chain = []
        current = genome_id
        while current in self._by_id:
            rec = self._by_id[current]
            chain.append(rec)
            current = rec.parent_id
        return chain

    @property
    def num_records(self) -> int:
        return len(self.records)

get_ancestors(genome_id)

Walk the ancestry chain to the root.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
443
444
445
446
447
448
449
450
451
def get_ancestors(self, genome_id: str) -> List[LineageRecord]:
    """Walk the ancestry chain to the root."""
    chain = []
    current = genome_id
    while current in self._by_id:
        rec = self._by_id[current]
        chain.append(rec)
        current = rec.parent_id
    return chain

FitnessResult dataclass

Fitness evaluation result.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
@dataclass
class FitnessResult:
    """Fitness evaluation result."""

    genome_id: str
    accuracy: float = 0.0
    energy_score: float = 0.0
    latency_score: float = 0.0
    composite: float = 0.0

    def compute_composite(
        self, w_acc: float = 0.5, w_energy: float = 0.3, w_latency: float = 0.2
    ) -> float:
        self.composite = (
            w_acc * self.accuracy + w_energy * self.energy_score + w_latency * self.latency_score
        )
        return self.composite

FitnessEvaluator

Evaluates organism fitness from simulation metrics.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
class FitnessEvaluator:
    """Evaluates organism fitness from simulation metrics."""

    def __init__(self, fitness_type: FitnessType = FitnessType.COMPOSITE):
        self.fitness_type = fitness_type

    def evaluate(self, genome: Genome, metrics: Dict[str, float]) -> FitnessResult:
        result = FitnessResult(genome_id=genome.genome_id)
        result.accuracy = metrics.get("accuracy", 0.0)

        # Energy: fewer neurons + shorter bitstreams = better
        neuron_pen = min(genome.topology.num_neurons / 1024.0, 1.0)
        bs_pen = min(genome.topology.bitstream_length / 1024.0, 1.0)
        result.energy_score = max(0.0, 1.0 - 0.5 * neuron_pen - 0.5 * bs_pen)

        # Latency: fewer layers = faster
        result.latency_score = max(0.0, 1.0 - genome.topology.num_layers / 10.0)

        result.compute_composite()
        return result

Organism dataclass

One evolving SC organism.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
512
513
514
515
516
517
518
519
520
521
522
@dataclass
class Organism:
    """One evolving SC organism."""

    genome: Genome
    fitness: Optional[FitnessResult] = None
    alive: bool = True
    tile_id: Optional[int] = None
    birth_generation: int = 0
    lifespan_steps: int = 0
    runtime_fault_checks: List[RuntimeFaultCheck] = field(default_factory=list)

RuntimeFaultConfig dataclass

Runtime fault-check settings for evolved SC organisms.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
525
526
527
528
529
530
531
532
533
534
@dataclass(frozen=True)
class RuntimeFaultConfig:
    """Runtime fault-check settings for evolved SC organisms."""

    fault_model: FaultModel = FaultModel.BIT_FLIP
    ber: float = 0.0
    seed_offset: int = 0
    sample_neurons: int = 8
    fitness_penalty_on_extend: float = 0.95
    fitness_penalty_on_replay: float = 0.85

RuntimeFaultCheck dataclass

Recorded runtime fault/degradation decision for one organism.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
@dataclass(frozen=True)
class RuntimeFaultCheck:
    """Recorded runtime fault/degradation decision for one organism."""

    organism_id: str
    generation: int
    action: str
    recommended_bitstream_length: int
    replay_seed: int
    affected_ratio: float
    audit_status: str
    reason: str

    @classmethod
    def from_plan(cls, organism: Organism, plan: DegradationPlan) -> RuntimeFaultCheck:
        return cls(
            organism_id=organism.genome.genome_id,
            generation=organism.genome.generation,
            action=plan.action.value,
            recommended_bitstream_length=plan.recommended_bitstream_length,
            replay_seed=plan.replay_seed,
            affected_ratio=plan.observation.affected_ratio,
            audit_status=plan.observation.audit.status.value,
            reason=plan.reason,
        )

    def to_dict(self) -> Dict[str, Any]:
        """Return a JSON-ready fault-check summary."""
        return {
            "organism_id": self.organism_id,
            "generation": self.generation,
            "action": self.action,
            "recommended_bitstream_length": self.recommended_bitstream_length,
            "replay_seed": self.replay_seed,
            "affected_ratio": self.affected_ratio,
            "audit_status": self.audit_status,
            "reason": self.reason,
        }

to_dict()

Return a JSON-ready fault-check summary.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
563
564
565
566
567
568
569
570
571
572
573
574
def to_dict(self) -> Dict[str, Any]:
    """Return a JSON-ready fault-check summary."""
    return {
        "organism_id": self.organism_id,
        "generation": self.generation,
        "action": self.action,
        "recommended_bitstream_length": self.recommended_bitstream_length,
        "replay_seed": self.replay_seed,
        "affected_ratio": self.affected_ratio,
        "audit_status": self.audit_status,
        "reason": self.reason,
    }

ReplicationEngine

Manages organism reproduction, mutation, and deployment.

Selection → Replication → Mutation → Safety Check → Deploy

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
class ReplicationEngine:
    """Manages organism reproduction, mutation, and deployment.

    Selection → Replication → Mutation → Safety Check → Deploy
    """

    def __init__(
        self,
        mutation_engine: Optional[MutationEngine] = None,
        crossover_engine: Optional[CrossoverEngine] = None,
        fitness_evaluator: Optional[FitnessEvaluator] = None,
        max_population: int = 32,
        elitism: int = 1,
        industrial_mode: bool = True,
        runtime_fault_config: Optional[RuntimeFaultConfig] = None,
        degradation_policy: Optional[GracefulDegradationPolicy] = None,
    ):
        self.mutator = mutation_engine or MutationEngine()
        self.crossover = crossover_engine or CrossoverEngine()
        self.evaluator = fitness_evaluator or FitnessEvaluator()
        self.max_population = max_population
        self.elitism = elitism
        self.population: List[Organism] = []
        self.graveyard: List[Organism] = []
        self.generation: int = 0
        self.total_replications: int = 0
        self.lineage = LineageTracker()
        self.runtime_fault_config = runtime_fault_config
        self.degradation_policy = degradation_policy or GracefulDegradationPolicy()

        # Industrial Features
        self.industrial_mode = industrial_mode
        self.tournament = TournamentSelector()
        self.safety_guard = FormalSafetyGuard()
        self.age_regulator = AgeRegulator(max_age=20)
        self.bloat_penalizer = BloatPenalizer()
        self.extinction_detector = ExtinctionDetector(stagnation_gens=10, kill_fraction=0.9)
        self.hall_of_fame = HallOfFame()
        self.pareto_front = ParetoFront()
        self.stats_tracker = EvoStatisticsTracker()

    def seed(self, genome: Genome) -> Organism:
        """Seed the population with an initial organism."""
        genome.compute_id()
        org = Organism(genome=genome, birth_generation=0)
        self.population.append(org)
        self.lineage.record(org, "seed")
        return org

    def replicate(self, parent: Organism) -> Optional[Organism]:
        """Create a mutated child from a parent."""
        child_genome, mut_type = self.mutator.mutate(parent.genome)

        if self.industrial_mode:
            result = self.safety_guard.check(child_genome)
            if not result.passed:
                return None

        child = Organism(
            genome=child_genome,
            birth_generation=self.generation,
        )
        self.total_replications += 1
        self.lineage.record(child, mut_type.value)
        if len(self.population) < self.max_population:
            self.population.append(child)
        return child

    def replicate_crossover(self, parent_a: Organism, parent_b: Organism) -> Optional[Organism]:
        """Create a child via crossover of two parents."""
        child_genome = self.crossover.crossover(parent_a.genome, parent_b.genome)

        if self.industrial_mode:
            result = self.safety_guard.check(child_genome)
            if not result.passed:
                return None

        child = Organism(
            genome=child_genome,
            birth_generation=self.generation,
        )
        self.total_replications += 1
        self.lineage.record(child, "crossover")
        if len(self.population) < self.max_population:
            self.population.append(child)
        return child

    def evaluate_all(self, metrics_fn) -> None:
        """Evaluate fitness for all living organisms."""
        for org in self.population:
            if org.alive:
                metrics = metrics_fn(org.genome)
                org.fitness = self.evaluator.evaluate(org.genome, metrics)

                if self.industrial_mode:
                    if org.fitness:
                        org.fitness.composite = self.bloat_penalizer.penalize(
                            org.fitness.composite, org.genome
                        )
                        org.fitness.composite = shared_fitness(org, self.population)
                    if self.runtime_fault_config is not None:
                        self.verify_runtime_faults(org, self.runtime_fault_config)
                    self.hall_of_fame.update(org)
                    self.pareto_front.update(org)

    def verify_runtime_faults(
        self,
        organism: Organism,
        config: Optional[RuntimeFaultConfig] = None,
    ) -> RuntimeFaultCheck:
        """Run seeded runtime fault diagnosis and apply bounded degradation."""
        cfg = config or self.runtime_fault_config or RuntimeFaultConfig()
        streams = self._runtime_bitstreams(organism.genome, cfg)
        replay_seed = int(organism.genome.weight_seed + cfg.seed_offset)
        plan = self.degradation_policy.evaluate(
            streams,
            layer_id=organism.genome.genome_id or "unidentified",
            fault_model=cfg.fault_model,
            ber=cfg.ber,
            seed=replay_seed,
        )
        check = RuntimeFaultCheck.from_plan(organism, plan)
        organism.runtime_fault_checks.append(check)
        self._apply_runtime_fault_plan(organism, plan, cfg)
        return check

    def _runtime_bitstreams(
        self, genome: Genome, config: RuntimeFaultConfig
    ) -> np.ndarray[Any, Any]:
        neurons = max(1, min(config.sample_neurons, genome.topology.num_neurons))
        length = max(1, genome.topology.bitstream_length)
        rng = np.random.default_rng(int(genome.weight_seed + config.seed_offset))
        return (rng.random((neurons, length)) < 0.5).astype(np.uint8)

    def _apply_runtime_fault_plan(
        self,
        organism: Organism,
        plan: DegradationPlan,
        config: RuntimeFaultConfig,
    ) -> None:
        if plan.action == DegradationAction.NOMINAL:
            return
        organism.genome.topology.bitstream_length = plan.recommended_bitstream_length
        organism.genome.compute_id()
        if organism.fitness is None:
            return
        if plan.action == DegradationAction.REPLAY_WITH_SEED:
            organism.fitness.composite *= config.fitness_penalty_on_replay
        else:
            organism.fitness.composite *= config.fitness_penalty_on_extend

    def select_and_cull(self, survival_fraction: float = 0.5) -> int:
        """Select fittest organisms, cull the rest. Elitism preserved."""
        if self.industrial_mode:
            killed = self.age_regulator.apply(self.population, self.generation)
            if self.extinction_detector.check(self.best_fitness):
                killed += self.extinction_detector.apply(self.population, self.mutator.rng)

        alive = [o for o in self.population if o.alive and o.fitness is not None]
        alive.sort(key=lambda o: o.fitness.composite, reverse=True)

        cutoff = max(self.elitism + 1, int(len(alive) * survival_fraction))
        killed = 0
        for org in alive[cutoff:]:
            org.alive = False
            self.graveyard.append(org)
            killed += 1

        self.population = [o for o in self.population if o.alive]
        return killed

    def evolve_generation(self, metrics_fn) -> Dict[str, Any]:
        """Run one full evolutionary generation."""
        self.generation += 1

        # 1. Evaluate
        self.evaluate_all(metrics_fn)

        # 2. Select + cull
        killed = self.select_and_cull()

        # 3. Replicate from survivors
        survivors = list(self.population)
        children_created = 0

        for i in range(len(survivors)):
            if len(self.population) >= self.max_population:
                break

            if self.industrial_mode:
                parent = self.tournament.select(survivors, self.mutator.rng)
                partner = self.tournament.select(survivors, self.mutator.rng)
            else:
                parent = survivors[i]
                partner = survivors[(i + 1) % len(survivors)] if len(survivors) > 1 else None

            if parent is None:
                continue

            child_added = None
            if partner and self.mutator.rng.random() < 0.3:
                child_added = self.replicate_crossover(parent, partner)
            else:
                child_added = self.replicate(parent)

            if child_added:
                children_created += 1

        stats = GenerationStats(
            generation=self.generation,
            population_size=len(self.population),
            best_fitness=self.best_fitness,
            mean_fitness=self.mean_fitness,
            diversity=population_diversity(self.population),
            extinctions=self.extinction_detector.extinction_count if self.industrial_mode else 0,
        )
        if self.industrial_mode:
            self.stats_tracker.record(stats)

        return {
            "generation": self.generation,
            "population_size": len(self.population),
            "killed": killed,
            "children": children_created,
            "best_fitness": self.best_fitness,
            "mean_fitness": self.mean_fitness,
            "diversity": stats.diversity,
            "extinctions": stats.extinctions,
        }

    @property
    def best_organism(self) -> Optional[Organism]:
        alive_with_fitness = [o for o in self.population if o.alive and o.fitness]
        return (
            max(alive_with_fitness, key=lambda o: o.fitness.composite)
            if alive_with_fitness
            else None
        )

    @property
    def best_fitness(self) -> float:
        b = self.best_organism
        return b.fitness.composite if b and b.fitness else 0.0

    @property
    def mean_fitness(self) -> float:
        fits = [o.fitness.composite for o in self.population if o.fitness]
        return float(np.mean(fits)) if fits else 0.0

seed(genome)

Seed the population with an initial organism.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
618
619
620
621
622
623
624
def seed(self, genome: Genome) -> Organism:
    """Seed the population with an initial organism."""
    genome.compute_id()
    org = Organism(genome=genome, birth_generation=0)
    self.population.append(org)
    self.lineage.record(org, "seed")
    return org

replicate(parent)

Create a mutated child from a parent.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
def replicate(self, parent: Organism) -> Optional[Organism]:
    """Create a mutated child from a parent."""
    child_genome, mut_type = self.mutator.mutate(parent.genome)

    if self.industrial_mode:
        result = self.safety_guard.check(child_genome)
        if not result.passed:
            return None

    child = Organism(
        genome=child_genome,
        birth_generation=self.generation,
    )
    self.total_replications += 1
    self.lineage.record(child, mut_type.value)
    if len(self.population) < self.max_population:
        self.population.append(child)
    return child

replicate_crossover(parent_a, parent_b)

Create a child via crossover of two parents.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
def replicate_crossover(self, parent_a: Organism, parent_b: Organism) -> Optional[Organism]:
    """Create a child via crossover of two parents."""
    child_genome = self.crossover.crossover(parent_a.genome, parent_b.genome)

    if self.industrial_mode:
        result = self.safety_guard.check(child_genome)
        if not result.passed:
            return None

    child = Organism(
        genome=child_genome,
        birth_generation=self.generation,
    )
    self.total_replications += 1
    self.lineage.record(child, "crossover")
    if len(self.population) < self.max_population:
        self.population.append(child)
    return child

evaluate_all(metrics_fn)

Evaluate fitness for all living organisms.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
def evaluate_all(self, metrics_fn) -> None:
    """Evaluate fitness for all living organisms."""
    for org in self.population:
        if org.alive:
            metrics = metrics_fn(org.genome)
            org.fitness = self.evaluator.evaluate(org.genome, metrics)

            if self.industrial_mode:
                if org.fitness:
                    org.fitness.composite = self.bloat_penalizer.penalize(
                        org.fitness.composite, org.genome
                    )
                    org.fitness.composite = shared_fitness(org, self.population)
                if self.runtime_fault_config is not None:
                    self.verify_runtime_faults(org, self.runtime_fault_config)
                self.hall_of_fame.update(org)
                self.pareto_front.update(org)

verify_runtime_faults(organism, config=None)

Run seeded runtime fault diagnosis and apply bounded degradation.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
def verify_runtime_faults(
    self,
    organism: Organism,
    config: Optional[RuntimeFaultConfig] = None,
) -> RuntimeFaultCheck:
    """Run seeded runtime fault diagnosis and apply bounded degradation."""
    cfg = config or self.runtime_fault_config or RuntimeFaultConfig()
    streams = self._runtime_bitstreams(organism.genome, cfg)
    replay_seed = int(organism.genome.weight_seed + cfg.seed_offset)
    plan = self.degradation_policy.evaluate(
        streams,
        layer_id=organism.genome.genome_id or "unidentified",
        fault_model=cfg.fault_model,
        ber=cfg.ber,
        seed=replay_seed,
    )
    check = RuntimeFaultCheck.from_plan(organism, plan)
    organism.runtime_fault_checks.append(check)
    self._apply_runtime_fault_plan(organism, plan, cfg)
    return check

select_and_cull(survival_fraction=0.5)

Select fittest organisms, cull the rest. Elitism preserved.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
def select_and_cull(self, survival_fraction: float = 0.5) -> int:
    """Select fittest organisms, cull the rest. Elitism preserved."""
    if self.industrial_mode:
        killed = self.age_regulator.apply(self.population, self.generation)
        if self.extinction_detector.check(self.best_fitness):
            killed += self.extinction_detector.apply(self.population, self.mutator.rng)

    alive = [o for o in self.population if o.alive and o.fitness is not None]
    alive.sort(key=lambda o: o.fitness.composite, reverse=True)

    cutoff = max(self.elitism + 1, int(len(alive) * survival_fraction))
    killed = 0
    for org in alive[cutoff:]:
        org.alive = False
        self.graveyard.append(org)
        killed += 1

    self.population = [o for o in self.population if o.alive]
    return killed

evolve_generation(metrics_fn)

Run one full evolutionary generation.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
def evolve_generation(self, metrics_fn) -> Dict[str, Any]:
    """Run one full evolutionary generation."""
    self.generation += 1

    # 1. Evaluate
    self.evaluate_all(metrics_fn)

    # 2. Select + cull
    killed = self.select_and_cull()

    # 3. Replicate from survivors
    survivors = list(self.population)
    children_created = 0

    for i in range(len(survivors)):
        if len(self.population) >= self.max_population:
            break

        if self.industrial_mode:
            parent = self.tournament.select(survivors, self.mutator.rng)
            partner = self.tournament.select(survivors, self.mutator.rng)
        else:
            parent = survivors[i]
            partner = survivors[(i + 1) % len(survivors)] if len(survivors) > 1 else None

        if parent is None:
            continue

        child_added = None
        if partner and self.mutator.rng.random() < 0.3:
            child_added = self.replicate_crossover(parent, partner)
        else:
            child_added = self.replicate(parent)

        if child_added:
            children_created += 1

    stats = GenerationStats(
        generation=self.generation,
        population_size=len(self.population),
        best_fitness=self.best_fitness,
        mean_fitness=self.mean_fitness,
        diversity=population_diversity(self.population),
        extinctions=self.extinction_detector.extinction_count if self.industrial_mode else 0,
    )
    if self.industrial_mode:
        self.stats_tracker.record(stats)

    return {
        "generation": self.generation,
        "population_size": len(self.population),
        "killed": killed,
        "children": children_created,
        "best_fitness": self.best_fitness,
        "mean_fitness": self.mean_fitness,
        "diversity": stats.diversity,
        "extinctions": stats.extinctions,
    }

OrganismEmitter

Emits evolved organisms as NIR graph or Verilog.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
class OrganismEmitter:
    """Emits evolved organisms as NIR graph or Verilog."""

    @staticmethod
    def to_nir(genome: Genome) -> Dict[str, Any]:
        """Emit a simplified NIR-compatible graph dict."""
        nodes = {}
        for i in range(genome.topology.num_neurons):
            nodes[f"n{i}"] = {
                "type": "ArcaneNeuron",
                "tau_fast": genome.neuron.tau_fast,
                "tau_work": genome.neuron.tau_work,
                "tau_deep": genome.neuron.tau_deep,
                "theta": genome.neuron.theta,
                "gamma": genome.neuron.gamma,
                "delta_conf": genome.neuron.delta_conf,
                "kappa": genome.neuron.kappa,
                "w_inh": genome.neuron.w_inh,
            }
        edges = []
        rng = np.random.default_rng(genome.weight_seed)
        for i in range(genome.topology.num_neurons):
            for j in range(genome.topology.num_neurons):
                if i != j and rng.random() < genome.topology.connectivity:
                    edges.append(
                        {"from": f"n{i}", "to": f"n{j}", "weight_q88": int(rng.integers(0, 256))}
                    )
        return {
            "genome_id": genome.genome_id,
            "generation": genome.generation,
            "nodes": nodes,
            "edges": edges,
            "bitstream_length": genome.topology.bitstream_length,
        }

    @staticmethod
    def to_verilog(genome: Genome, module_name: Optional[str] = None) -> str:
        """Emit Verilog wrapper for the organism."""
        name = module_name or f"sc_organism_{genome.genome_id[:8]}"
        n = genome.topology.num_neurons
        bs = genome.topology.bitstream_length
        return textwrap.dedent(f"""\
// SC-NeuroCore — Evolved Organism: {genome.genome_id}
// Generation: {genome.generation} | Neurons: {n} | Bitstream: {bs}

module {name} #(
    parameter NUM_NEURONS = {n},
    parameter BITSTREAM_W = {bs},
    parameter TAU_FAST    = {int(genome.neuron.tau_fast)},
    parameter TAU_WORK    = {int(genome.neuron.tau_work)},
    parameter THETA_Q88   = {int(genome.neuron.theta * 256)}
)(
    input  wire                    clk,
    input  wire                    rst_n,
    input  wire [BITSTREAM_W-1:0]  sc_input  [0:NUM_NEURONS-1],
    output wire [BITSTREAM_W-1:0]  sc_output [0:NUM_NEURONS-1],
    output wire [NUM_NEURONS-1:0]  spike_out
);

    genvar i;
    generate
        for (i = 0; i < NUM_NEURONS; i = i + 1) begin : neuron_gen
            sc_lif_neuron #(
                .BITSTREAM_W(BITSTREAM_W),
                .THRESHOLD(THETA_Q88)
            ) u_neuron (
                .clk(clk),
                .rst_n(rst_n),
                .bitstream_in(sc_input[i]),
                .bitstream_out(sc_output[i]),
                .spike(spike_out[i])
            );
        end
    endgenerate

endmodule
""")

    @staticmethod
    def to_photonic_netlist(genome: Genome, pml_layers: int = 12) -> Dict[str, Any]:
        """Emit a photonic netlist compatible with the optics PhotonicCompiler."""
        return {
            "version": "1.0",
            "metadata": {
                "genome_id": genome.genome_id,
                "generation": genome.generation,
                "num_neurons": genome.topology.num_neurons,
            },
            "parameters": {
                "wavelength": 1.55e-6,
                "n_core": 3.48,
                "n_clad": 1.44,
                "pml_layers": pml_layers,
            },
            "waveguides": [
                {"id": f"wg_{i}", "width": 0.5, "length": 10.0}
                for i in range(genome.topology.num_neurons)
            ],
        }

to_nir(genome) staticmethod

Emit a simplified NIR-compatible graph dict.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
@staticmethod
def to_nir(genome: Genome) -> Dict[str, Any]:
    """Emit a simplified NIR-compatible graph dict."""
    nodes = {}
    for i in range(genome.topology.num_neurons):
        nodes[f"n{i}"] = {
            "type": "ArcaneNeuron",
            "tau_fast": genome.neuron.tau_fast,
            "tau_work": genome.neuron.tau_work,
            "tau_deep": genome.neuron.tau_deep,
            "theta": genome.neuron.theta,
            "gamma": genome.neuron.gamma,
            "delta_conf": genome.neuron.delta_conf,
            "kappa": genome.neuron.kappa,
            "w_inh": genome.neuron.w_inh,
        }
    edges = []
    rng = np.random.default_rng(genome.weight_seed)
    for i in range(genome.topology.num_neurons):
        for j in range(genome.topology.num_neurons):
            if i != j and rng.random() < genome.topology.connectivity:
                edges.append(
                    {"from": f"n{i}", "to": f"n{j}", "weight_q88": int(rng.integers(0, 256))}
                )
    return {
        "genome_id": genome.genome_id,
        "generation": genome.generation,
        "nodes": nodes,
        "edges": edges,
        "bitstream_length": genome.topology.bitstream_length,
    }

to_verilog(genome, module_name=None) staticmethod

Emit Verilog wrapper for the organism.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
    @staticmethod
    def to_verilog(genome: Genome, module_name: Optional[str] = None) -> str:
        """Emit Verilog wrapper for the organism."""
        name = module_name or f"sc_organism_{genome.genome_id[:8]}"
        n = genome.topology.num_neurons
        bs = genome.topology.bitstream_length
        return textwrap.dedent(f"""\
// SC-NeuroCore — Evolved Organism: {genome.genome_id}
// Generation: {genome.generation} | Neurons: {n} | Bitstream: {bs}

module {name} #(
    parameter NUM_NEURONS = {n},
    parameter BITSTREAM_W = {bs},
    parameter TAU_FAST    = {int(genome.neuron.tau_fast)},
    parameter TAU_WORK    = {int(genome.neuron.tau_work)},
    parameter THETA_Q88   = {int(genome.neuron.theta * 256)}
)(
    input  wire                    clk,
    input  wire                    rst_n,
    input  wire [BITSTREAM_W-1:0]  sc_input  [0:NUM_NEURONS-1],
    output wire [BITSTREAM_W-1:0]  sc_output [0:NUM_NEURONS-1],
    output wire [NUM_NEURONS-1:0]  spike_out
);

    genvar i;
    generate
        for (i = 0; i < NUM_NEURONS; i = i + 1) begin : neuron_gen
            sc_lif_neuron #(
                .BITSTREAM_W(BITSTREAM_W),
                .THRESHOLD(THETA_Q88)
            ) u_neuron (
                .clk(clk),
                .rst_n(rst_n),
                .bitstream_in(sc_input[i]),
                .bitstream_out(sc_output[i]),
                .spike(spike_out[i])
            );
        end
    endgenerate

endmodule
""")

to_photonic_netlist(genome, pml_layers=12) staticmethod

Emit a photonic netlist compatible with the optics PhotonicCompiler.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
@staticmethod
def to_photonic_netlist(genome: Genome, pml_layers: int = 12) -> Dict[str, Any]:
    """Emit a photonic netlist compatible with the optics PhotonicCompiler."""
    return {
        "version": "1.0",
        "metadata": {
            "genome_id": genome.genome_id,
            "generation": genome.generation,
            "num_neurons": genome.topology.num_neurons,
        },
        "parameters": {
            "wavelength": 1.55e-6,
            "n_core": 3.48,
            "n_clad": 1.44,
            "pml_layers": pml_layers,
        },
        "waveguides": [
            {"id": f"wg_{i}", "width": 0.5, "length": 10.0}
            for i in range(genome.topology.num_neurons)
        ],
    }

SafetyBounds dataclass

Constrains the mutation space to prevent runaway replication.

Enforces hard limits on genome parameters that could cause resource exhaustion or unsafe behaviour on FPGA tiles.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
@dataclass
class SafetyBounds:
    """Constrains the mutation space to prevent runaway replication.

    Enforces hard limits on genome parameters that could cause
    resource exhaustion or unsafe behaviour on FPGA tiles.
    """

    max_neurons: int = 1024
    min_neurons: int = 4
    max_layers: int = 16
    max_bitstream: int = 4096
    min_bitstream: int = 32
    max_connectivity: float = 1.0
    max_tau_deep: float = 100000.0
    max_replications_per_gen: int = 64

    def clamp(self, genome: Genome) -> Genome:
        genome.topology.num_neurons = int(
            np.clip(genome.topology.num_neurons, self.min_neurons, self.max_neurons)
        )
        genome.topology.num_layers = min(self.max_layers, max(1, genome.topology.num_layers))
        genome.topology.bitstream_length = int(
            np.clip(genome.topology.bitstream_length, self.min_bitstream, self.max_bitstream)
        )
        genome.topology.connectivity = float(
            np.clip(genome.topology.connectivity, 0.01, self.max_connectivity)
        )
        genome.neuron.tau_deep = min(self.max_tau_deep, genome.neuron.tau_deep)
        return genome

    def is_within_bounds(self, genome: Genome) -> bool:
        return (
            self.min_neurons <= genome.topology.num_neurons <= self.max_neurons
            and 1 <= genome.topology.num_layers <= self.max_layers
            and self.min_bitstream <= genome.topology.bitstream_length <= self.max_bitstream
        )

TileAllocation dataclass

Maps an organism to a physical FPGA tile.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
976
977
978
979
980
981
982
983
984
@dataclass
class TileAllocation:
    """Maps an organism to a physical FPGA tile."""

    organism_id: str
    tile_id: int
    partition_id: int = 0
    deployed: bool = False
    bitstream_hash: str = ""

TileDeploymentTracker

Tracks which organisms are deployed on which FPGA tiles.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
class TileDeploymentTracker:
    """Tracks which organisms are deployed on which FPGA tiles."""

    def __init__(self, num_tiles: int = 8):
        self.num_tiles = num_tiles
        self.allocations: Dict[int, Optional[TileAllocation]] = {i: None for i in range(num_tiles)}

    def deploy(self, organism: Organism, tile_id: int) -> TileAllocation:
        alloc = TileAllocation(
            organism_id=organism.genome.genome_id,
            tile_id=tile_id,
            deployed=True,
            bitstream_hash=organism.genome.genome_id,
        )
        self.allocations[tile_id] = alloc
        organism.tile_id = tile_id
        return alloc

    def evict(self, tile_id: int) -> None:
        self.allocations[tile_id] = None

    @property
    def free_tiles(self) -> List[int]:
        return [tid for tid, a in self.allocations.items() if a is None]

    @property
    def utilisation(self) -> float:
        used = sum(1 for a in self.allocations.values() if a is not None)
        return used / self.num_tiles if self.num_tiles > 0 else 0.0

HallOfFame

Maintains the top-N organisms across all generations.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
class HallOfFame:
    """Maintains the top-N organisms across all generations."""

    def __init__(self, max_size: int = 10):
        self.max_size = max_size
        self.entries: List[Tuple[float, Genome]] = []  # (fitness, genome)

    def update(self, organism: Organism) -> bool:
        if organism.fitness is None:
            return False
        fit = organism.fitness.composite
        self.entries.append((fit, copy.deepcopy(organism.genome)))
        self.entries.sort(key=lambda x: x[0], reverse=True)
        if len(self.entries) > self.max_size:
            self.entries = self.entries[: self.max_size]
        return True

    @property
    def best_fitness(self) -> float:
        return self.entries[0][0] if self.entries else 0.0

    @property
    def size(self) -> int:
        return len(self.entries)

Island dataclass

One sub-population (deme) in an island model.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1050
1051
1052
1053
1054
1055
1056
@dataclass
class Island:
    """One sub-population (deme) in an island model."""

    island_id: int
    population: List[Organism] = field(default_factory=list)
    best_fitness: float = 0.0

IslandModel

Multi-deme evolution with periodic migration.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
class IslandModel:
    """Multi-deme evolution with periodic migration."""

    def __init__(self, num_islands: int = 4, migration_rate: float = 0.1):
        self.islands = {i: Island(i) for i in range(num_islands)}
        self.migration_rate = migration_rate
        self.total_migrations: int = 0

    def add_organism(self, island_id: int, organism: Organism) -> None:
        self.islands[island_id].population.append(organism)

    def migrate(self, rng: np.random.Generator) -> int:
        """Migrate best organisms between random island pairs."""
        ids = list(self.islands.keys())
        if len(ids) < 2:
            return 0
        migrations = 0
        for src_id in ids:
            if rng.random() < self.migration_rate:
                dst_id = rng.choice([i for i in ids if i != src_id])
                src = self.islands[src_id]
                if src.population:
                    migrant = copy.deepcopy(src.population[0])
                    self.islands[dst_id].population.append(migrant)
                    migrations += 1
        self.total_migrations += migrations
        return migrations

    @property
    def total_population(self) -> int:
        return sum(len(isl.population) for isl in self.islands.values())

migrate(rng)

Migrate best organisms between random island pairs.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
def migrate(self, rng: np.random.Generator) -> int:
    """Migrate best organisms between random island pairs."""
    ids = list(self.islands.keys())
    if len(ids) < 2:
        return 0
    migrations = 0
    for src_id in ids:
        if rng.random() < self.migration_rate:
            dst_id = rng.choice([i for i in ids if i != src_id])
            src = self.islands[src_id]
            if src.population:
                migrant = copy.deepcopy(src.population[0])
                self.islands[dst_id].population.append(migrant)
                migrations += 1
    self.total_migrations += migrations
    return migrations

GenomeSerializer

Serializes/deserializes genomes for persistence.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
class GenomeSerializer:
    """Serializes/deserializes genomes for persistence."""

    @staticmethod
    def to_dict(genome: Genome) -> Dict[str, Any]:
        return {
            "genome_id": genome.genome_id,
            "parent_id": genome.parent_id,
            "generation": genome.generation,
            "weight_seed": genome.weight_seed,
            "identity_deep": genome.identity_deep,
            "vector": genome.to_vector().tolist(),
        }

    @staticmethod
    def from_dict(d: Dict[str, Any]) -> Genome:
        v = np.array(d["vector"])
        g = Genome.from_vector(v, d.get("generation", 0))
        g.genome_id = d.get("genome_id", "")
        g.parent_id = d.get("parent_id", "")
        g.weight_seed = d.get("weight_seed", 42)
        g.identity_deep = d.get("identity_deep", 0.0)
        return g

NoveltyArchive

Behavioural novelty archive for novelty search.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
class NoveltyArchive:
    """Behavioural novelty archive for novelty search."""

    def __init__(self, k_nearest: int = 5, threshold: float = 0.1):
        self.k_nearest = k_nearest
        self.threshold = threshold
        self.archive: List[np.ndarray] = []

    def novelty_score(self, behaviour: np.ndarray) -> float:
        if not self.archive:
            return 1.0
        dists = [float(np.linalg.norm(behaviour - a)) for a in self.archive]
        dists.sort()
        k = min(self.k_nearest, len(dists))
        return float(np.mean(dists[:k]))

    def maybe_add(self, behaviour: np.ndarray) -> bool:
        score = self.novelty_score(behaviour)
        if score > self.threshold:
            self.archive.append(behaviour.copy())
            return True
        return False

    @property
    def size(self) -> int:
        return len(self.archive)

ResourceBudget dataclass

Per-organism resource constraints.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
@dataclass
class ResourceBudget:
    """Per-organism resource constraints."""

    max_area_um2: float = 1e6
    max_power_mw: float = 100.0
    max_neurons: int = 1024

    def check(self, genome: Genome) -> Tuple[bool, List[str]]:
        violations = []
        if genome.topology.num_neurons > self.max_neurons:
            violations.append(f"neurons={genome.topology.num_neurons}>{self.max_neurons}")
        est_area = genome.topology.num_neurons * genome.topology.bitstream_length * 0.1
        if est_area > self.max_area_um2:
            violations.append(f"area={est_area:.0f}>{self.max_area_um2:.0f}")
        return (len(violations) == 0, violations)

ExtinctionDetector

Detects population stagnation and triggers extinction events.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
class ExtinctionDetector:
    """Detects population stagnation and triggers extinction events."""

    def __init__(self, stagnation_gens: int = 10, kill_fraction: float = 0.9):
        self.stagnation_gens = stagnation_gens
        self.kill_fraction = kill_fraction
        self._best_history: List[float] = []
        self.extinction_count: int = 0

    def check(self, best_fitness: float) -> bool:
        self._best_history.append(best_fitness)
        if len(self._best_history) < self.stagnation_gens:
            return False
        recent = self._best_history[-self.stagnation_gens :]
        improvement = max(recent) - min(recent)
        if improvement < 1e-6:
            self.extinction_count += 1
            return True
        return False

    def apply(self, population: List[Organism], rng: np.random.Generator) -> int:
        """Kill kill_fraction of population randomly."""
        n_kill = int(len(population) * self.kill_fraction)
        indices = rng.choice(len(population), size=min(n_kill, len(population)), replace=False)
        killed = 0
        for i in sorted(indices, reverse=True):
            population[i].alive = False
            killed += 1
        return killed

apply(population, rng)

Kill kill_fraction of population randomly.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1195
1196
1197
1198
1199
1200
1201
1202
1203
def apply(self, population: List[Organism], rng: np.random.Generator) -> int:
    """Kill kill_fraction of population randomly."""
    n_kill = int(len(population) * self.kill_fraction)
    indices = rng.choice(len(population), size=min(n_kill, len(population)), replace=False)
    killed = 0
    for i in sorted(indices, reverse=True):
        population[i].alive = False
        killed += 1
    return killed

CoevoOrganism dataclass

Organism with a co-evolutionary role.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1216
1217
1218
1219
1220
1221
1222
@dataclass
class CoevoOrganism:
    """Organism with a co-evolutionary role."""

    organism: Organism
    role: CoevoRole
    interaction_score: float = 0.0

CoevolutionArena

Runs predator-prey or symbiotic co-evolution.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
class CoevolutionArena:
    """Runs predator-prey or symbiotic co-evolution."""

    def __init__(self):
        self.predators: List[CoevoOrganism] = []
        self.prey: List[CoevoOrganism] = []

    def add_predator(self, organism: Organism) -> None:
        self.predators.append(CoevoOrganism(organism, CoevoRole.PREDATOR))

    def add_prey(self, organism: Organism) -> None:
        self.prey.append(CoevoOrganism(organism, CoevoRole.PREY))

    def evaluate_interactions(self) -> Dict[str, float]:
        """Evaluate predator-prey fitness from pairwise interactions."""
        results = {}
        for pred in self.predators:
            score = sum(
                1.0
                for prey in self.prey
                if pred.organism.genome.topology.num_neurons
                > prey.organism.genome.topology.num_neurons
            )
            pred.interaction_score = score / max(1, len(self.prey))
            results[pred.organism.genome.genome_id] = pred.interaction_score
        for prey_org in self.prey:
            score = sum(
                1.0
                for pred in self.predators
                if prey_org.organism.genome.topology.connectivity
                < pred.organism.genome.topology.connectivity
            )
            prey_org.interaction_score = score / max(1, len(self.predators))
            results[prey_org.organism.genome.genome_id] = prey_org.interaction_score
        return results

    @property
    def total_organisms(self) -> int:
        return len(self.predators) + len(self.prey)

evaluate_interactions()

Evaluate predator-prey fitness from pairwise interactions.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
def evaluate_interactions(self) -> Dict[str, float]:
    """Evaluate predator-prey fitness from pairwise interactions."""
    results = {}
    for pred in self.predators:
        score = sum(
            1.0
            for prey in self.prey
            if pred.organism.genome.topology.num_neurons
            > prey.organism.genome.topology.num_neurons
        )
        pred.interaction_score = score / max(1, len(self.prey))
        results[pred.organism.genome.genome_id] = pred.interaction_score
    for prey_org in self.prey:
        score = sum(
            1.0
            for pred in self.predators
            if prey_org.organism.genome.topology.connectivity
            < pred.organism.genome.topology.connectivity
        )
        prey_org.interaction_score = score / max(1, len(self.predators))
        results[prey_org.organism.genome.genome_id] = prey_org.interaction_score
    return results

SafetyCheckResult dataclass

Result of a formal safety check on emitted Verilog/NIR.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
@dataclass
class SafetyCheckResult:
    """Result of a formal safety check on emitted Verilog/NIR."""

    genome_id: str
    passed: bool
    violations: List[str] = field(default_factory=list)
    neuron_count_ok: bool = True
    connectivity_ok: bool = True
    bitstream_ok: bool = True

FormalSafetyGuard

Validates emitted organisms against safety constraints before deployment.

Links to the safety_cert module for IEC 61508 compliance.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
class FormalSafetyGuard:
    """Validates emitted organisms against safety constraints before deployment.

    Links to the safety_cert module for IEC 61508 compliance.
    """

    def __init__(self, bounds: Optional[SafetyBounds] = None):
        self.bounds = bounds or SafetyBounds()
        self.checked: int = 0
        self.rejected: int = 0

    def check(self, genome: Genome) -> SafetyCheckResult:
        self.checked += 1
        violations = []
        n_ok = genome.topology.num_neurons <= self.bounds.max_neurons
        c_ok = genome.topology.connectivity <= self.bounds.max_connectivity
        b_ok = genome.topology.bitstream_length <= self.bounds.max_bitstream

        if not n_ok:
            violations.append(f"neurons={genome.topology.num_neurons}>{self.bounds.max_neurons}")
        if not c_ok:
            violations.append(
                f"connectivity={genome.topology.connectivity}>{self.bounds.max_connectivity}"
            )
        if not b_ok:
            violations.append(
                f"bitstream={genome.topology.bitstream_length}>{self.bounds.max_bitstream}"
            )

        passed = len(violations) == 0
        if not passed:
            self.rejected += 1

        return SafetyCheckResult(
            genome_id=genome.genome_id,
            passed=passed,
            violations=violations,
            neuron_count_ok=n_ok,
            connectivity_ok=c_ok,
            bitstream_ok=b_ok,
        )

    @property
    def rejection_rate(self) -> float:
        return self.rejected / self.checked if self.checked > 0 else 0.0

TournamentSelector

Tournament selection with configurable pressure.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
class TournamentSelector:
    """Tournament selection with configurable pressure."""

    def __init__(self, tournament_size: int = 3):
        self.tournament_size = tournament_size

    def select(self, population: List[Organism], rng: np.random.Generator) -> Organism:
        candidates = rng.choice(
            len(population),
            size=min(self.tournament_size, len(population)),
            replace=False,
        )
        best = None
        best_fit = -1.0
        for idx in candidates:
            org = population[idx]
            fit = org.fitness.composite if org.fitness else 0.0
            if fit > best_fit:
                best_fit = fit
                best = org
        return best

    def select_n(
        self, population: List[Organism], n: int, rng: np.random.Generator
    ) -> List[Organism]:
        return [self.select(population, rng) for _ in range(n)]

ParetoFront

Maintains a non-dominated Pareto front.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
class ParetoFront:
    """Maintains a non-dominated Pareto front."""

    def __init__(self):
        self.front: List[Organism] = []

    def update(self, organism: Organism) -> bool:
        if organism.fitness is None:
            return False
        dominated_by = [
            o for o in self.front if o.fitness and dominates(o.fitness, organism.fitness)
        ]
        if dominated_by:
            return False
        self.front = [
            o for o in self.front if not (o.fitness and dominates(organism.fitness, o.fitness))
        ]
        self.front.append(organism)
        return True

    @property
    def size(self) -> int:
        return len(self.front)

AgeRegulator

Culls organisms that exceed a maximum lifespan.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
class AgeRegulator:
    """Culls organisms that exceed a maximum lifespan."""

    def __init__(self, max_age: int = 20):
        self.max_age = max_age

    def apply(self, population: List[Organism], current_generation: int) -> int:
        killed = 0
        for org in population:
            age = current_generation - org.birth_generation
            if age > self.max_age:
                org.alive = False
                killed += 1
        return killed

BloatMetrics dataclass

Measures genome complexity for bloat detection.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
@dataclass
class BloatMetrics:
    """Measures genome complexity for bloat detection."""

    total_params: int
    neuron_count: int
    layer_count: int
    connection_count: int
    bloat_score: float = 0.0

    @property
    def is_bloated(self) -> bool:
        return self.bloat_score > 1.0

BloatPenalizer

Penalizes fitness for bloated genomes.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
class BloatPenalizer:
    """Penalizes fitness for bloated genomes."""

    def __init__(self, penalty_weight: float = 0.1, threshold: float = 2.0):
        self.penalty_weight = penalty_weight
        self.threshold = threshold

    def penalize(self, fitness: float, genome: Genome) -> float:
        bm = compute_bloat(genome)
        if bm.bloat_score > self.threshold:
            excess = bm.bloat_score - self.threshold
            return fitness * max(0.1, 1.0 - self.penalty_weight * excess)
        return fitness

CPPNNode dataclass

One node in a CPPN network.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1492
1493
1494
1495
1496
1497
1498
@dataclass
class CPPNNode:
    """One node in a CPPN network."""

    node_id: int
    activation: ActivationFunc = ActivationFunc.LINEAR
    bias: float = 0.0

CPPNEdge dataclass

One edge in a CPPN network.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1501
1502
1503
1504
1505
1506
1507
1508
@dataclass
class CPPNEdge:
    """One edge in a CPPN network."""

    src: int
    dst: int
    weight: float = 1.0
    enabled: bool = True

CPPNGenome

Compositional Pattern Producing Network for developmental encoding.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
class CPPNGenome:
    """Compositional Pattern Producing Network for developmental encoding."""

    def __init__(self):
        self.nodes: List[CPPNNode] = [
            CPPNNode(0, ActivationFunc.LINEAR),  # input x
            CPPNNode(1, ActivationFunc.LINEAR),  # input y
            CPPNNode(2, ActivationFunc.SIGMOID),  # output
        ]
        self.edges: List[CPPNEdge] = [
            CPPNEdge(0, 2, 1.0),
            CPPNEdge(1, 2, 1.0),
        ]

    def query(self, x: float, y: float) -> float:
        """Query the CPPN at coordinates (x, y)."""
        values = {0: x, 1: y}
        for node in self.nodes[2:]:
            total = node.bias
            for edge in self.edges:
                if edge.dst == node.node_id and edge.enabled and edge.src in values:
                    total += edge.weight * values[edge.src]
            values[node.node_id] = self._activate(total, node.activation)
        return values.get(2, 0.0)

    @staticmethod
    def _activate(x: float, func: ActivationFunc) -> float:
        if func == ActivationFunc.SIN:
            return float(np.sin(x))
        if func == ActivationFunc.GAUSS:
            return float(np.exp(-x * x))
        if func == ActivationFunc.SIGMOID:
            return float(1.0 / (1.0 + np.exp(-np.clip(x, -10, 10))))
        if func == ActivationFunc.STEP:
            return 1.0 if x > 0 else 0.0
        return float(x)  # LINEAR

    def generate_weight_matrix(self, rows: int, cols: int) -> np.ndarray:
        """Generate a weight matrix by querying CPPN at grid positions."""
        w = np.zeros((rows, cols))
        for r in range(rows):
            for c in range(cols):
                x = 2.0 * r / max(1, rows - 1) - 1.0
                y = 2.0 * c / max(1, cols - 1) - 1.0
                w[r, c] = self.query(x, y)
        return w

    @property
    def num_nodes(self) -> int:
        return len(self.nodes)

    @property
    def num_edges(self) -> int:
        return len(self.edges)

query(x, y)

Query the CPPN at coordinates (x, y).

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
def query(self, x: float, y: float) -> float:
    """Query the CPPN at coordinates (x, y)."""
    values = {0: x, 1: y}
    for node in self.nodes[2:]:
        total = node.bias
        for edge in self.edges:
            if edge.dst == node.node_id and edge.enabled and edge.src in values:
                total += edge.weight * values[edge.src]
        values[node.node_id] = self._activate(total, node.activation)
    return values.get(2, 0.0)

generate_weight_matrix(rows, cols)

Generate a weight matrix by querying CPPN at grid positions.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1548
1549
1550
1551
1552
1553
1554
1555
1556
def generate_weight_matrix(self, rows: int, cols: int) -> np.ndarray:
    """Generate a weight matrix by querying CPPN at grid positions."""
    w = np.zeros((rows, cols))
    for r in range(rows):
        for c in range(cols):
            x = 2.0 * r / max(1, rows - 1) - 1.0
            y = 2.0 * c / max(1, cols - 1) - 1.0
            w[r, c] = self.query(x, y)
    return w

HWFitnessReport dataclass

Fitness feedback from actual FPGA execution.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
@dataclass
class HWFitnessReport:
    """Fitness feedback from actual FPGA execution."""

    genome_id: str
    fpga_cycles: int = 0
    fpga_power_mw: float = 0.0
    fpga_accuracy: float = 0.0
    fmax_mhz: float = 0.0
    timing_met: bool = True

    @property
    def hw_composite(self) -> float:
        time_score = min(1.0, 100.0 / max(1.0, self.fmax_mhz)) if self.fmax_mhz > 0 else 0.0
        return (
            0.5 * self.fpga_accuracy
            + 0.3 * (1.0 - time_score)
            + 0.2 * (1.0 if self.timing_met else 0.0)
        )

HWFitnessCollector

Collects HW fitness from deployed organisms.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
class HWFitnessCollector:
    """Collects HW fitness from deployed organisms."""

    def __init__(self):
        self.reports: Dict[str, HWFitnessReport] = {}

    def submit(self, report: HWFitnessReport) -> None:
        self.reports[report.genome_id] = report

    def get(self, genome_id: str) -> Optional[HWFitnessReport]:
        return self.reports.get(genome_id)

    @property
    def total_reports(self) -> int:
        return len(self.reports)

GenerationStats dataclass

Statistics for one generation.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
@dataclass
class GenerationStats:
    """Statistics for one generation."""

    generation: int
    population_size: int
    best_fitness: float
    mean_fitness: float
    diversity: float
    species_count: int = 0
    extinctions: int = 0

EvoStatisticsTracker

Records per-generation statistics for analytics.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
class EvoStatisticsTracker:
    """Records per-generation statistics for analytics."""

    def __init__(self):
        self.history: List[GenerationStats] = []

    def record(self, stats: GenerationStats) -> None:
        self.history.append(stats)

    @property
    def generations_tracked(self) -> int:
        return len(self.history)

    @property
    def fitness_trajectory(self) -> List[float]:
        return [s.best_fitness for s in self.history]

    @property
    def diversity_trajectory(self) -> List[float]:
        return [s.diversity for s in self.history]

    def improvement_rate(self) -> float:
        if len(self.history) < 2:
            return 0.0
        return self.history[-1].best_fitness - self.history[0].best_fitness

GenomeDiff dataclass

Structural diff between two genomes.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
@dataclass
class GenomeDiff:
    """Structural diff between two genomes."""

    neuron_delta: int
    layer_delta: int
    connectivity_delta: float
    tau_fast_delta: float
    tau_deep_delta: float
    total_param_changes: int

    @property
    def is_identical(self) -> bool:
        return self.total_param_changes == 0

ComplexityTracker

Tracks population complexity over generations.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
class ComplexityTracker:
    """Tracks population complexity over generations."""

    def __init__(self):
        self.history: List[Tuple[int, float, float]] = []  # (gen, mean, max)

    def record(self, generation: int, population: List[Organism]) -> None:
        if not population:
            return
        complexities = [genome_complexity(o.genome) for o in population]
        self.history.append(
            (
                generation,
                float(np.mean(complexities)),
                float(np.max(complexities)),
            )
        )

    @property
    def mean_trajectory(self) -> List[float]:
        return [h[1] for h in self.history]

    @property
    def is_complexifying(self) -> bool:
        if len(self.history) < 3:
            return False
        return self.history[-1][1] > self.history[0][1]

genomic_distance(a, b)

Normalised L1 distance between genome vectors.

Dispatches to the Rust evo_substrate_core.py_genomic_distance when the compiled extension is importable. The NumPy fallback is kept as the reference implementation and produces bit-exact identical values.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
def genomic_distance(a: Genome, b: Genome) -> float:
    """Normalised L1 distance between genome vectors.

    Dispatches to the Rust ``evo_substrate_core.py_genomic_distance`` when
    the compiled extension is importable. The NumPy fallback is kept as
    the reference implementation and produces bit-exact identical values.
    """
    va, vb = a.to_vector(), b.to_vector()
    if _HAS_RUST_EVO:
        return float(
            _ec.py_genomic_distance(
                np.ascontiguousarray(va, dtype=np.float64),
                np.ascontiguousarray(vb, dtype=np.float64),
            )
        )
    diffs = va - vb
    norms = np.abs(va) + np.abs(vb) + 1e-10
    return float(np.mean(np.abs(diffs) / norms))

assign_species(population, threshold=0.3)

Assign organisms to species by genomic distance.

First organism of each species is the representative.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
def assign_species(
    population: List[Organism],
    threshold: float = 0.3,
) -> Dict[int, List[Organism]]:
    """Assign organisms to species by genomic distance.

    First organism of each species is the representative.
    """
    species: Dict[int, List[Organism]] = {}
    representatives: Dict[int, Genome] = {}
    next_id = 0

    for org in population:
        placed = False
        for sid, rep in representatives.items():
            if genomic_distance(org.genome, rep) < threshold:
                species[sid].append(org)
                placed = True
                break
        if not placed:
            species[next_id] = [org]
            representatives[next_id] = org.genome
            next_id += 1

    return species

population_diversity(population)

Mean pairwise genomic distance (0 = clones, 1 = max diversity).

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
399
400
401
402
403
404
405
406
407
def population_diversity(population: List[Organism]) -> float:
    """Mean pairwise genomic distance (0 = clones, 1 = max diversity)."""
    if len(population) < 2:
        return 0.0
    dists = []
    for i in range(len(population)):
        for j in range(i + 1, len(population)):
            dists.append(genomic_distance(population[i].genome, population[j].genome))
    return float(np.mean(dists))

dominates(a, b)

True if a Pareto-dominates b.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
def dominates(a: FitnessResult, b: FitnessResult) -> bool:
    """True if a Pareto-dominates b."""
    vals_a = [a.accuracy, a.energy_score, a.latency_score]
    vals_b = [b.accuracy, b.energy_score, b.latency_score]
    at_least_one_better = False
    for va, vb in zip(vals_a, vals_b):
        if va < vb:
            return False
        if va > vb:
            at_least_one_better = True
    return at_least_one_better

compute_bloat(genome, baseline_neurons=16)

Compute bloat relative to a baseline.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1437
1438
1439
1440
1441
1442
1443
1444
1445
def compute_bloat(genome: Genome, baseline_neurons: int = 16) -> BloatMetrics:
    """Compute bloat relative to a baseline."""
    n = genome.topology.num_neurons
    l = genome.topology.num_layers
    conn = int(n * n * genome.topology.connectivity)
    total = n * 8 + l + conn  # rough param count
    baseline = baseline_neurons * 8 + 2 + int(baseline_neurons**2 * 0.3)
    score = total / max(1, baseline)
    return BloatMetrics(total, n, l, conn, score)

shared_fitness(organism, population, sigma=0.3)

Shared fitness: divide by niche count to prevent species domination.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
def shared_fitness(
    organism: Organism,
    population: List[Organism],
    sigma: float = 0.3,
) -> float:
    """Shared fitness: divide by niche count to prevent species domination."""
    if organism.fitness is None:
        return 0.0
    raw = organism.fitness.composite
    niche_count = sum(
        1.0 for other in population if genomic_distance(organism.genome, other.genome) < sigma
    )
    return raw / max(1.0, niche_count)

genome_diff(a, b)

Compute structural diff between two genomes.

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
def genome_diff(a: Genome, b: Genome) -> GenomeDiff:
    """Compute structural diff between two genomes."""
    va, vb = a.to_vector(), b.to_vector()
    changes = int(np.sum(np.abs(va - vb) > 1e-8))
    return GenomeDiff(
        neuron_delta=b.topology.num_neurons - a.topology.num_neurons,
        layer_delta=b.topology.num_layers - a.topology.num_layers,
        connectivity_delta=b.topology.connectivity - a.topology.connectivity,
        tau_fast_delta=b.neuron.tau_fast - a.neuron.tau_fast,
        tau_deep_delta=b.neuron.tau_deep - a.neuron.tau_deep,
        total_param_changes=changes,
    )

genome_complexity(genome)

Measure evolved complexity (information-theoretic).

Source code in src/sc_neurocore/evo_substrate/evo_substrate.py
Python
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
def genome_complexity(genome: Genome) -> float:
    """Measure evolved complexity (information-theoretic)."""
    v = genome.to_vector()
    v_norm = v / (np.abs(v).max() + 1e-10)
    v_pos = np.abs(v_norm) + 1e-10
    v_pos = v_pos / v_pos.sum()
    entropy = -float(np.sum(v_pos * np.log2(v_pos)))
    topology_complexity = (
        genome.topology.num_neurons * genome.topology.num_layers * genome.topology.connectivity
    )
    return entropy + np.log2(1 + topology_complexity)