|
1
|
|
|
# -*- coding: utf-8 -*- |
|
2
|
|
|
|
|
3
|
|
|
""" |
|
4
|
|
|
A module for controlling processes via PID regulation. |
|
5
|
|
|
|
|
6
|
|
|
Qudi is free software: you can redistribute it and/or modify |
|
7
|
|
|
it under the terms of the GNU General Public License as published by |
|
8
|
|
|
the Free Software Foundation, either version 3 of the License, or |
|
9
|
|
|
(at your option) any later version. |
|
10
|
|
|
|
|
11
|
|
|
Qudi is distributed in the hope that it will be useful, |
|
12
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14
|
|
|
GNU General Public License for more details. |
|
15
|
|
|
|
|
16
|
|
|
You should have received a copy of the GNU General Public License |
|
17
|
|
|
along with Qudi. If not, see <http://www.gnu.org/licenses/>. |
|
18
|
|
|
|
|
19
|
|
|
Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the |
|
20
|
|
|
top-level directory of this distribution and at <https://github.com/Ulm-IQO/qudi/> |
|
21
|
|
|
""" |
|
22
|
|
|
|
|
23
|
|
|
import numpy as np |
|
24
|
|
|
|
|
25
|
|
|
from core.module import Connector, ConfigOption, StatusVar |
|
26
|
|
|
from core.util.mutex import Mutex |
|
27
|
|
|
from logic.generic_logic import GenericLogic |
|
28
|
|
|
from qtpy import QtCore |
|
29
|
|
|
|
|
30
|
|
|
|
|
31
|
|
|
class PIDLogic(GenericLogic): |
|
32
|
|
|
""" |
|
33
|
|
|
Control a process via software PID. |
|
34
|
|
|
""" |
|
35
|
|
|
_modclass = 'pidlogic' |
|
36
|
|
|
_modtype = 'logic' |
|
37
|
|
|
|
|
38
|
|
|
## declare connectors |
|
39
|
|
|
controller = Connector(interface='PIDControllerInterface') |
|
40
|
|
|
savelogic = Connector(interface='SaveLogic') |
|
41
|
|
|
_features = ConfigOption('features', ['PID_CONTROLLER']) |
|
42
|
|
|
# can include 'PID_CONTROLLER', 'SETPOINT_CONTROLLER', 'SETPOINT', 'PROCESS_VARIABLE' or 'PROCESS_CONTROL' |
|
43
|
|
|
# depending on what is available |
|
44
|
|
|
|
|
45
|
|
|
# status vars |
|
46
|
|
|
bufferLength = StatusVar('bufferlength', 1000) |
|
47
|
|
|
# TODO: Maybe the logic should keep everything (this should not be correlated with the GUI) |
|
48
|
|
|
|
|
49
|
|
|
_timestep = StatusVar(default=100) |
|
50
|
|
|
_loop_enabled = False |
|
51
|
|
|
|
|
52
|
|
|
# signals |
|
53
|
|
|
sigUpdateDisplay = QtCore.Signal() |
|
54
|
|
|
|
|
55
|
|
|
def __init__(self, config, **kwargs): |
|
56
|
|
|
super().__init__(config=config, **kwargs) |
|
57
|
|
|
|
|
58
|
|
|
# number of lines in the matrix plot |
|
59
|
|
|
self.NumberOfSecondsLog = 100 # This breaks logic/GUI compartmentalisation |
|
60
|
|
|
self.threadlock = Mutex() |
|
61
|
|
|
|
|
62
|
|
|
def on_activate(self): |
|
63
|
|
|
""" Initialisation performed during activation of the module. |
|
64
|
|
|
""" |
|
65
|
|
|
self._controller = self.controller() |
|
66
|
|
|
self._save_logic = self.savelogic() |
|
67
|
|
|
|
|
68
|
|
|
self._has_controller = set(['PID_CONTROLLER', 'SETPOINT_CONTROLLER']).intersection(set(self._features)) |
|
69
|
|
|
self._has_setpoint = bool(set(['PID_CONTROLLER', 'SETPOINT_CONTROLLER', 'SETPOINT']).intersection(set(self._features))) |
|
70
|
|
|
self._has_process_value = bool(set(['PID_CONTROLLER', 'SETPOINT_CONTROLLER', 'PROCESS_VARIABLE']).intersection(set(self._features))) |
|
71
|
|
|
self._has_control_value = bool(set(['PID_CONTROLLER', 'PROCESS_VARIABLE']).intersection(set(self._features))) |
|
72
|
|
|
self._has_pid = 'PID_CONTROLLER' in self._features |
|
73
|
|
|
|
|
74
|
|
|
|
|
75
|
|
|
self.history = np.zeros([3, self.bufferLength]) |
|
76
|
|
|
self.savingState = False |
|
77
|
|
|
|
|
78
|
|
|
self.timer = QtCore.QTimer() |
|
79
|
|
|
self.timer.setSingleShot(True) |
|
80
|
|
|
self.timer.setInterval(self._timestep) |
|
81
|
|
|
self.timer.timeout.connect(self.loop) |
|
82
|
|
|
|
|
83
|
|
|
self.start_loop() |
|
84
|
|
|
|
|
85
|
|
|
def on_deactivate(self): |
|
86
|
|
|
""" Perform required deactivation. """ |
|
87
|
|
|
pass |
|
88
|
|
|
|
|
89
|
|
|
def getBufferLength(self): #TODO: This breaks logic/GUI compartmentalisation (and naming conventions) |
|
90
|
|
|
""" Get the current data buffer length. |
|
91
|
|
|
""" |
|
92
|
|
|
return self.bufferLength |
|
93
|
|
|
|
|
94
|
|
|
def setBufferLength(self, newBufferLength): #TODO: This breaks logic/GUI compartmentalisation (and naming conventions) |
|
95
|
|
|
""" Change buffer length to new value. |
|
96
|
|
|
|
|
97
|
|
|
@param int newBufferLength: new buffer length |
|
98
|
|
|
""" |
|
99
|
|
|
self.bufferLength = newBufferLength |
|
100
|
|
|
self.history = np.zeros([3, self.bufferLength]) |
|
101
|
|
|
|
|
102
|
|
|
def start_loop(self): |
|
103
|
|
|
""" Start the data acquisition loop. |
|
104
|
|
|
""" |
|
105
|
|
|
self._loop_enabled = True |
|
106
|
|
|
self.timer.start(self._timestep) |
|
107
|
|
|
|
|
108
|
|
|
def stop_loop(self): |
|
109
|
|
|
""" Stop the data acquisition loop. |
|
110
|
|
|
""" |
|
111
|
|
|
self._loop_enabled = False |
|
112
|
|
|
|
|
113
|
|
|
def loop(self): |
|
114
|
|
|
""" Execute step in the data acquisition loop: save one of each control and process values |
|
115
|
|
|
""" |
|
116
|
|
|
self.history = np.roll(self.history, -1, axis=1) # TODO : What is the efficiency of "roll storing" method on large array ? |
|
117
|
|
|
|
|
118
|
|
|
#TODO: only store activated info, hybrid for now |
|
119
|
|
|
if self._has_process_value: |
|
120
|
|
|
self.history[0, -1] = self._controller.get_process_value() |
|
121
|
|
|
else: |
|
122
|
|
|
self.history[0, -1] = 0 |
|
123
|
|
|
|
|
124
|
|
|
if self._has_control_value: |
|
125
|
|
|
self.history[1, -1] = self._controller.get_control_value() |
|
126
|
|
|
else: |
|
127
|
|
|
self.history[1, -1] = 0 |
|
128
|
|
|
|
|
129
|
|
|
if self._has_setpoint: |
|
130
|
|
|
self.history[2, -1] = self._controller.get_setpoint() |
|
131
|
|
|
else: |
|
132
|
|
|
self.history[2, -1] = 0 |
|
133
|
|
|
|
|
134
|
|
|
self.sigUpdateDisplay.emit() |
|
135
|
|
|
if self._loop_enabled: |
|
136
|
|
|
self.timer.start(self._timestep) |
|
137
|
|
|
|
|
138
|
|
|
# TODO: to make the GUI happy for now, this could vary so this need to be redesigned |
|
139
|
|
|
def get_timestep(self): |
|
140
|
|
|
return self._timestep |
|
141
|
|
|
|
|
142
|
|
|
def get_features(self): |
|
143
|
|
|
return self._features |
|
144
|
|
|
|
|
145
|
|
|
def get_saving_state(self): |
|
146
|
|
|
""" Return whether we are saving data |
|
147
|
|
|
|
|
148
|
|
|
@return bool: whether we are saving data right now |
|
149
|
|
|
""" |
|
150
|
|
|
return self.savingState |
|
151
|
|
|
|
|
152
|
|
|
def start_saving(self): |
|
153
|
|
|
""" Start saving data. |
|
154
|
|
|
|
|
155
|
|
|
Function does nothing right now. |
|
156
|
|
|
""" |
|
157
|
|
|
pass |
|
158
|
|
|
|
|
159
|
|
|
def save_sata(self): |
|
160
|
|
|
""" Stop saving data and write data to file. |
|
161
|
|
|
|
|
162
|
|
|
Function does nothing right now. |
|
163
|
|
|
""" |
|
164
|
|
|
pass |
|
165
|
|
|
|
|
166
|
|
|
# Beginning of features dependent methods : |
|
167
|
|
|
|
|
168
|
|
|
def get_kp(self): |
|
169
|
|
|
""" Return the proportional constant. |
|
170
|
|
|
|
|
171
|
|
|
@return float: proportional constant of PID controller |
|
172
|
|
|
""" |
|
173
|
|
|
if self._has_pid: |
|
174
|
|
|
return self._controller.get_kp() |
|
175
|
|
|
else: |
|
176
|
|
|
return 0 |
|
177
|
|
|
|
|
178
|
|
|
def set_kp(self, kp): |
|
179
|
|
|
""" Set the proportional constant of the PID controller. |
|
180
|
|
|
|
|
181
|
|
|
@prarm float kp: proportional constant of PID controller |
|
182
|
|
|
""" |
|
183
|
|
|
if self._has_pid: |
|
184
|
|
|
return self._controller.set_kp(kp) |
|
185
|
|
|
else: |
|
186
|
|
|
return 0 |
|
187
|
|
|
|
|
188
|
|
|
def get_ki(self): |
|
189
|
|
|
""" Get the integration constant of the PID controller |
|
190
|
|
|
|
|
191
|
|
|
@return float: integration constant of the PID controller |
|
192
|
|
|
""" |
|
193
|
|
|
if self._has_pid: |
|
194
|
|
|
return self._controller.get_ki() |
|
195
|
|
|
else: |
|
196
|
|
|
return 0 |
|
197
|
|
|
|
|
198
|
|
|
def set_ki(self, ki): |
|
199
|
|
|
""" Set the integration constant of the PID controller. |
|
200
|
|
|
|
|
201
|
|
|
@param float ki: integration constant of the PID controller |
|
202
|
|
|
""" |
|
203
|
|
|
if self._has_pid: |
|
204
|
|
|
return self._controller.set_ki(ki) |
|
205
|
|
|
else: |
|
206
|
|
|
return 0 |
|
207
|
|
|
|
|
208
|
|
|
def get_kd(self): |
|
209
|
|
|
""" Get the derivative constant of the PID controller |
|
210
|
|
|
|
|
211
|
|
|
@return float: the derivative constant of the PID controller |
|
212
|
|
|
""" |
|
213
|
|
|
if self._has_pid: |
|
214
|
|
|
return self._controller.get_kd() |
|
215
|
|
|
else: |
|
216
|
|
|
return 0 |
|
217
|
|
|
|
|
218
|
|
|
def set_kd(self, kd): |
|
219
|
|
|
""" Set the derivative constant of the PID controller |
|
220
|
|
|
|
|
221
|
|
|
@param float kd: the derivative constant of the PID controller |
|
222
|
|
|
""" |
|
223
|
|
|
if self._has_pid: |
|
224
|
|
|
return self._controller.set_kd(kd) |
|
225
|
|
|
else: |
|
226
|
|
|
return 0 |
|
227
|
|
|
|
|
228
|
|
|
def get_setpoint(self): |
|
229
|
|
|
""" Get the current setpoint of the controller. |
|
230
|
|
|
|
|
231
|
|
|
@return float: current set point of the controller |
|
232
|
|
|
""" |
|
233
|
|
|
if self._has_setpoint: |
|
234
|
|
|
return self.history[2, -1] |
|
235
|
|
|
else: |
|
236
|
|
|
return 0 |
|
237
|
|
|
|
|
238
|
|
|
def set_setpoint(self, setpoint): |
|
239
|
|
|
""" Set the current setpoint of the PID controller. |
|
240
|
|
|
|
|
241
|
|
|
@param float setpoint: new set point of the controller |
|
242
|
|
|
""" |
|
243
|
|
|
if self._has_setpoint: |
|
244
|
|
|
return self._controller.set_setpoint(setpoint) |
|
245
|
|
|
else: |
|
246
|
|
|
return 0 |
|
247
|
|
|
|
|
248
|
|
|
def get_enabled(self): |
|
249
|
|
|
""" See if the PID controller is controlling a process. |
|
250
|
|
|
|
|
251
|
|
|
@return bool: whether the PID controller is preparing to or conreolling a process |
|
252
|
|
|
""" |
|
253
|
|
|
if self._has_controller: |
|
254
|
|
|
return self._controller.get_enabled() |
|
255
|
|
|
else: |
|
256
|
|
|
return 0 |
|
257
|
|
|
|
|
258
|
|
|
def set_enabled(self, enabled): |
|
259
|
|
|
""" Set the state of the PID controller. |
|
260
|
|
|
|
|
261
|
|
|
@param bool enabled: desired state of PID controller |
|
262
|
|
|
""" |
|
263
|
|
|
if self._has_controller: |
|
264
|
|
|
return self._controller.set_enabled(enabled) |
|
265
|
|
|
else: |
|
266
|
|
|
return 0 |
|
267
|
|
|
|
|
268
|
|
|
def get_control_limits(self): |
|
269
|
|
|
""" Get the minimum and maximum value of the control actuator. |
|
270
|
|
|
|
|
271
|
|
|
@return list(float): (minimum, maximum) values of the control actuator |
|
272
|
|
|
""" |
|
273
|
|
|
if self._has_control_value: |
|
274
|
|
|
return self._controller.get_control_limits() |
|
275
|
|
|
else: |
|
276
|
|
|
return 0 |
|
277
|
|
|
|
|
278
|
|
|
def set_control_limits(self, limits): # TODO: Should this be ok ? |
|
279
|
|
|
""" Set the minimum and maximum value of the control actuator. |
|
280
|
|
|
|
|
281
|
|
|
@param list(float) limits: (minimum, maximum) values of the control actuator |
|
282
|
|
|
|
|
283
|
|
|
This function does nothing, control limits are handled by the control module |
|
284
|
|
|
""" |
|
285
|
|
|
if self._has_pid: |
|
286
|
|
|
return self._controller.set_control_limits(limits) |
|
287
|
|
|
else: |
|
288
|
|
|
return 0 |
|
289
|
|
|
|
|
290
|
|
|
def get_pv(self): # TODO : change name ??? It's confusion with PID variables |
|
291
|
|
|
""" Get current process input value. |
|
292
|
|
|
|
|
293
|
|
|
@return float: current process input value |
|
294
|
|
|
""" |
|
295
|
|
|
if self._has_process_value: |
|
296
|
|
|
return self.history[0, -1] |
|
297
|
|
|
else: |
|
298
|
|
|
return 0 |
|
299
|
|
|
|
|
300
|
|
|
def get_cv(self): |
|
301
|
|
|
""" Get current control output value. |
|
302
|
|
|
|
|
303
|
|
|
@return float: control output value |
|
304
|
|
|
""" |
|
305
|
|
|
if self._has_process_value: |
|
306
|
|
|
return self.history[1, -1] |
|
307
|
|
|
else: |
|
308
|
|
|
return 0 |
|
309
|
|
|
|
|
310
|
|
|
# TODO : What is that exactly ? |
|
311
|
|
|
def get_extra(self): |
|
312
|
|
|
if self._has_pid: |
|
313
|
|
|
return self._controller.get_extra() |
|
314
|
|
|
else: |
|
315
|
|
|
return [] |
|
316
|
|
|
|
|
317
|
|
|
# TODO: How does manual fit into all this ? |
|
318
|
|
|
def get_manual_value(self): |
|
319
|
|
|
""" Return the control value for manual mode. |
|
320
|
|
|
|
|
321
|
|
|
@return float: control value for manual mode |
|
322
|
|
|
""" |
|
323
|
|
|
|
|
324
|
|
|
if self._has_pid: |
|
325
|
|
|
return self._controller.get_manual_value() |
|
326
|
|
|
else: |
|
327
|
|
|
return 0 |
|
328
|
|
|
|
|
329
|
|
|
def set_manual_value(self, manualvalue): |
|
330
|
|
|
""" Set the control value for manual mode. |
|
331
|
|
|
|
|
332
|
|
|
@param float manualvalue: control value for manual mode of controller |
|
333
|
|
|
""" |
|
334
|
|
|
if self._has_pid: |
|
335
|
|
|
return self._controller.set_manual_value(manualvalue) |
|
336
|
|
|
else: |
|
337
|
|
|
return 0 |