library.pid_controller
1class PID: 2 """PID controller. 3 4 Implements a simple proportional–integral–derivative controller. 5 6 Args: 7 kp (float): Proportional gain. 8 ki (float): Integral gain. 9 kd (float): Derivative gain. 10 setpoint (float, optional): Desired target value. Defaults to 0.0. 11 output_limits (tuple, optional): Tuple (min, max) to clamp the controller output. 12 Use None for no limit. Defaults to (None, None). 13 invert_output (bool): swap sign on output 14 15 Attributes: 16 kp (float): Proportional gain. 17 ki (float): Integral gain. 18 kd (float): Derivative gain. 19 setpoint (float): Target setpoint. 20 min_output (float or None): Minimum output limit. 21 max_output (float or None): Maximum output limit. 22 invert_output (bool): swap sign on output 23 24 """ 25 26 def __init__(self, kp, ki, kd, setpoint=0.0, 27 output_limits=(None, None), 28 invert_output=False): 29 """ 30 Initialize the PID controller. 31 32 Parameters are the same as described in the class docstring. 33 """ 34 self.kp = kp 35 self.ki = ki 36 self.kd = kd 37 38 self.setpoint = setpoint 39 40 self._last_error = 0.0 41 self._integral = 0.0 42 self._last_time = None 43 44 # Output limits: (min, max) 45 self.min_output, self.max_output = output_limits 46 self.invert_output = invert_output 47 48 def reset(self): 49 """ 50 Reset the controller internal state. 51 52 Clears the integral accumulator and last error/time so the controller 53 behaves as if newly constructed. 54 """ 55 self._last_error = 0.0 56 self._integral = 0.0 57 self._last_time = None 58 59 def __call__(self, measurement, dt): 60 """ 61 Calculate the PID output for a given measurement and timestep. 62 63 Args: 64 measurement (float): The current measured value. 65 dt (float): Time interval in seconds since the last call. If dt <= 0.0, 66 the derivative term is treated as zero and the integral is not updated. 67 68 Returns: 69 float: Control output after applying proportional, integral, and 70 derivative terms, clamped to output_limits if specified. 71 """ 72 error = self.setpoint - measurement 73 74 # proportional 75 p = self.kp * error 76 77 # integral (sum) 78 if dt > 0.0: 79 self._integral += error * dt 80 i = self.ki * self._integral 81 82 # derivative (slope) 83 if dt > 0.0: 84 derivative = (error - self._last_error) / dt 85 else: 86 derivative = 0.0 87 d = self.kd * derivative 88 89 output = p + i + d 90 91 # check limits 92 if self.min_output is not None: 93 output = max(self.min_output, output) 94 if self.max_output is not None: 95 output = min(self.max_output, output) 96 97 self._last_error = error 98 99 if self.invert_output: 100 output = output * -1 101 102 return output 103 104 105class Controller(): 106 """Multi-axis PID controller for coordinated control. 107 108 Manages multiple PID controllers, one per axis, for coordinated multi-axis 109 control systems such as robotics applications. Each axis can have independent 110 PID gains and setpoints. 111 112 Args: 113 axes (dict, optional): Dictionary mapping axis names to PID parameter 114 dictionaries. Each parameter dictionary should contain 'kp', 'ki', 115 'kd', and 'setpoint' keys. If None, creates default x, y, z axes 116 with zero gains. Defaults to None. 117 118 Attributes: 119 DEFAULT_PID (dict): Default PID parameters with all gains set to zero. 120 Used when no axes configuration is provided. 121 axes (dict): Dictionary mapping axis names to PID controller instances. 122 123 Example: 124 >>> axes = { 125 ... 'x': {'kp': 37, 'ki': 0, 'kd': 1.2, 'setpoint': 0.2}, 126 ... 'y': {'kp': 57, 'ki': 0, 'kd': 0.2, 'setpoint': 0.3}, 127 ... 'z': {'kp': 12, 'ki': 0, 'kd': 2.2, 'setpoint': 0.01}, 128 ... } 129 >>> controller = Controller(axes=axes) 130 >>> measurements = {'x': 0.15, 'y': 0.20, 'z': 0.005} 131 >>> outputs = controller(measurements, dt=0.01) 132 """ 133 134 DEFAULT_PID = { 135 'kp': 0, 136 'ki': 0, 137 'kd': 0, 138 'setpoint': 0 139 } 140 141 def __init__(self, axes=None): 142 """Initialize the multi-axis controller. 143 144 Args: 145 axes (dict, optional): Dictionary mapping axis names to PID parameter 146 dictionaries. Each parameter dictionary should contain 'kp', 'ki', 147 'kd', and 'setpoint' keys. If None, creates default x, y, z axes 148 with zero gains. Defaults to None. 149 150 Example: 151 >>> # Custom axes configuration 152 >>> axes = { 153 ... 'x': {'kp': 10, 'ki': 0.1, 'kd': 1, 'setpoint': 0.5}, 154 ... 'y': {'kp': 15, 'ki': 0.2, 'kd': 2, 'setpoint': 0.3} 155 ... } 156 >>> controller = Controller(axes=axes) 157 158 >>> # Default configuration (all zeros) 159 >>> controller = Controller() 160 """ 161 self.axes = axes 162 163 @property 164 def axes(self): 165 """Get dictionary mapping axis names to PID controller instances. 166 167 Returns: 168 dict: Dictionary with axis names as keys (e.g., 'x', 'y', 'z') and 169 PID controller instances as values. 170 """ 171 return self._axes 172 173 @axes.setter 174 def axes(self, axes): 175 """Set up PID controllers for each axis. 176 177 Creates individual PID controller instances for each axis based on the 178 provided configuration. If no configuration is provided, creates default 179 x, y, z axes with zero gains. 180 181 Args: 182 axes (dict or None): Dictionary mapping axis names to PID parameter 183 dictionaries, or None for default configuration. 184 """ 185 pid_axes = {} 186 if not axes: 187 my_axes = {'x': self.DEFAULT_PID, 188 'y': self.DEFAULT_PID, 189 'z': self.DEFAULT_PID} 190 else: 191 my_axes = axes 192 193 for a, value in my_axes.items(): 194 pid_axes[a] = PID(**value) 195 196 self._axes = pid_axes 197 198 def reset(self, axis='all'): 199 """Reset internal state of one or more PID controllers. 200 201 Clears the integral accumulator and last error/time for the specified 202 axis or axes, returning them to their initial state. 203 204 Args: 205 axis (str or list, optional): Specifies which axes to reset: 206 - 'all': Reset all axes (default) 207 - str: Reset single axis by name (e.g., 'x') 208 - list: Reset multiple specific axes (e.g., ['x', 'z']) 209 210 Raises: 211 KeyError: If a specified axis name does not exist. 212 213 Example: 214 >>> controller.reset() # Reset all axes 215 >>> controller.reset('x') # Reset only x-axis 216 >>> controller.reset(['x', 'y']) # Reset x and y axes 217 """ 218 if axis == 'all': 219 axis_list = list(self.axes.values()) 220 elif isinstance(axis, str): 221 # Single axis name 222 axis_list = [self.axes[axis]] 223 else: 224 # Assume it's an iterable of axis names 225 axis_list = [self.axes[a] for a in axis] 226 227 for a in axis_list: 228 a.reset() 229 230 def __call__(self, measurements, dt): 231 """Compute PID output for all axes. 232 233 Calculates control outputs for all configured axes based on current 234 measurements and the time step. This is the primary method for getting 235 control commands during simulation or real-time control. 236 237 Args: 238 measurements (dict): Current measured values for each axis. 239 Keys must match the axis names defined in the controller. 240 Example: {'x': 0.15, 'y': 0.20, 'z': 1.05} 241 dt (float): Time step in seconds since the last update. 242 Must be positive for proper integral and derivative calculations. 243 244 Returns: 245 dict: Control output (typically velocity commands) for each axis. 246 Keys match the input measurement keys. 247 Example: {'x': 0.05, 'y': -0.03, 'z': 0.01} 248 249 Raises: 250 KeyError: If measurements dict is missing a required axis. 251 252 Example: 253 >>> measurements = {'x': 0.15, 'y': 0.20, 'z': 0.005} 254 >>> outputs = controller(measurements, dt=0.01) 255 >>> print(outputs) 256 {'x': 1.85, 'y': 5.7, 'z': 0.06} 257 """ 258 return {axis: controller(measurements[axis], dt) 259 for axis, controller in self.axes.items()}
2class PID: 3 """PID controller. 4 5 Implements a simple proportional–integral–derivative controller. 6 7 Args: 8 kp (float): Proportional gain. 9 ki (float): Integral gain. 10 kd (float): Derivative gain. 11 setpoint (float, optional): Desired target value. Defaults to 0.0. 12 output_limits (tuple, optional): Tuple (min, max) to clamp the controller output. 13 Use None for no limit. Defaults to (None, None). 14 invert_output (bool): swap sign on output 15 16 Attributes: 17 kp (float): Proportional gain. 18 ki (float): Integral gain. 19 kd (float): Derivative gain. 20 setpoint (float): Target setpoint. 21 min_output (float or None): Minimum output limit. 22 max_output (float or None): Maximum output limit. 23 invert_output (bool): swap sign on output 24 25 """ 26 27 def __init__(self, kp, ki, kd, setpoint=0.0, 28 output_limits=(None, None), 29 invert_output=False): 30 """ 31 Initialize the PID controller. 32 33 Parameters are the same as described in the class docstring. 34 """ 35 self.kp = kp 36 self.ki = ki 37 self.kd = kd 38 39 self.setpoint = setpoint 40 41 self._last_error = 0.0 42 self._integral = 0.0 43 self._last_time = None 44 45 # Output limits: (min, max) 46 self.min_output, self.max_output = output_limits 47 self.invert_output = invert_output 48 49 def reset(self): 50 """ 51 Reset the controller internal state. 52 53 Clears the integral accumulator and last error/time so the controller 54 behaves as if newly constructed. 55 """ 56 self._last_error = 0.0 57 self._integral = 0.0 58 self._last_time = None 59 60 def __call__(self, measurement, dt): 61 """ 62 Calculate the PID output for a given measurement and timestep. 63 64 Args: 65 measurement (float): The current measured value. 66 dt (float): Time interval in seconds since the last call. If dt <= 0.0, 67 the derivative term is treated as zero and the integral is not updated. 68 69 Returns: 70 float: Control output after applying proportional, integral, and 71 derivative terms, clamped to output_limits if specified. 72 """ 73 error = self.setpoint - measurement 74 75 # proportional 76 p = self.kp * error 77 78 # integral (sum) 79 if dt > 0.0: 80 self._integral += error * dt 81 i = self.ki * self._integral 82 83 # derivative (slope) 84 if dt > 0.0: 85 derivative = (error - self._last_error) / dt 86 else: 87 derivative = 0.0 88 d = self.kd * derivative 89 90 output = p + i + d 91 92 # check limits 93 if self.min_output is not None: 94 output = max(self.min_output, output) 95 if self.max_output is not None: 96 output = min(self.max_output, output) 97 98 self._last_error = error 99 100 if self.invert_output: 101 output = output * -1 102 103 return output
PID controller.
Implements a simple proportional–integral–derivative controller.
Arguments:
- kp (float): Proportional gain.
- ki (float): Integral gain.
- kd (float): Derivative gain.
- setpoint (float, optional): Desired target value. Defaults to 0.0.
- output_limits (tuple, optional): Tuple (min, max) to clamp the controller output. Use None for no limit. Defaults to (None, None).
- invert_output (bool): swap sign on output
Attributes:
- kp (float): Proportional gain.
- ki (float): Integral gain.
- kd (float): Derivative gain.
- setpoint (float): Target setpoint.
- min_output (float or None): Minimum output limit.
- max_output (float or None): Maximum output limit.
- invert_output (bool): swap sign on output
27 def __init__(self, kp, ki, kd, setpoint=0.0, 28 output_limits=(None, None), 29 invert_output=False): 30 """ 31 Initialize the PID controller. 32 33 Parameters are the same as described in the class docstring. 34 """ 35 self.kp = kp 36 self.ki = ki 37 self.kd = kd 38 39 self.setpoint = setpoint 40 41 self._last_error = 0.0 42 self._integral = 0.0 43 self._last_time = None 44 45 # Output limits: (min, max) 46 self.min_output, self.max_output = output_limits 47 self.invert_output = invert_output
Initialize the PID controller.
Parameters are the same as described in the class docstring.
49 def reset(self): 50 """ 51 Reset the controller internal state. 52 53 Clears the integral accumulator and last error/time so the controller 54 behaves as if newly constructed. 55 """ 56 self._last_error = 0.0 57 self._integral = 0.0 58 self._last_time = None
Reset the controller internal state.
Clears the integral accumulator and last error/time so the controller behaves as if newly constructed.
106class Controller(): 107 """Multi-axis PID controller for coordinated control. 108 109 Manages multiple PID controllers, one per axis, for coordinated multi-axis 110 control systems such as robotics applications. Each axis can have independent 111 PID gains and setpoints. 112 113 Args: 114 axes (dict, optional): Dictionary mapping axis names to PID parameter 115 dictionaries. Each parameter dictionary should contain 'kp', 'ki', 116 'kd', and 'setpoint' keys. If None, creates default x, y, z axes 117 with zero gains. Defaults to None. 118 119 Attributes: 120 DEFAULT_PID (dict): Default PID parameters with all gains set to zero. 121 Used when no axes configuration is provided. 122 axes (dict): Dictionary mapping axis names to PID controller instances. 123 124 Example: 125 >>> axes = { 126 ... 'x': {'kp': 37, 'ki': 0, 'kd': 1.2, 'setpoint': 0.2}, 127 ... 'y': {'kp': 57, 'ki': 0, 'kd': 0.2, 'setpoint': 0.3}, 128 ... 'z': {'kp': 12, 'ki': 0, 'kd': 2.2, 'setpoint': 0.01}, 129 ... } 130 >>> controller = Controller(axes=axes) 131 >>> measurements = {'x': 0.15, 'y': 0.20, 'z': 0.005} 132 >>> outputs = controller(measurements, dt=0.01) 133 """ 134 135 DEFAULT_PID = { 136 'kp': 0, 137 'ki': 0, 138 'kd': 0, 139 'setpoint': 0 140 } 141 142 def __init__(self, axes=None): 143 """Initialize the multi-axis controller. 144 145 Args: 146 axes (dict, optional): Dictionary mapping axis names to PID parameter 147 dictionaries. Each parameter dictionary should contain 'kp', 'ki', 148 'kd', and 'setpoint' keys. If None, creates default x, y, z axes 149 with zero gains. Defaults to None. 150 151 Example: 152 >>> # Custom axes configuration 153 >>> axes = { 154 ... 'x': {'kp': 10, 'ki': 0.1, 'kd': 1, 'setpoint': 0.5}, 155 ... 'y': {'kp': 15, 'ki': 0.2, 'kd': 2, 'setpoint': 0.3} 156 ... } 157 >>> controller = Controller(axes=axes) 158 159 >>> # Default configuration (all zeros) 160 >>> controller = Controller() 161 """ 162 self.axes = axes 163 164 @property 165 def axes(self): 166 """Get dictionary mapping axis names to PID controller instances. 167 168 Returns: 169 dict: Dictionary with axis names as keys (e.g., 'x', 'y', 'z') and 170 PID controller instances as values. 171 """ 172 return self._axes 173 174 @axes.setter 175 def axes(self, axes): 176 """Set up PID controllers for each axis. 177 178 Creates individual PID controller instances for each axis based on the 179 provided configuration. If no configuration is provided, creates default 180 x, y, z axes with zero gains. 181 182 Args: 183 axes (dict or None): Dictionary mapping axis names to PID parameter 184 dictionaries, or None for default configuration. 185 """ 186 pid_axes = {} 187 if not axes: 188 my_axes = {'x': self.DEFAULT_PID, 189 'y': self.DEFAULT_PID, 190 'z': self.DEFAULT_PID} 191 else: 192 my_axes = axes 193 194 for a, value in my_axes.items(): 195 pid_axes[a] = PID(**value) 196 197 self._axes = pid_axes 198 199 def reset(self, axis='all'): 200 """Reset internal state of one or more PID controllers. 201 202 Clears the integral accumulator and last error/time for the specified 203 axis or axes, returning them to their initial state. 204 205 Args: 206 axis (str or list, optional): Specifies which axes to reset: 207 - 'all': Reset all axes (default) 208 - str: Reset single axis by name (e.g., 'x') 209 - list: Reset multiple specific axes (e.g., ['x', 'z']) 210 211 Raises: 212 KeyError: If a specified axis name does not exist. 213 214 Example: 215 >>> controller.reset() # Reset all axes 216 >>> controller.reset('x') # Reset only x-axis 217 >>> controller.reset(['x', 'y']) # Reset x and y axes 218 """ 219 if axis == 'all': 220 axis_list = list(self.axes.values()) 221 elif isinstance(axis, str): 222 # Single axis name 223 axis_list = [self.axes[axis]] 224 else: 225 # Assume it's an iterable of axis names 226 axis_list = [self.axes[a] for a in axis] 227 228 for a in axis_list: 229 a.reset() 230 231 def __call__(self, measurements, dt): 232 """Compute PID output for all axes. 233 234 Calculates control outputs for all configured axes based on current 235 measurements and the time step. This is the primary method for getting 236 control commands during simulation or real-time control. 237 238 Args: 239 measurements (dict): Current measured values for each axis. 240 Keys must match the axis names defined in the controller. 241 Example: {'x': 0.15, 'y': 0.20, 'z': 1.05} 242 dt (float): Time step in seconds since the last update. 243 Must be positive for proper integral and derivative calculations. 244 245 Returns: 246 dict: Control output (typically velocity commands) for each axis. 247 Keys match the input measurement keys. 248 Example: {'x': 0.05, 'y': -0.03, 'z': 0.01} 249 250 Raises: 251 KeyError: If measurements dict is missing a required axis. 252 253 Example: 254 >>> measurements = {'x': 0.15, 'y': 0.20, 'z': 0.005} 255 >>> outputs = controller(measurements, dt=0.01) 256 >>> print(outputs) 257 {'x': 1.85, 'y': 5.7, 'z': 0.06} 258 """ 259 return {axis: controller(measurements[axis], dt) 260 for axis, controller in self.axes.items()}
Multi-axis PID controller for coordinated control.
Manages multiple PID controllers, one per axis, for coordinated multi-axis control systems such as robotics applications. Each axis can have independent PID gains and setpoints.
Arguments:
- axes (dict, optional): Dictionary mapping axis names to PID parameter dictionaries. Each parameter dictionary should contain 'kp', 'ki', 'kd', and 'setpoint' keys. If None, creates default x, y, z axes with zero gains. Defaults to None.
Attributes:
- DEFAULT_PID (dict): Default PID parameters with all gains set to zero. Used when no axes configuration is provided.
- axes (dict): Dictionary mapping axis names to PID controller instances.
Example:
>>> axes = { ... 'x': {'kp': 37, 'ki': 0, 'kd': 1.2, 'setpoint': 0.2}, ... 'y': {'kp': 57, 'ki': 0, 'kd': 0.2, 'setpoint': 0.3}, ... 'z': {'kp': 12, 'ki': 0, 'kd': 2.2, 'setpoint': 0.01}, ... } >>> controller = Controller(axes=axes) >>> measurements = {'x': 0.15, 'y': 0.20, 'z': 0.005} >>> outputs = controller(measurements, dt=0.01)
142 def __init__(self, axes=None): 143 """Initialize the multi-axis controller. 144 145 Args: 146 axes (dict, optional): Dictionary mapping axis names to PID parameter 147 dictionaries. Each parameter dictionary should contain 'kp', 'ki', 148 'kd', and 'setpoint' keys. If None, creates default x, y, z axes 149 with zero gains. Defaults to None. 150 151 Example: 152 >>> # Custom axes configuration 153 >>> axes = { 154 ... 'x': {'kp': 10, 'ki': 0.1, 'kd': 1, 'setpoint': 0.5}, 155 ... 'y': {'kp': 15, 'ki': 0.2, 'kd': 2, 'setpoint': 0.3} 156 ... } 157 >>> controller = Controller(axes=axes) 158 159 >>> # Default configuration (all zeros) 160 >>> controller = Controller() 161 """ 162 self.axes = axes
Initialize the multi-axis controller.
Arguments:
- axes (dict, optional): Dictionary mapping axis names to PID parameter dictionaries. Each parameter dictionary should contain 'kp', 'ki', 'kd', and 'setpoint' keys. If None, creates default x, y, z axes with zero gains. Defaults to None.
Example:
>>> # Custom axes configuration >>> axes = { ... 'x': {'kp': 10, 'ki': 0.1, 'kd': 1, 'setpoint': 0.5}, ... 'y': {'kp': 15, 'ki': 0.2, 'kd': 2, 'setpoint': 0.3} ... } >>> controller = Controller(axes=axes)>>> # Default configuration (all zeros) >>> controller = Controller()
164 @property 165 def axes(self): 166 """Get dictionary mapping axis names to PID controller instances. 167 168 Returns: 169 dict: Dictionary with axis names as keys (e.g., 'x', 'y', 'z') and 170 PID controller instances as values. 171 """ 172 return self._axes
Get dictionary mapping axis names to PID controller instances.
Returns:
dict: Dictionary with axis names as keys (e.g., 'x', 'y', 'z') and PID controller instances as values.
199 def reset(self, axis='all'): 200 """Reset internal state of one or more PID controllers. 201 202 Clears the integral accumulator and last error/time for the specified 203 axis or axes, returning them to their initial state. 204 205 Args: 206 axis (str or list, optional): Specifies which axes to reset: 207 - 'all': Reset all axes (default) 208 - str: Reset single axis by name (e.g., 'x') 209 - list: Reset multiple specific axes (e.g., ['x', 'z']) 210 211 Raises: 212 KeyError: If a specified axis name does not exist. 213 214 Example: 215 >>> controller.reset() # Reset all axes 216 >>> controller.reset('x') # Reset only x-axis 217 >>> controller.reset(['x', 'y']) # Reset x and y axes 218 """ 219 if axis == 'all': 220 axis_list = list(self.axes.values()) 221 elif isinstance(axis, str): 222 # Single axis name 223 axis_list = [self.axes[axis]] 224 else: 225 # Assume it's an iterable of axis names 226 axis_list = [self.axes[a] for a in axis] 227 228 for a in axis_list: 229 a.reset()
Reset internal state of one or more PID controllers.
Clears the integral accumulator and last error/time for the specified axis or axes, returning them to their initial state.
Arguments:
- axis (str or list, optional): Specifies which axes to reset:
- 'all': Reset all axes (default)
- str: Reset single axis by name (e.g., 'x')
- list: Reset multiple specific axes (e.g., ['x', 'z'])
Raises:
- KeyError: If a specified axis name does not exist.
Example:
>>> controller.reset() # Reset all axes >>> controller.reset('x') # Reset only x-axis >>> controller.reset(['x', 'y']) # Reset x and y axes