Skip to content

Commit 6f73c65

Browse files
committed
fix(TrackingController): Introducing NaN prevention
With faulty inputs, an anti windup controller can accumulate values that reach beyond min/max of a float64, using the inf values in addition or subtraction then creates a NaN. To prevent the controller to give NaN as ControlSignal output we prevent the ControlError, ControlErrorIntegrand, ControlErrorIntegral and ControlErrorDerivative from reaching inf by clamping it to +/- MaxFloat64. This gives a more expected behavior and let the user have the possibility to handle the state as the user prefers.
1 parent ac48a68 commit 6f73c65

File tree

2 files changed

+61
-3
lines changed

2 files changed

+61
-3
lines changed

trackingcontroller.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import (
1212
// Chapter 6 of Åström and Murray, Feedback Systems:
1313
// An Introduction to Scientists and Engineers, 2008
1414
// (http://www.cds.caltech.edu/~murray/amwiki)
15+
//
16+
// The ControlError, ControlErrorIntegrand, ControlErrorIntegral and ControlErrorDerivative are prevented
17+
// from reaching +/- inf by clamping them to [-math.MaxFloat64, math.MaxFloat64].
1518
type TrackingController struct {
1619
// Config for the TrackingController.
1720
Config TrackingControllerConfig
@@ -86,9 +89,10 @@ func (c *TrackingController) Update(input TrackingControllerInput) {
8689
c.State.ControlSignal = math.Max(c.Config.MinOutput, math.Min(c.Config.MaxOutput, c.State.UnsaturatedControlSignal))
8790
c.State.ControlErrorIntegrand = e + c.Config.AntiWindUpGain*(input.AppliedControlSignal-
8891
c.State.UnsaturatedControlSignal)
89-
c.State.ControlErrorIntegral = controlErrorIntegral
90-
c.State.ControlErrorDerivative = controlErrorDerivative
91-
c.State.ControlError = e
92+
c.State.ControlErrorIntegrand = math.Max(-math.MaxFloat64, math.Min(math.MaxFloat64, c.State.ControlErrorIntegrand))
93+
c.State.ControlErrorIntegral = math.Max(-math.MaxFloat64, math.Min(math.MaxFloat64, controlErrorIntegral))
94+
c.State.ControlErrorDerivative = math.Max(-math.MaxFloat64, math.Min(math.MaxFloat64, controlErrorDerivative))
95+
c.State.ControlError = math.Max(-math.MaxFloat64, math.Min(math.MaxFloat64, e))
9296
}
9397

9498
// DischargeIntegral provides the ability to discharge the controller integral state

trackingcontroller_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package pid
22

33
import (
4+
"math"
45
"testing"
56
"time"
67

@@ -78,6 +79,59 @@ func TestTrackingController_PControllerUpdate(t *testing.T) {
7879
}
7980
}
8081

82+
func TestTrackingController_NaN(t *testing.T) {
83+
// Given a saturated I controller with a low AntiWindUpGain
84+
c := &TrackingController{
85+
Config: TrackingControllerConfig{
86+
LowPassTimeConstant: 1 * time.Second,
87+
IntegralGain: 10,
88+
IntegralDischargeTimeConstant: 10,
89+
MinOutput: -10,
90+
MaxOutput: 10,
91+
AntiWindUpGain: 0.01,
92+
},
93+
}
94+
for _, tt := range []struct {
95+
input TrackingControllerInput
96+
expectedState TrackingControllerState
97+
}{
98+
{
99+
// Negative faulty measurement
100+
input: TrackingControllerInput{
101+
ReferenceSignal: 5.0,
102+
ActualSignal: -math.MaxFloat64,
103+
FeedForwardSignal: 2.0,
104+
SamplingInterval: dtTest,
105+
},
106+
},
107+
{
108+
// Positive faulty measurement
109+
input: TrackingControllerInput{
110+
ReferenceSignal: 5.0,
111+
ActualSignal: math.MaxFloat64,
112+
FeedForwardSignal: 2.0,
113+
SamplingInterval: dtTest,
114+
},
115+
},
116+
} {
117+
tt := tt
118+
// When enough iterations have passed
119+
c.Reset()
120+
for i := 0; i < 220; i++ {
121+
c.Update(TrackingControllerInput{
122+
ReferenceSignal: tt.input.ReferenceSignal,
123+
ActualSignal: tt.input.ActualSignal,
124+
FeedForwardSignal: tt.input.FeedForwardSignal,
125+
SamplingInterval: tt.input.SamplingInterval,
126+
})
127+
}
128+
// Then
129+
assert.Assert(t, !math.IsNaN(c.State.UnsaturatedControlSignal))
130+
assert.Assert(t, !math.IsNaN(c.State.ControlSignal))
131+
assert.Assert(t, !math.IsNaN(c.State.ControlErrorIntegral))
132+
}
133+
}
134+
81135
func TestTrackingController_Reset(t *testing.T) {
82136
// Given a SaturatedPIDController with stored values not equal to 0
83137
c := &TrackingController{}

0 commit comments

Comments
 (0)