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 combines the Shannon entropy of the normalised absolute genome coordinates with a topology term:

$$ C(g) = H!\left(\frac{|\mathbf{g}|}{\lVert\mathbf{g}\rVert_1}\right) + \log_2!\left(1 + N_{\text{neurons}}N_{\text{layers}}c\right). $$

: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 demonstration 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

TournamentSelector.select() fails closed on empty populations before RNG sampling, and select_n() propagates the same validation boundary for batched selection.

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 neuron/layer/connectivity/time-constant deltas + changed-parameter count
:func:genome_diff structural deltas and vector coordinates changed above $10^{-8}$
:func:genome_complexity coordinate entropy + $\log_2(1 + NLC)$ topology term

6.8 Emission

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

7. Verified diagnostic benchmarks

The committed schema-v2 artefact was captured on 2026-07-12 with CPython 3.12.3 and NumPy 2.2.6 on an Intel i5-11600K workstation. Each value below is the median of 30 samples after two warmups. The process was pinned to one CPU, but the host had no kernel-reserved isolated cores, used the powersave governor, and had load averages near 22. These timings are local regression context only, not publishable throughput claims.

Operation Median latency Diagnostic rate
MutationEngine.mutate 217.10 µs 4 606 ops/s
CrossoverEngine.crossover 111.70 µs 8 953 ops/s
genomic_distance (19-D) 23.08 µs 43 330 ops/s
FormalSafetyGuard.check 2.00 µs 500 541 ops/s
assign_species (n=64, $\theta=0.3$) 1.81 ms 552 ops/s
ReplicationEngine.evolve_generation (pop=32, industrial) 16.28 ms 61 gen/s

benchmarks/bench_evo_substrate.py records all raw samples, summary statistics, source-tree SHA-256, runtime versions, affinity, governor, frequency, and host-load evidence in benchmarks/results/bench_evo_substrate.json. Rerun on reserved isolated cores before using the numbers in a release or publication.

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.

The source-bound schema-v2 comparison was captured on 2026-07-12 via benchmarks/bench_evo_substrate_multilang.py. It interleaves 30 samples per backend after two warmups; each sample executes 100 000 calls over 19-D Float64 vectors. All five required backends completed. The same non-exclusive, loaded-host caveat applies, so the medians below are diagnostic only.

Kernel (ns/call, dim=19) Rust Julia Go Mojo Python
genomic_distance 692.8 36.0 80.0 34.8 21 347.4
crossover_uniform 1 508.1 131.3 161.7 323.8 3 705.1
point_mutation 1 205.3 707.2 176.8 328.3 12 373.0

How to read this. Rust includes the PyO3 boundary. Julia, Go, and Mojo are standalone kernel processes and are parity references rather than Python-callable inner-loop backends. The raw samples, toolchain context, source digest, and empty unavailable set are committed in benchmarks/results/bench_evo_substrate_multilang.json.

From the Python orchestration's perspective, Rust is the only directly accessible compiled kernel because Julia, Go, and Mojo require subprocess dispatch. In this loaded diagnostic run, the Rust distance median was about 30.8 times lower than the Python reference median; this ratio is not a production claim.

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 responsibility-specific suites matching tests/test_evo_substrate/test_multilang_parity_*.py assert these four-way relationships. The benchmark producer fails closed when a required backend is missing; test environments may skip an unavailable optional toolchain.

7.3.2 Historical whole-process timing (seed=7, pop=16, 10 gens)

The figures below came from a 2026-04-20 exploratory run without the schema-v2 host-load and source-binding evidence now required. They explain dispatch-cost shape only and must not be used as release, regression, or publication evidence.

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 --manifest-path crates/evo_substrate_core/Cargo.toml — 17 unit tests.
  • Julia: JULIA_DEPOT_PATH=build/julia-depot julia --project=src/sc_neurocore/accel/julia/evo_substrate src/sc_neurocore/accel/julia/evo_substrate/test_evo_runner.jl — 17 unit tests (PRNG, roundtrip, fitness, safety, determinism).
  • Go: (cd src/sc_neurocore/accel/go/evo_substrate && go test -v ./...) — 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.
  • Cross-language parity: JULIA_DEPOT_PATH=build/julia-depot pytest tests/test_evo_substrate/test_multilang_parity_*.py — 18 tests (schema, Rust↔Julia bit-exact, Rust↔Go tolerance, Rust↔Mojo structure, determinism).

Interpretation. In the current loaded-host artefact, safety checks and distance computations have 2.00 µs and 23.08 µs medians, while a 32-organism generation has a 16.28 ms median. Mutation and crossover remain the slower Python inner operations because they allocate NumPy arrays per call. Treat these relationships as local regression context until the benchmark is rerun on reserved isolated cores.


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

  • Public compatibility facade: src/sc_neurocore/evo_substrate/evo_substrate.py (156 LOC).
  • Implementation: 14 responsibility modules under src/sc_neurocore/evo_substrate/; the largest is replication.py (304 LOC).
  • Tests: 54 focused test and support modules under tests/test_evo_substrate/; the largest is test_replication_replication_engine.py (265 LOC).
  • Demo: examples/16_evo_substrate_demo.py.
  • Benchmarks: benchmarks/bench_evo_substrate.py and benchmarks/bench_evo_substrate_multilang.py.

sc_neurocore.evo_substrate.evo_substrate

Preserve the original evolutionary-substrate API over focused modules.

New code may import from sc_neurocore.evo_substrate or the focused responsibility modules. Historical imports and pickle-qualified names remain stable.

TileAllocation dataclass

Maps an organism to a physical FPGA tile.

Source code in src/sc_neurocore/evo_substrate/deployment.py
Python
19
20
21
22
23
24
25
26
27
@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/deployment.py
Python
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class TileDeploymentTracker:
    """Tracks which organisms are deployed on which FPGA tiles."""

    def __init__(self, num_tiles: int = 8) -> None:
        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:
        """Assign an organism to a tile and return the recorded allocation."""
        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:
        """Mark a tile as free without mutating the former organism."""
        self.allocations[tile_id] = None

    @property
    def free_tiles(self) -> List[int]:
        """Return tile identifiers with no active allocation."""
        return [tid for tid, a in self.allocations.items() if a is None]

    @property
    def utilisation(self) -> float:
        """Return the fraction of tiles carrying an allocation."""
        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

free_tiles property

Return tile identifiers with no active allocation.

utilisation property

Return the fraction of tiles carrying an allocation.

deploy(organism, tile_id)

Assign an organism to a tile and return the recorded allocation.

Source code in src/sc_neurocore/evo_substrate/deployment.py
Python
37
38
39
40
41
42
43
44
45
46
47
def deploy(self, organism: Organism, tile_id: int) -> TileAllocation:
    """Assign an organism to a tile and return the recorded allocation."""
    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

evict(tile_id)

Mark a tile as free without mutating the former organism.

Source code in src/sc_neurocore/evo_substrate/deployment.py
Python
49
50
51
def evict(self, tile_id: int) -> None:
    """Mark a tile as free without mutating the former organism."""
    self.allocations[tile_id] = None

ActivationFunc

Bases: Enum

Select the nonlinear response used by a CPPN node.

Source code in src/sc_neurocore/evo_substrate/development.py
Python
20
21
22
23
24
25
26
27
class ActivationFunc(Enum):
    """Select the nonlinear response used by a CPPN node."""

    SIN = "sin"
    GAUSS = "gauss"
    LINEAR = "linear"
    SIGMOID = "sigmoid"
    STEP = "step"

CPPNEdge dataclass

One edge in a CPPN network.

Source code in src/sc_neurocore/evo_substrate/development.py
Python
39
40
41
42
43
44
45
46
@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/development.py
Python
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 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
class CPPNGenome:
    """Compositional Pattern Producing Network for developmental encoding."""

    def __init__(self) -> None:
        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[Any, Any]:
        """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 the number of nodes in the CPPN graph."""
        return len(self.nodes)

    @property
    def num_edges(self) -> int:
        """Return the number of edges in the CPPN graph."""
        return len(self.edges)

num_nodes property

Return the number of nodes in the CPPN graph.

num_edges property

Return the number of edges in the CPPN graph.

query(x, y)

Query the CPPN at coordinates (x, y).

Source code in src/sc_neurocore/evo_substrate/development.py
Python
63
64
65
66
67
68
69
70
71
72
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/development.py
Python
86
87
88
89
90
91
92
93
94
def generate_weight_matrix(self, rows: int, cols: int) -> np.ndarray[Any, Any]:
    """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

CPPNNode dataclass

One node in a CPPN network.

Source code in src/sc_neurocore/evo_substrate/development.py
Python
30
31
32
33
34
35
36
@dataclass
class CPPNNode:
    """One node in a CPPN network."""

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

CoevoOrganism dataclass

Organism with a co-evolutionary role.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
142
143
144
145
146
147
148
@dataclass
class CoevoOrganism:
    """Organism with a co-evolutionary role."""

    organism: Organism
    role: CoevoRole
    interaction_score: float = 0.0

CoevoRole dataclass

Bases: Enum

Identify an organism's role in a co-evolutionary interaction.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
133
134
135
136
137
138
139
@dataclass
class CoevoRole(Enum):
    """Identify an organism's role in a co-evolutionary interaction."""

    PREDATOR = "predator"
    PREY = "prey"
    SYMBIONT = "symbiont"

CoevolutionArena

Runs predator-prey or symbiotic co-evolution.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
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
182
183
184
185
186
187
188
189
190
191
192
class CoevolutionArena:
    """Runs predator-prey or symbiotic co-evolution."""

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

    def add_predator(self, organism: Organism) -> None:
        """Add an organism to the predator population."""
        self.predators.append(CoevoOrganism(organism, CoevoRole.PREDATOR))

    def add_prey(self, organism: Organism) -> None:
        """Add an organism to the prey population."""
        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 the combined predator and prey population."""
        return len(self.predators) + len(self.prey)

total_organisms property

Return the combined predator and prey population.

add_predator(organism)

Add an organism to the predator population.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
158
159
160
def add_predator(self, organism: Organism) -> None:
    """Add an organism to the predator population."""
    self.predators.append(CoevoOrganism(organism, CoevoRole.PREDATOR))

add_prey(organism)

Add an organism to the prey population.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
162
163
164
def add_prey(self, organism: Organism) -> None:
    """Add an organism to the prey population."""
    self.prey.append(CoevoOrganism(organism, CoevoRole.PREY))

evaluate_interactions()

Evaluate predator-prey fitness from pairwise interactions.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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

ExtinctionDetector

Detects population stagnation and triggers extinction events.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
class ExtinctionDetector:
    """Detects population stagnation and triggers extinction events."""

    def __init__(self, stagnation_gens: int = 10, kill_fraction: float = 0.9) -> None:
        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:
        """Record fitness and report whether recent progress stagnated."""
        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

check(best_fitness)

Record fitness and report whether recent progress stagnated.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
107
108
109
110
111
112
113
114
115
116
117
def check(self, best_fitness: float) -> bool:
    """Record fitness and report whether recent progress stagnated."""
    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

apply(population, rng)

Kill kill_fraction of population randomly.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
119
120
121
122
123
124
125
126
127
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

Island dataclass

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

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
23
24
25
26
27
28
29
@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/ecology.py
Python
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class IslandModel:
    """Multi-deme evolution with periodic migration."""

    def __init__(self, num_islands: int = 4, migration_rate: float = 0.1) -> None:
        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:
        """Append an organism to the selected island population."""
        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 the number of organisms across every island."""
        return sum(len(isl.population) for isl in self.islands.values())

total_population property

Return the number of organisms across every island.

add_organism(island_id, organism)

Append an organism to the selected island population.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
40
41
42
def add_organism(self, island_id: int, organism: Organism) -> None:
    """Append an organism to the selected island population."""
    self.islands[island_id].population.append(organism)

migrate(rng)

Migrate best organisms between random island pairs.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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

NoveltyArchive

Behavioural novelty archive for novelty search.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class NoveltyArchive:
    """Behavioural novelty archive for novelty search."""

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

    def novelty_score(self, behaviour: np.ndarray[Any, Any]) -> float:
        """Return mean distance to the nearest archived behaviours."""
        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[Any, Any]) -> bool:
        """Archive a copied behaviour when its novelty exceeds the threshold."""
        score = self.novelty_score(behaviour)
        if score > self.threshold:
            self.archive.append(behaviour.copy())
            return True
        return False

    @property
    def size(self) -> int:
        """Return the number of archived behaviour vectors."""
        return len(self.archive)

size property

Return the number of archived behaviour vectors.

novelty_score(behaviour)

Return mean distance to the nearest archived behaviours.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
75
76
77
78
79
80
81
82
def novelty_score(self, behaviour: np.ndarray[Any, Any]) -> float:
    """Return mean distance to the nearest archived behaviours."""
    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]))

maybe_add(behaviour)

Archive a copied behaviour when its novelty exceeds the threshold.

Source code in src/sc_neurocore/evo_substrate/ecology.py
Python
84
85
86
87
88
89
90
def maybe_add(self, behaviour: np.ndarray[Any, Any]) -> bool:
    """Archive a copied behaviour when its novelty exceeds the threshold."""
    score = self.novelty_score(behaviour)
    if score > self.threshold:
        self.archive.append(behaviour.copy())
        return True
    return False

OrganismEmitter

Emits evolved organisms as NIR graph or Verilog.

Source code in src/sc_neurocore/evo_substrate/emission.py
Python
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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/emission.py
Python
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@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/emission.py
Python
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
    @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/emission.py
Python
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@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)
        ],
    }

FitnessEvaluator

Evaluates organism fitness from simulation metrics.

Source code in src/sc_neurocore/evo_substrate/fitness.py
Python
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class FitnessEvaluator:
    """Evaluates organism fitness from simulation metrics."""

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

    def evaluate(self, genome: Genome, metrics: Dict[str, float]) -> FitnessResult:
        """Evaluate one genome from simulation metrics and hardware proxies.

        Parameters
        ----------
        genome
            Genome whose topology determines energy and latency proxies.
        metrics
            Simulation metrics; the optional accuracy key is in [0, 1].
        """
        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

evaluate(genome, metrics)

Evaluate one genome from simulation metrics and hardware proxies.

Parameters

genome Genome whose topology determines energy and latency proxies. metrics Simulation metrics; the optional accuracy key is in [0, 1].

Source code in src/sc_neurocore/evo_substrate/fitness.py
Python
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def evaluate(self, genome: Genome, metrics: Dict[str, float]) -> FitnessResult:
    """Evaluate one genome from simulation metrics and hardware proxies.

    Parameters
    ----------
    genome
        Genome whose topology determines energy and latency proxies.
    metrics
        Simulation metrics; the optional accuracy key is in [0, 1].
    """
    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

FitnessResult dataclass

Fitness evaluation result.

Source code in src/sc_neurocore/evo_substrate/fitness.py
Python
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@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:
        """Update and return the weighted three-objective fitness.

        Parameters
        ----------
        w_acc, w_energy, w_latency
            Weights for accuracy, energy, and latency scores.
        """
        self.composite = (
            w_acc * self.accuracy + w_energy * self.energy_score + w_latency * self.latency_score
        )
        return self.composite

compute_composite(w_acc=0.5, w_energy=0.3, w_latency=0.2)

Update and return the weighted three-objective fitness.

Parameters

w_acc, w_energy, w_latency Weights for accuracy, energy, and latency scores.

Source code in src/sc_neurocore/evo_substrate/fitness.py
Python
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def compute_composite(
    self, w_acc: float = 0.5, w_energy: float = 0.3, w_latency: float = 0.2
) -> float:
    """Update and return the weighted three-objective fitness.

    Parameters
    ----------
    w_acc, w_energy, w_latency
        Weights for accuracy, energy, and latency scores.
    """
    self.composite = (
        w_acc * self.accuracy + w_energy * self.energy_score + w_latency * self.latency_score
    )
    return self.composite

FitnessType

Bases: Enum

Identify the objective exposed by a fitness evaluation.

Source code in src/sc_neurocore/evo_substrate/fitness.py
Python
20
21
22
23
24
25
26
class FitnessType(Enum):
    """Identify the objective exposed by a fitness evaluation."""

    ACCURACY = "accuracy"
    ENERGY = "energy"
    LATENCY = "latency"
    COMPOSITE = "composite"

HWFitnessCollector

Collects HW fitness from deployed organisms.

Source code in src/sc_neurocore/evo_substrate/fitness.py
Python
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
class HWFitnessCollector:
    """Collects HW fitness from deployed organisms."""

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

    def submit(self, report: HWFitnessReport) -> None:
        """Store the latest FPGA fitness report for one genome."""
        self.reports[report.genome_id] = report

    def get(self, genome_id: str) -> Optional[HWFitnessReport]:
        """Return the latest report for the genome, if submitted."""
        return self.reports.get(genome_id)

    @property
    def total_reports(self) -> int:
        """Return the number of genomes with submitted FPGA reports."""
        return len(self.reports)

total_reports property

Return the number of genomes with submitted FPGA reports.

submit(report)

Store the latest FPGA fitness report for one genome.

Source code in src/sc_neurocore/evo_substrate/fitness.py
Python
114
115
116
def submit(self, report: HWFitnessReport) -> None:
    """Store the latest FPGA fitness report for one genome."""
    self.reports[report.genome_id] = report

get(genome_id)

Return the latest report for the genome, if submitted.

Source code in src/sc_neurocore/evo_substrate/fitness.py
Python
118
119
120
def get(self, genome_id: str) -> Optional[HWFitnessReport]:
    """Return the latest report for the genome, if submitted."""
    return self.reports.get(genome_id)

HWFitnessReport dataclass

Fitness feedback from actual FPGA execution.

Source code in src/sc_neurocore/evo_substrate/fitness.py
Python
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
@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:
        """Return the weighted accuracy, clock, and timing-closure score."""
        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)
        )

hw_composite property

Return the weighted accuracy, clock, and timing-closure score.

Genome dataclass

Complete genome for an evolving SC organism.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
@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[Any, Any]:
        """Return the canonical 19-value topology-neuron-plasticity vector."""
        return np.concatenate(
            [
                self.topology.to_vector(),
                self.neuron.to_vector(),
                self.plasticity.to_vector(),
            ]
        )

    @classmethod
    def from_vector(cls, v: np.ndarray[Any, Any], gen: int = 0) -> Genome:
        """Construct a genome from its canonical parameter vector.

        Parameters
        ----------
        v
            Nineteen-value canonical genome vector.
        gen
            Generation assigned to the reconstructed 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 the number of values in the canonical genome vector."""
        return len(self.to_vector())

    def compute_id(self) -> str:
        """Set and return the 12-hex SHA-256 prefix of the genome vector."""
        h = hashlib.sha256(self.to_vector().tobytes())
        self.genome_id = h.hexdigest()[:12]
        return self.genome_id

vector_dim property

Return the number of values in the canonical genome vector.

to_vector()

Return the canonical 19-value topology-neuron-plasticity vector.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
166
167
168
169
170
171
172
173
174
def to_vector(self) -> np.ndarray[Any, Any]:
    """Return the canonical 19-value topology-neuron-plasticity vector."""
    return np.concatenate(
        [
            self.topology.to_vector(),
            self.neuron.to_vector(),
            self.plasticity.to_vector(),
        ]
    )

from_vector(v, gen=0) classmethod

Construct a genome from its canonical parameter vector.

Parameters

v Nineteen-value canonical genome vector. gen Generation assigned to the reconstructed genome.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
@classmethod
def from_vector(cls, v: np.ndarray[Any, Any], gen: int = 0) -> Genome:
    """Construct a genome from its canonical parameter vector.

    Parameters
    ----------
    v
        Nineteen-value canonical genome vector.
    gen
        Generation assigned to the reconstructed 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]),
    )

compute_id()

Set and return the 12-hex SHA-256 prefix of the genome vector.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
199
200
201
202
203
def compute_id(self) -> str:
    """Set and return the 12-hex SHA-256 prefix of the genome vector."""
    h = hashlib.sha256(self.to_vector().tobytes())
    self.genome_id = h.hexdigest()[:12]
    return self.genome_id

GenomeSerializer

Serializes/deserializes genomes for persistence.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
class GenomeSerializer:
    """Serializes/deserializes genomes for persistence."""

    @staticmethod
    def to_dict(genome: Genome) -> Dict[str, Any]:
        """Return a JSON-ready mapping that preserves genome identity fields."""
        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:
        """Reconstruct a genome from :meth:`to_dict` output."""
        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

to_dict(genome) staticmethod

Return a JSON-ready mapping that preserves genome identity fields.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
209
210
211
212
213
214
215
216
217
218
219
@staticmethod
def to_dict(genome: Genome) -> Dict[str, Any]:
    """Return a JSON-ready mapping that preserves genome identity fields."""
    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(),
    }

from_dict(d) staticmethod

Reconstruct a genome from :meth:to_dict output.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
221
222
223
224
225
226
227
228
229
230
@staticmethod
def from_dict(d: Dict[str, Any]) -> Genome:
    """Reconstruct a genome from :meth:`to_dict` output."""
    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

NeuronGene dataclass

Encodes neuron-level parameters (ArcaneNeuron-compatible).

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 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
106
107
@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[Any, Any]:
        """Serialise neuron kinetics in their canonical eight-value order."""
        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[Any, Any]) -> NeuronGene:
        """Construct neuron kinetics while enforcing finite lower bounds.

        Parameters
        ----------
        v
            Eight values ordered as the documented neuron parameter block.
        """
        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)),
        )

to_vector()

Serialise neuron kinetics in their canonical eight-value order.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def to_vector(self) -> np.ndarray[Any, Any]:
    """Serialise neuron kinetics in their canonical eight-value order."""
    return np.array(
        [
            self.tau_fast,
            self.tau_work,
            self.tau_deep,
            self.theta,
            self.gamma,
            self.delta_conf,
            self.kappa,
            self.w_inh,
        ]
    )

from_vector(v) classmethod

Construct neuron kinetics while enforcing finite lower bounds.

Parameters

v Eight values ordered as the documented neuron parameter block.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
@classmethod
def from_vector(cls, v: np.ndarray[Any, Any]) -> NeuronGene:
    """Construct neuron kinetics while enforcing finite lower bounds.

    Parameters
    ----------
    v
        Eight values ordered as the documented neuron parameter block.
    """
    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/genome.py
Python
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
147
148
149
150
@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[Any, Any]:
        """Serialise plasticity fields in their canonical six-value order."""
        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[Any, Any]) -> PlasticityGene:
        """Construct plasticity parameters while enforcing valid rates.

        Parameters
        ----------
        v
            Six values ordered as the documented plasticity parameter block.
        """
        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])),
        )

to_vector()

Serialise plasticity fields in their canonical six-value order.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
121
122
123
124
125
126
127
128
129
130
131
132
def to_vector(self) -> np.ndarray[Any, Any]:
    """Serialise plasticity fields in their canonical six-value order."""
    return np.array(
        [
            self.stdp_lr,
            self.stdp_tau_plus,
            self.stdp_tau_minus,
            self.stp_u_base,
            self.homeostatic_rate,
            self.meta_sensitivity,
        ]
    )

from_vector(v) classmethod

Construct plasticity parameters while enforcing valid rates.

Parameters

v Six values ordered as the documented plasticity parameter block.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
@classmethod
def from_vector(cls, v: np.ndarray[Any, Any]) -> PlasticityGene:
    """Construct plasticity parameters while enforcing valid rates.

    Parameters
    ----------
    v
        Six values ordered as the documented plasticity parameter block.
    """
    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])),
    )

TopologyGene dataclass

Encodes the network topology.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@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[Any, Any]:
        """Serialise topology fields in their canonical five-value order."""
        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[Any, Any]) -> TopologyGene:
        """Construct a topology gene while enforcing physical parameter bounds.

        Parameters
        ----------
        v
            Five values ordered as neuron count, layer count, connectivity,
            recurrent fraction, and bitstream length.
        """
        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])),
        )

to_vector()

Serialise topology fields in their canonical five-value order.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
30
31
32
33
34
35
36
37
38
39
40
def to_vector(self) -> np.ndarray[Any, Any]:
    """Serialise topology fields in their canonical five-value order."""
    return np.array(
        [
            self.num_neurons,
            self.num_layers,
            self.connectivity,
            self.recurrent_fraction,
            self.bitstream_length,
        ]
    )

from_vector(v) classmethod

Construct a topology gene while enforcing physical parameter bounds.

Parameters

v Five values ordered as neuron count, layer count, connectivity, recurrent fraction, and bitstream length.

Source code in src/sc_neurocore/evo_substrate/genome.py
Python
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@classmethod
def from_vector(cls, v: np.ndarray[Any, Any]) -> TopologyGene:
    """Construct a topology gene while enforcing physical parameter bounds.

    Parameters
    ----------
    v
        Five values ordered as neuron count, layer count, connectivity,
        recurrent fraction, and bitstream length.
    """
    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])),
    )

LineageRecord dataclass

One entry in the ancestry log.

Source code in src/sc_neurocore/evo_substrate/lineage.py
Python
19
20
21
22
23
24
25
26
27
@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/lineage.py
Python
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class LineageTracker:
    """Tracks ancestry graph for all organisms."""

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

    def record(self, organism: Organism, mutation_type: str = "seed") -> None:
        """Append one ancestry record and index it by genome identifier."""
        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 the number of recorded organisms."""
        return len(self.records)

num_records property

Return the number of recorded organisms.

record(organism, mutation_type='seed')

Append one ancestry record and index it by genome identifier.

Source code in src/sc_neurocore/evo_substrate/lineage.py
Python
37
38
39
40
41
42
43
44
45
46
47
48
def record(self, organism: Organism, mutation_type: str = "seed") -> None:
    """Append one ancestry record and index it by genome identifier."""
    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

get_ancestors(genome_id)

Walk the ancestry chain to the root.

Source code in src/sc_neurocore/evo_substrate/lineage.py
Python
50
51
52
53
54
55
56
57
58
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

Organism dataclass

One evolving SC organism.

Source code in src/sc_neurocore/evo_substrate/organism.py
Python
23
24
25
26
27
28
29
30
31
32
33
@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)

ReplicationEngine

Manages organism reproduction, mutation, and deployment.

Selection → Replication → Mutation → Safety Check → Deploy

Source code in src/sc_neurocore/evo_substrate/replication.py
Python
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 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
106
107
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
147
148
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
182
183
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
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
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,
    ) -> 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: Callable[[Genome], Dict[str, float]]) -> 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:
                    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 if o.fitness is not None else float("-inf"),
            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: Callable[[Genome], Dict[str, float]]) -> 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

            parent: Optional[Organism]
            partner: Optional[Organism]
            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: Optional[Organism] = 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]:
        """Return the highest-fitness living organism, if one is evaluated."""
        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 o.fitness is not None else float("-inf"),
            )
            if alive_with_fitness
            else None
        )

    @property
    def best_fitness(self) -> float:
        """Return the best living composite fitness, or zero when unavailable."""
        b = self.best_organism
        return b.fitness.composite if b and b.fitness else 0.0

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

best_organism property

Return the highest-fitness living organism, if one is evaluated.

best_fitness property

Return the best living composite fitness, or zero when unavailable.

mean_fitness property

Return mean composite fitness across evaluated organisms.

seed(genome)

Seed the population with an initial organism.

Source code in src/sc_neurocore/evo_substrate/replication.py
Python
85
86
87
88
89
90
91
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/replication.py
Python
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
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/replication.py
Python
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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/replication.py
Python
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def evaluate_all(self, metrics_fn: Callable[[Genome], Dict[str, float]]) -> 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:
                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/replication.py
Python
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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/replication.py
Python
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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 if o.fitness is not None else float("-inf"),
        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/replication.py
Python
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
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
def evolve_generation(self, metrics_fn: Callable[[Genome], Dict[str, float]]) -> 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

        parent: Optional[Organism]
        partner: Optional[Organism]
        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: Optional[Organism] = 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,
    }

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/safety.py
Python
148
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
182
183
184
185
186
187
188
189
190
191
192
193
194
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) -> None:
        self.bounds = bounds or SafetyBounds()
        self.checked: int = 0
        self.rejected: int = 0

    def check(self, genome: Genome) -> SafetyCheckResult:
        """Evaluate topology limits and update cumulative rejection counters."""
        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 rejected checks divided by all completed checks."""
        return self.rejected / self.checked if self.checked > 0 else 0.0

rejection_rate property

Return rejected checks divided by all completed checks.

check(genome)

Evaluate topology limits and update cumulative rejection counters.

Source code in src/sc_neurocore/evo_substrate/safety.py
Python
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def check(self, genome: Genome) -> SafetyCheckResult:
    """Evaluate topology limits and update cumulative rejection counters."""
    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,
    )

ResourceBudget dataclass

Per-organism resource constraints.

Source code in src/sc_neurocore/evo_substrate/safety.py
Python
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
@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]]:
        """Return budget compliance and human-readable resource violations."""
        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)

check(genome)

Return budget compliance and human-readable resource violations.

Source code in src/sc_neurocore/evo_substrate/safety.py
Python
125
126
127
128
129
130
131
132
133
def check(self, genome: Genome) -> Tuple[bool, List[str]]:
    """Return budget compliance and human-readable resource violations."""
    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)

RuntimeFaultCheck dataclass

Recorded runtime fault/degradation decision for one organism.

Source code in src/sc_neurocore/evo_substrate/safety.py
Python
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@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:
        """Capture a degradation plan against the organism's current identity."""
        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,
        }

from_plan(organism, plan) classmethod

Capture a degradation plan against the organism's current identity.

Source code in src/sc_neurocore/evo_substrate/safety.py
Python
48
49
50
51
52
53
54
55
56
57
58
59
60
@classmethod
def from_plan(cls, organism: Organism, plan: DegradationPlan) -> RuntimeFaultCheck:
    """Capture a degradation plan against the organism's current identity."""
    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,
    )

to_dict()

Return a JSON-ready fault-check summary.

Source code in src/sc_neurocore/evo_substrate/safety.py
Python
62
63
64
65
66
67
68
69
70
71
72
73
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,
    }

RuntimeFaultConfig dataclass

Runtime fault-check settings for evolved SC organisms.

Source code in src/sc_neurocore/evo_substrate/safety.py
Python
23
24
25
26
27
28
29
30
31
32
@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

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/safety.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
106
107
108
109
110
111
112
113
114
@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:
        """Clamp mutable genome fields to configured deployment bounds."""
        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 whether topology dimensions fit the configured bounds."""
        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
        )

clamp(genome)

Clamp mutable genome fields to configured deployment bounds.

Source code in src/sc_neurocore/evo_substrate/safety.py
Python
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def clamp(self, genome: Genome) -> Genome:
    """Clamp mutable genome fields to configured deployment bounds."""
    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

is_within_bounds(genome)

Return whether topology dimensions fit the configured bounds.

Source code in src/sc_neurocore/evo_substrate/safety.py
Python
108
109
110
111
112
113
114
def is_within_bounds(self, genome: Genome) -> bool:
    """Return whether topology dimensions fit the configured bounds."""
    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
    )

SafetyCheckResult dataclass

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

Source code in src/sc_neurocore/evo_substrate/safety.py
Python
136
137
138
139
140
141
142
143
144
145
@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

AgeRegulator

Culls organisms that exceed a maximum lifespan.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
class AgeRegulator:
    """Culls organisms that exceed a maximum lifespan."""

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

    def apply(self, population: List[Organism], current_generation: int) -> int:
        """Mark over-age organisms dead and return the number culled."""
        killed = 0
        for org in population:
            age = current_generation - org.birth_generation
            if age > self.max_age:
                org.alive = False
                killed += 1
        return killed

apply(population, current_generation)

Mark over-age organisms dead and return the number culled.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
144
145
146
147
148
149
150
151
152
def apply(self, population: List[Organism], current_generation: int) -> int:
    """Mark over-age organisms dead and return the number culled."""
    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/selection.py
Python
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@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 whether complexity exceeds the baseline bloat score."""
        return self.bloat_score > 1.0

is_bloated property

Return whether complexity exceeds the baseline bloat score.

BloatPenalizer

Penalizes fitness for bloated genomes.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
185
186
187
188
189
190
191
192
193
194
195
196
197
198
class BloatPenalizer:
    """Penalizes fitness for bloated genomes."""

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

    def penalize(self, fitness: float, genome: Genome) -> float:
        """Apply a bounded multiplicative penalty above the bloat threshold."""
        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

penalize(fitness, genome)

Apply a bounded multiplicative penalty above the bloat threshold.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
192
193
194
195
196
197
198
def penalize(self, fitness: float, genome: Genome) -> float:
    """Apply a bounded multiplicative penalty above the bloat threshold."""
    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

HallOfFame

Maintains the top-N organisms across all generations.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class HallOfFame:
    """Maintains the top-N organisms across all generations."""

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

    def update(self, organism: Organism) -> bool:
        """Insert a fitted organism and retain the highest-ranked entries."""
        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 the highest retained composite fitness, or zero when empty."""
        return self.entries[0][0] if self.entries else 0.0

    @property
    def size(self) -> int:
        """Return the number of retained hall-of-fame entries."""
        return len(self.entries)

best_fitness property

Return the highest retained composite fitness, or zero when empty.

size property

Return the number of retained hall-of-fame entries.

update(organism)

Insert a fitted organism and retain the highest-ranked entries.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
31
32
33
34
35
36
37
38
39
40
def update(self, organism: Organism) -> bool:
    """Insert a fitted organism and retain the highest-ranked entries."""
    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

ParetoFront

Maintains a non-dominated Pareto front.

Source code in src/sc_neurocore/evo_substrate/selection.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
class ParetoFront:
    """Maintains a non-dominated Pareto front."""

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

    def update(self, organism: Organism) -> bool:
        """Insert an organism when no retained member dominates its fitness."""
        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 the number of non-dominated organisms."""
        return len(self.front)

size property

Return the number of non-dominated organisms.

update(organism)

Insert an organism when no retained member dominates its fitness.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def update(self, organism: Organism) -> bool:
    """Insert an organism when no retained member dominates its fitness."""
    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

TournamentSelector

Tournament selection with configurable pressure.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class TournamentSelector:
    """Tournament selection with configurable pressure."""

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

    def select(self, population: List[Organism], rng: np.random.Generator) -> Organism:
        """Return the highest-fitness member of one random tournament.

        Raises
        ------
        ValueError
            If the population is empty.
        """
        if not population:
            raise ValueError("Tournament selection requires a non-empty population")
        candidates = rng.choice(
            len(population),
            size=min(self.tournament_size, len(population)),
            replace=False,
        )
        first_idx = int(candidates[0])
        best = population[first_idx]
        best_fit = best.fitness.composite if best.fitness else 0.0
        for idx in candidates:
            org = population[int(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]:
        """Run independent tournaments and return the requested selections."""
        return [self.select(population, rng) for _ in range(n)]

select(population, rng)

Return the highest-fitness member of one random tournament.

Raises

ValueError If the population is empty.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def select(self, population: List[Organism], rng: np.random.Generator) -> Organism:
    """Return the highest-fitness member of one random tournament.

    Raises
    ------
    ValueError
        If the population is empty.
    """
    if not population:
        raise ValueError("Tournament selection requires a non-empty population")
    candidates = rng.choice(
        len(population),
        size=min(self.tournament_size, len(population)),
        replace=False,
    )
    first_idx = int(candidates[0])
    best = population[first_idx]
    best_fit = best.fitness.composite if best.fitness else 0.0
    for idx in candidates:
        org = population[int(idx)]
        fit = org.fitness.composite if org.fitness else 0.0
        if fit > best_fit:
            best_fit = fit
            best = org
    return best

select_n(population, n, rng)

Run independent tournaments and return the requested selections.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
85
86
87
88
89
def select_n(
    self, population: List[Organism], n: int, rng: np.random.Generator
) -> List[Organism]:
    """Run independent tournaments and return the requested selections."""
    return [self.select(population, rng) for _ in range(n)]

ComplexityTracker

Tracks population complexity over generations.

Source code in src/sc_neurocore/evo_substrate/statistics.py
Python
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
class ComplexityTracker:
    """Tracks population complexity over generations."""

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

    def record(self, generation: int, population: List[Organism]) -> None:
        """Append mean and maximum complexity for a non-empty population."""
        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 mean genome complexity in generation order."""
        return [h[1] for h in self.history]

    @property
    def is_complexifying(self) -> bool:
        """Return whether mean complexity increased over three or more samples."""
        if len(self.history) < 3:
            return False
        return self.history[-1][1] > self.history[0][1]

mean_trajectory property

Return mean genome complexity in generation order.

is_complexifying property

Return whether mean complexity increased over three or more samples.

record(generation, population)

Append mean and maximum complexity for a non-empty population.

Source code in src/sc_neurocore/evo_substrate/statistics.py
Python
123
124
125
126
127
128
129
130
131
132
133
134
def record(self, generation: int, population: List[Organism]) -> None:
    """Append mean and maximum complexity for a non-empty population."""
    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)),
        )
    )

EvoStatisticsTracker

Records per-generation statistics for analytics.

Source code in src/sc_neurocore/evo_substrate/statistics.py
Python
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class EvoStatisticsTracker:
    """Records per-generation statistics for analytics."""

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

    def record(self, stats: GenerationStats) -> None:
        """Append statistics for one completed generation."""
        self.history.append(stats)

    @property
    def generations_tracked(self) -> int:
        """Return the number of recorded generations."""
        return len(self.history)

    @property
    def fitness_trajectory(self) -> List[float]:
        """Return best fitness in generation order."""
        return [s.best_fitness for s in self.history]

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

    def improvement_rate(self) -> float:
        """Return best-fitness change from the first to latest generation."""
        if len(self.history) < 2:
            return 0.0
        return self.history[-1].best_fitness - self.history[0].best_fitness

generations_tracked property

Return the number of recorded generations.

fitness_trajectory property

Return best fitness in generation order.

diversity_trajectory property

Return population diversity in generation order.

record(stats)

Append statistics for one completed generation.

Source code in src/sc_neurocore/evo_substrate/statistics.py
Python
41
42
43
def record(self, stats: GenerationStats) -> None:
    """Append statistics for one completed generation."""
    self.history.append(stats)

improvement_rate()

Return best-fitness change from the first to latest generation.

Source code in src/sc_neurocore/evo_substrate/statistics.py
Python
60
61
62
63
64
def improvement_rate(self) -> float:
    """Return best-fitness change from the first to latest generation."""
    if len(self.history) < 2:
        return 0.0
    return self.history[-1].best_fitness - self.history[0].best_fitness

GenerationStats dataclass

Statistics for one generation.

Source code in src/sc_neurocore/evo_substrate/statistics.py
Python
22
23
24
25
26
27
28
29
30
31
32
@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

GenomeDiff dataclass

Structural diff between two genomes.

Source code in src/sc_neurocore/evo_substrate/statistics.py
Python
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@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 whether the canonical vectors contain no changed values."""
        return self.total_param_changes == 0

is_identical property

Return whether the canonical vectors contain no changed values.

CrossoverEngine

Uniform crossover between two parent genomes.

Source code in src/sc_neurocore/evo_substrate/variation.py
Python
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
class CrossoverEngine:
    """Uniform crossover between two parent genomes."""

    def __init__(self, rng_seed: int = 42) -> None:
        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/variation.py
Python
131
132
133
134
135
136
137
138
139
140
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

MutationConfig dataclass

Controls mutation rates and magnitudes.

Source code in src/sc_neurocore/evo_substrate/variation.py
Python
33
34
35
36
37
38
39
40
41
42
43
@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/variation.py
Python
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class MutationEngine:
    """Applies mutations to genomes."""

    def __init__(self, config: Optional[MutationConfig] = None, rng_seed: int = 42) -> None:
        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/variation.py
Python
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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

MutationType

Bases: Enum

Identify the mutation operator applied to a child genome.

Source code in src/sc_neurocore/evo_substrate/variation.py
Python
23
24
25
26
27
28
29
30
class MutationType(Enum):
    """Identify the mutation operator applied to a child genome."""

    POINT = "point"  # Gaussian perturbation
    STRUCTURAL = "structural"  # Add/remove neurons/connections
    DUPLICATION = "duplication"  # Gene duplication
    SWAP = "swap"  # Swap sub-gene blocks
    IDENTITY = "identity"  # No mutation (clone)

compute_bloat(genome, baseline_neurons=16)

Compute bloat relative to a baseline.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
174
175
176
177
178
179
180
181
182
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)

dominates(a, b)

Return whether the first result Pareto-dominates the second.

Source code in src/sc_neurocore/evo_substrate/selection.py
Python
 95
 96
 97
 98
 99
100
101
102
103
104
105
def dominates(a: FitnessResult, b: FitnessResult) -> bool:
    """Return whether the first result Pareto-dominates the second."""
    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

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/speciation.py
Python
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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

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/speciation.py
Python
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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 and _ec is not None:
        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))

population_diversity(population)

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

Source code in src/sc_neurocore/evo_substrate/speciation.py
Python
80
81
82
83
84
85
86
87
88
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))

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/speciation.py
Python
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
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_complexity(genome)

Measure evolved complexity (information-theoretic).

Source code in src/sc_neurocore/evo_substrate/statistics.py
Python
104
105
106
107
108
109
110
111
112
113
114
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 float(entropy + np.log2(1 + topology_complexity))

genome_diff(a, b)

Compute structural diff between two genomes.

Source code in src/sc_neurocore/evo_substrate/statistics.py
Python
87
88
89
90
91
92
93
94
95
96
97
98
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,
    )