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); # ...; endeffectors(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:
| register | referenced as | for |
|---|---|---|
register_task! | task=:sym | a TaskSpec |
register_body! | body=:sym | a Body |
register_drive! | drive=:sym | a Drive (e.g. :oosawa) |
register_metric! | metrics=[:sym] | a collective metric |
register_analysis! | (analysis suite) | a measure over a run |
register_ablation! | ablation=:sym | a named perturbation |
register_optimizer! | optimizer=:sym | an 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/.