def main(field, destination, epoch, force_subtract_better_seeing, subtraction_type, template_instrument):
    if destination[-1] != '/':
        destination = destination + '/'

    params = p.object_params_frb(field)
    destination_path = f'{params["data_dir"]}subtraction/{destination}/'
    u.mkdir_check(destination_path)

    params = p.object_params_frb(field)
    template_epoch = params['template_epoch_' + template_instrument.lower()]

    comparison_title = f'{field}_{epoch}'
    template_title = f'{field}_{template_epoch}'

    filters = params['filters']

    for f in filters:
        f_0 = f[0]

        destination_path_filter = f'{destination_path}{f}/'
        u.mkdir_check(destination_path_filter)

        template_params = p.load_params(destination_path_filter + f'{template_title}_template_output_values.yaml')
        comparison_params = p.load_params(destination_path_filter + f'{comparison_title}_comparison_output_values.yaml')

        if f_0 + '_fwhm_arcsec' in template_params:
            fwhm_template = template_params[f_0 + '_fwhm_arcsec']
        elif f_0.lower() + '_fwhm_arcsec' in template_params:
            fwhm_template = template_params[f_0.lower() + '_fwhm_arcsec']
        else:
            raise ValueError(f_0 + '_fwhm_arcsec or ' + f_0.lower() + '_fwhm_arcsec not found for template image.')

        if f_0 + '_fwhm_arcsec' in comparison_params:
            fwhm_comparison = comparison_params[f_0 + '_fwhm_arcsec']
        elif f_0.lower() + '_fwhm_arcsec' in comparison_params:
            fwhm_comparison = comparison_params[f_0.lower() + '_fwhm_arcsec']
        else:
            raise ValueError(f_0 + '_fwhm_arcsec or ' + f_0.lower() + '_fwhm_arcsec not found for comparison image.')

        template = destination_path_filter + f'{template_title}_template_aligned.fits'
        comparison = destination_path_filter + f'{comparison_title}_comparison_aligned.fits'

        _, difference = ph.subtract(template_origin=template,
                                    comparison_origin=comparison,
                                    output=destination_path_filter,
                                    template_fwhm=fwhm_template,
                                    comparison_fwhm=fwhm_comparison,
                                    force_subtract_better_seeing=force_subtract_better_seeing,
                                    comparison_title=comparison_title,
                                    template_title=template_title,
                                    field=field, comparison_epoch=epoch, template_epoch=template_epoch)
Exemple #2
0
def update_std_sdss_photometry(ra: float, dec: float, force: bool = False):
    """
    Retrieves and writes to disk the SDSS photometry for a standard-star calibration field, in a 0.2 x 0.2 degree box
    centred on the field coordinates. (Note - the width of the box is in RA degrees, not corrected for spherical
    distortion)
    :param ra: Right Ascension of the centre of the desired field, in degrees.
    :param dec: Declination of the centre of the desired field, in degrees.
    :return: Retrieved photometry table, as a pandas dataframe, if successful; if not, None.
    """
    data_dir = p.config['top_data_dir']
    field_path = f"{data_dir}/std_fields/RA{ra}_DEC{dec}/"
    outputs = p.load_params(field_path + "output_values")
    if outputs is None or "in_sdss" not in outputs or force:
        path = field_path + "SDSS/SDSS.csv"
        response = save_sdss_photometry(ra=ra, dec=dec, output=path)
        params = {}
        if response is not None:
            params["in_sdss"] = True
        else:
            params["in_sdss"] = False
        p.add_params(file=field_path + "output_values", params=params)
        return response
    elif outputs["in_sdss"] is True:
        print("There is already SDSS data present for this field.")
        return True
    else:
        print("This field is not present in SDSS.")
def main(data_title, sextractor_path, origin, destination):
    properties = p.object_params_imacs(data_title)
    data_dir = properties['data_dir']
    if sextractor_path is not None:
        if not os.path.isdir(sextractor_path):
            os.mkdir(sextractor_path)
        do_sextractor = True
        ap_diams_sex = p.load_params(f'param/aperture_diameters_fors2')
    else:
        do_sextractor = False

    origin_path = data_dir + origin
    destination_path = data_dir + destination
    u.mkdir_check(destination_path)
    filters = next(os.walk(origin_path))[1]

    for fil in filters:
        u.mkdir_check(destination_path + fil)
        if do_sextractor:
            if not os.path.isdir(sextractor_path + fil):
                os.mkdir(sextractor_path + fil)
        files = os.listdir(origin_path + fil + "/")
        for file_name in files:
            if file_name[-5:] == '.fits':
                science_origin = origin_path + fil + "/" + file_name
                science_destination = destination_path + fil + "/" + file_name
                print(science_origin)
                # Divide by exposure time to get an image in counts/second.
                f.divide_by_exp_time(file=science_origin,
                                     output=science_destination)
                if do_sextractor:
                    copyfile(science_origin,
                             sextractor_path + fil + "/" + file_name)

        # Write a sextractor file for photometric testing of the data from the upper chip.
        if do_sextractor:
            # Write a csv table of file properties to each filter directory.
            tbl = f.fits_table(input_path=sextractor_path + fil,
                               output_path=sextractor_path + fil + "/" + fil +
                               "_fits_tbl.csv",
                               science_only=False)
            # TODO: Rewrite to use psf-fitting (in FORS2 pipeline as well)
            for i, d in enumerate(ap_diams_sex):
                f.write_sextractor_script(table=tbl,
                                          output_path=sextractor_path + fil +
                                          "/sextract_aperture_" + str(d) +
                                          ".sh",
                                          sex_params=['c', 'PHOT_APERTURES'],
                                          sex_param_values=['im.sex',
                                                            str(d)],
                                          cat_name='sextracted_' + str(d),
                                          cats_dir='aperture_' + str(d),
                                          criterion='chip',
                                          value='CHIP1')

    if os.path.isfile(origin_path + data_title + '.log'):
        copyfile(origin_path + data_title + '.log',
                 destination_path + data_title + ".log")
    u.write_log(path=destination_path + data_title + ".log",
                action=f'Astrometry solved using 3-astrometry.py')
def main(data_title: str, origin: str, destination: str, redo: bool = False):
    properties = p.object_params_imacs(data_title)
    path = properties['data_dir']

    origin_path = path + origin
    astrometry_path = path + destination
    u.mkdir_check(astrometry_path)

    keys = params.load_params('param/keys')
    key = keys['astrometry']

    reduced_list = os.listdir(origin_path)
    astrometry_list = os.listdir(astrometry_path)

    if redo:
        to_send = list(filter(lambda f: f[-5:] == '.fits', reduced_list))
    else:
        to_send = list(
            filter(lambda f: f[-5:] == '.fits' and f not in astrometry_list,
                   reduced_list))

    filters = list(filter(lambda f: os.path.isdir(f), os.listdir(origin)))

    for f in filters:
        reduced_path_filter = origin_path + f + '/'
        astrometry_path_filter = astrometry_path + f + '/'
        print(f'To send to Astrometry.net from {f}:')
        for file in to_send:
            print('\t' + file)

        for file in to_send:
            hdu = fits.open(origin_path + file)
            header = hdu[0].header
            ra = header['RA-D']
            dec = header['DEC-D']
            scale_upper = header['SCALE'] + 0.1
            scale_lower = header['SCALE'] - 0.1
            hdu.close()
            print('Sending to Astrometry.net:', file)
            os.system(f'python /astrometry-client.py '
                      f'--apikey {key} '
                      f'-u {reduced_path_filter}{file} '
                      f'-w '
                      f'--newfits {astrometry_path_filter}{file} '
                      f'--ra {ra} --dec {dec} --radius {1.} '
                      f'--scale-upper {scale_upper} '
                      f'--scale-lower {scale_lower} '
                      f'--private --no_commercial')

    if os.path.isfile(origin_path + data_title + '.log'):
        shutil.copy(origin_path + data_title + '.log',
                    astrometry_path + data_title + ".log")
    u.write_log(path=astrometry_path + data_title + ".log",
                action=f'Astrometry solved using 3-astrometry.py')
def main(data_dir, data_title, origin, destination, fil):

    print("\nExecuting Python script pipeline_fors2/6-montage.py, with:")
    print(f"\tepoch {data_title}")
    print(f"\tdata directory {data_dir}")
    print(f"\torigin directory {destination}")
    print(f"\tdestination directory {destination}")
    print(f"\tfilter {fil}")
    print()

    table = fits_table(origin + '/' + fil, science_only=False)
    table.sort('identifier')

    fil = fil.replace('/', '')

    header_file = destination + '/' + fil[0] + '_template.hdr'

    header_stream = open(header_file, 'r')
    header = header_stream.readlines()
    header_stream.close()

    # Transfer and/or modify header information

    params = p.load_params(data_dir + '/output_values')
    airmass = table['airmass'].mean()
    saturate = table['saturate'].mean()
    obj = table['object'][0]
    old_gain = table['gain'].mean()
    n_frames = params[fil[0] + '_n_exposures']
    gain = gain_median_combine(old_gain=old_gain, n_frames=n_frames)

    header.insert(-1, f'AIRMASS = {airmass}\n')
    header.insert(-1, f'FILTER  = {fil}\n')
    header.insert(-1, f'OBJECT  = {obj}\n')
    header.insert(-1, f'EXPTIME = 1.\n')
    header.insert(-1, f'GAIN    = {gain}\n')
    header.insert(-1, f'SATURATE= {saturate}\n')
    header.insert(-1, f'MJD-OBS = {float(np.nanmin(table["mjd_obs"]))}\n')
    dates = table["mjd_obs"]
    dates.sort()
    header.insert(-1, f'DATE-OBS = {table["date_obs"][0]}\n')

    os.remove(header_file)

    with open(header_file, 'w') as file:
        file.writelines(header)
Exemple #6
0
def update_std_skymapper_photometry(ra: float, dec: float, force: bool = False):
    data_dir = p.config['top_data_dir']
    field_path = f"{data_dir}/std_fields/RA{ra}_DEC{dec}/"
    outputs = p.load_params(field_path + "output_values")
    if outputs is None or "in_skymapper" not in outputs or force:
        path = field_path + "SkyMapper/SkyMapper.csv"
        response = save_skymapper_photometry(ra=ra, dec=dec, output=path)
        params = {}
        if response is not None:
            params["in_skymapper"] = True
        else:
            params["in_skymapper"] = False
        p.add_params(file=field_path + "output_values", params=params)
        return response
    elif outputs["in_skymapper"] is True:
        print("There is already SkyMapper data present for this field.")
        return True
    else:
        print("This field is not present in SkyMapper.")
Exemple #7
0
def update_std_des_photometry(ra: float, dec: float, force: bool = False):
    """
    Attempts to retrieve and write to disk the DES photometry for a standard-star calibration field, in a 0.2 x 0.2
    degree box centred on the field coordinates (Note - the width of the box is in RA degrees, not corrected for
    spherical distortion).
    Updates the "in_des" value (in output_values.yaml in the field path) to True if the data is successfully retrieved,
    and to False if not.
    If the data already exists in the std_fields directory, or if the field is outside the DES footprint
    (as ascertained by a previous query and stored in output_values.yaml), no attempt is made (unless
    force is True).
    :param ra: Right Ascension of the centre of the desired field, in degrees.
    :param dec: Declination of the centre of the desired field, in degrees.
    :param force: If True, ignores both the existence of the data in the std_fields directory and whether the field is
    in the DES footprint.
    :return: If the query has run successfully, the retrieved photometry table as a Bytes object;
        If the query has not run because the data is already present, True;
        If the query has not run because "in_des" is False, None
    """
    data_dir = p.config['top_data_dir']
    field_path = f"{data_dir}/std_fields/RA{ra}_DEC{dec}/"
    outputs = p.load_params(field_path + "output_values")
    if outputs is None or "in_des" not in outputs or force:
        path = field_path + "DES/DES.csv"
        response = save_des_photometry(ra=ra, dec=dec, output=path)
        params = {}
        if response is not None:
            params["in_des"] = True
        else:
            params["in_des"] = False
        p.add_params(file=field_path + "output_values", params=params)
        return response
    elif outputs["in_des"] is True:
        print("There is already DES data present for this field.")
        return True
    else:
        print("This field is not present in DES.")
def main(destination, show, field, epoch, skip_reproject, offsets_yaml,
         copy_other_vals, f, manual, instrument_template):
    print('manual:', manual)
    properties = p.object_params_frb(field)
    alignment_ra = properties['alignment_ra']
    alignment_dec = properties['alignment_dec']
    skip_reproject = properties['skip_reproject'] or skip_reproject

    specific_star = False
    if alignment_dec != 0.0 and alignment_ra != 0.0 and offsets_yaml is not None:
        specific_star = True

    if destination[-1] != '/':
        destination = destination + '/'
    comparison = f'{field}_{epoch}_comparison.fits'

    if skip_reproject:
        comparison_tweaked = comparison.replace('comparison.fits',
                                                'comparison_aligned.fits')
    else:
        comparison_tweaked = comparison.replace('comparison.fits',
                                                'comparison_tweaked.fits')

    if offsets_yaml is not None:

        offsets = p.load_params(offsets_yaml)
        offset_ra = offsets['offset_ra']
        offset_dec = offsets['offset_dec']
        values = {'offset_ra': offset_ra, 'offset_dec': offset_dec}
        ast.offset_astrometry(hdu=destination + comparison,
                              offset_ra=offset_ra,
                              offset_dec=offset_dec,
                              output=destination + comparison_tweaked)

    else:
        if not manual:
            print('Doing specific star offset...')
            values = ast.tweak(sextractor_path=destination +
                               '/sextractor/alignment/comparison.cat',
                               destination=destination + comparison_tweaked,
                               image_path=destination + comparison,
                               cat_path=destination +
                               '/sextractor/alignment/template.cat',
                               cat_name='SExtractor',
                               tolerance=10.,
                               show=show,
                               stars_only=True,
                               psf=True,
                               specific_star=specific_star,
                               star_dec=alignment_dec,
                               star_ra=alignment_ra)
        else:
            print('Doing manual offset...')
            instrument_template = instrument_template.lower()
            manual_x = properties['offset_' + instrument_template + '_' +
                                  f[0] + '_x']
            manual_y = properties['offset_' + instrument_template + '_' +
                                  f[0] + '_y']
            values = ast.tweak(sextractor_path=destination +
                               '/sextractor/alignment/comparison.cat',
                               destination=destination + comparison_tweaked,
                               image_path=destination + comparison,
                               cat_path=destination +
                               '/sextractor/alignment/template.cat',
                               cat_name='SExtractor',
                               tolerance=10.,
                               show=show,
                               stars_only=True,
                               psf=True,
                               specific_star=specific_star,
                               star_dec=alignment_dec,
                               star_ra=alignment_ra,
                               manual=True,
                               offset_x=manual_x,
                               offset_y=manual_y)

    if instrument_template == 'FORS2':
        force = 2
    else:
        force = None

    if not skip_reproject:
        template = filter(lambda f: "template.fits" in f,
                          os.listdir(destination)).__next__()
        n = ff.reproject(
            image_1=destination + comparison_tweaked,
            image_2=destination + template,
            image_1_output=destination +
            comparison.replace('comparison.fits', 'comparison_aligned.fits'),
            image_2_output=destination +
            template.replace('template.fits', 'template_aligned.fits'),
            force=force)

        if n == 1:
            values['reprojected'] = 'comparison'
        else:
            values['reprojected'] = 'template'

    p.add_params(destination + 'output_values.yaml', values)
Exemple #9
0
def main(epoch, test_name, sex_x_col, sex_y_col, sex_ra_col, sex_dec_col,
         sex_flux_col, stars_only, show_plots, mag_range_sex_lower,
         mag_range_sex_upper, pix_tol, instrument):
    print("\nExecuting Python script pipeline_fors2/9-zeropoint.py, with:")
    print(f"\tepoch {epoch}")
    print()

    properties = p.object_params_fors2(epoch)
    outputs = p.object_output_params(obj=epoch, instrument='fors2')

    proj_paths = p.config

    output = properties['data_dir'] + '9-zeropoint/'
    mkdir_check(output)
    output_std = output + 'std/'
    mkdir_check(output_std)
    mkdir_check(output_std)

    std_path = properties['data_dir'] + 'calibration/std_star/'

    filters = outputs['filters']

    std_field_path = proj_paths['top_data_dir'] + "std_fields/"
    std_fields = list(
        filter(lambda path: os.path.isdir(path), os.listdir(std_field_path)))
    print('Standard fields with available catalogues:')
    print(std_fields)
    print()

    cat_names = photometry_catalogues

    for fil in filters:
        print('Doing filter', fil)

        f = fil[0]

        # Obtain zeropoints from FRB field, if data is available.

        print(
            f"\nDetermining science-field zeropoints for {epoch}, filter {fil}:\n"
        )

        if f"{f}_zeropoints" in outputs:
            zeropoints = outputs[f"{f}_zeropoints"]
        else:
            zeropoints = {}
        zeropoints["science_field"] = {}

        for cat_name in cat_names:
            zeropoints["science_field"][
                cat_name], _ = photometry.zeropoint_science_field(
                    epoch=epoch,
                    instrument=instrument,
                    test_name=None,
                    sex_x_col='XPSF_IMAGE',
                    sex_y_col='YPSF_IMAGE',
                    sex_ra_col='ALPHAPSF_SKY',
                    sex_dec_col='DELTAPSF_SKY',
                    sex_flux_col='FLUX_PSF',
                    stars_only=True,
                    star_class_col='CLASS_STAR',
                    star_class_tol=0.95,
                    show_plots=False,
                    mag_range_sex_lower=-100. * units.mag,
                    mag_range_sex_upper=100. * units.mag,
                    pix_tol=5. * units.pixel,
                    separate_chips=True,
                    cat_name=cat_name)

        # Obtain zeropoints from available standard fields and available data.

        print(
            f"\nDetermining standard-field zeropoints for {epoch}, filter {fil}\n"
        )

        zeropoints["standard_field"] = {}

        fil_path = std_path + fil + '/'

        if os.path.isdir(fil_path):
            fields = filter(lambda d: os.path.isdir(fil_path + d),
                            os.listdir(fil_path))

            output_path_fil_std = output_std + '/' + fil + '/'
            mkdir_check(output_path_fil_std)

            for field in fields:

                zeropoints["standard_field"][field] = {}

                ra = float(field[field.find("RA") + 2:field.find("_")])
                dec = float(field[field.find("DEC") + 3:])

                print("Looking for photometry data in field " + field + ":")
                mkdir_check(std_field_path + field)
                field_path = fil_path + field + '/'
                output_path = output_path_fil_std + field + '/'

                std_cat_path = std_field_path + field + '/'

                std_properties = p.load_params(field_path + 'params.yaml')
                use_sex_star_class = std_properties['use_sex_star_class']

                # Cycle through the three catalogues used to determine zeropoint, in order of preference.

                for cat_name in cat_names:

                    # Check for photometry on-disk in the relevant catalogue; if none present, attempt to retrieve from
                    # online archive.
                    print(f"In {cat_name}:")

                    output_path_cat = output_path + cat_name

                    cat_path = f"{std_cat_path}{cat_name}/{cat_name}.csv"

                    if not (os.path.isdir(std_cat_path + cat_name)
                            ) or os.path.isfile(cat_path):
                        print(
                            "None found on disk. Attempting retrieval from archive..."
                        )
                        if update_std_photometry(ra=ra, dec=dec,
                                                 cat=cat_name) is None:
                            print("\t\tNo data found in archive.")
                            zeropoints["standard_field"][field][
                                cat_name] = None
                            continue

                    column_names = cat_columns(cat=cat_name, f=f)
                    cat_ra_col = column_names['ra']
                    cat_dec_col = column_names['dec']
                    cat_mag_col = column_names['mag_psf']
                    if not use_sex_star_class:
                        star_class_col = column_names['class_star']
                    else:
                        star_class_col = 'CLASS_STAR'

                    cat_type = 'csv'

                    sextractor_path = field_path + 'sextractor/_psf-fit.cat'
                    image_path = field_path + '3-trimmed/standard_trimmed_img_up.fits'
                    star_class_tol = std_properties['star_class_tol']

                    now = time.Time.now()
                    now.format = 'isot'
                    test_name = str(now) + '_' + test_name

                    mkdir_check(properties['data_dir'] +
                                '/analysis/zeropoint/')
                    mkdir_check(output_path)

                    exp_time = ff.get_exp_time(image_path)

                    print('SExtractor catalogue path:', sextractor_path)
                    print('Image path:', image_path)
                    print('Catalogue name:', cat_name)
                    print('Catalogue path:', cat_path)
                    print('Class star column:', star_class_col)
                    print('Output:', output_path_cat)
                    print('Exposure time:', exp_time)
                    print("Use sextractor class star:", use_sex_star_class)

                    zeropoints["standard_field"][field][
                        cat_name] = photometry.determine_zeropoint_sextractor(
                            sextractor_cat=sextractor_path,
                            image=image_path,
                            cat_path=cat_path,
                            cat_name=cat_name,
                            output_path=output_path_cat,
                            show=show_plots,
                            cat_ra_col=cat_ra_col,
                            cat_dec_col=cat_dec_col,
                            cat_mag_col=cat_mag_col,
                            sex_ra_col=sex_ra_col,
                            sex_dec_col=sex_dec_col,
                            sex_x_col=sex_x_col,
                            sex_y_col=sex_y_col,
                            dist_tol=pix_tol,
                            flux_column=sex_flux_col,
                            mag_range_sex_upper=mag_range_sex_upper,
                            mag_range_sex_lower=mag_range_sex_lower,
                            stars_only=stars_only,
                            star_class_tol=star_class_tol,
                            star_class_col=star_class_col,
                            exp_time=exp_time,
                            y_lower=0,
                            cat_type=cat_type,
                        )

        output_dict = {f + '_zeropoints': zeropoints}
        p.add_output_values(obj=epoch, instrument='FORS2', params=output_dict)

    outputs = p.object_output_params(obj=epoch, instrument='fors2')

    output_path_final = output + "collated/"
    mkdir_check(output_path_final)

    print("Collating zeropoints...")

    for fil in filters:
        print(f"For {fil}:")
        f = fil[0]
        output_path_final_f = output_path_final + fil + "/"
        mkdir_check(output_path_final_f)
        zeropoints = outputs[f"{f}_zeropoints"]
        airmass_sci = outputs[f"{f}_airmass_mean"]
        airmass_sci_err = outputs[f"{f}_airmass_err"]
        extinction = outputs[f"{f}_extinction"]
        extinction_err = outputs[f"{f}_extinction_err"]
        zeropoint_tbl = table.Table(
            dtype=[("type", 'S15'), ("field", 'S25'), (
                "cat", 'S10'), ("zeropoint",
                                float), ("zeropoint_err",
                                         float), ("airmass",
                                                  float), ("airmass_err",
                                                           float),
                   ("extinction",
                    float), ("extinction_err",
                             float), ("zeropoint_ext_corr",
                                      float), ("zeropoint_ext_corr_err",
                                               float), ("n_matches", float)])

        if f"provided" in zeropoints:
            print(f"\tProvided:")
            zeropoint_prov = zeropoints["provided"]
            zeropoint_tbl.add_row([
                "provided", "N/A", "N/A", zeropoint_prov["zeropoint"],
                zeropoint_prov["zeropoint_err"], 0.0, 0.0, extinction,
                extinction_err, zeropoint_prov["zeropoint"],
                zeropoint_prov["zeropoint_err"], 0
            ])

        print(f"\tScience field:")
        for cat_name in zeropoints["science_field"]:
            print(f"\t\t{cat_name}")
            zeropoint_sci = zeropoints["science_field"][cat_name]

            if zeropoint_sci is not None:
                zeropoint_corrected = zeropoint_sci[
                    "zeropoint"] + extinction * airmass_sci

                print("\t\t\t", zeropoint_sci["zeropoint"], extinction,
                      airmass_sci)

                zeropoint_corrected_err = zeropoint_sci[
                    "zeropoint_err"] + error_product(
                        value=extinction * airmass_sci,
                        measurements=[extinction, airmass_sci],
                        errors=[extinction_err, airmass_sci_err])
                zp_cat = table.Table.read(zeropoint_sci["matches_cat_path"])
                zeropoint_tbl.add_row([
                    "science_field", epoch[:-2], cat_name,
                    zeropoint_sci["zeropoint"], zeropoint_sci["zeropoint_err"],
                    airmass_sci, airmass_sci_err, extinction, extinction_err,
                    zeropoint_corrected, zeropoint_corrected_err,
                    len(zp_cat)
                ])
                plt.scatter(zp_cat["mag_cat"], zp_cat["mag"], c='green')
                plt.errorbar(zp_cat["mag_cat"],
                             zp_cat["mag"],
                             yerr=zp_cat["mag_err"],
                             linestyle="None",
                             c='black')
                plt.plot(zp_cat["mag_cat"],
                         zp_cat["mag_cat"] - zeropoint_sci["zeropoint"],
                         c="violet")
                plt.title(f"{fil}: science-field, {cat_name}")
                plt.figtext(
                    0, 0.01,
                    f'zeropoint - kX: {zeropoint_sci["zeropoint"]} ± {zeropoint_sci["zeropoint_err"]}\n'
                    f'zeropoint: {zeropoint_corrected} ± {zeropoint_corrected_err}\n'
                    f'num stars: {len(zp_cat)}')
                plt.gca().set_aspect('equal', adjustable='box')
                plt.savefig(output_path_final_f +
                            f"zeropoint_science_{cat_name}.pdf")
                plt.show()
                plt.close()

        print("\tStandard fields:")
        for field in zeropoints["standard_field"]:
            print(f"\t\t{field}")
            for cat_name in zeropoints["standard_field"][field]:
                print(f"\t\t\t{cat_name}")
                zeropoint_std = zeropoints["standard_field"][field][cat_name]
                if zeropoint_std is not None:
                    zeropoint_corrected = zeropoint_std[
                        "zeropoint"] + extinction * zeropoint_std["airmass"]

                    print("\t\t\t\t", zeropoint_std["zeropoint"], extinction,
                          zeropoint_std["airmass"])

                    zeropoint_corrected_err = zeropoint_std[
                        "zeropoint_err"] + error_product(
                            value=extinction * zeropoint_std["airmass"],
                            measurements=[
                                extinction, zeropoint_std["airmass"]
                            ],
                            errors=[extinction_err, 0.0])
                    zp_cat = table.Table.read(
                        zeropoint_std["matches_cat_path"])
                    zeropoint_tbl.add_row([
                        "standard_field", field, cat_name,
                        zeropoint_std["zeropoint"],
                        zeropoint_std["zeropoint_err"],
                        zeropoint_std["airmass"], 0.0, extinction,
                        extinction_err, zeropoint_corrected,
                        zeropoint_corrected_err,
                        len(zp_cat)
                    ])
                    plt.scatter(zp_cat["mag_cat"], zp_cat["mag"], c='green')
                    plt.errorbar(zp_cat["mag_cat"],
                                 zp_cat["mag"],
                                 yerr=zp_cat["mag_err"],
                                 linestyle="None",
                                 c='black')
                    plt.plot(zp_cat["mag_cat"],
                             zp_cat["mag_cat"] - zeropoint_std["zeropoint"],
                             c="violet")
                    plt.title(f"{fil}: standard-field {field}, {cat_name}")
                    plt.gca().set_aspect('equal', adjustable='box')
                    plt.figtext(
                        0, 0.01,
                        f'zeropoint - kX: {zeropoint_std["zeropoint"]} ± {zeropoint_std["zeropoint_err"]}\n'
                        f'zeropoint: {zeropoint_corrected} ± {zeropoint_corrected_err}\n'
                        f'num stars: {len(zp_cat)}')
                    plt.savefig(output_path_final_f +
                                f"zeropoint_standard_{field}_{cat_name}.pdf")
                    plt.show()
                    plt.close()

        zeropoint_tbl["selection_index"] = zeropoint_tbl[
            "n_matches"] / zeropoint_tbl["zeropoint_ext_corr_err"]
        best_arg = np.argmax(zeropoint_tbl["selection_index"])
        print("Best zeropoint:")
        best_zeropoint = zeropoint_tbl[best_arg]
        print(best_zeropoint)

        zeropoints = outputs[f + '_zeropoints']
        zeropoints['best'] = {
            "zeropoint": float(best_zeropoint['zeropoint']),
            "zeropoint_err": float(best_zeropoint["zeropoint_err"]),
            "airmass": float(best_zeropoint["airmass"]),
            "airmass_err": float(best_zeropoint["airmass_err"]),
            "type": str(best_zeropoint["type"]),
            "catalogue": str(best_zeropoint["cat"])
        }
        output_dict = {f + '_zeropoints': zeropoints}
        p.add_output_values(obj=epoch, instrument='FORS2', params=output_dict)

        zeropoint_tbl.write(output_path_final_f + "zeropoints.csv",
                            format="ascii.csv",
                            overwrite=True)