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)
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)
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.")
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)
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)