Skip to content

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 RR (length n_receptors, the sensory input) and effectors EE (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)→ world
  • PassthroughBody relays a single-agent task env’s RR/EE unchanged — the Env owns the world encoding (sense / step!) and effector decode.
  • VENBody is the embodied swarm body: it turns bearing-vision into RR and VEN kinematics out of EE. 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:

  • Torussense_agents computes 62 bearing-vision values (two eyes × 31 angles, SENS_ANGLES_DEG), but VENBody.receptors hands the reservoir a 64-channel vector by copying those into inputs[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 NN 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-NN runs.

Built so far (CTRNN only): the compartmental nodes already implement integration sub-stepping — CompartmentalReservoir(...; substeps=k), default 5 (dtsub=dt/5=0.2\text{dt}_\text{sub} = \text{dt}/5 = 0.2), 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 noise
end
struct EffectorSpec
n::Int
decode::Symbol # :differential | :vote | :ven_kinematics | ...
end

The 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 layout
tuning = 10.0
noise = 0.1
[task.effectors]
n = 2
decode = "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 here
n_range = [2, 32] # variable length via fixed max + mask
tuning_range = [4.0, 20.0]

Mechanics: a morphology parameter block is appended to the genome; each gene is mapped through its bound, glo+(hilo)σ(g)g \mapsto \text{lo} + (\text{hi}-\text{lo})\,\sigma(g); 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).
  • Plannedevolvable 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.