Streaming Control of a Rotary Joint

Joint control with one Elevate neuron per controller, reciprocal inhibition and excitatory sensory feedback in ROS2 Humble.

Abstract

Two single-neuron Elevate controllers drive opposing muscles on a rotary joint with a 2 g tip mass under gravity. Motor spikes activate muscles and inhibit the peer; angle feedback excites each controller. Three ROS2 Humble nodes exchange 1 ms spike windows in Docker on separate CPUs.

Results

Dynamics
Callback Timings
Figure 1. Recorded joint control. Blue/pink denote flexor/extensor; green denotes joint timing. Activation bars (%) share a fixed scale: zero to the recording's peak rounded up to a whole percentage point. Arrows show inhibitory, sensory and motor paths. The downward reference is 0°; mass depiction and particle travel times are schematic. Dynamics shows angle (°) and neuron voltage (mV). Timings shows callback duration (ms), excluding input waiting, against elapsed wall time (s); dashed lines mark the 1 ms period. The cursor marks replay time; click either graph to seek.

The recording covers 30 s of model time over 30.00 s of wall time. All three nodes completed 30,000 updates each; mechanical stop hits: 0. Timing verdict: fail.

Control Approach

The current tuning came from an empirical parameter scan: we varied the neurons’ background excitation, sensory feedback strength and muscle response per spike, then selected a combination that produced stable, repeatable oscillation without hitting the joint limits. The retired recordings were reported at about 0.57 Hz with a 63° peak-to-peak excursion; those measurements are no longer available for inspection here.

To formalise this, we could add an online adaptive controller that measures each cycle’s frequency and amplitude, compares them with the desired values, and gradually adjusts the parameters to reduce both errors. Small parameter perturbations could reveal their effects during operation, allowing adaptation to account for their coupling. Updates would need bounds and a slower timescale than the oscillation to preserve stability; achievable targets and convergence would require validation.

Timings Summary

QuantityValueMeaning
Internal integration step0.025 msIntegrates neuron equations and detects upward voltage crossings.
External simulation step1 ms model time40 internal steps; fixed input conductances.
Exported spike flag0 or 1 per neuron per 1 msAny upward crossing sets the flag; multiple crossings collapse to one. Finer spike times are discarded.
Mechanical step1 ms model timeUpdates muscle activation, velocity and angle.
Model window1 ms model timeModel time advanced per callback.
CallbackOne node updateAdvances the model, publishes outputs and records diagnostics.
Callback durationVariable wall timeExecution time, excluding input waiting.
Control period1 ms wall timeInterval between scheduled releases.
DeadlineScheduled release + 1 msRequired finish time, even after a late start.
Logical transport delay1 ms model timeWindow nn's spikes enter window n+1n+1.

Mean callback duration (95th percentile): flexor/extensor 0.534 ms (0.702 ms); joint 0.046 ms (0.073 ms).

Of 30,000 callbacks per node, deadline misses were 2,154 flexor (7.18%), 2,183 extensor (7.28%) and 1,480 joint (4.93%). Callbacks over 1 ms: 195 flexor, 218 extensor, 0 joint. Short callbacks can miss deadlines through late starts.

Neuron model

Elevate is a phenomenological model with sodium, potassium, calcium, slow potassium and leak currents:

CdVdt=I0+ge(Ee−V)+gi(Ei−V)+∑k∈{Na,K,Ca,KCa,L}gk(Ek−V).C\frac{dV}{dt}=I_0+g_e(E_e-V)+g_i(E_i-V) +\sum_{k\in\{\mathrm{Na},\mathrm{K},\mathrm{Ca},\mathrm{KCa},L\}}g_k(E_k-V).

V,t,C,I0V,t,C,I_0 denote voltage, time, capacitance and applied current. Excitatory and inhibitory conductances ge,gig_e,g_i have reversals Ee,EiE_e,E_i; intrinsic conductance gkg_k has reversal EkE_k. Units are mV, ms, nF, µS and nA; no cell-area calibration is assumed.

For gates j∈{m,h,n,c,q}j\in\{m,h,n,c,q\},

τjdUjdt=V−Uj,xj=σ ⁣[aj(Uj−bj)],σ(z)=11+exp⁡(−z).\tau_j\frac{dU_j}{dt}=V-U_j,\qquad x_j=\sigma\!\left[a_j(U_j-b_j)\right],\qquad \sigma(z)=\frac{1}{1+\exp(-z)}.

UjU_j is filtered voltage (mV), τj\tau_j its timescale (ms), xjx_j a dimensionless gate, aja_j the slope (mV−1^{-1}), bjb_j the midpoint (mV), and zz the dimensionless sigmoid argument. In that order, slopes are (0.2,−0.1,0.1,0.2,0.1)(0.2,-0.1,0.1,0.2,0.1) and midpoints (−25,−40,−15,−45,−30)(-25,-40,-15,-45,-30) mV:

gNa=gˉNaxmxh,gK=gˉKxn,gCa=gˉCaxc,gKCa=gˉKCaxq,gL=gˉL.g_{\mathrm{Na}}=\bar g_{\mathrm{Na}}x_mx_h,\quad g_{\mathrm{K}}=\bar g_{\mathrm{K}}x_n,\quad g_{\mathrm{Ca}}=\bar g_{\mathrm{Ca}}x_c,\quad g_{\mathrm{KCa}}=\bar g_{\mathrm{KCa}}x_q,\quad g_L=\bar g_L.

gˉk\bar g_k is maximum conductance. Both potassium currents use EKE_{\mathrm K}. KCa depends on filtered voltage, with no calcium-concentration state. All six states, VV and the five UjU_j, start at −60-60 mV.

Numerical update and spike output

Each 1 ms step uses 40 split substeps of h=0.025h=0.025 ms with fixed input conductances. First update the filters using the old voltage:

Uj+=V+(Uj−V)exp⁡(−h/τj).U_j^+=V+(U_j-V)\exp(-h/\tau_j).

The superscript ++ denotes an updated value. Evaluate gates and conductances from Uj+U_j^+, then update the membrane with conductances frozen:

G=ge+gi+∑kgk,V∞=I0+geEe+giEi+∑kgkEkG,V+=V∞+(V−V∞)exp⁡(−hG/C).G=g_e+g_i+\sum_k g_k,\qquad V_\infty=\frac{I_0+g_eE_e+g_iE_i+\sum_k g_kE_k}{G}, \qquad V^+=V_\infty+(V-V_\infty)\exp(-hG/C).

GG is total conductance (µS), V∞V_\infty the equilibrium voltage (mV), and kk indexes the five intrinsic currents. Each frozen sub-update is exact; their composition approximates the coupled dynamics. A crossing V≤0<V+V\leq0<V^+ mV sets the window’s spike flag without resetting state.

Synaptic and local drive

Let r∈{f,e}r\in\{f,e\} denote flexor or extensor, rˉ\bar r its peer, and n=0,1,…n=0,1,\ldots the control window. Synapses decay, then receive spike increments:

gs,rn=e−Δt/τsgs,rn−1+wsSrn−1,gp,rn=e−Δt/τpgp,rn−1+wpMrˉn−1.\begin{aligned} g_{\mathrm{s},r}^{n} &=e^{-\Delta t/\tau_{\mathrm{s}}}g_{\mathrm{s},r}^{n-1} +w_{\mathrm{s}}S_r^{n-1},\\ g_{\mathrm{p},r}^{n} &=e^{-\Delta t/\tau_{\mathrm{p}}}g_{\mathrm{p},r}^{n-1} +w_{\mathrm{p}}M_{\bar r}^{n-1}. \end{aligned}

gs,rg_{\mathrm{s},r} and gp,rg_{\mathrm{p},r} are sensory AMPA and peer GABA conductances (µS). Srn−1S_r^{n-1} and Mrˉn−1M_{\bar r}^{n-1} count the preceding window’s sensory and peer motor spikes. With Δt=1\Delta t=1 ms, decay times are (τs,τp)=(10,50)(\tau_{\mathrm{s}},\tau_{\mathrm{p}})=(10,50) ms and per-spike weights are (ws,wp)=(0.035,0.2)(w_{\mathrm{s}},w_{\mathrm{p}})=(0.035,0.2) µS. Initial conductances and input counts are zero; increments are added in full, without timestep scaling.

Unfiltered local excitation switches from starting to tonic drive:

glocal,r(t)={gstart,r,0≤t<Tstart,gtonic,t≥Tstart.g_{\mathrm{local},r}(t)= \begin{cases} g_{\mathrm{start},r}, & 0\leq t<T_{\mathrm{start}},\\ g_{\mathrm{tonic}}, & t\geq T_{\mathrm{start}}. \end{cases}

tt is model time from startup, Tstart=20T_{\mathrm{start}}=20 ms, (gstart,f,gstart,e)=(0.5,0)(g_{\mathrm{start},f},g_{\mathrm{start},e})=(0.5,0) µS and gtonic=0.04g_{\mathrm{tonic}}=0.04 µS. At tn=nΔtt_n=n\Delta t, the membrane inputs are

ge,rn=glocal,r(tn)+gs,rn,gi,rn=gp,rn.g_{e,r}^{n}=g_{\mathrm{local},r}(t_n)+g_{\mathrm{s},r}^{n}, \qquad g_{i,r}^{n}=g_{\mathrm{p},r}^{n}.

ge,rng_{e,r}^{n} and gi,rng_{i,r}^{n} are total excitatory and inhibitory conductances. Recorded settings appear in the parameter table.

Joint dynamics model

Two muscles drive a spring-damped joint. Zero points downward; positive angles point right. Only the tip mass contributes gravity:

(J0+mL2)θ¨+bθ˙+k(θ−θr)+mgLsin⁡θ=τmax⁡(af−ae).(J_0+mL^2)\ddot{\theta}+b\dot{\theta}+k(\theta-\theta_r)+mgL\sin\theta =\tau_{\max}(a_f-a_e).

θ\theta is angle (rad); dots denote derivatives in seconds. J0J_0 is base inertia (kg m2^2), mm tip mass (kg), LL arm length (m), gg gravity (m/s2^2), θr\theta_r spring rest angle (rad), bb damping (N m s/rad), and kk stiffness (N m/rad). af,ae∈[0,1]a_f,a_e\in[0,1] are flexor/extensor activations; τmax⁡\tau_{\max} is maximum muscle torque (N m).

At mechanical step nn, muscle i∈{f,e}i\in\{f,e\} updates as

ai,n+1=min⁡ ⁣(1,ai,nexp⁡[−Δt/τa]+q si,n).a_{i,n+1}=\min\!\left(1, a_{i,n}\exp[-\Delta t/\tau_a]+q\,s_{i,n}\right).

Here Δt=0.001\Delta t=0.001 s, τa\tau_a is activation decay time (s), qq the dimensionless spike increment, and si,ns_{i,n} the preceding window’s motor spike count per neuron. Updated activations set torque. Semi-implicit Euler updates velocity, then angle; stops at θr±1\theta_r\pm1 rad clamp angle and zero velocity.

The joint starts at θr=π/2\theta_r=\pi/2 (90°), with zero velocity and activation. Total inertia is 0.005080.00508 kg m2^2; initial gravity torque is −0.003924-0.003924 N m. Flexor raises the mass against gravity; extensor lowers it.

Before each update, sensory spikes excite flexor for θ<θr−θs\theta<\theta_r-\theta_s and extensor for θ>θr+θs\theta>\theta_r+\theta_s. The sensory centre is θr\theta_r, threshold θs=0.25\theta_s=0.25 rad, and minimum interspike interval 10 ms. Each channel repeats while beyond threshold.

Learning rule

The separate native adaptation protocol uses bounded coordinate search to adjust tonic excitation gtonicg_{\mathrm{tonic}} and sensory synaptic strength wsw_{\mathrm s}, symmetrically across flexor and extensor. Reciprocal inhibition and mechanics remain fixed. This is a model-specific application of derivative-free direct search, using global movement errors:

L=(f−f⋆f⋆)2+(A−A⋆A⋆)2.\mathcal L= \left(\frac{f-f_\star}{f_\star}\right)^2+ \left(\frac{A-A_\star}{A_\star}\right)^2.

L\mathcal L is dimensionless loss. Frequency ff (Hz) is the reciprocal of mean cycle period; excursion AA (rad) is the mean peak-to-peak angle across measured cycles. Positive targets f⋆,A⋆f_\star,A_\star default to 0.65 Hz and π/3\pi/3 rad (60° peak-to-peak). The two relative errors receive equal weight.

Alternate between the two conductances. For the selected coordinate jj, test both proposals

pj′=clip⁡ ⁣((1+dρj)pj,  0.01,  0.10),d∈{−1,+1}.p'_j=\operatorname{clip}\!\left((1+d\rho_j)p_j,\;0.01,\;0.10\right), \qquad d\in\{-1,+1\}.

pjp_j is the current conductance and pj′p'_j a proposal, both in µS; dd chooses decrease or increase, ρj\rho_j is that coordinate’s fractional probe size, and clip⁡\operatorname{clip} bounds conductance to 0.01–0.10 µS. Keep the baseline unless an eligible proposal lowers L\mathcal L by more than 10−610^{-6}; choose the better eligible proposal. Probe sizes start at 10% and halve after an unsuccessful comparison, down to 1.25%.

Each baseline or probe lasts at least 26 s of model time. Discard the first 15 s and measure the final five complete cycles, delimited by upward crossings of θr\theta_r. Eligibility requires no stop hits and all cycle-repeatability errors below 1%: maximum relative period and turning-point errors, and worst-cycle normalized root-mean-square errors for angle and velocity. Period errors use the median period; turning-point and angle errors use median excursion, and velocity errors use the median waveform’s velocity range. Unsettled segments extend in 5 s increments up to 60 s. Unsettled probes are excluded; an unsettled baseline skips its update without shrinking the probe size.

Neural, synaptic, muscle and pending spike states continue between probes. After 16 coordinate attempts, freeze the parameters and validate for 60 s both continuously and from a fresh reset, assessing the final ten complete cycles after settling. Local repeatability alone does not guarantee target acquisition or repeatability after reset.

Architecture Pseudocode

Each node retains state. These Python-like helpers summarize execution.

Setup

dt = 1 * ms
controllers = paired_controllers(neuron="Elevate")
joint = loaded_joint(initial_angle=90 * degrees)

warm_up_then_reset(controllers)
start_together(*controllers, joint)

Warm-up resets controller states, inputs and model clocks before execution.

Each control window

Each node waits for its release and both preceding input windows, servicing messages and enforcing its lag limit. Startup inputs are empty; silence is published explicitly.

def run(node):
    for window in fixed_schedule(period=dt):
        inputs = receive_previous_window(window)
        outputs = node.update(inputs)
        publish_window(window, outputs)  # Include windows with no spikes.
        record_timing(deadline=window.release + dt)

Late nodes catch up without dropping windows or shifting deadlines.

Controller and joint updates

Controllers share this update but receive different starting excitation.

def update_controller(controller, sensory_spikes, peer_spikes, time):
    drive = starting_drive(controller) if time < 20 * ms else tonic_drive
    controller.excite(local_drive=drive, sensory_spikes=sensory_spikes)
    controller.inhibit(peer_spikes)
    return controller.advance(dt).spikes


def update_joint(joint, motor_spikes, time):
    sensory_spikes = sense_angle(joint, time)
    activation = decay_then_activate(joint.muscles, motor_spikes)
    torque = opposing_muscle_torque(activation)
    advance_mechanics(joint, torque, dt)
    return sensory_spikes

Network diagram

Native recorded controller circuit

Parameters

Values come from the selected recording’s native snapshot and configuration.

ParameterRecorded value
Network
Motor cells per controller1
Controller populationsmotor
Neuron modelelevate
Ionic membrane dynamics
Capacitance (nF)1
Sodium conductance (µS)100
Potassium conductance (µS)65
Calcium conductance (µS)2
Slow potassium conductance (µS)10
Leak conductance (µS)2
Sodium reversal (mV)40
Potassium reversal (mV)-90
Calcium reversal (mV)120
Leak reversal (mV)-50
Excitation reversal (mV)0
Inhibition reversal (mV)-80
Initial voltage (mV)-60
Constant applied current (nA)-5
Sodium activation filter (ms)0.1
Sodium inactivation filter (ms)1
Potassium activation filter (ms)1
Calcium activation filter (ms)1
Slow potassium activation filter (ms)75
Maximum internal step (ms)0.025
Synaptic decay (ms)
local_biasDirect conductance
peer_inhibition50
sensory_drive10
Synapses and weights
local_bias1
peer_inhibition0.2
sensory_drive0.035
Input and simulation
Duration (s)30
Numerical timestep (ms)1
Control period (ms)1
Seed0
Controller warm-up simulated duration (ms)1000
Starting pulse duration (ms)20
Starting flexor / extensor drive (µS)0.5 / 0
Ongoing tonic drive (µS)0.04
Spike transportlogical
SNNLab executiondefault
Lag stop / delivery budget (ms)8000 / 5
Analysis
Seed count1 (selected acquisition)
Assessed complete cycles10
Cycle burn-in (s)5
Cycle relative tolerance0.01
Phase samples per cycle128
Joint mechanics
Activation increment per normalized spike0.003
Activation decay (s)0.08
Angle conventionzero_downward_positive_right
Arm length (m)0.2
Damping (N m s/rad)0.02
Gravity (m/s²)9.81
Hard stop relative to centre (rad)1
Base inertia (kg m²)0.005
Initial angle (rad)1.5707963267948966
Maximum muscle torque (N m)0.8
Sensory and stop centre (rad)1.5707963267948966
Minimum sensory interval (s)0.01
Sensory threshold relative to centre (rad)0.25
Spring rest angle (rad)1.5707963267948966
Stiffness (N m/rad)0.02
Tip point mass (kg)0.002
Total inertia including tip mass (kg m²)0.00508

Dataset

Upstream: —

Downstream: —

RunDateDurationSizeEvidenceStorageRan onVerification
exp152-r116-present1s19.4 MBCurrentR2LocalLocal: absent; R2: catalog-verified
exp152-r115-analyse<1s4.8 MBCurrentR2LocalLocal: absent; R2: catalog-verified
exp152-r114-compute41s68.4 MBCurrentR2LocalLocal: absent; R2: catalog-verified
exp152-r113-analyse<1s127.9 KBCurrentR2LocalLocal: absent; R2: catalog-verified
exp152-r112-compute36s1.9 MBCurrentR2LocalLocal: absent; R2: catalog-verified