Index: trunk/trunk/base.py
===================================================================
--- trunk/trunk/base.py	(revision 2)
+++ trunk/trunk/base.py	(revision 2)
@@ -0,0 +1,23 @@
+from __future__ import absolute_import
+
+from collections import defaultdict
+import os
+import pprint
+import xlrd
+
+from mako.template import Template
+
+class Base:
+    """
+    """
+
+    @staticmethod
+    def render(context):
+        print 'base.render'
+        link = os.path.join(context['dirname'], 't2-1-%s.html' % context['result'][0])
+        with open(link, 'w') as f:
+            t2_1 = Template(filename='t2-1empty.template')
+            f.write(t2_1.render(**context))
+
+        return os.path.basename(link)
+            
Index: trunk/trunk/beamsgenerator.py
===================================================================
--- trunk/trunk/beamsgenerator.py	(revision 2)
+++ trunk/trunk/beamsgenerator.py	(revision 2)
@@ -0,0 +1,183 @@
+from __future__ import absolute_import
+
+import collections
+import numpy as np
+
+
+class Axis(object):
+    """Class to hold axis information.
+    """
+
+    def __init__(self, data, title='', units=''):
+        self.data = data
+        self.title = title
+        self.units = units
+
+
+class Image(object):
+    """Class to hold 2d image, with axes, units, title, etc.
+    """
+    def __init__(self, data, axes=[], title='', units=''):
+        self.data = data
+        self.axes = axes
+        self.title = title
+        self.units = units
+
+
+class BeamsGenerator(object):
+    """Class to generate the primary beam(s) of the simulated observation.
+    """
+
+    def __init__(self, previous_results):
+        self.previous_results = previous_results
+        self.result = collections.OrderedDict()
+
+    def run(self):
+        print 'BeamsGenerator.run'
+
+        uvmapgen = self.previous_results['uvmapgenerator']
+        fts = self.previous_results['fts']
+        telescope = self.previous_results['loadparameters']['substages']\
+          ['Telescope']
+
+        # number of mirror positions sampled by FTS
+        nsample = fts['ftsnsample']
+        fts_wn = fts['fts_wn']
+        fts_lambda = fts['fts_lambda']
+      
+        # max pixel size (radians) that will fully sample the image.
+        # Sampling freq is 2 * Nyquist freq. So sampling freq = 2 * b_max / lambda_min.
+        bmax = uvmapgen['bmax']
+        self.result['max baseline [m]'] = bmax
+        max_pixsize = np.min(fts_lambda) / (2.0 * bmax)
+        self.result['max pixsize [rad]'] = max_pixsize
+
+        # Number of pixels per beam
+        # radius of first null of Airy disk is theta=1.22lambda/d. Number of
+        # pixels is beam diameter/max_pixsize. Calculate this for the 
+        # longest wavelength, largest beam
+        max_beam_radius = 1.22 * np.max(fts_lambda) /\
+          telescope['Primary mirror diameter']
+        self.result['beam diam [rad]'] = 2.0 * max_beam_radius
+         
+        # go out to radius of first null
+        rpix = int(max_beam_radius / max_pixsize)
+#        rpix = 32
+        npix = 2 * rpix
+        self.result['npix'] = npix
+
+        # spatial axes same for all wavelengths
+        axis = np.arange(-rpix, rpix, dtype=np.float)
+        axis *= max_pixsize
+        axis *= 206264.806247
+        self.result['spatial axis [arcsec]'] = axis
+
+        # calculate beams for each point on fts_wn
+        # oversample uv grid by mult to minimise aliasing
+        mult = 5
+        mx,my = np.meshgrid(np.arange(-mult*rpix, mult*rpix),
+          np.arange(-mult*rpix, mult*rpix))
+        self.result['primary beam'] = collections.OrderedDict()
+        self.result['primary amplitude beam'] = collections.OrderedDict()
+        self.result['dirty beam'] = collections.OrderedDict()
+
+        for wn in fts_wn:
+            # first calculate primary beam
+            #
+            # assume uniform illumination of primary. uv goes out to
+            # +- bmax / lambda_min, fill uv plane appropriate to primary
+            # size
+            mirror = np.ones([mult*npix,mult*npix])
+            lambda_min = 1.0 / (np.max(fts_wn) * 100.0)
+            lamb = 1.0 / (wn * 100.0)
+            mirror[(np.sqrt(mx*mx + my*my) / (mult*rpix)) * 
+              (bmax / lambda_min) > \
+              (0.5 * telescope['Primary mirror diameter'] / lamb)] = 0.0
+
+            # fftshift mirror to be centred at [0,0],
+            # do a 2d ftt of it,
+            # fftshift transform so that 0 freq is in centre of array
+            temp = np.fft.fftshift(mirror)
+            temp = np.fft.fft2(temp)
+            temp = np.fft.fftshift(temp)
+            # convert to real, should be real anyway as FT of symmetric function 
+            # square to give intensity response
+            primary_amplitude_beam = np.real(temp)
+            primary_intensity_beam = np.power(np.abs(temp), 2)
+            # and normalise
+            primary_amplitude_beam /= np.max(primary_amplitude_beam)
+            primary_intensity_beam /= np.max(primary_intensity_beam)
+
+            # truncate primary beam pattern
+            primary_amplitude_beam = primary_amplitude_beam[
+              (mult-1)*rpix:(mult+1)*rpix,
+              (mult-1)*rpix:(mult+1)*rpix]
+            primary_intensity_beam = primary_intensity_beam[
+              (mult-1)*rpix:(mult+1)*rpix,
+              (mult-1)*rpix:(mult+1)*rpix]
+
+            # axes
+            axis1 = Axis(data=-axis, title='RA offset', units='arcsec') 
+            axis2 = Axis(data=axis, title='Dec offset', units='arcsec') 
+            image = Image(data=primary_intensity_beam, axes=[axis1, axis2],
+              title='Primary Beam %06.4g cm-1' % wn)
+            self.result['primary beam'][wn] = image
+
+            image = Image(data=primary_amplitude_beam, axes=[axis1, axis2],
+              title='Amplitude Primary Beam %06.4g cm-1' % wn)
+            self.result['primary amplitude beam'][wn] = image
+
+            # second calculate dirty beam
+            #
+            # 1. uv plane covered by discrete points so should use a
+            # slow dft rather than fft, which requires evenly spaced
+            # samples. Perhaps a bit slow though as calcs done in Python
+            # layer.
+            # 2. Could use shift theorem to give even-spaced equivalent
+            # of each discrete point, then use ffts. Equivalent to 1
+            # in speed.
+            # 3. Oversample uv plane and accept small error in point
+            # positions. Fastest but adequate?
+
+            uv = np.zeros([mult*npix,mult*npix])
+            maxbx = bmax / np.min(fts_lambda)
+            for bxby in uvmapgen['bxby']:
+                ibx = int(round(((bxby[0] / lamb) / maxbx) * mult * rpix))
+                if ibx < 0:
+                    ibx = mult*npix + ibx 
+                iby = int(round(((bxby[1] / lamb) / maxbx) * mult * rpix))
+                if iby < 0:
+                    iby = mult*npix + iby 
+                uv[ibx,iby] = 1.0
+ 
+            temp = np.fft.fftshift(uv)
+            temp = np.fft.fft2(temp)
+            temp = np.fft.fftshift(temp)
+            temp = np.power(temp, 2)
+            # convert to real, should be real anway as FT of symmetric function 
+            dirty_beam = np.abs(temp)
+            # and normalise
+            dirty_beam /= np.max(dirty_beam)
+
+            # truncate dirty beam pattern
+            dirty_beam = dirty_beam[
+              (mult-1)*rpix:(mult+1)*rpix,
+              (mult-1)*rpix:(mult+1)*rpix]
+
+            # axes
+            axis = np.arange(-rpix, rpix, dtype=np.float)
+            axis *= (np.min(fts_lambda) / bmax)
+            axis *= 206264.806247
+            axis1 = Axis(data=-axis, title='RA offset', units='arcsec') 
+            axis2 = Axis(data=axis, title='Dec offset', units='arcsec') 
+            
+            image = Image(data=dirty_beam, axes=[axis1, axis2],
+              title='Dirty Beam %06.4g cm-1' % wn)
+            
+            self.result['dirty beam'][wn] = image
+
+        return self.result
+
+    def __repr__(self):
+        return 'PrimaryBeamGenerator'
+
Index: trunk/trunk/doublefourier.py
===================================================================
--- trunk/trunk/doublefourier.py	(revision 2)
+++ trunk/trunk/doublefourier.py	(revision 2)
@@ -0,0 +1,327 @@
+from __future__ import absolute_import
+
+import collections
+import numpy as np
+import pp
+
+def test(nx=50, ny=50):
+    # NOT WORKING
+    # all wavenumbers are cm-1
+    wn_max = 100.0
+    wn_min = 50.0
+
+    resolution_min = 300.0
+
+    delta_wn = wn_min / resolution_min
+
+    # spectra are evenly sampled from 0 to wn_max with spacing delta_wn
+    nfreq = np.ceil(wn_max / delta_wn)
+    wn_max = delta_wn * nfreq
+
+    freqs = np.arange(nfreq)
+    freqs *= delta_wn
+
+    # construct a dummy 'sky'
+    # numpy default indexing is C-style so that the rightmost index
+    # 'changes the fastest'.
+    sky_s = np.zeros([nfreq, ny, nx], np.float)
+    sky_s[:,ny/2,nx/2] = 1.0
+
+    # x coords in radians
+    arcsec2rad = 1.0 / 206265
+    sky_x = (np.arange(nx) - nx/2) * 1.0 * arcsec2rad 
+    sky_y = (np.arange(ny) - ny/2) * 1.0 * arcsec2rad 
+
+    # dummy baselines (each baseline entry is [u,v] in centimetres)
+    baselines = []
+    for i in range(500):
+        baselines.append([20000.0, 20000.0])
+
+    df = DoubleFourier(sky_s=sky_s, freqs=freqs, sky_x=sky_x, sky_y=sky_y, 
+      baselines=baselines)
+
+    df.matlab_transform()
+
+
+class DoubleFourier(object):
+    """Class to compute interferograms.
+    """
+
+    def __init__(self, parameters, previous_results):
+        self.parameters = parameters
+        self.previous_results = previous_results
+
+        # paralle processing stuff
+        ppservers = ()
+        self.job_server = pp.Server(ppservers=ppservers)
+        print 'DoubleFourier starting pp with %s workers' % \
+          self.job_server.get_ncpus()
+
+        self.result = collections.OrderedDict()      
+
+    def run(self):
+        print 'DoubleFourier.run'
+
+        fts = self.previous_results['fts']
+        frequency_axis = fts['fts_wn']
+        opd_max = fts['opd_max']
+        fts_nsample = fts['ftsnsample']
+        vdrive = fts['vdrive']
+        delta_opd = fts['delta_opd']
+
+        times = np.arange(int(fts_nsample), dtype=np.float)
+        times *= (opd_max / vdrive) / float(fts_nsample-1)
+
+        beamsgenerator = self.previous_results['beamsgenerator']
+
+        uvmapgenerator = self.previous_results['uvmapgenerator']
+        bxby = uvmapgenerator['bxby']
+
+        skygenerator = self.previous_results['skygenerator']
+        skymodel = skygenerator['sky model']
+        spatial_axis = self.result['spatial axis'] = skygenerator['spatial axis']
+        self.result['frequency axis'] = frequency_axis
+
+        # assuming nx is even then transform has 0 freq at origin and [nx/2] is
+        # Nyquist frequency. Nyq freq = 0.5 * Nyquist sampling freq. 
+        # Assume further that the fft is shifted so that 0 freq is at nx/2
+        nx = len(spatial_axis)
+        spatial_freq_axis = np.arange(-nx/2, nx/2, dtype=np.float)
+        sample_freq = (180.0 * 3600.0 / np.pi) / (spatial_axis[1] - spatial_axis[0])
+        spatial_freq_axis *= (sample_freq / nx)
+        self.result['spatial frequency axis'] = spatial_freq_axis
+        
+        self.result['baseline interferograms'] = collections.OrderedDict()
+#        for baseline in bxby:
+        for baseline in bxby[:3]:
+            print baseline
+            measurement = np.zeros(np.shape(times))
+
+            # FTS path diff and possibly baseline itself vary with time
+            for tindex,t in enumerate(times):
+#            for tindex,t in enumerate(times[:1]):
+
+                sky_now = skymodel
+
+                # what is the system looking at?
+                # add 'errors' in order
+
+                # 1. baseline should be perp to centre of field.
+                #    If baseline is tilted then origin of sky map shifts.
+                #    (I think effect could be corrected by changing
+                #    FTS sample position to compensate.?)
+                # for now assume 0 error but do full calculation for timing
+                # purposes
+                baseline_dx = 0.0
+                baseline_dy = 0.0
+
+                # calculate xpos, ypos in units of pixel - numpy arrays 
+                # [row,col]
+                nx = len(spatial_axis)
+                colpos = float(nx-1) * float (baseline_dx - spatial_axis[0]) / \
+                  (spatial_axis[-1] - spatial_axis[0])
+                rowpos = float(nx-1) * float (baseline_dy - spatial_axis[0]) / \
+                  (spatial_axis[-1] - spatial_axis[0])
+
+                if colpos < 0 or colpos > (nx-1) or rowpos < 0 or rowpos > \
+                  (nx-1):
+                    raise Exception, \
+                      'baseline centre outside limits of sky model'
+
+                # calculate fourier phase shift to move point at [0,0] to 
+                # [rowpos, colpos]
+                shiftx = np.zeros([nx], np.complex)
+                shiftx[:nx/2] = np.arange(nx/2, dtype=np.complex)
+                shiftx[nx/2:] = np.arange(-nx/2, 0, dtype=np.complex)
+                shiftx = np.exp((-2.0j * np.pi * colpos * shiftx) / float(nx))
+
+                shifty = np.zeros([nx], np.complex)
+                shifty[:nx/2] = np.arange(nx/2, dtype=np.complex)
+                shifty[nx/2:] = np.arange(-nx/2, 0, dtype=np.complex)
+                shifty = np.exp((-2.0j * np.pi * rowpos * shifty) / float(nx))
+
+                shift = np.ones([nx,nx], np.complex)
+                for j in range(nx):
+                    shift[j,:] *= shiftx
+                for i in range(nx):
+                    shift[:,i] *= shifty
+
+                # go through freq planes and shift them
+                for iwn,wn in enumerate(frequency_axis):
+                    # move centre of image to array origin
+                    temp = np.fft.fftshift(sky_now[:,:,iwn])
+                    # 2d fft
+                    temp = np.fft.fft2(temp)
+                    # apply phase shift
+                    temp *= shift
+                    # transform and shift back
+                    temp = np.fft.ifft2(temp)
+                    temp = np.fft.fftshift(temp)
+#                    imag part should still be 0
+#                    print 'max real', np.max(np.real(temp))
+#                    print 'max imag', np.max(np.imag(temp))
+                    temp = np.abs(temp)
+                    sky_now[:,:,iwn] = np.fft.fftshift(temp)
+
+                if t == times[0]:
+                    self.result['sky at time 0'] = sky_now
+                
+                # 2. telescopes should be centred on centre of field
+                #    Telescopes collect flux from the 'sky' and pass
+                #    it to the FTS beam combiner. In doing this each
+                #    telescope multiplies the sky emission by its
+                #    amplitude beam response - always real but with
+                #    negative areas. Is this correct? Gives right
+                #    answer for 'no error' case.
+
+                # multiply sky by amplitude beam 1 * amplitude beam 2
+                for iwn,wn in enumerate(frequency_axis):
+                    # calculate shifted beams here
+                    # for now assume no errors and just use beam
+                    # calculated earlier
+
+                    amplitude_beam = beamsgenerator['primary amplitude beam']\
+                      [wn]                
+                    amp_beam_1 = amplitude_beam.data
+                    amp_beam_2 = amplitude_beam.data
+                    sky_now[:,:,iwn] *= amp_beam_1 * amp_beam_2 
+
+                if t == times[0]:
+                    self.result['sky*beams at time 0'] = sky_now
+                    
+                # 3. baseline error revisited
+                # derive baseline at this time
+                baseline_error = 0.0
+                baseline_now = baseline + baseline_error
+#                print 'baseline_now', baseline_now
+
+#                gamma_12[(baseline,t)] = 0.0
+                fft_now = np.zeros(np.shape(sky_now), np.complex)
+                spectrum = np.zeros(np.shape(frequency_axis), np.complex)
+                for iwn,wn in enumerate(frequency_axis):
+
+                    # derive shift needed to place baseline at one of FFT coords
+                    # this depends on physical baseline and frequency
+                    baseline_now_lambdas = baseline_now * wn * 100.0 
+#                    print 'baseline lambda', wn, baseline_now_lambdas
+
+                    # calculate baseline position in units of pixels of FFTed sky - 
+                    # numpy arrays [row,col]
+                    colpos = float(nx-1) * float(baseline_now_lambdas[0] - spatial_freq_axis[0]) / \
+                      (spatial_freq_axis[-1] - spatial_freq_axis[0])
+                    rowpos = float(nx-1) * float(baseline_now_lambdas[1] - spatial_freq_axis[0]) / \
+                      (spatial_freq_axis[-1] - spatial_freq_axis[0])
+#                    print 'spatial colpos, rowpos', colpos, rowpos
+
+                    # calculate fourier phase shift to move point at [rowpos,colpos] to 
+                    # [0,0]
+                    shiftx = np.zeros([nx], np.complex)
+                    shiftx[:nx/2] = np.arange(nx/2, dtype=np.complex)
+                    shiftx[nx/2:] = np.arange(-nx/2, 0, dtype=np.complex)
+                    shiftx = np.exp((-2.0j * np.pi * colpos * shiftx) / float(nx))
+
+                    shifty = np.zeros([nx], np.complex)
+                    shifty[:nx/2] = np.arange(nx/2, dtype=np.complex)
+                    shifty[nx/2:] = np.arange(-nx/2, 0, dtype=np.complex)
+                    shifty = np.exp((-2.0j * np.pi * rowpos * shifty) / float(nx))
+
+                    shift = np.ones([nx,nx], np.complex)
+                    for j in range(nx):
+                        shift[j,:] *= shiftx
+                    for i in range(nx):
+                        shift[:,i] *= shifty
+
+                    # move centre of sky image to origin
+                    temp = np.fft.fftshift(sky_now[:,:,iwn])
+                    # apply phase shift
+                    temp *= shift
+                    # 2d fft
+                    temp = np.fft.fft2(temp)
+                    # shift 0 freq to centre of array
+                    fft_now[:,:,iwn] = np.fft.ifftshift(temp)
+
+                    spectrum[iwn] = temp[0,0]
+
+                if t == times[0]:
+                    self.result['skyfft at time 0'] = fft_now
+
+                # 3. FTS sampling should be accurate
+                #    derive lag due to FTS path difference
+                #    0 error for now
+                mirror_error = 0.0
+                opd = 2.0 * (vdrive * t + mirror_error)
+                print 'opd', opd, vdrive, t, mirror_error
+                opd_ipos = opd / delta_opd
+
+                # make spectrum symmetric
+                nfreq = len(frequency_axis)
+                symmetric_spectrum = np.zeros([2*nfreq])
+                symmetric_spectrum[:nfreq] = spectrum
+                symmetric_spectrum[nfreq:] = spectrum[::-1]
+                
+                # calculate shift needed to move point at opd to 0
+                shift = np.zeros([2*nfreq], dtype=np.complex)
+                shift[:nfreq] = np.arange(nfreq, dtype=np.complex)
+                shift[nfreq:] = np.arange(-nfreq, 0, dtype=np.complex)
+                shift = np.exp((-2.0j * np.pi * opd_ipos * shift) / float(nfreq))
+
+                # apply phase shift and fft
+                symmetric_spectrum *= shift
+                spectrum_fft = np.fft.fft(symmetric_spectrum)
+                measurement[tindex] = spectrum_fft[0]
+
+            self.result['baseline interferograms'][tuple(baseline)] = measurement
+
+        return self.result
+
+    def matlab_transform(self):
+        # readers should look at Izumi et al. 2006, Applied Optics, 45, 2576
+        # for theoretical background. Names of variables in the code correspond
+        # to that work.
+
+        # For now, assume 2 light collectors giving one baseline at a time.
+        interferograms = {}
+
+        for baseline in self.baselines:
+            interferogram = 0
+
+            # baseline length (cm) and position angle
+            bu = baseline[0]
+            bv = baseline[1]
+            mod_b = np.sqrt(pow(bu,2) + pow(bv,2))
+            ang_b = np.arctan2(bv, bu)
+
+            # loop over sky pixels covered by primary beam
+            nx = self.sky_s.shape[1]
+            ny = self.sky_s.shape[2]
+            for j in range(ny):
+                for i in range(nx):
+
+                    # inverse fft of emission spectrum at this point
+                    temp = np.fft.ifft(self.sky_s[:,j,i])
+
+                    # move 0 frequency to centre of array
+                    temp = np.fft.fftshift(temp)
+
+                    # length (radians) and position angle of theta vector
+                    mod_theta = np.sqrt(pow(self.sky_x[i],2) + pow(self.sky_y[j],2))
+                    ang_theta = np.arctan2(self.sky_y[j], self.sky_x[i])
+
+                    # calculate b.theta (the projection of b on theta)
+                    # and the corresponding delay in units of wavelength at 
+                    # Nyquist frequency
+                    delay = mod_theta * mod_b * np.cos(ang_b - ang_theta) * self.freqs[-1]
+
+                    # sampling is done at twice Nyquist freq so shift transformed
+                    # spectrum by 2 * delay samples (approximated to nint)
+                    # NOTE factor of 2 discrepency with matlab version! I think
+                    # this is because there the variable 'Nyq' is the Nyquist
+                    # sampling rate, not the Nyquist frequency. 
+                    temp = np.roll(temp, int(round(2.0 * delay)))
+
+                    # want only the real part of the result
+                    interferogram += np.real(temp) 
+
+    def __repr__(self):
+        return 'DoubleFourier'
+
Index: trunk/trunk/fiins.py
===================================================================
--- trunk/trunk/fiins.py	(revision 2)
+++ trunk/trunk/fiins.py	(revision 2)
@@ -0,0 +1,71 @@
+from __future__ import absolute_import
+
+import collections
+
+import beamsgenerator
+import doublefourier
+import fts
+import loadparameters
+import pointingerrors
+import renderer
+import skygenerator
+import uvmapgenerator
+
+
+class Fiins(object):
+    """FISICA simulator.
+    """
+    def __init__(self, sky_spreadsheet='SkySparams.xlsx', sky_sheet='1point'):
+        self.sky_spreadsheet = sky_spreadsheet
+        self.sky_sheet = sky_sheet
+        self.result = collections.OrderedDict()
+
+    def simulate(self):
+        # read parameters
+        loadparams = loadparameters.LoadParameters(
+          sky_spreadsheet=self.sky_spreadsheet,
+          sky_sheet=self.sky_sheet)
+        obs_specification = loadparams.run()
+        self.result['loadparameters'] = obs_specification
+
+        # generate UV map
+        uvmapgen = uvmapgenerator.UVMapGenerator(parameters=obs_specification)
+        self.result['uvmapgenerator'] = uvmapgen.run()
+
+        # generate information on the FTS 
+        ftsd = fts.FTS(parameters=obs_specification)
+        self.result['fts'] = ftsd.run()
+
+        # generate primary beams
+        beamsgen = beamsgenerator.BeamsGenerator(
+          previous_results=self.result)
+        self.result['beamsgenerator'] = beamsgen.run()
+
+        # pointing errors
+        pointingerrs = pointingerrors.PointingErrors(
+          parameters=obs_specification)
+        self.result['pointingerrors'] = pointingerrs.run()
+
+        # construct sky
+        skygen = skygenerator.SkyGenerator(parameters=obs_specification,
+          previous_results=self.result)
+        self.result['skygenerator'] = skygen.run()
+
+        # calculate interferograms
+        doublef = doublefourier.DoubleFourier(parameters=obs_specification,
+          previous_results=self.result)
+        self.result['doublefourier'] = doublef.run()  
+
+        # construct html description of result
+        self.render()
+        
+    def import_result(self):
+        print 'does nothing'
+
+    def render(self):
+        htmlrenderer = renderer.Renderer(result=self.result)
+        htmlrenderer.run()
+
+    def __repr__(self):
+        return 'FISICA simulator'
+
Index: trunk/trunk/fts.py
===================================================================
--- trunk/trunk/fts.py	(revision 2)
+++ trunk/trunk/fts.py	(revision 2)
@@ -0,0 +1,75 @@
+from __future__ import absolute_import
+
+import collections
+import numpy as np
+
+
+class FTS(object):
+    """Class to describe the Fourier Transform Spectrometer.
+    """
+
+    def __init__(self, parameters):
+        self.parameters = parameters
+
+        print parameters['substages']['FTSpectrograph'].keys()
+
+        # read params from FTSpectrograph sheet
+        ftsparm = parameters['substages']['FTSpectrograph']
+
+        self.result = collections.OrderedDict()
+
+        # slightly awkward getting values as they are keyed by 
+        # the row number in the parent spreadsheet
+        row = ftsparm['wn min [cm-1]'].keys()[0]
+        self.result['wnmin'] = wnmin = ftsparm['wn min [cm-1]'][row]
+        self.result['wnmax'] = wnmax = ftsparm['wn max [cm-1]'][row]
+        self.result['specres'] = specres = ftsparm['Spectral Res'][row]
+
+        # read params from FTSMechanical sheet
+        ftsparm = parameters['substages']['FTSMechanical']
+        row = ftsparm['V drive [cm/s]'].keys()[0]
+        self.result['vdrive'] = ftsparm['V drive [cm/s]'][row]
+
+        # derived parameters
+        self.result['delta_wn'] = delta_wn = wnmin / specres
+        self.result['delta_opd'] = 1.0 / (2.0 * wnmax * 100.0)
+        self.result['ftsnsample'] = nsample = np.ceil(wnmax / delta_wn)
+        self.result['opd_max'] = 1.0 / (2.0 * delta_wn)
+
+        # spectral points sampled by FTS (wn=wavenumber[cm-1]
+        # lambda=wavelength[m])
+        i = np.arange(int(nsample))
+        fts_wn = wnmax * i / float(nsample-1)
+        # crop the spectrum below wnmin. This does not seem to be done
+        # in the matlab version.
+        fts_wn = fts_wn[fts_wn > wnmin]
+        fts_lambda = 1.0 / (fts_wn * 100.0)
+
+        self.result['fts_wn'] = fts_wn
+        self.result['fts_lambda'] = fts_lambda
+
+        print 'more to add in FTSDrive'
+
+    def run(self):
+        print 'FTS.run'        
+        return self.result
+
+    def __repr__(self):
+        return '''
+FTS:
+  wnmin    : {wnmin}
+  wnmax    : {wnmax}
+  specres  : {specres}
+  delta wn : {delta_wn}
+  delta opd: {delta_opd}
+  nsample  : {nsample}
+  max opd  : {opd_max}
+'''.format(
+          wnmin=self.result['wnmin'],
+          wnmax=self.result['wnmax'],
+          specres=self.result['specres'],
+          delta_wn=self.result['delta_wn'],
+          delta_opd=self.result['delta_opd'],
+          nsample=self.result['nsample'],
+          opd_max=self.result['opd_max'])
+
Index: trunk/trunk/hogbom.py
===================================================================
--- trunk/trunk/hogbom.py	(revision 2)
+++ trunk/trunk/hogbom.py	(revision 2)
@@ -0,0 +1,130 @@
+import numpy 
+
+def image(himage, psf, dirty, niter, thresh):
+    himage.open(psf)
+    psfdata = himage.getchunk()
+    print 'psf read, max val %s' % numpy.max(psfdata)
+    psfdata /= numpy.max(psfdata)
+    print 'psf normalised'
+
+#    if not dirty: 
+#        sky = numpy.zeros(numpy.shape(psf))
+#        dirty = numpy.zeros(numpy.shape(psf))
+    
+#        sky[70,70]=5000
+#        for i in range(numpy.shape(psf)[0]):
+#            for j in range(numpy.shape(psf)[1]):
+#                if pow((i-20),2) + pow((j-20),2) < 100:
+#                    sky[i,j] = 10000
+
+#        for i in range(numpy.shape(psf)[0]):
+#            for j in range(numpy.shape(psf)[1]):
+#                a1o, a2o=overlapIndices(sky, psf,
+#                                    i+1-sky.shape[0]/2,
+#                                    j+1-sky.shape[1]/2)
+#                dirty[a1o[0]:a1o[1],a1o[2]:a1o[3]]+=psf[a2o[0]:a2o[1],a2o[2]:a2o[3]]*sky[i,j]
+#    else:
+#        himage.open(dirty)
+#        dirtydata = himage.getchunk()
+#    himage.fromarray(outfile='dirty.im', pixels=dirty, overwrite=True) 
+#    print 'dirty read'
+
+    himage.open(dirty)
+    dirtydata = himage.getchunk()
+    print 'dirty read'
+
+    comps, res = hogbom(dirtydata, psfdata, True, 0.05, thresh, niter)
+    print 'clean completed'
+
+    himage.fromarray(outfile='residual.im', pixels=res, overwrite=True) 
+    himage.fromarray(outfile='comps.im', pixels=comps, overwrite=True) 
+    himage.fromarray(outfile='image.im', pixels=comps+res, overwrite=True) 
+    himage.close()
+
+
+# Bojan Nikolic <b.nikolic@mrao.cam.ac.uk>, <bojan@bnikolic.co.uk> 
+# Initial version August 2010
+#
+# This file is part of pydeconv. This work is licensed under GNU GPL
+# V2 (http://www.gnu.org/licenses/gpl.html)
+"""
+Clean based deconvolution, using numpy
+"""
+
+def overlapIndices(a1, a2, 
+                   shiftx, shifty):
+    if shiftx >=0:
+        a1xbeg=shiftx
+        a2xbeg=0
+        a1xend=a1.shape[0]
+        a2xend=a1.shape[0]-shiftx
+    else:
+        a1xbeg=0
+        a2xbeg=-shiftx
+        a1xend=a1.shape[0]+shiftx
+        a2xend=a1.shape[0]
+
+    if shifty >=0:
+        a1ybeg=shifty
+        a2ybeg=0
+        a1yend=a1.shape[1]
+        a2yend=a1.shape[1]-shifty
+    else:
+        a1ybeg=0
+        a2ybeg=-shifty
+        a1yend=a1.shape[1]+shifty
+        a2yend=a1.shape[1]
+
+    return (a1xbeg, a1xend, a1ybeg, a1yend), (a2xbeg, a2xend, a2ybeg, a2yend)
+
+        
+
+def hogbom(dirty,
+           psf,
+           window,
+           gain,
+           thresh,
+           niter):
+    """
+    Hogbom clean
+
+    :param dirty: The dirty image, i.e., the image to be deconvolved
+
+    :param psf: The point spread-function
+
+    :param window: Regions where clean components are allowed. If
+    True, thank all of the dirty image is assumed to be allowed for
+    clean components
+
+    :param gain: The "loop gain", i.e., the fraction of the brightest
+    pixel that is removed in each iteration
+
+    :param thresh: Cleaning stops when the maximum of the absolute
+    deviation of the residual is less than this value
+
+    :param niter: Maximum number of components to make if the
+    threshold "thresh" is not hit
+    """
+    comps=numpy.zeros(dirty.shape)
+    res=numpy.array(dirty)
+    if window is True:
+        window=numpy.ones(dirty.shape,
+                          numpy.bool)
+    for i in range(niter):
+        mx, my=numpy.unravel_index(numpy.fabs(res[window]).argmax(), res.shape)
+        print 'mx, my', mx, my
+        mval=res[mx, my]*gain
+        print 'mval', mval
+        comps[mx, my]+=mval
+        a1o, a2o=overlapIndices(dirty, psf,
+                                mx+1-dirty.shape[0]/2,
+                                my+1-dirty.shape[1]/2)
+#                                mx-dirty.shape[0]/2,
+#                                my-dirty.shape[1]/2)
+        print 'a1o', a1o
+        print 'a2o', a2o
+        res[a1o[0]:a1o[1],a1o[2]:a1o[3]]-=psf[a2o[0]:a2o[1],a2o[2]:a2o[3]]*mval
+        print '59,56', res[59,56]
+        if numpy.fabs(res).max() < thresh:
+            break
+    return comps, res
Index: trunk/trunk/loadparameters.py
===================================================================
--- trunk/trunk/loadparameters.py	(revision 2)
+++ trunk/trunk/loadparameters.py	(revision 2)
@@ -0,0 +1,237 @@
+from __future__ import absolute_import
+
+import collections
+import os
+import pprint
+import xlrd
+
+from mako.lookup import TemplateLookup
+
+class LoadParameters:
+    """Class to compute interferograms.
+    """
+
+    def __init__(self, sky_spreadsheet='Sky.xlsx', sky_sheet='1point',
+      instrument_spreadsheet='FIInS_Instrument_cor3.xlsx'):
+        self.result = collections.OrderedDict()
+        self.result['substages'] = collections.OrderedDict()
+
+        # access the relevant sheet of the spreadsheet
+        book = xlrd.open_workbook(sky_spreadsheet)
+        print 'names', book.sheet_names()
+        sheet = book.sheet_by_name(sky_sheet)
+
+        sheet_dict = collections.OrderedDict()
+
+        # Sheet has names in col 0, values in columns 1 to n
+        # Ignore row 0.
+
+        # read column names
+        for row in range(1,sheet.nrows):
+            colname = sheet.cell(row, 0)
+            sheet_dict[colname.value] = {}
+            for col in range(1,sheet.ncols):
+                sheet_dict[colname.value][col] = sheet.cell(row,col).value
+
+        self.result['substages']['Sky'] = sheet_dict
+
+        # the instrument spreadsheet
+        instrument_book = xlrd.open_workbook(instrument_spreadsheet)
+        for sheet in instrument_book.sheets():
+
+            if sheet.name == 'FTSpectrograph':
+                sheet_dict = collections.OrderedDict()
+
+                # ignore first row of sheet
+                # second row of sheet has column names
+                name_row = 1
+                # subsequent rows have entries, only one of which is
+                # 'selected'
+                select_col = None
+
+                # read column names
+                for col in range(sheet.ncols):
+                    sheet_dict[sheet.cell(name_row,col).value] = \
+                      collections.OrderedDict()
+                    if 'Selection' in sheet.cell(name_row,col).value:
+                        select_col = col
+
+                # sanity check 
+                if select_col is None:
+                    raise Exception, \
+                      'No Selection column in FTSpectrograph spreadsheet'
+        
+                # read entries from first 'selected' row
+                for row in range(name_row+1,sheet.nrows):
+                    if int(sheet.cell(row,select_col).value) > 0:
+                        for col in range(sheet.ncols):
+                            sheet_dict[sheet.cell(name_row,col).value][row] = \
+                              sheet.cell(row,col).value
+                        break
+
+                # resulting dict should contain the entries from the
+                # selected spreadsheet row
+
+                self.result['substages'][sheet.name] = sheet_dict
+
+            elif sheet.name == 'FTSMechanical':
+                sheet_dict = collections.OrderedDict()
+
+                # Sheet has names in col 0, values in col 1.
+                # Ignore row 0.
+                name_col = 0
+                entry_col = 1
+
+                # read column names
+                for row in range(1,sheet.nrows):
+                    sheet_dict[sheet.cell(row,name_col).value] = \
+                      collections.OrderedDict()
+                    sheet_dict[sheet.cell(row,name_col).value][entry_col] = \
+                      sheet.cell(row,entry_col).value
+
+                self.result['substages'][sheet.name] = sheet_dict
+
+            elif sheet.name == 'Telescope':
+                sheet_dict = collections.OrderedDict()
+
+                # Sheet has names in col 0, values in col 1, units in col 3.
+                # Ignore rows 0 and 1.
+                name_col = 0
+                entry_col = 1
+
+                # read column names
+                for row in range(2,sheet.nrows):
+                    sheet_dict[sheet.cell(row,name_col).value] = \
+                      sheet.cell(row,entry_col).value
+
+                self.result['substages'][sheet.name] = sheet_dict
+
+            elif sheet.name == 'Interferometer':
+                sheet_dict = collections.OrderedDict()
+
+                # ignore first 4 rows of sheet
+                # 5th row of sheet has column names
+                name_row = 4
+                # subsequent rows have entries, only one of which is
+                # 'selected'
+                select_col = None
+
+                # read column names
+                for col in range(sheet.ncols):
+                    sheet_dict[sheet.cell(name_row,col).value] = \
+                      collections.OrderedDict()
+                    if 'Select' in sheet.cell(name_row,col).value:
+                        select_col = col
+
+                # sanity check 
+                if select_col is None:
+                    raise Exception, \
+                      'No Selection column in Interferometer spreadsheet'
+        
+                # read entries from first 'selected' row
+                for row in range(name_row+1,sheet.nrows):
+                    select_cell = str(sheet.cell(row,select_col).value)
+                    select_val = 0
+                    try:
+                        select_val = int(float(select_cell))
+                    except:
+                        pass
+           
+                    if select_val == 1:
+                        for col in range(sheet.ncols):
+                            sheet_dict[sheet.cell(name_row,col).value][row] = \
+                              sheet.cell(row,col).value
+                        break
+
+                # resulting dict should contain the entries from the
+                # selected spreadsheet row
+
+                self.result['substages'][sheet.name] = sheet_dict
+
+            elif sheet.name == 'ColdOptics':
+                sheet_dict = collections.OrderedDict()
+
+                # Sheet has names in col 0, values in col 1.
+                # Ignore rows 0 and 1 and final row.
+                # Ignore cols 2 and higher - duplicate information.
+                name_col = 0
+                entry_col = 1
+
+                # read column names
+                for row in range(2,sheet.nrows-1):
+                    sheet_dict[sheet.cell(row,name_col).value] = \
+                      collections.OrderedDict()
+                    sheet_dict[sheet.cell(row,name_col).value][entry_col] = \
+                      sheet.cell(row,entry_col).value
+
+                self.result['substages'][sheet.name] = sheet_dict
+
+            elif sheet.name == 'Background':
+                sheet_dict = collections.OrderedDict()
+
+                # Sheet has names in col 0, values in col 1.
+                # Ignore rows 0 and 1.
+                name_col = 0
+                entry_col = 1
+
+                for row in range(2,sheet.nrows):
+                    sheet_dict[sheet.cell(row,name_col).value] = \
+                      collections.OrderedDict()
+                    sheet_dict[sheet.cell(row,name_col).value][entry_col] = \
+                      sheet.cell(row,entry_col).value
+
+                self.result['substages'][sheet.name] = sheet_dict
+
+            elif sheet.name == 'WarmOptics':
+                sheet_dict = collections.OrderedDict()
+
+                # Sheet has names in col 0, values in col 1.
+                # Ignore rows 0 and 1.
+                name_col = 0
+                entry_col = 1
+
+                for row in range(2,sheet.nrows):
+                    sheet_dict[sheet.cell(row,name_col).value] = \
+                      collections.OrderedDict()
+                    sheet_dict[sheet.cell(row,name_col).value][entry_col] = \
+                      sheet.cell(row,entry_col).value
+
+                self.result['substages'][sheet.name] = sheet_dict
+
+            elif sheet.name == 'Detectors':
+                sheet_dict = collections.OrderedDict()
+
+                # Sheet has names in col 0, values in col 1.
+                # Ignore rows 0 and 1.
+                name_col = 0
+                entry_col = 1
+
+                for row in range(2,sheet.nrows):
+                    sheet_dict[sheet.cell(row,name_col).value] = \
+                      collections.OrderedDict()
+                    sheet_dict[sheet.cell(row,name_col).value][entry_col] = \
+                      sheet.cell(row,entry_col).value
+
+                self.result['substages'][sheet.name] = sheet_dict
+
+            elif sheet.name == 'SimulatorControl':
+                sheet_dict = collections.OrderedDict()
+
+                # Sheet has names in col 0, values in col 1.
+                # Ignore row 0.
+                name_col = 0
+                entry_col = 1
+
+                for row in range(1,sheet.nrows):
+                    sheet_dict[sheet.cell(row,name_col).value] = \
+                      collections.OrderedDict()
+                    sheet_dict[sheet.cell(row,name_col).value][entry_col] = \
+                      sheet.cell(row,entry_col).value
+
+                self.result['substages'][sheet.name] = sheet_dict
+
+            else:
+                print 'loadparameters skipping', sheet.name
+
+    def run(self):
+        return self.result
Index: trunk/trunk/pointingerrors.py
===================================================================
--- trunk/trunk/pointingerrors.py	(revision 2)
+++ trunk/trunk/pointingerrors.py	(revision 2)
@@ -0,0 +1,38 @@
+from __future__ import absolute_import
+
+import collections
+import numpy as np
+
+
+class PointingErrors(object):
+    """Class to generate simulated pointing errors.
+    """
+
+    def __init__(self, parameters):
+        self.parameters = parameters
+        self.result = collections.OrderedDict()
+
+    def run(self):
+        print 'PointingErrors.run'
+        control = self.parameters['substages']['SimulatorControl']
+
+        # row number in the spreadsheet
+        row = control['PointingErrors'].keys()[0]
+        pointingerrors = control['PointingErrors'][row]
+        pointingerrors = int(round(pointingerrors))
+
+        if pointingerrors:
+            print 'ERROR: pointing errors not yet implemented'
+        else:
+            # dummy nbtot for now
+            nbtot = 10
+            pointing_err_rad = np.zeros([nbtot,2])
+            pointing_err_xy = np.zeros([nbtot,2])
+            pointing_err_t1 = np.zeros([nbtot,2])
+            pointing_err_t2 = np.zeros([nbtot,2])
+
+        return self.result
+
+    def __repr__(self):
+        return 'PointingErrors'
+
Index: trunk/trunk/renderer.py
===================================================================
--- trunk/trunk/renderer.py	(revision 2)
+++ trunk/trunk/renderer.py	(revision 2)
@@ -0,0 +1,57 @@
+from __future__ import absolute_import
+
+import collections
+import datetime
+import os
+import shutil
+
+from mako.template import Template
+from mako.lookup import TemplateLookup
+
+class Renderer(object):
+    """Class to render results into html.
+    """
+
+    def __init__(self, result):
+        self.result = result
+
+    def run(self):
+        # make a directory to contain this result
+        now = datetime.datetime.today()        
+        dirname = 'fisica-%s' % (now.strftime('%Y%m%dT%H%M%S'))
+        os.mkdir(dirname)
+
+        # make a resources directory and copy the Bootstrap stuff there
+        shutil.copytree('dist', os.path.join(dirname, 'resources'))
+
+        # copy the css file there too
+        shutil.copy('bootstrap-3.0.3/examples/navbar-fixed-top/navbar-fixed-top.css', dirname)
+
+        # where to find the template files
+        mylookup = TemplateLookup(directories=['templates'])
+
+        # render the simulation steps first
+        for stage in self.result.items():
+            if 'substages' in stage[1].keys():
+                for substage in stage[1]['substages'].items():
+                    print 'rendering', stage[0], substage[0]
+                    context = {'dirname':dirname, 'renderstage':stage[0],
+                      'rendersubstage':substage[0], 'data':self.result}
+
+                    link = os.path.join(context['dirname'], '%s-%s.html' % (stage[0], substage[0]))
+                    with open(link, 'w') as f:
+                        template = mylookup.get_template('%s-%s.html' % (stage[0], substage[0]))
+                        f.write(template.render(**context))
+
+            else:
+                # no substages
+                print 'rendering', stage[0]
+                context = {'mylookup':mylookup, 'dirname':dirname,
+                  'renderstage':stage[0],
+                  'data':self.result}
+
+                link = os.path.join(context['dirname'], '%s.html' % (stage[0]))
+                with open(link, 'w') as f:
+                    template = mylookup.get_template('%s.html' % (stage[0]))
+                    f.write(template.render(**context))
+
Index: trunk/trunk/skygenerator.py
===================================================================
--- trunk/trunk/skygenerator.py	(revision 2)
+++ trunk/trunk/skygenerator.py	(revision 2)
@@ -0,0 +1,169 @@
+from __future__ import absolute_import
+
+import collections
+import math
+import numpy as np
+import scipy.constants as sc
+
+def black_body(temperature, wavenumber):
+    """Function to calculate Planck function. 
+       temperature - Kelvin
+       wavenumber - frequency in cm-1
+    """
+    freq = wavenumber * sc.c * 100.0
+    jnu = 2.0 * sc.h * pow(freq,3) / (pow(sc.c,2) *
+      (math.exp((sc.h * freq) / (sc.k * temperature)) - 1.0))
+
+    return jnu
+
+
+class SkyGenerator(object):
+    """Class to generate a model sky.
+    """
+
+    def __init__(self, parameters, previous_results):
+        self.parameters = parameters
+        self.previous_results = previous_results
+        self.result = collections.OrderedDict()
+
+    def run(self):
+        print 'SkyGenerator.run'
+
+        fts = self.previous_results['fts']
+        frequency_axis = fts['fts_wn']
+        nspec = len(frequency_axis)
+
+        beamsgenerator = self.previous_results['beamsgenerator']
+        npix = beamsgenerator['npix']
+        spatial_axis = beamsgenerator['spatial axis [arcsec]']
+
+        # oversampling factor
+#        oversample = 10
+        oversample = 1
+
+#        print 'creating sky model with spatial dims [%s,%s] spectral dim %s',\
+#          (oversample * npix, oversample * npix, nspec)
+#        print 'spatial oversampling: %s' % oversample
+        skymodel = np.zeros([npix, npix, nspec], np.float)
+
+        sky = self.parameters['substages']['Sky']
+        columns = sky['SourceNum'].keys()
+
+        self.result['sources'] = collections.OrderedDict()
+
+        for column in columns:
+            temp = sky['SourceNum'][column]
+            sourcenum = int(round(temp))
+            if sourcenum not in self.result['sources'].keys():
+                self.result['sources'][sourcenum] = {}
+
+            type = sky['Type'][column]
+
+            temp = sky['x pos [asec]'][column]
+            xpos = float(temp)
+
+            temp = sky['y pos [asec]'][column]
+            ypos = float(temp)
+
+            temp = sky['Temp'][column]
+            temperature = float(temp)
+
+            temp = sky['cutoffmin'][column]
+            cutoffmin = float(temp)
+
+            temp = sky['cutoffmax'][column]
+            cutoffmax = float(temp)
+
+            temp = sky['emissivity'][column]
+            emissivity = float(temp)
+
+            print 'generating source:%s type:%s xpos:%s ypos:%s temperature:%s cutoffmin:%s cutoffmax:%s e:%s' % (
+              sourcenum, type, xpos, ypos, temperature, cutoffmin, cutoffmax,
+              emissivity)
+
+            if type.upper().strip() == 'POINT':
+                print 'creating point source'
+                source_spectrum = self._create_point_source(xpos, ypos,
+                  temperature, 
+                  cutoffmin, cutoffmax, emissivity, skymodel, 
+                  spatial_axis, frequency_axis)
+            else:
+                source_spectrum = None
+                print "source type '%s' not yet implemented" % type
+
+            self.result['sources'][sourcenum]['spectrum'] = source_spectrum
+
+        print 'sources', self.result['sources']
+        self.result['sky model'] = skymodel
+        self.result['spatial axis'] = spatial_axis
+        self.result['frequency axis'] = frequency_axis 
+
+        return self.result
+
+    def _create_point_source(self, xpos, ypos, temperature, cutoffmin,
+      cutoffmax, emissivity, skymodel, spatial_axis, frequency_axis):
+        """Create a point source.
+        """
+
+        # calculate xpos, ypos in units of pixel - numpy arrays [row,col]
+        nx = len(spatial_axis)
+        colpos = float(nx-1) * float (xpos - spatial_axis[0]) / (spatial_axis[-1] - spatial_axis[0])
+        rowpos = float(nx-1) * float (ypos - spatial_axis[0]) / (spatial_axis[-1] - spatial_axis[0])
+
+        if colpos < 0 or colpos > (nx-1) or rowpos < 0 or rowpos > (nx-1):
+            # point source is outside modelled area 
+            return
+
+        # calculate fourier phase shift to move point at [0,0] to [rowpos, colpos]
+        shiftx = np.zeros([nx], np.complex)
+        shiftx[:nx/2] = np.arange(nx/2, dtype=np.complex)
+        shiftx[nx/2:] = np.arange(-nx/2, 0, dtype=np.complex)
+        shiftx = np.exp((-2.0j * np.pi * colpos * shiftx) / float(nx))
+
+        shifty = np.zeros([nx], np.complex)
+        shifty[:nx/2] = np.arange(nx/2, dtype=np.complex)
+        shifty[nx/2:] = np.arange(-nx/2, 0, dtype=np.complex)
+        shifty = np.exp((-2.0j * np.pi * rowpos * shifty) / float(nx))
+
+        shift = np.ones([nx,nx], np.complex)
+        for j in range(nx):
+            shift[j,:] *= shiftx
+        for i in range(nx):
+            shift[:,i] *= shifty
+
+        # calculate black-body spectrum, modified for cutoffs in 
+        # sensitivity and source emissivity
+        bbmod = np.zeros(np.shape(frequency_axis))
+        print 'frequency axis', frequency_axis
+        # ignore floating point errors
+        old_settings = np.seterr(all='ignore')
+        for iwn,wn in enumerate(frequency_axis):
+            # simulate real-life 'rounded' cutoffs numerically  
+            f1 = 1.0 / (1.0 + pow(cutoffmin/wn, 18) + pow(wn/cutoffmax, 24))
+            print pow(cutoffmin/wn,18), pow(wn/cutoffmax,24)
+            print temperature, wn, black_body(temperature, wn), f1, emissivity
+            bbmod[iwn] = black_body(temperature, wn) * f1 * emissivity
+        # restore fp behaviour
+        ignore = np.seterr(**old_settings)
+
+        # go through freq planes and add point source to each
+        for iwn,wn in enumerate(frequency_axis):
+            # create point in frequency space
+            temp = np.zeros([nx,nx])
+            temp[0,0] = bbmod[iwn]
+            # 2d fft
+            temp = np.fft.fft2(temp)
+            # apply phase shift to move point to required offset
+            temp *= shift
+            # transform back
+            temp = np.fft.ifft2(temp)
+            temp = np.real(temp)
+
+            # add to sky model
+            skymodel[:,:,iwn] += temp
+
+        return bbmod
+
+    def __repr__(self):
+        return 'SkyGenerator'
+
Index: trunk/trunk/templates/base.html
===================================================================
--- trunk/trunk/templates/base.html	(revision 2)
+++ trunk/trunk/templates/base.html	(revision 2)
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="">
+    <meta name="author" content="">
+    <link rel="shortcut icon" href="../../docs-assets/ico/favicon.png">
+
+    <title>FISICA Results Navigator</title>
+
+    <!-- Bootstrap core CSS -->
+    <link href="resources/css/bootstrap.css" rel="stylesheet">
+
+    <!-- Custom styles for this template -->
+    <link href="navbar-fixed-top.css" rel="stylesheet">
+
+    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
+    <!--[if lt IE 9]>
+      <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
+      <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
+    <![endif]-->
+  </head>
+
+  <body>
+
+    <!-- Fixed navbar -->
+    <div class="navbar navbar-default navbar-fixed-top" role="navigation">
+      <div class="container">
+        <div class="navbar-header">
+          <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+            <span class="sr-only">Toggle navigation</span>
+            <span class="icon-bar"></span>
+            <span class="icon-bar"></span>
+            <span class="icon-bar"></span>
+          </button>
+          <a class="navbar-brand" href="#">FISICA</a>
+        </div>
+        <div class="navbar-collapse collapse">
+          <ul class="nav navbar-nav">
+            % for stage in context['data'].items():
+                % if stage[0] == context['renderstage']:                  
+            <li class="active"><a href="#">${stage[0]}</a></li>
+                % else:
+                    % if 'substages' in stage[1].keys():
+                    <!-- link to the first substage page in place of having a stage page -->
+            <li><a href="${stage[0]}-${stage[1]['substages'].keys()[0]}.html">${stage[0]}</a></li>
+                    % else:
+            <li><a href="${stage[0]}.html">${stage[0]}</a></li>
+                    % endif
+                % endif
+            % endfor
+          </ul>
+        </div><!--/.nav-collapse -->
+      </div>
+    </div>
+
+    <!--render the main body of the inheriting class-->
+    ${next.body()}
+
+    <!-- Bootstrap core JavaScript
+    ================================================== -->
+    <!-- Placed at the end of the document so the pages load faster -->
+    <script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
+    <script src="resources/js/bootstrap.min.js"></script>
+  </body>
+</html>
+
Index: trunk/trunk/templates/beamsgenerator.html
===================================================================
--- trunk/trunk/templates/beamsgenerator.html	(revision 2)
+++ trunk/trunk/templates/beamsgenerator.html	(revision 2)
@@ -0,0 +1,86 @@
+<%inherit file="base.html"/>
+
+<%
+import collections
+import matplotlib.pyplot as plt
+import os.path
+
+mylookup = context['mylookup']
+
+beamsgenerator = context['data']['beamsgenerator']
+wavenumbers = beamsgenerator['primary beam'].keys()
+%>
+
+<h2>Beam Generation Parameters</h2>
+
+<div class="row-fluid">
+  <table class="table table-condensed">
+    <tbody>
+    % for k,v in beamsgenerator.items():
+        <!-- don't render complicated objects-->
+        % if not isinstance(v, collections.Iterable):
+        <tr>
+          <td>${k}</td>
+          <td>${v}</td>
+        </tr>
+        % endif
+    % endfor
+    </tbody>
+  </table>
+<div>
+
+<%
+# construct html pages with primary beam plots
+for wn in wavenumbers:
+    link = os.path.join(context['dirname'], 'primarybeam%s.html' % (wn))
+    with open(link, 'w') as f:
+        template = mylookup.get_template('primarybeamplot.html')
+        context_copy = context.kwargs
+        context_copy['wn'] = wn
+        f.write(template.render(**context_copy))
+
+# construct html pages with dirty beam plots
+for wn in wavenumbers:
+    link = os.path.join(context['dirname'], 'dirtybeam%s.html' % (wn))
+    with open(link, 'w') as f:
+        template = mylookup.get_template('dirtybeamplot.html')
+        context_copy = context.kwargs
+        context_copy['wn'] = wn
+        f.write(template.render(**context_copy))
+%>
+
+<!-- link to the plot pages from this page -->
+
+<h3>Primary Beams</h3>
+<div class="row">
+% for wn in context['data']['beamsgenerator']['primary beam'].keys():
+    <% 
+    wnformatted = '%6.4gcm-1' % wn 
+    %>
+  <div class="col-xs-6 col-md-3">
+    <a href="primarybeam${wn}.html" class="thumbnail">
+      <img src="primarybeam${wn}.png" alt="...">
+      <div>
+        <h4>${wnformatted}</h4>
+      </div>
+    </a>
+  </div>
+% endfor
+</div>
+
+<h3>Dirty Beams</h3>
+<div class="row">
+% for wn in context['data']['beamsgenerator']['dirty beam'].keys():
+    <% 
+    wnformatted = '%6.4gcm-1' % wn 
+    %>
+  <div class="col-xs-6 col-md-3">
+    <a href="dirtybeam${wn}.html" class="thumbnail">
+      <img src="dirtybeam${wn}.png" alt="...">
+      <div>
+        <h4>${wnformatted}</h4>
+      </div>
+    </a>
+  </div>
+% endfor
+</div>
Index: trunk/trunk/templates/dirtybeamplot.html
===================================================================
--- trunk/trunk/templates/dirtybeamplot.html	(revision 2)
+++ trunk/trunk/templates/dirtybeamplot.html	(revision 2)
@@ -0,0 +1,30 @@
+<%inherit file="base.html"/>
+
+<!-- do the plotting -->
+<%
+import matplotlib.pyplot as plt
+import os.path
+
+plt.figure()
+
+wn = context['wn']
+beam_image = context['data']['beamsgenerator']['dirty beam'][wn]
+plt.imshow(beam_image.data, interpolation='nearest', origin='lower',
+  aspect='equal', extent=[beam_image.axes[0].data[0],
+  beam_image.axes[0].data[-1], beam_image.axes[1].data[0],
+  beam_image.axes[1].data[-1]])
+plt.axis('image')
+
+plt.colorbar(orientation='vertical')
+plt.xlabel(beam_image.axes[0].title)
+plt.ylabel(beam_image.axes[1].title)
+plt.title(beam_image.title)
+
+plt.savefig(os.path.join(context['dirname'], 'dirtybeam%s.png' % wn))
+plt.close()
+%>
+
+<!-- link to the plot from html -->
+
+<img src='dirtybeam${wn}.png' alt="The dirty beam should appear here">
+
Index: trunk/trunk/templates/doublefourier-data.html
===================================================================
--- trunk/trunk/templates/doublefourier-data.html	(revision 2)
+++ trunk/trunk/templates/doublefourier-data.html	(revision 2)
@@ -0,0 +1,3 @@
+<%inherit file="base.html"/>
+
+This template is a dummy.
Index: trunk/trunk/templates/doublefourier.html
===================================================================
--- trunk/trunk/templates/doublefourier.html	(revision 2)
+++ trunk/trunk/templates/doublefourier.html	(revision 2)
@@ -0,0 +1,139 @@
+<%inherit file="base.html"/>
+
+<!-- do the plotting -->
+<%
+import matplotlib.pyplot as plt
+import os.path
+import string
+
+_valid_chars = "_.%s%s" % (string.ascii_letters, string.digits)
+def _char_replacer(s):
+    '''A small utility function that echoes the argument or returns '_' if the
+    argument is in a list of forbidden characters.
+    '''
+    if s not in _valid_chars:
+        return '_'
+    return s
+
+def sanitize(text):
+    filename = ''.join(_char_replacer(c) for c in text)
+    return filename
+
+
+frequency_axis = context['data']['doublefourier']['frequency axis']
+spatial_axis = context['data']['doublefourier']['spatial axis']
+spatial_freq_axis = context['data']['doublefourier']['spatial frequency axis']
+print 'spatial frequency axis', spatial_freq_axis
+
+#for i,wn in enumerate(frequency_axis):
+print 'frequency axis', frequency_axis
+for i,wn in enumerate(frequency_axis[:1]):
+    plt.figure()
+
+    sky_image = context['data']['doublefourier']['sky at time 0'][:,:,i]
+    plt.imshow(sky_image, interpolation='nearest', origin='lower',
+      aspect='equal', extent=[spatial_axis[0], spatial_axis[-1],
+      spatial_axis[0], spatial_axis[-1]])
+    plt.axis('image')
+
+    plt.colorbar(orientation='vertical')
+##    plt.xlabel(beam_image.axes[0].title)
+##    plt.ylabel(beam_image.axes[1].title)
+##    plt.title(beam_image.title)
+
+##    plt.grid()
+    plt.savefig(os.path.join(context['dirname'], 'dbsky1%s.png' % wn))
+    plt.close()
+
+
+#for i,wn in enumerate(frequency_axis):
+for i,wn in enumerate(frequency_axis[:1]):
+    plt.figure()
+
+    sky_image = context['data']['doublefourier']['sky*beams at time 0'][:,:,i]
+    plt.imshow(sky_image, interpolation='nearest', origin='lower',
+      aspect='equal', extent=[spatial_axis[0], spatial_axis[-1],
+      spatial_axis[0], spatial_axis[-1]])
+    plt.axis('image')
+
+    plt.colorbar(orientation='vertical')
+##    plt.xlabel(beam_image.axes[0].title)
+##    plt.ylabel(beam_image.axes[1].title)
+##    plt.title(beam_image.title)
+
+##    plt.grid()
+    plt.savefig(os.path.join(context['dirname'], 'dbskybeams1%s.png' % wn))
+    plt.close()
+
+#for i,wn in enumerate(frequency_axis):
+for i,wn in enumerate(frequency_axis[:1]):
+    plt.figure()
+
+    sky_image = context['data']['doublefourier']['skyfft at time 0'][:,:,i].real
+    plt.imshow(sky_image, interpolation='nearest', origin='lower',
+      aspect='equal', extent=[spatial_freq_axis[0], spatial_freq_axis[-1],
+      spatial_freq_axis[0], spatial_freq_axis[-1]])
+    plt.axis('image')
+
+    plt.colorbar(orientation='vertical')
+##    plt.xlabel(beam_image.axes[0].title)
+##    plt.ylabel(beam_image.axes[1].title)
+##    plt.title(beam_image.title)
+
+##    plt.grid()
+    plt.savefig(os.path.join(context['dirname'], 'skyfft%s.png' % wn))
+    plt.close()
+
+#for i,wn in enumerate(frequency_axis):
+for i,wn in enumerate(frequency_axis[:1]):
+    plt.figure()
+
+    sky_image = context['data']['doublefourier']['skyfft at time 0'][:,:,i].imag
+    plt.imshow(sky_image, interpolation='nearest', origin='lower',
+      aspect='equal', extent=[spatial_axis[0], spatial_axis[-1],
+      spatial_axis[0], spatial_axis[-1]])
+    plt.axis('image')
+
+    plt.colorbar(orientation='vertical')
+##    plt.xlabel(beam_image.axes[0].title)
+##    plt.ylabel(beam_image.axes[1].title)
+##    plt.title(beam_image.title)
+
+##    plt.grid()
+    plt.savefig(os.path.join(context['dirname'], 'skyfftimag%s.png' % wn))
+    plt.close()
+
+# interferograms
+baseline_interferograms = context['data']['doublefourier']['baseline interferograms']
+for baseline,interferogram in baseline_interferograms.items():
+    plt.figure()
+    plt.plot(interferogram)
+    print 'base', baseline
+    plotfile = 'interferogram%s.png' % list(baseline)
+    print 'plotfile', plotfile
+    plotfile = sanitize(plotfile)
+    print 'plotfile', plotfile
+    plt.savefig(os.path.join(context['dirname'], plotfile))
+    plt.close()
+
+%>
+
+<!-- link to the plot from html -->
+
+% for wn in context['data']['doublefourier']['frequency axis'][:1]:
+    <img src='dbsky1${wn}.png' alt="The sky model should appear here<br>">
+    <img src='dbskybeams1${wn}.png' alt="The sky*beams should appear here<br>">
+    <img src='skyfft${wn}.png' alt="The skyfft should appear here<br>">
+    <img src='skyfftimag${wn}.png' alt="The skyfft.imag should appear here<br>">
+% endfor
+
+% for baseline in baseline_interferograms.keys():
+<%
+    print 'baseline', baseline, type(baseline)
+    plotfile = 'interferogram%s.png' % list(baseline)
+    print 'plotfile', plotfile
+    plotfile = sanitize(plotfile)
+    print 'plotfile', plotfile
+%>
+    <img src='${plotfile}' alt="The interferogram should appear here<br>">
+% endfor
Index: trunk/trunk/templates/fts.html
===================================================================
--- trunk/trunk/templates/fts.html	(revision 2)
+++ trunk/trunk/templates/fts.html	(revision 2)
@@ -0,0 +1,17 @@
+<%inherit file="base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="FTS parameters">
+        <tbody>
+          <%
+          fts = context['data']['fts']
+          %>
+          % for k,v in fts.items():
+          <tr>
+            <td>${k}</td>
+            <td>${v}</td>
+          </tr>
+          % endfor
+        </tbody>
+      </table>
+    <div>
Index: trunk/trunk/templates/loadparameters-Background.html
===================================================================
--- trunk/trunk/templates/loadparameters-Background.html	(revision 2)
+++ trunk/trunk/templates/loadparameters-Background.html	(revision 2)
@@ -0,0 +1,31 @@
+<%inherit file="loadparameters-base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="parameters">
+        <tbody>
+          <tr>
+          <%
+          renderstage = context['renderstage']
+          rendersubstage = context['rendersubstage']
+          %>
+          % for name in context['data'][renderstage]['substages'][rendersubstage].keys():
+            <th>${name}</th>
+          % endfor
+          </tr>
+          <%
+          table = context['data'][renderstage]['substages'][rendersubstage]
+          names = table.keys()
+          rows = table[names[0]].keys()
+          %>
+          % for row in rows:
+          <tr>
+              % for name in names:
+            <th>${table[name][row]}</th>
+              % endfor
+          </tr>
+          % endfor
+
+        </tbody>
+      </table>
+    <div>
+
Index: trunk/trunk/templates/loadparameters-ColdOptics.html
===================================================================
--- trunk/trunk/templates/loadparameters-ColdOptics.html	(revision 2)
+++ trunk/trunk/templates/loadparameters-ColdOptics.html	(revision 2)
@@ -0,0 +1,30 @@
+<%inherit file="loadparameters-base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="parameters">
+        <tbody>
+          <tr>
+          <%
+          renderstage = context['renderstage']
+          rendersubstage = context['rendersubstage']
+          %>
+          % for name in context['data'][renderstage]['substages'][rendersubstage].keys():
+            <th>${name}</th>
+          % endfor
+          </tr>
+          <%
+          table = context['data'][renderstage]['substages'][rendersubstage]
+          names = table.keys()
+          rows = table[names[0]].keys()
+          %>
+          % for row in rows:
+          <tr>
+              % for name in names:
+            <th>${table[name][row]}</th>
+              % endfor
+          </tr>
+          % endfor
+
+        </tbody>
+      </table>
+    <div>
Index: trunk/trunk/templates/loadparameters-Detectors.html
===================================================================
--- trunk/trunk/templates/loadparameters-Detectors.html	(revision 2)
+++ trunk/trunk/templates/loadparameters-Detectors.html	(revision 2)
@@ -0,0 +1,30 @@
+<%inherit file="loadparameters-base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="parameters">
+        <tbody>
+          <tr>
+          <%
+          renderstage = context['renderstage']
+          rendersubstage = context['rendersubstage']
+          %>
+          % for name in context['data'][renderstage]['substages'][rendersubstage].keys():
+            <th>${name}</th>
+          % endfor
+          </tr>
+          <%
+          table = context['data'][renderstage]['substages'][rendersubstage]
+          names = table.keys()
+          rows = table[names[0]].keys()
+          %>
+          % for row in rows:
+          <tr>
+              % for name in names:
+            <th>${table[name][row]}</th>
+              % endfor
+          </tr>
+          % endfor
+
+        </tbody>
+      </table>
+    <div>
Index: trunk/trunk/templates/loadparameters-FTSMechanical.html
===================================================================
--- trunk/trunk/templates/loadparameters-FTSMechanical.html	(revision 2)
+++ trunk/trunk/templates/loadparameters-FTSMechanical.html	(revision 2)
@@ -0,0 +1,30 @@
+<%inherit file="loadparameters-base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="parameters">
+        <tbody>
+          <tr>
+          <%
+          renderstage = context['renderstage']
+          rendersubstage = context['rendersubstage']
+          %>
+          % for name in context['data'][renderstage]['substages'][rendersubstage].keys():
+            <th>${name}</th>
+          % endfor
+          </tr>
+          <%
+          table = context['data'][renderstage]['substages'][rendersubstage]
+          names = table.keys()
+          rows = table[names[0]].keys()
+          %>
+          % for row in rows:
+          <tr>
+              % for name in names:
+            <th>${table[name][row]}</th>
+              % endfor
+          </tr>
+          % endfor
+
+        </tbody>
+      </table>
+    <div>
Index: trunk/trunk/templates/loadparameters-FTSpectrograph.html
===================================================================
--- trunk/trunk/templates/loadparameters-FTSpectrograph.html	(revision 2)
+++ trunk/trunk/templates/loadparameters-FTSpectrograph.html	(revision 2)
@@ -0,0 +1,30 @@
+<%inherit file="loadparameters-base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="parameters">
+        <tbody>
+          <tr>
+          <%
+          renderstage = context['renderstage']
+          rendersubstage = context['rendersubstage']
+          %>
+          % for name in context['data'][renderstage]['substages'][rendersubstage].keys():
+            <th>${name}</th>
+          % endfor
+          </tr>
+          <%
+          table = context['data'][renderstage]['substages'][rendersubstage]
+          names = table.keys()
+          rows = table[names[0]].keys()
+          %>
+          % for row in rows:
+          <tr>
+              % for name in names:
+            <th>${table[name][row]}</th>
+              % endfor
+          </tr>
+          % endfor
+
+        </tbody>
+      </table>
+    <div>
Index: trunk/trunk/templates/loadparameters-Interferometer.html
===================================================================
--- trunk/trunk/templates/loadparameters-Interferometer.html	(revision 2)
+++ trunk/trunk/templates/loadparameters-Interferometer.html	(revision 2)
@@ -0,0 +1,30 @@
+<%inherit file="loadparameters-base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="parameters">
+        <tbody>
+          <tr>
+          <%
+          renderstage = context['renderstage']
+          rendersubstage = context['rendersubstage']
+          %>
+          % for name in context['data'][renderstage]['substages'][rendersubstage].keys():
+            <th>${name}</th>
+          % endfor
+          </tr>
+          <%
+          table = context['data'][renderstage]['substages'][rendersubstage]
+          names = table.keys()
+          rows = table[names[0]].keys()
+          %>
+          % for row in rows:
+          <tr>
+              % for name in names:
+            <th>${table[name][row]}</th>
+              % endfor
+          </tr>
+          % endfor
+
+        </tbody>
+      </table>
+    <div>
Index: trunk/trunk/templates/loadparameters-SimulatorControl.html
===================================================================
--- trunk/trunk/templates/loadparameters-SimulatorControl.html	(revision 2)
+++ trunk/trunk/templates/loadparameters-SimulatorControl.html	(revision 2)
@@ -0,0 +1,30 @@
+<%inherit file="loadparameters-base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="parameters">
+        <tbody>
+          <tr>
+          <%
+          renderstage = context['renderstage']
+          rendersubstage = context['rendersubstage']
+          %>
+          % for name in context['data'][renderstage]['substages'][rendersubstage].keys():
+            <th>${name}</th>
+          % endfor
+          </tr>
+          <%
+          table = context['data'][renderstage]['substages'][rendersubstage]
+          names = table.keys()
+          rows = table[names[0]].keys()
+          %>
+          % for row in rows:
+          <tr>
+              % for name in names:
+            <th>${table[name][row]}</th>
+              % endfor
+          </tr>
+          % endfor
+
+        </tbody>
+      </table>
+    <div>
Index: trunk/trunk/templates/loadparameters-Sky.html
===================================================================
--- trunk/trunk/templates/loadparameters-Sky.html	(revision 2)
+++ trunk/trunk/templates/loadparameters-Sky.html	(revision 2)
@@ -0,0 +1,15 @@
+<%inherit file="loadparameters-base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="parameters">
+        <tbody>
+          <tr>
+          <%
+          renderstage = context['renderstage']
+          rendersubstage = context['rendersubstage']
+          %>
+          </tr>
+
+        </tbody>
+      </table>
+    <div>
Index: trunk/trunk/templates/loadparameters-Sky.html.keep
===================================================================
--- trunk/trunk/templates/loadparameters-Sky.html.keep	(revision 2)
+++ trunk/trunk/templates/loadparameters-Sky.html.keep	(revision 2)
@@ -0,0 +1,32 @@
+<%inherit file="loadparameters-base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="parameters">
+        <tbody>
+          <tr>
+          <%
+          print 'here'
+          renderstage = context['renderstage']
+          rendersubstage = context['rendersubstage']
+          %>
+          % for name in context['data'][renderstage]['substages'][rendersubstage].keys():
+            <th>${name}</th>
+          % endfor
+          </tr>
+          <%
+          table = context['data'][renderstage]['substages'][rendersubstage]
+          print 'table', table
+          names = table.keys()
+          rows = table[names[0]].keys()
+          %>
+          % for row in rows:
+          <tr>
+              % for name in names:
+            <th>${table[name][row]}</th>
+              % endfor
+          </tr>
+          % endfor
+
+        </tbody>
+      </table>
+    <div>
Index: trunk/trunk/templates/loadparameters-Telescope.html
===================================================================
--- trunk/trunk/templates/loadparameters-Telescope.html	(revision 2)
+++ trunk/trunk/templates/loadparameters-Telescope.html	(revision 2)
@@ -0,0 +1,27 @@
+<%inherit file="loadparameters-base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="parameters">
+        <tbody>
+          <tr>
+          <%
+          renderstage = context['renderstage']
+          rendersubstage = context['rendersubstage']
+          %>
+          % for name in context['data'][renderstage]['substages'][rendersubstage].keys():
+            <th>${name}</th>
+          % endfor
+          </tr>
+          <%
+          table = context['data'][renderstage]['substages'][rendersubstage]
+          names = table.keys()
+          %>
+          <tr>
+          % for name in names:
+            <th>${table[name]}</th>
+          % endfor
+          </tr>
+
+        </tbody>
+      </table>
+    <div>
Index: trunk/trunk/templates/loadparameters-WarmOptics.html
===================================================================
--- trunk/trunk/templates/loadparameters-WarmOptics.html	(revision 2)
+++ trunk/trunk/templates/loadparameters-WarmOptics.html	(revision 2)
@@ -0,0 +1,31 @@
+<%inherit file="loadparameters-base.html"/>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="parameters">
+        <tbody>
+          <tr>
+          <%
+          renderstage = context['renderstage']
+          rendersubstage = context['rendersubstage']
+          %>
+          % for name in context['data'][renderstage]['substages'][rendersubstage].keys():
+            <th>${name}</th>
+          % endfor
+          </tr>
+          <%
+          table = context['data'][renderstage]['substages'][rendersubstage]
+          names = table.keys()
+          rows = table[names[0]].keys()
+          %>
+          % for row in rows:
+          <tr>
+              % for name in names:
+            <th>${table[name][row]}</th>
+              % endfor
+          </tr>
+          % endfor
+
+        </tbody>
+      </table>
+    <div>
+
Index: trunk/trunk/templates/loadparameters-base.html
===================================================================
--- trunk/trunk/templates/loadparameters-base.html	(revision 2)
+++ trunk/trunk/templates/loadparameters-base.html	(revision 2)
@@ -0,0 +1,23 @@
+<%inherit file="base.html"/>
+
+    <div class="container">
+
+      <!-- Substage tabs -->
+      <ul class="nav nav-tabs">
+      % for stage in context['data'].items():
+          % if 'substages' in stage[1].keys():
+              % for substage in stage[1]['substages'].items():
+                  % if (stage[0] == context['renderstage']) and (substage[0] == context['rendersubstage']):
+        <li class="active"><a href="#">${substage[0]}</a></li>
+                  % else:
+        <li><a href="${stage[0]}-${substage[0]}.html">${substage[0]}</a></li>
+                  % endif
+              % endfor
+          % endif
+      % endfor
+      </ul>
+
+    </div> <!-- /container -->
+
+    <!--render the main body of the inheriting class-->
+    ${next.body()}
Index: trunk/trunk/templates/pointingerrors.html
===================================================================
--- trunk/trunk/templates/pointingerrors.html	(revision 2)
+++ trunk/trunk/templates/pointingerrors.html	(revision 2)
@@ -0,0 +1,28 @@
+<%inherit file="base.html"/>
+
+<!-- do the plotting -->
+<%
+import matplotlib.pyplot as plt
+import os.path
+%>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="Pointing Errors">
+        <tbody>
+          <%
+          pointingerrors = context['data']['pointingerrors']
+          %>
+          % for k,v in pointingerrors.items():
+             <!-- don't render large arrays-->
+             % if k not in ['primary beam']:
+          <tr>
+            <td>${k}</td>
+            <td>${v}</td>
+          </tr>
+             % endif
+          % endfor
+        </tbody>
+      </table>
+    <div>
+
+<!-- link to the plot from html -->
Index: trunk/trunk/templates/primarybeamplot.html
===================================================================
--- trunk/trunk/templates/primarybeamplot.html	(revision 2)
+++ trunk/trunk/templates/primarybeamplot.html	(revision 2)
@@ -0,0 +1,30 @@
+<%inherit file="base.html"/>
+
+<!-- do the plotting -->
+<%
+import matplotlib.pyplot as plt
+import os.path
+
+plt.figure()
+
+wn = context['wn']
+beam_image = context['data']['beamsgenerator']['primary beam'][wn]
+plt.imshow(beam_image.data, interpolation='nearest', origin='lower',
+  aspect='equal', extent=[beam_image.axes[0].data[0],
+  beam_image.axes[0].data[-1], beam_image.axes[1].data[0],
+  beam_image.axes[1].data[-1]])
+plt.axis('image')
+
+plt.colorbar(orientation='vertical')
+plt.xlabel(beam_image.axes[0].title)
+plt.ylabel(beam_image.axes[1].title)
+plt.title(beam_image.title)
+
+plt.savefig(os.path.join(context['dirname'], 'primarybeam%s.png' % wn))
+plt.close()
+%>
+
+<!-- link to the plot from html -->
+
+<img src='primarybeam${wn}.png' alt="The primary beam should appear here">
+
Index: trunk/trunk/templates/skygenerator.html
===================================================================
--- trunk/trunk/templates/skygenerator.html	(revision 2)
+++ trunk/trunk/templates/skygenerator.html	(revision 2)
@@ -0,0 +1,87 @@
+<%inherit file="base.html"/>
+
+<%
+import collections
+import matplotlib.pyplot as plt
+import os.path
+
+skygenerator = context['data']['skygenerator']
+frequency_axis = skygenerator['frequency axis']
+spatial_axis = skygenerator['spatial axis']
+sources = skygenerator['sources']
+%>
+
+<h2>Sky Model Parameters</h2>
+
+<div class="row-fluid">
+  <table class="table table-condensed">
+    <tbody>
+    % for k,v in skygenerator.items():
+        <!-- don't render complicated objects-->
+        % if not isinstance(v, collections.Iterable):
+        <tr>
+            <td>${k}</td>
+            <td>${v}</td>
+        </tr>
+        % endif
+    % endfor
+    </tbody>
+  </table>
+</div>
+
+<%
+# construct html pages with sky model plane plots
+for iwn,wn in enumerate(frequency_axis):
+    link = os.path.join(context['dirname'], 'skymodelplane%s.html' % (wn))
+    with open(link, 'w') as f:
+        template = mylookup.get_template('skymodelplane.html')
+        context_copy = context.kwargs
+        context_copy['wn'] = wn
+        context_copy['iwn'] = iwn
+        f.write(template.render(**context_copy))
+
+# construct html pages with model source spectra plots
+for sourceid in sources.keys():
+    link = os.path.join(context['dirname'], 'sourcespectrum%s.html' % (
+      sourceid))
+    with open(link, 'w') as f:
+        template = mylookup.get_template('sourcespectrum.html')
+        context_copy = context.kwargs
+        context_copy['sourceid'] = sourceid
+        f.write(template.render(**context_copy))
+%>
+
+<!-- link to the plot pages from this page -->
+
+<h3>Sky Model Planes</h3>
+<div class="row">
+% for wn in frequency_axis:
+    <%
+    wnformatted = '%6.4gcm-1' % wn
+    %>
+  <div class="col-xs-6 col-md-3">
+    <a href="skymodelplane${wn}.html" class="thumbnail">
+      <img src="skymodelplane${wn}.png" alt="...">
+      <div>
+        <h4>${wnformatted}</h4>
+      </div>
+    </a>
+  </div>
+% endfor
+</div>
+
+% if sources:
+    <h3>Model Source Spectra</h3>
+    <div class="row">
+    % for sourceid in sources.keys():
+      <div class="col-xs-6 col-md-3">
+        <a href="sourcespectrum${sourceid}.html" class="thumbnail">
+          <img src="sourcespectrum${sourceid}.png" alt="...">
+          <div>
+            <h4>Source${sourceid}</h4>
+          </div>
+        </a>
+      </div>
+    % endfor
+    </div>
+% endif
Index: trunk/trunk/templates/skymodelplane.html
===================================================================
--- trunk/trunk/templates/skymodelplane.html	(revision 2)
+++ trunk/trunk/templates/skymodelplane.html	(revision 2)
@@ -0,0 +1,29 @@
+<%inherit file="base.html"/>
+
+<!-- do the plotting -->
+<%
+import matplotlib.pyplot as plt
+import os.path
+
+plt.figure()
+
+iwn = context['iwn']
+sky_image = context['data']['skygenerator']['sky model'][:,:,iwn]
+spatial_axis = context['data']['skygenerator']['spatial axis']
+
+plt.imshow(sky_image, interpolation='nearest', origin='lower',
+  aspect='equal', extent=[spatial_axis[0], spatial_axis[-1],
+  spatial_axis[0], spatial_axis[-1]])
+plt.axis('image')
+
+wn = context['wn'] 
+plt.title('Sky model plane %6.4gcm-1' % wn)
+
+plt.colorbar(orientation='vertical')
+plt.savefig(os.path.join(context['dirname'], 'skymodelplane%s.png' % wn))
+plt.close()
+%>
+
+<!-- link to the plot from html -->
+
+<img src='skymodelplane${wn}.png' alt="The sky model should appear here<br>">
Index: trunk/trunk/templates/sourcespectrum.html
===================================================================
--- trunk/trunk/templates/sourcespectrum.html	(revision 2)
+++ trunk/trunk/templates/sourcespectrum.html	(revision 2)
@@ -0,0 +1,30 @@
+<%inherit file="base.html"/>
+
+<!-- do the plotting -->
+<%
+import matplotlib.pyplot as plt
+import os.path
+
+plt.figure()
+
+print 'context'
+print context
+
+sourceid = context['sourceid']
+print sourceid
+spectrum = context['data']['skygenerator']['sources'][sourceid]['spectrum']
+print 'here'
+frequency_axis = context['data']['skygenerator']['frequency axis']
+print 'here1'
+
+plt.plot(frequency_axis, spectrum)
+plt.title('Source %s' % sourceid)
+print 'here2'
+
+plt.savefig(os.path.join(context['dirname'], 'sourcespectrum%s.png' % sourceid))
+plt.close()
+%>
+
+<!-- link to the plot from html -->
+
+<img src='sourcespectrum${sourceid}.png' alt="Source spectrum">
Index: trunk/trunk/templates/t1-1-2.html
===================================================================
--- trunk/trunk/templates/t1-1-2.html	(revision 2)
+++ trunk/trunk/templates/t1-1-2.html	(revision 2)
@@ -0,0 +1,3 @@
+<%inherit file="base.html"/>
+
+body content of t1-1
Index: trunk/trunk/templates/t1-1.html
===================================================================
--- trunk/trunk/templates/t1-1.html	(revision 2)
+++ trunk/trunk/templates/t1-1.html	(revision 2)
@@ -0,0 +1,3 @@
+<%inherit file="base.html"/>
+
+body content of t1-1
Index: trunk/trunk/templates/uvmapgenerator.html
===================================================================
--- trunk/trunk/templates/uvmapgenerator.html	(revision 2)
+++ trunk/trunk/templates/uvmapgenerator.html	(revision 2)
@@ -0,0 +1,39 @@
+<%inherit file="base.html"/>
+
+<!-- do the plotting -->
+<%
+import matplotlib.pyplot as plt
+import os.path
+
+fig, ax = plt.subplots()
+bxby = context['data']['uvmapgenerator']['bxby']
+ax.plot(bxby[:,0], bxby[:,1], 'go-')
+ax.grid()
+ax.axis('equal')
+
+plt.savefig(os.path.join(context['dirname'], 'uvmap.png'))
+plt.close()
+%>
+
+    <div class="row-fluid">
+      <table class="table table-condensed" summary="FTS parameters">
+        <tbody>
+          <%
+          uvmap = context['data']['uvmapgenerator']
+          %>
+          % for k,v in uvmap.items():
+             <!-- don't render large arrays-->
+             % if k not in ['bxby']:
+          <tr>
+            <td>${k}</td>
+            <td>${v}</td>
+          </tr>
+             % endif
+          % endfor
+        </tbody>
+      </table>
+    <div>
+
+<!-- link to the plot from html -->
+
+<img src='uvmap.png' alt="The UV map should appear here">
Index: trunk/trunk/uvmapgenerator.py
===================================================================
--- trunk/trunk/uvmapgenerator.py	(revision 2)
+++ trunk/trunk/uvmapgenerator.py	(revision 2)
@@ -0,0 +1,56 @@
+from __future__ import absolute_import
+
+import collections
+import numpy as np
+
+
+class UVMapGenerator(object):
+    """Class to generate the UV map of the simulated observation.
+    """
+
+    def __init__(self, parameters):
+        self.parameters = parameters
+        self.result = collections.OrderedDict()
+
+    def run(self):
+        print 'UVMapGenerator.run'
+        interferometer = self.parameters['substages']['Interferometer']
+
+        # slightly awkward getting pattern value as it's keyed by its
+        # row number in the spreadsheet
+        row = interferometer['Pattern'].keys()[0]
+        pattern = interferometer['Pattern'][row]
+
+        if pattern.lower() == 'spiral':
+            bmax = interferometer['bmax [m]'][row]
+            bmin = interferometer['bmin [m]'][row]
+            n_baselines = int(interferometer['Num Baselines'][row])
+            bstep = interferometer['bstep [m]'][row]
+
+            # baseline increases by bstep for each circuit of spiral
+            n_laps = (bmax - bmin) / bstep
+
+            bxby = np.zeros([n_baselines,2])     
+
+            # n_baselines is total number of points along spiral
+            for i in range(n_baselines):
+                # phi is angle around spiral, r its radius
+                phi = n_laps * 2 * np.pi * (n_baselines-i) / n_baselines
+                r = bmin + bstep * phi / (2 * np.pi)
+
+                bxby[i,:] = [r * np.cos(phi), r * np.sin(phi)]
+
+            print 'spiral pattern computed'
+            self.result['bxby'] = bxby
+            self.result['bmin'] = bmin
+            self.result['bmax'] = bmax
+            self.result['n_baselines'] = n_baselines
+
+        else:
+            raise Exception, 'unknown baseline pattern: %s' % pattern
+
+        return self.result
+
+    def __repr__(self):
+        return 'UVMapGenerator'
+
