Skip to content

Commit 3c536e0

Browse files
committed
pidctl: add support for float64
1 parent 945389b commit 3c536e0

5 files changed

+199
-6
lines changed

controller.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func (p *Controller) init() {
6767
})
6868
}
6969

70-
// Accumulate updates the controller with the given process value since the last
70+
// Compute updates the controller with the given process value since the last
7171
// update. It returns the new output that should be used by the device to reach
7272
// the desired set point. Internally it assumes the duration between calls is
7373
// constant.

controller_float64.go

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
Copyright 2024 github.com/ucirello and cirello.io
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package pidctl
18+
19+
import (
20+
"math/big"
21+
"time"
22+
)
23+
24+
// ControllerFloat64 implements a PID controller using float64 for inputs and
25+
// outputs.
26+
type ControllerFloat64 struct {
27+
controller Controller
28+
}
29+
30+
// NewControllerFloat64 creates a new PID controller using float64 for inputs
31+
// and outputs.
32+
func NewControllerFloat64(p, i, d, setpoint float64) *ControllerFloat64 {
33+
c := &ControllerFloat64{
34+
controller: Controller{
35+
P: ratFloat64(p),
36+
I: ratFloat64(i),
37+
D: ratFloat64(d),
38+
Setpoint: ratFloat64(setpoint),
39+
},
40+
}
41+
c.controller.init()
42+
return c
43+
}
44+
45+
// SetSetpoint changes the desired setpoint of the controller.
46+
func (c *ControllerFloat64) SetSetpoint(setpoint float64) *ControllerFloat64 {
47+
c.controller.Setpoint = ratFloat64(setpoint)
48+
return c
49+
}
50+
51+
// SetMin changes the minimum output value of the controller.
52+
func (c *ControllerFloat64) SetMin(min float64) *ControllerFloat64 {
53+
c.controller.Min = ratFloat64(min)
54+
return c
55+
}
56+
57+
// SetMax changes the maxium output value of the controller.
58+
func (c *ControllerFloat64) SetMax(max float64) *ControllerFloat64 {
59+
c.controller.Max = ratFloat64(max)
60+
return c
61+
}
62+
63+
// Compute updates the controller with the given process value since the last
64+
// update. It returns the new output that should be used by the device to reach
65+
// the desired set point. Internally it assumes the duration between calls is
66+
// constant.
67+
func (c *ControllerFloat64) Compute(pv float64) float64 {
68+
v, _ := c.controller.Compute(ratFloat64(pv)).Float64()
69+
return v
70+
}
71+
72+
// Accumulate updates the controller with the given process value and duration
73+
// since the last update. It returns the new output that should be used by the
74+
// device to reach the desired set point.
75+
func (c *ControllerFloat64) Accumulate(pv float64, deltaTime time.Duration) float64 {
76+
v, _ := c.controller.Accumulate(ratFloat64(pv), deltaTime).Float64()
77+
return v
78+
}
79+
80+
func ratFloat64(i float64) *big.Rat {
81+
return new(big.Rat).SetFloat64(i)
82+
}

controller_float64_test.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
Copyright 2024 github.com/ucirello and cirello.io
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package pidctl
18+
19+
import (
20+
"bufio"
21+
"bytes"
22+
"fmt"
23+
"testing"
24+
"text/tabwriter"
25+
)
26+
27+
func TestControllerFloat64(t *testing.T) {
28+
for _, test := range tests {
29+
t.Run(test.name, func(t *testing.T) {
30+
defer func() {
31+
r := recover()
32+
switch {
33+
case r == nil:
34+
case test.expectPanic && fmt.Sprintf("%T", r) == "*pidctl.MinMaxError":
35+
t.Log("trapped:", r)
36+
default:
37+
panic(r)
38+
}
39+
}()
40+
c := NewControllerFloat64(test.p, test.i, test.d, 0)
41+
42+
if test.min != 0 || test.max != 0 {
43+
c.SetMin(test.min)
44+
c.SetMax(test.max)
45+
}
46+
47+
var buf bytes.Buffer
48+
log := tabwriter.NewWriter(&buf, 8, 0, 1, ' ', 0)
49+
fmt.Fprint(log, "\tcycle\tgot\texpected\tsetpoint\tinput\toutput\n")
50+
for i, u := range test.steps {
51+
if u.setpoint != 0 {
52+
c.SetSetpoint(u.setpoint)
53+
}
54+
got := c.Accumulate(u.input, test.stepDuration)
55+
roundedGot, roundedExpected := fmt.Sprintf("%0.3f", got), fmt.Sprintf("%0.3f", u.output)
56+
msg := ""
57+
if roundedGot != roundedExpected {
58+
msg = "error"
59+
t.Fail()
60+
}
61+
fmt.Fprintf(log, "%s\t%d\t%v\t%v\t%v\t%v\t%v\n", msg, i, roundedGot, roundedExpected, u.setpoint, u.input, u.output)
62+
}
63+
log.Flush()
64+
scanner := bufio.NewScanner(&buf)
65+
for scanner.Scan() {
66+
t.Log(scanner.Text())
67+
}
68+
if err := scanner.Err(); err != nil {
69+
panic(fmt.Sprint("reading table output:", err))
70+
}
71+
})
72+
}
73+
}

controller_test.go

-5
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"bufio"
2121
"bytes"
2222
"fmt"
23-
"math/big"
2423
"testing"
2524
"text/tabwriter"
2625
"time"
@@ -221,7 +220,3 @@ func TestController(t *testing.T) {
221220
})
222221
}
223222
}
224-
225-
func ratFloat64(i float64) *big.Rat {
226-
return new(big.Rat).SetFloat64(i)
227-
}

example_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,46 @@ func Example() {
7171
// 14s speed: 58.00 throttle: 4.00 (desired: 3.15)
7272
// 15s speed: 60.00 throttle: 3.00 (desired: 2.75)
7373
}
74+
75+
// A car whose PID controller is going to act to try to stabilize its
76+
// speed to the given setpoint (float64 version).
77+
func Example_float64() {
78+
car := pidctl.NewControllerFloat64(
79+
1.0/5, 1.0/100, 1.0/15,
80+
60). // target speed: 60 mph
81+
SetMin(-0.5). // min acceleration rate: 0.5 mps
82+
SetMax(5) // max acceleration rate: 5 mps
83+
speed := float64(20) // the car starts in motion. 20mph
84+
const travel = 15 * time.Second
85+
for i := time.Second; i <= travel; i += time.Second {
86+
desiredThrottle := car.Compute(speed)
87+
actualThrottle := math.Ceil(desiredThrottle)
88+
fmt.Printf("%s speed: %.2f throttle: %.2f (desired: %.2f)\n", i, speed, actualThrottle, desiredThrottle)
89+
speed += actualThrottle
90+
switch i % 5 {
91+
case 0:
92+
// head wind starts strong: 2mps
93+
speed -= 2
94+
case 1:
95+
// head wind ends weak: 1mps
96+
speed -= 1
97+
}
98+
}
99+
100+
// Output:
101+
// 1s speed: 20.00 throttle: 5.00 (desired: 5.00)
102+
// 2s speed: 23.00 throttle: 5.00 (desired: 5.00)
103+
// 3s speed: 26.00 throttle: 5.00 (desired: 5.00)
104+
// 4s speed: 29.00 throttle: 5.00 (desired: 5.00)
105+
// 5s speed: 32.00 throttle: 5.00 (desired: 5.00)
106+
// 6s speed: 35.00 throttle: 5.00 (desired: 5.00)
107+
// 7s speed: 38.00 throttle: 5.00 (desired: 5.00)
108+
// 8s speed: 41.00 throttle: 5.00 (desired: 5.00)
109+
// 9s speed: 44.00 throttle: 5.00 (desired: 5.00)
110+
// 10s speed: 47.00 throttle: 5.00 (desired: 5.00)
111+
// 11s speed: 50.00 throttle: 5.00 (desired: 4.55)
112+
// 12s speed: 53.00 throttle: 5.00 (desired: 4.02)
113+
// 13s speed: 56.00 throttle: 4.00 (desired: 3.46)
114+
// 14s speed: 58.00 throttle: 4.00 (desired: 3.15)
115+
// 15s speed: 60.00 throttle: 3.00 (desired: 2.75)
116+
}

0 commit comments

Comments
 (0)