15f0a79212747e22070bb18a11a5f28a599f539a
[pypid.git] / pypid / backend / __init__.py
1 # Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of pypid.
4 #
5 # pypid is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # pypid is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # pypid.  If not, see <http://www.gnu.org/licenses/>.
16
17 """Assorted backends for interfacing with your particular hardware.
18 """
19
20
21 def _import_by_name(modname):
22     """
23     >>> mod = _import_by_name('pypid.backend.melcor')
24     >>> 'MelcorBackend' in dir(mod)
25     True
26     >>> _import_by_name('pypid.backend.highly_unlikely')
27     Traceback (most recent call last):
28       ...
29     ImportError: No module named highly_unlikely
30     """
31     module = __import__(modname)
32     components = modname.split('.')
33     for comp in components[1:]:
34         module = getattr(module, comp)
35     return module
36
37 def get_backend(name):
38     n = '%s.%s' % (__name__, name)
39     mod = _import_by_name(n)
40     for attr in dir(mod):
41         obj = getattr(mod, attr)
42         try:
43             if obj != Backend and issubclass(obj, Backend):
44                 return obj
45         except TypeError:
46             pass
47     raise ValueError(name)
48
49
50 class Backend (object):
51     """Process-control backend
52
53     There are several common forms for a PID control formula.  For the
54     purpose of setting heating and cooling gains (`.get_*_gains()` and
55     `.set_*_gains()`), we'll use the standard form::
56
57       MV(t) = K_p ( e(t) + 1/T_i \int_0^t e(\tau) d\tau + T_d de(t)/dt )
58
59     where `e(t) = SP - PV` is the error function, MV is the
60     manipulated variable, SP is the setpoint, and PV is the process
61     variable.
62
63     In this formulation, the parameter units will be:
64
65     * K_p: MV-units/PV-units (e.g. amp/K for a thermoelectric
66       controller).  Don't confuse this `proportional gain` with the
67       `process gain` used in `TestBackend`.
68     * T_i, T_d: time (e.g. seconds)
69     """
70     pv_units = 'PV-units'
71     mv_units = 'MV-units'
72
73     def __init__(self):
74         self._max_mv = None
75
76     def cleanup(self):
77         "Release resources and disconnect from any hardware."
78         pass
79
80     def get_pv(self):
81         "Return the current process variable in PV-units"
82         raise NotImplementedError()
83
84     def get_ambient_pv(self):
85         "Return the abmient (bath) status in PV-units"
86         raise NotImplementedError()
87
88     def set_max_mv(self, max):
89         "Set the max manipulated variable in MV-units"
90         raise NotImplementedError()
91
92     def get_max_mvt(self):
93         "Get the max manipulated variable MV-units"
94         raise NotImplementedError()
95
96     def get_mv(self):
97         """Return the calculated manipulated varaible in MV-units
98
99         The returned current is not the actual MV, but the MV that the
100         controller calculates it should generate.  For example, if the
101         voltage required to generate an MV current exceeds the
102         controller's max voltage, then the physical current will be
103         less than the value returned here.
104         """
105         raise NotImplementedError()
106
107     def get_modes(self):
108         "Return a list of control modes supported by this backend"
109         raise NotImplementedError()
110
111     def get_mode(self):
112         "Return the current control mode"
113         raise NotImplementedError()
114
115     def set_mode(self, mode):
116         "Set the current control mode"
117         raise NotImplementedError
118
119     def dump_configuration(self):
120         """
121         """
122         raise NotImplementedError()
123
124     def restore_configuration(self):
125         """
126         """
127         raise NotImplementedError()
128
129
130 class ManualMixin (object):
131     def set_mv(self, current):
132         "Set the desired manipulated variable in MV-units"
133         raise NotImplementedError()
134
135
136 class PIDMixin (object):
137     def set_setpoint(self, setpoint):
138         "Set the process variable setpoint in PV-units"
139         raise NotImplementedError()
140         
141     def get_setpoint(self, setpoint):
142         "Get the process variable setpoint in PV-units"
143         raise NotImplementedError()
144
145     def get_duwn_gains(self):
146         """..."""
147         raise NotImplementedError()
148
149     def set_down_gains(self, proportional=None, integral=None,
150                        derivative=None):
151         """
152         ...
153         """
154         raise NotImplementedError()
155
156     def get_up_gains(self):
157         """..."""
158         raise NotImplementedError()
159
160     def set_up_gains(self, proportional=None, integral=None, derivative=None):
161         """
162         ...
163         """
164         raise NotImplementedError()
165
166     def get_feedback_terms(self):
167         """Experimental
168         """
169         raise NotImplementedError()
170
171     def clear_integral_term(self):
172         """Reset the integral feedback turn (removing integrator windup)
173
174         Because the proportional term provides no control signal when
175         the system exactly matches the setpoint, a P-only algorithm
176         will tend to "droop" off the setpoint.  The equlibrium
177         position is one where the droop-generated P term balances the
178         systems temperature leakage.  To correct for this, we add the
179         integral feedback term, which adjusts the control signal to
180         minimize long-term differences between the output and setpoint.
181
182         One issue with the integral term is "integral windup".  When
183         the signal spends a significant time away from the setpoint
184         (e.g. during a long ramp up to operating temperature), the
185         integral term can grow very large, causing overshoot once the
186         output reaches the setpoint.  To allow our controller to avoid
187         this, this method manually clears the intergal term for the
188         backend.
189         """
190         raise NotImplementedError()
191
192
193 class TemperatureMixin (object):
194     @staticmethod
195     def _convert_F_to_C(F):
196         return (F - 32)/1.8
197
198     @staticmethod
199     def _convert_C_to_F(C):
200         return C*1.8 + 32