genotype_codec (faber_tweann v2.4.0)

View Source

Canonical, lossless serialisation of a genotype, so an evolved topology can leave the VM it was bred in.

ROADMAP item 8b. 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, and it was a missing function rather than a missing capability.

========================================================================== WHY NOT term_to_binary/2 ==========================================================================

The deterministic option is not promised to be stable across OTP releases, so it is unfit for a content address: the same genotype would hash differently on two nodes running different releases, each side would conclude the other's genome was a different genome, and nothing would say so. The sibling failure is on record as hecate-dronex REGISTER I.12, where term_to_binary/1 over a map produced per-node byte differences, two identical images computed different engine fingerprints, each filtered the other out as incompatible, and no exchange was ever attempted with nothing logged anywhere.

So the encoding here is hand-rolled and closed. It covers exactly the term shapes a genotype contains, which is atoms, integers, floats, binaries, proper lists and tuples, and it REFUSES everything else rather than guessing. Verified against include/records.hrl: the genotype records contain no maps, and a map is the shape that made I.12 possible.

========================================================================== VALIDATE AND REJECT, NEVER CLAMP ==========================================================================

A limit that clamps changes the genome, and then the id no longer identifies the thing that ran. Every limit below refuses. The limits are a denial-of-service defence against a stranger's genome, not a quality bar.

Decoding resolves atoms with binary_to_existing_atom/2 for the same reason: the atom table is not garbage collected, so decoding an untrusted genome must not be able to mint atoms. A genotype's atoms are activation function names, aggregator names, neuron types and record tags, all of which exist in any VM that loaded this application. An unknown atom means the genome came from an incompatible build, and saying so loudly is the correct answer.

========================================================================== WHAT IS AND IS NOT PRESERVED ==========================================================================

Lossless. The agent record, its cortex, and every neuron, sensor and actuator the cortex names, verbatim, including bookkeeping fields that do not determine behaviour. Choosing which fields "matter" would be a silent lossy conversion, which is the exact defect this module sits beside: network_evaluator:from_genotype/1 approximates a topology and reports success. A caller that wants less can drop fields itself.

Identity is preserved too, so the content address covers WHICH genotype this is and not only what shape it has. Two structurally identical genotypes bred at different times have different ids and therefore different addresses. Structural equivalence is a different question and is deliberately not answered here.

Summary

Functions

Restore a packed genotype into the local tables, verbatim.

The content address of a packed genotype.

The limits a genome is validated against, so a caller can check before building rather than after being refused.

Pack an agent and everything its cortex names into canonical bytes.

Types

reason/0

-type reason() ::
          {unsupported_term, term()} |
          {non_finite_float, binary()} |
          {too_deep, pos_integer()} |
          {too_many_elements, non_neg_integer()} |
          {atom_too_long, non_neg_integer()} |
          {too_many, neurons | sensors | actuators, non_neg_integer()} |
          {too_large, non_neg_integer()} |
          {unknown_atom, binary()} |
          bad_magic | truncated | trailing_bytes |
          {missing, cortex | neuron | sensor | actuator, term()} |
          {agent_exists, term()} |
          {agent_not_found, term()}.

Functions

from_binary(Bin)

-spec from_binary(binary()) -> {ok, term()} | {error, reason()}.

Restore a packed genotype into the local tables, verbatim.

Refuses if an agent with that identity is already present. Importing a stranger's genome as a distinct local agent is clone_Agent/1 on the restored id, which is existing machinery and is not duplicated here.

genome_id(Bin)

-spec genome_id(binary() | term()) -> {ok, binary()} | {error, reason()}.

The content address of a packed genotype.

Takes either an agent id or already-packed bytes. It identifies this genotype, identity included, and not its structure alone.

limits()

-spec limits() -> #{atom() => pos_integer()}.

The limits a genome is validated against, so a caller can check before building rather than after being refused.

to_binary(AgentId)

-spec to_binary(term()) -> {ok, binary()} | {error, reason()}.

Pack an agent and everything its cortex names into canonical bytes.

The same genotype packs to the same bytes on any node and any OTP release, which is what makes genome_id/1 an address rather than a hint.