1"""
2This module contains an implementation of a PID controller and a
3learner (i.e. tuning method) based on an evolutionary algorithm.
4"""
5from typing import Optional
6from copy import deepcopy
7import numpy as np
8
9from ..envs.rl_env import RlEnv
10
11
[docs]
12class PidController():
13 """
14 Implementation of a Proportional-Integral-Derivative (PID) controller.
15
16 Parameters
17 ----------
18 proportional_gain : `float`
19 Proportional gain coefficient.
20 integral_gain : `float`
21 Integral gain coefficient.
22 derivative_gain : `float`
23 Derivative gain coefficient.
24 target_value : `float`
25 Target value (observed system state) which the controller is supposed to reach.
26 action_lower_bound : `float`, optional
27 Lower bound of the computed action.
28 Smaller control outputs will be clipped.
29
30 The default is None.
31 action_upper_bound : `float`, optional
32 Upper bound of the computed action.
33 Control outputs exceeding this upper bound will be clipped.
34
35 The default is None.
36 """
37 def __init__(self, proportional_gain: float, integral_gain: float, derivative_gain: float,
38 target_value: float, action_lower_bound: Optional[float] = None,
39 action_upper_bound: Optional[float] = None):
40 if not isinstance(proportional_gain, float):
41 raise TypeError("'proportional_gain' must be an instance of 'float' " +
42 f"but not of '{type(proportional_gain)}'")
43 if not isinstance(integral_gain, float):
44 raise TypeError("'integral_gain' must be an instance of 'float' " +
45 f"but not of '{type(integral_gain)}'")
46 if not isinstance(derivative_gain, float):
47 raise TypeError("'derivative_gain' must be an instance of 'float' " +
48 f"but not of '{type(derivative_gain)}'")
49 if not isinstance(target_value, float):
50 raise TypeError("'target_value' must be an instance of 'float' " +
51 f"but not of '{type(target_value)}'")
52 if action_lower_bound is not None:
53 if not isinstance(action_lower_bound, float):
54 raise TypeError("'action_lower_bound' must be an instance of 'float' " +
55 f"but not of '{type(action_lower_bound)}'")
56 if action_upper_bound is not None:
57 if not isinstance(action_upper_bound, float):
58 raise TypeError("'action_upper_bound' must be an instance of 'float' " +
59 f"but not of '{type(action_upper_bound)}'")
60 if action_upper_bound is not None and action_lower_bound is not None:
61 if action_lower_bound >= action_upper_bound:
62 raise ValueError("'action_lower_bound' must be smaller than 'action_upper_bound'")
63
64 self._proportional_gain = proportional_gain
65 self._derivative_gain = derivative_gain
66 self._integral_gain = integral_gain
67 self._target_value = target_value
68 self._action_lower_bound = action_lower_bound
69 self._action_upper_bound = action_upper_bound
70
71 self._last_error = 0
72 self._integral = 0
73
74 def __str__(self) -> str:
75 return f"proportional_gain: {self._proportional_gain} " + \
76 f"derivative_gain: {self._derivative_gain} integral_gain: {self._integral_gain} " + \
77 f"target_value: {self._target_value} action_lower_bound: {self._action_lower_bound}" + \
78 f" action_upper_bound: {self._action_upper_bound}"
79
80 def __eq__(self, other) -> bool:
81 if not isinstance(other, PidController):
82 raise TypeError(f"Can not compare 'PidController' to '{type(other)}'")
83
84 return self._proportional_gain == other.proportional_gain and \
85 self._derivative_gain == other.derivative_gain and \
86 self._integral_gain == other.integral_gain and \
87 self._target_value == other.target_value and \
88 self._action_lower_bound == other.action_lower_bound and \
89 self._action_upper_bound == other.action_upper_bound
90
91 @property
92 def proportional_gain(self) -> float:
93 """
94 Returns the proportional gain coefficient.
95
96 Returns
97 -------
98 `float`
99 Proportional gain coefficient.
100 """
101 return self._proportional_gain
102
103 @property
104 def integral_gain(self) -> float:
105 """
106 Returns the integral gain coefficient.
107
108 Returns
109 -------
110 `float`
111 Integral gain coefficient.
112 """
113 return self._integral_gain
114
115 @property
116 def derivative_gain(self) -> float:
117 """
118 Returns the derivative gain coefficient.
119
120 Returns
121 -------
122 `float`
123 Derivative gain coefficient.
124 """
125 return self._derivative_gain
126
127 @property
128 def target_value(self) -> float:
129 """
130 Returns the target value (observed system state) which the controller is supposed to reach.
131
132 Returns
133 -------
134 `float`
135 Target value (system state).
136 """
137 return self._target_value
138
139 @property
140 def action_lower_bound(self) -> float:
141 """
142 Lower bound of the computed action.
143 Smaller control outputs will be clipped.
144
145 Returns
146 -------
147 `float`
148 Lower bound of the computed action.
149 """
150 return self._action_lower_bound
151
152 @property
153 def action_upper_bound(self) -> float:
154 """
155 Upper bound of the computed action.
156 Control outputs exceeding this upper bound will be clipped.
157
158 Returns
159 -------
160 `float`
161 Upper bound of the computed action.
162 """
163 return self._action_upper_bound
164
[docs]
165 def step(self, cur_value: float) -> float:
166 """
167 Computes the current/next control action/signal based on the
168 given observation (i.e. system state).
169
170 Parameters
171 ----------
172 cur_value : `float`
173 Current observation (i.e. system state).
174
175 Returns
176 -------
177 `float`
178 Computed action -- i.e. control signal.
179 """
180 error = self._target_value - cur_value
181 self._integral += self._integral_gain * error
182
183 action = self._proportional_gain * error + self._integral_gain * self._integral + \
184 (self._derivative_gain * (error - self._last_error))
185 if np.isnan(action):
186 action = 0
187
188 # Clip if action is outside of bounds
189 if self._action_lower_bound is not None:
190 action = max(action, self._action_lower_bound)
191 if self._action_upper_bound is not None:
192 action = min(action, self._action_upper_bound)
193
194 self._last_error = error
195 return action