Beispiel #1
0
    def __init__(self, wins, time, nxmax, nymax, good, head):
        """
        Creates a new CCD frame.

        Arguments:

        wins    -- list of non-overlapping Window objects.
        time    -- a Time representing the central time of the CCD (can be None)
        nxmax   -- maximum dimension in X, unbinned pixels.
        nymax   -- maximum dimension in Y, unbinned pixels.
        good    -- True if data are not junk.
        head    -- header. Must be None or a 'Uhead'
        """
        for win in wins:
            if not isinstance(win, Window):
                raise UltracamError(
                    'CCD.__init__: all windows must be Windows.')

        if head is not None and not isinstance(head, Uhead):
            raise UltracamError(
                'CCD.__init__: head should be a Uhead (or None).')

        if time and not isinstance(time, Time):
            raise UltracamError('CCD.__init__: time should be a Time.')

        self._data = wins
        self.time = time
        self.nxmax = nxmax
        self.nymax = nymax
        self.good = good
        self.head = head
Beispiel #2
0
 def __setitem__(self, i, ccd):
     """
     Sets the i-th Window
     """
     if not isinstance(ccd, CCD):
         raise UltracamError('MCCD.__setitem__: ccd must be a CCD')
     self._data[i] = ccd
Beispiel #3
0
    def centile(self, pcent):
        """
        Returns percentile(s) over all Windows of a CCD. Given pcent, this
        routine returns the image level below which pcent percent of the pixel
        values lie.  pcent can be a single number or array-like. In the latter
        case a list of values is returned.

        pcent -- percentile or percentiles (array-like)

        Returns image value or values as a list equivalent to the input
        percentiles.
        """

        # check against a string which can look array-like
        if isinstance(pcent, six.string_types):
            raise UltracamError(
                'CCD.centile: argument "pcent" cannot be a string')

        # generate combined list of all pixels in CCD called 'arr'
        larr = []
        for win in self._data:
            larr.append(win.flatten())
        arr = np.concatenate(larr)

        return np.percentile(arr, pcent)
Beispiel #4
0
 def __setitem__(self, i, win):
     """
     Sets the i-th Window
     """
     if not isinstance(win, Window):
         raise UltracamError('CCD.__setitem__: win must be a Window')
     self._data[i] = win
Beispiel #5
0
    def __init__(self, data, head):
        """
        Imposes some restrictions on the inputs not set by MCCD. There must be
        3 CCDs, and each must have an even number of Windows. 
        """
        if len(data) != 3:
            raise UltracamError(
                'UCAM.__init__: require list of 3 CCDs for data')

        for ccd in data:
            if ccd.nwin % 2 != 0:
                raise UltracamError(
                    'UCAM.__init__: all CCDs must have an even number of Windows'
                )

        MCCD.__init__(self, data, head)
Beispiel #6
0
def get_nframe_from_server(run):
    """
    Returns the number of frames in the run via the FileServer
    """

    if URL is None:
        raise UltracamError('get_nframe_from_server: no url for server found.' +
                            ' Have you set the ULTRACAM_DEFAULT_URL environment variable?')
    # get from FileServer
    full_url = URL + run + '?action=get_num_frames'
    resp = urllib.request.urlopen(full_url).read()

    # parse the response
    loc = resp.find('nframes="')
    if loc > -1:
        end = resp[loc+9:].find('"')
        return int(resp[loc+9:loc+9+end])
    else:
        raise UltracamError('get_nframe_from_server: failed to parse server response to ' + full_url)
Beispiel #7
0
    def cropTo(self, mccd):
        """
        Crops the MCCD to match mccd if possible, returns the cropped
        MCCD. Raises an UltracamError if it does not succeed.
        """
        if len(self) != len(mccd):
            raise UltracamError('MCCD.crop: number of CCDs did not match')

        ccds = []
        for ccd, ccdo in zip(self._data, mccd._data):
            ccds.append(ccd.cropTo(ccdo))
        return MCCD(ccds, self.head)
Beispiel #8
0
    def cropTo(self, ccd):
        """
        Crops the CCD to match ccd returning the cropped
        CCD with the CCD itself unchanged. Raises an UltracamError
        if it does not succeed.
        """
        if self.nxmax != ccd.nxmax or self.nymax != ccd.nymax:
            raise UltracamError('CCD.crop: maximum dimensions did not match')

        wins = []
        for nwino, wino in enumerate(ccd._data):
            for win in self._data:
                if win.canCropTo(wino):
                    wins.append(win.cropTo(wino))
                    break
            else:
                raise UltracamError(
                    'CCD.crop: could not crop any window of CCD to match window '
                    + str(nwino + 1) + ' of other.')
        return CCD(wins, self.time, self.nxmax, self.nymax, self.good,
                   self.head)
Beispiel #9
0
def runID(mjd):
    """
    Identifies a run from an MJD. Returns the run ID and telescope.
    Raises an UltracamError if it cannot match the time.
    """
    for dtup in RUN_DATES:
        run_id, start, stop = dtup
        mstart = str2mjd(start)
        mstop = str2mjd(stop)
        if mstart < mjd and mjd < mstop + 1.5:
            return (run_id, RUN_TELS[run_id])

    raise UltracamError('runID: could not identify time = ' + mjd2str(mjd))
Beispiel #10
0
    def __init__(self, data, head):
        """
        Creates an MCCD.

        Arguments:

          data  -- list of CCD objects

          head -- the header, either None or a Uhead object.

        Sets the equivalent attribute 'head'
        """
        for ccd in data:
            if not isinstance(ccd, CCD):
                raise UltracamError(
                    'MCCC.__init__: one or more of the elements of data is not a CCD.'
                )

        if head is not None and not isinstance(head, Uhead):
            raise UltracamError(
                'MCCC.__init__: head should be a Uhead (or None).')

        self._data = data
        self.head = head
Beispiel #11
0
    def __init__(self, fname):
        """
        Constructs a new Log given a file. Makes empty
        dictionaries if none found and reports an error
        """
        self.format  = 2
        self.target  = {}
        self.filters = {}
        self.comment = {}

        rec    = re.compile('file\s+object\s+filter', re.I)
        old    = re.compile('\s*(\S+)\s+(\S+)\s+(.*)$')
        oldii  = re.compile('\s*(\S+)\s*$')
        f  = open(fname)
        for line in f:
            m = rec.search(line)
            if m:
                self.format = 1
                if len(self.comment):
                    raise UltracamError('Error in night log = ' + fname + ', line = ' + line)

            if line.startswith('run'):
                run = line[:6]
                if self.format == 2:
                    self.comment[run] = line[6:].strip()
                else:
                    m = old.search(line[6:])
                    if m:
                        self.target[run]  = m.group(1)
                        self.filters[run] = m.group(2)
                        self.comment[run] = m.group(3)
                    else:
                        m = oldii.search(line[6:])
                        if m:
                            self.target[run]  = m.group(1)
                        else:
                            self.target[run]  = 'UNKNOWN'
                        self.filters[run] = 'UNKNOWN'
                        self.comment[run] = ''
Beispiel #12
0
def check_ucm(fobj):
    """
    Check a file opened for reading in binary mode to see if it is a ucm.

    Returns endian which is a string to be passed
    to later routines indicating endian-ness.
    """

    # read the format code
    fbytes = fobj.read(4)
    fcode = struct.unpack('i', fbytes)[0]
    if fcode != MAGIC:
        fcode = struct.unpack('>i', fbytes)[0]
        if fcode != MAGIC:
            fobj.close()
            raise UltracamError(
                'check_ucm: could not recognise first 4 bytes of ' + fname +
                ' as a ucm file')
        endian = '>'
    else:
        endian = '<'
    return endian
Beispiel #13
0
def get_runs_from_server(dir=None):
    """
    Returns with a list of runs from the server

    dir -- name of sub-directory on server
    """
    if URL is None:
        raise UltracamError('get_runs_from_server: no url for server found.' + 
                            ' Have you set the ULTRACAM_DEFAULT_URL environment variable?')
    # get from FileServer
    if dir is None:
        full_url = URL + '?action=dir'
    else:
        full_url = URL + dir + '?action=dir'
    resp = urllib.request.urlopen(full_url).read()

    # parse response from server
    ldir = resp.split('<li>')
    runs = [entry[entry.find('>run')+1:entry.find('>run')+7] for entry in ldir
            if entry.find('getdata">run') > -1]
    runs.sort()
    return runs
Beispiel #14
0
 def __setitem__(self, key, value):
     raise UltracamError(
         'Uhead.__setitem__ disabled to prevent invalid items being defined. Use add_entry'
     )
Beispiel #15
0
    def add_entry(self, *args):
        """
        Adds a new Uhead item, checking the various arguments to reduce the
        chances of problems.  This can have either 2 or 4 arguments. The 4
        argument case is as follows:

        Args
          key : hierarchical string of the form 'User.Filter' where 'User'
                is a directory or folder of grouped entries. It cannot have
                blanks and any implied directories must already exists.
                Thus to set a key 'User.Filter.Wheel', 'User.Filter' would
                need to exist and be a directory. The existence of the
                implied 'User' would not be checked in this case, on the
                assumption that it was checked when 'User.Filter' was created.

          value : value to associate (will be ignored in the case of
                  directories, but see the 2 argument case below). The nature
                  of the value varies with the itype; see next.

          itype : one of a range of possible data types. This rather
                  'unpythonic' argument is to address the need to match up
                  with data files and the C++ ULTRACAM pipeline when it
                  comes to writing to disk. Look for integers called
                  'ITYPE_*' to see the set of potential types. The meaning
                  of most data types is obvious. e.g.  ITYPE_DOUBLE or
                  ITYPE_FLOAT expect floating point numbers. In this case
                  both will be stored as a Python float in memory, but
                  will be saved to disk with different numbers of bytes.
                  Less obvious ones are:

                   ITYPE_TIME : the corresponding value should be a two-element
                                tuple or list with first an integer for the
                                number of days and then a float for the
                                number of hours passed.


          comment : comment string with details of the variable.

        If just 2 arguments are given, they will be interpreted as just a key
        and comment for a directory.
        """

        # elementary checks
        if len(args) == 2:
            key, comment = args
            itype = ITYPE_DIR
            value = None
        elif len(args) == 4:
            key, value, itype, comment = args
        else:
            raise UltracamError(
                'Uhead.add_entry: takes either 2 or 4 arguments')

        if not isinstance(key, six.string_types):
            raise UltracamError(
                'Uhead.add_entry: argument "key" must be a string.')

        if not isinstance(comment, six.string_types):
            raise UltracamError('Uhead.add_entry: key = ' + key +
                                ': "comment" must be a string.')

        # now look at the key: must have no blanks
        if key.find(' ') > -1:
            raise UltracamError('Uhead.add_entry: key = "' + key +
                                '" contains at least one blank.')

        # if key has a '.' then the part before last dot must already exist
        # and must be a directory. Search in reverse order, as all being well, it
        # should usually be fastest.
        ldot = key.rfind('.')
        if ldot > -1:
            dir = key[:ldot]
            for kold in list(self.keys())[::-1]:
                if dir == kold and self[kold][1] == ITYPE_DIR:
                    break
            else:
                raise UltracamError('Uhead.add_entry: key = ' + key +
                                    ': could not locate directory = ' +
                                    key[:ldot])

            # determine position of key within Odict. Must add onto
            # whichever directory it belongs to.
            for index, kold in enumerate(self.keys()):
                if kold.startswith(dir): lind = index

        # the next implicitly check the value: if they can't be converted to
        # the right type, something is wrong.
        if itype == ITYPE_DOUBLE or itype == ITYPE_FLOAT:
            value = float(value)
        elif itype == ITYPE_INT or itype == ITYPE_UINT or \
                itype == ITYPE_UCHAR or itype == ITYPE_USINT:
            value = int(value)
        elif itype == ITYPE_STRING:
            value = str(value)
        elif itype == ITYPE_BOOL:
            value = bool(value)
        elif itype == ITYPE_DIR:
            pass
        elif itype == ITYPE_TIME:
            if len(value) != 2:
                raise UltracamError(
                    'Uhead.add_entry: key = ' + key +
                    ': require a 2-element tuple or list (int,float) for ITYPE_TIME)'
                )
            value = (int(value[0]), float(value[1]))
        elif itype == ITYPE_DVECTOR:
            if not isinstance(value, np.ndarray) or len(value.shape) != 1:
                raise UltracamError(
                    'Uhead.add_entry: key = ' + key +
                    ': require a 1D numpy.ndarray for ITYPE_DVECTOR)')
            value = value.astype(float64)
        elif itype == ITYPE_IVECTOR:
            if not isinstance(value, np.ndarray) or len(value.shape) != 1:
                raise UltracamError(
                    'Uhead.add_entry: key = ' + key +
                    ': require a 1D numpy.ndarray for ITYPE_IVECTOR)')
            value = value.astype(int)
        elif itype == ITYPE_FVECTOR:
            if not isinstance(value, np.ndarray) or len(value.shape) != 1:
                raise UltracamError(
                    'Uhead.add_entry: key = ' + key +
                    ': require a 1D numpy.ndarray for ITYPE_FVECTOR)')
            value = value.astype(float32)
        else:
            raise UltracamError('Uhead.add_entry: key = ' + key +
                                ': itype = ' + str(itype) + ' not recognised.')

        # checks passed, finally set item
        if ldot > -1:
            self.insert(key, (value, itype, comment), lind + 1)
        else:
            Odict.__setitem__(self, key, (value, itype, comment))
Beispiel #16
0
 def data(self, wins):
     for win in wins:
         if not isinstance(win, Window):
             raise UltracamError(
                 'CCD.data: wins must be a list of Windows.')
     self._data = wins
Beispiel #17
0
 def data(self, ccds):
     for ccd in ccds:
         if not isinstance(ccd, CCD):
             raise UltracamError('MCCD.data: ccds must be a list of CCDs.')
     self._data = ccds
Beispiel #18
0
    def rucm(cls, fname, flt=True):
        """
        Factory method to produce an MCCD from a ucm file.

        fname -- ucm file name. '.ucm' will be appended if not supplied.

        flt    -- convert to 4-byte floats whatever the input data, or not. ucm
                  files can either contain 4-bytes floats or for reduced disk
                  footprint, unsigned 2-byte integers. If flt=True, either type
                  will end up as float32 internally. If flt=False, the disk type
                  will be retained. The latter is unsafe when arithematic is involved
                  hence the default is to convert to 4-byte floats.

        Exceptions are thrown if the file cannot be found, or an error during the
        read occurs.
        """

        # Assume it is a file object, if that fails, assume it is
        # the name of a file.
        if not fname.endswith('.ucm'): fname += '.ucm'
        uf = open(fname, 'rb')
        start_format = check_ucm(uf)

        # read the header
        lmap = struct.unpack(start_format + 'i', uf.read(4))[0]

        head = Uhead()
        for i in range(lmap):
            name = read_string(uf, start_format)
            itype = struct.unpack(start_format + 'i', uf.read(4))[0]
            comment = read_string(uf, start_format)

            if itype == ITYPE_DOUBLE:
                value = struct.unpack(start_format + 'd', uf.read(8))[0]
            elif itype == ITYPE_INT:
                value = struct.unpack(start_format + 'i', uf.read(4))[0]
            elif itype == ITYPE_UINT:
                value = struct.unpack(start_format + 'I', uf.read(4))[0]
            elif itype == ITYPE_FLOAT:
                value = struct.unpack(start_format + 'f', uf.read(4))[0]
            elif itype == ITYPE_STRING:
                value = read_string(uf, start_format)
            elif itype == ITYPE_BOOL:
                value = struct.unpack(start_format + 'B', uf.read(1))[0]
            elif itype == ITYPE_DIR:
                value = None
            elif itype == ITYPE_TIME:
                value = struct.unpack(start_format + 'id', uf.read(12))
            elif itype == ITYPE_DVECTOR:
                nvec = struct.unpack(start_format + 'i', uf.read(4))[0]
                value = struct.unpack(start_format + str(nvec) + 'd',
                                      uf.read(8 * nvec))
            elif itype == ITYPE_UCHAR:
                value = struct.unpack(start_format + 'c', uf.read(1))[0]
            elif itype == ITYPE_USINT:
                value = struct.unpack(start_format + 'H', uf.read(2))[0]
            elif itype == ITYPE_IVECTOR:
                nvec = struct.unpack(start_format + 'i', uf.read(4))[0]
                value = struct.unpack(start_format + str(nvec) + 'i',
                                      uf.read(4 * nvec))
            elif itype == ITYPE_FVECTOR:
                nvec = struct.unpack(start_format + 'i', uf.read(4))[0]
                value = struct.unpack(start_format + str(nvec) + 'f',
                                      uf.read(4 * nvec))
            else:
                raise UltracamError(
                    'ultracam.MCCD.rucm: do not recognize itype = ' +
                    str(itype))

            # store header information, fast method
            Odict.__setitem__(head, name, (value, itype, comment))

        # now for the data
        data = []

        # read number of CCDs
        nccd = struct.unpack(start_format + 'i', uf.read(4))[0]

        for nc in range(nccd):
            # read number of wndows
            nwin = struct.unpack(start_format + 'i', uf.read(4))[0]
            wins = []
            for nw in range(nwin):
                llx, lly, nx, ny, xbin, ybin, nxmax, nymax, iout = struct.unpack(
                    start_format + '9i', uf.read(36))
                if iout == 0:
                    win = np.fromfile(file=uf, dtype=np.float32, count=nx * ny)
                elif iout == 1:
                    if flt:
                        win = np.fromfile(file=uf,
                                          dtype=np.uint16,
                                          count=nx * ny).astype(np.float32)
                    else:
                        win = np.fromfile(file=uf,
                                          dtype=np.uint16,
                                          count=nx * ny)
                else:
                    raise UltracamError('ultracam.MCCD.rucm: iout = ' +
                                        str(iout) + ' not recognised')
                win = win.reshape((ny, nx))
                wins.append(Window(win, llx, lly, xbin, ybin))

            data.append(CCD(wins, None, nxmax, nymax, True, None))
        uf.close()

        return cls(data, head)
Beispiel #19
0
    def checkData(self):
        """
        Checks the CCDs for some standard problems with ULTRACAM data. For each
        CCD it returns with a flag that is True if the corresponding CCD is thought
        to have a problem, along with a string describing the issue. This should
        only be applied to raw ULTRACAM images, not ones that have been processed
        in any way and it will only work if they have been read in as integers.

        The problems this routine picks up are:

         -- too low overall level (judged from many previous images)
         -- too high an overall level (loosely based on when peppering occurs)
         -- left, right windows differ too much which sometimes happens
         -- too many pixels with the same value

        It will only report one problem per CCD at most and breaks off checking as 
        soon as it finds an issue, thus it is possible that there are other problems.
        It is chiefly here to enable warnings of possible problems. It tries to
        find the worst problems first.

        Returns a list of tuples, one for each CCD. Each of these consists of a 
        True/False flag and a string. Thus the following code makes sense:

        r,g,b = mccd.checkData()
        if r[0]: print 'Red CCD has a problem: ',r[1]
        if g[0]: print 'Green CCD has a problem: ',g[1]
        if b[0]: print 'Blue CCD has a problem: ',b[1]

        """
        if self[0][0].dtype != np.uint16:
            raise UltracamError(
                'UCAM.checkData: only works with raw unsigned 16-bit int images'
            )

        ret = []
        for nc, ccd in enumerate(self._data):
            for winl, winr in zip(ccd[::2], ccd[1::2]):

                # check the modes
                hist = np.bincount(winl.flatten())
                nmode = hist[np.argmax(hist)]
                if nmode > winl.size // 4:
                    ret.append(
                        (True, 'a window has >25% pixels of same value'))
                    break

                hist = np.bincount(winr.flatten())
                nmode = hist[np.argmax(hist)]
                if nmode > winl.size // 4:
                    ret.append(
                        (True, 'a window has >25% pixels of same value'))
                    break

                # check the medians
                l = winl.median()
                r = winr.median()

                if nc == 0:
                    if l < 1580 or r < 1580:
                        ret.append((True, 'too low'))
                        break
                    elif l > 55000 or r > 55000:
                        ret.append((True, 'too high'))
                        break
                    elif abs(r - l - 70) > 30 + 0.05 * max(0, l - 1700):
                        ret.append((True, 'left and right too different'))
                        break

                elif nc == 1:
                    if l < 1200 or r < 1200:
                        ret.append((True, 'too low'))
                        break
                    elif l > 30000 or r > 30000:
                        ret.append((True, 'too high'))
                        break
                    elif abs(r - l - 10) > 60 + 0.05 * max(0, l - 1300):
                        ret.append((True, 'left and right too different'))
                        break

                elif nc == 2:
                    if l < 1000 or r < 1000:
                        ret.append((True, 'too low'))
                        break
                    elif l > 30000 or r > 30000:
                        ret.append((True, 'too high'))
                        break
                    elif abs(r - l - 100 +
                             5.5 * l / 60.) > 70 + 0.05 * max(0, l - 1500):
                        ret.append((True, 'left and right too different'))
                        break

            else:
                # loop traversed without a problem
                ret.append((False, ''))

        return ret
Beispiel #20
0
    def plot(self, vlo=2., vhi=98., nc=-1, method='p', mpl=False, cmap=CMDEF, \
                 close=True, x1=None, x2=None, y1=None, y2=None, sepmin=1.):
        """
        Plots an MCCD using pgplot or matplotlib if preferred.

        :Parameters:
         vlo : float
            number specifying the lowest level to plot (default as a percentile)
         vhi : float
            number specifying the lowest level to plot (default as a percentile)
         nc : int
            CCD number (starting from 0, -1 for all)
         method : string
            how vlo and vhi are to be interpreted. 'p' = percentile, 'a' = automatic (min to max,
            vlo and vhi are irrelevant), 'd' = direct, i.e. just take the values given.
         mpl : bool
            True to prefer matplotlib over pgplot (which may not even be an option)
         cmap : matplotlib.cm.binary
            colour map if using matplotlib
         close : bool
            close (pgplot) or 'show' (matplotlib) the plot at the end (or not, to allow 
            you to plot something else, use a cursor etc). In the case of pgplot, this also
            implies opening the plot at the start, i.e. a self-contained quick plot.
         x1 : float
            left-hand plot limit. Defaults to 0.5
         x2 : float
             right-hand plot limit. Defaults to nxmax+0.5
         y1 : float
            lower plot limit. Defaults to 0.5
         y2 : float
             upper plot limit. Defaults to nymax+0.5
         sepmin : float
             minimum separation between intensity limits (> 0 to stop PGPLOT complaining)

        :Returns:
         range(s) : tuple or list
            the plot range(s) used either as a single 2-element tuple, or
            a list of them, one per CCD plotted.
        """

        if nc == -1:
            nc1 = 0
            nc2 = len(self)
        else:
            nc1 = nc
            nc2 = nc + 1

        if not mpl:
            if close: pg.pgopen('/xs')
            if nc2 - nc1 > 1: pg.pgsubp(nc2 - nc1, 1)

        prange = []
        for nc, ccd in enumerate(self._data[nc1:nc2]):

            # Determine intensity range to display
            if method == 'p':
                vmin, vmax = ccd.centile((vlo, vhi))
            elif method == 'a':
                vmin, vmax = ccd.min(), ccd.max()
            elif method == 'd':
                vmin, vmax = vlo, vhi
            else:
                raise UltracamError(
                    'MCCD.plot: method must be one of p, a or d.')

            if vmin == vmax:
                vmin -= sepmin / 2.
                vmax += sepmin / 2.
            prange.append((vmin, vmax))

            # start
            nxmax, nymax = ccd.nxmax, ccd.nymax
            x1 = 0.5 if x1 is None else x1
            x2 = nxmax + 0.5 if x2 is None else x2
            y1 = 0.5 if y1 is None else y1
            y2 = nymax + 0.5 if y2 is None else y2

            if mpl:
                if nc2 - nc1 > 1:
                    plt.subplot(1, nc2 - nc1, nc + 1)
                plt.axis('equal')
            else:
                if nc2 - nc1 > 1: pg.pgpanl(nc - nc1 + 1, 1)
                pg.pgwnad(x1, x2, y1, y2)

            # plot CCD
            ccd.plot(vmin, vmax, mpl, cmap)

            # per-frame finishing-off
            if mpl:
                plt.xlim(x1, x2)
                plt.ylim(y1, y2)
            else:
                pg.pgbox('bcnst', 0, 0, 'bcnst', 0, 0)
                pg.pglab('X', 'Y', '')

        if close:
            if mpl:
                plt.show()
            else:
                pg.pgclos()

        # return intensity range(s) used
        if len(prange) == 1:
            return prange[0]
        else:
            return tuple(prange)
Beispiel #21
0
    def wucm(self, fname):
        """
        Writes to disk in ucm format. The data are saved as 32-bit floats
        or 16-bit unsigned integers according to the internal types.

        fname  -- file to write to. '.ucm' will be appended if necessary.
        """

        if not fname.strip().endswith('.ucm'):
            fname = fname.strip() + '.ucm'
        uf = open(fname, 'wb')

        # write the format code
        uf.write(struct.pack('i', MAGIC))

        # write the header, starting with the number of entries
        if self.head is None:
            uf.write(struct.pack('i', 0))
        else:
            lmap = len(self.head)
            uf.write(struct.pack('i', lmap))

            for key, val in six.iteritems(self.head):
                write_string(uf, key)

                value, itype, comment = val

                uf.write(struct.pack('i', itype))
                write_string(uf, comment)

                if itype == ITYPE_DOUBLE:
                    uf.write(struct.pack('d', value))
                elif itype == ITYPE_INT:
                    uf.write(struct.pack('i', value))
                elif itype == ITYPE_UINT:
                    uf.write(struct.pack('I', value))
                elif itype == ITYPE_FLOAT:
                    uf.write(struct.pack('f', value))
                elif itype == ITYPE_STRING:
                    write_string(uf, value)
                elif itype == ITYPE_BOOL:
                    uf.write(struct.pack('B', value))
                elif itype == ITYPE_DIR:
                    pass
                elif itype == ITYPE_TIME:
                    uf.write(struct.pack('i', value[0]))
                    uf.write(struct.pack('d', value[1]))
                elif itype == ITYPE_DVECTOR:
                    uf.write(struct.pack('i', len(value)))
                    uf.write(struct.pack(str(len(value)) + 'd', *value))
                elif itype == ITYPE_UCHAR:
                    uf.write(struct.pack('B', value))
                elif itype == ITYPE_USINT:
                    uf.write(struct.pack('H', value))
                elif itype == ITYPE_IVECTOR:
                    uf.write(struct.pack('i', len(value)))
                    uf.write(struct.pack(str(len(value)) + 'i', *value))
                elif itype == ITYPE_FVECTOR:
                    uf.write(struct.pack('i', len(value)))
                    uf.write(struct.pack(str(len(value)) + 'f', *value))
                else:
                    raise UltracamError('Hitem: type =' + str(itype) +
                                        'not recognised')

        # number of CCDs
        nccd = len(self)
        uf.write(struct.pack('i', nccd))

        iout = 0 if self.anyFloat() else 1

        for ccd in self._data:

            # number of windows
            nwin = len(ccd)
            uf.write(struct.pack('i', nwin))

            for win in ccd:
                uf.write(
                    struct.pack('9i', win.llx, win.lly, win.nx, win.ny,
                                win.xbin, win.ybin, ccd.nxmax, ccd.nymax,
                                iout))
                if iout == 0:
                    win.astype(np.float32).tofile(uf)
                elif iout == 1:
                    win.astype(np.uint16).tofile(uf)

        uf.close()