genotype_to_dag (faber_tweann v2.2.0)
View SourceConvert an evolved genotype into the flat node list the DAG evaluator takes, so an arbitrary evolved topology can be flown at inference speed.
This is the counterpart to genotype_to_network. That one targets network_evaluator, which is a stack of dense layers, so it refuses any genotype whose connections skip or cross a layer. This one targets tweann_nif:compile_network/3, which imposes no layer structure at all: any acyclic connection pattern converts.
========================================================================== WHAT THIS BUYS, AND WHAT IT DOES NOT ==========================================================================
Buys: arbitrary feedforward topology, evaluated synchronously, in Rust when the native path is loaded. That is the only route by which a topology produced by genome_mutator can be flown at the rate a simulation needs. The process-per-neuron phenotype is the alternative and it is orders of magnitude slower.
Memory comes from a DELAY ORGANELLE rather than from per-neuron state. A neuron whose neuron_type is delay emits what it captured last tick and applies no activation, so its output does not depend on this tick's inputs. Two consequences, and the second is the point:
The state vector holds one float per organelle rather than one per neuron, so it stays small and its layout is explicit.
⚠ And A FEEDBACK PATH THROUGH A DELAY IS NOT A CYCLE. A delay contributes no ordering constraint, so a genotype where neuron A feeds a delay that feeds back into A converts, sorts and evaluates. A cycle that does NOT pass through a delay is still refused, because that one really has no order.
⚠⚠ CfC and LTC neurons are still refused. Their dynamics are a per-neuron continuous-time update, which is a different thing from a unit delay, and converting one into the other would be an approximation reported as success.
========================================================================== THE CONTRACT, WHICH IS TIGHTER THAN THE SPEC SUGGESTS ==========================================================================
The node tuple carries an index, and the two implementations do not treat it the same way. The native compile_network discards it and pushes nodes into a vector, so a node's LIST POSITION is its index. The Erlang fallback builds a map keyed on the index it was given. The two agree only when index equals position, and nothing checks.
They diverge again on a source index that does not exist: the native evaluator indexes a vector and would panic, the fallback reads a map with a default of 0.0 and carries on.
And neither sorts. The native loop iterates the vector once in order, so a connection whose source appears later reads whatever that slot held, which is 0.0. A caller passing an unsorted list gets a silently wrong answer from both.
So this module emits, and asserts, all four:
1. index equals list position, over one contiguous run from zero 2. the first InputCount nodes are the inputs 3. topological order, every source strictly earlier than its consumer, EXCEPT for a delay's own sources, which are read a tick later and may therefore name anything 4. every source index in range
A genotype that cannot satisfy 3 is cyclic, which is recurrence, and is refused rather than evaluated into nonsense.
Summary
Functions
Convert and hand straight to the evaluator.
The flat node list, the input count and the output indices.
Types
-type dag() :: {[node_tuple()], non_neg_integer(), [non_neg_integer()]}.
-type node_tuple() :: {non_neg_integer(), atom(), atom(), float(), [{non_neg_integer(), float()}]}.
-type why() :: {cyclic, [term()]} | {unsupported_neuron_type, atom()} | {unknown_source, term(), term()} | {weight_count_mismatch, term(), non_neg_integer(), non_neg_integer()} | no_neurons.
Functions
Convert and hand straight to the evaluator.
The flat node list, the input count and the output indices.
Pure, so it can be inspected and tested without loading a NIF.