diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sitesurvey.py	Wed Sep 21 15:00:24 2011 +0930
@@ -0,0 +1,265 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2011
+#      Daniel O'Connor <darius@dons.net.au>.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+
+import calendar
+import ConfigParser
+import datetime
+import exceptions
+import numpy
+import os
+import scpi
+import specan
+import sys
+import time
+
+defaults = {}
+confname = "sitesurvey.ini"
+confpaths = [ ".", os.path.dirname(os.path.realpath(sys.argv[0]))]
+
+class Experiment(object):
+    def __init__(self, conf, name):
+        if not conf.has_section(name):
+            raise exceptions.KeyError("No section for experiment " + name)
+
+        self.name = name
+        self.recurrence = None
+        self.opts = {}
+        for k, v in conf.items(name):
+            if k == "recurrence":
+                # In seconds
+                self.recurrence = int(v)
+                continue
+            
+            try:
+                self.opts[k] = int(v)
+            except exceptions.ValueError, e:
+                try:
+                    self.opts[k] = float(v)
+                except exceptions.ValueError, e:
+                    self.opts[k] = v
+
+        if self.recurrence == None:
+            raise exceptions.KeyError("Mandatory parameter 'recurrence' is missing")
+        self.recurrence = datetime.timedelta(seconds = self.recurrence)
+
+        self.last_run = None
+
+    def __repr__(self):
+        return "<" + self.name + ">"
+
+class CalFile(object):
+    def __init__(self, fname):
+        f = file(fname)
+        if f.readline().strip() != "Frequencies":
+            raise exceptions.SyntaxError("Format of cal file incorrect (frequencies header missing)")
+        freqs = numpy.fromstring(f.readline().strip(), sep = ',')
+        if f.readline().strip() != "Gain":
+            raise exceptions.SyntaxError("Format of cal file incorrect (gains header missing)")
+        gains = numpy.fromstring(f.readline().strip(), sep = ',')
+        if len(gains) != len(freqs):
+            raise exceptions.SyntaxError("Format of cal file incorrect (length of gain and freqs aren't equal)")
+
+        self.calfreqs = freqs
+        self.calgains = gains
+
+    def interp(self, freqs):
+        '''Interoplate the calibration over freqs and return an array'''
+        deltas = numpy.zeros(freqs.shape)
+
+        for i in range(len(freqs)):
+            if freqs[i] < self.calfreqs[0] or freqs[i] > self.calfreqs[-1]:
+                raise exceptions.SyntaxError("Frequency %.1f is out of range of calibration %f - %f" % (f, calfreqs[0], calfreqs[-1]))
+
+            # Find idx such that calfreqs[idx - 1] < freqs[i] <= calfreqs[idx]
+            idx = self.calfreqs.searchsorted(freqs[i])
+            sf = (freqs[i] - self.calfreqs[idx - 1]) / (self.calfreqs[idx] - self.calfreqs[idx - 1])
+            delta = ((self.calgains[idx] - self.calgains[idx - 1]) * sf) + self.calgains[idx]
+            deltas[i] = delta
+        return deltas
+
+
+def getexpt(sequence):
+    '''Given a sequence return the experiment which should be run next and how long until it should start'''
+
+    now = datetime.datetime.utcnow()
+    #print "now is " + str(now)
+    soonestdly = None
+    soonestexp = None
+    
+    for e in sequence:
+        #print "Looking at " + str(e)
+        # If an experiment has ever run do it now
+        if e.last_run == None:
+            return e, datetime.timedelta(0)
+
+        # Time until this experiment should be run
+        nextrun = e.last_run + e.recurrence
+        dly = nextrun - now
+        #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())
+        # Haven't looked at an experiment yet or this one is sooner
+        if soonestdly == None or dly < soonestdly:
+            #print "sooner"
+            soonestdly = dly
+            soonestexp = e
+
+    if soonestdly < datetime.timedelta(0):
+        #print "Capping " + e.name + " to run now"
+        soonestdly = datetime.timedelta(0)
+
+    #print "Returning " + str(soonestexp)
+    return soonestexp, soonestdly
+
+def getsweep(inst, conf):
+    print " Sending configuration"
+
+    for k in conf:
+        #time.sleep(0.3)
+        inst.setconf(k, conf[k])
+
+    # Otherwise the R&S doens't respond..
+    #time.sleep(0.3)
+    rconf = inst.dumpconf()
+    fstart = rconf['fstart']
+    fstop = rconf['fstop']
+    print " Configuration is " + str(rconf)
+    
+    print " Fetching trace"
+    yaxis = inst.gettrace()
+    xaxis = numpy.arange(fstart, fstop, (fstop - fstart) / yaxis.shape[0])
+
+    return xaxis, yaxis, rconf
+
+def savesweep(fname, exp, x, y):
+    f = open(fname, 'wb')
+    for k in exp:
+        f.write("%s %s\n" % (k.upper(), str(exp[k])))
+    f.write("XDATA ")
+    numpy.savetxt(f, [x], delimiter = ', ', fmt = '%.3f') # Produces a trailing \n
+    f.write("YDATA ")
+    numpy.savetxt(f, [y], delimiter = ', ', fmt = '%.3f')
+    del f
+
+def total_seconds(td):
+    return (td.microseconds + (td.seconds + td.days * 24.0 * 3600.0) * 10.0**6) / 10.0**6
+
+if __name__ == '__main__':
+    # Read in config file(s)
+    conf = ConfigParser.SafeConfigParser(defaults)
+    r = conf.read(map(lambda a: os.path.join(a, confname), confpaths))
+    if len(r) == 0:
+        print "Unable to find any configuration file(s)"
+        sys.exit(1)
+
+    if not conf.has_section('general'):
+        print "Configuration file doesn't have a 'general' section"
+        sys.exit(1)
+
+    if not conf.has_option('general', 'url'):
+        print "Configuration file doesn't have a 'url' option in the 'general' section"
+        sys.exit(1)
+
+    if not conf.has_option('general', 'type'):
+        print "Configuration file doesn't have a 'type' option in the 'general' section"
+        sys.exit(1)
+
+    if not conf.has_option('general', 'sequence'):
+        print "Configuration file doesn't have a 'sequence' option in the 'general' section"
+        sys.exit(1)
+
+    if not conf.has_option('general', 'fname'):
+        print "Configuration file doesn't have a 'fname' option in the 'general' section"
+        sys.exit(1)
+
+    if conf.has_option('general', 'ampcal'):
+        ampcal = CalFile(conf.get('general', 'ampcal'))
+    else:
+        ampcal = None
+
+    if conf.has_option('general', 'antcal'):
+        antcal = CalFile(conf.get('general', 'antcal'))
+    else:
+        antcal = None
+
+
+    sequence = []
+    seqnames = conf.get('general', 'sequence').split()
+    for e in seqnames:
+        sequence.append(Experiment(conf, e))
+                        
+    url = conf.get('general', 'url')
+    insttype = conf.get('general', 'type')
+    fnamefmt = conf.get('general', 'fname')
+    
+    # Connect to the instrument
+    print "Connecting to " + url
+    con = scpi.instURL(url)
+    con.write("*IDN?")
+    idn = con.read()
+    print "Instrument is a " + idn
+
+    # Get class for this instrument & instantiate it
+    inst = specan.getInst(insttype)(con)
+
+    while True:
+        # Find the next experiment to run
+        exp, dly = getexpt(sequence)
+
+        # Sleep if necessary
+        dly = total_seconds(dly)
+        if dly > 1:
+            print "Sleeping for %.1f seconds" % (dly)
+            time.sleep(dly)
+
+        # Run it
+        print "--> Running experiment " + str(exp)
+        freqs, power, opts = getsweep(inst, exp.opts)
+
+        # Adjust power based on amplifier and antenna calibration
+        if ampcal != None:
+            adj = ampcal.interp(freqs)
+            power = power - adj
+
+        if antcal != None:
+            adj = antcal.interp(freqs)
+            power = power - adj
+
+        # Update last run time
+        exp.last_run = datetime.datetime.utcnow()
+
+        # Add some informative params
+        tsepoch = calendar.timegm(exp.last_run.utctimetuple())
+        
+        extras = { 'timestamp' : exp.last_run,
+                   'timestamp_hex' : '%08x' % (tsepoch),
+                   'timestamp_dec' : '%d' % (tsepoch),
+                   'tag' : exp.name,
+                   }
+        opts = dict(opts.items() + extras.items())
+        fname = fnamefmt % opts
+
+        # Save data
+        savesweep(fname, opts, freqs, power)