SoftLimiter

SoftLimiter bounds a signal the way a hard limiter does, but rounds the approach instead of cutting it off.
3.30–3.34

SoftLimiter bounds a signal the way a hard limiter does, but rounds the approach instead of cutting it off. Between the break points the output equals the input exactly; beyond them it curves over and approaches the limit gradually.

It adds no lag and no dynamics — the curve depends only on the current input, so unlike a rate limiter or a filter it costs nothing in responsiveness. The trade is that the limit is never actually reached, only approached.

flowchart LR
    i1(["input — signal to limit"]) --> B["SoftLimiter"]
    i2(["disable"]) --> B
    B --> o1(["output — softly limited signal"])
    B --> o2(["isLimiting — per channel"])
    B --> o3(["isEnabled"])

Between lowerBreakPoint and upperBreakPoint the output is the input, exactly. Beyond a break point it curves toward the matching limit and gets there only at infinity. The fade distance — limit minus break point — is the whole tuning: a wide gap gives a long gentle curve, a narrow one a sharp bend. Set the break point equal to the limit and you get a hard clamp.

This limit is not a guarantee. If the bound must be respected exactly, put a Limiter after this one.

Disabling this block freezes its output rather than passing the input through — see Limits and errors. Unlike most limiters here, it ships enabled.

Signals

Inputs

Path Unit Range Description
input signal unit unbounded The signal to limit. One element per channel; the channel count is fixed by the machine configuration.
disable - - True stops the block updating. It does not bypass it — output freezes at its last value. Use enable for the configured intent.

Outputs

Path Unit Description
output signal unit The softly limited signal, one element per channel. Equals input exactly between the break points. Frozen at its last value while the block is disabled.
isLimiting - True per channel while the input is beyond either break point, so the taper is active. It also freezes while disabled.
isEnabled - True when enable is true and disable is false. A single value for the whole block.

Parameters

Path Unit Default Range Effect
enable - true - Ships on. False freezes the output rather than bypassing.
lowerLimit signal unit −1.0 below lowerBreakPoint The lower asymptote. The output approaches it and never reaches it. Swapped with upperLimit if the two are inverted.
lowerBreakPoint signal unit −0.9 between lowerLimit and upperBreakPoint Where the lower taper begins. Corrected up to lowerLimit if set below it.
upperBreakPoint signal unit +0.9 between lowerBreakPoint and upperLimit Where the upper taper begins. Corrected down to upperLimit if set above it.
upperLimit signal unit +1.0 above upperBreakPoint The upper asymptote.

All five are persistent and survive a restart. Every limit and break point is per channel. No parameters exist below this block.

Setup

  1. Set upperLimit and lowerLimit to the values the signal must stay inside. Remember the output only approaches them.

  2. Set the two break points inside those limits. The gap between a break point and its limit is the fade distance — start with about 10% of the range, as the defaults do.

  3. Confirm output equals input exactly while the signal stays between the break points, and that isLimiting reads false.

    Step 3 is the check that matters. If the output differs from the input in the middle of the range, a break point is inside your working range and the block is tapering signals you meant to pass untouched.

  4. Drive the signal past a break point and confirm output curves over and isLimiting goes true.

  5. Drive it far past the limit and confirm output gets close to, but never equals, the limit.

  6. Trace every element. All four bounds are per channel.

Tuning

  1. Choose the limits from the consumer, as you would for a hard limiter.
  2. Choose the break points from how much of the range you are willing to distort. Everything between them is untouched; everything outside is compressed.
  3. Widen the fade distance if the bend is harsh, and narrow it if too much of your working range is being compressed. Those two pull against each other, and the gap between them is the whole tuning.
  4. If the machine needs the bound respected exactly, follow this block with a hard limiter set to the same value. The soft taper does the shaping and the hard clamp does the guaranteeing.
  5. To turn a channel into a hard clamp, set its break point equal to its limit. The block handles that case explicitly.
  6. Nothing here needs re-checking after a task-rate change.

Output against input with a limit of 1.0 at three break points: a break pointof 0.9 gives a short taper, 0.5 gives a long gentle curve, and 1.0 gives a hardclip with no taper.

Read the fade distance off the gap between where a curve bends and where it flattens.

Symptom Cause Action
The output never quite reaches the limit Expected: the limit is an asymptote, not a bound Add a hard limiter after this block if the bound must be exact
The output is distorted in the middle of the working range A break point is inside the range you meant to leave alone Move the break points outward
The bend at the break point feels harsh The fade distance is too small Move the break point further from the limit
Too much of the range is compressed The fade distance is too large Move the break point closer to the limit
The output froze and will not follow the input Expected: disabling this block freezes it rather than bypassing Set enable true and disable false
isLimiting is stuck true Same cause — the flag freezes with the output Re-enable the block
A break point reads back different from what was written Expected: break points are corrected inside their own limits Write it inside the limit
The limits read back swapped Expected: inverted limits are corrected Write the lower value to lowerLimit
Only the upper taper seems to work The two break points are crossed, and the upper taper is applied last Keep lowerBreakPoint below upperBreakPoint
A channel clips hard instead of tapering Its break point equals its limit Separate them, or keep it if a hard clamp is what you wanted
Limits were corrected only after the block was enabled Expected: the correction runs only while the block is running Enable it, then read the values back
One channel behaves differently from the others Expected: all four bounds are per channel Check every element of all four arrays
A non-numeric value passed straight through untapered Expected: the comparisons that trigger the taper fail for it Fix the source; the block holds no state and recovers at once

A starting point for shaping a command that must stay inside ±10, tapering over the last 20% of the range:

enable          = true
lowerLimit      = -10.0
lowerBreakPoint = -8.0
upperBreakPoint = 8.0
upperLimit      = 10.0

Limits and errors

Limit Set by What happens Reported
output against the limits The taper Approached, never reached. This block cannot guarantee a bound — follow it with a hard limiter if you need one isLimiting, per channel
lowerLimit below upperLimit Fixed Inverted limits are swapped in place Silently; read both back
Break points inside their limits Fixed A break point outside its own limit is corrected onto it. The two break points are not ordered against each other — if they cross, only the upper taper applies Silently; read them back
Parameter correction While enabled only The corrections above run only while the block is enabled Not reported
Disabled behaviour Fixed The output freezes at its last value rather than passing the input through, unlike every other limiter here Not reported
Fade distance zero Fixed A break point equal to its limit gives a hard clamp at that value Not reported
Limit values Nothing Not checked beyond the ordering above Not reported
Non-numeric input Nothing Passes through untapered. The block is stateless and recovers immediately Not reported
Channel count Machine configuration Fixed once the controller starts 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).