Rewrite with a more modular structure.
[pypid.git] / tempcontrol / test.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 "Basic testing for `Controller`\s and `Backend`\s"
20
21 import time as _time
22
23 from . import LOG as _LOG
24 from .backend import get_backend as _get_backend
25 from controller import Controller as _Controller
26
27
28 def test_backend(backend=None):
29     internal_backend = False
30     if not backend:
31         internal_backend = True
32         backend = _get_backend('test')()
33     try:
34         sp = backend.get_setpoint()
35         _LOG.info('temperature = {:n} C'.format(backend.get_temp()))
36         _LOG.info('setpoint    = {:n} C'.format(sp))
37         _LOG.info('current     = {:n} A'.format(backend.get_current()))
38
39         _set_and_check_setpoint(backend=backend, setpoint=5.0)
40         _check_max_current(backend=backend)
41         _set_and_check_setpoint(backend=backend, setpoint=50.0)
42         _check_max_current(backend=backend)
43         _set_and_check_setpoint(backend=backend, setpoint=sp)
44     finally:
45         if internal_backend:
46             backend.cleanup()
47
48 def _set_and_check_setpoint(backend, setpoint):
49     _LOG.info('setting setpoint to {:n} C'.format(setpoint))
50     c.set_setpoint(setpoint)
51     sp = c.get_setpoint()
52     _LOG.info('setpoint    = {:n} C'.format(sp))
53     if sp != setpoint:
54         msg = 'read setpoint {:n} != written setpoint {:n}'.format(
55             sp, setpoint)
56         _LOG.error(msg)
57         raise Exception(msg)
58
59 def _check_max_current(backend):
60     # give the backend some time to overcome any integral gain
61     _time.sleep(10)
62     cur = c.get_current()
63     _LOG.info('current     = {:n} A'.format(cur))
64     mcur = c.get_max_current()
65     if cur != mcur:
66         temp = backend.get_temp()
67         sp = backend.get_setpoint()
68         msg = ('current of {:n} A is not the max {:n} A, but the system is '
69                'at {:n} C while the setpoint is at {:n}').format(
70             cur, mcur, temp, sp)
71         _LOG.error(msg)
72         raise Exception(msg)
73
74 def test_controller_step_response(backend=None, setpoint=25):
75     internal_backend = False
76     if not backend:
77         internal_backend = True
78         backend = _get_backend('test')()
79     try:
80         backend.set_mode('PID')
81         c = _Controller(backend=backend)
82         max_current = backend.get_max_current()
83         current_a = 0.4 * max_current
84         current_b = 0.5 * max_current
85         step_response = c.get_step_response(
86             current_a=current_a, current_b=current_b, tolerance=0.5, stable_time=4.)
87         if True:
88             with open('step_response.dat', 'w') as d:
89                 s = step_response[0][0]
90                 for t,T in step_response:
91                     d.write('{:n}\t{:n}\n'.format(t-s, T))
92         gain,dead_time,tau,max_rate = c.analyze_step_response(
93             step_response, current_shift=current_b-current_a)
94         _LOG.debug(('step response: dead time {:n}, gain {:n}, tau {:n}, '
95                     'max-rate {:n}').format(dead_time, gain, tau, max_rate))
96         for name,response_fn,modes in [
97             ('Zeigler-Nichols', c.ziegler_nichols_step_response,
98              ['P', 'PI', 'PID']),
99             ('Cohen-Coon', c.cohen_coon_step_response,
100              ['P', 'PI', 'PID']), # 'PD'
101             ('Wang-Juan-Chan', c.wang_juang_chan_step_response,
102              ['PID']),
103             ]:
104             for mode in modes:
105                 p,i,d = response_fn(
106                     gain=gain, dead_time=dead_time, tau=tau, mode=mode)
107                 _LOG.debug(
108                     '{} step response {}: p {:n}, i {:n}, d {:n}'.format(
109                         name, mode, p, i, d))
110     finally:
111         if internal_backend:
112             backend.cleanup()
113
114 def test_controller_bang_bang_response(backend=None, setpoint=25):
115     internal_backend = False
116     if not backend:
117         internal_backend = True
118         backend = _get_backend('test')(log_stream=open('pid.log', 'w'))
119         # shift our noise-less system off its setpoint
120         backend.set_setpoint(backend.get_temp()+0.1)
121     try:
122         c = _Controller(backend=backend)
123         dead_band = 3*c.estimate_temperature_sensitivity()
124         bang_bang_response = c.get_bang_bang_response(dead_band=dead_band, num_oscillations=4)
125         if True:
126             with open('bang_bang_response.dat', 'w') as d:
127                 s = bang_bang_response[0][0]
128                 for t,T in bang_bang_response:
129                     d.write('{:n}\t{:n}\n'.format(t-s, T))
130         amplitude,period = c.analyze_bang_bang_response(bang_bang_response)
131         _LOG.debug('bang-bang response: amplitude {:n}, period {:n}'.format(
132                 amplitude,period))
133         p,i,d = c.ziegler_nichols_bang_bang_response(
134             amplitude=amplitude, period=period, mode='PID')
135         _LOG.debug(('Zeigler-Nichols bang-bang response: '
136                     'p {:n}, i {:n}, d {:n}').format(p, i, d))
137     finally:
138         if internal_backend:
139             backend.cleanup()