Skip to content

Commit fc4d550

Browse files
feat: add bang bang controller (#167)
* add bang bang controller * remove old files from roadmap * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * add missing include --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent ade498b commit fc4d550

12 files changed

Lines changed: 282 additions & 168 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Refer to the documentation to quickly integrate and utilize the library's signal
1818
|--------------------------------------------------------------------|----------------------------------------------------------------------|
1919
| [Analysis](doc/analysis/README.md) | FFT, Power Spectral Density, DCT, Window Functions, Signal Detectors |
2020
| [Control Analysis](doc/control_analysis/README.md) | Frequency Response, Root Locus |
21-
| [Controllers](doc/controllers/README.md) | PID, LQR, MPC, Saturation, Rate Limiter, Slew-Limited Saturation |
21+
| [Controllers](doc/controllers/README.md) | Bang-Bang/Hysteresis, PID, LQR, MPC, Saturation, Rate Limiter, Slew-Limited Saturation |
2222
| [Dynamics](doc/dynamics/README.md) | Euler-Lagrange, Newton-Euler, Recursive Newton-Euler, ABA |
2323
| [Estimators](doc/estimators/README.md) | Linear Regression, Yule-Walker (offline), Recursive Least Squares (online) |
2424
| [Filters](doc/filters/README.md) | Kalman, Extended Kalman, Unscented Kalman, FIR, IIR, Exponential Moving Average, Moving Average |

ROADMAP.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ Difficulty legend:
2929
| # | Component | Target module | Difficulty |
3030
|----|------------------------------------------------------|---------------------------|------------|
3131
| 1 | Exponential Moving Average (one-pole) | `filters/passive` | ★☆☆☆☆ |
32-
| 4 | Bang-bang / hysteresis (relay) controller | `controllers` | ★☆☆☆☆ |
3332
| 5 | Peak / zero-crossing / RMS-envelope detectors | `analysis` | ★☆☆☆☆ |
3433
| 6 | Median filter | `filters/passive` | ★★☆☆☆ |
3534
| 7 | Feedforward / 2-DOF controller | `controllers` | ★★☆☆☆ |
@@ -146,12 +145,6 @@ Difficulty legend:
146145
- **Algorithm / paper:** K. J. Åström, R. M. Murray, *Feedback Systems* (2008), actuator saturation & windup.
147146
- **Reuses:** Scalar/`Vector` templates.
148147

149-
### 4. Bang-bang / hysteresis (relay) controller
150-
- **What:** Two-state relay output with a Schmitt-trigger dead-band to prevent chatter.
151-
- **Embedded value:** The standard control law for thermostats, level control, and power-stage on/off regulation.
152-
- **Algorithm / paper:** Åström & Murray, *Feedback Systems*, relay feedback; Ya. Z. Tsypkin, *Relay Control Systems* (1984).
153-
- **Reuses:** `controllers` interfaces.
154-
155148
### 5. Peak / zero-crossing / RMS-envelope detectors
156149
- **What:** Lightweight feature extractors: rising/falling peak hold, sign-change (zero-crossing) counter, and RMS envelope via one-pole on ``.
157150
- **Embedded value:** Cheap building blocks for frequency estimation, VU metering, activity detection, and event triggers.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Bang-Bang / Hysteresis (Relay) Controller
2+
3+
## Overview & Motivation
4+
5+
The bang-bang controller is the simplest closed-loop regulator: its output switches between two discrete levels depending on whether the controlled variable is above or below a threshold. Without hysteresis, a plain threshold comparator chatters — switching at high frequency whenever noise nudges the signal across the boundary.
6+
7+
Adding a **dead-band (Schmitt trigger)** eliminates chatter by giving the relay memory: once the output goes High it stays High until the measurement falls all the way to a *lower* threshold, and vice versa. This makes the relay a practical actuator-safe control primitive for thermostats, fridge compressors, tank level switches, and power-stage on/off regulation.
8+
9+
## Mathematical Theory
10+
11+
### State Transition
12+
13+
The relay holds a binary state $s \in \{\text{Low}, \text{High}\}$ with the following transition rules:
14+
15+
$$
16+
s[k] = \begin{cases}
17+
\text{High} & \text{if } s[k-1] = \text{Low} \text{ and } x[k] \geq \theta_H \\
18+
\text{Low} & \text{if } s[k-1] = \text{High} \text{ and } x[k] \leq \theta_L \\
19+
s[k-1] & \text{otherwise}
20+
\end{cases}
21+
$$
22+
23+
where $\theta_L < \theta_H$ are the lower and upper switching thresholds (the hysteresis band).
24+
25+
### Output Map
26+
27+
$$
28+
u[k] = \begin{cases}
29+
u_H & \text{if } s[k] = \text{High} \\
30+
u_L & \text{if } s[k] = \text{Low}
31+
\end{cases}
32+
$$
33+
34+
The output levels $u_L$ and $u_H$ are arbitrary; common choices are $\{0, 1\}$ or $\{-1, +1\}$.
35+
36+
### Hysteresis Band Width
37+
38+
The band width $\Delta = \theta_H - \theta_L$ is the key design parameter. It bounds the switching frequency $f_s$ given a signal slope $\dot{x}$:
39+
40+
$$
41+
f_s \leq \frac{|\dot{x}|}{2\Delta}
42+
$$
43+
44+
A wider band reduces $f_s$ (protecting relays and power stages) at the cost of a larger steady-state limit cycle amplitude.
45+
46+
## Complexity Analysis
47+
48+
| Case | Time | Space | Notes |
49+
|------|--------|--------|--------------------------------------------|
50+
| All | $O(1)$ | $O(1)$ | Two comparisons, one state bit, one select |
51+
52+
No arithmetic on the signal path — only comparisons — so the relay introduces no numerical error and is exactly representable in any floating-point format.
53+
54+
## Step-by-Step Walkthrough
55+
56+
**Setup:** band $[\theta_L, \theta_H] = [-0.2, 0.2]$, outputs $u_L = 0$, $u_H = 1$, initial state Low.
57+
58+
| Step | $x[k]$ | Condition | $s[k]$ | $u[k]$ |
59+
|------|--------|------------------------------|--------|--------|
60+
| 1 | 0.0 | Low, $x < 0.2$ | Low | 0 |
61+
| 2 | 0.3 | Low, $x \geq 0.2$ → switch | High | 1 |
62+
| 3 | 0.1 | High, $x > -0.2$ → hold | High | 1 |
63+
| 4 | −0.3 | High, $x \leq -0.2$ → switch | Low | 0 |
64+
| 5 | 0.0 | Low, $x < 0.2$ → hold | Low | 0 |
65+
66+
## Pitfalls & Edge Cases
67+
68+
- **Inverted band.** $\theta_H \leq \theta_L$ latches the output in an undefined state; reject this at construction time via a precondition assertion.
69+
- **Exactly on threshold.** Transitions are inclusive: $x = \theta_H$ triggers Low→High and $x = \theta_L$ triggers High→Low. This avoids a dead-zone at the switching points.
70+
- **Zero-width band.** $\theta_H = \theta_L$ collapses the relay to a pure comparator (no hysteresis). The logic is still correct but offers no chatter suppression.
71+
- **Noise sizing.** The band width must exceed the peak-to-peak noise amplitude; otherwise noise alone drives state transitions at the sampling rate.
72+
- **Actuator minimum on-time.** Size $\Delta$ so that the minimum on-time (derived from $\Delta / |\dot{x}|_\text{max}$) is above the actuator's rated minimum switching period.
73+
74+
## Variants & Generalizations
75+
76+
| Variant | Key Difference |
77+
|-------------------------------------------|-----------------------------------------------------------------------------------------------------------|
78+
| **Plain comparator** | $\Delta = 0$; no memory, chatters on noise |
79+
| **Asymmetric band** | $\theta_H$ and $\theta_L$ not symmetric around the set-point; biases the duty cycle |
80+
| **Three-state relay** | Adds a dead-band output level $u_0$; used in motor direction control |
81+
| **Adaptive hysteresis** | Band width tracks signal variance online to maintain a target switching rate |
82+
| **Relay feedback test (Åström–Hägglund)** | Deliberate oscillation under relay feedback to identify the ultimate gain/period for automatic PID tuning |
83+
84+
## Applications
85+
86+
- **Thermostats and HVAC** — heating/cooling switched on/off around a temperature set-point.
87+
- **Tank and vessel level control** — pump on/off between high- and low-level floats.
88+
- **Power-stage converters** — hysteretic current-mode control in DC-DC converters and class-D amplifiers.
89+
- **Motor drive enable/disable** — protecting power stages with a current-band relay.
90+
- **Åström–Hägglund auto-tuning** — the relay feedback experiment that drives limit-cycle oscillation for PID parameter identification.
91+
92+
## Connections to Other Algorithms
93+
94+
| Algorithm | Relationship |
95+
|-------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
96+
| [PID Controller](Pid.md) | The relay's limit cycle can be used to identify PID tuning parameters via the Åström–Hägglund relay-feedback test |
97+
| [Saturation / Rate Limiter](SaturationRateLimiter.md) | Continuous-output counterpart for actuator constraint; often combined with a relay in cascaded loops |
98+
99+
## References & Further Reading
100+
101+
- K. J. Åström, R. M. Murray, *Feedback Systems: An Introduction for Scientists and Engineers*, Princeton University Press, 2008 — relay feedback, Chapter 10.
102+
- Ya. Z. Tsypkin, *Relay Control Systems*, Cambridge University Press, 1984.
103+
- K. J. Åström, T. Hägglund, "Automatic Tuning of Simple Regulators with Specifications on Phase and Amplitude Margins," *Automatica*, 20(5), 1984.

doc/controllers/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Feedback control algorithms for regulating dynamic systems in real time.
66

77
| Algorithm | Description |
88
|--------------------------|-----------------------------------------------------------------------------------------|
9+
| [Bang-Bang / Hysteresis Controller](BangBangHysteresis.md) | Two-state relay controller with Schmitt-trigger dead-band to prevent chatter |
910
| [PID Controller](Pid.md) | Proportional-Integral-Derivative controller using a discrete recursive formulation |
1011
| [LQR Controller](Lqr.md) | Linear Quadratic Regulator — optimal state-feedback control minimizing a quadratic cost |
1112
| [LQG Controller](Lqg.md) | Linear Quadratic Gaussian — output-feedback optimal control via LQR + Kalman Filter |
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#include "numerical/controllers/implementations/BangBangHysteresis.hpp"
2+
3+
namespace controllers
4+
{
5+
template class BangBangHysteresis<float>;
6+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#pragma once
2+
3+
#if defined(__GNUC__) || defined(__clang__)
4+
#pragma GCC optimize("O3", "fast-math")
5+
#endif
6+
7+
#include "infra/util/ReallyAssert.hpp"
8+
#include "numerical/math/CompilerOptimizations.hpp"
9+
#include <type_traits>
10+
11+
namespace controllers
12+
{
13+
enum class RelayState
14+
{
15+
Low,
16+
High
17+
};
18+
19+
template<typename T>
20+
class BangBangHysteresis
21+
{
22+
public:
23+
static_assert(std::is_floating_point_v<T>, "BangBangHysteresis supports floating-point types");
24+
25+
BangBangHysteresis(T lowThreshold, T highThreshold, T outputLow, T outputHigh);
26+
27+
OPTIMIZE_FOR_SPEED T Update(T measurement);
28+
void Reset(RelayState initial = RelayState::Low);
29+
RelayState State() const;
30+
31+
private:
32+
T lowThreshold;
33+
T highThreshold;
34+
T outputLow;
35+
T outputHigh;
36+
RelayState state{ RelayState::Low };
37+
};
38+
39+
template<typename T>
40+
BangBangHysteresis<T>::BangBangHysteresis(T lowThreshold, T highThreshold, T outputLow, T outputHigh)
41+
: lowThreshold{ lowThreshold }
42+
, highThreshold{ highThreshold }
43+
, outputLow{ outputLow }
44+
, outputHigh{ outputHigh }
45+
{
46+
really_assert(highThreshold > lowThreshold);
47+
}
48+
49+
template<typename T>
50+
OPTIMIZE_FOR_SPEED T BangBangHysteresis<T>::Update(T measurement)
51+
{
52+
if (state == RelayState::Low && measurement >= highThreshold)
53+
state = RelayState::High;
54+
else if (state == RelayState::High && measurement <= lowThreshold)
55+
state = RelayState::Low;
56+
57+
return (state == RelayState::High) ? outputHigh : outputLow;
58+
}
59+
60+
template<typename T>
61+
void BangBangHysteresis<T>::Reset(RelayState initial)
62+
{
63+
state = initial;
64+
}
65+
66+
template<typename T>
67+
RelayState BangBangHysteresis<T>::State() const
68+
{
69+
return state;
70+
}
71+
72+
#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD
73+
extern template class BangBangHysteresis<float>;
74+
#endif
75+
}

numerical/controllers/implementations/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ target_link_libraries(numerical.controllers.implementations ${NUMERICAL_VISIBILI
1212
)
1313

1414
target_sources(numerical.controllers.implementations PRIVATE
15+
BangBangHysteresis.hpp
1516
Lqg.hpp
1617
Lqr.hpp
1718
Mpc.hpp
@@ -20,6 +21,7 @@ target_sources(numerical.controllers.implementations PRIVATE
2021
)
2122

2223
numerical_add_coverage_sources(numerical.controllers.implementations
24+
BangBangHysteresis.cpp
2325
Lqg.cpp
2426
Lqr.cpp
2527
Mpc.cpp

numerical/controllers/implementations/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ target_link_libraries(numerical.controllers_test PUBLIC
99
)
1010

1111
target_sources(numerical.controllers_test PRIVATE
12+
TestBangBangHysteresis.cpp
1213
TestLqg.cpp
1314
TestLqr.cpp
1415
TestMpc.cpp
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#include "numerical/controllers/implementations/BangBangHysteresis.hpp"
2+
#include "gtest/gtest.h"
3+
#include <array>
4+
5+
namespace
6+
{
7+
class TestBangBangHysteresis
8+
: public ::testing::Test
9+
{
10+
public:
11+
controllers::BangBangHysteresis<float> relay{ -0.2f, 0.2f, 0.0f, 1.0f };
12+
};
13+
}
14+
15+
TEST_F(TestBangBangHysteresis, starts_in_low_state)
16+
{
17+
EXPECT_EQ(relay.State(), controllers::RelayState::Low);
18+
EXPECT_FLOAT_EQ(relay.Update(0.0f), 0.0f);
19+
}
20+
21+
TEST_F(TestBangBangHysteresis, switches_high_at_upper_threshold)
22+
{
23+
float output = relay.Update(0.2f);
24+
25+
EXPECT_EQ(relay.State(), controllers::RelayState::High);
26+
EXPECT_FLOAT_EQ(output, 1.0f);
27+
}
28+
29+
TEST_F(TestBangBangHysteresis, stays_high_inside_band)
30+
{
31+
relay.Update(0.2f);
32+
33+
float output = relay.Update(0.0f);
34+
35+
EXPECT_EQ(relay.State(), controllers::RelayState::High);
36+
EXPECT_FLOAT_EQ(output, 1.0f);
37+
}
38+
39+
TEST_F(TestBangBangHysteresis, switches_low_at_lower_threshold)
40+
{
41+
relay.Update(0.2f);
42+
43+
float output = relay.Update(-0.2f);
44+
45+
EXPECT_EQ(relay.State(), controllers::RelayState::Low);
46+
EXPECT_FLOAT_EQ(output, 0.0f);
47+
}
48+
49+
TEST_F(TestBangBangHysteresis, hysteresis_prevents_chatter)
50+
{
51+
std::array<float, 5> inputs{ 0.1f, -0.1f, 0.15f, -0.15f, 0.05f };
52+
53+
for (float x : inputs)
54+
relay.Update(x);
55+
56+
EXPECT_EQ(relay.State(), controllers::RelayState::Low);
57+
}
58+
59+
TEST_F(TestBangBangHysteresis, full_cycle_sequence)
60+
{
61+
std::array<float, 5> inputs{ 0.0f, 0.3f, 0.1f, -0.3f, 0.0f };
62+
std::array<controllers::RelayState, 5> expectedStates{
63+
controllers::RelayState::Low,
64+
controllers::RelayState::High,
65+
controllers::RelayState::High,
66+
controllers::RelayState::Low,
67+
controllers::RelayState::Low
68+
};
69+
70+
for (std::size_t i = 0; i < inputs.size(); ++i)
71+
{
72+
relay.Update(inputs[i]);
73+
EXPECT_EQ(relay.State(), expectedStates[i]);
74+
}
75+
}
76+
77+
TEST_F(TestBangBangHysteresis, reset_restores_initial_state)
78+
{
79+
relay.Update(0.2f);
80+
EXPECT_EQ(relay.State(), controllers::RelayState::High);
81+
82+
relay.Reset(controllers::RelayState::Low);
83+
84+
EXPECT_EQ(relay.State(), controllers::RelayState::Low);
85+
}
86+
87+
TEST_F(TestBangBangHysteresis, custom_output_levels)
88+
{
89+
controllers::BangBangHysteresis<float> customRelay{ -0.2f, 0.2f, -1.0f, 1.0f };
90+
91+
EXPECT_FLOAT_EQ(customRelay.Update(0.0f), -1.0f);
92+
EXPECT_FLOAT_EQ(customRelay.Update(0.2f), 1.0f);
93+
}

roadmap/controllers/BangBangHysteresis/explanation.md

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)