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