Changelog

View Source

All notable changes to the faber-tweann project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

Planned

  • See individual version documents for detailed planning

[2.4.0]

ROADMAP item 9 closes. Memory on the DAG path now comes in three kinds, and getting the third one there meant closing a divergence that had three implementations of CfC disagreeing by up to 0.36.

Added

  • leaky organelle and topological_mutations:add_leaky/1. A leaky integrator moves its state toward its input by one part in time_constant each tick, and the state is the output. Unlike a delay it READS this tick's inputs, so it is ordered normally and does not break a cycle. A chain of delays gives discrete memory; a leaky integrator gives a decaying trace.

  • mutation_helpers:select_tau_neuron/1, and mutate_time_constant now uses it, so a leaky organelle's time constant is tuned by the machinery that already tunes an LTC neuron's. Placement comes from the operator, the constant from mutation; that is what makes the organelle evolvable rather than fixed where it was spliced.

    ⚠ Deliberately a separate selector rather than widening select_ltc_neuron/1, which also feeds mutate_neuron_type. Widening that one would let a mutation flip an organelle into a CfC neuron.

  • cfc on the DAG path. A CfC neuron converts and evaluates through genotype_to_dag, computing exactly what tweann_nif:evaluate_cfc/4 computes, reached through the same code so the two cannot drift.

Fixed

  • ⚠ THREE IMPLEMENTATIONS OF CfC DISAGREED BY UP TO 0.36. ltc_dynamics:evaluate_cfc/4 (reached by neuron_ltc, so by the process phenotype), the native NIF, and tweann_nif_fallback:evaluate_cfc/4. The last discarded tau entirely, binding it as _Tau, so a liquid TIME-CONSTANT neuron's time constant did nothing; it also used a different backbone and returned tanh(state) rather than the state. pop_cfc_neuron/6 had the same defect, so a whole population evaluated there ignored every time constant.

    This was not confined to a corner. network_evaluator:evaluate_with_state/2 routes CfC through tweann_nif:evaluate_cfc/4, so a CfC network computed a different function depending on whether the native library had loaded, and nothing said so.

    The native implementation is now the reference. The fallback matches it to one unit in the last place across a 240-case sweep, held there by cfc_reference_tests.

  • The fallback's sigmoid raised instead of saturating. 1/(1+exp(-X)) errors on this VM for large negative X, where Rust's returns infinity and carries on to give 0.0. A CfC backbone is input/tau, so a tau of 0.001 reaches the thousands from an ordinary input. Now computed in the stable branched form.

Changed

  • mutate_time_constant returns {error, no_tau_neurons} where it returned {error, no_ltc_neurons}. Same behaviour when there is nothing to select; the reason is accurate now that the selector spans every type carrying a tau.

  • The phenotype guard raises {organelle_has_no_process_phenotype, Type, Id} where 2.3.0 raised {delay_organelle_has_no_process_phenotype, Id}. It covers leaky as well as delay now, so it names which kind it refused.

⚠ Owed, and each is a decision rather than an oversight

  • ltc_dynamics still differs from the reference, so the process phenotype computes a different CfC from every other path. Its backbone is sigmoid(input/tau), a second squash, confining the retention gate to about (0.269, 0.5): the state can never hold more than half its value per step, whatever tau is. It is not changed here, because insights 017 and 018 were measured on it.

  • The native sigmoid clamps its argument to ±10, flooring the gate at 4.54e-5 rather than zero. The stable form needs no clamp, so this distorts rather than protects. Mirrored in the fallback rather than removed, because insights 024 to 048 were measured with it in place.

  • ⚠⚠ A correction to this changelog's own 2.2.0 entry. That entry cited insight 018's title, "CfC's smoothing actively hurts fast control", as rationale for choosing the delay organelle. An adversarial review of the research corpus found that sentence is mis-scoped: 018's CfC arm ran on ltc_dynamics, whose gate is structurally confined to (0.269, 0.5), so the arm was pinned in a corner of mixing space no canonical CfC occupies. The ranking 018 measured stands as a fact about the artifact it tested; it is not a characterisation of CfC, and this changelog should not have leaned on it as one. The organelle design does not depend on that sentence, but the citation was doing work it could not carry.

[2.3.0]

The memory organelle 2.2.0 introduced can now be reached by evolution rather than only by hand. One operator and two guards.

Added

  • topological_mutations:add_delay/1, and an add_delay entry in genome_mutator's operator dispatch. It splices a delay organelle into an existing connection: A to B becomes A to D to B, the way add_neuron/1 splices an ordinary neuron.

    The weights are arranged differently from add_neuron/1, on purpose. That operator puts the original weight on both the new neuron's input and the link out, which squares the path gain. For a delay that would make the organelle a gain change as well as a delay, and the two would be inseparable afterwards. So unity goes in and the original weight moves to the link out: the mutation costs the signal one tick and changes nothing else.

    This is what makes the delay a TWEANN capability rather than a substrate one. Until now an organelle could be authored into a genotype and flown, but no mutation produced one, so evolution could not discover memory. Insight 023's finding was that evolution builds a delay line when it needs memory, out of ordinary neurons because nothing else was on offer.

    ⚠⚠ Deliberately NOT in #constraint{}'s default operator list, and a test pins that it stays out. A delay is evaluated by genotype_to_dag and by nothing else, so a population driven by population_monitor would raise the moment this fired. Add it to a constraint's mutation_operators for a population evaluated through the DAG path.

Fixed

  • Both phenotype builders would have run a delay organelle as an ordinary neuron. exoself and constructor each dispatch on neuron_type, routing ltc and cfc to neuron_ltc and falling through to a standard neuron for everything else. A delay hit that catch-all, so it would have computed an activation of its inputs instead of emitting last tick's value, and the same genotype would have meant one thing there and another under genotype_to_dag, silently.

    exoself's own comment on that dispatch says it exists "so the evolutionary path evaluates LTC genotypes as LTC, not as standard neurons that silently ignore their temporal parameters". A delay would have walked into exactly the failure that comment describes.

    Both now raise {delay_organelle_has_no_process_phenotype, Id}. The process path expresses the same idea as a recurrent connection, which it already supports and which seeds its target with 0.0 on the first cycle; that or the DAG path are the two honest options, and silently doing neither is not.

Notes for upgraders

Nothing changes for an existing population. add_delay is not in the default operator list, so no run reaches the new code unless its constraint asks for it.

If you author a delay neuron into a genotype and build a phenotype with constructor:construct/1 or through population_monitor, that now raises where it previously produced a network computing the wrong function. That is the intended direction.

[2.2.0]

An evolved topology can now be flown, and a run can be a function of its seed. Together with 2.1.0's serialisation, that is everything between genome_mutator and a service that can actually use it.

Added

  • genotype_to_dag, converting a genotype into the flat node list tweann_nif:compile_network/3 takes. That evaluator imposes no layer structure, so any acyclic connection pattern converts, including the skip and cross-layer connections network_evaluator:from_genotype/1 refuses. It is also the only route by which a mutated topology can be evaluated at simulation rates: the alternative is the process-per-neuron phenotype, which is orders of magnitude slower.

    nodes/1 is pure and returns the node list, the input count and the output indices, so it can be inspected without loading a NIF. compile/1 hands straight to the evaluator.

    The contract is tighter than the spec suggests, and neither implementation checks any of it. The native compile_network discards the index it is given and uses list position, while the Erlang fallback keys on the index; they agree only when the two coincide. On a source index that does not exist the native side indexes a vector and would panic where the fallback reads a map with a default of zero. And neither sorts, so a connection whose source appears later silently reads zero on both. This module emits and then asserts all four properties rather than trusting them.

    Refuses a cycle rather than evaluating it into a number, and refuses a CfC or LTC neuron, because compile_network/3 carries no per-node state and converting one would drop the dynamics silently.

  • genotype_rand, the genotype layer's own generator. seed/1 makes construction and mutation reproducible; state/0 and set_state/1 let a caller carry the generator across a call and recover pure-value semantics without any signature changing.

Fixed

  • Building a genotype advanced the caller's random generator. Every draw in construction and mutation was a bare rand:uniform/0, which reads and writes the one state Erlang keeps per process, and that is the state the caller is also using. So genome_mutator:mutate/1 moved the caller's generator by an unpredictable number of steps, and a caller who had seeded deliberately no longer got the sequence they seeded for. A downstream project records this as the reason a benchmark became irreproducible.

    Twelve draws across genotype, mutation_helpers, ltc_mutations, perturbation_utils and selection_utils now go through a separate state under its own key. That is everything construct_Agent/3 and genome_mutator:mutate/1,2 reach. What is not routed is listed in the genotype_rand module documentation rather than left to be discovered: crossover, genome_crossover, selection_algorithm, tuning_selection, species_identifier, network_evaluator's weight initialisation, the ES optimisers and the scapes.

  • The two DAG evaluators disagreed on ten of the seventeen activations. tweann_nif_fallback's apply_activation/2 implemented eight, spelled one of them differently from the genotype layer (abs against absolute), and ended in a catch-all returning the input unchanged, while the native side ends in one returning tanh. So gaussian computed a gaussian on the native path and a straight line on the fallback, and the two defaults did not agree with each other either. Both halves now resolve through functions, which implements all seventeen, and there is no catch-all: an activation neither side knows is refused rather than guessed at.

Notes for upgraders

tweann_nif:evaluate/2 on the fallback path now computes gaussian, sigmoid1, bin, trinary, multiquadric, quadratic, cubic, absolute, sqrt and log correctly, where it previously returned the input unchanged for all of them. If you measured anything on the fallback path using one of those activations, the numbers change, and the new ones are the right ones. The native path is unaffected.

An unknown activation now raises on the fallback path instead of silently behaving as linear.

Nothing else changes. Everything added in 2.1.0 and earlier is untouched.

The memory organelle

  • tweann_nif:evaluate_with_state/3, and a delay neuron type. A neuron whose neuron_type is delay emits what it captured last tick and applies no activation. Returns {Outputs, NextState}; pass [] to start an episode from zeros without knowing the organelle count.

    A feedback path through a delay is not a cycle. A delay's output does not depend on this tick's inputs, so it contributes no ordering constraint. A genotype where a neuron feeds a delay that feeds back into that neuron converts, sorts and runs as a DAG plus state. A loop with no delay in it is still refused, because that one genuinely has no order.

    The state vector holds one float per organelle rather than one per neuron, so it stays small and its layout is explicit. A state of the wrong length returns two empty lists rather than being padded.

    Why this shape rather than per-neuron state. Insight 018 compared the three places memory can live on this engine and ranked them none > wiring > neuron, with CfC last; its title records that CfC's smoothing actively hurts fast control. Insight 023 then dumped the wiring of a solver that actually solved a memory task and found a pure linear chain, every neuron with exactly one input. Evolution built a delay line out of ordinary neurons because the substrate offered nothing else.

    Three passes, and the order is the mechanism: emit every delay's stored value, run the ordinary nodes in topological order, then capture each delay's weighted input sum, which may name nodes that come after it. Implemented in both Rust and Erlang, and held in agreement by a differential test with two halves: an exact one on linear activations, where any difference is a logic difference, and a tolerant one on tanh, where Rust and libm legitimately differ by a unit in the last place.

Still not available

CfC and LTC remain refused on the DAG path. Their dynamics are a per-neuron continuous-time update, which is a different thing from a unit delay, and mapping one onto the other would be an approximation reported as success. A leaky integrator with an evolvable time constant, which subsumes both smoothing and delay, is the next increment. ROADMAP item 9.

[2.1.0]

An evolved genotype can now leave the machine it was bred on, and three functions that used to return a confidently wrong answer now return the right one or an error. ROADMAP items 8a and 8b move to the README; 8c stays open.

Added

  • genotype:to_binary/1, from_binary/1 and genome_id/1 (ROADMAP 8b), over a new genotype_codec. Until this existed a genotype lived in ETS and nowhere else: it could not be persisted, could not be put on a wire and could not be handed to another node. That was the whole of what stopped topology evolution being usable by a service.

    Canonical and lossless. The encoding is hand-rolled over the closed set of term shapes a genotype contains, and refuses everything else rather than guessing. It is deliberately not term_to_binary/2: that function's deterministic option is not promised to be stable across OTP releases, so it is unfit for a content address, and a sibling project lost days to exactly that when two identical images computed different fingerprints and each filtered the other out as incompatible.

    Atoms decode through binary_to_existing_atom/2, so an untrusted genome cannot mint atoms and one from an incompatible build is refused by name. Explicit size limits are a denial-of-service defence and they reject rather than clamp, because clamping changes the genome and then its published identifier no longer identifies what ran.

  • genotype_to_network, the conversion behind from_genotype/1, exporting supported_activations/0 so a caller can ask before converting.

  • scripts/check_onnx_export.escript and scripts/check_onnx_export.py, a guard that exports networks, runs them in onnxruntime and compares against network_evaluator:evaluate/2. This is how the ONNX defects below were found.

Fixed

  • network_evaluator:from_genotype/1 discarded the weights and reported success (ROADMAP 8a). It counted the neurons, invented a layer shape and filled it with random numbers, while its own documentation claimed to read the structure and weights from Mnesia. There is no Mnesia and there were no weights. An evolved champion came back correctly shaped and knowing nothing, and nothing raised.

    It now converts faithfully or returns {error, {not_layerable, Why}} naming what stopped it. Five things can: recurrence, a connection that skips or crosses a layer, mixed activation functions, an activation the evaluator does not implement, and an ltc neuron. Missing connections are filled with 0.0, which is exact rather than approximate.

    ⚠ The fourth is easy to underestimate. The evaluator implements tanh, sigmoid, relu and linear, and its activation dispatch ends in a catch-all returning math:tanh/1. A genotype carrying gaussian, sin, cos or any of the other functions the genotype layer supports would have converted into a network computing a different function, silently.

  • network_onnx:to_onnx/1 ignored the output activation. The hidden activation was applied to every layer including the last, so a network with relu hidden and linear output exported with relu on its output: onnxruntime returned 0.0 where the evaluator returned -0.676. Invisible whenever the two activations are equal, which is what every existing test used.

  • network_onnx silently substituted Tanh for any activation it could not map, exporting a model that computed something else. Now refused with {error, {unsupported_activation, Af}}.

  • network_onnx silently dropped CfC state. A CfC network's behaviour comes from evaluate_with_state/2, where each neuron carries internal state across ticks; the exporter reads only weight matrices and activations, so it emitted the stateless evaluate/2 function instead. Measured on a 3-4-2 CfC network, the same input three times: the exported function gives a constant -0.139 while the real network gives -0.055, -0.091, -0.111. Exporting that quietly hands somebody a champion that is a different controller, so it is now refused with {error, {unsupported_neuron_type, cfc}}. A stateful export, carrying the internal state as an extra input and output tensor, is future work.

  • README claimed topology evolution removes neurons and connections. There is no remove operator. Complexification only; parsimony pressure is available through fitness_postprocessor:size_proportional/2.

  • tweann_nif:weight_distance_batch/3 had a spec wrong on both the argument and the return. It promised UseL2 :: boolean() and [{non_neg_integer(), float()}]. The native NIF and the Erlang fallback both take an l1 | l2 atom and return a plain distance list in input order, and the Rust source records that it previously took a boolean and was changed. The fallback and the faber_nn_nifs wrapper were corrected at the time; this dispatcher was missed. A caller following the spec got a badarg on the boolean, which is why the disagreement was never exercised.

  • actuator's actuate/4 carried an unreachable branch for an attached scape, under a comment saying it was retained for callers that predate the fitness channel. There are no such callers: the single call site reaches it only on its own undefined branch. It matches undefined in the head now, so a scape arriving there fails loudly rather than taking a path that cannot run.

  • exoself's compute_max_attempts/1 had a clause that could never match, since tuning_duration_function is undefined or a two-tuple and the first two clauses cover both. Removed rather than kept as insurance against a state that cannot occur.

Housekeeping

  • Dialyzer is clean. Ten standing warnings are gone. Three were the defects above; the other seven were values built for their side effects and discarded without saying so. Three list comprehensions that existed for their sends are lists:foreach now, and four discarded returns are bound explicitly.
  • The documentation build works. rebar3 ex_doc had been failing with fatal XML parser errors in five modules, every one a less-than sign inside a doc comment that EDoc reads as an opening tag, plus a bare ampersand in a citation, two doc blocks that had drifted away from the function they described, and a @doc on a -callback. Zero errors and zero warnings now.

Changed

  • Package links and ex_doc source_url now point at GitHub, which has been canonical since 2026-07-26. The Codeberg repository is a soon-to-be-deleted copy and is no longer linked.
  • CONTRIBUTING.md and CODE_OF_CONDUCT.md added.

Notes for upgraders

from_genotype/1 and to_onnx/1 can now return {error, _} where they previously returned {ok, _}. In every such case the previous {ok, _} carried a result that was wrong, so this is a fix rather than a removal of capability, but code that pattern-matched {ok, Net} without a fallback clause will now crash where it used to proceed on bad data. That is the intended direction.

Nothing else changes. create_feedforward/3,4,5, create_cfc_feedforward/5, set_weights/2, get_weights/1, evaluate/2 and evaluate_with_state/2 are untouched.

[2.0.1]

Fixed

  • v2.0.0 on hex is unbuildable. This release fixes it. The published 2.0.0 package shipped without native/, so priv/build-nifs.sh hard-errors with "no crate source" and every consumer fails to compile. The hard error is correct behaviour, introduced in 2.0.0 when the silent fallback was removed; the packaging was what was wrong.

    Root cause: rebar3_hex reads the package file list from rebar_app_info:app_details/1, which is src/faber_tweann.app.src, not from rebar.config's {hex, [{files, ...}]} block. See rebar3_hex_build.erl:446, proplists:get_value(files, AppDetails, ?DEFAULT_FILES). The list had been placed in rebar.config, where the plugin never reads it, so the publish silently fell back to the default file set and dropped native/, assets/, guides/ and ROADMAP.md. licenses and links shipped correctly in 2.0.0 precisely because they already lived in .app.src.

    The file list now lives in .app.src, and the crate's paths are enumerated individually rather than as a bare native directory, because a directory glob sweeps in native/faber_nn_nifs/target/ and exceeds hex's 16.7 MB compressed limit.

    Verified by unpacking the built tarball and running priv/build-nifs.sh against it: the NIF compiles and priv/libfaber_nn_nifs.so is produced.

Changed

  • links now lists Codeberg first, with GitHub marked as a mirror. Codeberg is canonical.

[2.0.0]

Changed (breaking)

  • Absorbed the faber-nn-nifs package. The Rust NIFs now ship with faber_tweann and are built from source by priv/build-nifs.sh during compilation. Remove faber_nn_nifs from your deps. A Rust toolchain is now required; use FABER_TWEANN_SKIP_NIF=1 to build without one.

  • Removed the silent NIF fallback. Implementation is selected explicitly via {faber_tweann, [{nif_impl, nif | fallback}]}, defaulting to nif. If the native library is missing or unloadable, faber_tweann now raises on first use with an explanatory error instead of quietly running the slower pure Erlang path. Selection is resolved lazily on first use, not in -on_load, because the application environment is not necessarily loaded at module load time.

  • Corrected weight_distance_l1/2. The native implementation divided by vector length, computing mean absolute deviation rather than Manhattan distance. It now returns the sum, agreeing with the Erlang fallback. Callers relying on the old normalized value will see results larger by a factor of the vector length.

  • Corrected weight_distance_l2/2. Likewise divided by sqrt(length), computing a root-mean-square deviation rather than Euclidean distance.

  • Corrected weight_distance_batch/3. Took a boolean and returned {Index, Distance} pairs sorted ascending. It now takes the metric as the atom l1 or l2 and returns one distance per input vector in input order, matching the documented spec and the Erlang fallback.

  • Corrected random_weights_batch/1. Took a bare list of sizes and produced uniform weights in [-1, 1], silently discarding the requested mean and standard deviation. It now takes {Count, Mean, StdDev} specs and produces gaussian weights, matching the Erlang fallback.

Fixed

  • ONNX export has been broken since v1.2.0. network_onnx's get_network_data/1 matched #network{} as a fixed-arity tuple. The CfC/LTC work added neuron_meta and internal_state, so every export raised function_clause and returned {error, {onnx_export_failed, ...}}. network_evaluator now exposes get_layers/1, get_activation/1 and get_output_activation/1, and network_onnx uses them, so further record fields cannot break consumers silently.

Added

  • tweann_nif:impl/0 reports the active implementation. Record it in any benchmark; a performance number that does not name its execution path is not a number.
  • test/unit/nif_fallback_conformance_tests.erl runs the native and fallback implementations over the same inputs and asserts they agree. The four contract bugs above survived because each package tested only its own side.

Removed

  • The "Community vs Enterprise Edition" framing from the README, the guides/enterprise-nifs.md guide, and the installation guide. There was never a second edition; the NIF repository was simply private. Replaced by guides/native-nifs.md.
  • Unbacked performance claims. The README quoted 30-200x while the faber-nn-nifs README quoted 10-15x for the same code, with no committed measurement behind either. No figures are published until a benchmark runs and its output is committed.

0.16.0 - 2025-12-23

Summary

Enterprise NIF Package Support - Added automatic detection and integration with the separate faber_nn_nifs enterprise package, providing 10-15x performance improvements for compute-intensive operations.

Added

Enterprise NIF Detection

  • tweann_nif.erl: Automatic detection of enterprise NIF package
    • Checks for faber_nn_nifs module at startup
    • Uses persistent_term for cached implementation lookup
    • Priority: faber_nn_nifs (enterprise) > bundled NIF > pure Erlang fallback
    • Zero code changes required - detection is automatic

Documentation

  • guides/enterprise-nifs.md: New comprehensive guide for enterprise NIFs
    • Performance comparison table (10-15x speedups)
    • Installation instructions for Community vs Enterprise editions
    • Complete list of 44 accelerated functions across 8 categories
    • Verification and troubleshooting sections
  • guides/installation.md: Updated with Enterprise Edition section
    • Clear separation of Community and Enterprise installation
    • Enterprise requirements (Rust 1.70+, SSH access)
    • NIF verification examples

Changed

  • rebar.config: Added enterprise-nifs.md to ex_doc extras

Enterprise NIF Package

The faber_nn_nifs package (v0.1.0) is now available as a separate enterprise-only repository:

  • 44 Rust NIF functions for compute-intensive operations
  • Categories: Network Evaluation, Signal Aggregation, LTC/CfC, Novelty Search, Statistics, Selection, Meta-Controller, Evolutionary Genetics
  • 98 unit tests with full coverage
  • Apache-2.0 license (enterprise license required for commercial use)

Test Results

  • 593 tests passing
  • Dialyzer clean

0.15.3 - 2025-12-23

Summary

Memory Leak Prevention - Fixed NIF ResourceArc memory accumulation during evolution by switching to lazy compilation and adding explicit memory management functions.

Changed

Network Evaluator Memory Management

  • network_evaluator.erl: Lazy NIF compilation to prevent memory leaks
    • create_feedforward/3,4 no longer auto-compiles for NIF
    • set_weights/2 no longer auto-compiles for NIF
    • NIF compilation is now opt-in via compile_for_nif/1
    • Networks use pure Erlang evaluation by default (fallback path)
    • Rationale: During breeding, set_weights/2 is called for EVERY offspring. Eager compilation created millions of ResourceArc references that accumulated unboundedly, causing memory to grow without bound across generations.

Added

  • network_evaluator.erl: New memory management functions
    • strip_compiled_ref/1 - Remove compiled_ref to release NIF memory
    • compile_for_nif/1 - Explicit opt-in for NIF compilation
    • Usage: Call strip_compiled_ref/1 before storing networks in archives, events, or long-term storage to prevent ResourceArc accumulation.

Fixed

  • population_monitor.erl: Changed "Generation" to "Cohort" in log messages
    • Aligns with terminology used elsewhere in the codebase

Test Results

  • 593 tests passing
  • Dialyzer clean

0.15.2 - 2025-12-12

Summary

Documentation Enhancement - Added comprehensive visual diagram index and consolidated all SVG assets into unified assets/ directory.

Added

  • guides/diagram-index.md: New visual guide indexing all 25 SVG diagrams

    • Core Concepts: TWEANN structure, neuroevolution cycle, NEAT evolution
    • Architecture: Genotype/phenotype, supervision tree, C4 model
    • LTC Neurons: Architecture and comparison diagrams
    • Learning Mechanisms: Plasticity, activation functions, mutation sequences
    • Distributed Evolution: Multi-node, federated, swarm models
    • Application Domains: Military, civil, defense scenarios
  • New educational SVG diagrams in assets/:

    • tweann-structure.svg - Core TWEANN architecture (sensors → hidden → actuators)
    • neuroevolution-cycle.svg - The evolutionary optimization loop
    • neat-evolution.svg - NEAT topology mutations and speciation
    • genotype-phenotype.svg - Constructor pattern transformation
    • neural-plasticity.svg - Online learning mechanisms (Hebbian, Oja, etc.)
    • activation-functions.svg - Comparison of activation functions

Changed

  • rebar.config: Consolidated assets configuration
    • Single assets path: {assets, #{"assets" => "assets"}}
    • Added diagram-index.md to ex_doc extras
    • Removed redundant design_docs/diagrams from hex files

Documentation

  • All 25 SVG diagrams now in unified assets/ directory
  • Visual diagram index for easier navigation in hexdocs
  • Educational descriptions for each diagram category

0.15.1 - 2025-12-12

Summary

Community vs Enterprise Edition - NIF compilation hooks are now disabled by default in the hex.pm package. The Community Edition uses pure Erlang fallbacks, while Enterprise Edition users can enable Rust NIF acceleration from source.

Changed

  • rebar.config: NIF compilation hooks commented out for hex.pm package
    • Community Edition (hex.pm) uses pure Erlang fallbacks
    • Enterprise Edition users can uncomment hooks to enable NIF acceleration
    • No Rust toolchain required for Community Edition users
  • README.md: Added "Community vs Enterprise Edition" section documenting the two editions

Documentation

  • Clear documentation of feature differences between editions
  • Instructions for Enterprise users to enable NIF acceleration
  • Contact information for enterprise licensing

0.15.0 - 2025-12-12

Summary

NIF Acceleration Phase 2 Release - Major NIF expansion with 18 new functions for novelty search, fitness statistics, selection, and reward computation. Includes complete pure Erlang fallback module for portability without Rust toolchain.

Added

Distance and KNN Functions (Novelty Search)

  • native/src/lib.rs: New NIF functions for novelty search
    • euclidean_distance/2 - Distance between two behavior vectors
    • euclidean_distance_batch/2 - Batch distance calculation sorted by distance
    • knn_novelty/4 - K-nearest neighbor novelty score
    • knn_novelty_batch/3 - Batch kNN novelty for entire population

Statistics Functions

  • native/src/lib.rs: Vectorized fitness statistics
    • fitness_stats/1 - Single-pass (min, max, mean, variance, std_dev, sum)
    • weighted_moving_average/2 - Exponential decay weighted average
    • shannon_entropy/1 - Entropy calculation for diversity metrics
    • histogram/4 - Histogram binning for distribution analysis

Selection Functions

  • native/src/lib.rs: Selection acceleration
    • build_cumulative_fitness/1 - Build cumulative array for roulette wheel
    • roulette_select/3 - O(log n) binary search roulette selection
    • roulette_select_batch/3 - Batch roulette selection
    • tournament_select/2 - Tournament selection

Reward and Meta-Controller Functions

  • native/src/lib.rs: LC v2 reward computation
    • z_score/3 - Z-score normalization
    • compute_reward_component/2 - Component computation with sigmoid normalization
    • compute_weighted_reward/1 - Weighted sum of reward components

Weight/Genome Utilities

  • native/src/lib.rs: Weight structure optimization
    • flatten_weights/1 - Flatten nested weight structure avoiding intermediate lists
    • dot_product_preflattened/3 - Dot product on pre-flattened arrays

Test Coverage

  • test/unit/tweann_nif_v2_tests.erl: 42 new tests for all NIF functions
    • Distance and kNN tests
    • Statistics function tests
    • Selection function tests
    • Reward computation tests
    • Performance sanity tests

Changed

  • src/tweann_nif.erl: Added exports and stubs for 18 new functions
  • src/tweann_nif.erl: Rewritten with try-catch fallback pattern for portability
  • All new NIFs use DirtyCpu scheduler for long-running operations

Pure Erlang Fallback Module

  • src/tweann_nif_fallback.erl: NEW - Complete Erlang implementations of all NIFs
    • Full fallback for all 30+ NIF functions
    • Automatic fallback when NIF not loaded (no Rust compilation required)
    • Enables library use on any Erlang/OTP system without Rust toolchain
    • Helper functions: sigmoid, clamp, apply_activation

Fixed

  • src/genome_mutator.erl: Substrate mutations (add_cpp, add_cep) now log warning instead of silent no-op
  • src/genotype.erl:343: Removed misleading TODO - link_FromElementToElement is fully implemented
  • src/genotype.erl:498: Implemented update_fingerprint using species_identifier:create_fingerprint/1
  • src/network_evaluator.erl: Documented feedforward approximation limitation with recommendation to use tweann_nif:compile_network/3 for exact topology evaluation

Performance Targets

  • Euclidean distance batch: 30-100x speedup for novelty search
  • kNN novelty: 50-200x speedup for behavior distance calculations
  • Fitness statistics: 5-10x speedup for single-pass computation
  • Roulette selection: 5-15x speedup with O(log n) binary search

Test Results

  • 593 tests passing (includes 42 new NIF v2 tests)
  • Dialyzer clean
  • All fallback functions verified working

0.14.0 - 2025-12-12

Summary

Documentation Cleanup Release - Archived legacy release docs and vision documents to streamline hexdocs.

Changed

  • Moved version-specific release docs (v0.1.0 through v1.0.0) to archive/releases/
  • Moved vision/addendum documents to archive/vision/ (for future macula-vision repo)
  • Updated rebar.config to exclude archive/ from hex package
  • Removed broken hexdocs links from guides
  • Streamlined documentation for cleaner hexdocs experience

Removed

  • Removed duplicate addendum files from guides/ (now only in archive)
  • Removed FUTURE_OPTIMIZATION.md and RELEASE_STRATEGY.md (superseded)
  • Removed CODE_QUALITY_REVIEW_v0.10.0.md (superseded)

Test Results

  • 801 tests passing
  • Dialyzer clean

0.13.0 - 2025-12-07

Summary

Memory Optimization & NIF Acceleration Release - Major performance and stability improvements with 4x memory reduction target and 10-50x speedup on hot paths.

Added

NIF Acceleration (Phase 3)

  • native/src/lib.rs: New NIF functions
    • dot_product_flat/3 - Flat array dot product for signal aggregation
    • dot_product_batch/1 - Batch dot product with dirty scheduler
    • Added schedule = "DirtyCpu" to evaluate_batch to prevent blocking
  • tweann_nif.erl: Erlang wrappers for new NIFs

Signal Aggregation Optimization (Phase 4)

  • signal_aggregator.erl: NIF-accelerated aggregation
    • dot_product_nif/2 - NIF-backed dot product with Erlang fallback
    • flatten_for_nif/2 - Convert nested weight structure to flat arrays
  • neuron.erl: Pre-compiled weight matrices
    • compiled_weights field in state record
    • compile_weights_for_nif/4 - Pre-compile at init/link time
    • flatten_signals/2, aggregate_compiled/3 - Fast path evaluation
    • Weights recompiled on {update_weights, ...} and {link, input_weights, ...}

Benchmark Suite (Phase 5)

  • test/benchmark/bench_common.erl: Benchmark utilities
    • measure_time/1,2 - Execution timing in microseconds
    • measure_memory/1 - Memory delta measurement
    • run_trials/3, run_trials_gc/3 - Multiple trial execution
    • calc_stats/1 - Statistical analysis (min, max, avg, median, std)
    • format_bytes/1, format_time/1 - Human-readable formatting
  • test/benchmark/bench_forward_pass.erl: Network evaluation benchmarks
    • Small/medium/large/XOR network tests
    • Batch evaluation benchmarks
    • Memory usage measurements
  • test/benchmark/bench_nif_vs_erlang.erl: NIF comparison benchmarks
    • dot_product NIF vs Erlang comparison
    • Flat dot product benchmarks
    • Batch dot product benchmarks

Changed

Memory Architecture (Phase 2)

  • genotype.erl: Replaced Mnesia with ETS
    • 11 tables migrated from Mnesia RAM to ETS
    • Faster startup, lower overhead
    • Same semantics, simpler implementation
  • innovation.erl: Migrated to atomics/counters
    • counters module for innovation numbers
    • persistent_term for counter reference storage
    • Eliminated Mnesia dependency
  • genotype.erl: Generation-based cleanup
    • cleanup_old_agents/1 - Remove agents older than N generations
    • cap_evo_hist/2 - Limit evo_hist to last 50 mutations
    • clear_dead_pool/1 - Clear dead_pool each generation

Process Lifecycle Fixes (Phase 1)

  • neuron.erl: Fixed infinite timeout loop
    • Exit after 3 consecutive timeouts (was infinite loop)
    • MAX_TIMEOUT_COUNT = 3 constant
    • Notifies cortex on timeout termination
  • population_monitor.erl: Reduced timeout 60s → 5s
  • cortex.erl: Synchronous termination
    • Wait for child processes before exit
    • Monitor-based termination tracking
  • exoself.erl: Race condition fix on shutdown
  • All process modules: Added catch-all receive clauses
    • Prevents mailbox bloat from unexpected messages
    • Logs warnings for debugging

Network Evaluator (Phase 3)

  • network_evaluator.erl: NIF integration
    • Added compiled_ref field to network record
    • maybe_compile_for_nif/1 - Compile network for NIF at creation
    • evaluate/2 uses NIF when compiled_ref is available
    • set_weights/2 recompiles for NIF after weight changes
  • network_onnx.erl: Fixed for new record format
    • Handles both 3-tuple and 4-tuple network records

Configuration

  • rebar.config:
    • Enabled NIF build hooks (were commented out)
    • Added test/benchmark to extra_src_dirs

Fixed

  • Zombie neuron processes from infinite timeout loop
  • Orphaned child processes on cortex termination
  • Mailbox bloat from unhandled messages
  • Memory growth from unbounded evo_hist
  • Memory growth from persistent dead_pool

Performance Targets

  • Memory: 2-4 GB → 500 MB - 1 GB (4x reduction)
  • Forward pass: 5-10x faster with NIF for dot_product
  • Generation time: 4-6x faster with NIF acceleration

Test Results

  • 801 tests passing (10 new benchmark tests)
  • Dialyzer clean
  • Documentation links validated

Dependencies

  • No new dependencies
  • Rust NIF uses existing rustler setup

0.12.0 - 2025-12-07

Summary

Complete Topology Evolution & Test Coverage Release - NEAT-style topology evolution with innovation tracking, comprehensive test coverage (791 tests), and enhanced documentation.

Added

Topology Evolution (NEAT-Style)

  • innovation.erl (~200 lines): Innovation number tracking for structural mutations

    • init/0, reset/0 - Initialize/reset innovation tracking
    • get_or_create_link_innovation/2 - Track link additions
    • get_or_create_node_innovation/2 - Track node additions
    • Mnesia persistence for innovation history
  • genome_crossover.erl (~250 lines): Variable-topology crossover

    • crossover/3 - NEAT-style crossover with gene alignment
    • align_genomes/2 - Align genes by innovation number
    • compatibility_distance/3 - Species distance calculation
    • Matching, disjoint, and excess gene handling
  • topological_mutations.erl enhancements:

    • add_sensor/2 - Add sensor to existing network
    • add_actuator/2 - Add actuator to existing network
    • Innovation number assignment for all structural changes

Comprehensive Test Coverage

  • 198 new tests bringing total to 791 tests
  • New test files:
    • functions_tests.erl - 76 tests for activation functions
    • morphology_tests.erl - 25 tests for morphology system
    • brain_system_tests.erl - 28 tests for brain API
    • network_evaluator_tests.erl - 27 tests for synchronous evaluation
    • network_onnx_tests.erl - 21 tests for ONNX export
    • app_tests.erl - 21 tests for application modules

Documentation Enhancements

  • SVG Diagrams: Created professional diagrams

    • assets/ltc-neuron-architecture.svg - LTC neuron diagram
    • assets/module-dependencies.svg - Module architecture
    • guides/assets/planetary-mesh-vision.svg - Distributed vision
  • Research Opportunities: Added to ltc-neurons.md

    • Temporal dynamics evolution
    • Hybrid architecture research
    • Application domain suggestions
  • Value Sections: Added competitive comparisons

    • "Why Choose faber-tweann" in overview.md
    • "Why This Architecture" in architecture.md
    • Comparison tables with alternatives

Tooling

  • validate-docs.sh: Link validation script
    • SVG reference checking
    • Markdown link validation
    • ASCII diagram detection
    • CI-ready exit codes

Changed

  • Version bumped from 0.11.3 to 0.12.0
  • README test count updated to 791
  • Replaced ASCII diagram in vision guide with SVG

Fixed

  • Broken SVG links in README.md (created missing assets)
  • ASCII diagram in vision-distributed-mega-brain.md replaced with SVG

Academic References

  • NEAT paper (Stanley & Miikkulainen, 2002) referenced in innovation.erl and genome_crossover.erl

Test Results

  • 791 tests passing
  • Dialyzer clean
  • All documentation links validated

0.11.2 - 2025-12-06

Summary

Documentation Link Fixes - Fixed all internal documentation links to use .md extensions.

Fixed

  • Converted all .html links to .md in guides (ex_doc converts automatically)
  • Fixed custom_morphologies.html -> custom-morphologies.md
  • Fixed api-reference.html -> removed (use module docs in sidebar)
  • Fixed ltc_dynamics.html -> removed (use module docs directly)
  • Added scripts/fix-html-links.sh utility script

0.11.1 - 2025-12-06

Summary

Documentation Alignment Release - Fixed release documentation structure and versioning.

Fixed

  • Renamed v0.10.0-optimized.md to FUTURE_OPTIMIZATION.md (content was mismatched)
  • Created proper v0.10.0-ltc-neurons.md release document
  • Updated rebar.config ex_doc to point to correct files

Added

  • v0.10.0-ltc-neurons.md - Comprehensive release document for LTC neurons feature

0.11.0 - 2025-12-06

Summary

ONNX Export & Documentation Release - Export trained networks to ONNX format for inference in Python, JavaScript, and other frameworks.

Added

ONNX Export

  • network_onnx.erl (~200 lines): Export evolved networks to ONNX format
    • export/2 - Export network to ONNX binary file
    • to_onnx/1 - Convert network to ONNX protobuf structure
    • Supports feedforward networks with standard activation functions
    • Compatible with ONNX Runtime, PyTorch, TensorFlow

Documentation

  • Academic references added to README.md and guides/overview.md

    • Hasani et al. (2021) - Liquid Time-constant Networks
    • Hasani et al. (2022) - Closed-form Continuous-time Neural Networks
    • Stanley & Miikkulainen (2002) - NEAT
    • Sher (2012) - Handbook of Neuroevolution Through Erlang (DXNN2)
  • scripts/check-links.sh: Documentation link quality checker

Fixed

  • Broken DXNN2 reference link in v0.3.1-architectural-alignment.md

Test Results

  • 270+ tests passing
  • Dialyzer clean

0.10.0 - 2025-12-03

Summary

Liquid Time-Constant (LTC) Neurons Release - First TWEANN library with LTC/CfC neuron support in Erlang/OTP.

LTC neurons enable adaptive temporal processing with input-dependent time constants. This is a major feature release that extends faber-tweann with continuous-time neural dynamics based on peer-reviewed research.

Added

Core LTC Modules

  • ltc_dynamics.erl (~380 lines): Core LTC/CfC computation engine

    • evaluate_cfc/4,5 - CfC closed-form evaluation (~100x faster than ODE)
    • evaluate_ode/5,6 - ODE-based evaluation (Euler integration)
    • compute_backbone/3 - Time constant modulation network
    • compute_head/2 - Target state computation
    • compute_liquid_tau/4 - Adaptive time constant calculation
    • clamp_state/2, reset_state/0 - State management utilities
    • Full EDoc with academic references
  • neuron_ltc.erl (~280 lines): LTC-specific neuron process

    • Full process lifecycle with internal state persistence
    • CfC and ODE modes supported
    • Reset/get state operations
    • LTC parameter update support

LTC Evolution Support (genome_mutator.erl)

  • mutate_neuron_type/1 - Switch neurons between standard/ltc/cfc modes
  • mutate_time_constant/1 - Perturb tau (base time constant)
  • mutate_state_bound/1 - Perturb state bound A
  • mutate_ltc_weights/1 - Perturb backbone/head network weights
  • select_ltc_neuron/1 - Helper to select LTC/CfC neurons

Rust NIF LTC Support (native/src/lib.rs)

  • evaluate_cfc/4 - Fast CfC evaluation in Rust
  • evaluate_cfc_with_weights/6 - CfC with custom backbone/head weights
  • evaluate_ode/5 - ODE-based evaluation in Rust
  • evaluate_ode_with_weights/7 - ODE with custom weights
  • evaluate_cfc_batch/4 - Batch CfC evaluation for time series

Extended Records

  • records.hrl: Extended #neuron record with LTC fields

    • neuron_type (standard | ltc | cfc)

    • time_constant (τ - base time constant)
    • state_bound (A - prevents state explosion)
    • ltc_backbone_weights - f() backbone network
    • ltc_head_weights - h() head network
    • internal_state - x(t) persistent state
  • types.hrl: New LTC type specifications

    • neuron_type(), time_constant(), state_bound()
    • internal_state(), time_step()
    • ltc_backbone_weights(), ltc_head_weights()
    • ltc_params() map type

Documentation

  • guides/ltc-neurons.md: Comprehensive LTC concepts guide

    • Mathematical foundations (LTC ODE, CfC closed-form)
    • Neuron types comparison table
    • Implementation details and key properties
    • Use cases and academic references
  • guides/ltc-usage-guide.md: Practical usage guide

    • API reference with examples
    • Parameter tuning guide
    • Time series processing examples
    • Troubleshooting section
  • design_docs/diagrams/ltc-neuron-architecture.svg: Architecture diagram

  • design_docs/diagrams/ltc-vs-standard-neurons.svg: Comparison diagram

Changed

  • constructor.erl: Extended to spawn LTC neurons

    • spawn_neuron_by_type/2 dispatches based on neuron_type
    • spawn_standard_neuron/2 for standard neurons
    • spawn_ltc_neuron/3 for ltc/cfc neurons
  • README.md: Updated with LTC as primary feature

  • rebar.config: Added LTC guides to ex_doc configuration

Performance

  • CfC evaluation: ~100x faster than ODE-based LTC
  • State bounded dynamics prevent numerical overflow
  • Configurable time constants for different response speeds

Academic References

  • Hasani, R., Lechner, M., et al. (2021). "Liquid Time-constant Networks." AAAI 2021.
  • Hasani, R., Lechner, M., et al. (2022). "Closed-form Continuous-time Neural Networks." Nature Machine Intelligence.

Test Results

  • 468 tests passing (including 68 new LTC tests: 45 core + 11 mutation + 12 NIF)
  • Dialyzer clean (1 pre-existing warning)

Migration from DXNN2

Key Differences

  1. Naming: All cryptic abbreviations replaced

    • idps -> weighted_inputs
    • af -> activation_function
    • pf -> plasticity_function
    • vl -> vector_length
  2. State Management: Records instead of parameter lists

    • cortex: 10 parameters -> cortex_state record
    • neuron: 14 parameters -> neuron_state record
    • exoself: 24 parameters -> exoself_state record
  3. Error Handling: Structured errors instead of exit()

    • exit("ERROR...") -> {error, {type, reason}}
  4. APIs: Modern OTP

    • now() -> erlang:monotonic_time()
    • random -> rand

Migration Steps

  1. Update type imports from types.hrl
  2. Update record field names
  3. Update function return types for error cases
  4. Update time-related code
  5. Run test suite to verify

References

  • design_docs/DXNN2_CODEBASE_ANALYSIS.md - DXNN2 original codebase analysis (internal)
  • design_docs/README.md - Refactoring principles (internal)