Mercurial > ~darius > hgwebdir.cgi > pyinst
comparison sitesurvey.py @ 31:c6c86dcb54ba
Add code to automate a sitesurvey (to some degree).
author | Daniel O'Connor <darius@dons.net.au> |
---|---|
date | Wed, 21 Sep 2011 15:00:24 +0930 |
parents | |
children | 660a2997e720 |
comparison
equal
deleted
inserted
replaced
30:9ce709b7da4b | 31:c6c86dcb54ba |
---|---|
1 #!/usr/bin/env python | |
2 | |
3 # Copyright (c) 2011 | |
4 # Daniel O'Connor <darius@dons.net.au>. All rights reserved. | |
5 # | |
6 # Redistribution and use in source and binary forms, with or without | |
7 # modification, are permitted provided that the following conditions | |
8 # are met: | |
9 # 1. Redistributions of source code must retain the above copyright | |
10 # notice, this list of conditions and the following disclaimer. | |
11 # 2. Redistributions in binary form must reproduce the above copyright | |
12 # notice, this list of conditions and the following disclaimer in the | |
13 # documentation and/or other materials provided with the distribution. | |
14 # | |
15 # THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND | |
16 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
17 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
18 # ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE | |
19 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
20 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |
21 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
22 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |
23 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | |
24 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | |
25 # SUCH DAMAGE. | |
26 # | |
27 | |
28 import calendar | |
29 import ConfigParser | |
30 import datetime | |
31 import exceptions | |
32 import numpy | |
33 import os | |
34 import scpi | |
35 import specan | |
36 import sys | |
37 import time | |
38 | |
39 defaults = {} | |
40 confname = "sitesurvey.ini" | |
41 confpaths = [ ".", os.path.dirname(os.path.realpath(sys.argv[0]))] | |
42 | |
43 class Experiment(object): | |
44 def __init__(self, conf, name): | |
45 if not conf.has_section(name): | |
46 raise exceptions.KeyError("No section for experiment " + name) | |
47 | |
48 self.name = name | |
49 self.recurrence = None | |
50 self.opts = {} | |
51 for k, v in conf.items(name): | |
52 if k == "recurrence": | |
53 # In seconds | |
54 self.recurrence = int(v) | |
55 continue | |
56 | |
57 try: | |
58 self.opts[k] = int(v) | |
59 except exceptions.ValueError, e: | |
60 try: | |
61 self.opts[k] = float(v) | |
62 except exceptions.ValueError, e: | |
63 self.opts[k] = v | |
64 | |
65 if self.recurrence == None: | |
66 raise exceptions.KeyError("Mandatory parameter 'recurrence' is missing") | |
67 self.recurrence = datetime.timedelta(seconds = self.recurrence) | |
68 | |
69 self.last_run = None | |
70 | |
71 def __repr__(self): | |
72 return "<" + self.name + ">" | |
73 | |
74 class CalFile(object): | |
75 def __init__(self, fname): | |
76 f = file(fname) | |
77 if f.readline().strip() != "Frequencies": | |
78 raise exceptions.SyntaxError("Format of cal file incorrect (frequencies header missing)") | |
79 freqs = numpy.fromstring(f.readline().strip(), sep = ',') | |
80 if f.readline().strip() != "Gain": | |
81 raise exceptions.SyntaxError("Format of cal file incorrect (gains header missing)") | |
82 gains = numpy.fromstring(f.readline().strip(), sep = ',') | |
83 if len(gains) != len(freqs): | |
84 raise exceptions.SyntaxError("Format of cal file incorrect (length of gain and freqs aren't equal)") | |
85 | |
86 self.calfreqs = freqs | |
87 self.calgains = gains | |
88 | |
89 def interp(self, freqs): | |
90 '''Interoplate the calibration over freqs and return an array''' | |
91 deltas = numpy.zeros(freqs.shape) | |
92 | |
93 for i in range(len(freqs)): | |
94 if freqs[i] < self.calfreqs[0] or freqs[i] > self.calfreqs[-1]: | |
95 raise exceptions.SyntaxError("Frequency %.1f is out of range of calibration %f - %f" % (f, calfreqs[0], calfreqs[-1])) | |
96 | |
97 # Find idx such that calfreqs[idx - 1] < freqs[i] <= calfreqs[idx] | |
98 idx = self.calfreqs.searchsorted(freqs[i]) | |
99 sf = (freqs[i] - self.calfreqs[idx - 1]) / (self.calfreqs[idx] - self.calfreqs[idx - 1]) | |
100 delta = ((self.calgains[idx] - self.calgains[idx - 1]) * sf) + self.calgains[idx] | |
101 deltas[i] = delta | |
102 return deltas | |
103 | |
104 | |
105 def getexpt(sequence): | |
106 '''Given a sequence return the experiment which should be run next and how long until it should start''' | |
107 | |
108 now = datetime.datetime.utcnow() | |
109 #print "now is " + str(now) | |
110 soonestdly = None | |
111 soonestexp = None | |
112 | |
113 for e in sequence: | |
114 #print "Looking at " + str(e) | |
115 # If an experiment has ever run do it now | |
116 if e.last_run == None: | |
117 return e, datetime.timedelta(0) | |
118 | |
119 # Time until this experiment should be run | |
120 nextrun = e.last_run + e.recurrence | |
121 dly = nextrun - now | |
122 #print "Last run was at %s, nextrun at %s, rec = %s, dly = %s / %f" % (str(e.last_run), str(nextrun), str(e.recurrence), str(dly), dly.total_seconds()) | |
123 # Haven't looked at an experiment yet or this one is sooner | |
124 if soonestdly == None or dly < soonestdly: | |
125 #print "sooner" | |
126 soonestdly = dly | |
127 soonestexp = e | |
128 | |
129 if soonestdly < datetime.timedelta(0): | |
130 #print "Capping " + e.name + " to run now" | |
131 soonestdly = datetime.timedelta(0) | |
132 | |
133 #print "Returning " + str(soonestexp) | |
134 return soonestexp, soonestdly | |
135 | |
136 def getsweep(inst, conf): | |
137 print " Sending configuration" | |
138 | |
139 for k in conf: | |
140 #time.sleep(0.3) | |
141 inst.setconf(k, conf[k]) | |
142 | |
143 # Otherwise the R&S doens't respond.. | |
144 #time.sleep(0.3) | |
145 rconf = inst.dumpconf() | |
146 fstart = rconf['fstart'] | |
147 fstop = rconf['fstop'] | |
148 print " Configuration is " + str(rconf) | |
149 | |
150 print " Fetching trace" | |
151 yaxis = inst.gettrace() | |
152 xaxis = numpy.arange(fstart, fstop, (fstop - fstart) / yaxis.shape[0]) | |
153 | |
154 return xaxis, yaxis, rconf | |
155 | |
156 def savesweep(fname, exp, x, y): | |
157 f = open(fname, 'wb') | |
158 for k in exp: | |
159 f.write("%s %s\n" % (k.upper(), str(exp[k]))) | |
160 f.write("XDATA ") | |
161 numpy.savetxt(f, [x], delimiter = ', ', fmt = '%.3f') # Produces a trailing \n | |
162 f.write("YDATA ") | |
163 numpy.savetxt(f, [y], delimiter = ', ', fmt = '%.3f') | |
164 del f | |
165 | |
166 def total_seconds(td): | |
167 return (td.microseconds + (td.seconds + td.days * 24.0 * 3600.0) * 10.0**6) / 10.0**6 | |
168 | |
169 if __name__ == '__main__': | |
170 # Read in config file(s) | |
171 conf = ConfigParser.SafeConfigParser(defaults) | |
172 r = conf.read(map(lambda a: os.path.join(a, confname), confpaths)) | |
173 if len(r) == 0: | |
174 print "Unable to find any configuration file(s)" | |
175 sys.exit(1) | |
176 | |
177 if not conf.has_section('general'): | |
178 print "Configuration file doesn't have a 'general' section" | |
179 sys.exit(1) | |
180 | |
181 if not conf.has_option('general', 'url'): | |
182 print "Configuration file doesn't have a 'url' option in the 'general' section" | |
183 sys.exit(1) | |
184 | |
185 if not conf.has_option('general', 'type'): | |
186 print "Configuration file doesn't have a 'type' option in the 'general' section" | |
187 sys.exit(1) | |
188 | |
189 if not conf.has_option('general', 'sequence'): | |
190 print "Configuration file doesn't have a 'sequence' option in the 'general' section" | |
191 sys.exit(1) | |
192 | |
193 if not conf.has_option('general', 'fname'): | |
194 print "Configuration file doesn't have a 'fname' option in the 'general' section" | |
195 sys.exit(1) | |
196 | |
197 if conf.has_option('general', 'ampcal'): | |
198 ampcal = CalFile(conf.get('general', 'ampcal')) | |
199 else: | |
200 ampcal = None | |
201 | |
202 if conf.has_option('general', 'antcal'): | |
203 antcal = CalFile(conf.get('general', 'antcal')) | |
204 else: | |
205 antcal = None | |
206 | |
207 | |
208 sequence = [] | |
209 seqnames = conf.get('general', 'sequence').split() | |
210 for e in seqnames: | |
211 sequence.append(Experiment(conf, e)) | |
212 | |
213 url = conf.get('general', 'url') | |
214 insttype = conf.get('general', 'type') | |
215 fnamefmt = conf.get('general', 'fname') | |
216 | |
217 # Connect to the instrument | |
218 print "Connecting to " + url | |
219 con = scpi.instURL(url) | |
220 con.write("*IDN?") | |
221 idn = con.read() | |
222 print "Instrument is a " + idn | |
223 | |
224 # Get class for this instrument & instantiate it | |
225 inst = specan.getInst(insttype)(con) | |
226 | |
227 while True: | |
228 # Find the next experiment to run | |
229 exp, dly = getexpt(sequence) | |
230 | |
231 # Sleep if necessary | |
232 dly = total_seconds(dly) | |
233 if dly > 1: | |
234 print "Sleeping for %.1f seconds" % (dly) | |
235 time.sleep(dly) | |
236 | |
237 # Run it | |
238 print "--> Running experiment " + str(exp) | |
239 freqs, power, opts = getsweep(inst, exp.opts) | |
240 | |
241 # Adjust power based on amplifier and antenna calibration | |
242 if ampcal != None: | |
243 adj = ampcal.interp(freqs) | |
244 power = power - adj | |
245 | |
246 if antcal != None: | |
247 adj = antcal.interp(freqs) | |
248 power = power - adj | |
249 | |
250 # Update last run time | |
251 exp.last_run = datetime.datetime.utcnow() | |
252 | |
253 # Add some informative params | |
254 tsepoch = calendar.timegm(exp.last_run.utctimetuple()) | |
255 | |
256 extras = { 'timestamp' : exp.last_run, | |
257 'timestamp_hex' : '%08x' % (tsepoch), | |
258 'timestamp_dec' : '%d' % (tsepoch), | |
259 'tag' : exp.name, | |
260 } | |
261 opts = dict(opts.items() + extras.items()) | |
262 fname = fnamefmt % opts | |
263 | |
264 # Save data | |
265 savesweep(fname, opts, freqs, power) |