Skip to content

Extending it

Every surface has a registry: register a part under a symbol, then reference that symbol from high-level code. The framework is designed to be extended without forking.

Add a node

A node constructor must accept (n_nodes, n_receptors, n_effectors; seed=0, kwargs...) and return a Reservoir implementing the contract. Because you’re adding methods to BrainlessLab’s generics, you must import them (not just using):

import BrainlessLab: step!, effectors, reset!, n_receptors, n_effectors
struct MyNode <: Reservoir
# ...
end
function step!(r::MyNode, R); # ...; end
effectors(r::MyNode, spikes) = # ...
register_node!(:mynode,
(n, r, e; seed=0, kwargs...) -> MyNode(n, r, e; seed=seed, kwargs...);
genome_type = MyNodeParams) # so evolve() derives the genome from the contract
simulate(:wall; node=:mynode)

Without import, Julia won’t add your methods to the package generics and simulate won’t dispatch to your node.

The other registries

The same register_*! / resolve_* pattern is consumed by the framework for:

registerreferenced asfor
register_task!task=:syma TaskSpec
register_body!body=:syma Body
register_drive!drive=:syma Drive (e.g. :oosawa)
register_metric!metrics=[:sym]a collective metric
register_analysis!(analysis suite)a measure over a run
register_ablation!ablation=:syma named perturbation
register_optimizer!optimizer=:syman AbstractEvolutionStrategy
register_view!view(sim, :sym)a Makie panel

Genotype vs state

Keep the two separate:

  • pack_params / unpack_params / paramdim — evolvable parameters an optimizer stores and mutates (the genome).
  • snapshot_state / load_state! — transient runtime state (activations, learned weights, buffers) for replay/reset.

A ready-to-copy skeleton lives in examples/templates/new_project/. See Contracts for the full participant-facing contract each register_*! implies.

Source: src/core/Registry.jl, src/core/Interfaces.jl, examples/templates/new_project/.