Example #1
0
def hpackage(args=None):
    """``hpackage runs dname tar``

    ``hpackage`` looks for standard reduce data products and bundles
    them up into a single directory and optionally creates a tar
    file. The idea is to copy all the files needed to be able to
    re-run the reduction with the pipeline, while also adding a few
    helpful extras where possible. Given 'run123' for instance, it
    looks for:

      run123.hcm -- typically the result from a run of |averun|
      run123.ape -- file of photometric apertures
      run123.red -- reduce file as made by |genred|
      run123.log -- result from |reduce|

    It also looks for calibration files inside the reduce file and
    copies them. It requires them to be within the same directory and
    will fail if they are not.

    It produces several extra files which are:

      run123.fits -- FITS version of the log file

      run123_ccd1.fits -- joined-up ds9-able version of run123.hcm
                          (and ccd2 etc) [but only if the windows are
                          in sync.

      run123_ccd1.reg -- ds9-region file representing the apertures
                         from run123.ape [see above re synced windows]

      README -- a file of explanation.

    The files are all copied to a temporary directory. 

    Arguments:

      run : str
         Series of run names of the ones to copy, separated by spaces.

      dname : str
         Name for the directory to store all files forming the root of
         any output tar file created from it.

      tar : bool
         Make a tar.gz file of the directory at the end; the directory and
         the files in it will be deleted. Otherwise, no tar file is made and
         the directory is left untouched. The directory will however be deleted
         if the program is aborted early.

    """

    command, args = utils.script_args(args)
    FEXTS = (hcam.HCAM, hcam.APER, hcam.LOG, hcam.RED)

    # get the inputs
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("runs", Cline.LOCAL, Cline.PROMPT)
        cl.register("dname", Cline.LOCAL, Cline.PROMPT)
        cl.register("tar", Cline.LOCAL, Cline.PROMPT)

        runs = cl.get_value(
            "runs", "run names [space separated]",
            'run005'
        )
        runs = runs.split()
        for run in runs:
            if os.path.dirname(run) != '':
                raise hcam.HipercamError(
                    'hpackage only runs on files in the working directory'
                )
            for fext in FEXTS:
                if not os.path.exists(run + fext):
                    raise hcam.HipercamError(
                        f'could not find {run+fext}'
                    )

        dname = cl.get_value(
            "dname", "name of directory for storage of files (will be used to any tar file as well)",
            'hdata'
        )

        tar = cl.get_value(
            "tar", "make a tar file (and delete temporary directory at end)?", True
        )

    # Make name of temporary directory
    tdir = utils.temp_dir()
    tmpdir = os.path.join(tdir, dname)

    with CleanUp(tmpdir, tar) as cleanup:

        # create directory
        os.makedirs(tmpdir, exist_ok=True)
        print(f'Will write files to {tmpdir}')

        # Get on
        for run in runs:

            # strip extension
            root = os.path.splitext(run)[0]

            # need to read the file to determine
            # the number of CCDs
            print(
                run,root,utils.add_extension(run,hcam.HCAM)
            )
            mccd = hcam.MCCD.read(
                utils.add_extension(run,hcam.HCAM)
            )

            try:
                # convert the  hcm and ape files using joinup
                args = [
                    None,'prompt','list','hf',run,'no'
                ]
                if len(mccd) > 1:
                    args += ['0']
                args += [
                    root + hcam.APER,
                    'none','none','none','none','no',
                    'float32',str(100),str(100),
                    'no','rice',tmpdir
                ]
                hcam.scripts.joinup(args)
            except:
                print('failed to joinup hcm / aper files')

            # convert log to fits as well
            args = [
                None,'prompt',run,'h',tmpdir
            ]
            hcam.scripts.hlog2fits(args)

            # copy standard files over
            for fext in FEXTS:
                source = utils.add_extension(root,fext)
                target = os.path.join(tmpdir,source)
                shutil.copyfile(source, target)
                print(f'copied {source} to {target}')

            # now the calibrations
            rfile = hcam.reduction.Rfile.read(run + hcam.RED)
            csec = rfile['calibration']
            if rfile.bias is not None:
                source = utils.add_extension(
                    csec['bias'], hcam.HCAM
                )
                if os.path.dirname(source) != '':
                    raise HipercamError(
                        f'bias = {source} is not in the present working directory'
                    )
                target = os.path.join(tmpdir,source)
                shutil.copyfile(source, target)
                print(f'copied {source} to {target}')

            if rfile.dark is not None:
                source = utils.add_extension(
                    csec['dark'], hcam.HCAM
                )
                if os.path.dirname(source) != '':
                    raise HipercamError(
                        f'dark = {source} is not in the present working directory'
                    )
                target = os.path.join(tmpdir,source)
                shutil.copyfile(source, target)
                print(f'copied {source} to {target}')

            if rfile.flat is not None:
                source = utils.add_extension(
                    csec['flat'], hcam.HCAM
                )
                if os.path.dirname(source) != '':
                    raise HipercamError(
                        f'flat = {source} is not in the present working directory'
                    )
                target = os.path.join(tmpdir,source)
                shutil.copyfile(source, target)
                print(f'copied {source} to {target}')

            if rfile.fmap is not None:

                source = utils.add_extension(
                    csec['fmap'], hcam.HCAM
                )
                if os.path.dirname(source) != '':
                    raise HipercamError(
                        f'fringe map = {source} is not in the present working directory'
                    )
                target = os.path.join(tmpdir,source)
                shutil.copyfile(source, target)
                print(f'copied {source} to {target}')

                if rfile.fpair is not None:

                    source = utils.add_extension(
                        csec['fpair'], hcam.FRNG
                    )
                    if os.path.dirname(source) != '':
                        raise HipercamError(
                            f'fringe peak/trough pair file = {source}'
                            ' is not in the present working directory'
                        )
                    target = os.path.join(tmpdir,source)
                    shutil.copyfile(source, target)
                    print(f'copied {source} to {target}')

        readme = os.path.join(tmpdir,'README')
        with open(readme,'w') as fp:
            fp.write(README)

        # tar up the results
        args = ['tar','cvfz',f'{dname}.tar.gz','-C',tdir,dname]
        subprocess.run(args)
Example #2
0
def daophot(cnam, ccd, xlo, xhi, ylo, yhi, niters, method, fwhm, beta, gfac,
            thresh, rejthresh):
    """
    Perform iterative PSF photometry and star finding on region of CCD
    """
    print(xlo, ylo, xhi, yhi)
    # first check we are within a single window
    wnam1 = ccd.inside(xlo, ylo, 2)
    wnam2 = ccd.inside(xhi, yhi, 2)
    if wnam1 != wnam2:
        raise hcam.HipercamError(
            'PSF photometry cannot currently be run across seperate windows')
    wnam = wnam1
    print(wnam)
    # background stats from whole windpw
    # estimate background RMS
    wind = ccd[wnam]

    rms_func = MADStdBackgroundRMS(sigma_clip=SigmaClip(sigma=rejthresh))
    bkg_rms = rms_func(wind.data)
    bkg_func = MMMBackground(sigma_clip=SigmaClip(sigma=rejthresh))
    bkg = bkg_func(wind.data)
    print('  Background estimate = {}, BKG RMS = {}'.format(bkg, bkg_rms))

    # crop window to ROI
    wind = ccd[wnam].window(xlo, xhi, ylo, yhi)

    # correct FWHM for binning
    fwhm /= wind.xbin
    if method == 'm':
        psf_model = MoffatPSF(fwhm, beta)
        print('  FWHM = {:.1f}, BETA={:.1f}'.format(fwhm, beta))
    else:
        psf_model = IntegratedGaussianPRF(sigma=fwhm * gaussian_fwhm_to_sigma)
        print('  FWHM = {:.1f}'.format(fwhm))

    # region to extract around positions for fits
    fitshape = int(5 * fwhm)
    # ensure odd
    if fitshape % 2 == 0:
        fitshape += 1

    photometry_task = DAOPhotPSFPhotometry(gfac * fwhm,
                                           thresh * bkg_rms,
                                           fwhm,
                                           psf_model,
                                           fitshape,
                                           niters=niters,
                                           sigma=rejthresh)

    with warnings.catch_warnings():
        warnings.simplefilter('ignore')
        results = photometry_task(wind.data - bkg)

    # filter out junk fits
    tiny = 1e-30
    bad_errs = (results['flux_unc'] < tiny) | (results['x_0_unc'] < tiny) | (
        results['y_0_unc'] < tiny)
    results = results[~bad_errs]

    results.write('table_{}.fits'.format(cnam))
    print('  found {} stars'.format(len(results)))

    xlocs, ylocs = results['x_fit'], results['y_fit']

    # convert to device coordinates
    xlocs = wind.x(xlocs)
    ylocs = wind.y(ylocs)
    return xlocs, ylocs
Example #3
0
def filtid(args=None):
    """``filtid [source] (run first last [twait tmax] |
    flist) trim ([ncol nrow]) ccdref maxlev plot``

    Computes the ratio of the mean values in each CCD versus those of a reference CCD.
    This is supposed to be applied to flat fields. The hope is that this might be
    characteristic of the filter combination. Obviously requires > 1 CCD, i.e. not
    ULTRASPEC.

    Parameters:

        source : str [hidden]
           Data source, five options:

             |  'hs' : HiPERCAM server
             |  'hl' : local HiPERCAM FITS file
             |  'us' : ULTRACAM server
             |  'ul' : local ULTRACAM .xml/.dat files
             |  'hf' : list of HiPERCAM hcm FITS-format files

           'hf' is used to look at sets of frames generated by 'grab'
           or converted from foreign data formats. The standard
           start-off default for ``source'' can be set using the
           environment variable HIPERCAM_DEFAULT_SOURCE. e.g. in bash
           :code:`export HIPERCAM_DEFAULT_SOURCE="us"` would ensure it
           always started with the ULTRACAM server by default. If
           unspecified, it defaults to 'hl'.

        run : str [if source ends 's' or 'l']
           run number to access, e.g. 'run034'

        flist : str [if source ends 'f']
           name of file list

        first : int [if source ends 's' or 'l']
           exposure number to start from. 1 = first frame; set = 0 to always
           try to get the most recent frame (if it has changed).  For data
           from the |hiper| server, a negative number tries to get a frame not
           quite at the end.  i.e. -10 will try to get 10 from the last
           frame. This is mainly to sidestep a difficult bug with the
           acquisition system.

        last : int [if source ends 's' or 'l']
           last frame to reduce. 0 to just continue until the end.

        twait : float [if source ends 's' or 'l'; hidden]
           time to wait between attempts to find a new exposure, seconds.

        tmax : float [if source ends 's' or 'l'; hidden]
           maximum time to wait between attempts to find a new exposure,
           seconds.

        trim : bool [if source starts with 'u']
           True to trim columns and/or rows off the edges of windows nearest
           the readout which can sometimes contain bad data.

        ncol : int [if trim, hidden]
           Number of columns to remove (on left of left-hand window, and right
           of right-hand windows)

        nrow : int [if trim, hidden]
           Number of rows to remove (bottom of windows)

        ccdref : str
           The reference CCD (usually choose the g-band one)

        bias : str
           bias frame to subtract (required)

        lower : list of floats
           Lower limits to the mean count level for a flat to be
           included (after bias subtraction).  Should be the same
           number as the number of CCDs, and will be assumed to be in
           the same order. Separate with spaces. Prevents low exposure
           data from being included.

        upper : list of floats
           Upper limits to the mean count level for a flat to be
           included.  Should be the same number as the selected CCDs,
           and will be assumed to be in the same order. Use to
           eliminate saturated, peppered or non-linear
           frames. Suggested hipercam values: 58000, 58000, 58000,
           40000 and 40000 for CCDs 1, 2, 3, 4 and 5. Enter values
           separated by spaces.  ULTRACAM values 49000, 29000, 27000
           for CCDs 1, 2 and 3.

        plot: bool
           Plot the fit or not.

    .. Note::

       This is currently adapted specifically for ULTRACAM data

    """

    command, args = utils.script_args(args)

    # get the inputs
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("source", Cline.GLOBAL, Cline.HIDE)
        cl.register("run", Cline.GLOBAL, Cline.PROMPT)
        cl.register("first", Cline.LOCAL, Cline.PROMPT)
        cl.register("last", Cline.LOCAL, Cline.PROMPT)
        cl.register("trim", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ncol", Cline.GLOBAL, Cline.HIDE)
        cl.register("nrow", Cline.GLOBAL, Cline.HIDE)
        cl.register("twait", Cline.LOCAL, Cline.HIDE)
        cl.register("tmax", Cline.LOCAL, Cline.HIDE)
        cl.register("flist", Cline.LOCAL, Cline.PROMPT)
        cl.register("ccdref", Cline.LOCAL, Cline.PROMPT)
        cl.register("bias", Cline.LOCAL, Cline.PROMPT)
        cl.register("lower", Cline.LOCAL, Cline.PROMPT)
        cl.register("upper", Cline.LOCAL, Cline.PROMPT)
        cl.register("plot", Cline.LOCAL, Cline.PROMPT)

        # get inputs
        default_source = os.environ.get('HIPERCAM_DEFAULT_SOURCE', 'hl')
        source = cl.get_value(
            "source",
            "data source [hs, hl, us, ul, hf]",
            default_source,
            lvals=("hs", "hl", "us", "ul", "hf"),
        )

        # set some flags
        server_or_local = source.endswith("s") or source.endswith("l")

        if server_or_local:
            resource = cl.get_value("run", "run name", "run005")
            first = cl.get_value("first", "first frame to consider", 1, 1)
            last = cl.get_value("last", "last frame to consider", 0, 0)
            if last and last < first + 2:
                raise hcam.HipercamError(
                    "Must consider at least 2 exposures, and preferably more")

            twait = cl.get_value("twait",
                                 "time to wait for a new frame [secs]", 1.0,
                                 0.0)
            tmax = cl.get_value("tmax",
                                "maximum time to wait for a new frame [secs]",
                                10.0, 0.0)

        else:
            resource = cl.get_value("flist", "file list",
                                    cline.Fname("files.lis", hcam.LIST))
            first = 1

        trim = cl.get_value("trim", "do you want to trim edges of windows?",
                            True)
        if trim:
            ncol = cl.get_value("ncol",
                                "number of columns to trim from windows", 0)
            nrow = cl.get_value("nrow", "number of rows to trim from windows",
                                0)

        # define the panel grid. first get the labels and maximum dimensions
        ccdinf = spooler.get_ccd_pars(source, resource)

        if len(ccdinf) == 1:
            raise hcam.HipercamError(
                "Only one CCD; this routine only works for > 1 CCD")

        ccds = list(ccdinf.keys())
        ccdref = cl.get_value("ccdref",
                              "the reference CCD for the ratios",
                              ccds[1],
                              lvals=ccds)

        bias = cl.get_value("bias", "bias frame",
                            cline.Fname("bias", hcam.HCAM))

        # read the bias frame
        bias = hcam.MCCD.read(bias)
        if len(bias) != len(ccds):
            raise ValueError('bias has an incompatible number of CCDs')

        # need to check that the default has the right number of items, if not,
        # over-ride it
        lowers = cl.get_default("lower")
        if lowers is not None and len(lowers) != len(ccds):
            cl.set_default("lower", len(ccds) * (5000, ))

        lowers = cl.get_value(
            "lower",
            "lower limits on mean count level for included flats, 1 per CCD",
            len(ccds) * (5000, ),
        )
        lowers = {k: v for k, v in zip(ccds, lowers)}

        uppers = cl.get_default("upper")
        if uppers is not None and len(uppers) != len(ccds):
            cl.set_default("upper", len(ccds) * (50000, ))
        uppers = cl.get_value(
            "upper",
            "lower limits on mean count level for included flats, 1 per CCD",
            len(ccds) * (50000, ),
        )
        uppers = {k: v for k, v in zip(ccds, uppers)}

        plot = cl.get_value("plot", "plot the results?", True)

    ################################################################
    #
    # all the inputs have now been obtained. Get on with doing stuff

    total_time = 0.
    xdata, ydata = {}, {}

    # only want to look at CCDs other than ccdref
    ccds.remove(ccdref)

    # access images
    with spooler.data_source(source, resource, first, full=False) as spool:

        # 'spool' is an iterable source of MCCDs
        nframe = 0
        for mccd in spool:

            if server_or_local:
                # Handle the waiting game ...
                give_up, try_again, total_time = spooler.hang_about(
                    mccd, twait, tmax, total_time)

                if give_up:
                    print("rtplot stopped")
                    break
                elif try_again:
                    continue

            # Trim the frames: ULTRACAM windowed data has bad columns
            # and rows on the sides of windows closest to the readout
            # which can badly affect reduction. This option strips
            # them.
            if trim:
                hcam.ccd.trim_ultracam(mccd, ncol, nrow)

            if nframe == 0:
                # crop the bias on the first frame only
                bias = bias.crop(mccd)

            # bias subtraction
            mccd -= bias

            # indicate progress
            tstamp = Time(mccd.head["TIMSTAMP"], format="isot", precision=3)
            print(f"{mccd.head.get('NFRAME',nframe+1)}, utc= {tstamp.iso} " +
                  f"({'ok' if mccd.head.get('GOODTIME', True) else 'nok'})")

            # add in data if it is in range
            xd = mccd[ccdref].mean()
            if xd > lowers[ccdref] and xd < uppers[ccdref]:
                for cnam in ccds:
                    yd = mccd[cnam].mean()
                    if yd > lowers[cnam] and yd < uppers[cnam]:
                        if cnam in xdata:
                            xdata[cnam].append(xd)
                            ydata[cnam].append(yd)
                        else:
                            xdata[cnam] = [xd]
                            ydata[cnam] = [yd]

            if last and nframe == last:
                break

            # update the frame number
            nframe += 1

    print()
    invalid = True
    for cnam in ccds:
        if cnam in xdata:
            xdata[cnam] = np.array(xdata[cnam])
            ydata[cnam] = np.array(ydata[cnam])
            print(
                f'{cnam}-vs-{ccdref}: found {len(xdata[cnam])} valid points ranging from'
                +
                f' {xdata[cnam].min():.1f} to {xdata[cnam].max():.1f} (CCD {ccdref}), '
                +
                f' {ydata[cnam].min():.1f} to {ydata[cnam].max():.1f} (CCD {cnam})'
            )
            invalid = False

    if invalid:
        raise hcam.HipercamError(f"No valid points found at all")

    print()
    if plot:
        fig, axes = plt.subplots(len(ccds), 1, sharex=True)

        for cnam, ax in zip(ccds, axes):
            if cnam in xdata:
                xds = xdata[cnam]
                ratios = ydata[cnam] / xds
                mratio = ratios.mean()
                ax.plot(xds, ratios, '.g')
                ax.plot([xds.min(), xds.max()], [mratio, mratio], 'r--')
                ax.set_ylabel(f'Ratio CCD {cnam} / CCD {ccdref}')
                if cnam == ccds[-1]:
                    ax.set_xlabel(f'CCD = {ccdref} counts/pixel')
                print(f'{cnam}-vs-{ccdref} = {mratio:.4f}')
        plt.show()

    else:
        for cnam in ccds:
            if cnam in xdata:
                ratios = ydata[cnam] / xdata[cnam]
                mratio = ratios.mean()
                print(f'{cnam}-vs-{ccdref} = {mratio:.4f}')
Example #4
0
def combine(args=None):
    """``combine list bias dark flat method (sigma) adjust (usemean) [plot clobber]
    output``

    Combines a series of images defined by a list using median or clipped
    mean combination. Only combines those CCDs for which is_data() is true
    (i.e. it skips blank frames caused by NSKIP / NBLUE options)

    Parameters:

        list : string
           list of hcm files with images to combine. The formats of the
           images should all match

        bias : string
           Name of bias frame to subtract, 'none' to ignore.

        dark : string
           Name of dark frame to subtract, 'none' to ignore.

        flat : string
           Name of flat field frame to subtract, 'none' to ignore.

        method  : string
           'm' for median, 'c' for clipped mean. See below for pros and cons.

        sigma   : float [if method == 'c']
           With clipped mean combination, pixels that deviate by more than
           sigma RMS from the mean are kicked out. This is carried out in an
           iterative manner. sigma <= 0 implies no rejection, just a straight
           average. sigma=3 is typical.

        adjust  : string
           adjustments to make: 'i' = ignore; 'n' = normalise the mean or
           median of all frames to match the first; 'b' = add offsets so that
           the mean or median of all frames is the same as the first. Option
           'n' is useful for twilight flats and fringe frames; 'b' is good
           for combining biases.

        usemean : bool [if adjust == 'n' or 'b']
           True to use the mean rather than the median for normalisation or
           biass offsetting.

        plot    : bool [hidden, if adjust == 'n' or 'b'; defaults to False]
           make a plot of the mean versus frame number. This can provide a
           quick check that the frames are not too different.

        clobber : bool [hidden]
           clobber any pre-existing output files

        output  : string
           output file

       Clipped mean can work well for large numbers of frames but gets worse
       for small numbers as the RMS can be heavily influenced by a single bad
       value. The median can be better in such cases, but has the downside of
       digitisation noise. For instance, the average of 100 bias frames could
       have a noise level significantly below 1 count, depending upon the
       readout noise, and the +/- 0.5 count uncertainty of median combination
       may be worse than this.

    .. Note::

       This routine reads all inputs into memory, so can be a bit of a
       hog. However, it does so one CCD at a time to alleviate this. It will
       fail if it cannot find a valid frame for any CCD.

    """

    command, args = utils.script_args(args)

    # get the inputs
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("list", Cline.GLOBAL, Cline.PROMPT)
        cl.register("bias", Cline.LOCAL, Cline.PROMPT)
        cl.register("dark", Cline.LOCAL, Cline.PROMPT)
        cl.register("flat", Cline.LOCAL, Cline.PROMPT)
        cl.register("method", Cline.LOCAL, Cline.PROMPT)
        cl.register("sigma", Cline.LOCAL, Cline.PROMPT)
        cl.register("adjust", Cline.LOCAL, Cline.PROMPT)
        cl.register("usemean", Cline.LOCAL, Cline.PROMPT)
        cl.register("plot", Cline.LOCAL, Cline.HIDE)
        cl.register("clobber", Cline.LOCAL, Cline.HIDE)
        cl.register("output", Cline.LOCAL, Cline.PROMPT)

        # get inputs
        flist = cl.get_value("list", "list of files to combine",
                             cline.Fname("files", hcam.LIST))

        # bias frame (if any)
        bias = cl.get_value(
            "bias",
            "bias frame ['none' to ignore]",
            cline.Fname("bias", hcam.HCAM),
            ignore="none",
        )
        if bias is not None:
            # read the bias frame
            bias = hcam.MCCD.read(bias)

        # dark frame (if any)
        dark = cl.get_value(
            "dark",
            "dark frame ['none' to ignore]",
            cline.Fname("dark", hcam.HCAM),
            ignore="none",
        )
        if dark is not None:
            # read the dark frame
            dark = hcam.MCCD.read(dark)

        # flat frame (if any)
        flat = cl.get_value(
            "flat",
            "flat frame ['none' to ignore]",
            cline.Fname("flat", hcam.HCAM),
            ignore="none",
        )
        if flat is not None:
            # read the flat frame
            flat = hcam.MCCD.read(flat)

        method = cl.get_value("method",
                              "c(lipped mean), m(edian)",
                              "c",
                              lvals=("c", "m"))

        if method == "c":
            sigma = cl.get_value("sigma", "number of RMS deviations to clip",
                                 3.0)

        adjust = cl.get_value(
            "adjust",
            "i(gnore), n(ormalise), b(ias offsets)",
            "i",
            lvals=("i", "n", "b"),
        )

        if adjust == "n" or adjust == "b":

            usemean = cl.get_value(
                "usemean",
                "use the mean for normalisation / offsetting [else median]",
                True,
            )

            plot = cl.get_value(
                "plot",
                "plot mean levels versus frame number?"
                if usemean else "plot median levels versus frame number?",
                False,
            )

        else:
            plot = False
            usemean = False

        clobber = cl.get_value("clobber",
                               "clobber any pre-existing files on output",
                               False)

        outfile = cl.get_value(
            "output",
            "output file",
            cline.Fname("hcam", hcam.HCAM,
                        cline.Fname.NEW if clobber else cline.Fname.NOCLOBBER),
        )

    # inputs done with

    # Read the first file of the list to act as a template
    # for the CCD names etc.
    with open(flist) as fin:
        for line in fin:
            if not line.startswith("#") and not line.isspace():
                template_name = line.strip()
                break
        else:
            raise hcam.HipercamError("List = {:s} is empty".format(flist))

    template = hcam.MCCD.read(utils.add_extension(template_name, hcam.HCAM))

    if bias is not None:
        # crop the bias
        bias = bias.crop(template)

    if dark is not None:
        # crop the dark
        dark = dark.crop(template)

    if flat is not None:
        # crop the flat
        flat = flat.crop(template)

    # Now process each file CCD by CCD to reduce the memory
    # footprint
    for cnam in template:

        # Read files into memory, insisting that they
        # all have the same set of CCDs
        print("\nLoading all CCDs labelled '{:s}' from {:s}".format(
            cnam, flist))

        ccds, means = [], []
        nrej, ntot = 0, 0
        with spooler.HcamListSpool(flist, cnam) as spool:

            if bias is not None:
                # extract relevant CCD from the bias
                bccd = bias[cnam]
                bexpose = bias.head.get("EXPTIME", 0.0)
            else:
                bexpose = 0.0

            if dark is not None:
                # extract relevant CCD from the dark
                dccd = dark[cnam]
                dexpose = dark.head["EXPTIME"]

            if flat is not None:
                # extract relevant CCD from the flat
                fccd = flat[cnam]

            mean = None
            for ccd in spool:

                if ccd.is_data():

                    if bias is not None:
                        # subtract bias
                        ccd -= bccd

                    if dark is not None:
                        # subtract dark
                        scale = (ccd.head["EXPTIME"] - bexpose) / dexpose
                        ccd -= scale * dccd

                    if flat is not None:
                        # apply flat
                        ccd /= fccd

                    # keep the result
                    ccds.append(ccd)

                    if (adjust == "b" or adjust == "n") and mean is None:
                        # store the first mean [median]
                        if usemean:
                            mean = ccd.mean()
                        else:
                            mean = ccd.median()

            if len(ccds) == 0:
                raise hcam.HipercamError("Found no valid examples of CCD {:s}"
                                         " in list = {:s}".format(cnam, flist))

            else:
                print("Loaded {:d} CCDs".format(len(ccds)))

        if adjust == "b" or adjust == "n":

            # adjust the means
            print("Computing and adjusting their mean levels")

            # now carry out the adjustments
            for ccd in ccds:
                if usemean:
                    cmean = ccd.mean()
                else:
                    cmean = ccd.median()

                means.append(cmean)
                if adjust == "b":
                    ccd += mean - cmean
                elif adjust == "n":
                    ccd *= mean / cmean

            if plot:
                plt.plot(means)
                plt.plot(means, ".k")
                plt.text(len(means) + 1,
                         means[-1],
                         cnam,
                         va="center",
                         ha="left")

        # Finally, combine
        if method == "m":
            print("Combining them (median) and storing the result")
        elif method == "c":
            print("Combining them (clipped mean) and storing the result")
        else:
            raise NotImplementedError(
                "method = {:s} not implemented".format(method))

        for wnam, wind in template[cnam].items():

            # build list of all data arrays
            arrs = [ccd[wnam].data for ccd in ccds]
            arr3d = np.stack(arrs)

            # at this point, arr3d is a 3D array, with the first dimension
            # (axis=0) running over the images. We want to average / median
            # over this axis.

            if method == "m":
                # median
                wind.data = np.median(arr3d, axis=0)

            elif method == "c":
                # Cython routine avgstd requires np.float32 input
                arr3d = arr3d.astype(np.float32)
                if sigma > 0.0:
                    avg, std, num = support.avgstd(arr3d, sigma)
                    nrej += len(ccds) * num.size - num.sum()
                    ntot += len(ccds) * num.size
                else:
                    avg = np.mean(arr3d, axis=0)
                wind.data = avg

        # Add history
        if method == "m":
            template[cnam].head.add_history(
                "Median combine of {:d} images".format(len(ccds)))
        elif method == "c":
            print("Rejected {:d} pixels = {:.3f}% of the total".format(
                nrej, 100 * nrej / ntot))
            template[cnam].head.add_history(
                "Clipped mean combine of {:d} images, sigma = {:.1f}".format(
                    len(ccds), sigma))

    # write out
    template.write(outfile, clobber)
    print("\nFinal result written to {:s}".format(outfile))

    # finally
    if plot:
        plt.xlabel("Frame number")
        plt.ylabel("Mean counts")
        plt.show()
Example #5
0
def hlog2fits(args=None):
    """``hlog2fits log [origin dir]``

    Converts a |hiper| ASCII log into a FITS file. As well as a modest
    reduction in file size (~40%, the ASCII logs are written relatively
    efficiently), the resulting file is faster to read than the ASCII log
    so this may be useful for very large log files [test of 78,000 frame file:
    12.9 seconds to read the ASCII file, 1.9 to read the FITS version]. The FITS
    log is also much easier to understand than the ASCII files, but they don't
    have all the header information, so are not a replacement. At the
    moment no significant header information is transferred beyond the CCD
    names. Each CCD appears as a single binary table, starting at the second
    HDU (or HDU 1 if you number them 0,1,2 ..). This can be read using
    :meth:`hipercam.hlog.Hlog.from_fits`.

    Parameters:

      log : str
         name of the log file (should end .log). The output FITS file
         will have the same root name but end .fits. The routine will abort
         if there is a pre-existing file of the same name.

      origin : str [hidden]
         'h' or 'u' depending upon whether the log file was created with
         the hipercam or old ultracam pipeline. Defaults to 'h'.

      dir : str [hidden]
         directory for the output; defaults to the present working directory

    NB Because of the danger of over-writing raw data (also ends
    .fits), this routine will not over-write pre-existing files. You
    should delete clashing files if you really want to proceed.

    """

    command, args = utils.script_args(args)

    # get input section
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("log", Cline.LOCAL, Cline.PROMPT)
        cl.register("origin", Cline.LOCAL, Cline.HIDE)
        cl.register("dir", Cline.LOCAL, Cline.HIDE)

        # get inputs
        log = cl.get_value(
            "log",
            'name of log file from "reduce" to convert to FITS',
            cline.Fname("red", hcam.LOG),
        )

        cl.set_default('origin', 'h')
        origin = cl.get_value("origin",
                              "h(ipercam) or u(ltracam) pipeline?",
                              "h",
                              lvals=["h", "u"])

        cl.set_default('dir', '.')
        dir = cl.get_value(
            "dir",
            "directory for output",
            ".",
        )

    root = os.path.splitext(os.path.basename(log))[0]
    oname = os.path.join(dir, root + ".fits")
    if os.path.exists(oname):
        raise hcam.HipercamError(f"A file called {oname} already exists and"
                                 " will not be over-written; aborting")

    # Read in the ASCII log
    if origin == "h":
        hlg = hcam.hlog.Hlog.rascii(log)
    elif origin == "u":
        hlg = hcam.hlog.Hlog.fulog(log)

    print(f"Loaded ASCII log = {log}")

    # Generate HDU list

    # First the primary HDU (no data)
    phdr = fits.Header()
    phdr["LOGFILE"] = (os.path.basename(log), "Original log file")
    phdu = fits.PrimaryHDU(header=phdr)
    hdul = [
        phdu,
    ]

    # Now a BinTable for each CCD
    for cnam in sorted(hlg):
        hdr = fits.Header()
        hdr["CCDNAME"] = (cnam, "CCD name")
        hdul.append(fits.BinTableHDU(hlg[cnam], header=hdr,
                                     name=f"CCD {cnam}"))

    hdul = fits.HDUList(hdul)

    # finally write to disk
    print(f"Writing to disk in file = {oname}")
    hdul.writeto(oname)

    print(f"Converted {log} to {oname}")
Example #6
0
def makemovie(args=None):
    """``makemovie [source] (run first last | flist) trim ([ncol nrow])
    (ccd (nx)) bias flat defect log (targ comp ymin ymax yscales
    yoffset location fraction lpad) cmap width height dstore ndigit fext msub iset
    (ilo ihi | plo phi) xlo xhi ylo yhi [dpi (style (ms lw))]``

    ``makemovie`` is for generating stills to combine into a movie for
    presentations. It can optionally also read a log file from the run
    to display an evolving light curve. There are lots of fiddly
    parameters mostly to do with the plot positioning, so try it out
    on a small number of frames first before going mad.

    Parameters:

        source : str [hidden]
           Data source, five options:

             |  'hs' : HiPERCAM server
             |  'hl' : local HiPERCAM FITS file
             |  'us' : ULTRACAM server
             |  'ul' : local ULTRACAM .xml/.dat files
             |  'hf' : list of HiPERCAM hcm FITS-format files

           'hf' is used to look at sets of frames generated by 'grab'
           or converted from foreign data formats. The standard
           start-off default for ``source`` can be set using the
           environment variable HIPERCAM_DEFAULT_SOURCE. e.g. in bash
           :code:`export HIPERCAM_DEFAULT_SOURCE="us"` would ensure it
           always started with the ULTRACAM server by default. If
           unspecified, it defaults to 'hl'.

        run : str [if source ends 's' or 'l']
           run number to access, e.g. 'run034'

        flist : str [if source ends 'f']
           name of file list

        first : int [if source ends 's' or 'l']
           exposure number to start from. 1 = first frame.

        last : int [if source ends 's' or 'l']
           last exposure

        trim : bool [if source starts with 'u']
           True to trim columns and/or rows off the edges of windows nearest
           the readout which can sometimes contain bad data.

        ncol : int [if trim, hidden]
           Number of columns to remove (on left of left-hand window, and right
           of right-hand windows)

        nrow : int [if trim, hidden]
           Number of rows to remove (bottom of windows)

        ccd : str
           CCD(s) to plot, '0' for all, '1 3' to plot '1' and '3' only, etc. If
           you plot more than one, then a legend is added to any light curve panel
           to distinguish the light curves.

        nx : int [if more than 1 CCD]
           number of panels across to display for the image display.

        bias : str
           Name of bias frame to subtract, 'none' to ignore.

        dark : str
           Name of dark frame to subtract, 'none' to ignore.

        flat : str
           Name of flat field to divide by, 'none' to ignore. Should normally
           only be used in conjunction with a bias, although it does allow you
           to specify a flat even if you haven't specified a bias.

        defect : str
           Name of defect file, 'none' to ignore.

        fmap : str
           Name of fringe map (see e.g. `makefringe`), 'none' to ignore.

        fpair : str [if fringe is not 'none']
           Name of fringe pair file (see e.g. `setfringe`). Required if
           a fringe map has been specified.

        nhalf : int [if fringe is not 'none', hidden]
           When calculating the differences for fringe measurement,
           a region extending +/-nhalf binned pixels will be used when
           measuring the amplitudes. Basically helps the stats.

        rmin : float [if fringe is not 'none', hidden]
           Minimum individual ratio to accept prior to calculating the overall
           median in order to reduce the effect of outliers. Although all ratios
           should be positive, you might want to set this a little below zero
           to allow for some statistical fluctuation.

        rmax : float [if fringe is not 'none', hidden]
           Maximum individual ratio to accept prior to calculating the overall
           median in order to reduce the effect of outliers. Probably typically
           < 1 if fringe map was created from longer exposure data.

        log : str
           Name of reduce log file for light curve plot, 'none' to ignore

        targ : str [if log defined]
           Target aperture

        comp : str [if log defined]
           Comparison aperture

        ymin : float [if log defined]
           Minimum Y value for light curve plot

        ymax : float [if log defined]
           Maximum Y value for light curve plot

        ynorm : list(float) [if log defined]
           Normalisation factors, one per CCD for light curve plot

        yoffset : list(float) [if log defined]
           Offsets, one per CCD for light curve plot

        location : str [if log defined]
           Offsets, one per CCD for light curve plot

        fraction : float [if log defined]
           Fraction of figure to occupy, by height if location is South,
           by width if it is East

        lpad : tuple(float) [if log defined]
           padding on left, bottom, right and top of light curve plot as fraction
           of allocated width and height

        cmap : str
           The matplotlib colour map to use. "Greys" gives the usual greyscale.
           "none" will give whatever the current default is. Many other choices:
           "viridis", "jet", "hot", "Oranges", etc. Enter an invalid one and
           the program will fail but return a huge list of possibles in the
           process.

        width : float
           plot width in inches.

        height : float
           plot height in inches.

        dstore : str
           root directory for plot files. Will get names like dstore/run003_001.png.

        ndigit : int
           number of digits in frame counter, i.e. the '001' of the previous section.

        fext : str
           file extension 'png', 'jpeg' for images generated

        msub : bool
           subtract the median from each window before scaling for the
           image display or not. This happens after any bias subtraction.

        iset : str [single character]
           determines how the intensities are determined. There are three
           options: 'a' for automatic simply scales from the minimum to the
           maximum value found on a per CCD basis. 'd' for direct just takes
           two numbers from the user. 'p' for percentile dtermines levels
           based upon percentiles determined from the entire CCD on a per CCD
           basis.

        ilo : list(float) [if iset='d']
           lower intensity level, one per image

        ihi : list(float) [if iset='d']
           upper intensity level, one per image

        plo : float [if iset='p']
           lower percentile level

        phi : float [if iset='p']
           upper percentile level

        xlo : float
           left-hand X-limit for plot, initially at least since it is
           possible to re-size. For iset='p' these limits also set the
           region of the frame over which the percentil will be
           calculated. You will usually want yhi-ylo ~ xhi-xlo in
           magnitude because the aspect ratio is preserved.

        xhi : float
           right-hand X-limit for plot (can be < xlo to invert the display)

        ylo : float
           lower Y-limit for plot

        yhi : float
           upper Y-limit for plot (can be < ylo)

        dpi : int [hidden]
           dots per inch of output. Default 72. Allows control over font size versus image size,
           in combination with width and height.

        style : str [hidden, if log defined]
           style for light curves 'dots', 'line', 'both'. The line will be grey for the 'both' option.

        ms : float [hidden, if log defined and style==dots or both]
           markersize. Controls dot size which is useful when fiddling with dpi

        lw : float [hidden, if log defined and style==line or both]
           line width
    """

    command, args = utils.script_args(args)

    # get the inputs
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("source", Cline.GLOBAL, Cline.HIDE)
        cl.register("run", Cline.GLOBAL, Cline.PROMPT)
        cl.register("first", Cline.LOCAL, Cline.PROMPT)
        cl.register("last", Cline.LOCAL, Cline.PROMPT)
        cl.register("trim", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ncol", Cline.GLOBAL, Cline.HIDE)
        cl.register("nrow", Cline.GLOBAL, Cline.HIDE)
        cl.register("flist", Cline.LOCAL, Cline.PROMPT)
        cl.register("ccd", Cline.LOCAL, Cline.PROMPT)
        cl.register("nx", Cline.LOCAL, Cline.PROMPT)
        cl.register("bias", Cline.GLOBAL, Cline.PROMPT)
        cl.register("dark", Cline.GLOBAL, Cline.PROMPT)
        cl.register("flat", Cline.GLOBAL, Cline.PROMPT)
        cl.register("fmap", Cline.GLOBAL, Cline.PROMPT)
        cl.register("fpair", Cline.GLOBAL, Cline.PROMPT)
        cl.register("nhalf", Cline.GLOBAL, Cline.HIDE)
        cl.register("rmin", Cline.GLOBAL, Cline.HIDE)
        cl.register("rmax", Cline.GLOBAL, Cline.HIDE)
        cl.register("defect", Cline.GLOBAL, Cline.PROMPT)
        cl.register("log", Cline.LOCAL, Cline.PROMPT)
        cl.register("targ", Cline.LOCAL, Cline.PROMPT)
        cl.register("comp", Cline.LOCAL, Cline.PROMPT)
        cl.register("ymin", Cline.LOCAL, Cline.PROMPT)
        cl.register("ymax", Cline.LOCAL, Cline.PROMPT)
        cl.register("ynorm", Cline.LOCAL, Cline.PROMPT)
        cl.register("yoffset", Cline.LOCAL, Cline.PROMPT)
        cl.register("location", Cline.LOCAL, Cline.PROMPT)
        cl.register("fraction", Cline.LOCAL, Cline.PROMPT)
        cl.register("lpad", Cline.LOCAL, Cline.PROMPT)
        cl.register("cmap", Cline.LOCAL, Cline.PROMPT)
        cl.register("width", Cline.LOCAL, Cline.PROMPT)
        cl.register("height", Cline.LOCAL, Cline.PROMPT)
        cl.register("dstore", Cline.LOCAL, Cline.PROMPT)
        cl.register("ndigit", Cline.LOCAL, Cline.PROMPT)
        cl.register("fext", Cline.LOCAL, Cline.PROMPT)
        cl.register("msub", Cline.GLOBAL, Cline.PROMPT)
        cl.register("iset", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ilo", Cline.LOCAL, Cline.PROMPT)
        cl.register("ihi", Cline.LOCAL, Cline.PROMPT)
        cl.register("plo", Cline.GLOBAL, Cline.PROMPT)
        cl.register("phi", Cline.LOCAL, Cline.PROMPT)
        cl.register("xlo", Cline.GLOBAL, Cline.PROMPT)
        cl.register("xhi", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ylo", Cline.GLOBAL, Cline.PROMPT)
        cl.register("yhi", Cline.GLOBAL, Cline.PROMPT)
        cl.register("dpi", Cline.LOCAL, Cline.HIDE)
        cl.register("style", Cline.LOCAL, Cline.HIDE)
        cl.register("ms", Cline.LOCAL, Cline.HIDE)
        cl.register("lw", Cline.LOCAL, Cline.HIDE)

        # get inputs
        default_source = os.environ.get('HIPERCAM_DEFAULT_SOURCE', 'hl')
        source = cl.get_value(
            "source",
            "data source [hs, hl, us, ul, hf]",
            default_source,
            lvals=("hs", "hl", "us", "ul", "hf"),
        )

        # set some flags
        server_or_local = source.endswith("s") or source.endswith("l")

        if server_or_local:
            resource = cl.get_value("run", "run name", "run005")
            first = cl.get_value("first", "first frame to plot", 1)
            last = cl.get_value("last",
                                "last frame to plot [0 to go to the end]",
                                max(1, first), 0)
        else:
            resource = cl.get_value("flist", "file list",
                                    cline.Fname("files.lis", hcam.LIST))
            first = 1

        trim = cl.get_value("trim", "do you want to trim edges of windows?",
                            True)
        if trim:
            ncol = cl.get_value("ncol",
                                "number of columns to trim from windows", 0)
            nrow = cl.get_value("nrow", "number of rows to trim from windows",
                                0)
        else:
            ncol, nrow = None, None

        # define the panel grid. first get the labels and maximum dimensions
        ccdinf = spooler.get_ccd_pars(source, resource)

        nxdef = cl.get_default("nx", 3)

        if len(ccdinf) > 1:
            ccd = cl.get_value("ccd", "CCD(s) to plot [0 for all]", "0")
            if ccd == "0":
                ccds = list(ccdinf.keys())
            else:
                ccds = ccd.split()
                check = set(ccdinf.keys())
                if not set(ccds) <= check:
                    raise hcam.HipercamError(
                        "At least one invalid CCD label supplied")

            if len(ccds) > 1:
                nxdef = min(len(ccds), nxdef)
                cl.set_default("nx", nxdef)
                nx = cl.get_value("nx", "number of panels in X", 3, 1)
            else:
                nx = 1
        else:
            nx = 1
            ccds = list(ccdinf.keys())

        # bias frame (if any)
        bias = cl.get_value(
            "bias",
            "bias frame ['none' to ignore]",
            cline.Fname("bias", hcam.HCAM),
            ignore="none",
        )
        if bias is not None:
            # read the bias frame
            bias = hcam.MCCD.read(bias)
            fprompt = "flat frame ['none' to ignore]"
        else:
            fprompt = "flat frame ['none' is normal choice with no bias]"

        # dark (if any)
        dark = cl.get_value("dark",
                            "dark frame to subtract ['none' to ignore]",
                            cline.Fname("dark", hcam.HCAM),
                            ignore="none")
        if dark is not None:
            # read the dark frame
            dark = hcam.MCCD.read(dark)

        # flat (if any)
        flat = cl.get_value("flat",
                            fprompt,
                            cline.Fname("flat", hcam.HCAM),
                            ignore="none")
        if flat is not None:
            # read the flat frame
            flat = hcam.MCCD.read(flat)

        # fringe file (if any)
        fmap = cl.get_value(
            "fmap",
            "fringe map ['none' to ignore]",
            cline.Fname("fmap", hcam.HCAM),
            ignore="none",
        )
        if fmap is not None:
            # read the fringe map
            fmap = hcam.MCCD.read(fmap)
            fpair = cl.get_value("fpair", "fringe pair file",
                                 cline.Fname("fpair", hcam.FRNG))
            fpair = fringe.MccdFringePair.read(fpair)

            nhalf = cl.get_value("nhalf",
                                 "half-size of fringe measurement regions", 2,
                                 0)
            rmin = cl.get_value("rmin", "minimum fringe pair ratio", -0.2)
            rmax = cl.get_value("rmax", "maximum fringe pair ratio", 1.0)

        # defect file (if any)
        dfct = cl.get_value(
            "defect",
            "defect file ['none' to ignore]",
            cline.Fname("defect", hcam.DFCT),
            ignore="none",
        )
        if dfct is not None:
            # read the defect frame
            dfct = defect.MccdDefect.read(dfct)

        # reduce log file (if any)
        rlog = cl.get_value(
            "log",
            "reduce log file ['none' to ignore]",
            cline.Fname("reduce", hcam.LOG),
            ignore="none",
        )

        if rlog is not None:
            # Read reduce file
            hlg = hlog.Hlog.rascii(rlog)
            targ = cl.get_value("targ", "target aperture", "1")
            comp = cl.get_value("comp", "comparison aperture", "2")
            fmin = cl.get_value("ymin", "minimum Y value for light curve plot",
                                0.)
            fmax = cl.get_value("ymax", "maxmum Y value for light curve plot",
                                1.)

            # need to check that the default has the right number of
            # items, if not overr-ride it
            ynorm = cl.get_default("ynorm")
            if ynorm is not None:
                if len(ynorm) > len(ccds):
                    cl.set_default("ynorm", ynorm[:len(ccds)])
                elif len(ynorm) < len(ccds):
                    cl.set_default("ynorm",
                                   ynorm + (len(ccds) - len(ynorm)) * [1.])

            ynorm = cl.get_value(
                "ynorm",
                "normalisation factors for light curves (one per CCD)",
                len(ccds) * [1.])

            yoffset = cl.get_default("yoffset")
            if yoffset is not None:
                if len(yoffset) > len(ccds):
                    cl.set_default("yoffset", yoffset[:len(ccds)])
                elif len(yoffset) < len(ccds):
                    cl.set_default("yoffset",
                                   yoffset + (len(ccds) - len(yoffset)) * [0.])

            yoffset = cl.get_value(
                "yoffset", "vertical offsets for light curves (one per CCD)",
                len(ccds) * [0.])

            location = cl.get_value(
                "location",
                "position of light curve plot relative to images",
                "s",
                lvals=['s', 'e', 'S', 'E'])
            if location.lower() == 's':
                fraction = cl.get_value(
                    "fraction",
                    "fraction of figure in terms of height occupied by light curve",
                    0.5)
            elif location.lower() == 'e':
                fraction = cl.get_value(
                    "fraction",
                    "fraction of figure in terms of width occupied by light curve",
                    0.67)
            lpad = cl.get_value(
                "lpad",
                "padding (left,bottom,right,top) around light curve plot",
                (0.05, 0.05, 0.02, 0.02))

            # trim down to the specified frames
            lcs = []
            T0, tmax = None, None
            for cnam, yn, yo in zip(ccds, ynorm, yoffset):
                nframes = hlg[cnam]['nframe']
                keep = (nframes >= first) and (last == 0 or (nframes <= last))
                hlg[cnam] = hlg[cnam][keep]
                lc = (hlg.tseries(cnam, targ) /
                      hlg.tseries(cnam, comp)) / yn + yo
                if T0 is None:
                    T0 = lc.t.min()
                lc.ttrans(lambda t: 1440 * (t - T0))
                if tmax is None:
                    tmax = lc.t.max()
                else:
                    tmax = max(tmax, lc.t.max())
                lcs.append((nframes[keep], lc))

        # Some settings for the image plots
        cmap = cl.get_value("cmap",
                            "colour map to use ['none' for mpl default]",
                            "Greys")
        cmap = None if cmap == "none" else cmap

        width = cl.get_value("width", "plot width [inches]", 10., 0.5)

        height = cl.get_value("height", "plot height [inches]", 10., 0.5)

        dstore = cl.get_value("dstore", "directory for images", "tmp")
        if not os.path.isdir(dstore):
            raise hcam.HipercamError(f"'{dstore}' is not a directory")

        ndigit = cl.get_value("ndigit",
                              "number of digits for appended frame counter", 4,
                              1)
        fext = cl.get_value("fext",
                            "file extension for images",
                            "png",
                            lvals=["png", "jpg"])

        # define the display intensities
        msub = cl.get_value("msub", "subtract median from each window?", True)

        iset = cl.get_value(
            "iset",
            "set intensity a(utomatically),"
            " d(irectly) or with p(ercentiles)?",
            "a",
            lvals=["a", "d", "p"],
        )
        iset = iset.lower()

        plo, phi = 5, 95
        ilos, ihis = len(ccds) * [0], len(ccds) * [1000]
        if iset == "d":
            # fiddle with the defaults
            ilo = cl.get_default("ilo")
            if ilo is not None:
                if len(ilo) > len(ccds):
                    cl.set_default("ilo", ilo[:len(ccds)])
                elif len(ilo) < len(ccds):
                    cl.set_default("ilo", ilo + (len(ccds) - len(ilo)) * [0.])

            ihi = cl.get_default("ihi")
            if ihi is not None:
                if len(ihi) > len(ccds):
                    cl.set_default("ihi", ihi[:len(ccds)])
                elif len(ihi) < len(ccds):
                    cl.set_default("ihi", ihi + (len(ccds) - len(ihi))[1000.])

            ilos = cl.get_value("ilo", "lower intensity limit",
                                len(ccds) * [0.])
            ihis = cl.get_value("ihi", "upper intensity limit",
                                len(ccds) * [1000.])

        elif iset == "p":
            plo = cl.get_value("plo", "lower intensity limit percentile", 5.0,
                               0.0, 100.0)
            phi = cl.get_value("phi", "upper intensity limit percentile", 95.0,
                               0.0, 100.0)

        # region to plot
        for i, cnam in enumerate(ccds):
            nxtot, nytot, nxpad, nypad = ccdinf[cnam]
            if i == 0:
                xmin, xmax = float(-nxpad), float(nxtot + nxpad + 1)
                ymin, ymax = float(-nypad), float(nytot + nypad + 1)
            else:
                xmin = min(xmin, float(-nxpad))
                xmax = max(xmax, float(nxtot + nxpad + 1))
                ymin = min(ymin, float(-nypad))
                ymax = max(ymax, float(nytot + nypad + 1))

        xlo = cl.get_value("xlo",
                           "left-hand X value",
                           xmin,
                           xmin,
                           xmax,
                           enforce=False)
        xhi = cl.get_value("xhi",
                           "right-hand X value",
                           xmax,
                           xmin,
                           xmax,
                           enforce=False)
        ylo = cl.get_value("ylo",
                           "lower Y value",
                           ymin,
                           ymin,
                           ymax,
                           enforce=False)
        yhi = cl.get_value("yhi",
                           "upper Y value",
                           ymax,
                           ymin,
                           ymax,
                           enforce=False)
        dpi = cl.get_value("dpi", "dots per inch", 200)
        if rlog is not None:
            style = cl.get_value("style",
                                 "light curve plot style",
                                 "dots",
                                 lvals=('dots', 'line', 'both'))
            ms, lw = 0, 0
            if style == 'dots' or style == 'both':
                ms = cl.get_value("ms", "markersize", 2.)
            if style == 'line' or style == 'both':
                lw = cl.get_value("lw", "line width", 2.)

    ###############################################################################

    # Phew. We finally have all the inputs and now can now display stuff.

    # track which CCDs have been plotted at least once for the profile fits
    nccd = len(ccds)
    plotted = np.array(nccd * [False])
    current_ccds = nccd * [None]
    ny = nccd // nx if nccd % nx == 0 else nccd // nx + 1

    first_fig = True

    # Now go through data
    with spooler.data_source(source, resource, first, full=False) as spool:

        for nframe, mccd in enumerate(spool):

            # Trim the frames: ULTRACAM windowed data has bad columns
            # and rows on the sides of windows closest to the readout
            # which can badly affect reduction. This option strips
            # them.
            if trim:
                hcam.ccd.trim_ultracam(mccd, ncol, nrow)

            # indicate progress
            tstamp = Time(mccd.head["TIMSTAMP"], format="isot", precision=3)
            print(f'{mccd.head.get("NFRAME",nframe+1)}, utc= {tstamp.iso}')

            if nframe == 0:
                # get the bias and flat into shape first time through

                if bias is not None:
                    # crop the bias on the first frame only
                    bias = bias.crop(mccd)
                    bexpose = bias.head.get("EXPTIME", 0.0)
                else:
                    bexpose = 0.

                if dark is not None:
                    # crop the dark on the first frame only
                    dark = dark.crop(mccd)

                if flat is not None:
                    # crop the flat on the first frame only
                    flat = flat.crop(mccd)

                if fmap is not None:
                    # crop the fringe map and pair file
                    fmap = fmap.crop(mccd)
                    fpair = fpair.crop(mccd, nhalf)

            # wind through the CCDs to display, accumulating stuff
            # to send to the plot manager
            message = ""

            skipped = True
            for nc, cnam in enumerate(ccds):
                ccd = mccd[cnam]

                if ccd.is_data():
                    # "is_data" indicates genuine data as opposed to junk
                    # that results from nskip > 0.
                    plotted[nc] = True

                    # subtract the bias
                    if bias is not None:
                        ccd -= bias[cnam]

                    if dark is not None:
                        dexpose = dark.head["EXPTIME"]
                        cexpose = ccd.head["EXPTIME"]
                        scale = (cexpose - bexpose) / dexpose
                        ccd -= scale * dark[cnam]

                    # divide out the flat
                    if flat is not None:
                        ccd /= flat[cnam]

                    # Remove fringes
                    if fmap is not None and cnam in fmap and cnam in fpair:
                        fscale = fpair[cnam].scale(ccd, fmap[cnam], nhalf,
                                                   rmin, rmax)
                        ccd -= fscale * fmap[cnam]

                    if msub:
                        # subtract median from each window
                        for wind in ccd.values():
                            wind -= wind.median()

                    # keep list of current CCDs
                    current_ccds[nc] = ccd

            # at this point current_ccds contains the current set of CCDs to plot

            if plotted.all():

                # Finally have at least one proper exposure of all
                # CCDs and can make plots every time with skipped
                # frames staying unchanged Set up the plot. ny by nx
                # rows x columns of images + 1 for optional light
                # curve which is either South or East of images

                prop_cycle = plt.rcParams['axes.prop_cycle']
                colors = prop_cycle.by_key()['color'][:len(ccds)]

                # create the figure just once to avoid memory
                # problems, i.e. we re-use the same figure for every
                # plot. plt.close() which I thought work instead
                # didn't and the programme went a bit bonkers at the
                # end releasing resources. Putting '0' didn't work either.
                if first_fig:
                    fig = plt.figure(figsize=(width, height))
                    first_fig = False

                if rlog is not None:
                    # plot light curve

                    # first the location
                    if location.lower() == 's':
                        # light curve "South" of the images
                        rect = [
                            lpad[0], fraction * lpad[1], 1 - lpad[0] - lpad[2],
                            fraction * (1 - lpad[1] - lpad[3])
                        ]
                        gs = GridSpec(ny, nx, figure=fig, bottom=fraction)
                    elif location.lower() == 'e':
                        # light curve "East" of the images
                        rect = [
                            1 - fraction + fraction * lpad[0], lpad[1],
                            fraction * (1 - lpad[0] - lpad[2]),
                            1 - lpad[1] - lpad[3]
                        ]
                        gs = GridSpec(ny, nx, right=1 - fraction)

                    # light curve axes
                    ax = fig.add_axes(rect)
                    ax.set_xlim(0, tmax)
                    ax.set_ylim(fmin, fmax)
                    ax.set_xlabel(f'Time [mins, since MJD = {T0:.4f}]')
                    ax.set_ylabel(f'Target / Comparison')
                    ax.tick_params(axis="x", direction="in")
                    ax.tick_params(axis="y", direction="in", rotation=90)
                    ax.tick_params(bottom=True,
                                   top=True,
                                   left=True,
                                   right=True)
                    for cnam, (nframes, lc), col in zip(ccds, lcs, colors):
                        plot = (nframes >= first) & (nframes <= first + nframe)
                        lct = lc[plot]
                        if style == 'dots':
                            fmt = '.'
                            color = None
                        elif style == 'line':
                            fmt = '-'
                            color = None
                            col = None
                        elif style == 'both':
                            fmt = '.-'
                            color = '0.7'

                        lct.mplot(ax,
                                  fmt=fmt,
                                  color=color,
                                  mfc=col,
                                  mec=col,
                                  ms=ms,
                                  lw=lw,
                                  label=f'CCD {cnam}')

                    if len(ccds) > 1:
                        ax.legend(loc='upper right')
                else:
                    # images only
                    gs = GridSpec(ny, nx, figure=fig)

                # plot images
                for n, (cnam, ccd, ilo,
                        ihi) in enumerate(zip(ccds, current_ccds, ilos, ihis)):
                    ax = fig.add_subplot(gs[n // nx, n % nx])
                    mpl.pCcd(ax,
                             ccd,
                             iset,
                             plo,
                             phi,
                             ilo,
                             ihi,
                             f'CCD {cnam}',
                             xlo=xlo,
                             xhi=xhi,
                             ylo=ylo,
                             yhi=yhi,
                             cmap=cmap)
                    ax.set_xlim(xlo, xhi)
                    ax.set_ylim(ylo, yhi)
                    ax.tick_params(axis="x", direction="in")
                    ax.tick_params(axis="y", direction="in", rotation=90)
                    ax.tick_params(bottom=True,
                                   top=True,
                                   left=True,
                                   right=True)

                # save to disk, clear figure
                oname = os.path.join(
                    dstore,
                    f'{os.path.basename(resource)}_{first+nframe:0{ndigit}d}.{fext}'
                )
                plt.savefig(oname, dpi=dpi)
                plt.clf()
                print(f'   written figure to {oname}')

            if last and nframe + first == last:
                break
Example #7
0
def grab(args=None):
    """``grab [source] run [temp] (ndigit) first last (trim [ncol nrow])
    [twait tmax] bias [dtype]``

    This downloads a sequence of images from a raw data file and writes them
    out to a series CCD / MCCD files.

    Parameters:

       source  : string [hidden]
           Data source, four options:

              | 'hs' : HiPERCAM server
              | 'hl' : local HiPERCAM FITS file
              | 'us' : ULTRACAM server
              | 'ul' : local ULTRACAM .xml/.dat files

       run     : string
           run name to access

       temp    : bool [hidden, defaults to False]
           True to indicate that the frames should be written to temporary
           files with automatically-generated names in the expectation of
           deleting them later. This also writes out a file listing the names.
           The aim of this is to allow grab to be used as a feeder for other
           routines in larger scripts.  If temp == True, grab will return with
           the name of the list of hcm files. Look at 'makebias' for an
           example that uses this feature.

       ndigit  : int [if not temp]
           Files created will be written as 'run005_0013.fits' etc. `ndigit`
           is the number of digits used for the frame number (4 in this
           case). Any pre-existing files of the same name will be
           over-written.

       first   : int
           First frame to access

       last    : int
           Last frame to access, 0 for the lot

       trim : bool [if source starts with 'u']
           True to trim columns and/or rows off the edges of windows nearest
           the readout. This is particularly for ULTRACAM windowed data where
           the first few rows and columns can contain bad data.

       ncol : int [if trim]
           Number of columns to remove (on left of left-hand window, and right
           of right-hand windows)

       nrow : int [if trim]
           Number of rows to remove (bottom of windows)

       twait   : float [hidden]
           time to wait between attempts to find a new exposure, seconds.

       tmax    : float [hidden]
           maximum time to wait between attempts to find a new exposure,
           seconds.

       bias    : string
           Name of bias frame to subtract, 'none' to ignore.

       dtype   : string [hidden, defaults to 'f32']
           Data type on output. Options:

            | 'f32' : output as 32-bit floats (default)

            | 'f64' : output as 64-bit floats.

            | 'u16' : output as 16-bit unsigned integers. A warning will be
                      issued if loss of precision occurs; an exception will
                      be raised if the data are outside the range 0 to 65535.

    """

    command, args = utils.script_args(args)

    # get inputs
    with Cline('HIPERCAM_ENV', '.hipercam', command, args) as cl:

        # register parameters
        cl.register('source', Cline.GLOBAL, Cline.HIDE)
        cl.register('run', Cline.GLOBAL, Cline.PROMPT)
        cl.register('temp', Cline.GLOBAL, Cline.HIDE)
        cl.register('ndigit', Cline.LOCAL, Cline.PROMPT)
        cl.register('first', Cline.LOCAL, Cline.PROMPT)
        cl.register('last', Cline.LOCAL, Cline.PROMPT)
        cl.register('trim', Cline.GLOBAL, Cline.PROMPT)
        cl.register('ncol', Cline.GLOBAL, Cline.HIDE)
        cl.register('nrow', Cline.GLOBAL, Cline.HIDE)
        cl.register('twait', Cline.LOCAL, Cline.HIDE)
        cl.register('tmax', Cline.LOCAL, Cline.HIDE)
        cl.register('bias', Cline.GLOBAL, Cline.PROMPT)
        cl.register('dtype', Cline.LOCAL, Cline.HIDE)

        # get inputs
        source = cl.get_value('source',
                              'data source [hs, hl, us, ul]',
                              'hl',
                              lvals=('hs', 'hl', 'us', 'ul'))

        # OK, more inputs
        resource = cl.get_value('run', 'run name', 'run005')

        cl.set_default('temp', False)
        temp = cl.get_value(
            'temp', 'save to temporary automatically-generated file names?',
            True)

        if not temp:
            ndigit = cl.get_value('ndigit',
                                  'number of digits in frame identifier', 3, 0)

        first = cl.get_value('first', 'first frame to grab', 1, 0)
        last = cl.get_value('last', 'last frame to grab', 0)
        if last < first and last != 0:
            sys.stderr.write('last must be >= first or 0')
            sys.exit(1)

        if source.startswith('u'):
            trim = cl.get_value(
                'trim',
                'do you want to trim edges of windows? (ULTRACAM only)', True)
            if trim:
                ncol = cl.get_value('ncol',
                                    'number of columns to trim from windows',
                                    0)
                nrow = cl.get_value('nrow',
                                    'number of rows to trim from windows', 0)
        else:
            trim = False

        twait = cl.get_value('twait', 'time to wait for a new frame [secs]',
                             1., 0.)
        tmax = cl.get_value('tmax',
                            'maximum time to wait for a new frame [secs]', 10.,
                            0.)

        bias = cl.get_value('bias',
                            "bias frame ['none' to ignore]",
                            cline.Fname('bias', hcam.HCAM),
                            ignore='none')

        cl.set_default('dtype', 'f32')
        dtype = cl.get_value('dtype',
                             'data type [f32, f64, u16]',
                             'f32',
                             lvals=('f32', 'f64', 'u16'))

    # Now the actual work.

    # strip off extensions
    if resource.endswith(hcam.HRAW):
        resource = resource[:resource.find(hcam.HRAW)]

    # initialisations
    total_time = 0  # time waiting for new frame
    nframe = first
    root = os.path.basename(resource)
    bframe = None

    # Finally, we can go
    if temp:
        # create a directory on temp for the temporary file to avoid polluting
        # it too much
        fnames = []
        tdir = os.path.join(tempfile.gettempdir(),
                            'hipercam-{:s}'.format(getpass.getuser()))
        os.makedirs(tdir, exist_ok=True)

    with spooler.data_source(source, resource, first) as spool:

        try:

            for mccd in spool:

                # Handle the waiting game ...
                give_up, try_again, total_time = spooler.hang_about(
                    mccd, twait, tmax, total_time)

                if give_up:
                    print('grab stopped')
                    break
                elif try_again:
                    continue

                # Trim the frames: ULTRACAM windowed data has bad
                # columns and rows on the sides of windows closest to
                # the readout which can badly affect reduction. This
                # option strips them.
                if trim:
                    hcam.ccd.trim_ultracam(mccd, ncol, nrow)

                if bias is not None:
                    # read bias after first frame so we can
                    # chop the format
                    if bframe is None:

                        # read the bias frame
                        bframe = hcam.MCCD.read(bias)

                        # reformat
                        bframe = bframe.crop(mccd)

                    mccd -= bframe

                if dtype == 'u16':
                    mccd.uint16()
                elif dtype == 'f32':
                    mccd.float32()
                elif dtype == 'f64':
                    mccd.float64()

                # write to disk
                if temp:
                    # generate name automatically
                    fd, fname = tempfile.mkstemp(suffix=hcam.HCAM, dir=tdir)
                    mccd.write(fname, True)
                    os.close(fd)
                    fnames.append(fname)
                else:
                    fname = '{:s}_{:0{:d}}{:s}'.format(root, nframe, ndigit,
                                                       hcam.HCAM)
                    mccd.write(fname, True)

                print('Written frame {:d} to {:s}'.format(nframe, fname))

                # update the frame number
                nframe += 1
                if last and nframe > last:
                    break

        except KeyboardInterrupt:
            # trap ctrl-C so we can delete temporary files if temp
            if temp:
                for fname in fnames:
                    os.remove(fname)
                print('\ntemporary files deleted')
                print('grab aborted')
            else:
                print('\ngrab aborted')
            sys.exit(1)

    if temp:
        if len(fnames) == 0:
            raise hcam.HipercamError(
                'no files were grabbed; please check input parameters, especially "first"'
            )
        # write the file names to a list
        fd, fname = tempfile.mkstemp(suffix=hcam.LIST, dir=tdir)
        with open(fname, 'w') as fout:
            for fnam in fnames:
                fout.write(fnam + '\n')
        os.close(fd)
        print('temporary file names written to {:s}'.format(fname))

        # return the name of the file list
        return fname
Example #8
0
def extractFlux(cnam, ccd, rccd, read, gain, ccdwin, rfile, store):
    """This extracts the flux of all apertures of a given CCD.

    The steps are (1) creation of PSF model, (2) PSF fitting, (3)
    flux extraction. The apertures are assumed to be correctly positioned.

    It returns the results as a dictionary keyed on the aperture label. Each
    entry returns a list:

    [x, ex, y, ey, fwhm, efwhm, beta, ebeta, counts, countse, sky, esky,
    nsky, nrej, flag]

    flag = bitmask. See hipercam.core to see all the options which are
    referred to by name in the code e.g. ALL_OK. The various flags can
    signal that there no sky pixels (NO_SKY), the sky aperture was off
    the edge of the window (SKY_AT_EDGE), etc.

    This code::

       >> bset = flag & TARGET_SATURATED

    determines whether the data saturation flag is set for example.

    Arguments::

       cnam     : string
          CCD identifier label

       ccd       : CCD
           the debiassed, flat-fielded CCD.

       rccd : CCD
          corresponding raw CCD, used to work out whether data are
          saturated in target aperture.

       read      : CCD
           readnoise divided by the flat-field

       gain      : CCD
           gain multiplied by the flat field

       ccdwin   : dictionary of strings
           the Window label corresponding to each Aperture

       rfile     : Rfile
           reduce file configuration parameters

       store     : dict of dicts
           see moveApers for what this contains.

    """

    # initialise flag
    flag = hcam.ALL_OK

    ccdaper = rfile.aper[cnam]

    results = {}
    # get profile params from aperture store
    mfwhm = store["mfwhm"]
    mbeta = store["mbeta"]
    method = "m" if mbeta > 0.0 else "g"

    if mfwhm <= 0:
        # die hard, die soon as there's nothing we can do.
        print((" *** WARNING: CCD {:s}: no measured FWHM to create PSF model"
               "; no extraction possible").format(cnam))
        # set flag to indicate no FWHM
        flag = hcam.NO_FWHM

        for apnam, aper in ccdaper.items():
            info = store[apnam]
            results[apnam] = {
                "x": aper.x,
                "xe": info["xe"],
                "y": aper.y,
                "ye": info["ye"],
                "fwhm": info["fwhm"],
                "fwhme": info["fwhme"],
                "beta": info["beta"],
                "betae": info["betae"],
                "counts": 0.0,
                "countse": -1,
                "sky": 0.0,
                "skye": 0.0,
                "nsky": 0,
                "nrej": 0,
                "flag": flag,
            }
        return results

    # all apertures have to be in the same window, or we can't easily make a
    # postage stamp of the data
    wnames = set(ccdwin.values())
    if len(wnames) != 1:
        print((" *** WARNING: CCD {:s}: not all apertures"
               " lie within the same window; no extraction possible"
               ).format(cnam))

        # set flag to indicate no extraction
        flag = hcam.NO_EXTRACTION

        # return empty results
        for apnam, aper in ccdaper.items():
            info = store[apnam]
            results[apnam] = {
                "x": aper.x,
                "xe": info["xe"],
                "y": aper.y,
                "ye": info["ye"],
                "fwhm": info["fwhm"],
                "fwhme": info["fwhme"],
                "beta": info["beta"],
                "betae": info["betae"],
                "counts": 0.0,
                "countse": -1,
                "sky": 0.0,
                "skye": 0.0,
                "nsky": 0,
                "nrej": 0,
                "flag": flag,
            }
            return results
    wnam = wnames.pop()

    # PSF params are in binned pixels, so find binning
    bin_fac = ccd[wnam].xbin

    # create PSF model
    if method == "m":
        psf_model = MoffatPSF(beta=mbeta, fwhm=mfwhm / bin_fac)
    else:
        psf_model = IntegratedGaussianPRF(sigma=mfwhm *
                                          gaussian_fwhm_to_sigma / bin_fac)

    # force photometry only at aperture positions
    # this means PSF shape and positions are fixed, we are only fitting flux
    if rfile["psf_photom"]["positions"] == "fixed":
        psf_model.x_0.fixed = True
        psf_model.y_0.fixed = True

    # create instances for PSF photometry
    gfac = float(rfile["psf_photom"]["gfac"])
    sclip = float(rfile["sky"]["thresh"])
    daogroup = DAOGroup(gfac * mfwhm / bin_fac)
    mmm_bkg = MMMBackground(sigma_clip=SigmaClip(sclip))
    fitter = LevMarLSQFitter()
    fitshape_box_size = int(2 * int(rfile["psf_photom"]["fit_half_width"]) + 1)
    fitshape = (fitshape_box_size, fitshape_box_size)

    photometry_task = BasicPSFPhotometry(
        group_maker=daogroup,
        bkg_estimator=mmm_bkg,
        psf_model=psf_model,
        fitter=fitter,
        fitshape=fitshape,
    )

    # initialise flag
    flag = hcam.ALL_OK

    # extract Windows relevant for these apertures
    wdata = ccd[wnam]
    wraw = rccd[wnam]

    # extract sub-windows that include all of the apertures, plus a little
    # extra around the edges.
    x1 = min([ap.x - ap.rsky2 - wdata.xbin for ap in ccdaper.values()])
    x2 = max([ap.x + ap.rsky2 + wdata.xbin for ap in ccdaper.values()])
    y1 = min([ap.y - ap.rsky2 - wdata.ybin for ap in ccdaper.values()])
    y2 = max([ap.y + ap.rsky2 + wdata.ybin for ap in ccdaper.values()])

    # extract sub-Windows
    swdata = wdata.window(x1, x2, y1, y2)
    swraw = wraw.window(x1, x2, y1, y2)

    # compute pixel positions of apertures in windows
    xpos, ypos = zip(*((swdata.x_pixel(ap.x), swdata.y_pixel(ap.y))
                       for ap in ccdaper.values()))
    positions = Table(names=["x_0", "y_0"], data=(xpos, ypos))

    # do the PSF photometry
    photom_results = photometry_task(swdata.data, init_guesses=positions)
    slevel = mmm_bkg(swdata.data)

    # unpack the results and check apertures
    for apnam, aper in ccdaper.items():
        try:
            # reset flag
            flag = hcam.ALL_OK

            result_row = photom_results[photom_results["id"] == int(apnam)]
            if len(result_row) == 0:
                flag |= hcam.NO_DATA
                raise hcam.HipercamError(
                    "no source in PSF photometry for this aperture")
            elif len(result_row) > 1:
                flag |= hcam.NO_EXTRACTION
                raise hcam.HipercamError(
                    "ambiguous lookup for this aperture in PSF photometry")
            else:
                result_row = result_row[0]

            # compute X, Y arrays over the sub-window relative to the centre
            # of the aperture and the distance squared from the centre (Rsq)
            # to save a little effort.
            x = swdata.x(np.arange(swdata.nx)) - aper.x
            y = swdata.y(np.arange(swdata.ny)) - aper.y
            X, Y = np.meshgrid(x, y)
            Rsq = X**2 + Y**2

            # size of a pixel which is used to taper pixels as they approach
            # the edge of the aperture to reduce pixellation noise
            size = np.sqrt(wdata.xbin * wdata.ybin)

            # target selection, accounting for extra apertures and allowing
            # pixels to contribute if their centres are as far as size/2 beyond
            # the edge of the circle (but with a tapered weight)
            dok = Rsq < (aper.rtarg + size / 2.0)**2
            if not dok.any():
                # check there are some valid pixels
                flag |= hcam.NO_DATA
                raise hcam.HipercamError("no valid pixels in aperture")

            # check for saturation and nonlinearity
            if cnam in rfile.warn:
                if swraw.data[dok].max() >= rfile.warn[cnam]["saturation"]:
                    flag |= hcam.TARGET_SATURATED

                if swraw.data[dok].max() >= rfile.warn[cnam]["nonlinear"]:
                    flag |= hcam.TARGET_NONLINEAR
            else:
                warnings.warn(
                    "CCD {:s} has no nonlinearity or saturation levels set")

            counts = result_row["flux_fit"]
            countse = result_row["flux_unc"]
            info = store[apnam]

            results[apnam] = {
                "x": aper.x,
                "xe": info["xe"],
                "y": aper.y,
                "ye": info["ye"],
                "fwhm": info["fwhm"],
                "fwhme": info["fwhme"],
                "beta": info["beta"],
                "betae": info["betae"],
                "counts": counts,
                "countse": countse,
                "sky": slevel,
                "skye": 0,
                "nsky": 0,
                "nrej": 0,
                "flag": flag,
            }

        except hcam.HipercamError as err:

            info = store[apnam]
            flag |= hcam.NO_EXTRACTION

            results[apnam] = {
                "x": aper.x,
                "xe": info["xe"],
                "y": aper.y,
                "ye": info["ye"],
                "fwhm": info["fwhm"],
                "fwhme": info["fwhme"],
                "beta": info["beta"],
                "betae": info["betae"],
                "counts": 0.0,
                "countse": -1,
                "sky": 0.0,
                "skye": 0.0,
                "nsky": 0,
                "nrej": 0,
                "flag": flag,
            }

    # finally, we are done
    return results
Example #9
0
def hlog2fits(args=None):
    """``hlog2fits log``

    Converts a |hiper| ASCII log into a FITS file. As well as a modest
    reduction in file size (~40%, the ASCII logs are written relatively
    efficiently), the resulting file is much faster to read than the ASCII log
    so this may be useful for very large log files [test of 78,000 frame file:
    12.9 seconds to read the ASCII file, 1.9 to read the FITS version]. At the
    moment no significant header information is transferred beyond the CCD
    names. Each CCD appears as a single binary table, starting at the second
    HDU (or HDU 1 if you number them 0,1,2 ..). This can be read using
    :meth:`hipercam.hlog.Hlog.from_fits`.

    Parameters:

      log : string
         name of the log file (should end .log). The output FITS file
         will have the same root name but end .fits. The routine will abort
         if there is a pre-existing file of the same name.

    NB Because of the danger of over-writing raw data (also ends .fits), this 
    routine will not over-write pre-existing files. You should delete clashing
    files if you really want to proceed.
    """

    command, args = utils.script_args(args)

    # get input section
    with Cline('HIPERCAM_ENV', '.hipercam', command, args) as cl:

        # register parameters
        cl.register('log', Cline.LOCAL, Cline.PROMPT)
        cl.register('origin', Cline.LOCAL, Cline.PROMPT)

        # get inputs
        log = cl.get_value(
            'log', 'name of log file from "reduce" to convert to FITS',
            cline.Fname('red', hcam.LOG))

        origin = cl.get_value('origin',
                              'h(ipercam) or u(ltracam)',
                              'h',
                              lvals=['h', 'u'])

    oname = os.path.basename(log)
    oname = oname[:oname.rfind('.')] + '.fits'
    if os.path.exists(oname):
        raise hcam.HipercamError(
            ('A file called {:s} already exists and'
             ' will not be over-written; aborting').format(oname))

    # Read in the ASCII log
    if origin == 'h':
        hlg = hcam.hlog.Hlog.read(log)
    elif origin == 'u':
        hlg = hcam.hlog.Hlog.fulog(log)

    print('Loaded ASCII log = {:s}'.format(log))

    # Generate HDU list

    # First the primary HDU (no data)
    phdr = fits.Header()
    phdr['LOGFILE'] = (os.path.basename(log), 'Original log file')
    phdu = fits.PrimaryHDU(header=phdr)
    hdul = [
        phdu,
    ]

    # Now a BinTable for each CCD
    for cnam in sorted(hlg):
        hdr = fits.Header()
        hdr['CCDNAME'] = (cnam, 'CCD name')
        hdul.append(
            fits.BinTableHDU(hlg[cnam],
                             header=hdr,
                             name='CCD {:s}'.format(cnam)))

    hdul = fits.HDUList(hdul)

    # finally write to disk
    print('Writing to disk in file = {:s}'.format(oname))
    hdul.writeto(oname)

    print('Converted {:s} to {:s}'.format(log, oname))
Example #10
0
def splice(args=None):
    """``splice input1 input2 ccd [win] output``

    Merges two multi-CCD images so that parts of input1 are replaced
    by equivalent parts of input2. This can be useful e.g. to create
    a combined flat field out of different frames.

    Parameters:

       input1 : str
           first input frame

       input2 : str
           second input frame

       ccd : str
           the CCD(s) to transfer, '0' for all of them. '1 3', i.e.
           separate with spaces for more than one CCD.

       win : str [hidden]
           the window(s) to transfer, '0' for all of them (default). The
           same windows must exist in the selected CCDs of both inputs 

       output : str
           output file.

    """

    command, args = utils.script_args(args)

    # get inputs
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("input1", Cline.GLOBAL, Cline.PROMPT)
        cl.register("input2", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ccd", Cline.LOCAL, Cline.PROMPT)
        cl.register("win", Cline.LOCAL, Cline.HIDE)
        cl.register("output", Cline.LOCAL, Cline.PROMPT)

        # get inputs
        infile1 = cl.get_value(
            "input1", "first input frame", cline.Fname("frame1", hcam.HCAM)
        )
        mccd1 = hcam.MCCD.read(infile1)

        infile2 = cl.get_value(
            "input2", "second input frame", cline.Fname("frame2", hcam.HCAM)
        )
        mccd2 = hcam.MCCD.read(infile2)

        if len(mccd1) > 1:
            ccd = cl.get_value("ccd", "CCD(s) to transfer ('0' for all)", "1")
            if ccd == "0":
                ccds = list(mccd1.keys())
            else:
                ccds = ccd.split()
        else:
            ccds = list(mccd1.keys())

        # Find window names in common across all selected CCDs
        # (intersection of sets)
        wins = None
        for cnam in ccds:
            if wins is None:
                wins = set(mccd1[cnam].keys())
            else:
                wins &= set(mccd1[cnam].keys())

        if len(wins) == 0:
            raise hcam.HipercamError(
                "There were no windows in common across the selected CCDs"
            )
        elif len(wins) > 1:
            cl.set_default("win", "0")
            win = cl.get_value("win", "windows to transfer ('0' for all)", "0")
            if win != "0":
                wins = wins.split()

        outfile = cl.get_value(
            "output", "output file", cline.Fname("output", hcam.HCAM, cline.Fname.NEW)
        )

    # Now the (trivial) actual work.

    for cnam in ccds:
        for wnam in wins:
            mccd1[cnam][wnam] = mccd2[cnam][wnam]

    mccd1.write(outfile, True)
Example #11
0
def tanalysis(args=None):
    """``tanalysis [source] run [mintim dcmax] details (plot [check (output)])``

    .. Warning::

       This script should only be run if you know what you are doing.

    Analyses timing data in |hiper|, ULTRACAM or ULTRASPEC data.  You
    must first have made copy of the timing bytes (see ``tbytes'' and
    ``atbytes''). It looks for a timing bytes file in a sub-directory
    of the directory the script is run from called "tbytes". If the
    file does not exist, the script will exit.

    On occasion there are problems with ULTRACAM, ULTRASPEC and, very
    rarely, |hiper| timestamps. The main purpose of this script is to
    provide an analysis of the timing to spot these is as clear a way
    as possible. Some of the problems are::

      1) ULTRASPEC null timestamps: ULTRASPEC runs occasionally feature
         weird null timestamps. They don't replace the proper ones but
         push them down the line so they turn up later.

      2) Extra OK-looking timestamps: on three occasions, genuine but
         spurious timestamps have been added to the FIFO buffer. Again
         these are extra, and push the real ones down the line.

    Parameters:

        source : string [hidden]
           Data source, two options:

              | 'hl' : local HiPERCAM FITS file
              | 'ul' : ULTRA(CAM|SPEC) server

        run : string
           run number to access, e.g. 'run0034'. This will also be
           used to generate the name for the timing bytes file
           (extension '.tbts'). If a file of this name already exists,
           the script will attempt to read and compare the bytes of
           the two files and report any changes.  The timing bytes
           file will be written to the present working directory, not
           necessarily the location of the data file.

        mintim : int [hidden]
           Minimum number of times to attempt to do anything
           with. This must be at least 4 so that there are 3+ time
           differences to try to get a median time, but in practice it
           is probably advisable to use a larger number still.

        dcmax : float [hidden]
           Maximum differential in terms of cycle number from exact integers
           to use to indicate "failed" times. Pre 2010-02 needs to be looser
           than post 2010-02. Number of order 0.002 is fine post 2010-02,
           whereas 0.02 is more like it pre 2010-02.

        details : bool
           Print out detailed information on problem times

        plot : bool
           True to make diagnostic plots if problem runs found

        check : bool [if plot, hidden]
           True simply to check a run for timing problems, not try to
           fix them. Defaults to True, and is also hidden by
           default. If you set check=False, the code also attempts to
           fix the times and generate a new data file. At the moment
           it is very specific code set to try to correct for spurious
           extra timestamps as happened for the following runs:
           2011-10-31 run022, 2013-12-31 run031 and 2015-06-22
           run024. It works by calculating its best estimate of cycle
           numbers and rejecting any too far off being integers. The
           corrected times are analysed for duplications and gaps. You
           do not want any duplications (and ideally no gaps either).
           You will be asked one final time if you want to proceed.

        output : str [if not check]
           Name of the modified data file. For safety this will not overwrite
           any existing file. It is up to the user to move / copy / rename files
           appropriately. Take care to check the results before deleting any
           original data. Keeping a copy is probably advisable.

    .. Note::

       The final frame on many runs can be terminated early and thus ends with
       an out of sequence timestamp (it comes earlier than expected). The script
       regards such a run as being "OK" if there are not other issues.

    """

    command, args = utils.script_args(args)

    # get the inputs
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("source", Cline.LOCAL, Cline.HIDE)
        cl.register("run", Cline.GLOBAL, Cline.PROMPT)
        cl.register("mintim", Cline.LOCAL, Cline.HIDE)
        cl.register("dcmax", Cline.LOCAL, Cline.HIDE)
        cl.register("plot", Cline.LOCAL, Cline.PROMPT)
        cl.register("details", Cline.LOCAL, Cline.PROMPT)
        cl.register("check", Cline.LOCAL, Cline.HIDE)
        cl.register("output", Cline.LOCAL, Cline.PROMPT)

        # get inputs
        source = cl.get_value("source",
                              "data source [hl, ul]",
                              "hl",
                              lvals=("hl", "ul"))

        run = cl.get_value("run", "run name", "run005")
        if run.endswith(".fits"):
            run = run[:-5]

        mintim = cl.get_value("mintim", "minimum number of times needed", 6, 4)
        dcmax = cl.get_value("dcmax",
                             "maximum cycle number offset from an integer",
                             0.003, 0)
        details = cl.get_value("details",
                               "print detailed diagnostics of problems", True)
        plot = cl.get_value("plot", "make diagnostic plots", True)

        if plot:
            cl.set_default("check", True)
            check = cl.get_value("check",
                                 "check for, but do not fix, any problems",
                                 True)
            if not check:
                output = cl.get_value(
                    "output", "output file for data with modified times",
                    cline.Fname("modified", ftype=cline.Fname.NOCLOBBER))
        else:
            check = True

    # create name of timing file
    tfile = os.path.join("tbytes", os.path.basename(run) + hcam.TBTS)
    if not os.path.isfile(tfile):
        raise hcam.HipercamError(
            "Could not find timing file = {}".format(tfile))

    # create name of run file and the copy which will only get made
    # later if problems are picked up
    if source == "hl":
        rfile = run + ".fits"
    else:
        rfile = run + ".dat"

    # first load all old time stamps, and compute MJDs
    if source == "ul":

        # Load up the time stamps from the timing data file. Need also
        # the header of the original run to know how many bytes to
        # read and how to interpret them.
        rhead = hcam.ucam.Rhead(run)
        with open(tfile, "rb") as fin:
            atbytes, mjds, tflags, gflags = [], [], [], []
            nframe = 0
            while 1:
                tbytes = fin.read(rhead.ntbytes)
                if len(tbytes) != rhead.ntbytes:
                    break
                nframe += 1
                atbytes.append(tbytes)

                # interpret times
                mjd, tflag, gflag, uformat = u_tbytes_to_mjd(
                    tbytes, rhead, nframe)
                mjds.append(mjd)
                tflags.append(tflag)
                gflags.append(gflag)

    else:
        raise NotImplementedError(
            "source = {} not yet implemented".format(source))

    if len(mjds) < mintim:
        # Must have specified minimum of times to work with. This is
        # checked more stringently later, but this avoids silly crashes
        # when there are no times.
        print(
            run,
            "has too few frames to work with ({} vs minimum = {})".format(
                len(mjds), mintim),
        )
        return

    # Independent of source, at this stage 'atbytes' is a list of all
    # timestamp bytes, while 'mjds' is a list of all equivalent MJDs,
    # and 'tflags' are bools which are True for OK times, False for
    # null timestamps.
    mjds = np.array(mjds)
    tflags = np.array(tflags)
    gflags = np.array(gflags)
    inds = np.arange(len(mjds))
    nulls = ~tflags
    nulls_present = nulls.any()
    btimes = ~gflags
    bad_times_present = btimes.any()

    # Remove null timestamps
    mjds_ok = mjds[tflags & gflags]
    inds_ok = inds[tflags & gflags]

    # Must have specified minimum of times to work with.
    if len(mjds_ok) < mintim:
        print(
            run,
            "has too few non-null times to work with ({} vs minimum = {})".
            format(len(mjds_ok), mintim),
        )
        return

    # Median time difference of GPS timestamps with guard against identical
    # times that can dominate leading to mdiff = 0
    diffs = mjds_ok[1:] - mjds_ok[:-1]
    pos = diffs > 1.0e-9
    if len(diffs[pos]) == 0:
        print(run, "has zero positive time differences to work with")
        return
    mdiff = np.median(diffs[pos])

    # Maximum deviation from median separation to allow (days)
    # 1.e-9 ~ 1e-4 seconds, ~30x shorter than shortest ULTRACAM
    # cycle time. This could allow some bad stamps through but
    # they will be spotted later by looking at cycle numbers.
    MDIFF = 1.0e-9

    # Identify OK timestamps. Could miss some at this point, but
    # that's OK) kick off by marking all timestamps as False
    ok = mjds_ok == mjds_ok + 1
    for n in range(len(mjds_ok) - 1):
        if abs(mjds_ok[n + 1] - mjds_ok[n] - mdiff) < MDIFF:
            # if two timestamps differ by the right amount, mark both
            # as ok
            ok[n] = ok[n + 1] = True

    gmjds = mjds_ok[ok]
    ginds = inds_ok[ok]
    if len(gmjds) < 2:
        print("{}: found fewer than 2 good timestamps".format(run))
        return

    # Work out integer cycle numbers. First OK timestamp is given
    # cycle number = 0 automatically by the method used.  The cycle
    # numbers can go wrong if the median is not precise enough leading
    # to jumps in the cycle number on runs with large numbers of short
    # exposures, so we build up to the full fit in stages, doubling the
    # number fitted each time

    NMAX = 100
    cycles = (gmjds[:NMAX] - gmjds[0]) / mdiff
    moffset = (cycles - np.round(cycles)).mean()
    icycles = np.round(cycles - moffset).astype(int)

    while NMAX < len(gmjds):
        # fit linear trend of first NMAX times where hopefully NMAX is small
        # enough for there not to be a big error but large enough to allow extrapolation.
        slope, intercept, r, p, err = stats.linregress(icycles[:NMAX],
                                                       gmjds[:NMAX])
        NMAX *= 1.5
        NMAX = int(NMAX)
        icycles = np.round((gmjds[:NMAX] - intercept) / slope).astype(int)

    # hope the cycle numbers should be good across the board for all the "gmjds"
    # at this point. Carry out final fits to try to refine the intercept and
    # slope because it is possible to be upset by jumps in time of less
    # than a cycle (i.e 2002-09-20, run014)

    c*k = icycles == icycles
    cinds = np.arange(len(c*k))
    nrej = 0
    while 1:
        # fit to ok ones
        slope, intercept, r, p, err = stats.linregress(icycles[c*k],
                                                       gmjds[c*k])
        cycles = (gmjds - intercept) / slope

        # cdiffs, acdiffs -- cycle differences and their absolute
        # values
        cdiffs = cycles - icycles
        acdiffs = np.abs(cdiffs)

        dmax = np.argmax(acdiffs[c*k])
        if acdiffs[c*k][dmax] > dcmax:
            # at least one is out of spec. reject it
            c*k[cinds[c*k][dmax]] = False
            nrej += 1
        else:
            break
    print("Rejected {} times when fitting nominally good times".format(nrej))

    # now try to compute cycle numbers for *all* non-null timestamps
    acycles = (mjds_ok - intercept) / slope
    iacycles = np.round(acycles).astype(int)

    if iacycles[-1] == iacycles[-2]:
        # because the last frame can be terminated early, it is
        # not uncommon for it to end with same cycle number as the
        # penultimate one. Correct this here
        iacycles[-1] += 1

    cadiffs = acycles - iacycles
    monotonic = (iacycles[1:] - iacycles[:-1] > 0).all()

    # check for early termination on run causing last frame to appear
    # early
    terminated_early = cadiffs[-1] < -dcmax

    # check that all is OK
    if (not nulls_present and monotonic
            and ((terminated_early and (np.abs(cadiffs[:-1]) < dcmax).all()) or
                 (not terminated_early and (np.abs(cadiffs) < dcmax).all()))):
        if terminated_early:
            mdev = np.abs(cadiffs[:-1]).max()
            print(
                "{} times are OK; {} frames; max. dev. all bar last = {:.2g} cyc, {:.2g} msec; last = {:.2g} cyc"
                .format(run, len(iacycles), mdev, 86400 * 1000 * slope * mdev,
                        cadiffs[-1]))
        else:
            mdev = np.abs(cadiffs).max()
            print(
                "{} times are OK; {} frames; max. dev. all = {:.2g} cyc, {:.2g} msec"
                .format(run, len(iacycles), mdev, 86400 * 1000 * slope * mdev))
        # to prevent crash during plot
        fails = cadiffs != cadiffs
        run_ok = True

    else:
        # search for duplicate cycle numbers
        u, c = np.unique(iacycles, return_counts=True)
        dupes = u[c > 1]
        dupes_present = len(dupes) > 0

        ntot = len(mjds)
        ndupe = len(dupes)
        nnull = len(mjds[nulls])
        nbad = len(mjds[btimes])
        fails = np.abs(cadiffs) > dcmax
        if terminated_early:
            nfail = len(cadiffs[fails]) - 1
            mdev = np.abs(cadiffs[:-1]).max()
        else:
            nfail = len(cadiffs[fails])
            mdev = np.abs(cadiffs).max()

        if ndupe == 0 and nbad == 0 and nnull == 0 and inds_ok[fails][
                -1] < nfail:
            # save some runs with trivial level issues from being flagged
            if nfail < len(cadiffs):
                mdev = np.abs(cadiffs[nfail:]).max()
            comment = " [All fails are at start]"
            run_ok = True
        else:
            comment = ""
            run_ok = False

        # Summarise problems
        print(
            "{} timestamps corrupt. TOT,DUP,NULL,BAD,FAIL,FOK = {},{},{},{},{},{}; max dev = {:.2g} cyc, {:.2g} sec.{}"
            .format(
                run,
                ntot,
                ndupe,
                nnull,
                nbad,
                nfail,
                inds_ok[ok][0] + 1,
                mdev,
                86400 * slope * mdev,
                comment,
            ))

        if details:

            # Some details
            fcadiffs = iacycles[1:] - iacycles[:-1]
            back = fcadiffs <= 0
            oinds = inds_ok[:-1][back]
            ninds = np.arange(len(fcadiffs))[back]
            cycs = iacycles[:-1][back]
            ncycs = iacycles[1:][back]
            tims = mjds_ok[:-1][back]
            if len(oinds):
                print("\nInverted order times:")
                for oind, nind, cyc, ncyc, mjd in zip(oinds, ninds, cycs,
                                                      ncycs, tims):
                    print(
                        "  Old index = {}, new index = {}, cycle = {}, next cycle = {}, time = {}"
                        .format(oind, nind, cyc, ncyc, mjd))

            if nfail:
                print("\nFailed times:")
                inds = inds_ok[fails]
                cycs = iacycles[fails]
                cydiffs = cadiffs[fails]
                tims = mjds_ok[fails]
                for ind, cyc, cdiff, mjd in zip(inds, cycs, cydiffs, tims):
                    print(
                        "  Index = {}, cycle = {}, cycle diff. = {:.2g}, time = {}"
                        .format(ind, cyc, cdiff, mjd))

    if plot:
        # diagnostic plot: cycle differences vs cycle numbers
        def c2t(x):
            return 86400 * slope * x

        def t2c(x):
            return x / slope / 86400

        fig = plt.figure()
        ax = fig.add_subplot()
        ax.plot(iacycles[~fails], cadiffs[~fails], ".b")
        ax.plot(iacycles[fails], cadiffs[fails], ".r")
        ax.set_xlabel("Cycle number")
        ax.set_ylabel('Cycle - nint(Cycle) [cycles]')

        secxax = ax.secondary_xaxis("top", functions=(c2t, t2c))
        secxax.set_xlabel("Time [MJD - {}] (seconds)".format(intercept))
        secyax = ax.secondary_yaxis("right",
                                    functions=(lambda x: 1000 * c2t(x),
                                               lambda x: t2c(x) / 1000))
        secyax.set_xlabel("$\Delta t$ (msec)".format(intercept))
        plt.show()

    if not plot or run_ok or check:
        # go no further if run is OK, no plot has been made, or we are in check mode
        return

    # very specific code at this point for the post-2010 problem runs: remove duplicate
    # time stamps.

    if ginds[0] != 0 or nfail == 0 or (source == 'ul' and uformat != 2):
        # this case needs checking
        raise hcam.HipercamError(
            'Cannot yet handle cases where the first timestamp is no good, nfail == 0, or format==2 ultracam data'
        )

    # these frames have been ID-ed as junk
    skip_inds = inds_ok[fails]
    inds_keep = inds == inds
    inds_keep[skip_inds] = False

    # inds_keep : False for any Frame we want to forget
    new_mjds, new_atbytes = [], []
    nframe = 0
    # manipulate the frame numbers to avoid warnings
    for keep, mjd, tbytes in zip(inds_keep, mjds, atbytes):
        if keep:
            nframe += 1
            new_mjds.append(mjd)
            new_atbytes.append(tbytes[:4] + struct.pack("<I", nframe) +
                               tbytes[8:])

    nskip = len(mjds) - len(new_mjds)

    # modify the last few times to make up for the skips,
    # extrapolating the times using the fitted time step per cycle
    # from before. We don't simply add "slope" to the last MJD one by
    # one because of subtle round-off issues.
    mjds_nskip = mjd + slope * np.linspace(1, nskip, nskip)
    for tbytes, mjd in zip(atbytes[-nskip:], mjds_nskip):
        unix_sec = ucam.DSEC * (mjd - ucam.UNIX)
        nsec = int(np.floor(unix_sec))
        nnsec = int(round(1.e7 * (unix_sec - nsec)))
        new_mjds.append(ucam.UNIX + float(nsec + nnsec / 1.0e7) / ucam.DSEC)
        nframe += 1
        tbytes = tbytes[:4] + struct.pack("<I",
                                          nframe) + tbytes[8:12] + struct.pack(
                                              "<II", nsec, nnsec) + tbytes[20:]
        new_atbytes.append(tbytes)

    # transfer byte 0 over as this contains a flag indicating the status of the blue data for the NBLUE option
    # (god, this is painful)
    for n in range(len(atbytes)):
        new_atbytes[n] = atbytes[n][0:1] + new_atbytes[n][1:]

    new_mjds_ok = np.array(new_mjds)
    new_mjds_ok = new_mjds_ok[tflags & gflags]
    acycles = (new_mjds_ok - intercept) / slope
    iacycles = np.round(acycles).astype(int)
    diffs = acycles - iacycles

    # search for duplicates
    u, c = np.unique(iacycles, return_counts=True)
    ndupes = len(u[c > 1])

    # search for gaps
    csteps = iacycles[1:] - iacycles[:-1]
    ngaps = len(iacycles[1:][csteps > 1])

    print(
        '\nFound {} spurious time stamps; new times have {} duplications and {} gaps > 1'
        .format(nskip, ndupes, ngaps))
    print('Ideally, the last two numbers should be 0 and 0.')

    plt.plot(iacycles[:-nskip], diffs[:-nskip], '.g')
    plt.plot(iacycles[-nskip:], diffs[-nskip:], '.b')
    plt.xlabel('Cycle number')
    plt.ylabel('Cycle - int(Cycle) [cycles]')
    plt.show()

    tdiffs = 86400 * (mjds_ok - new_mjds_ok)
    plt.plot(iacycles[:-nskip], tdiffs[:-nskip], '.g')
    plt.plot(iacycles[-nskip:], tdiffs[-nskip:], '.b')
    plt.xlabel('Cycle number')
    plt.ylabel('Old time - new [seconds]')
    plt.show()
    reply = input('Generate data with corrected time stamps? [no] ')
    if reply != 'yes':
        print('time correction aborted')
        return

    # OK, finally, let's go for it!
    if source == "ul":
        with open(output, "wb") as fout:
            with open(rfile, "rb") as fin:
                nframe = 0
                while 1:
                    # Read a whole frame
                    buff = fin.read(rhead.framesize)
                    if len(buff) != rhead.framesize:
                        break

                    # write to output, writing modified timing bytes
                    # in place of old ones
                    fout.write(new_atbytes[nframe])
                    fout.write(buff[rhead.ntbytes:])
                    nframe += 1
                    print('Processed frame {} / {}'.format(
                        nframe, len(atbytes)))

    else:
        raise NotImplementedError(
            "source = {} not yet implemented".format(source))
Example #12
0
def ftargets(args=None):
    """``ftargets [source device width height] (run first [twait tmax] |
    flist) trim ([ncol nrow]) (ccd (nx)) [pause] thresh fwhm minpix
    output bias flat msub iset (ilo ihi | plo phi) xlo xhi ylo yhi``

    This script carries out the following steps for each of a series
    of images:

      (1) detects the sources,
      (2) identifies isolated targets suited to profile fits,
      (3) fits 2D Moffat profiles to these,
      (4) Saves results to disk.

    The profile fits are carried out because `sep` does not return
    anything that can be used reliably for a FWHM.

    Several parameters depends on the object detection threshold
    retuned by the source detection. This is referred to as
    `threshold`. The source detection is carried out using `sep` which
    runs according to the usual source extractor algorithm of Bertin.

    The script plots the frames, with ellipses at 3*a,3*b indicated in
    red, green boxes indicating the range of pixels identified by
    `sep`, and blue boxes marking the targets selected for FWHM
    fitting (the boxes indicate the fit region).


    Parameters:

        source : string [hidden]
           Data source, five options:

             |  'hs' : HiPERCAM server
             |  'hl' : local HiPERCAM FITS file
             |  'us' : ULTRACAM server
             |  'ul' : local ULTRACAM .xml/.dat files
             |  'hf' : list of HiPERCAM hcm FITS-format files

           'hf' is used to look at sets of frames generated by 'grab' or
           converted from foreign data formats.

        device : string [hidden]
          Plot device. PGPLOT is used so this should be a PGPLOT-style name,
          e.g. '/xs', '1/xs' etc. At the moment only ones ending /xs are
          supported.

        width : float [hidden]
           plot width (inches). Set = 0 to let the program choose.

        height : float [hidden]
           plot height (inches). Set = 0 to let the program choose. BOTH width
           AND height must be non-zero to have any effect

        run : string [if source ends 's' or 'l']
           run number to access, e.g. 'run034'

        flist : string [if source ends 'f']
           name of file list

        first : int [if source ends 's' or 'l']
           exposure number to start from. 1 = first frame; set = 0 to always
           try to get the most recent frame (if it has changed).  For data
           from the |hiper| server, a negative number tries to get a frame not
           quite at the end.  i.e. -10 will try to get 10 from the last
           frame. This is mainly to sidestep a difficult bug with the
           acquisition system.

        twait : float [if source ends 's' or 'l'; hidden]
           time to wait between attempts to find a new exposure, seconds.

        tmax : float [if source ends 's' or 'l'; hidden]
           maximum time to wait between attempts to find a new exposure,
           seconds.

        trim : bool [if source starts with 'u']
           True to trim columns and/or rows off the edges of windows nearest
           the readout which can sometimes contain bad data.

        ncol : int [if trim, hidden]
           Number of columns to remove (on left of left-hand window, and right
           of right-hand windows)

        nrow : int [if trim, hidden]
           Number of rows to remove (bottom of windows)

        ccd : string
           CCD(s) to plot, '0' for all, '1 3' to plot '1' and '3' only, etc.

        nx : int [if more than 1 CCD]
           number of panels across to display.

        pause : float [hidden]
           seconds to pause between frames (defaults to 0)

        thresh : float
           threshold (mutiple of RMS) to use for object detection. Typical
           values 2.5 to 4. The higher it is, the fewer objects will be located,
           but the fewer false detections will be made.

        fwhm : float
           FWHM to use for smoothing during object detection. Should be
           comparable to the seeing.

        minpix : int
           Minimum number of pixels above threshold before convolution to count
           as a detection. Useful in getting rid of cosmics and high dark count
           pixels.

        rmin : float
           Closest distance of any other detected object for an attempt
           to be made to fit the FWHM of an object [unbinned pixels].

        pmin : float
           Minimum peak height for an attempt to be made to fit the
           FWHM of an object. This should be a multiple of the object
           detection threshold (returned by `sep` for each object).

        pmax : float
           Maximum peak height for an attempt to be made to fit the
           FWHM of an object. Use to exclude saturated targets
           [counts]

        emax : float
           Maximum elongation (major/minor axis ratio = a/b), > 1. Use
           to reduce very non-stellar profiles.

        nmax : int
           Maximum number of FWHMs to measure. Will take the brightest first,
           judging by the flux.

        bias : string
           Name of bias frame to subtract, 'none' to ignore.

        flat : string
           Name of flat field to divide by, 'none' to ignore. Should normally
           only be used in conjunction with a bias, although it does allow you
           to specify a flat even if you haven't specified a bias.

        output: string
           Name of file for storage of results. Will be a fits file, with
           results saved to the HDU 1 as a table.

        iset : string [single character]
           determines how the intensities are determined. There are three
           options: 'a' for automatic simply scales from the minimum to the
           maximum value found on a per CCD basis. 'd' for direct just takes
           two numbers from the user. 'p' for percentile dtermines levels
           based upon percentiles determined from the entire CCD on a per CCD
           basis.

        ilo : float [if iset='d']
           lower intensity level

        ihi : float [if iset='d']
           upper intensity level

        plo : float [if iset='p']
           lower percentile level

        phi : float [if iset='p']
           upper percentile level

        xlo : float
           left-hand X-limit for plot

        xhi : float
           right-hand X-limit for plot (can actually be < xlo)

        ylo : float
           lower Y-limit for plot

        yhi : float
           upper Y-limit for plot (can be < ylo)

    """

    command, args = utils.script_args(args)

    # get the inputs
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("source", Cline.GLOBAL, Cline.HIDE)
        cl.register("device", Cline.LOCAL, Cline.HIDE)
        cl.register("width", Cline.LOCAL, Cline.HIDE)
        cl.register("height", Cline.LOCAL, Cline.HIDE)
        cl.register("run", Cline.GLOBAL, Cline.PROMPT)
        cl.register("first", Cline.LOCAL, Cline.PROMPT)
        cl.register("trim", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ncol", Cline.GLOBAL, Cline.HIDE)
        cl.register("nrow", Cline.GLOBAL, Cline.HIDE)
        cl.register("twait", Cline.LOCAL, Cline.HIDE)
        cl.register("tmax", Cline.LOCAL, Cline.HIDE)
        cl.register("flist", Cline.LOCAL, Cline.PROMPT)
        cl.register("ccd", Cline.LOCAL, Cline.PROMPT)
        cl.register("nx", Cline.LOCAL, Cline.PROMPT)
        cl.register("pause", Cline.LOCAL, Cline.HIDE)
        cl.register("thresh", Cline.LOCAL, Cline.PROMPT)
        cl.register("fwhm", Cline.LOCAL, Cline.PROMPT)
        cl.register("minpix", Cline.LOCAL, Cline.PROMPT)
        cl.register("rmin", Cline.LOCAL, Cline.PROMPT)
        cl.register("pmin", Cline.LOCAL, Cline.PROMPT)
        cl.register("pmax", Cline.LOCAL, Cline.PROMPT)
        cl.register("emax", Cline.LOCAL, Cline.PROMPT)
        cl.register("nmax", Cline.LOCAL, Cline.PROMPT)
        cl.register("gain", Cline.LOCAL, Cline.PROMPT)
        cl.register("rej", Cline.LOCAL, Cline.PROMPT)
        cl.register("bias", Cline.GLOBAL, Cline.PROMPT)
        cl.register("flat", Cline.GLOBAL, Cline.PROMPT)
        cl.register("output", Cline.LOCAL, Cline.PROMPT)
        cl.register("iset", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ilo", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ihi", Cline.GLOBAL, Cline.PROMPT)
        cl.register("plo", Cline.GLOBAL, Cline.PROMPT)
        cl.register("phi", Cline.LOCAL, Cline.PROMPT)
        cl.register("xlo", Cline.GLOBAL, Cline.PROMPT)
        cl.register("xhi", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ylo", Cline.GLOBAL, Cline.PROMPT)
        cl.register("yhi", Cline.GLOBAL, Cline.PROMPT)

        # get inputs
        source = cl.get_value(
            "source",
            "data source [hs, hl, us, ul, hf]",
            "hl",
            lvals=("hs", "hl", "us", "ul", "hf"),
        )

        # set some flags
        server_or_local = source.endswith("s") or source.endswith("l")

        # plot device stuff
        device = cl.get_value("device", "plot device", "1/xs")
        width = cl.get_value("width", "plot width (inches)", 0.0)
        height = cl.get_value("height", "plot height (inches)", 0.0)

        if server_or_local:
            resource = cl.get_value("run", "run name", "run005")
            if source == "hs":
                first = cl.get_value("first", "first frame to plot", 1)
            else:
                first = cl.get_value("first", "first frame to plot", 1, 0)

            twait = cl.get_value(
                "twait", "time to wait for a new frame [secs]", 1.0, 0.0
            )
            tmax = cl.get_value(
                "tmax", "maximum time to wait for a new frame [secs]", 10.0, 0.0
            )

        else:
            resource = cl.get_value(
                "flist", "file list", cline.Fname("files.lis", hcam.LIST)
            )
            first = 1

        trim = cl.get_value("trim", "do you want to trim edges of windows?", True)
        if trim:
            ncol = cl.get_value("ncol", "number of columns to trim from windows", 0)
            nrow = cl.get_value("nrow", "number of rows to trim from windows", 0)

        # define the panel grid. first get the labels and maximum dimensions
        ccdinf = spooler.get_ccd_pars(source, resource)

        try:
            nxdef = cl.get_default("nx")
        except:
            nxdef = 3

        if len(ccdinf) > 1:
            ccd = cl.get_value("ccd", "CCD(s) to plot [0 for all]", "0")
            if ccd == "0":
                ccds = list(ccdinf.keys())
            else:
                ccds = ccd.split()
                check = set(ccdinf.keys())
                if not set(ccds) <= check:
                    raise hcam.HipercamError("At least one invalid CCD label supplied")

            if len(ccds) > 1:
                nxdef = min(len(ccds), nxdef)
                cl.set_default("nx", nxdef)
                nx = cl.get_value("nx", "number of panels in X", 3, 1)
            else:
                nx = 1
        else:
            nx = 1
            ccds = list(ccdinf.keys())

        cl.set_default("pause", 0.0)
        pause = cl.get_value(
            "pause", "time delay to add between" " frame plots [secs]", 0.0, 0.0
        )

        thresh = cl.get_value("thresh", "source detection threshold [RMS]", 3.0)
        fwhm = cl.get_value("fwhm", "FWHM for source detection [binned pixels]", 4.0)
        minpix = cl.get_value("minpix", "minimum number of pixels above threshold", 3)
        rmin = cl.get_value(
            "rmin", "nearest neighbour for FWHM fits [unbinned pixels]", 20.0
        )
        pmin = cl.get_value(
            "pmin", "minimum peak value for profile fits [multiple of threshold]", 5.0
        )
        pmax = cl.get_value(
            "pmax", "maximum peak value for profile fits [counts]", 60000.0
        )
        emax = cl.get_value(
            "emax", "maximum elongation (a/b) for profile fits", 1.2, 1.0
        )
        nmax = cl.get_value("nmax", "maximum number of profile fits", 10, 1)
        gain = cl.get_value("gain", "CCD gain [electrons/ADU]", 1.0, 0.0)
        rej = cl.get_value(
            "rej", "rejection threshold for profile fits [RMS]", 6.0, 2.0
        )

        # bias frame (if any)
        bias = cl.get_value(
            "bias",
            "bias frame ['none' to ignore]",
            cline.Fname("bias", hcam.HCAM),
            ignore="none",
        )
        if bias is not None:
            # read the bias frame
            bias = hcam.MCCD.read(bias)
            fprompt = "flat frame ['none' to ignore]"
        else:
            fprompt = "flat frame ['none' is normal choice with no bias]"

        # flat (if any)
        flat = cl.get_value(
            "flat", fprompt, cline.Fname("flat", hcam.HCAM), ignore="none"
        )
        if flat is not None:
            # read the flat frame
            flat = hcam.MCCD.read(flat)

        output = cl.get_value(
            "output",
            "output file for results",
            cline.Fname("sources", hcam.SEP, cline.Fname.NEW),
        )

        iset = cl.get_value(
            "iset",
            "set intensity a(utomatically)," " d(irectly) or with p(ercentiles)?",
            "a",
            lvals=["a", "d", "p"],
        )
        iset = iset.lower()

        plo, phi = 5, 95
        ilo, ihi = 0, 1000
        if iset == "d":
            ilo = cl.get_value("ilo", "lower intensity limit", 0.0)
            ihi = cl.get_value("ihi", "upper intensity limit", 1000.0)
        elif iset == "p":
            plo = cl.get_value(
                "plo", "lower intensity limit percentile", 5.0, 0.0, 100.0
            )
            phi = cl.get_value(
                "phi", "upper intensity limit percentile", 95.0, 0.0, 100.0
            )

        # region to plot
        for i, cnam in enumerate(ccds):
            nxtot, nytot, nxpad, nypad = ccdinf[cnam]
            if i == 0:
                xmin, xmax = float(-nxpad), float(nxtot + nxpad + 1)
                ymin, ymax = float(-nypad), float(nytot + nypad + 1)
            else:
                xmin = min(xmin, float(-nxpad))
                xmax = max(xmax, float(nxtot + nxpad + 1))
                ymin = min(ymin, float(-nypad))
                ymax = max(ymax, float(nytot + nypad + 1))

        xlo = cl.get_value("xlo", "left-hand X value", xmin, xmin, xmax)
        xhi = cl.get_value("xhi", "right-hand X value", xmax, xmin, xmax)
        ylo = cl.get_value("ylo", "lower Y value", ymin, ymin, ymax)
        yhi = cl.get_value("yhi", "upper Y value", ymax, ymin, ymax)

    ################################################################
    #
    # all the inputs have now been obtained. Get on with doing stuff

    # open image plot device
    imdev = hcam.pgp.Device(device)
    if width > 0 and height > 0:
        pgpap(width, height / width)

    # set up panels and axes
    nccd = len(ccds)
    ny = nccd // nx if nccd % nx == 0 else nccd // nx + 1

    # slice up viewport
    pgsubp(nx, ny)

    # plot axes, labels, titles. Happens once only
    for cnam in ccds:
        pgsci(hcam.pgp.Params["axis.ci"])
        pgsch(hcam.pgp.Params["axis.number.ch"])
        pgenv(xlo, xhi, ylo, yhi, 1, 0)
        pglab("X", "Y", "CCD {:s}".format(cnam))

    # initialisations. 'last_ok' is used to store the last OK frames of each
    # CCD for retrieval when coping with skipped data.

    total_time = 0  # time waiting for new frame

    nhdu = len(ccds) * [0]
    thetas = np.linspace(0, 2 * np.pi, 100)

    # values of various parameters
    fwhm_min, beta, beta_min, beta_max, readout, max_nfev = (
        2.0,
        4.0,
        1.5,
        10.0,
        5.5,
        100,
    )

    # number of failed fits
    nfail = 0

    # open the output file for results
    with fitsio.FITS(output, "rw", clobber=True) as fout:

        # plot images
        with spooler.data_source(source, resource, first, full=False) as spool:

            # 'spool' is an iterable source of MCCDs
            n = 0
            for nf, mccd in enumerate(spool):

                if server_or_local:
                    # Handle the waiting game ...
                    give_up, try_again, total_time = spooler.hang_about(
                        mccd, twait, tmax, total_time
                    )

                    if give_up:
                        print("ftargets stopped")
                        break
                    elif try_again:
                        continue

                # Trim the frames: ULTRACAM windowed data has bad columns
                # and rows on the sides of windows closest to the readout
                # which can badly affect reduction. This option strips
                # them.
                if trim:
                    hcam.ccd.trim_ultracam(mccd, ncol, nrow)

                # indicate progress
                tstamp = Time(mccd.head["TIMSTAMP"], format="isot", precision=3)
                print(
                    "{:d}, utc= {:s} ({:s}), ".format(
                        mccd.head["NFRAME"],
                        tstamp.iso,
                        "ok" if mccd.head.get("GOODTIME", True) else "nok",
                    ),
                    end="",
                )

                # accumulate errors
                emessages = []

                if n == 0:
                    if bias is not None:
                        # crop the bias on the first frame only
                        bias = bias.crop(mccd)

                    if flat is not None:
                        # crop the flat on the first frame only
                        flat = flat.crop(mccd)

                    # compute maximum length of window name strings
                    lsmax = 0
                    for ccd in mccd.values():
                        for wnam in ccd:
                            lsmax = max(lsmax, len(wnam))

                # display the CCDs chosen
                message = ""
                pgbbuf()
                for nc, cnam in enumerate(ccds):
                    ccd = mccd[cnam]

                    if ccd.is_data():
                        # this should be data as opposed to a blank frame
                        # between data frames that occur with nskip > 0

                        # subtract the bias
                        if bias is not None:
                            ccd -= bias[cnam]

                        # divide out the flat
                        if flat is not None:
                            ccd /= flat[cnam]

                        # Where the fancy stuff happens ...
                        # estimate sky background, look for stars
                        objs, dofwhms = [], []
                        nobj = 0
                        for wnam in ccd:
                            try:
                                # chop window, find objects
                                wind = ccd[wnam].window(xlo, xhi, ylo, yhi)
                                wind.data = wind.data.astype("float")
                                objects, bkg = findStars(wind, thresh, fwhm, True)

                                # subtract background from frame for display
                                # purposes.
                                bkg.subfrom(wind.data)
                                ccd[wnam] = wind

                                # remove targets with too few pixels
                                objects = objects[objects["tnpix"] >= minpix]

                                # run nearest neighbour search on all
                                # objects, but select only a subset
                                # with right count levels for FWHM
                                # measurement, and which are not too
                                # elongated
                                results = isolated(objects["x"], objects["y"], rmin)

                                peaks = objects["peak"]
                                ok = (
                                    (peaks < pmax)
                                    & (peaks > pmin * objects["thresh"])
                                    & (objects["a"] < emax * objects["b"])
                                )
                                dfwhms = np.array(
                                    [
                                        i
                                        for i in range(len(results))
                                        if ok[i] and len(results[i]) == 1
                                    ], dtype=int
                                )

                                # pick the brightest. 'dfwhms' are the
                                # indices of the selected targets for
                                # FWHM measurement
                                if len(dfwhms):
                                    fluxes = objects["flux"][dfwhms]
                                    isort = np.argsort(fluxes)[::-1]
                                    dfwhms = dfwhms[isort[:nmax]]

                                # buffer for storing the FWHMs, including NaNs
                                # for the ones thet are skipped
                                fwhms = np.zeros_like(peaks, dtype=np.float32)
                                betas = np.zeros_like(peaks, dtype=np.float32)
                                nfevs = np.zeros_like(peaks, dtype=np.int32)

                                for i, (x, y, peak, fwhm) in enumerate(
                                        zip(
                                            objects["x"],
                                            objects["y"],
                                            peaks,
                                            objects["fwhm"],
                                        )
                                ):
                                    # fit FWHMs of selected targets

                                    if i in dfwhms:

                                        try:
                                            # extract fit Window
                                            fwind = wind.window(
                                                x - rmin, x + rmin, y - rmin, y + rmin
                                            )

                                            # fit profile
                                            ofwhm = fwhm
                                            obeta = beta
                                            (
                                                (sky, height, x, y, fwhm, beta),
                                                epars,
                                                (
                                                    wfit,
                                                    X,
                                                    Y,
                                                    sigma,
                                                    chisq,
                                                    nok,
                                                    nrej,
                                                    npar,
                                                    nfev,
                                                ),
                                            ) = hcam.fitting.fitMoffat(
                                                fwind,
                                                None,
                                                peak,
                                                x,
                                                y,
                                                fwhm,
                                                2.0,
                                                False,
                                                beta,
                                                beta_max,
                                                False,
                                                readout,
                                                gain,
                                                rej,
                                                1,
                                                max_nfev,
                                            )
                                            fwhms[i] = fwhm
                                            betas[i] = beta
                                            nfevs[i] = nfev

                                            # keep value of beta for next round under control
                                            beta = min(beta_max, max(beta_min, beta))

                                        except hcam.HipercamError as err:
                                            emessages.append(
                                                " >> Targ {:d}: fit failed ***: {!s}".format(
                                                    i, err
                                                )
                                            )
                                            fwhms[i] = np.nan
                                            betas[i] = np.nan
                                            nfevs[i] = 0
                                            nfail += 1
                                    else:
                                        # skip this one
                                        fwhms[i] = np.nan
                                        betas[i] = np.nan
                                        nfevs[i] = 0

                                # tack on frame number & window name
                                frames = (nf + first) * np.ones(
                                    len(objects), dtype=np.int32
                                )
                                wnams = np.array(
                                    len(objects) * [wnam], dtype="U{:d}".format(lsmax)
                                )
                                objects = append_fields(
                                    objects,
                                    ("ffwhm", "beta", "nfev", "nframe", "wnam"),
                                    (fwhms, betas, nfevs, frames, wnams),
                                )

                                # save the objects and the
                                objs.append(objects)
                                dofwhms.append(dfwhms + nobj)
                                print(dfwhms,nobj,dofwhms)
                                nobj += len(objects)

                            except hcam.HipercamError:
                                # window may have no overlap with xlo, xhi
                                # ylo, yhi
                                pass

                        if len(objs):
                            # Plot targets found

                            # concatenate results of all Windows
                            objs = np.concatenate(objs)
                            dofwhms = np.concatenate(dofwhms)

                            # set to the correct panel and then plot CCD
                            ix = (nc % nx) + 1
                            iy = nc // nx + 1
                            pgpanl(ix, iy)
                            vmin, vmax = hcam.pgp.pCcd(
                                ccd,
                                iset,
                                plo,
                                phi,
                                ilo,
                                ihi,
                                xlo=xlo,
                                xhi=xhi,
                                ylo=ylo,
                                yhi=yhi,
                            )

                            pgsci(core.CNAMS["red"])
                            As, Bs, Thetas, Xs, Ys = (
                                objs["a"],
                                objs["b"],
                                objs["theta"],
                                objs["x"],
                                objs["y"],
                            )
                            for a, b, theta0, x, y in zip(As, Bs, Thetas, Xs, Ys):
                                xs = x + 3 * a * np.cos(thetas + theta0)
                                ys = y + 3 * b * np.sin(thetas + theta0)
                                pgline(xs, ys)

                            pgsci(core.CNAMS["green"])
                            for xmin, xmax, ymin, ymax in zip(
                                objs["xmin"], objs["xmax"], objs["ymin"], objs["ymax"]
                            ):
                                xs = [xmin, xmax, xmax, xmin, xmin]
                                ys = [ymin, ymin, ymax, ymax, ymin]
                                pgline(xs, ys)

                            pgsci(core.CNAMS["blue"])
                            if len(dofwhms):
                                print(dofwhms)
                                for x, y in zip(objs["x"][dofwhms], objs["y"][dofwhms]):
                                    xs = [x - rmin, x + rmin, x + rmin, x - rmin, x - rmin]
                                    ys = [y - rmin, y - rmin, y + rmin, y + rmin, y - rmin]
                                    pgline(xs, ys)

                            # remove some less useful fields to save a
                            # bit more space prior to saving to disk
                            objs = remove_field_names(
                                objs,
                                (
                                    "x2",
                                    "y2",
                                    "xy",
                                    "cxx",
                                    "cxy",
                                    "cyy",
                                    "hfd",
                                    "cflux",
                                    "cpeak",
                                    "xcpeak",
                                    "ycpeak",
                                    "xmin",
                                    "xmax",
                                    "ymin",
                                    "ymax",
                                ),
                            )

                            # save to disk
                            if nhdu[nc]:
                                fout[nhdu[nc]].append(objs)
                            else:
                                fout.write(objs)
                                nhdu[nc] = nc + 1

                        # accumulate string of image scalings
                        if nc:
                            message += ", ccd {:s}: {:.1f}, {:.1f}, exp: {:.4f}".format(
                                cnam, vmin, vmax, mccd.head["EXPTIME"]
                            )
                        else:
                            message += "ccd {:s}: {:.1f}, {:.1f}, exp: {:.4f}".format(
                                cnam, vmin, vmax, mccd.head["EXPTIME"]
                            )

                pgebuf()
                # end of CCD display loop
                print(message)
                for emessage in emessages:
                    print(emessage)

                if pause > 0.0:
                    # pause between frames
                    time.sleep(pause)

                # update the frame number
                n += 1

    print("Number of failed fits =", nfail)
Example #13
0
def setdefect(args=None):
    """``setdefect mccd defect ccd [width height] nx msub [cmap] ffield
    (auto (hlo hhi severity)) hsbox iset (ilo ihi | plo phi)``

    Interactive definition of CCD defects. This is a matplotlib-based routine
    allowing you to define defects using the cursor. Various routines allow
    you to overplot defects, above all |rtplot| / |nrtplot|. Point-like and
    line-like defects of two different colours (orange for moderate, red for
    severe can be marked). Hot pixels can be marked by their count rate as
    measured in dark frames.

    You should apply this in stages using bias frames and flat fields
    to set the point and line defects, and a darkframe from |makedark|
    to set hot pixels. The latter can be set semi-automatically by
    flagging pixels between pre-set rate values.

    Parameters:

      mccd : str
         name of an MCCD file, as produced by e.g. 'grab'

      defect : str
         the name of a defect file. If it exists it will be read so that
         defects can be added to it. If it does not exist, it will be
         created on exiting the routine. The defect files are in a fairly
         readable / editiable text format

      ccd : str
         CCD(s) to plot, '0' for all. If not '0' then '1', '2' or even '3 4'
         are possible inputs (without the quotes). '3 4' will plot CCD '3' and
         CCD '4'. If you want to plot more than one CCD, then you will be
         prompted for the number of panels in the X direction. This parameter
         will not be prompted if there is only one CCD in the file.

      width : float [hidden]
         plot width (inches). Set = 0 to let the program choose.

      height : float [hidden]
         plot height (inches). Set = 0 to let the program choose. BOTH width
         AND height must be non-zero to have any effect

      nx : int
         number of panels across to display, prompted if more than one CCD is
         to be plotted.

      msub : bool
         True/False to subtract median from each window before scaling

      cmap : str [hidden]
         The colour map to use. "Greys" is the usual; "Greys_r" reverses it.
         There are many others; typing an incorrect one will give a list. "none"
         for matplotlib default.

      ffield : bool
         If True, all defects will be assumed to be flat-field or poor
         charge transfer defects as opposed to hot pixels. The latter
         should be set from dark frames, and have a different impact
         than the first two types in that they are worst for faint
         targets. Hot pixels and flat-field defects are shown with the
         same colours for moderate and severe, but different symbols
         (filled circles for flat-field defects, stars for hot
         pixels). If you say "no" in order to add hot pixels, the line
         defect option is not available.

      auto : bool [if ffield==False]
         Hot pixels can be set automatically if wanted. If so then a
         few extra parameters are needed. This only makes sense if you
         are feeding setdefect with a dark frame produced by
         |makedark| so that the intensity levels mean something.

      hlo : float [if auto]
         lower limit to flag as a hot pixel as a count rate per second

      hhi : float [if auto]
         upper limit to flag as a hot pixel as a count rate per second

      severity : str [if auto]
         'm' for moderate, 'n' for nasty. Controls colour used to plot
         the hot pixel; it will be labelled with a rate as well.

      hsbox : int
         half-width in binned pixels of stats box as offset from central pixel
         hsbox = 1 gives a 3x3 box; hsbox = 2 gives 5x5 etc. This is used by
         the "show" option when setting defects.

      iset : str [single character]
         determines how the intensities are determined. There are three
         options: 'a' for automatic simply scales from the minimum to the
         maximum value found on a per CCD basis. 'd' for direct just takes two
         numbers from the user. 'p' for percentile dtermines levels based upon
         percentiles determined from the entire CCD on a per CCD bais.

      ilo : float [if iset=='d']
         lower intensity level

      ihi : float [if iset=='d']
         upper intensity level

      plo : float [if iset=='p']
         lower percentile level

      phi : float [if iset=='p']
         upper percentile level

    There are a few conveniences to make setdefect easier:

      1. The plot is initialised in pan mode whereby you can move around and
         scale using the left and right mouse buttons.

      2. All input is accomplished with the keyboard; the mouse buttons are
         only for navigating the image.

      3. The label input can be switched between sequential numerical,
         single- and multi-character input ('linput').

    Various standard keyboard shortcuts (e.g. 's' to save) are disabled as
    they just confuse things and are of limited use in setdefect in any case.

    Some aspects of the usage of matplotlib in setdefect are tricky. It is
    possible that particular 'backends' will cause problems. I have tested
    this with Qt4Agg, Qt5agg and GTK3Agg. One aspect is the cursor icon in pan
    mode is a rather indistinct hand where one can't tell what is being
    pointed at. I have therefore suppressed this, but only for the tested
    backends. Others would need require further investigation.

    NB At the end of this routine, it re-orders the defects so that the severe
    ones follows the moderates. This helps emphasize the severe ones over the
    moderates when running rtplot.

    """

    command, args = utils.script_args(args)

    # get input section
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("mccd", Cline.LOCAL, Cline.PROMPT)
        cl.register("defect", Cline.LOCAL, Cline.PROMPT)
        cl.register("ccd", Cline.LOCAL, Cline.PROMPT)
        cl.register("width", Cline.LOCAL, Cline.HIDE)
        cl.register("height", Cline.LOCAL, Cline.HIDE)
        cl.register("nx", Cline.LOCAL, Cline.PROMPT)
        cl.register("msub", Cline.GLOBAL, Cline.PROMPT)
        cl.register("cmap", Cline.LOCAL, Cline.HIDE)
        cl.register("ffield", Cline.LOCAL, Cline.PROMPT)
        cl.register("auto", Cline.LOCAL, Cline.PROMPT)
        cl.register("hlo", Cline.LOCAL, Cline.PROMPT)
        cl.register("hhi", Cline.LOCAL, Cline.PROMPT)
        cl.register("severity", Cline.LOCAL, Cline.PROMPT)
        cl.register("hsbox", Cline.GLOBAL, Cline.HIDE)
        cl.register("iset", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ilo", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ihi", Cline.GLOBAL, Cline.PROMPT)
        cl.register("plo", Cline.GLOBAL, Cline.PROMPT)
        cl.register("phi", Cline.GLOBAL, Cline.PROMPT)

        # get inputs
        mccd = cl.get_value("mccd", "frame to plot",
                            cline.Fname("hcam", hcam.HCAM))
        mccd = hcam.MCCD.read(mccd)

        dfct = cl.get_value(
            "defect",
            "name of defect file",
            cline.Fname("defect", hcam.DFCT, exist=False),
        )

        if os.path.exists(dfct):
            # read in old defects
            mccd_dfct = defect.MccdDefect.read(dfct)
            print("Loaded existing file = {:s}".format(dfct))
        else:
            # create empty container
            mccd_dfct = defect.MccdDefect()
            print("No file called {:s} exists; "
                  "will create from scratch".format(dfct))

        max_ccd = len(mccd)
        if max_ccd > 1:
            ccd = cl.get_value("ccd", "CCD(s) to plot [0 for all]", "0")
            if ccd == "0":
                ccds = list(mccd.keys())
            else:
                ccds = ccd.split()
        else:
            ccds = list(mccd.keys())

        width = cl.get_value("width", "plot width (inches)", 0.0)
        height = cl.get_value("height", "plot height (inches)", 0.0)

        nxdef = cl.get_default("nx", 3)

        # number of panels in X
        if len(ccds) > 1:
            nxdef = min(len(ccds), nxdef)
            cl.set_default("nx", nxdef)
            nx = cl.get_value("nx", "number of panels in X", 3, 1)
        else:
            nx = 1

        # define the display intensities
        msub = cl.get_value("msub", "subtract median from each window?", True)

        cmap = cl.get_value("cmap",
                            "colour map to use ['none' for mpl default]",
                            "Greys")
        cmap = None if cmap == "none" else cmap

        ffield = cl.get_value("ffield",
                              "flat field defects? [else hot pixels]", True)
        if not ffield:
            auto = cl.get_value("auto", "automatic assignation of hot pixels?",
                                False)
            hlo = cl.get_value("hlo", "lower rate threshold", 10., 0.)
            hhi = cl.get_value("hhi", "upper rate threshold", max(hlo, 100.),
                               hlo)
            severity = cl.get_value("severity",
                                    "severity of hot pixels",
                                    'm',
                                    lvals=('m', 'n'))
            if severity == "m":
                severity = defect.Severity.MODERATE
            elif severity == "n":
                severity = defect.Severity.SEVERE
            else:
                raise hcam.HipercamError(
                    f"Severity = {severity} not recognised; programming error")
        else:
            auto = False

        hsbox = cl.get_value("hsbox",
                             "half-width of stats box (binned pixels)", 2, 1)
        iset = cl.get_value(
            "iset",
            "set intensity a(utomatically),"
            " d(irectly) or with p(ercentiles)?",
            "a",
            lvals=["a", "A", "d", "D", "p", "P"],
        )
        iset = iset.lower()

        plo, phi = 5, 95
        ilo, ihi = 0, 1000
        if iset == "d":
            ilo = cl.get_value("ilo", "lower intensity limit", 0.0)
            ihi = cl.get_value("ihi", "upper intensity limit", 1000.0)
        elif iset == "p":
            plo = cl.get_value("plo", "lower intensity limit percentile", 5.0,
                               0.0, 100.0)
            phi = cl.get_value("phi", "upper intensity limit percentile", 95.0,
                               0.0, 100.0)

        nxmax, nymax = 0, 0
        for cnam in ccds:
            nxmax = max(nxmax, mccd[cnam].nxtot)
            nymax = max(nymax, mccd[cnam].nytot)

        # might be worth trying to improve this at some point
        xlo, xhi, ylo, yhi = 0, nxmax + 1, 0, nymax + 1

    # Inputs obtained.

    # re-configure keyboard shortcuts to avoid otherwise confusing behaviour
    # quit_all does not seem to be universal, hence the try/except
    try:
        mpl.rcParams["keymap.back"] = ""
        mpl.rcParams["keymap.forward"] = ""
        mpl.rcParams["keymap.fullscreen"] = ""
        mpl.rcParams["keymap.grid"] = ""
        mpl.rcParams["keymap.home"] = ""
        mpl.rcParams["keymap.pan"] = ""
        mpl.rcParams["keymap.quit"] = ""
        mpl.rcParams["keymap.save"] = ""
        mpl.rcParams["keymap.pan"] = ""
        mpl.rcParams["keymap.save"] = ""
        mpl.rcParams["keymap.xscale"] = ""
        mpl.rcParams["keymap.yscale"] = ""
        mpl.rcParams["keymap.zoom"] = ""
    except KeyError:
        pass

    # start plot
    nccd = len(ccds)
    ny = nccd // nx if nccd % nx == 0 else nccd // nx + 1

    if width > 0 and height > 0:
        fig = plt.figure(figsize=(width, height))
    else:
        fig = plt.figure()

    # get the navigation toolbar. Go straight into pan mode
    # where we want to stay.
    toolbar = fig.canvas.manager.toolbar
    toolbar.pan()

    # we need to store some stuff
    ax = None
    cnams = {}
    anams = {}

    # this is a container for all the objects used to plot Defects to
    # allow deletion.
    pobjs = {}

    for n, cnam in enumerate(ccds):

        ccd = mccd[cnam]

        if ax is None:
            axes = ax = fig.add_subplot(ny, nx, n + 1)
            axes.set_aspect("equal", adjustable="box")
            axes.set_xlim(xlo, xhi)
            axes.set_ylim(ylo, yhi)
        else:
            axes = fig.add_subplot(ny, nx, n + 1, sharex=ax, sharey=ax)
            axes.set_aspect("equal", adjustable="box")

        if msub:
            # subtract median from each window
            for wind in ccd.values():
                wind -= wind.median()

        hcam.mpl.pCcd(axes,
                      ccd,
                      iset,
                      plo,
                      phi,
                      ilo,
                      ihi,
                      f"CCD {cnam}",
                      cmap=cmap)

        # keep track of the CCDs associated with each axes
        cnams[axes] = cnam

        # and axes associated with each CCD
        anams[cnam] = axes

        if auto:
            # add hot pixels if auto option enabled
            if cnam in mccd_dfct:
                cdfct = mccd_dfct[cnam]
            else:
                cdfct = mccd_dfct[cnam] = defect.CcdDefect()

            # Calculate the largest number, label the new Defect
            # with one more
            high = 0
            for ndfct in cdfct:
                try:
                    high = max(high, int(ndfct))
                except ValueError:
                    pass
            high += 1

            dexpose = mccd.head["EXPTIME"]
            clo = hlo * dexpose
            chi = hhi * dexpose

            for wnam, wind in ccd.items():
                flag = (wind.data > clo) & (wind.data < chi)
                xs = wind.x(np.arange(wind.nx))
                ys = wind.y(np.arange(wind.ny))
                Xs, Ys = np.meshgrid(xs, ys)
                for x, y, c in zip(Xs[flag], Ys[flag], wind.data[flag]):
                    cdfct[str(high)] = defect.Hot(severity, x, y, c / dexpose)
                    high += 1

        if cnam in mccd_dfct:
            # plot any pre-existing Defects, keeping track of
            # the plot objects
            pobjs[cnam] = hcam.mpl.pCcdDefect(axes, mccd_dfct[cnam])

        else:
            # add in an empty CcdDefect for any CCD not already present
            mccd_dfct[cnam] = defect.CcdDefect()

            # and an empty container for any new plot objects
            pobjs[cnam] = {}

    # create the Defect picker (see below for class def)
    picker = PickDefect(mccd, cnams, anams, toolbar, fig, mccd_dfct, dfct,
                        ffield, hsbox, pobjs)

    picker.action_prompt(False)

    # squeeze space a bit
    plt.subplots_adjust(wspace=0.1, hspace=0.1)

    # finally show stuff ....
    plt.show()
Example #14
0
def target_lookup(target,
                  ra_tel=None,
                  dec_tel=None,
                  dist=5.5,
                  url='http://simbad.u-strasbg.fr'):
    """
    Tries to determine coordinates of a target in
    one of three ways:

      1) First it attempts to find coordinates in simbad by running
         a query ID with target (unless target is None or ''). If the
         target name starts with "STD_" (GTC flux standard), that is
         stripped off first.

      2) If (1) fails, try to find coordinates within the name in the
         form JHHMMSS.S[+/-]DDMMSS [can be more precise, but not less]

      4) If (1) and (2) fail or target == '' or None, then attempt a coordinate
         search in simbad within a limit of dist arcmin (default equals half
         diagonal of ULTRASPEC. It needs a unique match in this case. The
         search position comes from ra_tel and dec_tel

    If successful, it comes back with (name, ra, dec), the matched name and position.
    ra, dec are in decimal form (hours, degrees). If it fails, it returns ('UNDEF','UNDEF','UNDEF').
    This is for use by the logging scripts.
    """

    err_msgs = []

    if url.endswith('/'):
        url += 'simbad/sim-script'
    else:
        url += '/simbad/sim-script'
    RESPC = re.compile('\s+')

    if target is not None and target != '':
        if target.startswith('STD_'):
            target = target[4:]

        # simbad query script
        query = f"""set limit 2
format object form1 "Target: %IDLIST(1) | %COO(A D;ICRS)"
query id {target}"""

        payload = {'submit': 'submit script', 'script': query}
        rep = requests.post(url, data=payload)

        data = False
        found = False
        fail = False
        nres = 0
        for line in rep.text.split('\n'):
            if line.startswith('::data::'):
                data = True

            if line.startswith('::error::'):
                err_msgs.append(
                    f'  Error occurred querying simbad for {target}')
                fail = True
                break

            if data:

                if line.startswith('Target:'):
                    nres += 1
                    if nres > 1:
                        err_msgs.append(
                            f'More than one target returned when querying simbad for {target}'
                        )
                        fail = True
                        break

                    name, coords = line[7:].split(' | ')
                    found = True

        if found and not fail:
            # OK we have found one, but we are still not done --
            # some SIMBAD lookups are no good
            name = name.strip()
            name = re.sub(RESPC, ' ', name)
            try:
                ra, dec, syst = str2radec(coords)
                return (name, ra, dec)
            except:
                err_msgs.append(
                    f'Matched "{target}" with "{name}", but failed to translate position = {coords}'
                )

        # simbad ID lookup failed, so now try to read position from
        # coordinates
        REPOS = re.compile(
            r'J(\d\d)(\d\d)(\d\d\.\d(?:\d*)?)([+-])(\d\d)(\d\d)(\d\d(?:\.\d*)?)$'
        )

        m = REPOS.search(target)
        if m:
            rah, ram, ras, decsgn, decd, decm, decs = m.group(
                1, 2, 3, 4, 5, 6, 7)
            rah, ram, ras, decd, decm, decs = int(rah), int(ram), float(
                ras), int(decd), int(decm), float(decs)
            if rah > 23 or ram > 59 or ras >= 60. or decd > 89 or decm > 59 or decs >= 60.:
                err_msgs.append(
                    f'{target} matched the positional regular expression but the numbers were out of range'
                )
            else:
                name = re.sub(RESPC, ' ', target)
                ra = rah + ram / 60 + ras / 3600
                dec = decd + decm / 60 + decs / 3600
                dec = dec if decsgn == '+' else -dec
                return (name, ra, dec)

        err_msgs.append(f'Could not extract position from {target}')

    if ra_tel is not None and dec_tel is not None and dist is not None:
        # Second
        query = f"""set limit 2
format object form1 "Target: %IDLIST(1) | %COO(A D;ICRS)"
query coo {ra_tel} {dec_tel} radius={dist}m"""

        payload = {'submit': 'submit script', 'script': query}
        rep = requests.post(url, data=payload)

        data = False
        found = False
        fail = False
        nres = 0
        for line in rep.text.split('\n'):
            if line.startswith('::data::'):
                data = True

            if line.startswith('::error::'):
                err_msgs.append(
                    f'Error occured during simbad coordinate query for {target}'
                )
                fail = True
                break

            if data:
                if line.startswith('Target:'):
                    nres += 1
                    if nres > 1:
                        err_msgs.append(
                            f'More than one target returned when querying simbad for telescope position = {ra_tel} {dec_tel} ({target})'
                        )
                        fail = True
                        break

                    name, coords = line[7:].split(' | ')
                    found = True

        if found and not fail:
            # OK we have found one, but we are still not done --
            # some SIMBAD lookups are no good
            name = name.strip()
            name = re.sub(RESPC, ' ', name)
            try:
                ra, dec, syst = str2radec(coords)
                ra_str = dec2sexg(ra, False, 2)
                dec_str = dec2sexg(dec, True, 1)
                print(
                    f'  Matched telescope position = {coords} {dec} with "{name}", position = {ra_str} {dec_str}'
                )
                return (name, ra, dec)
            except:
                err_msgs.append(
                    f'Matched telescope position = {ra} {dec} ({target}) with "{name}", but failed to translate position = {coords}'
                )

    raise hcam.HipercamError('\n'.join(err_msgs))
Example #15
0
def extractFlux(cnam, ccd, rccd, read, gain, ccdwin, rfile, store):
    """This extracts the flux of all apertures of a given CCD.

    The steps are (1) aperture resizing, (2) sky background estimation, (3)
    flux extraction. The apertures are assumed to be correctly positioned.

    It returns the results as a dictionary keyed on the aperture label. Each
    entry returns a list:

    [x, ex, y, ey, fwhm, efwhm, beta, ebeta, counts, countse, sky, esky,
    nsky, nrej, flag]

    flag = bitmask. See hipercam.core to see all the options which are
    referred to by name in the code e.g. ALL_OK. The various flags can
    signal that there no sky pixels (NO_SKY), the sky aperture was off
    the edge of the window (SKY_AT_EDGE), etc.

    This code::

       >> bset = flag & TARGET_SATURATED

    determines whether the data saturation flag is set for example.

    Arguments::

       cnam : string
          CCD identifier label

       ccd : CCD
           the debiassed, flat-fielded CCD.

       rccd : CCD
          corresponding raw CCD, used to work out whether data are
          saturated in target aperture.

       read : CCD
           readnoise divided by the flat-field

       gain : CCD
           gain multiplied by the flat field

       ccdwin : dictionary of strings
           the Window label corresponding to each Aperture

       rfile : Rfile
           reduce file configuration parameters

       store : dict of dicts
           see moveApers for what this contains.

    """

    # initialise flag
    flag = hcam.ALL_OK

    ccdaper = rfile.aper[cnam]

    # get the control parameters
    (
        resize,
        extype,
        r1fac,
        r1min,
        r1max,
        r2fac,
        r2min,
        r2max,
        r3fac,
        r3min,
        r3max,
    ) = rfile["extraction"][cnam]

    results = {}
    mfwhm = store["mfwhm"]

    if resize == "variable" or extype == "optimal":

        if mfwhm <= 0:
            # return early here as there is nothing we can do.
            print((" *** WARNING: CCD {:s}: no measured FWHM to re-size"
                   " apertures or carry out optimal extraction; no"
                   " extraction possible").format(cnam))
            # set flag to indicate no FWHM
            flag = hcam.NO_FWHM

            for apnam, aper in ccdaper.items():
                info = store[apnam]
                results[apnam] = {
                    "x": aper.x,
                    "xe": info["xe"],
                    "y": aper.y,
                    "ye": info["ye"],
                    "fwhm": info["fwhm"],
                    "fwhme": info["fwhme"],
                    "beta": info["beta"],
                    "betae": info["betae"],
                    "counts": NaN,
                    "countse": NaN,
                    "sky": NaN,
                    "skye": NaN,
                    "nsky": 0,
                    "nrej": 0,
                    "flag": flag,
                    "cmax": 0,
                }
            return results

        else:

            # Re-size the apertures
            for aper in ccdaper.values():
                aper.rtarg = max(r1min, min(r1max, r1fac * mfwhm))
                aper.rsky1 = max(r2min, min(r2max, r2fac * mfwhm))
                aper.rsky2 = max(r3min, min(r3max, r3fac * mfwhm))

    elif resize == "fixed":

        # just apply the max and min limits
        for aper in ccdaper.values():
            aper.rtarg = max(r1min, min(r1max, aper.rtarg))
            aper.rsky1 = max(r2min, min(r2max, aper.rsky1))
            aper.rsky2 = max(r3min, min(r3max, aper.rsky2))

    else:
        raise hcam.HipercamError(
            "CCD {:s}: 'variable' and 'fixed' are the only"
            " aperture resizing options".format(cnam))

    # apertures have been positioned in moveApers and now re-sized. Finally
    # we can extract something.
    for apnam, aper in ccdaper.items():

        # initialise flag
        flag = hcam.ALL_OK

        # extract Windows relevant for this aperture
        wnam = ccdwin[apnam]

        wdata = ccd[wnam]
        wread = read[wnam]
        wgain = gain[wnam]
        wraw = rccd[wnam]

        # extract sub-windows that include all of the pixels that could
        # conceivably affect the aperture. We have to check that 'extra'
        # apertures do not go beyond rsky2 which would normally be expected to
        # be the default outer radius
        rmax = aper.rsky2
        for xoff, yoff in aper.extra:
            rmax = max(rmax, np.sqrt(xoff**2 + yoff**2) + aper.rtarg)

        # this is the region of interest
        x1, x2, y1, y2 = (
            aper.x - aper.rsky2 - wdata.xbin,
            aper.x + aper.rsky2 + wdata.xbin,
            aper.y - aper.rsky2 - wdata.ybin,
            aper.y + aper.rsky2 + wdata.ybin,
        )

        try:

            # extract sub-Windows
            swdata = wdata.window(x1, x2, y1, y2)
            swread = wread.window(x1, x2, y1, y2)
            swgain = wgain.window(x1, x2, y1, y2)
            swraw = wraw.window(x1, x2, y1, y2)

            # some checks for possible problems. bitmask flags will be set if
            # they are encountered.
            xlo, xhi, ylo, yhi = swdata.extent()
            if (xlo > aper.x - aper.rsky2 or xhi < aper.x + aper.rsky2
                    or ylo > aper.y - aper.rsky2 or yhi < aper.y + aper.rsky2):
                # the sky aperture overlaps the edge of the window
                flag |= hcam.SKY_AT_EDGE

            if (xlo > aper.x - aper.rtarg or xhi < aper.x + aper.rtarg
                    or ylo > aper.y - aper.rtarg or yhi < aper.y + aper.rtarg):
                # the target aperture overlaps the edge of the window
                flag |= hcam.TARGET_AT_EDGE

            for xoff, yoff in aper.extra:
                rout = np.sqrt(xoff**2 + yoff**2) + aper.rtarg
                if (xlo > aper.x - rout or xhi < aper.x + rout
                        or ylo > aper.y - rout or yhi < aper.y + rout):
                    # an extra target aperture overlaps the edge of the window
                    flag |= hcam.TARGET_AT_EDGE

            # compute X, Y arrays over the sub-window relative to the centre
            # of the aperture and the distance squared from the centre (Rsq)
            # to save a little effort.
            x = swdata.x(np.arange(swdata.nx)) - aper.x
            y = swdata.y(np.arange(swdata.ny)) - aper.y
            X, Y = np.meshgrid(x, y)
            Rsq = X**2 + Y**2

            # squared aperture radii for comparison
            R1sq, R2sq, R3sq = aper.rtarg**2, aper.rsky1**2, aper.rsky2**2

            # sky selection, accounting for masks and extra (which we assume
            # acts like a sky mask as well)
            sok = (Rsq > R2sq) & (Rsq < R3sq)
            for xoff, yoff, radius in aper.mask:
                sok &= (X - xoff)**2 + (Y - yoff)**2 > radius**2
            for xoff, yoff in aper.extra:
                sok &= (X - xoff)**2 + (Y - yoff)**2 > R1sq

            # sky data
            dsky = swdata.data[sok]

            if len(dsky):

                # we have some sky!

                if rfile["sky"]["method"] == "clipped":

                    # clipped mean. Take average, compute RMS,
                    # reject pixels > thresh*rms from the mean.
                    # repeat until no new pixels are rejected.

                    thresh = rfile["sky"]["thresh"]
                    ok = np.ones_like(dsky, dtype=bool)
                    nrej = 1
                    while nrej:
                        slevel = dsky[ok].mean()
                        srms = dsky[ok].std()
                        nold = len(dsky[ok])
                        ok = ok & (np.abs(dsky - slevel) < thresh * srms)
                        nrej = nold - len(dsky[ok])
                        if len(dsky[ok]) == 0:
                            # no sky. will still return the flux in
                            # the aperture but set flag and the sky
                            # uncertainty to -1
                            flag |= hcam.NO_SKY
                            slevel = 0
                            serror = -1
                            nrej = 0
                            break

                    nsky = len(dsky[ok])
                    if nsky:
                        # serror -- error in the sky estimate.
                        serror = srms / np.sqrt(nsky)

                else:

                    # 'median' goes with 'photon'
                    slevel = dsky.median()
                    nsky = len(dsky)
                    nrej = 0

                    # read*gain/flat and flat over sky region
                    dread = swread.data[sok]
                    dgain = swgain.data[sok]

                    serror = np.sqrt(
                        (dread**2 + np.max(0, dsky) / dgain).sum() / nsky**2)

            else:
                # no sky. will still return the flux in the aperture but set
                # flag and the sky uncertainty to -1
                flag |= hcam.NO_SKY
                slevel = NaN
                serror = NaN
                nsky = 0
                nrej = 0

            # size of a pixel which is used to taper pixels as they approach
            # the edge of the aperture to reduce pixellation noise
            size = np.sqrt(wdata.xbin * wdata.ybin)

            # target selection, accounting for extra apertures and allowing
            # pixels to contribute if their centres are as far as size/2 beyond
            # the edge of the circle (but with a tapered weight)
            dok = Rsq < (aper.rtarg + size / 2.0)**2

            if not dok.any():
                # check there are some valid pixels
                flag |= hcam.NO_DATA
                raise hcam.HipercamError("no valid pixels in aperture")

            # check for saturation and nonlinearity
            cmax = int(swraw.data[dok].max())
            if cnam in rfile.warn:
                if cmax >= rfile.warn[cnam]["saturation"]:
                    flag |= hcam.TARGET_SATURATED

                if cmax >= rfile.warn[cnam]["nonlinear"]:
                    flag |= hcam.TARGET_NONLINEAR

            else:
                warnings.warn(
                    "CCD {:s} has no nonlinearity or saturation levels set".
                    format(cnam))

            # Pixellation amelioration:
            #
            # The weight of a pixel is set to 1 at the most and then linearly
            # declines as it approaches the edge of the aperture. The scale over
            # which it declines is set by 'size', the geometric mean of the
            # binning factors. A pixel with its centre exactly on the edge
            # gets a weight of 0.5.
            wgt = np.minimum(
                1,
                np.maximum(0, (aper.rtarg + size / 2.0 - np.sqrt(Rsq)) / size))
            for xoff, yoff in aper.extra:
                rsq = (X - xoff)**2 + (Y - yoff)**2
                dok |= rsq < (aper.rtarg + size / 2.0)**2
                wg = np.minimum(
                    1,
                    np.maximum(0, (aper.rtarg + size / 2.0 - np.sqrt(rsq)) /
                               size))
                wgt = np.maximum(wgt, wg)

            # the values needed to extract the flux.
            dtarg = swdata.data[dok]
            dread = swread.data[dok]
            dgain = swgain.data[dok]
            wtarg = wgt[dok]

            # 'override' to indicate we want to override the readout noise.
            if nsky and rfile["sky"]["error"] == "variance":
                # from sky variance
                rd = srms
                override = True
            else:
                rd = dread
                override = False

            # count above sky
            diff = dtarg - slevel

            if extype == "normal" or extype == "optimal":

                if extype == "optimal":
                    # optimal extraction. Need the profile
                    warnings.warn("Transmission plot is not reliable"
                                  " with optimal extraction")

                    mbeta = store["mbeta"]
                    if mbeta > 0.0:
                        prof = fitting.moffat(
                            X[dok],
                            Y[dok],
                            0.0,
                            1.0,
                            0.0,
                            0.0,
                            mfwhm,
                            mbeta,
                            wdata.xbin,
                            wdata.ybin,
                            rfile["apertures"]["fit_ndiv"],
                        )
                    else:
                        prof = fitting.gaussian(
                            X[dok],
                            Y[dok],
                            0.0,
                            1.0,
                            0.0,
                            0.0,
                            mfwhm,
                            wdata.xbin,
                            wdata.ybin,
                            rfile["apertures"]["fit_ndiv"],
                        )

                    # multiply weights by the profile
                    wtarg *= prof

                # now extract
                counts = (wtarg * diff).sum()

                if override:
                    # in this case, the "readout noise" includes the component
                    # due to the sky background so we use the sky-subtracted
                    # counts above sky for the object contribution.
                    var = (wtarg**2 *
                           (rd**2 + np.maximum(0, diff) / dgain)).sum()
                else:
                    # in this case we are using the true readout noise and we
                    # just use the data (which should be debiassed) without
                    # removal of the sky.
                    var = (wtarg**2 *
                           (rd**2 + np.maximum(0, dtarg) / dgain)).sum()

                if serror > 0:
                    # add in factor due to uncertainty in sky estimate
                    var += (wtarg.sum() * serror)**2

                countse = np.sqrt(var)

            else:
                raise hcam.HipercamError(
                    "extraction type = {:s} not recognised".format(extype))

            info = store[apnam]

            results[apnam] = {
                "x": aper.x,
                "xe": info["xe"],
                "y": aper.y,
                "ye": info["ye"],
                "fwhm": info["fwhm"],
                "fwhme": info["fwhme"],
                "beta": info["beta"],
                "betae": info["betae"],
                "counts": counts,
                "countse": countse,
                "sky": slevel,
                "skye": serror,
                "nsky": nsky,
                "nrej": nrej,
                "flag": flag,
                "cmax": cmax,
            }

        except hcam.HipercamError as err:

            info = store[apnam]
            flag |= hcam.NO_EXTRACTION

            results[apnam] = {
                "x": aper.x,
                "xe": info["xe"],
                "y": aper.y,
                "ye": info["ye"],
                "fwhm": info["fwhm"],
                "fwhme": info["fwhme"],
                "beta": info["beta"],
                "betae": info["betae"],
                "counts": NaN,
                "countse": NaN,
                "sky": NaN,
                "skye": NaN,
                "nsky": 0,
                "nrej": 0,
                "flag": flag,
                "cmax": 0,
            }

    # finally, we are done
    return results
Example #16
0
def makefringe(args=None):
    """``makefringe [source] (run first last [twait tmax] | flist) bias
    dark flat fpair ([nhalf]) ccd fwhm [clobber] output``

    Averages a set of images to make a frame for defringing (referred
    to elsewhere as a "fringe map").

    At long wavelengths, CCDs suffer what is known as "fringing",
    which in terms of structure looks something like the coloured
    patterns you see when there is a thin layer of oil on water. Both
    patterns are caused by interference.  In the case of CCDs this is
    caused by sky emission lines and is variable in strength, and
    additive in nature, but seems mostly fairly fixed in position.

    De-fringing in |hiper| is implemented according to the method
    suggested by Snodgrass & Carry (2013Msngr.152...14S). The idea is
    to create a set of pairs of points marking the peak and troughs of
    fringes. The differences in intensity between these point pairs in
    the data to be corrected is compared with the differences for the
    same pairs in a reference "fringe map" by taking their
    ratios. Many pairs are used so that the median can be taken to
    eliminate ratios affected by celestial targets or cosmic rays. The
    median ratio is used to scale the reference data and thereby
    subtract the fringes. ``makefringe`` is used to make the reference
    fringe map. It works as follows: given a single run or a list of
    files, it reads them all in, optionally debiases, dark-subtracts
    and flat-fields them, calculates a median count level of each one
    which is subtracted from each CCD individually. The pixel-by-pixel
    median of all frames is then calculated. It is also possible (and
    a good idea) to apply the fringe pair ratio measurements to scale
    each frame before combining them, which allows frames of similar
    pattern but varying amplitude to be combined. Finally, the output
    can be smoothed because fringes are usually a medium- to
    large-scale pattern.

    Parameters:

        source : string [hidden]
           Data source, five options:

               | 'hs' : HiPERCAM server
               | 'hl' : local HiPERCAM FITS file
               | 'us' : ULTRACAM server
               | 'ul' : local ULTRACAM .xml/.dat files
               | 'hf' : list of HiPERCAM hcm FITS-format files

           'hf' is used to look at sets of frames generated by 'grab' or
           converted from foreign data formats.

        run : string [if source ends 's' or 'l']
           run number to access, e.g. 'run034'

        first : int [if source ends 's' or 'l']
           exposure number to start from. 1 = first frame ('0' is
           not supported).

        last : int [if source ends 's' or 'l']
           last exposure number must be >= first or 0 for the whole lot.

        twait : float [if source ends 's' or 'l'; hidden]
           time to wait between attempts to find a new exposure, seconds.

        tmax : float [if source ends 's' or 'l'; hidden]
           maximum time to wait between attempts to find a new exposure,
           seconds.

        flist : string [if source ends 'f']
           name of file list. Assumed that bias, flat-fielding etc have been applied to
           these already.

        bias : str
           Name of bias frame to subtract, 'none' to ignore.

        dark : str
           Name of dark frame to subtract, 'none' to ignore.

        flat : str
           Name of flat field, 'none' to ignore.

        fpair : str
           FringePair file (see setfringe), or 'none' to ignore. If specified
           if will be used to scale the frames prior to combining.

        nhalf : int [if fpair is not 'none']
           When calculating the differences for fringe measurement,
           a region extending +/-nhalf binned pixels will be used when
           measuring the amplitudes. Basically helps the stats.

        ccd : str
           CCD(s) to process, '0' for all, '1 3' for '1' and '3' only, etc.

        fwhm : float
           FWHM (unbinned pixels) of gaussian smoothing to apply to the output.
           0 to ignore. Should be << smallest scale between fringes. Probably
           no more than 4.

        clobber : bool [hidden]
           clobber any pre-existing output files

        output  : str
           output file. Set by default to match the last part of "run"
           (but it will have a different extension so they won't
           clash). The default is 

      .. Note::

         This routine writes the files returned by 'grab' to
         automatically generated files, typically in .hipercam/tmp, to
         avoid polluting the working directory. These are removed at
         the end, but may not be if you ctrl-C. You should check
         .hipercam/tmp for redundant files every so often

    """

    command, args = utils.script_args(args)

    # get the inputs
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("source", Cline.GLOBAL, Cline.HIDE)
        cl.register("run", Cline.GLOBAL, Cline.PROMPT)
        cl.register("first", Cline.LOCAL, Cline.PROMPT)
        cl.register("last", Cline.LOCAL, Cline.PROMPT)
        cl.register("twait", Cline.LOCAL, Cline.HIDE)
        cl.register("tmax", Cline.LOCAL, Cline.HIDE)
        cl.register("flist", Cline.LOCAL, Cline.PROMPT)
        cl.register("bias", Cline.LOCAL, Cline.PROMPT)
        cl.register("dark", Cline.LOCAL, Cline.PROMPT)
        cl.register("flat", Cline.LOCAL, Cline.PROMPT)
        cl.register("fpair", Cline.LOCAL, Cline.PROMPT)
        cl.register("nhalf", Cline.LOCAL, Cline.PROMPT)
        cl.register("ccd", Cline.LOCAL, Cline.PROMPT)
        cl.register("fwhm", Cline.LOCAL, Cline.PROMPT)
        cl.register("clobber", Cline.LOCAL, Cline.HIDE)
        cl.register("output", Cline.LOCAL, Cline.PROMPT)

        # get inputs
        source = cl.get_value(
            "source",
            "data source [hs, hl, us, ul, hf]",
            "hl",
            lvals=("hs", "hl", "us", "ul", "hf"),
        )

        # set a flag
        server_or_local = source.endswith("s") or source.endswith("l")

        if server_or_local:
            resource = cl.get_value("run", "run name", "run005")
            root = os.path.basename(resource)
            cl.set_default('output', cline.Fname(root, hcam.HCAM))
            first = cl.get_value("first", "first frame to average", 1, 1)
            last = cl.get_value("last", "last frame to average (0 for all)",
                                first, 0)
            twait = cl.get_value("twait",
                                 "time to wait for a new frame [secs]", 1.0,
                                 0.0)
            tmax = cl.get_value("tmax",
                                "maximum time to wait for a new frame [secs]",
                                10.0, 0.0)

        else:
            resource = cl.get_value("flist", "file list",
                                    cline.Fname("files.lis", hcam.LIST))
            first = 1

        # bias frame (if any)
        bias = cl.get_value(
            "bias",
            "bias frame ['none' to ignore]",
            cline.Fname("bias", hcam.HCAM),
            ignore="none",
        )

        # dark frame (if any)
        dark = cl.get_value(
            "dark",
            "dark frame ['none' to ignore]",
            cline.Fname("dark", hcam.HCAM),
            ignore="none",
        )

        # flat field (if any)
        flat = cl.get_value(
            "flat",
            "flat field frame ['none' to ignore]",
            cline.Fname("flat", hcam.HCAM),
            ignore="none",
        )

        fpair = cl.get_value("fpair",
                             "fringe pair file",
                             cline.Fname("fringe", hcam.FRNG),
                             ignore="none")
        if fpair is not None:
            nhalf = cl.get_value("nhalf",
                                 "half-size of fringe measurement region", 2,
                                 0)

        ccdinf = spooler.get_ccd_pars(source, resource)

        if len(ccdinf) > 1:
            ccd = cl.get_value("ccd", "CCD(s) to process [0 for all]", "0")
            if ccd == "0":
                ccds = list(ccdinf.keys())
            else:
                ccds = ccd.split()
        else:
            ccds = list(ccdinf.keys())

        fwhm = cl.get_value(
            "fwhm", "FWHM of gaussian smoothing to apply"
            " to final result [unbinned pixels, 0 to ignore]", 4., 0.)
        clobber = cl.get_value("clobber",
                               "clobber any pre-existing files on output",
                               False)

        output = cl.get_value(
            "output",
            "median output fringe frame",
            cline.Fname("hcam", hcam.HCAM,
                        cline.Fname.NEW if clobber else cline.Fname.NOCLOBBER),
        )

    # inputs done with.

    if server_or_local or bias is not None or dark is not None or flat is not None:

        print("\nCalling 'grab' ...")

        args = [None, "prompt", source, "yes", resource]
        if server_or_local:
            args += [str(first), str(last), str(twait), str(tmax)]
        args += [
            "no",
            "none" if bias is None else bias,
            "none" if dark is None else dark,
            "none" if flat is None else flat,
            "none",
            "f32",
        ]
        resource = hcam.scripts.grab(args)

    # at this point 'resource' is a list of files, no matter the input
    # method.

    fnames = []
    with CleanUp(resource, fnames, server_or_local or bias is not None
                 or dark is not None) as cleanup:

        # Read all the files to determine mean levels (after bias
        # subtraction) save the bias-subtracted, flat-fielded,
        # mean-level subtracted and normalised results to temporary
        # files

        print("Reading all files in to determine their mean levels")
        bframe, fframe, dframe, fpairobj = None, None, None, None

        medians = {}
        for cnam in ccds:
            medians[cnam] = {}

        # We might have a load of temporaries from grab, but we are about to
        # make some more to save the bias-subtracted normalised versions.
        tdir = utils.temp_dir()

        mtype = []
        fref = {}
        with spooler.HcamListSpool(resource) as spool:

            for mccd in spool:

                if fpair is not None:
                    # prepare fringepairs
                    if fpairobj is None:
                        fpairobj = fringe.MccdFringePair.read(fpair)
                        fpairobj = fpairobj.crop(mccd, nhalf)

                # here we determine the median levels, subtract them
                # then (optionally) normalise by the ratio of fringe
                # amplitudes

                # generate the name to save to automatically
                fd, fname = tempfile.mkstemp(suffix=hcam.HCAM, dir=tdir)

                for cnam in ccds:
                    # its unlikely that fringe frames would be taken
                    # with skips, but you never know. Eliminate them
                    # from consideration now.
                    ccd = mccd[cnam]
                    if ccd.is_data():
                        cmedian = mccd[cnam].median()
                        medians[cnam][fname] = cmedian
                        mccd[cnam] -= cmedian
                        if cnam not in fref:
                            # save reference
                            fref[cnam] = mccd[cnam]

                        elif fpair is not None:
                            fscale = fpairobj[cnam].scale(
                                mccd[cnam], fref[cnam], nhalf)
                            mccd[cnam] *= fscale
                            print(f'{cnam}, fscale = {fscale:.3f}')

                # write to disk, save the name, close the filehandle
                fnames.append(fname)
                mccd.write(fname)
                os.close(fd)

                # a bit of progress info
                print(f"Saved {', '.join(mtype)} frame to {fname}")

        # now we go through CCD by CCD, using the first as a template
        # for the window names in which we will also store the results.
        template = hcam.MCCD.read(fnames[0])

        # Now process each file CCD by CCD to reduce the memory
        # footprint
        for cnam in ccds:

            # Read files into memory, insisting that they
            # all have the same set of CCDs
            print(f"\nLoading all CCDs labelled '{cnam}'")

            accds = []
            with spooler.HcamListSpool(fnames, cnam) as spool:

                mean = None
                for ccd in spool:
                    if ccd.is_data():
                        accds.append(ccd)

            if len(accds) == 0:
                raise hcam.HipercamError(
                    f"Found no valid examples of CCD {cnam}"
                    f" in {fnames}")
            else:
                print(f"Loaded {len(accds)} CCDs")

            # Median combine
            for wnam, wind in template[cnam].items():

                # build list of all data arrays
                arrs = [ccd[wnam].data for ccd in accds]

                # convert to 3D numpy array
                arr3d = np.stack(arrs)

                wind.data = np.median(arr3d, axis=0)

            # Add history
            template[cnam].head.add_history(
                f"Median combine of {len(accds)} images")
            print("Computed and stored their pixel-by-pixel median")

        # Remove any CCDs not included to avoid impression of having done
        # something to them
        dcnams = []
        for cnam in template.keys():
            if cnam not in ccds:
                dcnams.append(cnam)
        for cnam in dcnams:
            del template[cnam]

        if fwhm > 0:
            # apply gaussian smoothing
            print(f'Smoothing with FWHM = {fwhm}')
            sigma = fwhm / np.sqrt(8 * np.log(2))
            kern = Gaussian2DKernel(sigma)
            for ccd in template.values():
                for wind in ccd.values():
                    wind.data = convolve_fft(wind.data, kern, "fill",
                                             wind.median())

        # write out
        template.write(output, clobber)
        print(f"\nFinal result written to {output}")
    print('makefringe finished')
Example #17
0
def genred(args=None):
    """``genred apfile rfile comment bias flat dark linear inst [ncpu extendx
    ccd location smoothfwhm method beta betamax fwhm fwhmmin searchwidth thresh
    hminref hminnrf rfac rmin rmax sinner souter scale psfgfac psfwidth psfpostweak]``

    Generates a reduce file as needed by |reduce| or |psf_reduce|. You give it
    the name of an aperture file and a few other parameters and it will write
    out a reduce file which you can then refine by hand. A few simplifying
    assumptions are made, e.g. that the target is called '1'; see below for more.
    This script effectively defines the format of reduce files. The script attempts
    above all to generate a self-consistent reduce file.  e.g. if there are no
    apertures in CCD 5, it does not attempt to plot any corresponding light
    curves.

    To avoid excessive prompting, |genred| has many hidden parameters. The
    very first time you use it on a run, specify ``prompt`` on the command line
    to see all of these.  They are chosen to be the parameters most likely to
    vary with telescope or conditions; many others are left at default values
    and require editing to change. If you find yourself repeatedly editing a
    parameter, let me know and I will add it to this routine.

    Parameters:

        apfile   : string
           the input aperture file created using |setaper| (default extension
           .ape). This will be read for the targets. The main target will be
           assumed to have been called '1', the main comparison '2'. If there
           is a '3' it will be plotted relative to '2'; all others will be
           ignored for plotting purposes. Target '2' will be used to define
           the position and transmission plots for one CCD only [user
           definable]. Target '1' will be used for the seeing plot unless it
           is linked when target '2' will be used instead.

        rfile    : string
           the output reduce file created using |setaper|. This will be read
           for the targets. The main target will be assumed to have been
           called '1', the main comparison '2'. If there is a '3' it will be
           plotted relative to '2'; all others will be ignored for plotting
           purposes.

        comment : string
           comment to add near the top of the reduce file. Obvious things to say
           are the target name and the name of the observer for instance.

        bias : string
           Name of bias frame; 'none' to ignore.

        flat : string
           Name of flat field frame; 'none' to ignore.

        dark : string
           Name of dark frame; 'none' to ignore.

        linear  : string
           light curve plot linear (else magnitudes)

        inst : string
           the instrument (needed to set nonlinearity and saturation levels for
           warning purposes. Possible options listed.

        ncpu : int [hidden]
           some increase in speed can be obtained by running the
           reduction in parallel. This parameter is the number of CPUs
           to use. The parellisation is over the CCDs, and there is no
           point having ncpu greater than the number of CCDs, but it should
           ideally equal the number of CCDs. There is no point using this
           for single CCD reduction.

        ngroup : int [hidden, if ncpu > 1]
           to reduce parallelisation overheads, this parameter means that ngroup
           frames are read before being split up for the parallisation step is applied.

        extendx : float [hidden]
           how many minutes to extend light curve plot by at a time

        ccd : string [hidden]
           label of the (single) CCD used for the position plot

        location : string [hidden]
           whether to reposition apertures or leave them fixed.

        toffset : int [hidden]
           integer offset to subtract from the MJD times in order to
           reduce round-off.  i.e. rather 55678.123456789, if you
           specified toffset=55600, you would reduce the round-off
           error by a factor ~1000. With typical MJDs, round off is
           around the 0.5 microsecond level. If you want to improve on
           that, set an appropriate offset value. It is set by default
           to 0, i.e. it will be 0 unless you explicitly set it
           otherwise. The value used is recorded in the log
           file. toffset must be >= 0.

        smoothfwhm : float [hidden]
           FWHM to use for smoothing during initial search [binned pixels]

        fft : bool [hidden]
           whether or not to use FFTs when carrying out the convolution
           operation used in the initial search. No effect on results,
           but could be faster for large values of smoothfwhm.

        method   : string
           profile fitting method. 'g' for gaussian, 'm' for moffat

        beta     : float [hidden]
           default Moffat exponent to use to start fitting

        betamax  : float [hidden]
           maximum Moffat exponent to pass on to subsequent fits. Prevents
           it wandering to silly values which can happen.

        fwhm     : float [hidden]
           the default FWHM to use when fitting [unbinned pixels].

        fwhmmin  : float [hidden]
           the default FWHM to use when fitting [unbinned pixels].

        searchwidth : int [hidden]
           half width in (binned) pixels for the target searches

        fitwidth : int [hidden]
           half width in (binned) pixels for the profile fits

        maxshift : float [hidden]
           maximum shift of non-reference targets relative to the initial
           positions derived from the reference targets. The reference targets
           should give good initial positions, thus this can be set to quite a
           small value to improve robustness against duff positions, caused
           e.g. by cosmic rays or field crowding in periods of bad seeing. Use
           linked apertures for more difficult cases still.

        thresh : float [hidden]
           RMS rejection threshold for profile fits.

        hminref : float [hidden]

           minimum peak height for a fit to a reference aperture to be
           accepted. This applies to the peak height in the *smoothed* image
           used during the initial search as well as the peak height after
           profile fitting. It is the check on the smoothed image that is more
           consistent since the seeing-limited peak height can be highly
           variable, and the more stringent since smoothing reduces the peak
           height by seeing**2/(seeing**2+smoothfwhm**2) where 'seeing' is the
           FWHM of the seeing in binned pixels. If smoothfwhm is chosen to be
           larger than seeing is likely to be, this makes the peak heights
           more consistent so that the thresholding is better behaved. But, it
           means you should use a value lower than you might guess from the
           images.  A peak of height `h` in a smoothed image will contain
           about (seeing**2+smoothfwhm**2)*h counts in total (but fewer if
           binned).

        hminnrf : float [hidden]
           minimum peak height for a fit to a non-reference aperture to be
           accepted. This applies above all to the smoothed peak height as for
           hminref.

        alpha : float [hidden]
           amount by which non-reference apertures are corrected relative to
           their expected positions when reference apertures are enabled. The
           idea is that the positions of non-reference relative to reference
           apertures should vary little, so rather than simply re-positioning
           independently every frame, one might want to build in a bit of past
           history. This can be done by setting alpha small. If alpha = 1,
           then that simply returns to fully independent positioning for each
           frame.

        diff : float [hidden]
           maximum difference in the shifts of reference apertures, when more
           than one has been defined on a CCD. If exceeded, no extraction is
           performed. This is to guard against perturbations of the reference
           apertures by meteors, cosmic rays and the like. [unbinned pixels]

        rfac : float [hidden]
           target aperture radius relative to the FWHM for 'variable' aperture
           photometry. Usual values 1.5 to 2.5.

        rmin : float [hidden]
           minimum target aperture radius [unbinned pixels]

        rmax     : float [hidden]
           maximum target aperture radius [unbinned pixels]

        sinner   : float [hidden]
           inner sky aperture radius [unbinned pixels]

        souter   : float [hidden]
           outer sky aperture radius [unbinned pixels]

        readout : float | string [hidden]
           readout noise, RMS ADU. Can either be a single value or an hcm file.

        gain : float [hidden]
           gain, electrons per ADU. Can either be a single value or an hcm file.

        scale    : float [hidden]
           image scale in arcsec/pixel

        psfgfac : float [hidden]
            multiple of FWHM used to group objects together for PSF fitting

        psfwidth : int [hidden]
            half-width of box used to extract data around objects for PSF fitting

        psfpostweak : string [hidden]
            During PSF fitting, either hold positions at aperture location ('fixed'),
            or fit as part of PSF model ('variable')

    """

    #    print(my_version)

    command, args = utils.script_args(args)

    with Cline('HIPERCAM_ENV', '.hipercam', command, args) as cl:

        # register parameters
        cl.register('apfile', Cline.LOCAL, Cline.PROMPT)
        cl.register('rfile', Cline.GLOBAL, Cline.PROMPT)
        cl.register('comment', Cline.LOCAL, Cline.PROMPT)
        cl.register('bias', Cline.LOCAL, Cline.PROMPT)
        cl.register('flat', Cline.LOCAL, Cline.PROMPT)
        cl.register('dark', Cline.LOCAL, Cline.PROMPT)
        cl.register('linear', Cline.LOCAL, Cline.PROMPT)
        cl.register('inst', Cline.LOCAL, Cline.HIDE)
        cl.register('ncpu', Cline.LOCAL, Cline.HIDE)
        cl.register('ngroup', Cline.LOCAL, Cline.HIDE)
        cl.register('extendx', Cline.LOCAL, Cline.HIDE)
        cl.register('ccd', Cline.LOCAL, Cline.HIDE)
        cl.register('location', Cline.LOCAL, Cline.HIDE)
        cl.register('toffset', Cline.LOCAL, Cline.HIDE)
        cl.register('smoothfwhm', Cline.LOCAL, Cline.HIDE)
        cl.register('fft', Cline.LOCAL, Cline.HIDE)
        cl.register('beta', Cline.LOCAL, Cline.HIDE)
        cl.register('betamax', Cline.LOCAL, Cline.HIDE)
        cl.register('fwhm', Cline.LOCAL, Cline.HIDE)
        cl.register('method', Cline.LOCAL, Cline.HIDE)
        cl.register('fwhmmin', Cline.LOCAL, Cline.HIDE)
        cl.register('searchwidth', Cline.LOCAL, Cline.HIDE)
        cl.register('fitwidth', Cline.LOCAL, Cline.HIDE)
        cl.register('maxshift', Cline.LOCAL, Cline.HIDE)
        cl.register('thresh', Cline.LOCAL, Cline.HIDE)
        cl.register('hminref', Cline.LOCAL, Cline.HIDE)
        cl.register('hminnrf', Cline.LOCAL, Cline.HIDE)
        cl.register('alpha', Cline.LOCAL, Cline.HIDE)
        cl.register('diff', Cline.LOCAL, Cline.HIDE)
        cl.register('rfac', Cline.LOCAL, Cline.HIDE)
        cl.register('rmin', Cline.LOCAL, Cline.HIDE)
        cl.register('rmax', Cline.LOCAL, Cline.HIDE)
        cl.register('sinner', Cline.LOCAL, Cline.HIDE)
        cl.register('souter', Cline.LOCAL, Cline.HIDE)
        cl.register('readout', Cline.LOCAL, Cline.HIDE)
        cl.register('gain', Cline.LOCAL, Cline.HIDE)
        cl.register('scale', Cline.LOCAL, Cline.HIDE)
        cl.register('psfgfac', Cline.LOCAL, Cline.HIDE)
        cl.register('psfwidth', Cline.LOCAL, Cline.HIDE)
        cl.register('psfpostweak', Cline.LOCAL, Cline.HIDE)

        # get inputs

        # the aperture file
        apfile = cl.get_value('apfile', 'aperture input file',
                              cline.Fname('aper.ape', hcam.APER))
        # Read the aperture file
        aper = hcam.MccdAper.read(apfile)

        # the reduce file
        rfile = cl.get_value(
            'rfile', 'reduce output file',
            cline.Fname('reduce.red', hcam.RED, cline.Fname.NEW))

        # user comment string
        comment = cl.get_value(
            'comment', 'user comment to add [<cr>'
            ' for newline to get multilines]', '')
        if comment == '':
            comment = '# There was no user comment\n'
        else:
            comment_lines = comment.split('<cr>')
            comment = '# User comment:\n#\n# ' + '\n# '.join(comment_lines)

        # ones you might quite often want to change

        # bias frame
        bias = cl.get_value('bias',
                            "bias frame ['none' to ignore]",
                            cline.Fname('bias', hcam.HCAM),
                            ignore='none')
        bias = '' if bias is None else bias

        # flat field frame
        flat = cl.get_value('flat',
                            "flat field frame ['none' to ignore]",
                            cline.Fname('flat', hcam.HCAM),
                            ignore='none')
        flat = '' if flat is None else flat

        # dark frame
        dark = cl.get_value('dark',
                            "dark field frame ['none' to ignore]",
                            cline.Fname('dark', hcam.HCAM),
                            ignore='none')
        dark = '' if dark is None else dark

        inst = cl.get_value(
            'inst',
            'instrument (hipercam, ultracam, ultraspec)',
            'hipercam',
            lvals=['hipercam', 'ultracam', 'ultraspec', 'ignore'])

        if inst == 'hipercam':
            warn_levels = """# Warning levels for instrument = HiPERCAM
warn = 1 50000 64000
warn = 2 50000 64000
warn = 3 50000 64000
warn = 4 50000 64000
warn = 5 50000 64000
"""
            maxcpu = 5

        elif inst == 'ultracam':
            warn_levels = """# Warning levels for instrument = ULTRACAM
warn = 1 28000 64000
warn = 2 28000 64000
warn = 3 50000 64000
"""
            maxcpu = 3

        elif inst == 'ultraspec':
            warn_levels = """# Warning levels for instrument = ULTRASPEC
warn = 1 60000 64000
"""
            maxcpu = 1

        else:
            warn_levels = """# No warning levels have been set!!"""
            maxcpu = 20

        if maxcpu > 1:
            ncpu = cl.get_value('ncpu',
                                'number of CPUs to use (<= number of CCDs)', 1,
                                1, maxcpu)
        else:
            ncpu = 1

        if ncpu > 1:
            ngroup = cl.get_value(
                'ngroup',
                'number of frames per group to reduce parallelisation overheads',
                1, 1)
        else:
            ngroup = 1

        linear = cl.get_value('linear', 'linear light curve plot?', False)
        linear = 'yes' if linear else 'no'

        # hidden parameters

        extendx = cl.get_value('extendx',
                               'how much to extend light curve plot [mins]',
                               10., 0.01)

        ccd = cl.get_value('ccd',
                           'label for the CCD used for the position plot', '2')
        if ccd not in aper:
            raise hcam.HipercamError(
                'CCD {:s} not found in aperture file {:s}'.format(ccd, apfile))

        # hidden parameters
        location = cl.get_value('location',
                                'aperture location, f(ixed) or v(ariable)',
                                'v',
                                lvals=['f', 'v'])
        location = 'variable' if location == 'v' else 'fixed'
        comm_seeing = '#' if location == 'fixed' else ''
        comm_position = '#' if location == 'fixed' else ''

        cl.set_default('toffset', 0)
        toffset = cl.get_value(
            'toffset',
            'offset to subtract from the MJD times (to reduce round-off) [days]',
            0, 0)

        smooth_fwhm = cl.get_value('smoothfwhm',
                                   'search smoothing FWHM [binned pixels]', 6.,
                                   3.)

        smooth_fft = cl.get_value('fft', 'use FFT when smoothing', False)

        profile_type = cl.get_value(
            'method',
            'profile fit method, g(aussian) or m(offat)',
            'g',
            lvals=['g', 'm'])
        profile_type = 'gaussian' if profile_type == 'g' else 'moffat'

        beta = cl.get_value('beta', 'starting value of beta', 4., 3.)

        beta_max = cl.get_value(
            'betamax', 'maximum value of beta to start consecutive fits', 20.,
            beta)

        fwhm = cl.get_value('fwhm', 'starting FWHM, unbinned pixels', 5., 1.5)

        fwhm_min = cl.get_value('fwhmmin', 'minimum FWHM, unbinned pixels',
                                1.5, 0.)

        search_half_width = cl.get_value(
            'searchwidth', 'half width for initial searches, unbinned pixels',
            11, 3)

        fit_half_width = cl.get_value(
            'fitwidth', 'half width for profile fits, unbinned pixels', 21, 5)

        fit_max_shift = cl.get_value(
            'maxshift', 'maximum non-reference shift, unbinned pixels', 15.,
            0.)

        thresh = cl.get_value('thresh',
                              'RMS rejection threshold for fits (sigma)', 5.,
                              2.)

        height_min_ref = cl.get_value(
            'hminref',
            'minimum peak height for a fit to reference aperture [counts]',
            10., 0.)

        height_min_nrf = cl.get_value(
            'hminnrf',
            'minimum peak height for a fit to non-reference aperture [counts]',
            5., 0.)

        fit_alpha = cl.get_value(
            'alpha',
            'non-reference aperture fractional shift parameter (range: (0,1])',
            1., 1.e-5, 1.)

        fit_diff = cl.get_value(
            'diff', 'maximum differential reference aperture shift', 2., 1.e-5)

        rfac = cl.get_value('rfac', 'target aperture scale factor', 1.8, 1.0)

        rmin = cl.get_value(
            'rmin', 'minimum target aperture radius [unbinned pixels]', 6., 1.)

        rmax = cl.get_value(
            'rmax', 'maximum target aperture radius [unbinned pixels]', 30.,
            rmin)

        sinner = cl.get_value('sinner',
                              'inner sky aperture radius [unbinned pixels]',
                              30., rmax)

        souter = cl.get_value('souter',
                              'outer sky aperture radius [unbinned pixels]',
                              50., sinner + 1)

        readout = cl.get_value('readout',
                               'readout noise, RMS ADU (float or file name)',
                               '4.5')

        gain = cl.get_value('gain',
                            'gain, electrons/ADU, (float or file name)', '1.1')

        scale = cl.get_value('scale', 'image scale [arcsec/unbinned pixel]',
                             0.3, 0.001)

        psfgfac = cl.get_value(
            'psfgfac',
            'multiple of FWHM used to group objects for PSF fitting', 3, 0.1)

        psfwidth = cl.get_value('psfwidth',
                                'half width for PSF fits, unbinned pixels', 15,
                                5)

        psfpostweak = cl.get_value(
            'psfpostweak',
            'locations during PSF fitting stage, f(ixed) or v(ariable)',
            'f',
            lvals=['f', 'v'])
        psfpostweak = 'variable' if psfpostweak == 'v' else 'fixed'

    ################################################################
    #
    # all the inputs have now been obtained. Get on with doing stuff

    # Generate the extraction lines. Note that the aperture location
    # parameter maps into the same names as the aperture re-size
    # parameter
    extraction = ''
    for cnam in aper:
        extraction += ('{:s} = {:s} normal'
                       ' {:.2f} {:.1f} {:.1f}'
                       ' 2.5 {:.1f} {:.1f}'
                       ' 3.0 {:.1f} {:.1f}\n').format(cnam, location, rfac,
                                                      rmin, rmax, sinner,
                                                      sinner, souter, souter)

    # standard colours for CCDs
    if inst == 'hipercam':
        CCD_COLS = {
            '1': 'purple',
            '2': 'green',
            '3': 'orange',
            '4': 'red',
            '5': 'darkred'
        }

    elif inst == 'ultracam':
        CCD_COLS = {
            '1': 'red',
            '2': 'green',
            '3': 'blue',
        }

    elif inst == 'ultraspec':
        CCD_COLS = {
            '1': 'green',
        }

    # Generate the light curve plot lines
    light_plot = ''
    no_light = True
    for cnam in aper:
        ccdaper = aper[cnam]
        if '1' in ccdaper and '2' in ccdaper:
            light_plot += (
                'plot = {:s} 1 2 0 1 {:10s} !  '
                ' # ccd, targ, comp, off, fac, dcol, ecol\n').format(
                    cnam, CCD_COLS[cnam])
            no_light = False

        elif '1' in ccdaper and '2' not in ccdaper:
            light_plot += (
                'plot = {:s} 1 ! 0 1 {:10s} !  '
                ' # ccd, targ, comp, off, fac, dcol, ecol\n').format(
                    cnam, CCD_COLS[cnam])
            no_light = False

        if '2' in ccdaper and '3' in ccdaper:
            light_plot += (
                'plot = {:s} 3 2 0 1 {:10s} !  '
                ' # ccd, targ, domp, off, fac, dcol, ecol\n').format(
                    cnam, CCD_COLS[cnam])
            no_light = False

    if no_light:
        raise hcam.HipercamError(
            'Found no targets for light curve plots in any CCD; cannot make light curve plot'
        )

    # Generate the position plot lines
    position_plot = ''
    ccdaper = aper[ccd]
    no_position = True
    if '2' in ccdaper:
        position_plot += ('{:s}plot = {:s} 2 {:10s} !  '
                          ' # ccd, targ, dcol, ecol\n').format(
                              comm_position, ccd, CCD_COLS[ccd])
        no_position = False

    elif '3' in ccdaper:
        position_plot += ('{:s}plot = {:s} 3 {:10s} !  '
                          ' # ccd, targ, dcol, ecol\n').format(
                              comm_position, ccd, CCD_COLS[ccd])
        no_position = False

    elif '1' in ccdaper:
        position_plot += ('{:s}plot = {:s} 1 {:10s} !  '
                          ' # ccd, targ, dcol, ecol\n').format(
                              comm_position, ccd, CCD_COLS[ccd])
        no_position = False

    if no_position:
        raise hcam.HipercamError(
            'Targets 1, 2 and 3 not found in '
            'CCD = {:s}; cannot make position plot'.format(ccd))

    # Generate the transmission plot lines
    transmission_plot = ''
    no_transmission = True
    for cnam in aper:
        ccdaper = aper[cnam]
        if '2' in ccdaper:
            transmission_plot += ('plot = {:s} 2 {:10s} !  '
                                  ' # ccd, targ, dcol, ecol\n').format(
                                      cnam, CCD_COLS[cnam])
            no_transmission = False

        elif '3' in ccdaper:
            transmission_plot += ('plot = {:s} 3 {:10s} !  '
                                  ' # ccd, targ, dcol, ecol\n').format(
                                      cnam, CCD_COLS[cnam])
            no_transmission = False

        elif '1' in ccdaper:
            transmission_plot += ('plot = {:s} 1 {:10s} !  '
                                  ' # ccd, targ, dcol, ecol\n').format(
                                      cnam, CCD_COLS[cnam])
            no_transmission = False

    if no_transmission:
        raise hcam.HipercamError('Targets 1, 2 and 3 not found in any CCDs;'
                                 ' cannot make transmission plot')

    # Generate the seeing plot lines
    seeing_plot = ''
    no_seeing = True
    for cnam in aper:
        ccdaper = aper[cnam]
        if '1' in ccdaper and not ccdaper['1'].linked:
            seeing_plot += ('{:s}plot = {:s} 1 {:10s} !  '
                            ' # ccd, targ, dcol, ecol\n').format(
                                comm_seeing, cnam, CCD_COLS[cnam])
            no_seeing = False

        elif '2' in ccdaper and not ccdaper['2'].linked:
            seeing_plot += ('{:s}plot = {:s} 2 {:10s} !  '
                            ' # ccd, targ, dcol, ecol\n').format(
                                comm_seeing, cnam, CCD_COLS[cnam])
            no_seeing = False

        elif '3' in ccdaper and not ccdaper['3'].linked:
            seeing_plot += ('{:s}plot = {:s} 3 {:10s} !  '
                            ' # ccd, targ, dcol, ecol\n').format(
                                comm_seeing, cnam, CCD_COLS[cnam])
            no_seeing = False

    if no_seeing:
        raise hcam.HipercamError(
            'Targets 1, 2 and 3 not found in any CCD'
            ' (or they are linked); cannot make seeing plot')

    # monitor targets (whole lot by default)
    targs = set()
    for cnam in aper:
        ccdaper = aper[cnam]
        for targ in ccdaper:
            targs.add(targ)
    monitor = ''
    for targ in sorted(targs):
        monitor += ('{:s} = NO_EXTRACTION TARGET_SATURATED TARGET_AT_EDGE'
                    ' TARGET_NONLINEAR NO_SKY NO_FWHM NO_DATA SKY_AT_EDGE\n'
                    ).format(targ)

    # time stamp
    tstamp = strftime("%d %b %Y %H:%M:%S (UTC)", gmtime())

    # finally write out the reduce file.
    with open(rfile, 'w') as fout:
        # write out file
        fout.write(
            TEMPLATE.format(version=hcam.REDUCE_FILE_VERSION,
                            apfile=apfile,
                            fwhm=fwhm,
                            fwhm_min=fwhm_min,
                            extraction=extraction,
                            bias=bias,
                            flat=flat,
                            dark=dark,
                            smooth_fwhm=smooth_fwhm,
                            linear=linear,
                            light_plot=light_plot,
                            position_plot=position_plot,
                            transmission_plot=transmission_plot,
                            seeing_plot=seeing_plot,
                            monitor=monitor,
                            comment=comment,
                            tstamp=tstamp,
                            hipercam_version=hipercam_version,
                            location=location,
                            comm_seeing=comm_seeing,
                            extendx=extendx,
                            comm_position=comm_position,
                            scale=scale,
                            warn_levels=warn_levels,
                            ncpu=ncpu,
                            ngroup=ngroup,
                            search_half_width=search_half_width,
                            fit_half_width=fit_half_width,
                            profile_type=profile_type,
                            height_min_ref=height_min_ref,
                            height_min_nrf=height_min_nrf,
                            beta=beta,
                            beta_max=beta_max,
                            thresh=thresh,
                            readout=readout,
                            gain=gain,
                            fit_max_shift=fit_max_shift,
                            fit_alpha=fit_alpha,
                            fit_diff=fit_diff,
                            psfgfac=psfgfac,
                            psfpostweak=psfpostweak,
                            psfwidth=psfwidth,
                            toffset=toffset,
                            smooth_fft='yes' if smooth_fft else 'no'))

    print('Reduce file written to {:s}'.format(rfile))
Example #18
0
def genred(args=None):
    """``genred apfile rfile bias dark flat fmap fpair seeing (sworst
    binfac) template (inst (nccd (ccd) nonlin sat scale readout
    gain))``

    Generates a reduce file as needed by |reduce| or |psf_reduce|. You
    give it the name of an aperture file, calibration frames and a few
    other parameters and it will write out a reduce file which you can
    then refine by hand. The aim is to try to get a reduce file that
    is self-consistent and works to get observing started as soon as
    possible. The parameters are not necessarily the best.

    It is assumed that the target is called '1', the main comparison '2'.

    To avoid endless prompts, genred does not try to prompt for all
    parameters even via hidden ones (a change from its previous
    behaviour).  However, if you find yourself always making the samed
    edits to the file produced by genred, you may start wanting a way
    to alter some of the otherwise fixed parameters. You can do so by
    supplying a template file, which can just be from a previous run
    of genred where such parameters can be altered by editing with a
    standard text editor before running genred.

    genred recognises some standard instrument telescope combinations
    which it uses to set parameters such as the pixel scale and
    readout noise, but if you choose 'other', you will be prompted for
    these details. The main parameters it does prompt for are
    calibration files, and it allows you to define a "seeing" which
    then controls the setting of various profile fit parameters. This
    can be ignored by setting = 0, in which case any template
    parameters will be passed unchanged.

    Parameters:

        apfile : str
           the input aperture file created using |setaper| (default
           extension .ape). This will be read for the targets. The
           main target will be assumed to have been called '1', the
           main comparison '2'. If there is a '3' it will be plotted
           relative to '2'; all others will be ignored for plotting
           purposes. Target '2' will be used to define the position
           and transmission plots for one CCD only [user
           definable]. Target '1' will be used for the seeing plot
           unless it is linked when target '2' will be used instead.

        rfile : str
           the output reduce file created by this script. The main
           target will be assumed to have been called '1', the main
           comparison '2'. If there is a '3' it will be plotted
           relative to '2'; all others will be ignored for plotting
           purposes.

        bias : str
           Name of bias frame; 'none' to ignore.

        dark : str
           Name of dark frame; 'none' to ignore.

        flat : str
           Name of flat field frame; 'none' to ignore.

        fmap : str
           Name of fringe frame; 'none' to ignore.

        fpair : str [if fmap not 'none']
           File defining pairs to measure fringe amplitudes.

        seeing : float
           estimate of seeing which will be used to define several of
           the profile fitting parameters. Enter 0 to ignore and use
           defaults from the template (or genred if no template)
           instead.

        sworst : float [if seeing > 0]
           worst seeing expected during run. Expands the search and
           fitting boxes which can cause difficulties if they are
           too small. It also sets the maximum target aperture radius
           which will set to ~ 1.8*sworst, converted to pixels, and the
           sky annulus radii. It has to be at least 2*seeing. Always
           remember that you can override the automated settings using
           a pre-edited template and seeing = 0.

        binfac : int [if seeing > 0]
           binning factor. e.g. 4 if using 4x4. Needed to optimise some
           profile parameters.

        template : str
           Reduce file to use as a template for any parameters not set
           by genred. This allows one to tweak settings by running
           genred without a template, then modifying the resultant
           file and using it as a template. 'none' to ignore, and then
           these values will be set by genred by default. Some will be
           modified anyway (e.g. the calibration file names), and if
           seeing > 0, then several of the profile fitting paramaters
           will be adapted to the value given along with the
           instrument parameters.  genred will try to keep as many of
           the 'extraction', 'position', 'transmission', 'light' etc
           lines associated with particular CCDs in this case without
           modification, and will pass readout, saturation levels etc
           unchanged from the template.

        inst : str [if template == 'none']
           the instrument. 'hipercam-gtc', 'ultracam-ntt', 'ultraspec-tnt',
           'ultracam-wht', 'ultracam-vlt',  or 'other'. Sets pixel scale and
           standard readnoise params.

        nccd : int [if inst == 'other']
           number of CCDs.

        ccd : int [if nccd > 1]
           the CCD to use for position plots

        nonlin : list(float) [if inst == 'other']
           values for warning of non linearity

        sat : list(float) [if inst == 'other']
           values for warning of saturation

        scale : float [if inst == 'other']
           image scale in arcsec/pixel

        readout : float | string [if inst == 'other']
           readout noise, RMS ADU. Can either be a single value or an
           hcm file.

        gain : float | string [if inst == 'other']
           gain, electrons per ADU. Can either be a single value or an
           hcm file.

    """

    command, args = utils.script_args(args)

    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("apfile", Cline.LOCAL, Cline.PROMPT)
        cl.register("rfile", Cline.GLOBAL, Cline.PROMPT)
        cl.register("comment", Cline.LOCAL, Cline.PROMPT)
        cl.register("bias", Cline.LOCAL, Cline.PROMPT)
        cl.register("dark", Cline.LOCAL, Cline.PROMPT)
        cl.register("flat", Cline.LOCAL, Cline.PROMPT)
        cl.register("fmap", Cline.LOCAL, Cline.PROMPT)
        cl.register("fpair", Cline.LOCAL, Cline.PROMPT)
        cl.register("seeing", Cline.LOCAL, Cline.PROMPT)
        cl.register("sworst", Cline.LOCAL, Cline.PROMPT)
        cl.register("binfac", Cline.LOCAL, Cline.PROMPT)
        cl.register("inst", Cline.LOCAL, Cline.PROMPT)
        cl.register("nccd", Cline.LOCAL, Cline.PROMPT)
        cl.register("ccd", Cline.LOCAL, Cline.PROMPT)
        cl.register("nonlin", Cline.LOCAL, Cline.PROMPT)
        cl.register("sat", Cline.LOCAL, Cline.PROMPT)
        cl.register("scale", Cline.LOCAL, Cline.PROMPT)
        cl.register("readout", Cline.LOCAL, Cline.PROMPT)
        cl.register("gain", Cline.LOCAL, Cline.PROMPT)
        cl.register("template", Cline.LOCAL, Cline.PROMPT)

        # get inputs

        # the aperture file
        apfile = cl.get_value(
            "apfile", "aperture input file", cline.Fname("aper.ape", hcam.APER)
        )
        # Read the aperture file
        aper = hcam.MccdAper.read(apfile)

        # the reduce file
        root = os.path.splitext(os.path.basename(apfile))[0]
        cl.set_default("rfile", cline.Fname(root, hcam.RED, cline.Fname.NEW))
        rfile = cl.get_value(
            "rfile", "reduce output file",
            cline.Fname("reduce.red", hcam.RED, cline.Fname.NEW),
        )

        # calibrations

        # bias frame
        bias = cl.get_value(
            "bias",
            "bias frame ['none' to ignore]",
            cline.Fname("bias", hcam.HCAM),
            ignore="none",
        )

        # dark frame
        dark = cl.get_value(
            "dark",
            "dark field frame ['none' to ignore]",
            cline.Fname("dark", hcam.HCAM),
            ignore="none",
        )

        # flat field frame
        flat = cl.get_value(
            "flat",
            "flat field frame ['none' to ignore]",
            cline.Fname("flat", hcam.HCAM),
            ignore="none",
        )

        # fringe frame
        fmap = cl.get_value(
            "fmap",
            "fringe frame ['none' to ignore]",
            cline.Fname("fringe", hcam.HCAM),
            ignore="none",
        )
        if fmap is not None:
            fpair = cl.get_value(
                "fpair", "fringe pair file",
                cline.Fname("fpair", hcam.FRNG),
            )
        else:
            fpair = None

        seeing = cl.get_value(
            "seeing",
            "approximate seeing [arcsec, 0 to ignore]",
            1.0, 0.
        )
        if seeing > 0.:
            sworst = cl.get_value(
                "sworst",
                "worst expected seeing [arcsec]",
                max(3., 2.5*seeing), 2.*seeing
            )
            binfac = cl.get_value(
                "binfac", "binning factor",
                1, 1
            )

        # template
        template = cl.get_value(
            "template",
            "template reduce file ['none' to ignore]",
            cline.Fname("template", hcam.RED),
            ignore="none",
        )


        if template is None:

            instrument = cl.get_value(
                "inst",
                "the instrument-telescope",
                "hipercam-gtc",
                lvals=[
                    "hipercam-gtc", "ultracam-ntt",
                    "ultracam-wht", "ultracam-vlt",
                    "ultraspec-tnt", "other"
                ],
            )

            if instrument.startswith("hipercam"):
                warn_levels = """
warn = 1 50000 64000
warn = 2 50000 64000
warn = 3 50000 64000
warn = 4 50000 64000
warn = 5 50000 64000
                """
                maxcpu = 5
                skipbadt = False
                scale = 0.081
                readout = '4.2'
                gain = '1.1'
                nccd = 5
                ccd = '2'

            elif instrument.startswith("ultracam"):
                warn_levels = """
warn = 1 28000 64000
warn = 2 28000 64000
warn = 3 50000 64000
            """
                maxcpu = 3
                skipbadt = True
                readout = '2.8'
                gain = '1.1'
                nccd = 3
                ccd = '2'
                if instrument.endswith("ntt"):
                    scale = 0.35
                elif instrument.endswith("wht"):
                    scale = 0.30
                elif instrument.endswith("vlt"):
                    scale = 0.15

            elif instrument.startswith("ultraspec"):
                warn_levels = """
warn = 1 60000 64000
            """
                maxcpu = 1
                skipbadt = True
                scale = 0.45
                readout = '5.0'
                gain = '0.8'
                nccd = 1
                ccd = '1'

            elif instrument == "other":

                # unrecognised instrument need extra stuff
                nccd = cl.get_value("nccd", "how many CCDs?", 3, 1)
                if nccd > 1:
                    ccd = cl.get_value("ccd", "which CCD for the position plot?", 1, 1, nccd)
                    ccd = str(ccd)
                else:
                    ccd = '1'

                # non-linearity warning levels
                nonlin = cl.get_default("nonlin")
                if nonlin is not None and len(nonlin) != nccd:
                    cl.set_default("nonlin", nccd * (50000.,))
                nonlins = cl.get_value(
                    "nonlin",
                    "levels to indicate non-linear behaviour, 1 per CCD",
                    nccd*(50000.,)
                )

                # saturation warning levels
                sat = cl.get_default("sat")
                if sat is not None and len(sat) != nccd:
                    cl.set_default("sat", nccd * (62000.,))
                sats = cl.get_value(
                    "sat",
                    "levels to indicate saturation, 1 per CCD",
                    nccd*(62000.,)
                )

                warn_levels = ""
                for n, (nonlin,sat) in enumerate(zip(nonlins,sats)):
                    warn_levels += \
                        f"warn = {n+1} {nonlin} {sat}\n"
                maxcpu = nccd

                scale = cl.get_value(
                    "scale", "image scale [arcsec/unbinned pixel]",
                    0.3, 0.001
                )

                readout = cl.get_value(
                    "readout", "readout noise, RMS ADU (float or file name)", "4.5"
                )

                gain = cl.get_value(
                    "gain", "gain, electrons/ADU, (float or file name)", "1.1"
                )

            else:
                raise hcam.HipercamError(
                    "Programming error: instrument unrecognised"
                )


            if ccd not in aper:
                # make sure 'ccd' is in aper, even if our
                # favoured one isn't
                for cnam in aper:
                    ccd = cnam
                    break

    ################################################################
    #
    # all the inputs have now been obtained. Get on with doing stuff


    if template is not None:

        # read the template to define many unprompted values
        # shortcut names define.
        tvals = reduction.Rfile.read(template, False)
        gsec = tvals['general']
        instrument = gsec.get('instrument')
        scale = gsec.get('scale')
        warn_levels = ""
        for value in gsec['warn']:
            warn_levels += f'warn = {value}\n'

        asec = tvals['apertures']
        psfsec = tvals['psf_photom']
        sksec = tvals['sky']
        csec = tvals['calibration']
        lcsec = tvals['lcplot']
        lsec = tvals['light']
        psec = tvals.get('position',None)
        tsec = tvals.get('transmission',None)
        ssec = tvals.get('seeing',None)
        fsec = tvals.get('focal_mask',None)

    else:

        # define default values for unprompted params.
        if seeing == 0.:
            seeing = 1.5
            sworst = 4.0
            binfac = 1

        # same name as read into template
        tvals = {}

        # general section
        gsec = tvals['general'] = {}
        gsec['instrument'] = instrument
        gsec['scale'] = scale
        gsec['ldevice'] = '1/xs'
        gsec['lwidth'] = 0.
        gsec['lheight'] = 0.
        gsec['idevice'] = '2/xs'
        gsec['iwidth'] = 0.
        gsec['iheight'] = 0.
        gsec['toffset'] = 0
        gsec['skipbadt'] = 'yes'
        gsec['ncpu'] = 1
        gsec['ngroup'] = 1

        # aperture section
        asec = tvals['apertures'] = {}
        asec['location'] = 'variable'
        asec['search_smooth_fft'] = 'no'
        asec['fit_method'] = 'moffat'
        asec['fit_beta'] = 5.0
        asec['fit_beta_max'] = 20.0
        asec['fit_ndiv'] = 0
        asec['fit_fwhm_fixed'] = 'no'
        asec['fit_thresh'] = 8.
        asec['fit_height_min_ref'] = 40.
        asec['fit_height_min_nrf'] = 10.
        asec['fit_alpha'] = 0.1

        # sky
        sksec = tvals['sky'] = {}
        sksec['error'] = 'variance'
        sksec['method'] = 'clipped'
        sksec['thresh'] = 3.0

        # calibration
        csec = tvals['calibration'] = {}
        csec['crop'] = 'yes'
        csec['nhalf'] = 3
        csec['rmin'] = -1.
        csec['rmax'] = 2.
        csec['readout'] = readout
        csec['gain'] = gain

        # PSF photom
        psfsec = tvals['psf_photom'] = {}
        psfsec['gfac'] = 3.0
        psfsec['fit_half_width'] = 15.0
        psfsec['positions'] = 'fixed'

        # light curve
        lcsec = tvals['lcplot'] = {}
        lcsec['xrange'] = 0
        lcsec['extend_x'] = 10.0

        # light
        lsec = tvals['light'] = {}
        lsec['linear'] = 'no'
        lsec['y_fixed'] = 'no'
        lsec['y1'] = 0
        lsec['y2'] = 0
        lsec['extend_y'] = 0.1

        # position
        psec = tvals['position'] = {}
        psec['height'] = 0.5
        psec['x_fixed'] = 'no'
        psec['x_min'] = -5.0
        psec['x_max'] = +5.0
        psec['y_fixed'] = 'no'
        psec['y_min'] = -5.0
        psec['y_max'] = +5.0
        psec['extend_y'] = 0.2

        # transmission
        tsec = tvals['transmission'] = {}
        tsec['height'] = 0.5
        tsec['ymax'] = 110.

        # seeing
        ssec = tvals['seeing'] = {}
        ssec['height'] = 0.5
        ssec['ymax'] = 2.999
        ssec['y_fixed'] = 'no'
        ssec['extend_y'] = 0.2

        fsec = tvals['focal_mask'] = {}
        fsec['demask'] = 'no'
        fsec['dthresh'] = 3.0

    # apply seeing/instrument-related fixes...
    # *always* gets run in the no template case
    if seeing > 0.:
        asec['search_half_width'] = max(5., sworst/scale)
        asec['search_smooth_fwhm'] = max(2., seeing/scale/binfac)
        asec['fwhm_min'] = 1.5*binfac
        asec['fwhm'] = max(1.5*binfac, seeing/scale)
        asec['fwhm_max'] = 3.*sworst/scale
        asec['fit_max_shift'] = max(1., sworst/scale/4.)
        asec['fit_half_width'] = max(5., 2*sworst/scale)
        asec['fit_diff'] = max(1., sworst/scale/6.)

        rfac = 1.8
        ramin = 3*binfac
        ramax = max(ramin,rfac*sworst/scale)
        sinner = ramax
        souter = 1.5*sinner

    # standard colours for CCDs
    if instrument.startswith("hipercam"):
        CCD_COLS = {
            "1": "blue",
            "2": "green",
            "3": "orange",
            "4": "red",
            "5": "darkred",
        }

    elif instrument.startswith("ultracam"):
        CCD_COLS = {
            "1": "red",
            "2": "green",
            "3": "blue",
        }

    elif instrument.startswith("ultraspec"):
        CCD_COLS = {
            "1": "green",
        }

    elif instrument == "other":
        # 'other'
        CCD_COLS = {
            "1": "green",
            "2": "blue",
            "3": "red",
            "4": "orange",
            "5": "purple",
        }
    else:
        raise hcam.HipercamError(
            "Programming error: instrument unrecognised"
        )

    # Extraction lines
    extraction = ""
    if template is None:
        # generate one for each CCD
        for cnam in aper:
            extraction += (
                f"{cnam} = variable normal"
                f" {rfac:.2f} {ramin:.1f} {ramax:.1f}"
                f" 2.5 {sinner:.1f} {sinner:.1f}"
                f" 3.0 {souter:.1f} {souter:.1f}\n"
            )
    else:
        # pass through unchanged where possible if
        # a template being used but check for consistency
        esec = tvals['extraction']
        for cnam, lst in esec.items():
            if cnam in aper:
                # unpack items
                ltype,etype,rfac,ramin,ramax,sifac,simin,simax,sofac,somin,somax = lst
                extraction += (
                    f"{cnam} = variable normal"
                    f" {rfac:.2f} {ramin:.1f} {ramax:.1f}"
                    f" {sifac:.2f} {simin:.1f} {simax:.1f}"
                    f" {sofac:.2f} {somin:.1f} {somax:.1f}\n"
                )
            else:
                warnings.warn(
                    f'CCD {cnam} has an extraction line in {template} but no apertures in {apfile} and will be skipped'
                )

        # warn if there are apertures for a CCD but no extraction line
        for cnam in aper:
            if cnam not in esec:
                warnings.warn(
                    f'CCD {cnam} has apertures defined in {apfile} but no extraction line in {template}'
                )

    # Generate the light curve plot lines
    light_plot = ""
    no_light = True

    if template is None:
        # Here we assume target in aperture 1, main comparison in aperture 2
        for cnam in aper:
            ccdaper = aper[cnam]
            if "1" in ccdaper and "2" in ccdaper:
                # favour 2 as the comparison
                light_plot += (
                    f'plot = {cnam} 1 2 0 1 {CCD_COLS.get(cnam,"black")} !  '
                    " # ccd, targ, comp, off, fac, dcol, ecol\n"
                )
                no_light = False

            elif "1" in ccdaper and "3" in ccdaper:
                # but try 3 if need be
                light_plot += (
                    f'plot = {cnam} 1 3 0 1 {CCD_COLS.get(cnam,"black")} !  '
                    " # ccd, targ, comp, off, fac, dcol, ecol\n"
                )
                no_light = False

            elif "1":
                # just plot 1 as last resort
                light_plot += (
                    f'plot = {cnam} 1 ! 0 1 {CCD_COLS.get(cnam,"black")} !  '
                    " # ccd, targ, comp, off, fac, dcol, ecol\n"
                )
                no_light = False

    else:

        # template case: pass as many of the lines as we can but
        # run checks of apertures
        plots = tvals['light']['plot']
        for pl in plots:
            cnam, targ, comp = pl['ccd'], pl['targ'], pl['comp']
            off, fac, dcol, ecol = pl['off'], pl['fac'], ct(pl['dcol']), ct(pl['ecol'])

            if cnam in aper and targ in aper[cnam] and (comp == '!' or comp in aper[cnam]):
                light_plot += (
                    f"plot = {cnam} {targ} {comp} {off} {fac} {dcol} {ecol}"
                    " # ccd, targ, comp, off, fac, dcol, ecol\n"
                )
                no_light = False
            else:
                warnings.warn(
                    "Light curve plot line:\n"
                    f"plot = {cnam} {targ} {comp} {off} {fac} {dcol} {ecol}\n"
                    f"from {template} is incompatible with {apfile} and has been skipped"
                )

    if no_light:
        raise hcam.HipercamError(
            "Found no targets for light curve plots in any CCD; cannot make light "
            "curve plot. Needs at least aperture '1' defined and preferably '2' as well,"
            "for at least one CCD"
        )

    # Generate the position plot lines. Favour the comparisons first, then the target.
    position_plot = ""
    no_position = True
    if template is None:
        ccdaper = aper[ccd]
        for targ in ('2','3','1'):
            if targ in ccdaper:
                position_plot += (
                    f'plot = {ccd} {targ} '
                    f'{CCD_COLS.get(cnam,"black")} ! '
                    '# ccd, targ, dcol, ecol\n'
                )
                no_position = False
                break

        if no_position:
            warnings.warn(
                f"Targets 2, 1, or 3 not found in CCD = {ccd} in {apfile}"
            )

    elif psec is not None:
        plots = psec['plot']
        for pl in plots:
            cnam, targ, dcol, ecol = pl['ccd'], pl['targ'], ct(pl['dcol']), ct(pl['ecol'])
            if cnam in aper and targ in aper[cnam]:
                position_plot += (
                    f'plot = {cnam} {targ} {dcol} {ecol} # ccd, targ, dcol, ecol\n'
                )
                no_position = False
            else:
                warnings.warn(
                    f"Position target {targ} / CCD {cnam} not found in {apfile}; will skip"
                )

    if no_position:
        warnings.warn(f"No position plot will be made")

    # Generate the transmission plot lines, again comparisons first,
    # target only if desperate.
    transmission_plot = ""
    no_transmission = True
    if template is None:
        for cnam in aper:
            ccdaper = aper[cnam]
            for targ in ('2','3','1'):
                if targ in ccdaper:
                    transmission_plot += (
                        f'plot = {cnam} {targ} '
                        f'{CCD_COLS.get(cnam,"black")} ! '
                        f"# ccd, targ, dcol, ecol\n"
                    )
                    no_transmission = False
                    break

        if no_transmission:
            warnings.warn(
                f"Targets 2, 3, or 1 not found in any CCD within {apfile}; no transmission plot"
            )

    elif tsec is not None:
        plots = tsec['plot']
        for pl in plots:
            cnam, targ, dcol, ecol = pl['ccd'], pl['targ'], ct(pl['dcol']), ct(pl['ecol'])
            if cnam in aper and targ in aper[cnam]:
                transmission_plot += (
                    f'plot = {cnam} {targ} {dcol} {ecol} # ccd, targ, dcol, ecol\n'
                )
                no_transmission = False
            else:
                warnings.warn(
                    f"Transmission target {targ} / CCD {cnam} not found in {apfile}; will skip"
                )

    if no_transmission:
        warnings.warn(f"No transmission plot will be made")

    # Generate the seeing plot lines. Prioritise the target in this case.
    seeing_plot = ""
    no_seeing = True
    if template is None:
        for cnam in aper:
            ccdaper = aper[cnam]
            for targ in ('1','2','3'):
                if targ in ccdaper and not ccdaper[targ].linked:
                    seeing_plot += (
                        f"plot = {cnam} {targ}"
                        f' {CCD_COLS.get(cnam,"black")} ! '
                        "# ccd, targ, dcol, ecol\n"
                    )
                    no_seeing = False
                    break

        if no_seeing:
            raise hcam.HipercamError(
                "Targets 1, 2 and 3 not found in any CCD"
                " (or they are linked); cannot make seeing plot"
            )

    elif ssec is not None:
        plots = tsec['plot']
        for pl in plots:
            cnam, targ, dcol, ecol = pl['ccd'], pl['targ'], ct(pl['dcol']), ct(pl['ecol'])
            if cnam in aper and targ in aper[cnam] and not aper[cnam][targ].linked:
                seeing_plot += (
                    f'plot = {cnam} {targ} {dcol} {ecol} # ccd, targ, dcol, ecol\n'
                )
                no_seeing = False
            else:
                warnings.warn(
                    f"Seeing target {targ} / CCD {cnam} not found or linked in {apfile}; will skip"
                )

    if no_seeing:
        warnings.warn(f"No seeing plot will be made")

    # monitor targets (all of them by default)
    if template is None:
        targs = set()
        for cnam in aper:
            ccdaper = aper[cnam]
            for targ in ccdaper:
                targs.add(targ)
        monitor = ""
        for targ in sorted(targs):
            monitor += (
                f"{targ} = NO_EXTRACTION TARGET_SATURATED TARGET_AT_EDGE"
                " TARGET_NONLINEAR NO_SKY NO_FWHM NO_DATA SKY_AT_EDGE\n"
            )
    else:
        monitor = ""
        msec = tvals['monitor']
        rlook = dict([(k,v) for v,k in hcam.FLAGS])
        for targ, masks in msec.items():
            smasks = ' '.join([rlook[mask] for mask in masks])
            monitor += f'{targ} = {smasks}\n'

    # time stamp
    tstamp = strftime("%d %b %Y %H:%M:%S (UTC)", gmtime())

    # Finally, write out the reduce file ...
    with open(rfile, "w") as fout:
        # write out file
        fout.write(
            f"""#
# This is a HiPERCAM "reduce file" which defines the operation of the
# reduce script. It was written by the HiPERCAM pipeline command
# 'genred'.  It consists of a series of sections each of which
# contains a number of parameters. The file is self-documenting on the
# meaning of these parameters. The idea is that these are to large
# extent unchanging and it would be annoying to be prompted every time
# for them, but it also acts as a record of how reduction was carried
# out and is fed into the log file produce by 'reduce'.

# File written on {tstamp}
#
# HiPERCAM pipeline version: {hcam.REDUCE_FILE_VERSION}
#
# Start with some general items that tend not to change
# much. 'version' is the version of the reduce file format which
# changes more slowly than the software does. It must match the same
# parameter in 'reduce' for reduction to proceed. This is
# automatically the case at the time of creation, but old versions of
# the reduce file may become incompatible with later versions of
# reduce. Either they will require updating to be used, or the
# software version can be rolled back to give a compatible version of
# reduce using 'git'. The script 'rupdate', which attempts automatic
# update, may be worth trying if you need to update. It attempts to
# make the minimum changes needed to an old reduce file to run with
# later version dates.

[general]
version = {hcam.REDUCE_FILE_VERSION} # must be compatible with the version in reduce

instrument = {gsec['instrument']} # instrument
scale = {gsec['scale']} # pixel scale, arcsec/pixel

ldevice = {gsec['ldevice']} # PGPLOT plot device for light curve plots
lwidth = {gsec['lwidth']} # light curve plot width, inches, 0 to let program choose
lheight = {gsec['lheight']} # light curve plot height, inches

idevice = {gsec['idevice']} # PGPLOT plot device for image plots [if implot True]
iwidth = {gsec['iwidth']} # image curve plot width, inches, 0 to let program choose
iheight = {gsec['iheight']} # image curve plot height, inches

toffset = {gsec['toffset']} # integer offset to subtract from the MJD

# skip points with bad times in plots. HiPERCAM has a problem in not
# correctly indicating bad times so one does not usually want to
# skip "bad time" points, whereas one should for ULTRACAM and ULTRASPEC.
skipbadt = {gsec['skipbadt']}

# series of count levels at which warnings will be triggered for (a)
# non linearity and (b) saturation. Each line starts 'warn =', and is
# then followed by the CCD label, the non-linearity level and the
# saturation level

{warn_levels}

# The aperture reposition and extraction stages can be run in separate
# CPUs in parallel for each CCD potentially offering speed
# advantages. 'ncpu' is the number of CPUs to use for this. The
# maximum useful and best number to use is the number of CCDs in the
# instrument, e.g. 5 for HiPERCAM. You probably also want to leave at
# least one CPU to do other stuff, but if you have more than 2 CPUs,
# this parameter may help speed things. If you use this option (ncpu >
# 1), then there is also an advantage in terms of reducing
# parallelisation overheads in reading frames a few at a time before
# processing. This is controlled using 'ngroup'. i.e. with ngroup=10,
# 10 full frames are read before being processed. This parameter is
# ignored if ncpu==1

# I have had mixed results with this, which could depend a lot on the
# installation. On my home desktop is works better with ncpu=1 because
# it already seems to parallelise and it only gets worse if I split things
# up. But no harm trying.

ncpu = {gsec['ncpu']}
ngroup = {gsec['ngroup']}

# The next section '[apertures]' defines how the apertures are
# re-positioned from frame to frame. Apertures are re-positioned
# through a combination of a search near a start location followed by
# a 2D profile fit. Several parameters below are associated with this
# process and setting these right can be the key to a successful
# reduction.  If there are reference apertures, they are located first
# to give a mean shift. This is used to avoid the initial search for
# any non-reference apertures which has the advantage of reducing the
# chance of problems. The search is carried out by first extracting a
# sub-window centred on the last good position of a target. This is
# then smoothed by a gaussian (width 'search_smooth_fwhm'), and the
# closest peak to the last valid position higher than
# 'fit_height_min_ref' above background (median of the square box) is
# taken as the initial position for later profile fits. The smoothing
# serves to make the process more robust against cosmic rays. The
# width of the search box ('search_half_width') depends on how good
# the telescope guiding is. It should be large enough to cope with the
# largest likely shift in position between any two consecutive
# frames. Well-chosen reference targets, which should be isolated and
# bright, can help this process a great deal. The threshold is applied
# to the *smoothed* image. This means that it can be significantly
# lower than simply the raw peak height. e.g. a target might have a
# typical peak height around 100, in seeing of 4 pixels FWHM. If you
# smooth by 10 pixels, the peak height will drop to
# 100*4**2/(4**2+10**2) = 14 counts. It will be much more stable as a
# result, but you should then probably choose a threshold of 7 when
# you might have thought 50 was appropriate. The smoothing itself can
# be carried out by direct convolution or by an FFT-based method. The
# end-result is the same either way but for large values of
# 'search_smooth_fwhm', i.e. >> 1, FFTs may offer an advantage
# speed-wise. But the only way to tell is by explicity running with
# 'search_smooth_fft' switched from 'no' to 'yes'.

# The boxes for the fits ('fit_half_width') need to be large enough to
# include the target and a bit of sky to ensure that the FWHM is
# accurately measured, remembering that seeing can flare of course. If
# your target was defocussed, a gaussian or Moffat function will be a
# poor fit and you may be better keeping the FWHM fixed at a large
# value comparable to the widths of your defoccused images (and use
# the gaussian option in such cases). If the apertures are chosen to
# be fixed, there will be no search or fit carried out in which case
# you must choose 'fixed' as well when it comes the extraction since
# otherwise it needs a FWHM. 'fixed' is a last resort and you will
# very likely need to use large aperture radii in the extraction
# section.

# An obscure parameter is 'fit_ndiv'. If this is made > 0, the fit
# routine attempts to allow for pixellation by evaluating the profile
# at multiple points within each pixel of the fit. First it will
# evaluate the profile for every unbinned pixel within a binned pixel
# if the pixels are binned; second, it will evaluate the profile over
# an ndiv by ndiv square grid within each unbinned pixel. Thus ndiv=1
# means one evaluation at the centre of each unbinned pixel,
# etc. Obviously this will slow things, but it could help if your
# images are under-sampled. I would always start with fit_ndiv=0, and
# only raise it if the measured FWHM seem to be close to or below two
# binned pixels.

# If you use reference targets (you should if possible), the initial
# positions for the non-reference targets should be good. You can then
# guard further against problems using the parameter 'fit_max_shift'
# to reject positions for the non-reference targets that shift too far
# from the initial guess. 'fit_alpha' is another parameter that
# applies only in this case. If reference apertures are being used,
# the expected locations of non-reference apertures can be predicted
# with some confidence. In this case when the non-reference aperture's
# position is measured, its position will be adjusted by 'fit_alpha'
# times the measured change in its position. Its value is bounded by 0
# < fit_alpha <= 1. "1" just means use the full measured change from
# the current frame to update the position. Anything < 1 builds in a
# bit of past history. The hope is that this could make the aperture
# positioning, especially for faint targets, more robust to cosmic
# rays and other issues.  Of course it will correlate the positions
# from frame to frame. fit_alpha = 0.1 for instance will lead to a
# correlation length ~ 10 frames.

# If you use > 1 reference targets, then the parameter 'fit_diff'
# comes into play.  Multiple reference targets should move together
# and give very consistent shifts. If they don't, then a problem may
# have occurred, e.g. one or more have been affected by a meteor trail
# for instance. The maximum acceptable differential shift is defined
# by 'fit_diff'. If exceeded, then the entire extraction will be
# aborted and positions held fixed.

# To get and idea of the right values of some of these parameters, in
# particular the 'search_half_width', the height thresholds,
# 'fit_max_shift' and 'fit_diff', the easiest approach is probably to
# run a reduction with loose values and see how it goes.

[apertures]
aperfile = {apfile} # file of software apertures for each CCD
location = {asec['location']} # aperture locations: 'fixed' or 'variable'

search_half_width = {asec['search_half_width']:.1f} # for initial search for objects around previous position, unbinned pixels
search_smooth_fwhm = {asec['search_smooth_fwhm']:.1f} # smoothing FWHM, binned pixels
search_smooth_fft = {asec['search_smooth_fft']} # use FFTs for smoothing, 'yes' or 'no'.

fit_method = {asec['fit_method']} # gaussian or moffat
fit_beta = {asec['fit_beta']:.1f} # Moffat exponent
fit_beta_max = {asec['fit_beta_max']:.1f} # max Moffat expt
fit_fwhm = {asec['fwhm']:.1f} # FWHM, unbinned pixels
fit_fwhm_min = {asec['fwhm_min']:.1f} # Minimum FWHM, unbinned pixels [MUST be < fit_fwhm!]
fit_fwhm_max = {asec['fwhm_max']:.1f} # Maximum FWHM, unbinned pixels
fit_ndiv = {asec['fit_ndiv']} # sub-pixellation factor
fit_fwhm_fixed = {asec['fit_fwhm_fixed']} # Might want to set = 'yes' for defocussed images
fit_half_width = {asec['fit_half_width']:.1f} # for fit, unbinned pixels
fit_thresh = {asec['fit_thresh']:.2f} # RMS rejection threshold for fits
fit_height_min_ref = {asec['fit_height_min_ref']:.1f} # minimum height to accept a fit, reference aperture
fit_height_min_nrf = {asec['fit_height_min_nrf']:.1f} # minimum height to accept a fit, non-reference aperture
fit_max_shift = {asec['fit_max_shift']:.1f} # max. non-ref. shift, unbinned pixels.
fit_alpha = {asec['fit_alpha']:.2f} # Fraction of non-reference aperture shift to apply
fit_diff = {asec['fit_diff']:.2f} # Maximum differential shift of multiple reference apertures, unbinned

# The next lines define how the apertures will be re-sized and how the
# flux will be extracted from the aperture. There is one line per CCD
# with format:

# <CCD label> = <resize> <extract method> [scale min max] [scale min max]
#               [scale min max]

# where: <CCD label> is the CCD label; <resize> is either 'variable'
# or 'fixed' and sets how the aperture size will be determined -- if
# variable it will be scaled relative to the FWHM, so profile fitting
# will be attempted; <extract method> is either 'normal' or 'optimal'
# to say how the flux will be extracted -- 'normal' means a straight
# sum of sky subtracted flux over the aperture, 'optimal' use Tim
# Naylor's profile weighting, and requires profile fits to
# work. Finally there follow a series of numbers in three triplets,
# each of which is a scale factor relative to the FWHM for the
# aperture radius if the 'variable' option was chosen, then a minimum
# and a maximum aperture radius in unbinned pixels.  The three triples
# correspond to the target aperture radius, the inner sky radius and
# finally the outer sky radius. The mininum and maximum also apply if
# you choose 'fixed' apertures and can be used to override whatever
# value comes from the aperture file. A common approach is set them
# equal to each other to give a fixed value, especially for the sky
# where one does not necessarily want the radii to vary.  For PSF
# photometry, all these settings have no effect, but this section can
# still be used to determine which CCDs have fluxes extracted.

[extraction]
{extraction}

# The next lines are specific to the PSF photometry option. 'gfac' is
# used to label the sources according to groups, such that stars
# closer than 'gfac' times the FWHM are labelled in the same
# group. Each group has a PSF model fit independently. The reason
# behind the construction of groups is to reduce the dimensionality of
# the fitting procedure. Usually you want closely seperated stars to
# be fit simultaneously, but too large a value will mean fitting a
# model with many free parameters, which can fail to converge. The
# size of the box over which data is collected for fitting is set by
# 'fit_half_width'. Finally, 'positions' determines whether the star's
# positions should be considered variable in the PSF fitting. If this
# is set to fixed, the positions are held at the locations found in
# the aperture repositioning step, otherwise the positions are refined
# during PSF fitting. This step can fail for PSF photometry of faint
# sources.

[psf_photom]
gfac = {psfsec['gfac']}  # multiple of the FWHM to use in grouping objects
fit_half_width = {psfsec['fit_half_width']}  # size of window used to collect the data to do the fitting
positions = {psfsec['positions']}   # 'fixed' or 'variable'

# Next lines determine how the sky background level is
# calculated. Note you can only set error = variance if method =
# 'clipped'. 'median' should usually be avoided as it can cause
# noticable steps in light curves. It's here as a comparator.

[sky]
method = {sksec['method']} # 'clipped' | 'median'
error  = {sksec['error']} # 'variance' | 'photon': first uses actual variance of sky
thresh = {sksec['thresh']:.1f} # threshold in terms of RMS for 'clipped'

# Calibration frames and constants

# If you specify "!" for the readout, an attempt to estimate it from
# +/- 1 sigma percentiles will be made. This could help if you have no
# bias (and hence variance calculation from the count level will be
# wrong)

# The fringe parameters are the most complex. To de-fringe you need
# first a fringe map, e.g. as returned by makefringe. Then you need a
# file of FringePairs as returned by 'setfringe'. These are pairs of
# points placed at peaks and troughs of fringes. They will be used to
# measure a series of differences in each frame of interest and the
# fringe map from which a series of ratios is formed. The differences
# are measured from a group of pixels extending +/-nhlaf around the
# central pixel of each point of a FringePair. Thus nhalf=2 would give
# a 5x5 array (unbinned pixels). Finally to reduce the effect of bad
# values the individual ratios can be pruned if they lie outside a
# range rmin to rmax prior to their overall median being calculated.
# rmin should be small, but probably slightly less than zero to allow
# for a bit of statistical fluctuation, depending on your setup. One
# would normally expect rmax < 1 if the fringe map came from frames with
# a long exposure compared to the data.

[calibration]
crop = {csec['crop']} # Crop calibrations to match the data
bias = {'' if bias is None else bias} # Bias frame, blank to ignore
flat = {'' if flat is None else flat} # Flat field frame, blank to ignore
dark = {'' if dark is None else dark} # Dark frame, blank to ignore
fmap = {'' if fmap is None else fmap} # Fringe map frame, blank to ignore
fpair = {'' if fpair is None else fpair} # File of fringe pairs, ignored if fmap blank

nhalf = {csec['nhalf']} # Half-size of region used for fringe ratios, binned pix, ignored if fmap blank=""
rmin = {csec['rmin']} # Mininum acceptable individual fmap ratio, ignored if fmap blank
rmax = {csec['rmax']} # Maximum acceptable individual fmap ratio, ignored if fmap blank
readout = {csec['readout']} # RMS ADU. Float or string name of a file or "!" to estimate on the fly
gain = {csec['gain']} # Gain, electrons/ADU. Float or string name of a file

# The light curve plot which consists of light curves, X & Y
# poistions, the transmission and seeing. All but the light curves can
# be switched off by commenting them out (in full). First a couple of
# general parameters.

[lcplot]
xrange  = {lcsec['xrange']} # maximum range in X to plot (minutes), <= 0 for everything
extend_x = {lcsec['extend_x']} # amount by which to extend xrange, minutes.

# The light curve panel (must be present). Mostly obvious, then a
# series of lines, each starting 'plot' which specify one light curve
# to be plotted giving CCD, target, comparison ('!' if you don't want
# a comparison), an additive offset, a multiplicative scaling factor
# and then a colour for the data and a colour for the error bar There
# will always be a light curve plot, whereas later elements are
# optional, therefore the light curve panel is defined to have unit
# height and all others are scaled relative to this.

[light]
linear  = {lsec['linear']} # linear vertical scale (else magnitudes): 'yes' or 'no'
y_fixed = {lsec['y_fixed']} # keep a fixed vertical range or not: 'yes' or 'no'
y1 = {lsec['y1']} # initial lower y value
y2 = {lsec['y2']} # initial upper y value. y1=y2 for auto scaling
extend_y = {lsec['extend_y']} # fraction of plot height to extend when rescaling

# line or lines defining the targets to plot
{light_plot}
"""
        )

        # position plot (optional)
        if no_position:
            fout.write(
                f"""# The X,Y position panel. Uncomment to show

#[position]
#height = 0.5 # height relative to light curve plot
#x_fixed = no # keep X-position vertical range fixed
#x_min = -5 # lower limit for X-position
#x_max = +5 # upper limit for X-position
#y_fixed = no # keep Y-position vertical range fixed
#y_min = -5 # lower limit for Y-position
#y_max = +5 # upper limit for Y-position
#extend_y = 0.2 # Vertical extension fraction if limits exceeded

## line or lines defining the targets to plot
# plot = 1 1 darkred ! # ccd, targ, dcol, ecol
"""
            )

        else:
            fout.write(
                f"""# The X,Y position panel. Comment *every* line
# if you don't want it.

[position]
height = {psec['height']} # height relative to light curve plot
x_fixed = {psec['x_fixed']} # keep X-position vertical range fixed
x_min = {psec['x_min']} # lower limit for X-position
x_max = {psec['x_max']} # upper limit for X-position
y_fixed = {psec['y_fixed']} # keep Y-position vertical range fixed
y_min = {psec['y_min']} # lower limit for Y-position
y_max = {psec['y_max']} # upper limit for Y-position
extend_y = {psec['extend_y']} # Vertical extension fraction if limits exceeded

# line or lines defining the targets to plot
{position_plot}
"""
            )

        # Transmission plot, optional
        if no_transmission:
            fout.write(
                f"""# The transmission panel. Uncomment if you want it
# Simply plots the flux in whatever apertures are chosen,
# scaling them by their maximum (hence one can sometimes
# find that what you thought was 100% transmission was
# actually only 50% revealed as the cloud clears).

#[transmission]
#height = 0.5 # height relative to the light curve plot
#ymax = 110 # Maximum transmission to plot (>= 100 to slow replotting)

## line or lines defining the targets to plot
# plot = 1 2 green ! # ccd, targ, dcol, ecol
"""
            )
        else:
            fout.write(
                f"""# The transmission panel. If you comment it out,
# comment *every* line.
# Simply plots the flux in whatever apertures are chosen,
# scaling them by their maximum (hence one can sometimes
# find that what you thought was 100% transmission was
# actually only 50% revealed as the cloud clears).

[transmission]
height = {tsec['height']} # height relative to the light curve plot
ymax = {tsec['ymax']} # Maximum transmission to plot (>= 100 to slow replotting)

# line or lines defining the targets to plot
{transmission_plot}
"""
            )

        if no_seeing:
            fout.write(
                f"""# The seeing panel. Uncomment if you want it.
# You can have multiple plot lines. Don't choose linked
# targets as their FWHMs are not measured.

#[seeing]
#height = 0.5 # height relative to the light curve plot
#ymax = 2.999 # Initial maximum seeing
#y_fixed = no # fix the seeing scale (or not)
#extend_y = 0.2 # Y extension fraction if out of range and not fixed

## line or lines defining the targets to plot
# plot = 1 2 green ! # ccd, targ, dcol, ecol
"""
            )

        else:
            fout.write(
                f"""# The seeing panel. Can be commented out if you don't want one but make
# sure to comment it out completely, section name and all
# parameters. You can have multiple plot lines. Don't choose linked
# targets as their FWHMs are not measured.

[seeing]
height = {ssec['height']} # height relative to the light curve plot
ymax = {ssec['ymax']} # Initial maximum seeing
y_fixed = {ssec['y_fixed']} # fix the seeing scale (or not)
extend_y = {ssec['extend_y']} # Y extension fraction if out of range and not fixed

# line or lines defining the targets to plot
{seeing_plot}
"""
            )

        # Finish up
        fout.write(
            f"""
# This option attempts to correct for a badly-positioned focal plane mask
# in drift mode which combined with a high background can lead to steps in
# illumination in the Y direction. This tries to subtract the median in the
# X-direction of each window. 'dthresh' is a threshold used to reject X
# pixels prior to taking the median. The purpose is to prevent the stars
# from distorting the median. Take care with this option which is experimental.

[focal_mask]
demask = {fsec['demask']}
dthresh = {fsec['dthresh']}

# Monitor section. This section allows you to monitor particular
# targets for problems. If they occur, then messages will be printed
# to the terminal during reduce. The messages are determined by the
# bitmask flag set during the extraction of each
# target. Possibilities:

#  NO_FWHM           : no FWHM measured
#  NO_SKY            : no sky pixels at all
#  SKY_AT_EDGE       : sky aperture off edge of window
#  TARGET_AT_EDGE    : target aperture off edge of window
#  TARGET_SATURATED  : at least one pixel in target above saturation level
#  TARGET_NONLINEAR  : at least one pixel in target above nonlinear level
#  NO_EXTRACTION     : no extraction possible
#  NO_DATA           : no valid pixels in aperture

# For a target you want to monitor, type its label, '=', then the
# bitmask patterns you want to be flagged up if they are set. This is
# designed mainly for observing, as there is less you can do once the
# data have been taken, but it still may prove useful.

[monitor]
{monitor}
"""
        )

    print("Reduce file written to {:s}".format(rfile))
Example #19
0
def tbytes(args=None):
    """``tbytes [source] ([old]) run``

    Reads all timing bytes from a |hiper|, ULTRACAM or ULTRASPEC run
    and dumps them to a disk file. Designed as a safety fallback for
    correcting timing issues where one wants to manipulate the raw
    data.

    Parameters:

        source : string [hidden]
           Data source, two options:

              | 'hl' : local HiPERCAM FITS file
              | 'ul' : ULTRA(CAM|SPEC) server

        old : bool [if source=='ul', hidden, defaults to False]
           Will attempt first to read timing bytes from an old version
           of the data file, i.e. one with extension '.dat.old' rather
           than plain '.dat' (applies to ULTRA(CAM|SPEC) data only. Such
           files are generated by the null timestamp correction script
           routinely applied to ULTRASPEC data for example. 'old' will
           switch to False by default if not explicitly set.

        run : str
           run number to access, e.g. 'run0034'. This will also be
           used to generate the name for the timing bytes file
           (extension '.tbts'). If a file of this name already exists,
           the script will attempt to read and compare the bytes of
           the two files and report any changes.  The timing bytes
           file will be written to the present working directory,
           unless it contains a sub-directory named "tbytes", in which
           case the timing data will be read/written from/to there.

    """

    command, args = utils.script_args(args)

    # get the inputs
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("source", Cline.LOCAL, Cline.HIDE)
        cl.register("old", Cline.LOCAL, Cline.HIDE)
        cl.register("run", Cline.GLOBAL, Cline.PROMPT)

        # get inputs
        source = cl.get_value(
            "source", "data source [hl, ul]", "hl", lvals=("hl", "ul")
        )

        if source == "ul":
            cl.set_default("old", False)
            old = cl.get_value("old", "try to access .dat.old files first?", False)

        run = cl.get_value("run", "run name", "run005")
        if run.endswith(".fits"):
            run = run[:-5]

    # create name of timing bytes file
    if os.path.isdir('tbytes'):
        ofile = os.path.join('tbytes', os.path.basename(run) + hcam.TBTS)
    else:
        ofile = os.path.basename(run) + hcam.TBTS

    if source == "hl":

        nframe = 0
        if os.path.exists(ofile):

            # interpret timing bytes. In this case the file of timing
            # bytes already exists. We read it and the run and compare
            # the times, reporting any differences.
            ndiffer = 0
            with spooler.HcamTbytesSpool(run) as rtbytes:
                with open(ofile, "rb") as fin:
                    for tbytes in rtbytes:
                        otbytes = fin.read(rtbytes.ntbytes)
                        if len(otbytes) != rtbytes.ntbytes:
                            raise hcam.HipercamError(
                                "Failed to read", rtbytes.ntbytes, "bytes from", ofile
                            )
                        nframe += 1

                        if tbytes != otbytes:
                            # now need to interpret times
                            nmjd = h_tbytes_to_mjd(tbytes, nframe)
                            omjd = h_tbytes_to_mjd(otbytes, nframe)
                            print(
                                "Frame {:d}, new vs old GPS timestamp (MJD) = {:.12f} vs {:.12f}".format(
                                    nframe, nmjd, omjd
                                )
                            )
                            ndiffer += 1

            print(
                "{:s} vs {:s}: there were {:d} time stamp differences in {:d} frames".format(
                    run, ofile, ndiffer, nframe
                )
            )

        else:

            # save timing bytes to disk
            with spooler.HcamTbytesSpool(run) as rtbytes:
                with open(ofile, "wb") as fout:
                    for tbytes in rtbytes:
                        fout.write(tbytes)
                        nframe += 1

            print("Found", nframe, "frames in", run, "\nWrote timing data to", ofile)

    elif source == "ul":

        nframe = 0
        if os.path.exists(ofile):

            # interpret timing bytes. In this case the file of timing
            # bytes already exists. We read it and the run and compare
            # the times, reporting any differences.
            ndiffer = 0
            with spooler.UcamTbytesSpool(run, old=old) as rtbytes:
                with open(ofile, "rb") as fin:
                    for tbytes in rtbytes:
                        otbytes = fin.read(rtbytes.ntbytes)
                        if len(otbytes) != rtbytes.ntbytes:
                            raise hcam.HipercamError(
                                "Failed to read", rtbytes.ntbytes, "bytes from", ofile
                            )
                        nframe += 1

                        if tbytes != otbytes:
                            # now need to interpret times
                            nmjd = u_tbytes_to_mjd(tbytes, rtbytes, nframe)
                            omjd = u_tbytes_to_mjd(otbytes, rtbytes, nframe)
                            print(
                                "Frame {:d}, new vs old GPS timestamp (MJD) = {:.12f} vs {:.12f}".format(
                                    nframe, nmjd, omjd
                                )
                            )
                            ndiffer += 1

            print(
                "{:s} vs {:s}: there were {:d} time stamp differences in {:d} frames".format(
                    run, ofile, ndiffer, nframe
                )
            )

        else:
            # no timing bytes files exists; save to disk
            with spooler.UcamTbytesSpool(run, old=old) as rtbytes:
                with open(ofile, "wb") as fout:
                    for tbytes in rtbytes:
                        fout.write(tbytes)
                        nframe += 1

            print("Found", nframe, "frames in", run, "\nWrote timing data to", ofile)
Example #20
0
def rtplot(args=None):
    """``rtplot [source device width height] (run first (trim [ncol nrow])
    twait tmax | flist) [pause plotall] ccd (nx) bias flat defect
    setup [hurl] msub iset (ilo ihi | plo phi) xlo xhi ylo yhi (profit
    [fdevice fwidth fheight method beta fwhm fwhm_min shbox smooth
    splot fhbox hmin read gain thresh])``

    Plots a sequence of images as a movie in near 'real time', hence
    'rt'. Designed to be used to look at images coming in while at the
    telescope, 'rtplot' comes with many options, a large number of which are
    hidden by default, and many of which are only prompted if other arguments
    are set correctly. If you want to see them all, invoke as 'rtplot prompt'.
    This is worth doing once to know rtplot's capabilities.

    rtplot can source data from both the ULTRACAM and HiPERCAM servers, from
    local 'raw' ULTRACAM and HiPERCAM files (i.e. .xml + .dat for ULTRACAM, 3D
    FITS files for HiPERCAM) and from lists of HiPERCAM '.hcm' files.

    rtplot optionally allows the selection of targets to be fitted with
    gaussian or moffat profiles, and, if successful, will plot circles of 2x
    the measured FWHM in green over the selected targets. This option only
    works if a single CCD is being plotted.

    Parameters:

        source : string [hidden]
           Data source, five options:

             |  'hs' : HiPERCAM server
             |  'hl' : local HiPERCAM FITS file
             |  'us' : ULTRACAM server
             |  'ul' : local ULTRACAM .xml/.dat files
             |  'hf' : list of HiPERCAM hcm FITS-format files

           'hf' is used to look at sets of frames generated by 'grab' or
           converted from foreign data formats.

        device : string [hidden]
          Plot device. PGPLOT is used so this should be a PGPLOT-style name,
          e.g. '/xs', '1/xs' etc. At the moment only ones ending /xs are
          supported.

        width : float [hidden]
           plot width (inches). Set = 0 to let the program choose.

        height : float [hidden]
           plot height (inches). Set = 0 to let the program choose. BOTH width
           AND height must be non-zero to have any effect

        run : string [if source ends 's' or 'l']
           run number to access, e.g. 'run034'

        flist : string [if source ends 'f']
           name of file list

        first : int [if source ends 's' or 'l']
           exposure number to start from. 1 = first frame; set = 0 to always
           try to get the most recent frame (if it has changed).  For data
           from the |hiper| server, a negative number tries to get a frame not
           quite at the end.  i.e. -10 will try to get 10 from the last
           frame. This is mainly to sidestep a difficult bug with the
           acquisition system.

        trim : bool [if source starts with 'u']
           True to trim columns and/or rows off the edges of windows nearest
           the readout. This is particularly for ULTRACAM windowed data where
           the first few rows and columns can contain bad data.

        ncol : int [if trim, hidden]
           Number of columns to remove (on left of left-hand window, and right
           of right-hand windows)

        nrow : int [if trim, hidden]
           Number of rows to remove (bottom of windows)

        twait : float [if source ends 's' or 'l'; hidden]
           time to wait between attempts to find a new exposure, seconds.

        tmax : float [if source ends 's' or 'l'; hidden]
           maximum time to wait between attempts to find a new exposure,
           seconds.

        pause : float [hidden]
           seconds to pause between frames (defaults to 0)

        plotall : bool [hidden]
           plot all frames regardless of status (i.e. including blank frames
           when nskips are enabled (defaults to False). The profile fitting
           will still be disabled for bad frames.

        ccd : string
           CCD(s) to plot, '0' for all, '1 3' to plot '1' and '3' only, etc.

        nx : int [if more than 1 CCD]
           number of panels across to display.

        bias : string
           Name of bias frame to subtract, 'none' to ignore.

        flat : string
           Name of flat field to divide by, 'none' to ignore. Should normally
           only be used in conjunction with a bias, although it does allow you
           to specify a flat even if you haven't specified a bias.

        defect : string
           Name of defect file, 'none' to ignore.

        setup : bool
           True/yes to access the current windows from hdriver. Useful
           during observing when seeting up windows, but not normally
           otherwise. Next argument (hidden) is the URL to get to
           hdriver. Once setup, you should probably turn this off to
           avoid overloading hdriver, especially if in drift mode as
           it makes a request for the windows for every frame.

        hurl : string [if setup; hidden]
           URL needed to access window setting from hdriver. The internal
           server in hdriver must be switched on using rtplot_server_on
           in the hdriver config file.

        msub : bool
           subtract the median from each window before scaling for the
           image display or not. This happens after any bias subtraction.

        iset : string [single character]
           determines how the intensities are determined. There are three
           options: 'a' for automatic simply scales from the minimum to the
           maximum value found on a per CCD basis. 'd' for direct just takes
           two numbers from the user. 'p' for percentile dtermines levels
           based upon percentiles determined from the entire CCD on a per CCD
           basis.

        ilo : float [if iset='d']
           lower intensity level

        ihi : float [if iset='d']
           upper intensity level

        plo : float [if iset='p']
           lower percentile level

        phi : float [if iset='p']
           upper percentile level

        xlo : float
           left-hand X-limit for plot

        xhi : float
           right-hand X-limit for plot (can actually be < xlo)

        ylo : float
           lower Y-limit for plot

        yhi : float
           upper Y-limit for plot (can be < ylo)

        profit : bool [if plotting a single CCD only]
           carry out profile fits or not. If you say yes, then on the first
           plot, you will have the option to pick objects with a cursor. The
           program will then attempt to track these from frame to frame, and
           fit their profile. You may need to adjust 'first' to see anything.
           The parameters used for profile fits are hidden and you may want to
           invoke the command with 'prompt' the first time you try profile
           fitting.

        fdevice : string [if profit; hidden]
           plot device for profile fits, PGPLOT-style name.
           e.g. '/xs', '2/xs' etc.

        fwidth : float [if profit; hidden]
           fit plot width (inches). Set = 0 to let the program choose.

        fheight : float [if profit; hidden]
           fit plot height (inches). Set = 0 to let the program choose.
           BOTH fwidth AND fheight must be non-zero to have any effect

        method : string [if profit; hidden]
           this defines the profile fitting method, either a gaussian or a
           moffat profile. The latter is usually best.

        beta : float [if profit and method == 'm'; hidden]
           default Moffat exponent

        fwhm : float [if profit; hidden]
           default FWHM, unbinned pixels.

        fwhm_min : float [if profit; hidden]
           minimum FWHM to allow, unbinned pixels.

        shbox : float [if profit; hidden]
           half width of box for searching for a star, unbinned pixels. The
           brightest target in a region +/- shbox around an intial position
           will be found. 'shbox' should be large enough to allow for likely
           changes in position from frame to frame, but try to keep it as
           small as you can to avoid jumping to different targets and to
           reduce the chances of interference by cosmic rays.

        smooth : float [if profit; hidden]
           FWHM for gaussian smoothing, binned pixels. The initial position
           for fitting is determined by finding the maximum flux in a smoothed
           version of the image in a box of width +/- shbox around the starter
           position. Typically should be comparable to the stellar width. Its
           main purpose is to combat cosmi rays which tend only to occupy a
           single pixel.

        splot : bool [if profit; hidden]
           Controls whether an outline of the search box and a target number
           is plotted (in red) or not.

        fhbox : float [if profit; hidden]
           half width of box for profile fit, unbinned pixels. The fit box is
           centred on the position located by the initial search. It should
           normally be > ~2x the expected FWHM.

        hmin : float [if profit; hidden]
           height threshold to accept a fit. If the height is below this
           value, the position will not be updated. This is to help in cloudy
           conditions.

        read : float [if profit; hidden]
           readout noise, RMS ADU, for assigning uncertainties

        gain : float [if profit; hidden]
           gain, ADU/count, for assigning uncertainties

        thresh : float [if profit; hidden]
           sigma rejection threshold for fits

    """

    command, args = utils.script_args(args)

    # get the inputs
    with Cline('HIPERCAM_ENV', '.hipercam', command, args) as cl:

        # register parameters
        cl.register('source', Cline.GLOBAL, Cline.HIDE)
        cl.register('device', Cline.LOCAL, Cline.HIDE)
        cl.register('width', Cline.LOCAL, Cline.HIDE)
        cl.register('height', Cline.LOCAL, Cline.HIDE)
        cl.register('run', Cline.GLOBAL, Cline.PROMPT)
        cl.register('first', Cline.LOCAL, Cline.PROMPT)
        cl.register('trim', Cline.GLOBAL, Cline.PROMPT)
        cl.register('ncol', Cline.GLOBAL, Cline.HIDE)
        cl.register('nrow', Cline.GLOBAL, Cline.HIDE)
        cl.register('twait', Cline.LOCAL, Cline.HIDE)
        cl.register('tmax', Cline.LOCAL, Cline.HIDE)
        cl.register('flist', Cline.LOCAL, Cline.PROMPT)
        cl.register('ccd', Cline.LOCAL, Cline.PROMPT)
        cl.register('pause', Cline.LOCAL, Cline.HIDE)
        cl.register('plotall', Cline.LOCAL, Cline.HIDE)
        cl.register('nx', Cline.LOCAL, Cline.PROMPT)
        cl.register('bias', Cline.GLOBAL, Cline.PROMPT)
        cl.register('flat', Cline.GLOBAL, Cline.PROMPT)
        cl.register('defect', Cline.GLOBAL, Cline.PROMPT)
        cl.register('setup', Cline.GLOBAL, Cline.PROMPT)
        cl.register('hurl', Cline.GLOBAL, Cline.HIDE)
        cl.register('msub', Cline.GLOBAL, Cline.PROMPT)
        cl.register('iset', Cline.GLOBAL, Cline.PROMPT)
        cl.register('ilo', Cline.GLOBAL, Cline.PROMPT)
        cl.register('ihi', Cline.GLOBAL, Cline.PROMPT)
        cl.register('plo', Cline.GLOBAL, Cline.PROMPT)
        cl.register('phi', Cline.LOCAL, Cline.PROMPT)
        cl.register('xlo', Cline.GLOBAL, Cline.PROMPT)
        cl.register('xhi', Cline.GLOBAL, Cline.PROMPT)
        cl.register('ylo', Cline.GLOBAL, Cline.PROMPT)
        cl.register('yhi', Cline.GLOBAL, Cline.PROMPT)
        cl.register('profit', Cline.LOCAL, Cline.PROMPT)
        cl.register('fdevice', Cline.LOCAL, Cline.HIDE)
        cl.register('fwidth', Cline.LOCAL, Cline.HIDE)
        cl.register('fheight', Cline.LOCAL, Cline.HIDE)
        cl.register('method', Cline.LOCAL, Cline.HIDE)
        cl.register('beta', Cline.LOCAL, Cline.HIDE)
        cl.register('fwhm', Cline.LOCAL, Cline.HIDE)
        cl.register('fwhm_min', Cline.LOCAL, Cline.HIDE)
        cl.register('shbox', Cline.LOCAL, Cline.HIDE)
        cl.register('smooth', Cline.LOCAL, Cline.HIDE)
        cl.register('splot', Cline.LOCAL, Cline.HIDE)
        cl.register('fhbox', Cline.LOCAL, Cline.HIDE)
        cl.register('hmin', Cline.LOCAL, Cline.HIDE)
        cl.register('read', Cline.LOCAL, Cline.HIDE)
        cl.register('gain', Cline.LOCAL, Cline.HIDE)
        cl.register('thresh', Cline.LOCAL, Cline.HIDE)

        # get inputs
        source = cl.get_value('source', 'data source [hs, hl, us, ul, hf]',
                              'hl', lvals=('hs','hl','us','ul','hf'))

        # set some flags
        server_or_local = source.endswith('s') or source.endswith('l')

        # plot device stuff
        device = cl.get_value('device', 'plot device', '1/xs')
        width = cl.get_value('width', 'plot width (inches)', 0.)
        height = cl.get_value('height', 'plot height (inches)', 0.)

        if server_or_local:
            resource = cl.get_value('run', 'run name', 'run005')
            if source == 'hs':
                first = cl.get_value('first', 'first frame to plot', 1)
            else:
                first = cl.get_value('first', 'first frame to plot', 1, 0)

            if source.startswith('u'):
                trim = cl.get_value(
                    'trim',
                    'do you want to trim edges of windows? (ULTRACAM only)',
                    True
                )
                if trim:
                    ncol = cl.get_value(
                        'ncol', 'number of columns to trim from windows', 0)
                    nrow = cl.get_value(
                        'nrow', 'number of rows to trim from windows', 0)
            else:
                trim = False

            twait = cl.get_value(
                'twait', 'time to wait for a new frame [secs]', 1., 0.)
            tmax = cl.get_value(
                'tmax', 'maximum time to wait for a new frame [secs]', 10., 0.)

        else:
            resource = cl.get_value(
                'flist', 'file list', cline.Fname('files.lis',hcam.LIST)
            )
            first = 1
            trim = False

        # define the panel grid. first get the labels and maximum dimensions
        ccdinf = spooler.get_ccd_pars(source, resource)

        try:
            nxdef = cl.get_default('nx')
        except:
            nxdef = 3

        if len(ccdinf) > 1:
            ccd = cl.get_value('ccd', 'CCD(s) to plot [0 for all]', '0')
            if ccd == '0':
                ccds = list(ccdinf.keys())
            else:
                ccds = ccd.split()
                check = set(ccdinf.keys())
                if not set(ccds) <= check:
                    raise hcam.HipercamError(
                        'At least one invalid CCD label supplied'
                    )

            if len(ccds) > 1:
                nxdef = min(len(ccds), nxdef)
                cl.set_default('nx', nxdef)
                nx = cl.get_value('nx', 'number of panels in X', 3, 1)
            else:
                nx = 1
        else:
            nx = 1
            ccds = list(ccdinf.keys())

        cl.set_default('pause', 0.)
        pause = cl.get_value(
            'pause', 'time delay to add between'
            ' frame plots [secs]', 0., 0.
        )

        cl.set_default('plotall', False)
        plotall = cl.get_value('plotall', 'plot all frames,'
                               ' regardless of status?', False)

        # bias frame (if any)
        bias = cl.get_value(
            'bias', "bias frame ['none' to ignore]",
            cline.Fname('bias', hcam.HCAM), ignore='none'
        )
        if bias is not None:
            # read the bias frame
            bias = hcam.MCCD.read(bias)
            fprompt = "flat frame ['none' to ignore]"
        else:
            fprompt = "flat frame ['none' is normal choice with no bias]"

        # flat (if any)
        flat = cl.get_value(
            'flat', fprompt,
            cline.Fname('flat', hcam.HCAM), ignore='none'
        )
        if flat is not None:
            # read the flat frame
            flat = hcam.MCCD.read(flat)

        # defect file (if any)
        dfct = cl.get_value(
            'defect', "defect file ['none' to ignore]",
            cline.Fname('defect', hcam.DFCT), ignore='none'
        )
        if dfct is not None:
            # read the defect frame
            dfct = defect.MccdDefect.read(dfct)

        # Get windows from hdriver
        setup = cl.get_value(
            'setup', 'display current hdriver window settings', False
        )

        if setup:
            hurl = cl.get_value(
                'hurl', 'URL for hdriver windows', 'http://192.168.1.2:5100'
            )

        # define the display intensities
        msub = cl.get_value('msub', 'subtract median from each window?', True)

        iset = cl.get_value(
            'iset', 'set intensity a(utomatically),'
            ' d(irectly) or with p(ercentiles)?',
            'a', lvals=['a','d','p'])
        iset = iset.lower()

        plo, phi = 5, 95
        ilo, ihi = 0, 1000
        if iset == 'd':
            ilo = cl.get_value('ilo', 'lower intensity limit', 0.)
            ihi = cl.get_value('ihi', 'upper intensity limit', 1000.)
        elif iset == 'p':
            plo = cl.get_value('plo', 'lower intensity limit percentile',
                               5., 0., 100.)
            phi = cl.get_value('phi', 'upper intensity limit percentile',
                               95., 0., 100.)

        # region to plot
        for i, cnam in enumerate(ccds):
            nxtot, nytot, nxpad, nypad = ccdinf[cnam]
            if i == 0:
                xmin, xmax = float(-nxpad), float(nxtot + nxpad + 1)
                ymin, ymax = float(-nypad), float(nytot + nypad + 1)
            else:
                xmin = min(xmin, float(-nxpad))
                xmax = max(xmax, float(nxtot + nxpad + 1))
                ymin = min(ymin, float(-nypad))
                ymax = max(ymax, float(nytot + nypad + 1))

        xlo = cl.get_value('xlo', 'left-hand X value', xmin, xmin, xmax)
        xhi = cl.get_value('xhi', 'right-hand X value', xmax, xmin, xmax)
        ylo = cl.get_value('ylo', 'lower Y value', ymin, ymin, ymax)
        yhi = cl.get_value('yhi', 'upper Y value', ymax, ymin, ymax)

        # profile fitting if just one CCD chosen
        if len(ccds) == 1:
            # many parameters for profile fits, although most are not plotted
            # by default
            profit = cl.get_value('profit', 'do you want profile fits?', False)

            if profit:
                fdevice = cl.get_value('fdevice', 'plot device for fits',
                                       '2/xs')
                fwidth = cl.get_value('fwidth', 'fit plot width (inches)', 0.)
                fheight = cl.get_value(
                    'fheight', 'fit plot height (inches)', 0.)
                method = cl.get_value(
                    'method', 'fit method g(aussian) or m(offat)',
                    'm', lvals=['g','m'])
                if method == 'm':
                    beta = cl.get_value(
                        'beta', 'initial exponent for Moffat fits', 5., 0.5)
                else:
                    beta = 0.
                fwhm_min = cl.get_value(
                    'fwhm_min', 'minimum FWHM to allow [unbinned pixels]',
                    1.5, 0.01
                )
                fwhm = cl.get_value(
                    'fwhm', 'initial FWHM [unbinned pixels] for profile fits',
                    6., fwhm_min
                )
                shbox = cl.get_value(
                    'shbox', 'half width of box for initial location'
                    ' of target [unbinned pixels]', 11., 2.)
                smooth = cl.get_value(
                    'smooth', 'FWHM for smoothing for initial object'
                    ' detection [binned pixels]', 6.)
                splot = cl.get_value(
                    'splot', 'plot outline of search box?', True)
                fhbox = cl.get_value(
                    'fhbox', 'half width of box for profile fit'
                    ' [unbinned pixels]', 21., 3.)
                hmin = cl.get_value(
                    'hmin', 'minimum peak height to accept the fit', 50.)
                read = cl.get_value('read', 'readout noise, RMS ADU', 3.)
                gain = cl.get_value('gain', 'gain, ADU/e-', 1.)
                thresh = cl.get_value('thresh', 'number of RMS to reject at', 4.)

        else:
            profit = False


    ################################################################
    #
    # all the inputs have now been obtained. Get on with doing stuff

    # open image plot device
    imdev = hcam.pgp.Device(device)
    if width > 0 and height > 0:
        pgpap(width,height/width)

    # set up panels and axes
    nccd = len(ccds)
    ny = nccd // nx if nccd % nx == 0 else nccd // nx + 1

    # slice up viewport
    pgsubp(nx,ny)

    # plot axes, labels, titles. Happens once only
    for cnam in ccds:
        pgsci(hcam.pgp.Params['axis.ci'])
        pgsch(hcam.pgp.Params['axis.number.ch'])
        pgenv(xlo, xhi, ylo, yhi, 1, 0)
        pglab('X','Y','CCD {:s}'.format(cnam))

    # initialisations. 'last_ok' is used to store the last OK frames of each
    # CCD for retrieval when coping with skipped data.

    total_time = 0 # time waiting for new frame
    fpos = []      # list of target positions to fit
    fframe = True   # waiting for first valid frame with profit

    # plot images
    with spooler.data_source(source, resource, first, full=False) as spool:

        # 'spool' is an iterable source of MCCDs
        n = 0
        for mccd in spool:

            if server_or_local:
                # Handle the waiting game ...
                give_up, try_again, total_time = spooler.hang_about(
                    mccd, twait, tmax, total_time
                )

                if give_up:
                    print('rtplot stopped')
                    break
                elif try_again:
                    continue

            # Trim the frames: ULTRACAM windowed data has bad columns
            # and rows on the sides of windows closest to the readout
            # which can badly affect reduction. This option strips
            # them.
            if trim:
                hcam.ccd.trim_ultracam(mccd, ncol, nrow)

            # indicate progress
            tstamp = Time(mccd.head['TIMSTAMP'], format='isot', precision=3)
            print(
                '{:d}, utc = {:s}, '.format(
                    mccd.head['NFRAME'], tstamp.iso), end=''
            )

            # accumulate errors
            emessages = []

            if n == 0:
                if bias is not None:
                    # crop the bias on the first frame only
                    bias = bias.crop(mccd)

                if flat is not None:
                    # crop the flat on the first frame only
                    flat = flat.crop(mccd)

            if setup:
                # Get windows from hdriver. Fair bit of error checking
                # needed. 'got_windows' indicates if anything useful
                # found, 'hwindows' is a list of (llx,lly,nx,ny) tuples
                # if somthing is found.
                try:
                    r = requests.get(
                        'http://192.168.1.2:5100',timeout=0.2
                    )

                    if r.text.strip() == 'No valid data available':
                        emessages.append(
                            '** bad return from hdriver = {:s}'.format(r.text.strip())
                        )
                        got_windows = False

                    elif r.text.strip() == 'fullframe':
                        # to help Stu out a bit, effectively just ignore this one
                        got_windows = False

                    else:
                        # OK, got something
                        lines = r.text.split('\r\n')
                        xbinh,ybinh,nwinh = lines[0].split()
                        xbinh,ybinh,nwinh = int(xbinh),int(ybinh),int(nwinh)
                        hwindows = []
                        for line in lines[1:nwinh+1]:
                            llxh,llyh,nxh,nyh = line.split()
                            hwindows.append(
                                (int(llxh),int(llyh),int(nxh),int(nyh))
                            )

                        if nwinh != len(hwindows):
                            emessages.append(
                                ('** expected {:d} windows from'
                                 ' hdriver but got {:d}').format(nwinh,len(hwindows))
                            )
                            got_windows = False

                        got_windows = True

                except (requests.exceptions.ConnectionError, socket.timeout,
                        requests.exceptions.Timeout) as err:
                    emessages.append(' ** hdriver error: {!r}'.format(err))
                    got_windows = False

            else:
                got_windows = False

            # display the CCDs chosen
            message = ''
            pgbbuf()
            for nc, cnam in enumerate(ccds):
                ccd = mccd[cnam]

                if plotall or ccd.is_data():
                    # this should be data as opposed to a blank frame
                    # between data frames that occur with nskip > 0

                    # subtract the bias
                    if bias is not None:
                        ccd -= bias[cnam]

                    # divide out the flat
                    if flat is not None:
                        ccd /= flat[cnam]

                    if msub:
                        # subtract median from each window
                        for wind in ccd.values():
                            wind -= wind.median()

                    # set to the correct panel and then plot CCD
                    ix = (nc % nx) + 1
                    iy = nc // nx + 1
                    pgpanl(ix,iy)
                    vmin, vmax = hcam.pgp.pCcd(
                        ccd,iset,plo,phi,ilo,ihi,
                        xlo=xlo, xhi=xhi, ylo=ylo, yhi=yhi
                    )

                    if got_windows:
                        # plot the current hdriver windows
                        pgsci(hcam.CNAMS['yellow'])
                        pgsls(2)
                        for llxh,llyh,nxh,nyh in hwindows:
                            pgrect(llxh-0.5,llxh+nxh-0.5,llyh-0.5,llyh+nyh-0.5)

                    if dfct is not None and cnam in dfct:
                        # plot defects
                        hcam.pgp.pCcdDefect(dfct[cnam])

                    # accumulate string of image scalings
                    if nc:
                        message += ', ccd {:s}: {:.1f}, {:.1f}, exp: {:.4f}'.format(
                            cnam,vmin,vmax,mccd.head['EXPTIME']
                        )
                    else:
                        message += 'ccd {:s}: {:.1f}, {:.1f}, exp: {:.4f}'.format(
                            cnam,vmin,vmax,mccd.head['EXPTIME']
                        )

            pgebuf()
            # end of CCD display loop
            print(message)
            for emessage in emessages:
                print(emessage)

            if ccd.is_data() and profit and fframe:
                fframe = False

                # cursor selection of targets after first plot, if profit
                # accumulate list of starter positions

                print('Please select targets for profile fitting. You can select as many as you like.')
                x, y, reply = (xlo+xhi)/2, (ylo+yhi)/2, ''
                ntarg = 0
                pgsci(2)
                pgslw(2)
                while reply != 'Q':
                    print("Place cursor on fit target. Any key to register, 'q' to quit")
                    x, y, reply = pgcurs(x, y)
                    if reply == 'q':
                        break
                    else:
                        # check that the position is inside a window
                        wnam = ccd.inside(x, y, 2)

                        if wnam is not None:
                            # store the position, Window label, target number,
                            # box size fwhm, beta
                            ntarg += 1
                            fpos.append(Fpar(x,y,wnam,ntarg,shbox,fwhm,beta))

                            # report information, overplot search box
                            print(
                                ('Target {:d} selected at {:.1f},'
                                 '{:.1f} in window {:s}').format(ntarg,x,y,wnam)
                            )
                            if splot:
                                fpos[-1].plot()

                if len(fpos):
                    print(len(fpos),'targets selected')
                    # if some targets were selected, open the fit plot device
                    fdev = hcam.pgp.Device(fdevice)
                    if fwidth > 0 and fheight > 0:
                        pgpap(fwidth,fheight/fwidth)

            if ccd.is_data():

                # carry out fits. Nothing happens if fpos is empty
                for fpar in fpos:
                    # switch to the image plot
                    imdev.select()

                    # plot search box
                    if splot:
                        fpar.plot()

                    try:
                        # extract search box from the CCD. 'fpar' is updated later
                        # if the fit is successful to reflect the new position
                        swind = fpar.swind(ccd)

                        # carry out initial search
                        x,y,peak = swind.search(smooth, fpar.x, fpar.y, hmin, False)

                        # now for a more refined fit. First extract fit Window
                        fwind = ccd[fpar.wnam].window(
                            x-fhbox, x+fhbox, y-fhbox, y+fhbox
                        )

                        # crude estimate of sky background
                        sky = np.percentile(fwind.data, 50)

                        # refine the Aperture position by fitting the profile
                        (sky, height, x, y, fwhm, beta), epars, \
                            (wfit, X, Y, sigma, chisq, nok,
                             nrej, npar, message) = hcam.fitting.combFit(
                                fwind, method, sky, peak-sky,
                                x, y, fpar.fwhm, fwhm_min, False,
                                fpar.beta, read, gain, thresh
                            )

                        print('Targ {:d}: {:s}'.format(fpar.ntarg,message))

                        if peak > hmin and ccd[fpar.wnam].distance(x,y) > 1:
                            # update some initial parameters for next time
                            if method == 'g':
                                fpar.x, fpar.y, fpar.fwhm = x, y, fwhm
                            elif method == 'm':
                                fpar.x, fpar.y, fpar.fwhm, \
                                    fpar.beta = x, y, fwhm, beta

                            # plot values versus radial distance
                            ok = sigma > 0
                            R = np.sqrt((X-x)**2+(Y-y)**2)
                            fdev.select()
                            vmin = min(sky, sky+height, fwind.min())
                            vmax = max(sky, sky+height, fwind.max())
                            extent = vmax-vmin
                            pgeras()
                            pgvstd()
                            pgswin(0,R.max(),vmin-0.05*extent,vmax+0.05*extent)
                            pgsci(4)
                            pgbox('bcnst',0,0,'bcnst',0,0)
                            pgsci(2)
                            pglab('Radial distance [unbinned pixels]','Counts','')
                            pgsci(1)
                            pgpt(R[ok].flat, fwind.data[ok].flat, 1)
                            if nrej:
                                pgsci(2)
                                pgpt(R[~ok].flat, fwind.data[~ok].flat, 5)

                            # line fit
                            pgsci(3)
                            r = np.linspace(0,R.max(),400)
                            if method == 'g':
                                alpha = 4*np.log(2.)/fwhm**2
                                f = sky+height*np.exp(-alpha*r**2)
                            elif method == 'm':
                                alpha = 4*(2**(1/beta)-1)/fwhm**2
                                f = sky+height/(1+alpha*r**2)**beta
                            pgline(r,f)

                            # back to the image to plot a circle of radius FWHM
                            imdev.select()
                            pgsci(3)
                            pgcirc(x,y,fwhm)

                        else:
                            print('  *** below detection threshold; position & FWHM will not updated')
                            pgsci(2)

                        # plot location on image as a cross
                        pgpt1(x, y, 5)

                    except hcam.HipercamError as err:
                        print(' >> Targ {:d}: fit failed ***: {!s}'.format(
                            fpar.ntarg, err)
                        )
                        pgsci(2)

            if pause > 0.:
                # pause between frames
                time.sleep(pause)

            # update the frame number
            n += 1
Example #21
0
def grab(args=None):
    """``grab [source temp] (run first last (ndigit) [twait tmax] | flist
    (prefix)) trim ([ncol nrow]) bias dark flat fmap (fpair [nhalf rmin
    rmax verbose]) [dtype]``

    This downloads a sequence of images from a raw data file and writes them
    out to a series CCD / MCCD files.

    Parameters:

       source : str [hidden]
           Data source, four options:

              | 'hs' : HiPERCAM server
              | 'hl' : local HiPERCAM FITS file
              | 'us' : ULTRACAM server
              | 'ul' : local ULTRACAM .xml/.dat files
              | 'hf' : list of HiPERCAM hcm FITS-format files

           The standard start-off default for ``source`` can be set
           using the environment variable
           HIPERCAM_DEFAULT_SOURCE. e.g. in bash :code:`export
           HIPERCAM_DEFAULT_SOURCE="us"` would ensure it always
           started with the ULTRACAM server by default. If
           unspecified, it defaults to 'hl'. 'hf' is useful if you
           want to apply pipeline calibrations (bias, flat, etc) to
           files imported from a 'foreign' format. In this case the
           output files will have the same name as the inputs but
           with a prefix added.

       temp : bool [hidden, defaults to False]
           True to indicate that the frames should be written to
           temporary files with automatically-generated names in the
           expectation of deleting them later. This also writes out a
           file listing the names.  The aim of this is to allow grab
           to be used as a feeder for other routines in larger
           scripts.  If temp == True, grab will return with the name
           of the list of hcm files. Look at 'makebias' for an example
           that uses this feature.

       run : str [if source ends 's' or 'l']
           run number to access, e.g. 'run034'

       first : int [if source ends 's' or 'l']
           First frame to access

       last : int [if source ends 's' or 'l']
           Last frame to access, 0 for the lot

       ndigit : int [if source ends 's' or 'l' and not temp]
           Files created will be written as 'run005_0013.fits'
           etc. `ndigit` is the number of digits used for the frame
           number (4 in this case). Any pre-existing files of the same
           name will be over-written.

       twait : float [hidden]
           time to wait between attempts to find a new exposure, seconds.

       tmax : float [hidden]
           maximum time to wait between attempts to find a new exposure,
           seconds.

       flist : str [if source ends 'f']
           name of file list. Output files will have the same names
           as the input files with a prefix added

       prefix : str [if source ends 'f' and not temp]
           prefix to add to create output file names

       trim : bool
           True to trim columns and/or rows off the edges of windows
           nearest the readout. Particularly useful for ULTRACAM
           windowed data where the first few rows and columns can
           contain bad data.

       ncol : int [if trim, hidden]
           Number of columns to remove (on left of left-hand window,
           and right of right-hand windows)

       nrow : int [if trim, hidden]
           Number of rows to remove (bottom of windows)

       bias : str
           Name of bias frame to subtract, 'none' to ignore.

       dark : str
           Name of dark frame, 'none' to ignore.

       flat : str
           Name of flat field to divide by, 'none' to ignore. Should normally
           only be used in conjunction with a bias, although it does allow you
           to specify a flat even if you haven't specified a bias.

       fmap : str
           Name of fringe map (see e.g. `makefringe`), 'none' to ignore.

       fpair : str [if fmap is not 'none']
           Name of fringe pair file (see e.g. `setfringe`). Required if
           a fringe map has been specified.

       nhalf : int [if fmap is not 'none', hidden]
           When calculating the differences for fringe measurement,
           a region extending +/-nhalf binned pixels will be used when
           measuring the amplitudes. Basically helps the stats.

       rmin : float [if fmap is not 'none', hidden]
           Minimum individual ratio to accept prior to calculating the overall
           median in order to reduce the effect of outliers. Although all ratios
           should be positive, you might want to set this a little below zero
           to allow for some statistical fluctuation.

       rmax : float [if fmap is not 'none', hidden]
           Maximum individual ratio to accept prior to calculating the overall
           median in order to reduce the effect of outliers. Probably typically
           < 1 if fringe map was created from longer exposure data.

       verbose : bool
           True to print lots of details of fringe ratios

       dtype : string [hidden, defaults to 'f32']
           Data type on output. Options:

            | 'f32' : output as 32-bit floats (default)

            | 'f64' : output as 64-bit floats.

            | 'u16' : output as 16-bit unsigned integers. A warning will be
                      issued if loss of precision occurs; an exception will
                      be raised if the data are outside the range 0 to 65535.

    .. Note::

       |grab| is used by several other scripts such as |averun| so take great
       care when changing anything to do with its input parameters.

       If you use the "temp" option to write to temporary files, then those
       files will be deleted if you interrup with CTRL-C. This is to prevent
       the accumulation of such frames.
    """

    command, args = utils.script_args(args)

    # get inputs
    with Cline("HIPERCAM_ENV", ".hipercam", command, args) as cl:

        # register parameters
        cl.register("source", Cline.GLOBAL, Cline.HIDE)
        cl.register("temp", Cline.GLOBAL, Cline.HIDE)
        cl.register("run", Cline.GLOBAL, Cline.PROMPT)
        cl.register("first", Cline.LOCAL, Cline.PROMPT)
        cl.register("last", Cline.LOCAL, Cline.PROMPT)
        cl.register("ndigit", Cline.LOCAL, Cline.PROMPT)
        cl.register("twait", Cline.LOCAL, Cline.HIDE)
        cl.register("tmax", Cline.LOCAL, Cline.HIDE)
        cl.register("flist", Cline.GLOBAL, Cline.PROMPT)
        cl.register("prefix", Cline.GLOBAL, Cline.PROMPT)
        cl.register("trim", Cline.GLOBAL, Cline.PROMPT)
        cl.register("ncol", Cline.GLOBAL, Cline.HIDE)
        cl.register("nrow", Cline.GLOBAL, Cline.HIDE)
        cl.register("bias", Cline.LOCAL, Cline.PROMPT)
        cl.register("flat", Cline.LOCAL, Cline.PROMPT)
        cl.register("dark", Cline.LOCAL, Cline.PROMPT)
        cl.register("fmap", Cline.LOCAL, Cline.PROMPT)
        cl.register("fpair", Cline.LOCAL, Cline.PROMPT)
        cl.register("nhalf", Cline.GLOBAL, Cline.HIDE)
        cl.register("rmin", Cline.GLOBAL, Cline.HIDE)
        cl.register("rmax", Cline.GLOBAL, Cline.HIDE)
        cl.register("verbose", Cline.LOCAL, Cline.HIDE)
        cl.register("dtype", Cline.LOCAL, Cline.HIDE)

        # get inputs
        default_source = os.environ.get('HIPERCAM_DEFAULT_SOURCE', 'hl')
        source = cl.get_value(
            "source",
            "data source [hs, hl, us, ul, hf]",
            default_source,
            lvals=("hs", "hl", "us", "ul", "hf"),
        )

        cl.set_default("temp", False)
        temp = cl.get_value(
            "temp", "save to temporary automatically-generated file names?",
            True)

        # set some flags
        server_or_local = source.endswith("s") or source.endswith("l")

        if server_or_local:
            resource = cl.get_value("run", "run name", "run005")
            first = cl.get_value("first", "first frame to grab", 1, 0)
            last = cl.get_value("last", "last frame to grab", 0)
            if last < first and last != 0:
                sys.stderr.write("last must be >= first or 0")
                sys.exit(1)

            if not temp:
                ndigit = cl.get_value("ndigit",
                                      "number of digits in frame identifier",
                                      3, 0)

            twait = cl.get_value("twait",
                                 "time to wait for a new frame [secs]", 1.0,
                                 0.0)
            tmax = cl.get_value("tmax",
                                "maximum time to wait for a new frame [secs]",
                                10.0, 0.0)

        else:
            resource = cl.get_value("flist", "file list",
                                    cline.Fname("files.lis", hcam.LIST))
            if not temp:
                prefix = cl.get_value("prefix",
                                      "prefix to add to file names on output",
                                      "pfx_")
            first = 1

        trim = cl.get_value("trim", "do you want to trim edges of windows?",
                            True)

        if trim:
            ncol = cl.get_value("ncol",
                                "number of columns to trim from windows", 0)
            nrow = cl.get_value("nrow", "number of rows to trim from windows",
                                0)

        bias = cl.get_value(
            "bias",
            "bias frame ['none' to ignore]",
            cline.Fname("bias", hcam.HCAM),
            ignore="none",
        )
        if bias is not None:
            # read the bias frame
            bias = hcam.MCCD.read(bias)

        # dark
        dark = cl.get_value("dark",
                            "dark frame ['none' to ignore]",
                            cline.Fname("dark", hcam.HCAM),
                            ignore="none")
        if dark is not None:
            # read the dark frame
            dark = hcam.MCCD.read(dark)

        # flat
        flat = cl.get_value("flat",
                            "flat field frame ['none' to ignore]",
                            cline.Fname("flat", hcam.HCAM),
                            ignore="none")
        if flat is not None:
            # read the flat field frame
            flat = hcam.MCCD.read(flat)

        # fringe file (if any)
        fmap = cl.get_value(
            "fmap",
            "fringe map ['none' to ignore]",
            cline.Fname("fmap", hcam.HCAM),
            ignore="none",
        )
        if fmap is not None:
            # read the fringe frame and prompt other parameters
            fmap = hcam.MCCD.read(fmap)

            fpair = cl.get_value("fpair", "fringe pair file",
                                 cline.Fname("fringe", hcam.FRNG))
            fpair = fringe.MccdFringePair.read(fpair)

            nhalf = cl.get_value("nhalf",
                                 "half-size of fringe measurement regions", 2,
                                 0)
            rmin = cl.get_value("rmin", "minimum fringe pair ratio", -0.2)
            rmax = cl.get_value("rmax", "maximum fringe pair ratio", 1.0)
            verbose = cl.get_value("verbose", "verbose output", False)

        cl.set_default("dtype", "f32")
        dtype = cl.get_value("dtype",
                             "data type [f32, f64, u16]",
                             "f32",
                             lvals=("f32", "f64", "u16"))

    # Now the actual work.

    # strip off extensions
    if resource.endswith(hcam.HRAW):
        resource = resource[:resource.find(hcam.HRAW)]

    # initialisations
    total_time = 0  # time waiting for new frame
    nframe = first
    root = os.path.basename(resource)
    bframe = None

    # Finally, we can go
    fnames = []
    if temp:
        tdir = utils.temp_dir()

    with CleanUp(fnames, temp) as cleanup:
        # The above line to handle ctrl-c and temporaries

        with spooler.data_source(source, resource, first) as spool:

            for mccd in spool:

                if server_or_local:
                    # Handle the waiting game ...
                    give_up, try_again, total_time = spooler.hang_about(
                        mccd, twait, tmax, total_time)

                    if give_up:
                        print("grab stopped")
                        break
                    elif try_again:
                        continue

                # Trim the frames: ULTRACAM windowed data has bad
                # columns and rows on the sides of windows closest to
                # the readout which can badly affect reduction. This
                # option strips them.
                if trim:
                    hcam.ccd.trim_ultracam(mccd, ncol, nrow)

                if nframe == first:
                    # First time through, need to manipulate calibration data
                    if bias is not None:
                        bias = bias.crop(mccd)
                        bexpose = bias.head.get("EXPTIME", 0.0)
                    else:
                        bexpose = 0.
                    if flat is not None:
                        flat = flat.crop(mccd)
                    if dark is not None:
                        dark = dark.crop(mccd)
                    if fmap is not None:
                        fmap = fmap.crop(mccd)
                        fpair = fpair.crop(mccd, nhalf)

                # now any time through, apply calibrations
                if bias is not None:
                    mccd -= bias

                if dark is not None:
                    # subtract dark, CCD by CCD
                    dexpose = dark.head["EXPTIME"]
                    for cnam in mccd:
                        ccd = mccd[cnam]
                        if ccd.is_data():
                            cexpose = ccd.head["EXPTIME"]
                            scale = (cexpose - bexpose) / dexpose
                            ccd -= scale * dark[cnam]

                if flat is not None:
                    mccd /= flat

                if fmap is not None:
                    # apply CCD by CCD
                    for cnam in fmap:
                        if cnam in fpair:
                            ccd = mccd[cnam]
                            if ccd.is_data():
                                if verbose:
                                    print(f' CCD {cnam}')

                                fscale = fpair[cnam].scale(ccd,
                                                           fmap[cnam],
                                                           nhalf,
                                                           rmin,
                                                           rmax,
                                                           verbose=verbose)

                                if verbose:
                                    print(f'  Median scale factor = {fscale}')

                                ccd -= fscale * fmap[cnam]

                if dtype == "u16":
                    mccd.uint16()
                elif dtype == "f32":
                    mccd.float32()
                elif dtype == "f64":
                    mccd.float64()

                # write to disk
                if temp:
                    # generate name automatically
                    fd, fname = tempfile.mkstemp(suffix=hcam.HCAM, dir=tdir)
                    fnames.append(fname)
                    mccd.write(fname, True)
                    os.close(fd)

                elif server_or_local:
                    fname = f"{root}_{nframe:0{ndigit}}{hcam.HCAM}"
                    mccd.write(fname, True)

                else:
                    fname = utils.add_extension(
                        f"{prefix}{mccd.head['FILENAME']}")
                    mccd.write(fname, True)

                print("Written frame {:d} to {:s}".format(nframe, fname))

                # update the frame number
                nframe += 1
                if server_or_local and last and nframe > last:
                    break

        if temp:
            if len(fnames) == 0:
                raise hcam.HipercamError(
                    'no files were grabbed; please check'
                    ' input parameters, especially "first"')

            # write the file names to a list
            fd, fname = tempfile.mkstemp(suffix=hcam.LIST, dir=tdir)
            cleanup.flist = fname
            with open(fname, "w") as fout:
                for fnam in fnames:
                    fout.write(fnam + "\n")
            os.close(fd)
            print("temporary file names written to {:s}".format(fname))

            # return the name of the file list
            return fname

    print("grab finished")