e5b499bd74ba5c46ba4ea5d189a8acdb8d3f9f72
[pypid.git] / pypid / backend / __init__.py
1 # Copyright (C) 2008-2011 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of pypid.
4 #
5 # pypid 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 # pypid 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 pypid.  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('pypid.backend.melcor')
26     >>> 'MelcorBackend' in dir(mod)
27     True
28     >>> _import_by_name('pypid.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     """Process-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 for a thermoelectric
68       controller).  Don't confuse this `proportional gain` with the
69       `process gain` used in `TestBackend`.
70     * T_i, T_d: time (e.g. seconds)
71     """
72     pv_units = 'PV-units'
73     mv_units = 'MV-units'
74
75     def __init__(self):
76         self._max_mv = None
77
78     def cleanup(self):
79         "Release resources and disconnect from any hardware."
80         pass
81
82     def get_pv(self):
83         "Return the current process variable in PV-units"
84         raise NotImplementedError()
85
86     def get_ambient_pv(self):
87         "Return the abmient (bath) status in PV-units"
88         raise NotImplementedError()
89
90     def set_max_mv(self, max):
91         "Set the max manipulated variable in MV-units"
92         raise NotImplementedError()
93
94     def get_max_mvt(self):
95         "Get the max manipulated variable MV-units"
96         raise NotImplementedError()
97
98     def get_mv(self):
99         """Return the calculated manipulated varaible in MV-units
100
101         The returned current is not the actual MV, but the MV that the
102         controller calculates it should generate.  For example, if the
103         voltage required to generate an MV current exceeds the
104         controller's max voltage, then the physical current will be
105         less than the value returned here.
106         """
107         raise NotImplementedError()
108
109     def get_modes(self):
110         "Return a list of control modes supported by this backend"
111         raise NotImplementedError()
112
113     def get_mode(self):
114         "Return the current control mode"
115         raise NotImplementedError()
116
117     def set_mode(self, mode):
118         "Set the current control mode"
119         raise NotImplementedError
120
121     def dump_configuration(self):
122         """
123         """
124         raise NotImplementedError()
125
126     def restore_configuration(self):
127         """
128         """
129         raise NotImplementedError()
130
131
132 class ManualMixin (object):
133     def set_mv(self, current):
134         "Set the desired manipulated variable in MV-units"
135         raise NotImplementedError()
136
137
138 class PIDMixin (object):
139     def set_setpoint(self, setpoint):
140         "Set the process variable setpoint in PV-units"
141         raise NotImplementedError()
142         
143     def get_setpoint(self, setpoint):
144         "Get the process variable setpoint in PV-units"
145         raise NotImplementedError()
146
147     def get_duwn_gains(self):
148         """..."""
149         raise NotImplementedError()
150
151     def set_down_gains(self, proportional=None, integral=None,
152                        derivative=None):
153         """
154         ...
155         """
156         raise NotImplementedError()
157
158     def get_up_gains(self):
159         """..."""
160         raise NotImplementedError()
161
162     def set_up_gains(self, proportional=None, integral=None, derivative=None):
163         """
164         ...
165         """
166         raise NotImplementedError()
167
168     def get_feedback_terms(self):
169         """Experimental
170         """
171         raise NotImplementedError()
172
173     def clear_integral_term(self):
174         """Reset the integral feedback turn (removing integrator windup)
175
176         Because the proportional term provides no control signal when
177         the system exactly matches the setpoint, a P-only algorithm
178         will tend to "droop" off the setpoint.  The equlibrium
179         position is one where the droop-generated P term balances the
180         systems temperature leakage.  To correct for this, we add the
181         integral feedback term, which adjusts the control signal to
182         minimize long-term differences between the output and setpoint.
183
184         One issue with the integral term is "integral windup".  When
185         the signal spends a significant time away from the setpoint
186         (e.g. during a long ramp up to operating temperature), the
187         integral term can grow very large, causing overshoot once the
188         output reaches the setpoint.  To allow our controller to avoid
189         this, this method manually clears the intergal term for the
190         backend.
191         """
192         raise NotImplementedError()
193
194
195 class TemperatureMixin (object):
196     @staticmethod
197     def _convert_F_to_C(F):
198         return (F - 32)/1.8
199
200     @staticmethod
201     def _convert_C_to_F(C):
202         return C*1.8 + 32