SoftLimiter
SoftLimiter bounds a signal the way a hard limiter does, but rounds the approach instead of cutting it off.7 minute read
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
lowerBreakPointandupperBreakPointthe 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
-
Set
upperLimitandlowerLimitto the values the signal must stay inside. Remember the output only approaches them. -
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.
-
Confirm
outputequalsinputexactly while the signal stays between the break points, and thatisLimitingreads 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.
-
Drive the signal past a break point and confirm
outputcurves over andisLimitinggoes true. -
Drive it far past the limit and confirm
outputgets close to, but never equals, the limit. -
Trace every element. All four bounds are per channel.
Tuning
- Choose the limits from the consumer, as you would for a hard limiter.
- Choose the break points from how much of the range you are willing to distort. Everything between them is untouched; everything outside is compressed.
- 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.
- 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.
- To turn a channel into a hard clamp, set its break point equal to its limit. The block handles that case explicitly.
- Nothing here needs re-checking after a task-rate change.
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).