pctl, "process control" is a package for industrial control in Go.
It contains an implementation of the classic PID controller with integral anti-windup, as well as many filter types that can be used for loop shaping:
- single pole low pass
- single pole high pass
- Biquads
- State-Space filters with an arbitrary number of states
- FIR filters with an arbitrary number of taps
The package declares the top-level Cascade
function, which takes a sequence of
interfaces that are met by all types in the package to facilitate SOS and other
"fluent" designs. Use of Cascade will be somewhat slower or less efficient than
manually writing a chain of function calls due to the virtualization implied by
interfaces.
Its types are not concurrent safe, and use double precision, which is low cost on most software platforms. Tinygo may perform relatively worse, although it should not matter much. The implementations of each type in this repository are relatively optimized, easily able to function at up to MHz on even a raspberry pi.
For Biquads, design methods are included to synthesize common filter types from corner frequencies, etc, in applications where detailed analysis of the transfer functions or plant response are not required.
// Biquad, 1k sample rate, 50Hz corner freq, maximally flat in band
// 6 = gain; unused for LPF; see NewBiquad interface
// or bring your own a0, a1, a2, b1, b2 coefs
inputFilter := pctl.NewBiquadLowPass(1000, 50, math.Sqrt(2), 6)
controller := pctl.PID{P: 1, I: 0.5, Setpt: 50, DT: 1e-3}
for {
input := getInput()
controlCommand := pctl.Cascade(input, inputFilter, controller)
applyControl(controlCommand)
}
// State-space second order lowpass filter,
// 900Hz sample rate, 2Hz corner freq, -6dB/octave
A := [][]float64{
{2, -1},
{1, 0},
}
B := []float64{5e-5, 0}
C := []float64{4, 0.02}
D := 5e-5
setpt := pctl.Setpoint(50)
// FB = feedback
FBFilter := pctl.NewStateSpaceFilter(A, B, C, D, nil)
for {
input := getInput()
controlCommand := pctl.Cascade(input, setpt, FBFilter)
applyControl(controlCommand)
}
The previous examples lack prefilters on the setpoint, so the system can be
destabilized by large setpoint changes. A prefilter can be added that operates
on *setpt
to remedy this.
Opening or closing the control loop independent of measurement is also not possible. The latter can be achieved by simply adding one line:
for {
// ...
if controlLoopClosed {
applyControl(process)
}
}
Manipulating of this variable is outside the scope of pctl. It could be e.g. a struct member, or simply a pointer to a bool that is dereferenced at the if. The "size" of the solution can scale with the "size" of the processor and problem.
See pctl_test.go
for a benchmark suite. The FIR filter in the benchmark has
32 taps.
M1 Pro Boost frequency = 3.2GHz; 1 clock ~=0.3125 ns.
name time/op
PIDLoop-10 3.50ns ± 1%
LPF-10 4.52ns ± 2%
HPF-10 4.49ns ± 2%
Biquad-10 4.89ns ± 1%
StateSpace-10 12.5ns ± 3%
Setpoint-10 0.32ns ± 1%
FIRFilter-10 11.8ns ± 1%
A reasonable average is the Biquad filter, 15.6 clocks.
This CPU boosts to 4.6GHz during the benchmark; 1 clock ~=0.217 ns.
name time/op
PIDLoop-8 1.99ns ± 2%
LPF-8 3.74ns ± 1%
HPF-8 2.80ns ± 1%
Biquad-8 3.65ns ± 1%
StateSpace-8 9.72ns ± 1%
Setpoint-8 0.21ns ± 3%
FIRFilter-8 8.66ns ± 2%
The Biquad filter takes 16.8 clocks. Broadly comparable to the ARM64 M1.
This CPU boosts to 5.3GHz during the benchmark; 1 clock ~= 0.189 ns. cTDP 105w eco mode is enabled.
name time/op
PIDLoop-8 3.444n ± 0%
LPF-8 4.317n ± 0%
HPF-8 4.312n ± 3%
Biquad-8 4.694n ± 3%
StateSpace-8 10.90n ± 1%
Setpoint-8 0.29n ± 1%
FIRFilter-8 10.88n ± 0%
Despite having a considerably higher clockspeed, this CPU takes more time to perform the functions within pctl.
name time/op
PIDLoop-32 2.168n ± 1%
LPF-32 3.190n ± 0%
HPF-32 2.654n ± 0%
Biquad-32 3.191n ± 0%
StateSpace-32 7.301n ± 1%
Setpoint-32 0.1801n ± 1%
FIRFilter-32 6.943n ± 2%
Performance is ~50% higher in Windows subsystem for Linux / Ubuntu.
This benchmark is run virtualized on a Synology NAS, with two vCPUs and 2GB of RAM. The clock speed is 2.2GHz.
name time/op
PIDLoop-32 5.037n ± 1%
LPF-32 8.506n ± 0%
HPF-32 7.068n ± 0%
Biquad-32 8.261n ± 0%
StateSpace-32 24.76n ± 1%
Setpoint-32 0.487n ± 1%
FIRFilter-32 19.00n ± 2%
Several designs have been iterated in this repository. An early design used channels to communicate, which took about 500ns per update. This was less composable than methods/functions.
An intermediate design maintained clocks inside each control element. This was less performant, but more importantly could not be used in a simulation capacity running at any speed other than real time. Explicitly including dT (fielded as DT) in the structs allows these controllers to be used in simulation studies as well. The nearly 10x increase in performance and better friendliness to tinygo platforms are also nice benefits.
The current design has been released as v1 (guaranteed stable) and is unlikely to change for marginal improvements in favor of API stability.
This library is dependency-free outside stdlib/math and easily portable to tiny platforms, even if a float32 type-change would be required (this is as simply as ctrl+F). Future additions shall not disturb that property. LQR/LQG, Kalman filtering, etc, may be implemented here if the the implementations do not require a dependency on e.g. Gonum.