Source code for epyt_control.controllers.pid

  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