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 |