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)