Control
Control: feedback loops that keep or drive a system towards a goal, and the controllers that close them.
CartPendulumElement(label, visual=None, *, x=0.0, x_d1=0.0, theta=0.0, theta_d1=0.0, force=0.0, M=1.0, m=0.1, l=0.5, g=GRAVITY)
Bases: Element
Inverted pendulum on a cart that a bounded horizontal force pushes, after Åström and Murray.
The cart of mass M at position x carries a point mass m at the end of a rod of length l, at the angle
theta from upright, positive towards negative x. The equations of motion are
(M + m) x'' - m l theta'' cos(theta) + m l theta'^2 sin(theta) = force and
l theta'' - x'' cos(theta) = g sin(theta), so upright is unstable and a positive force tilts the pendulum back
from a positive angle.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str
|
The label of the cart. |
required |
visual
|
VisualSpec | None
|
How the element is drawn, or |
None
|
x
|
Annotated[float, StateSpec(order=2)]
|
The initial position of the cart. |
0.0
|
x_d1
|
float
|
The initial velocity of the cart. |
0.0
|
theta
|
Annotated[float, StateSpec(order=2)]
|
The initial angle from upright. |
0.0
|
theta_d1
|
float
|
The initial angular velocity. |
0.0
|
force
|
Annotated[float, ParameterSpec(adaptive=True, bounds=(-FORCE_LIMIT, FORCE_LIMIT))]
|
The horizontal force on the cart, the control input. |
0.0
|
M
|
Annotated[float, ParameterSpec(domain=open(0, oo))]
|
The mass of the cart. |
1.0
|
m
|
Annotated[float, ParameterSpec(domain=open(0, oo))]
|
The mass at the end of the rod. |
0.1
|
l
|
Annotated[float, ParameterSpec(domain=open(0, oo))]
|
The length of the rod. |
0.5
|
g
|
Annotated[float, ParameterSpec(domain=open(0, oo))]
|
The acceleration of gravity. |
GRAVITY
|
Examples:
>>> from skcomplex.models.engineering.control import CartPendulumElement
>>> CartPendulumElement('cart', theta=0.1).theta
0.1
Source code in src/skcomplex/models/engineering/control/_cart_pendulum.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | |
DrivenOscillatorElement(label, visual=None, *, x=0.0, x_d1=0.0, force=0.0, omega=1.0, zeta=0.0)
Bases: Element
Damped harmonic oscillator that a bounded force drives, x'' = -omega^2 x - 2 zeta omega x' + force.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str
|
The label of the oscillator. |
required |
visual
|
VisualSpec | None
|
How the element is drawn, or |
None
|
x
|
Annotated[float, StateSpec(order=2)]
|
The initial displacement. |
0.0
|
x_d1
|
float
|
The initial velocity. |
0.0
|
force
|
Annotated[float, ParameterSpec(adaptive=True, bounds=(-FORCE_LIMIT, FORCE_LIMIT))]
|
The force per unit mass, the control input. |
0.0
|
omega
|
Annotated[float, ParameterSpec(domain=open(0, oo))]
|
The natural frequency. |
1.0
|
zeta
|
Annotated[float, ParameterSpec(domain=closed_open(0, oo))]
|
The damping ratio. |
0.0
|
Examples:
>>> from skcomplex.models.engineering.control import DrivenOscillatorElement
>>> DrivenOscillatorElement('oscillator', x=1.0).omega
1.0
Source code in src/skcomplex/models/engineering/control/_driven_oscillator.py
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | |
IntegratorElement(label, visual=None, *, value=0.0)
Bases: Element
Element that holds the integral of a signal, whose rate a controller sets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str
|
The label of the integrator. |
required |
visual
|
VisualSpec | None
|
How the element is drawn, or |
None
|
value
|
Annotated[float, StateSpec()]
|
The initial value of the integral. |
0.0
|
Examples:
>>> from skcomplex.models.engineering.control import IntegratorElement
>>> IntegratorElement('memory').value
0.0
Source code in src/skcomplex/models/engineering/control/_integrator.py
31 32 33 34 35 36 37 38 39 | |
PIDInteraction(label, *, plant, integrator, kp=1.0, ki=0.0, kd=0.0, setpoint=0.0, control='u', measure='y', limits=None)
Bases: Interaction
Proportional-integral-derivative controller that sets a control input of a plant from the error of a measure.
The error is e = setpoint - y for the measured state y, and the controller sets the control input to
u = kp e + ki I - kd y', clipped to the limits, with the derivative taken on the measure. The integral
I' = e of the error lives in an IntegratorElement bound to the role integrator. The control input is an
adaptive parameter of the plant, which the controller sets after every solver step, so it holds within a step like a
sampled controller. The derivative term reads the first derivative of the measure, so a measure with a derivative
term is a state of order 2.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str
|
The label of the controller. |
required |
plant
|
Annotated[str, RoleSpec()]
|
The path of the element that holds the measure and the control input. |
required |
integrator
|
Annotated[str, RoleSpec()]
|
The path of the integrator of the error. |
required |
kp
|
Annotated[float, ParameterSpec()]
|
The proportional gain. |
1.0
|
ki
|
Annotated[float, ParameterSpec()]
|
The integral gain. |
0.0
|
kd
|
Annotated[float, ParameterSpec()]
|
The derivative gain. |
0.0
|
setpoint
|
Annotated[float, ParameterSpec()]
|
The value the measure is kept at. |
0.0
|
control
|
str
|
The name of the adaptive parameter of the plant that the controller sets. |
'u'
|
measure
|
str
|
The name of the measured state of the plant. |
'y'
|
limits
|
tuple[float, float] | None
|
The lower and upper limit of the control input, or |
None
|
Examples:
>>> from skcomplex.models.engineering.control import PIDInteraction
>>> PIDInteraction('pid', plant='cart', integrator='memory', control='force', measure='theta', kp=40.0).kp
40.0
Source code in src/skcomplex/models/engineering/control/_pid.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
StateFeedbackInteraction(label, *, plant, control='u', states=(), gain=(), limits=None)
Bases: Interaction
Linear state feedback u = -K x that sets a control input of a plant from its states.
The control input is an adaptive parameter of the plant, which the controller sets after every solver step, so it
holds within a step like a sampled controller. A gain from LinearizationAnalysis of the plant makes the
controller a linear-quadratic regulator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str
|
The label of the controller. |
required |
plant
|
Annotated[str, RoleSpec()]
|
The path of the element that holds the states and the control input. |
required |
control
|
str
|
The name of the adaptive parameter of the plant that the controller sets. |
'u'
|
states
|
Sequence[str]
|
The names of the fed back states of the plant, such as |
()
|
gain
|
Sequence[float]
|
The gain of every state, in the order of |
()
|
limits
|
tuple[float, float] | None
|
The lower and upper limit of the control input, or |
None
|
Examples:
>>> from skcomplex.models.engineering.control import StateFeedbackInteraction
>>> StateFeedbackInteraction(
... 'regulator', plant='mass', control='force', states=('x', 'x_d1'), gain=(1.0, 1.7)
... ).gain
(1.0, 1.7)
Source code in src/skcomplex/models/engineering/control/_state_feedback.py
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
lqr_oscillator(x=1.0, *, omega=1.0, zeta=0.0, q=1.0, r=0.1)
Return a harmonic oscillator that a linear-quadratic regulator brings to rest.
The gain K of the regulator minimises ∫ (q (x^2 + x'^2) + r force^2) dt for the linearisation of the
oscillator, which LinearizationAnalysis computes from the Riccati equation, and the state feedback sets
force = -K (x, x'). A smaller weight r of the force gives a larger gain and a faster return to rest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
float
|
The initial displacement. |
1.0
|
omega
|
float
|
The natural frequency. |
1.0
|
zeta
|
float
|
The damping ratio without control. |
0.0
|
q
|
float
|
The weight of the displacement and the velocity in the cost. |
1.0
|
r
|
float
|
The weight of the force in the cost. |
0.1
|
Returns:
| Name | Type | Description |
|---|---|---|
system |
System
|
The model, with the displacement |
Examples:
>>> from skcomplex.models.engineering.control import lqr_oscillator
>>> [round(value, 3) for value in lqr_oscillator().interactions[0].gain]
[2.317, 3.825]
Source code in src/skcomplex/models/engineering/control/_regulation.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |
pendulum_swing_up(theta=0.0, omega0=0.5, *, omega=1.5652475842498528, mu=0.1)
Return a pendulum swung up to its inverted position by a bang-bang acceleration of its pivot.
The pivot of an acrobat's pendulum moves horizontally with acceleration f'' = u, so the pendulum follows
θ'' + ω^2 sin θ + u cos θ = 0. The control u = -μ sign(θ' cos θ) sign(E_s - E) pumps the energy
E = θ'^2 / 2 - ω^2 cos θ towards E_s = ω^2, the energy of the inverted position, from below or from above.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
theta
|
float
|
The initial angle from the hanging position. |
0.0
|
omega0
|
float
|
The initial angular velocity. |
0.5
|
omega
|
float
|
The natural frequency |
1.5652475842498528
|
mu
|
float
|
The size of the acceleration of the pivot. |
0.1
|
Returns:
| Name | Type | Description |
|---|---|---|
system |
System
|
The model, with the angle |
Examples:
>>> from skcomplex.models.engineering.control import pendulum_swing_up
>>> pendulum_swing_up(mu=0.2).elements[0].mu
0.2
Source code in src/skcomplex/models/engineering/control/_control.py
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | |
pid_inverted_pendulum(theta=0.1, *, kp=40.0, ki=1.0, kd=8.0, M=1.0, m=0.1, l=0.5)
Return an inverted pendulum on a cart that a PID controller of its angle keeps upright.
The controller sets the force on the cart to kp e + ki ∫ e dt - kd theta' from the error e = -theta of the
angle, within the bounds of the force. Near upright the closed loop is stable when kp > (M + m) g and kd > 0,
and the angle returns to upright while the cart drifts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
theta
|
float
|
The initial angle from upright. |
0.1
|
kp
|
float
|
The proportional gain. |
40.0
|
ki
|
float
|
The integral gain. |
1.0
|
kd
|
float
|
The derivative gain. |
8.0
|
M
|
float
|
The mass of the cart. |
1.0
|
m
|
float
|
The mass at the end of the rod. |
0.1
|
l
|
float
|
The length of the rod. |
0.5
|
Returns:
| Name | Type | Description |
|---|---|---|
system |
System
|
The model, with the angle |
Examples:
>>> from skcomplex.models.engineering.control import pid_inverted_pendulum
>>> pid_inverted_pendulum(kp=50.0).interactions[0].kp
50.0
Source code in src/skcomplex/models/engineering/control/_regulation.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
pid_loop(y=0.0, *, tau=1.0, gain=1.0, Kp=2.0, Ki=1.0, Kd=0.1, setpoint=1.0, change=0.0, disturbance=-0.5)
Return a work process whose output a PID controller keeps at a set point against a disturbance.
The output follows the first-order process τ y' = -y + gain u + d(t), and the controller sets
u = Kp e + Ki ∫ e dt - Kd y' from the error e = r(t) - y, with the derivative taken on the output, so
(τ + gain Kd) y' = -y + gain (Kp e + Ki ∫ e dt) + d(t). The disturbance d steps in at time 10, and a
coordinator moves the set point r by change at time 20. The integral action removes the error that the
disturbance leaves under proportional control alone.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
float
|
The initial output. |
0.0
|
tau
|
float
|
The time constant of the process. |
1.0
|
gain
|
float
|
The gain of the actuator. |
1.0
|
Kp
|
float
|
The proportional gain. |
2.0
|
Ki
|
float
|
The integral gain. |
1.0
|
Kd
|
float
|
The derivative gain. |
0.1
|
setpoint
|
float
|
The initial set point. |
1.0
|
change
|
float
|
The change of the set point at time 20. |
0.0
|
disturbance
|
float
|
The size of the disturbance from time 10. |
-0.5
|
Returns:
| Name | Type | Description |
|---|---|---|
system |
System
|
The model, with the output |
Examples:
>>> from skcomplex.models.engineering.control import pid_loop
>>> pid_loop(Ki=0.0).elements[0].Ki
0.0
Source code in src/skcomplex/models/engineering/control/_control.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | |
satellite_transfer(s=0.314, velocity=0.0, theta=0.0, *, Lz=0.7, epsilon=0.1, H_final=-0.2)
Return a satellite moved between Kepler orbits of equal angular momentum by radial thrust.
In normalised polar coordinates the satellite follows s'' = L_z^2 / s^3 - 1 / s^2 - ε sigma and
θ' = L_z / s^2. Radial thrust keeps the angular momentum and changes the effective energy
H = s'^2 / 2 + L_z^2 / (2 s^2) - 1 / s at the rate -ε sigma s', so the controller pushes inwards, sigma = 1,
while the satellite falls and outwards, sigma = -1, while it rises, until H reaches the final energy. From the
periapsis of the orbit with H = -0.7, the transfer to H = -0.2 takes about 9.81.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
s
|
float
|
The initial distance to the planet. |
0.314
|
velocity
|
float
|
The initial radial velocity. |
0.0
|
theta
|
float
|
The initial angle. |
0.0
|
Lz
|
float
|
The angular momentum. |
0.7
|
epsilon
|
float
|
The thrust. |
0.1
|
H_final
|
float
|
The effective energy of the final orbit. |
-0.2
|
Returns:
| Name | Type | Description |
|---|---|---|
system |
System
|
The model, with the distance |
Examples:
>>> from skcomplex.models.engineering.control import satellite_transfer
>>> satellite_transfer().elements[0].Lz
0.7
Source code in src/skcomplex/models/engineering/control/_control.py
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | |
thermostat(T=10.0, *, setting=18.0, furnace=5.0, leak=0.1, outside=10.0, swing=0.0)
Return a room heated by a furnace under a thermostat while heat leaks to the outside.
The furnace adds the gap between the setting and the room temperature, at most furnace degrees per hour, and
nothing when the room is warmer than the setting. The room loses leak of its difference to the outside
temperature per hour, so it settles below the setting, at (setting + leak * outside) / (1 + leak) for a
constant outside. The outside temperature swings by swing around outside over a day, warmest at noon.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
T
|
float
|
The initial room temperature. |
10.0
|
setting
|
float
|
The thermostat setting. |
18.0
|
furnace
|
float
|
The largest heating rate of the furnace, in degrees per hour. |
5.0
|
leak
|
float
|
The fraction of the difference to the outside lost per hour. |
0.1
|
outside
|
float
|
The mean outside temperature. |
10.0
|
swing
|
float
|
The amplitude of the daily swing of the outside temperature. |
0.0
|
Returns:
| Name | Type | Description |
|---|---|---|
system |
System
|
The model, with the room temperature |
Examples:
>>> from skcomplex.models.engineering.control import thermostat
>>> thermostat(leak=0.3).elements[0].leak
0.3
Source code in src/skcomplex/models/engineering/control/_control.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |