MassSpringDamperModel
MassSpringDamperModel simulates a point mass on a spring and damper, coupled to a neighbouring model.9 minute read
MassSpringDamperModel simulates a point mass on a spring and damper, coupled
to a neighbouring model. Its purpose is chains: connect several instances
and you have a flexible structure, a series-elastic joint, or a multi-body
compliance model built from copies of one block.
It does both compliance directions in one block. By default it behaves like an
admittance — force in, motion out. Set enableImpedance
and it behaves like an impedance — motion in, force out —
and adds an inertia term to its output force.
flowchart LR
i1(["inputForce — external force on the mass"]) --> B["MassSpringDamperModel"]
i2(["inputState — external state, impedance mode only"]) --> B
i3(["connectedForce — force from the next model"]) --> B
i4(["connectedState — state of the previous model"]) --> B
B --> o1(["outputState — this mass's motion"])
B --> o2(["outputForce — coupling force"])
B --> o3(["mass — effective mass"])
B --> o4(["damping — effective damping"])
B --> o5(["stiffness — effective stiffness"])
B --> o6(["relativePosition — spring extension"])
B --> o7(["relativeVelocity"])
The mass obeys $M\ddot{x} =$
inputForce$+$connectedForce$-$outputForce, and the coupling force is $K,\Delta x + D,\Delta\dot{x}$, plus $M,\Delta\ddot{x}$ in impedance mode. Aim at $\omega_n = \sqrt{K/M}$ [rad/s] and $\zeta = D/(2\sqrt{KM})$ — target $\zeta$ near 1. Keep the task period below $2/\omega_n$ or the simulation grows without bound; nothing checks this.
To build a chain: connect this model’s outputForce to the previous
model’s connectedForce, and the previous model’s outputState to this one’s
connectedState. The spring anchors on connectedState.
This block has no enable and no disable. disableDynamics pins the motion
to connectedState but still publishes a coupling force, so a chain keeps
transmitting while it is set.
Signals
Inputs
| Path | Unit | Range | Description |
|---|---|---|---|
inputForce |
N or N·m | unbounded | The external force pushing on this mass. Forces smaller than inputForceDeadZone are ignored. |
inputState |
m, m/s, m/s² | unbounded | An externally measured state, as x, xDot and xDDot together. Only read in impedance mode — in the default admittance mode it is ignored entirely. |
connectedForce |
N or N·m | unbounded | The coupling force from the next model in the chain. Added directly to the net force. |
connectedState |
m, m/s, m/s² | unbounded | The previous model’s state. It is the spring’s anchor and the reference both integrators return to. |
Outputs
| Path | Unit | Description |
|---|---|---|
outputState |
m, m/s, m/s² | This mass’s position, velocity and acceleration, together. Chain it to the next model’s connectedState. Starts at zero and is not cleared by a stop. |
outputForce |
N or N·m | The coupling force the spring and damper transmit. Chain it to the previous model’s connectedForce. Not zeroed by disableDynamics. |
mass |
kg or kg·m² | The effective mass — massGain times the mass table’s output. Watch this, not the table. |
damping |
N·s/m | The effective damping. |
stiffness |
N/m | The effective stiffness. |
relativePosition |
m or rad | The spring’s extension or compression. In admittance mode this is outputState minus connectedState; in impedance mode inputState minus connectedState. |
relativeVelocity |
m/s or rad/s | The velocity across the damper, from the same source as above. |
Parameters
| Path | Unit | Default | Range | Effect |
|---|---|---|---|---|
enableImpedance |
- | false | - | False gives admittance: the coupling force is spring + damper, computed from this block’s own motion. True gives impedance: the coupling force is computed from inputState instead, and gains an inertia term. |
massGain |
- | 1 | 0.001 upward | Scales the mass table. Values below 0.001 are corrected up, so the mass gain can never be zero or negative. |
stiffnessGain |
- | 1 | any | Scales the stiffness table. Its sign is forced positive, so a negative value acts as its magnitude. |
dampingGain |
- | 1 | any | Scales the damping table. Its sign is forced positive too. |
disableDynamics |
- | false | - | True pins outputState to connectedState every cycle, so the mass stops moving on its own. It does not stop the coupling force. |
inputForceDeadZone |
N or N·m | 0.1 | 0 upward | Forces smaller than this do not push the mass. Raise it to stop a noisy force reading creeping the model. |
All six are persistent and survive a restart.
The three coefficients live below this block, as lookup tables:
massLookup driven by this mass’s own position, dampingLookup by the
relative velocity, and stiffnessLookup by the relative position. Each ships
as a single point at 1.0. The two integrators — velocityIntegrator and
positionIntegrator — carry the motion limits. See lookup.md
and integrator.md.
Never let massLookup return zero. The mass divides the net force, and a
zero mass makes the simulation non-numeric. A table that is not strictly
increasing in x also returns zero, so check monotonicity as well as values.
Setup
-
Configure
massLookupwith the mass you want this point to have. A single point is enough if it does not vary with position. Confirm it never returns zero. -
Configure
dampingLookupandstiffnessLookup. Start with a low stiffness and generous damping — a soft, well-damped model is safe to experiment with. -
Compute $\omega_n = \sqrt{K/M}$ and check the task period is below $2/\omega_n$, comfortably. A stiff spring on a slow task will not simulate; it will diverge.
Step 3 is the check nothing does for you. There is no warning and no clamp. Too stiff a spring for the task rate makes
outputStategrow without bound, and the only way out is a restart. -
Set the integrators' output limits to the travel and speed this mass may have.
-
Leave
enableImpedancefalse for a first pass. LinkinputForceand watchoutputStaterespond. -
Read
mass,dampingandstiffnessback and confirm they are the values you intended. They are the gain times the table output. -
To chain: link this model’s
outputForceto the previous model’sconnectedForce, and that model’soutputStateto this one’sconnectedState. Build the chain one link at a time and check each is stable before adding the next.
Tuning
- Set the mass first, from the physical mass this point represents.
- Set the stiffness from the compliance you are modelling. For a series-elastic joint that is the spring’s real rate.
- Set the damping for stability: compute $\zeta = D/(2\sqrt{KM})$ and aim near
- Below about 0.5 the model rings, and in a chain that ringing couples between links.
- Re-check step 3 of Setup after every stiffness or mass change. The stability bound moves with both.
- Use the tables rather than the gains for anything non-linear — a progressive spring, or a damper that softens at speed.
- Choose the mode deliberately, and not mid-run. Switching
enableImpedancechanges which inputs matter: admittance mode ignoresinputState, and impedance mode stops using this block’s own position for the coupling force. - In a chain, tune from one end. Each link’s
connectedStateis the previous link’s output, so an unstable link destabilises everything after it.
Read the damping ratio off the overshoot on any curve.
| Symptom | Cause | Action |
|---|---|---|
outputState grew without bound |
The task period is too slow for the stiffness and mass | Work step 3 of Setup, then restart the controller |
| Everything went non-numeric | massLookup returned zero — either its value is zero or its x values are not strictly increasing |
Fix the table, then restart the controller |
| The mass does not move at all | disableDynamics is set, or inputForce is inside the dead zone |
Clear disableDynamics; lower inputForceDeadZone |
The mass still transmits force while disableDynamics is set |
Expected: that parameter pins the motion, not the force | Set the stiffness and damping gains to 0 as well |
outputForce does not respond to this mass’s own motion |
Expected in impedance mode: the coupling force is computed from inputState |
Clear enableImpedance, or link inputState |
inputState seems to be ignored |
Expected in admittance mode, which is the default | Set enableImpedance if you meant impedance |
| The force gained an extra term when the mode changed | Expected: impedance mode adds an inertia term to the coupling force | This is the mode’s purpose |
| The model rings after a force step | Damping too low for the stiffness | Raise dampingGain until $\zeta$ is near 1 |
| The model is sluggish | Damping too high, or the mass too large | Lower dampingGain, then the mass |
| A chain oscillates although each link looks fine alone | Links couple, and a lightly damped link excites its neighbours | Raise damping across the chain, and tune from one end |
| The effective coefficients are not what the table says | Expected: each is the gain times the table output | Read mass, damping and stiffness — all three are published |
| A negative stiffness or damping gain had no effect on the sign | Expected: both are forced positive | Use the table values if you need a sign change, though a negative spring is unphysical |
massGain reads back as 0.001 after writing 0 |
Expected: it is floored | Set a real value |
| The simulation kept running after a controller stop and start | Expected: the state is not cleared at start | Set disableDynamics for a cycle to pin it back to connectedState |
| The mass feels heavier at one end of its travel | Expected if massLookup is shaped that way — it is driven by this mass’s own position |
Flatten the table |
| A non-numeric value appeared and will not clear | There is no reset input, and the integrators hold it | Restart the controller |
A starting point for a soft, well-damped single mass on a 1 ms task — $\omega_n$ = 4.5 rad/s, $\zeta$ = 1.1, comfortably inside the stability bound:
enableImpedance = false
massGain = 1.0
stiffnessGain = 1.0
dampingGain = 1.0
disableDynamics = false
inputForceDeadZone = 0.1
massLookup: numPoints = 1, x = 0, y = 1.0
stiffnessLookup: numPoints = 1, x = 0, y = 20.0
dampingLookup: numPoints = 1, x = 0, y = 10.0
This is a starting point, not a final tuning.
Limits and errors
| Limit | Set by | What happens | Reported |
|---|---|---|---|
| Task period against the model’s natural frequency | Nothing | Not checked. The task period must stay below 2 divided by $\sqrt{K/M}$. Too stiff a spring for the task rate makes the state grow without bound; only a restart clears it | Not reported |
| Effective mass above zero | Nothing | Not checked. massGain is floored at 0.001, but the mass table can still return zero — including when its x values are not strictly increasing, which returns zero by design. The result is a division by zero |
Not reported; visible on mass |
stiffnessGain, dampingGain |
Sign forced | Both are used as their magnitude, so a negative value cannot invert the force | Not reported; read the value back |
massGain ≥ 0.001 |
Fixed | Corrected up, which also excludes negatives | Not reported; read the value back |
outputForce |
Nothing | Unbounded. Bound it downstream, and remember it is not zeroed by disableDynamics |
Not reported |
outputState |
The two integrators' output limits | Clamped by whatever those sub-trees are set to | Not reported |
| Non-numeric inputs | Nothing | Not guarded anywhere. A non-numeric value latches into the integrators and there is no reset input to clear it | Not reported |
| Model state | disableDynamics |
Pins the state to connectedState. That is the only reset, and it is a parameter rather than a linkable input |
Not reported |
| Channel count | Fixed | One mass per instance. A chain is several instances | Not reported |
The block raises no errors or warnings and logs nothing. Every failure above shows as a value on a trace, not as a message.
Verified against motorcortex-control3 3.30.0 (bc348fd).