Receptors & Effectors
The sensorimotor interface — how many sensors an agent has, where they point, how the world becomes receptor current, and how effector output becomes action — is a first-class part of the experiment, not an implementation detail. This page is the mechanism and configuration of that interface, plus where it is going. For the per-task R/E counts, see Environments & Tasks; here we cover how those counts are produced and how they will become settable.
The interface
The reservoir talks to the world through two vectors: receptors (length
n_receptors, the sensory input) and effectors (length n_effectors, the
motor output). The Body owns the morphology that sits between them and the world:
percept ─receptors(body)→ R ─step!(reservoir,R)→ spikes ─effectors(reservoir)→ E ─decode_effectors(body)→ command ─actuate!(env)→ worldPassthroughBodyrelays a single-agent task env’s / unchanged — theEnvowns the world encoding (sense/step!) and effector decode.VENBodyis the embodied swarm body: it turns bearing-vision into and VEN kinematics out of . Coupling between agents is vision, so the sensor geometry literally is the interaction topology.
Effector semantics are task-specific, not globally uniform — channel 1 of Pong is
not the same physical quantity as channel 1 of cartpole. A reservoir is always built to
match the task/body it will drive, i.e. to the resulting (n_receptors, n_effectors).
See the node contract for what a reservoir must implement.
Source: src/world/Body.jl (PassthroughBody, VENBody, sense_agents), src/world/Environments.jl (env-side encoding/decode).
Current state: fixed per task/body
Today counts and placements are hardcoded constants baked into the env/body. The per-task numbers live in Environments & Tasks; the two mechanics worth knowing here are the swarm’s padding conventions:
- Torus —
sense_agentscomputes 62 bearing-vision values (two eyes × 31 angles,SENS_ANGLES_DEG), butVENBody.receptorshands the reservoir a 64-channel vector by copying those intoinputs[3:64]. Swarm reservoirs are therefore built(n_receptors=64, n_effectors=3). - Forage — keeps that 64-channel conspecific bank intact and appends a second
64-wide source bank with the same bearing geometry (weighted by
source_gain), so reservoirs are built(n_receptors=128, n_effectors=3). Effector semantics are unchanged.
Placements are equally fixed: TrackingEnv bakes eye_offsets_deg = (30, -30) and
sensor_offsets_deg = -60:4:60; VENBody uses SENS_ANGLES_DEG. There is no way to
vary the sensor count or geometry without editing env/body code — that is the gap the
roadmap below closes.
Source: src/world/Body.jl (SENS_ANGLES_DEG, receptors, the inputs[3:64] padding), src/world/Morphology.jl (VENMorphology, PortSpec).
Timing & temporal coding
Distinct from what the sensors are is how the reservoir is clocked against the world.
Today, the Falandays path runs exactly one reservoir tick per env step: graded
continuous input, effector read from that single tick’s spike pattern. The “rate” is
the spatial proportion of the nodes spiking — a population code, not a temporal
accumulation. The reservoir is leaky, recurrent, and not reset between steps, so
temporal memory lives in its state; only the readout is instantaneous. This is
the stable clocking for :falandays_base (see Falandays).
Where it bites: fast tasks with a sensitive readout — Pong above all (fast ball, jittery single-tick paddle command) — and small- runs.
Built so far (CTRNN only): the compartmental nodes already implement integration
sub-stepping — CompartmentalReservoir(...; substeps=k), default 5
(), holding the afferent across sub-steps and
reporting the per-node spike rate over the window; substeps=1 reproduces the
single-step oracle. This is the CTRNN’s internal-integration version of the knob.
Source: src/nodes/CompartmentalReservoir.jl (substeps, dt_sub, per-sub-step integration).
Planned: the spec refactor
Both roadmap angles start from the same refactor: lift the R/E layout out of hardcoded constants into a spec object that defaults to today’s values.
struct SensorSpec angles_deg::Vector{Float64} # one entry per sensor (placement) tuning::Float64 # Gaussian width / receptive-field size encoding::Symbol # :raycast | :gaussian_bearing | :spikeff2 | ... noise::Float64 # additive sensory noiseend
struct EffectorSpec n::Int decode::Symbol # :differential | :vote | :ven_kinematics | ...endThe env/body would read its SensorSpec / EffectorSpec instead of constants;
n_receptors becomes the decoded input width after any task/body padding.
Angle A — config-driven layout
Expose the spec through the TOML config so an experimenter sets sensor count, placement, tuning, encoding (and effector count/decode) per task/body without touching code:
[task.sensors]encoding = "gaussian_bearing"angles_deg = [-60, -40, -20, 0, 20, 40, 60] # 7 sensors, not the default layouttuning = 10.0noise = 0.1
[task.effectors]n = 2decode = "differential"resolve would validate counts, angles, and decode compatibility, then build the node
to the resulting (R, E). This is also where the timing knobs above would live.
Angle B — evolvable morphology (within set ranges)
Make part of the spec a bounded morphology genome that sep-CMA co-evolves alongside the controller — “evolve where the sensors point, and how many,” with researcher-set ranges:
[task.sensors.evolve]angle_range_deg = [-90, 90] # each sensor angle bounded heren_range = [2, 32] # variable length via fixed max + masktuning_range = [4.0, 20.0]Mechanics: a morphology parameter block is appended to the genome; each gene is mapped through its bound, ; the env builds sensors from the decoded morphology each rollout. Variable sensor count is handled by a fixed-max layout plus per-sensor enable gates. Controller and body then co-adapt — the agent discovers its own sensorimotor arrangement. Angle B depends on Angle A.
Why this matters for the collective
In the swarm the default coupling between agents is VENBody vision — so the sensor
geometry (eye offsets, field of view, range) is the interaction topology. Making it
tunable and evolvable means you can study how collective behaviour (flocking, milling,
foraging convergence) depends on, or co-evolves with, the sensorimotor interface —
not just the controller.
Status
- Done — per-task/body R/E documented and inspectable (Environments & Tasks); baseline timing scheme (1 tick/env-step for Falandays; CTRNN internal
substeps). - Planned — the spec refactor (Angle A: sensor/effector layout + readout timing knobs).
- Planned — evolvable morphology (Angle B), which depends on Angle A.
These are the next build targets for the I/O layer. For adding your own env, body, or node against the existing contract, see Extending it.