Changelog
View SourceAll 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
leakyorganelle andtopological_mutations:add_leaky/1. A leaky integrator moves its state toward its input by one part intime_constanteach 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, andmutate_time_constantnow 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 feedsmutate_neuron_type. Widening that one would let a mutation flip an organelle into a CfC neuron.cfcon the DAG path. A CfC neuron converts and evaluates throughgenotype_to_dag, computing exactly whattweann_nif:evaluate_cfc/4computes, 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 byneuron_ltc, so by the process phenotype), the native NIF, andtweann_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 returnedtanh(state)rather than the state.pop_cfc_neuron/6had 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/2routes CfC throughtweann_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 isinput/tau, so a tau of 0.001 reaches the thousands from an ordinary input. Now computed in the stable branched form.
Changed
mutate_time_constantreturns{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 coversleakyas well asdelaynow, so it names which kind it refused.
⚠ Owed, and each is a decision rather than an oversight
ltc_dynamicsstill differs from the reference, so the process phenotype computes a different CfC from every other path. Its backbone issigmoid(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-5rather 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 anadd_delayentry ingenome_mutator's operator dispatch. It splices a delay organelle into an existing connection: A to B becomes A to D to B, the wayadd_neuron/1splices 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 bygenotype_to_dagand by nothing else, so a population driven bypopulation_monitorwould raise the moment this fired. Add it to a constraint'smutation_operatorsfor a population evaluated through the DAG path.
Fixed
Both phenotype builders would have run a delay organelle as an ordinary neuron.
exoselfandconstructoreach dispatch onneuron_type, routingltcandcfctoneuron_ltcand falling through to a standard neuron for everything else. Adelayhit 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 undergenotype_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 listtweann_nif:compile_network/3takes. That evaluator imposes no layer structure, so any acyclic connection pattern converts, including the skip and cross-layer connectionsnetwork_evaluator:from_genotype/1refuses. 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/1is pure and returns the node list, the input count and the output indices, so it can be inspected without loading a NIF.compile/1hands straight to the evaluator.⚠ The contract is tighter than the spec suggests, and neither implementation checks any of it. The native
compile_networkdiscards 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/3carries no per-node state and converting one would drop the dynamics silently.genotype_rand, the genotype layer's own generator.seed/1makes construction and mutation reproducible;state/0andset_state/1let 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. Sogenome_mutator:mutate/1moved 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_utilsandselection_utilsnow go through a separate state under its own key. That is everythingconstruct_Agent/3andgenome_mutator:mutate/1,2reach. What is not routed is listed in thegenotype_randmodule 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'sapply_activation/2implemented eight, spelled one of them differently from the genotype layer (absagainstabsolute), and ended in a catch-all returning the input unchanged, while the native side ends in one returning tanh. Sogaussiancomputed 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 throughfunctions, 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 adelayneuron type. A neuron whoseneuron_typeisdelayemits 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/1andgenome_id/1(ROADMAP 8b), over a newgenotype_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'sdeterministicoption 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 behindfrom_genotype/1, exportingsupported_activations/0so a caller can ask before converting.scripts/check_onnx_export.escriptandscripts/check_onnx_export.py, a guard that exports networks, runs them in onnxruntime and compares againstnetwork_evaluator:evaluate/2. This is how the ONNX defects below were found.
Fixed
network_evaluator:from_genotype/1discarded 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 anltcneuron. Missing connections are filled with0.0, which is exact rather than approximate.⚠ The fourth is easy to underestimate. The evaluator implements
tanh,sigmoid,reluandlinear, and its activation dispatch ends in a catch-all returningmath:tanh/1. A genotype carryinggaussian,sin,cosor any of the other functions the genotype layer supports would have converted into a network computing a different function, silently.network_onnx:to_onnx/1ignored the output activation. The hidden activation was applied to every layer including the last, so a network withreluhidden andlinearoutput exported withreluon its output: onnxruntime returned0.0where the evaluator returned-0.676. Invisible whenever the two activations are equal, which is what every existing test used.network_onnxsilently substituted Tanh for any activation it could not map, exporting a model that computed something else. Now refused with{error, {unsupported_activation, Af}}.network_onnxsilently dropped CfC state. A CfC network's behaviour comes fromevaluate_with_state/2, where each neuron carries internal state across ticks; the exporter reads only weight matrices and activations, so it emitted the statelessevaluate/2function instead. Measured on a 3-4-2 CfC network, the same input three times: the exported function gives a constant-0.139while 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/3had a spec wrong on both the argument and the return. It promisedUseL2 :: boolean()and[{non_neg_integer(), float()}]. The native NIF and the Erlang fallback both take anl1 | l2atom 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 thefaber_nn_nifswrapper were corrected at the time; this dispatcher was missed. A caller following the spec got abadargon 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
undefinedbranch. It matchesundefinedin 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_functionisundefinedor 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:foreachnow, and four discarded returns are bound explicitly. - The documentation build works.
rebar3 ex_dochad 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@docon a-callback. Zero errors and zero warnings now.
Changed
- Package links and
ex_docsource_urlnow 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.mdandCODE_OF_CONDUCT.mdadded.
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/, sopriv/build-nifs.shhard-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_hexreads the package file list fromrebar_app_info:app_details/1, which issrc/faber_tweann.app.src, not from rebar.config's{hex, [{files, ...}]}block. Seerebar3_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 droppednative/,assets/,guides/andROADMAP.md.licensesandlinksshipped 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 barenativedirectory, because a directory glob sweeps innative/faber_nn_nifs/target/and exceeds hex's 16.7 MB compressed limit.Verified by unpacking the built tarball and running
priv/build-nifs.shagainst it: the NIF compiles andpriv/libfaber_nn_nifs.sois produced.
Changed
linksnow lists Codeberg first, with GitHub marked as a mirror. Codeberg is canonical.
[2.0.0]
Changed (breaking)
Absorbed the
faber-nn-nifspackage. The Rust NIFs now ship with faber_tweann and are built from source bypriv/build-nifs.shduring compilation. Removefaber_nn_nifsfrom your deps. A Rust toolchain is now required; useFABER_TWEANN_SKIP_NIF=1to build without one.Removed the silent NIF fallback. Implementation is selected explicitly via
{faber_tweann, [{nif_impl, nif | fallback}]}, defaulting tonif. 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 bysqrt(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 atoml1orl2and 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 addedneuron_metaandinternal_state, so every export raisedfunction_clauseand returned{error, {onnx_export_failed, ...}}.network_evaluatornow exposesget_layers/1,get_activation/1andget_output_activation/1, andnetwork_onnxuses them, so further record fields cannot break consumers silently.
Added
tweann_nif:impl/0reports 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.erlruns 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.mdguide, and the installation guide. There was never a second edition; the NIF repository was simply private. Replaced byguides/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_nifsmodule at startup - Uses
persistent_termfor cached implementation lookup - Priority: faber_nn_nifs (enterprise) > bundled NIF > pure Erlang fallback
- Zero code changes required - detection is automatic
- Checks for
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,4no longer auto-compiles for NIFset_weights/2no 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/2is 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 memorycompile_for_nif/1- Explicit opt-in for NIF compilation- Usage: Call
strip_compiled_ref/1before 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 loopneat-evolution.svg- NEAT topology mutations and speciationgenotype-phenotype.svg- Constructor pattern transformationneural-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/diagramsfrom hex files
- Single assets path:
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 vectorseuclidean_distance_batch/2- Batch distance calculation sorted by distanceknn_novelty/4- K-nearest neighbor novelty scoreknn_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 averageshannon_entropy/1- Entropy calculation for diversity metricshistogram/4- Histogram binning for distribution analysis
Selection Functions
- native/src/lib.rs: Selection acceleration
build_cumulative_fitness/1- Build cumulative array for roulette wheelroulette_select/3- O(log n) binary search roulette selectionroulette_select_batch/3- Batch roulette selectiontournament_select/2- Tournament selection
Reward and Meta-Controller Functions
- native/src/lib.rs: LC v2 reward computation
z_score/3- Z-score normalizationcompute_reward_component/2- Component computation with sigmoid normalizationcompute_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 listsdot_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
DirtyCpuscheduler 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.configto excludearchive/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.mdandRELEASE_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 aggregationdot_product_batch/1- Batch dot product with dirty scheduler- Added
schedule = "DirtyCpu"toevaluate_batchto 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 fallbackflatten_for_nif/2- Convert nested weight structure to flat arrays
- neuron.erl: Pre-compiled weight matrices
compiled_weightsfield in state recordcompile_weights_for_nif/4- Pre-compile at init/link timeflatten_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 microsecondsmeasure_memory/1- Memory delta measurementrun_trials/3,run_trials_gc/3- Multiple trial executioncalc_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
countersmodule for innovation numberspersistent_termfor counter reference storage- Eliminated Mnesia dependency
- genotype.erl: Generation-based cleanup
cleanup_old_agents/1- Remove agents older than N generationscap_evo_hist/2- Limit evo_hist to last 50 mutationsclear_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 = 3constant- 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_reffield to network record maybe_compile_for_nif/1- Compile network for NIF at creationevaluate/2uses NIF whencompiled_refis availableset_weights/2recompiles for NIF after weight changes
- Added
- 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/benchmarkto 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 trackingget_or_create_link_innovation/2- Track link additionsget_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 alignmentalign_genomes/2- Align genes by innovation numbercompatibility_distance/3- Species distance calculation- Matching, disjoint, and excess gene handling
topological_mutations.erl enhancements:
add_sensor/2- Add sensor to existing networkadd_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 functionsmorphology_tests.erl- 25 tests for morphology systembrain_system_tests.erl- 28 tests for brain APInetwork_evaluator_tests.erl- 27 tests for synchronous evaluationnetwork_onnx_tests.erl- 21 tests for ONNX exportapp_tests.erl- 21 tests for application modules
Documentation Enhancements
SVG Diagrams: Created professional diagrams
assets/ltc-neuron-architecture.svg- LTC neuron diagramassets/module-dependencies.svg- Module architectureguides/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
.htmllinks to.mdin 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.shutility script
0.11.1 - 2025-12-06
Summary
Documentation Alignment Release - Fixed release documentation structure and versioning.
Fixed
- Renamed
v0.10.0-optimized.mdtoFUTURE_OPTIMIZATION.md(content was mismatched) - Created proper
v0.10.0-ltc-neurons.mdrelease 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 fileto_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 networkcompute_head/2- Target state computationcompute_liquid_tau/4- Adaptive time constant calculationclamp_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 modesmutate_time_constant/1- Perturb tau (base time constant)mutate_state_bound/1- Perturb state bound Amutate_ltc_weights/1- Perturb backbone/head network weightsselect_ltc_neuron/1- Helper to select LTC/CfC neurons
Rust NIF LTC Support (native/src/lib.rs)
evaluate_cfc/4- Fast CfC evaluation in Rustevaluate_cfc_with_weights/6- CfC with custom backbone/head weightsevaluate_ode/5- ODE-based evaluation in Rustevaluate_ode_with_weights/7- ODE with custom weightsevaluate_cfc_batch/4- Batch CfC evaluation for time series
Extended Records
records.hrl: Extended
#neuronrecord with LTC fieldsneuron_type(standard | ltc | cfc)time_constant(τ - base time constant)state_bound(A - prevents state explosion)ltc_backbone_weights- f() backbone networkltc_head_weights- h() head networkinternal_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/2dispatches based onneuron_typespawn_standard_neuron/2for standard neuronsspawn_ltc_neuron/3for 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
Naming: All cryptic abbreviations replaced
idps->weighted_inputsaf->activation_functionpf->plasticity_functionvl->vector_length
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
Error Handling: Structured errors instead of exit()
exit("ERROR...")->{error, {type, reason}}
APIs: Modern OTP
now()->erlang:monotonic_time()random->rand
Migration Steps
- Update type imports from types.hrl
- Update record field names
- Update function return types for error cases
- Update time-related code
- Run test suite to verify
References
design_docs/DXNN2_CODEBASE_ANALYSIS.md- DXNN2 original codebase analysis (internal)design_docs/README.md- Refactoring principles (internal)