Beispiel #1
0
def computeFlux(config, dir_path, ftpdetectinfo_path, shower_code, dt_beg, dt_end, timebin, mass_index, \
    timebin_intdt=0.25, ht_std_percent=5.0, mask=None):
    """ Compute flux using measurements in the given FTPdetectinfo file. 
    
    Arguments:
        config: [Config instance]
        dir_path: [str] Path to the working directory.
        ftpdetectinfo_path: [str] Path to a FTPdetectinfo file.
        shower_code: [str] IAU shower code (e.g. ETA, PER, SDA).
        dt_beg: [Datetime] Datetime object of the observation beginning.
        dt_end: [Datetime] Datetime object of the observation end.
        timebin: [float] Time bin in hours.
        mass_index: [float] Cumulative mass index of the shower.

    Keyword arguments:
        timebin_intdt: [float] Time step for computing the integrated collection area in hours. 15 minutes by
            default. If smaller than that, only one collection are will be computed.
        ht_std_percent: [float] Meteor height standard deviation in percent.
        mask: [Mask object] Mask object, None by default.

    """


    # Get a list of files in the night folder
    file_list = sorted(os.listdir(dir_path))



    # Find and load the platepar file
    if config.platepar_name in file_list:

        # Load the platepar
        platepar = Platepar.Platepar()
        platepar.read(os.path.join(dir_path, config.platepar_name), use_flat=config.use_flat)

    else:
        print("Cannot find the platepar file in the night directory: ", config.platepar_name)
        return None




    # # Load FTPdetectinfos
    # meteor_data = []
    # for ftpdetectinfo_path in ftpdetectinfo_list:

    #     if not os.path.isfile(ftpdetectinfo_path):
    #         print('No such file:', ftpdetectinfo_path)
    #         continue

    #     meteor_data += readFTPdetectinfo(*os.path.split(ftpdetectinfo_path))


    # Load meteor data from the FTPdetectinfo file
    meteor_data = readFTPdetectinfo(*os.path.split(ftpdetectinfo_path))

    if not len(meteor_data):
        print("No meteors in the FTPdetectinfo file!")
        return None




    # Find and load recalibrated platepars
    if config.platepars_recalibrated_name in file_list:
        with open(os.path.join(dir_path, config.platepars_recalibrated_name)) as f:
            recalibrated_platepars_dict = json.load(f)

            print("Recalibrated platepars loaded!")

    # If the file is not available, apply the recalibration procedure
    else:

        recalibrated_platepars_dict = applyRecalibrate(ftpdetectinfo_path, config)

        print("Recalibrated platepar file not available!")
        print("Recalibrating...")


    # Convert the dictionary of recalibrated platepars to a dictionary of Platepar objects
    recalibrated_platepars = {}
    for ff_name in recalibrated_platepars_dict:
        pp = Platepar.Platepar()
        pp.loadFromDict(recalibrated_platepars_dict[ff_name], use_flat=config.use_flat)

        recalibrated_platepars[ff_name] = pp


    # Compute nighly mean of the photometric zero point
    mag_lev_nightly_mean = np.mean([recalibrated_platepars[ff_name].mag_lev \
                                        for ff_name in recalibrated_platepars])




    # Locate and load the mask file
    if config.mask_file in file_list:
        mask_path = os.path.join(dir_path, config.mask_file)
        mask = loadMask(mask_path)
        print("Using mask:", mask_path)

    else:
        print("No mask used!")
        mask = None



    # Compute the population index using the classical equation
    population_index = 10**((mass_index - 1)/2.5)


    ### SENSOR CHARACTERIZATION ###
    # Computes FWHM of stars and noise profile of the sensor
    
    # File which stores the sensor characterization profile
    sensor_characterization_file = "flux_sensor_characterization.json"
    sensor_characterization_path = os.path.join(dir_path, sensor_characterization_file)

    # Load sensor characterization file if present, so the procedure can be skipped
    if os.path.isfile(sensor_characterization_path):

        # Load the JSON file
        with open(sensor_characterization_path) as f:
            
            data = " ".join(f.readlines())
            sensor_data = json.loads(data)

            # Remove the info entry
            if '-1' in sensor_data:
                del sensor_data['-1']

    else:

        # Run sensor characterization
        sensor_data = sensorCharacterization(config, dir_path)

        # Save to file for posterior use
        with open(sensor_characterization_path, 'w') as f:

            # Add an explanation what each entry means
            sensor_data_save = dict(sensor_data)
            sensor_data_save['-1'] = {"FF file name": ['median star FWHM', 'median background noise stddev']}

            # Convert collection areas to JSON
            out_str = json.dumps(sensor_data_save, indent=4, sort_keys=True)

            # Save to disk
            f.write(out_str)



    # Compute the nighly mean FWHM and noise stddev
    fwhm_nightly_mean = np.mean([sensor_data[key][0] for key in sensor_data])
    stddev_nightly_mean = np.mean([sensor_data[key][1] for key in sensor_data])

    ### ###



    # Perform shower association
    associations, shower_counts = showerAssociation(config, [ftpdetectinfo_path], shower_code=shower_code, \
        show_plot=False, save_plot=False, plot_activity=False)

    # If there are no shower association, return nothing
    if not associations:
        print("No meteors associated with the shower!")
        return None


    # Print the list of used meteors
    peak_mags = []
    for key in associations:
        meteor, shower = associations[key]

        if shower is not None:

            # Compute peak magnitude
            peak_mag = np.min(meteor.mag_array)

            peak_mags.append(peak_mag)

            print("{:.6f}, {:3s}, {:+.2f}".format(meteor.jdt_ref, shower.name, peak_mag))

    print()


    # Init the flux configuration
    flux_config = FluxConfig()



    ### COMPUTE COLLECTION AREAS ###

    # Make a file name to save the raw collection areas
    col_areas_file_name = generateColAreaJSONFileName(platepar.station_code, flux_config.side_points, \
        flux_config.ht_min, flux_config.ht_max, flux_config.dht, flux_config.elev_limit)

    # Check if the collection area file exists. If yes, load the data. If not, generate collection areas
    if col_areas_file_name in os.listdir(dir_path):
        col_areas_ht = loadRawCollectionAreas(dir_path, col_areas_file_name)

        print("Loaded collection areas from:", col_areas_file_name)

    else:

        # Compute the collecting areas segments per height
        col_areas_ht = collectingArea(platepar, mask=mask, side_points=flux_config.side_points, \
            ht_min=flux_config.ht_min, ht_max=flux_config.ht_max, dht=flux_config.dht, \
            elev_limit=flux_config.elev_limit)

        # Save the collection areas to file
        saveRawCollectionAreas(dir_path, col_areas_file_name, col_areas_ht)

        print("Saved raw collection areas to:", col_areas_file_name)


    ### ###



    # Compute the pointing of the middle of the FOV
    _, ra_mid, dec_mid, _ = xyToRaDecPP([jd2Date(J2000_JD.days)], [platepar.X_res/2], [platepar.Y_res/2], \
        [1], platepar, extinction_correction=False)
    azim_mid, elev_mid = raDec2AltAz(ra_mid[0], dec_mid[0], J2000_JD.days, platepar.lat, platepar.lon)

    # Compute the range to the middle point
    ref_ht = 100000
    r_mid, _, _, _ = xyHt2Geo(platepar, platepar.X_res/2, platepar.Y_res/2, ref_ht, indicate_limit=True, \
        elev_limit=flux_config.elev_limit)


    ### Compute the average angular velocity to which the flux variation throught the night will be normalized 
    #   The ang vel is of the middle of the FOV in the middle of observations

    # Middle Julian date of the night
    jd_night_mid = (datetime2JD(dt_beg) + datetime2JD(dt_end))/2

    # Compute the apparent radiant
    ra, dec, v_init = shower.computeApparentRadiant(platepar.lat, platepar.lon, jd_night_mid)

    # Compute the radiant elevation
    radiant_azim, radiant_elev = raDec2AltAz(ra, dec, jd_night_mid, platepar.lat, platepar.lon)

    # Compute the angular velocity in the middle of the FOV
    rad_dist_night_mid = angularSeparation(np.radians(radiant_azim), np.radians(radiant_elev), 
                np.radians(azim_mid), np.radians(elev_mid))
    ang_vel_night_mid = v_init*np.sin(rad_dist_night_mid)/r_mid

    ###




    # Compute the average limiting magnitude to which all flux will be normalized

    # Standard deviation of star PSF, nightly mean (px)
    star_stddev = fwhm_nightly_mean/2.355

    # Compute the theoretical stellar limiting magnitude (nightly average)
    star_sum = 2*np.pi*(config.k1_det*stddev_nightly_mean + config.j1_det)*star_stddev**2
    lm_s_nightly_mean = -2.5*np.log10(star_sum) + mag_lev_nightly_mean

    # A meteor needs to be visible on at least 4 frames, thus it needs to have at least 4x the mass to produce
    #   that amount of light. 1 magnitude difference scales as -0.4 of log of mass, thus:
    frame_min_loss = np.log10(config.line_minimum_frame_range_det)/(-0.4)

    lm_s_nightly_mean += frame_min_loss

    # Compute apparent meteor magnitude
    lm_m_nightly_mean = lm_s_nightly_mean - 5*np.log10(r_mid/1e5) - 2.5*np.log10( \
        np.degrees(platepar.F_scale*v_init*np.sin(rad_dist_night_mid)/(config.fps*r_mid*fwhm_nightly_mean)) \
        )

    #
    print("Stellar lim mag using detection thresholds:", lm_s_nightly_mean)
    print("Apparent meteor limiting magnitude:", lm_m_nightly_mean)


    ### Apply time-dependent corrections ###

    sol_data = []
    flux_lm_6_5_data = []

    # Go through all time bins within the observation period
    total_time_hrs = (dt_end - dt_beg).total_seconds()/3600
    nbins = int(np.ceil(total_time_hrs/timebin))
    for t_bin in range(nbins):

        # Compute bin start and end time
        bin_dt_beg = dt_beg + datetime.timedelta(hours=timebin*t_bin)
        bin_dt_end = bin_dt_beg + datetime.timedelta(hours=timebin)

        if bin_dt_end > dt_end:
            bin_dt_end = dt_end


        # Compute bin duration in hours
        bin_hours = (bin_dt_end - bin_dt_beg).total_seconds()/3600

        # Convert to Julian date
        bin_jd_beg = datetime2JD(bin_dt_beg)
        bin_jd_end = datetime2JD(bin_dt_end)

        # Only select meteors in this bin
        bin_meteors = []
        bin_ffs = []
        for key in associations:
            meteor, shower = associations[key]

            if shower is not None:
                if (shower.name == shower_code) and (meteor.jdt_ref > bin_jd_beg) \
                    and (meteor.jdt_ref <= bin_jd_end):
                    
                    bin_meteors.append([meteor, shower])
                    bin_ffs.append(meteor.ff_name)



        if len(bin_meteors) > 0:


            ### Compute the radiant elevation at the middle of the time bin ###

            jd_mean = (bin_jd_beg + bin_jd_end)/2

            # Compute the mean solar longitude
            sol_mean = np.degrees(jd2SolLonSteyaert(jd_mean))

            print()
            print()
            print("-- Bin information ---")
            print("Bin beg:", bin_dt_beg)
            print("Bin end:", bin_dt_end)
            print("Sol mid: {:.5f}".format(sol_mean))
            print("Meteors:", len(bin_meteors))

            # Compute the apparent radiant
            ra, dec, v_init = shower.computeApparentRadiant(platepar.lat, platepar.lon, jd_mean)

            # Compute the mean meteor height
            meteor_ht_beg = heightModel(v_init, ht_type='beg')
            meteor_ht_end = heightModel(v_init, ht_type='end')
            meteor_ht = (meteor_ht_beg + meteor_ht_end)/2

            # Compute the standard deviation of the height
            meteor_ht_std = meteor_ht*ht_std_percent/100.0

            # Init the Gaussian height distribution
            meteor_ht_gauss = scipy.stats.norm(meteor_ht, meteor_ht_std)


            # Compute the radiant elevation
            radiant_azim, radiant_elev = raDec2AltAz(ra, dec, jd_mean, platepar.lat, platepar.lon)

            ### ###


            ### Weight collection area by meteor height distribution ###

            # Determine weights for each height
            weight_sum = 0
            weights = {}
            for ht in col_areas_ht:
                wt = meteor_ht_gauss.pdf(float(ht))
                weight_sum += wt
                weights[ht] = wt

            # Normalize the weights so that the sum is 1
            for ht in weights:
                weights[ht] /= weight_sum

            ### ###


            # Compute the angular velocity in the middle of the FOV
            rad_dist_mid = angularSeparation(np.radians(radiant_azim), np.radians(radiant_elev), 
                        np.radians(azim_mid), np.radians(elev_mid))
            ang_vel_mid = v_init*np.sin(rad_dist_mid)/r_mid



            ### Compute the limiting magnitude ###

            # Compute the mean star FWHM in the given bin
            fwhm_bin_mean = np.mean([sensor_data[ff_name][0] for ff_name in bin_ffs])

            # Compute the mean background stddev in the given bin
            stddev_bin_mean = np.mean([sensor_data[ff_name][1] for ff_name in bin_ffs])

            # Compute the mean photometric zero point in the given bin
            mag_lev_bin_mean = np.mean([recalibrated_platepars[ff_name].mag_lev for ff_name in bin_ffs if ff_name in recalibrated_platepars])



            # Standard deviation of star PSF, nightly mean (px)
            star_stddev = fwhm_bin_mean/2.355

            # Compute the theoretical stellar limiting magnitude (nightly average)
            star_sum = 2*np.pi*(config.k1_det*stddev_bin_mean + config.j1_det)*star_stddev**2
            lm_s = -2.5*np.log10(star_sum) + mag_lev_bin_mean
            lm_s += frame_min_loss

            # Compute apparent meteor magnitude
            lm_m = lm_s - 5*np.log10(r_mid/1e5) - 2.5*np.log10( \
                    np.degrees(platepar.F_scale*v_init*np.sin(rad_dist_mid)/(config.fps*r_mid*fwhm_bin_mean))\
                    )

            ### ###


            # Final correction area value (height-weightned)
            collection_area = 0

            # Go through all heights and segment blocks
            for ht in col_areas_ht:
                for img_coords in col_areas_ht[ht]:

                    x_mean, y_mean = img_coords

                    # Unpack precomputed values
                    area, azim, elev, sensitivity_ratio, r = col_areas_ht[ht][img_coords]


                    # Compute the angular velocity (rad/s) in the middle of this block
                    rad_dist = angularSeparation(np.radians(radiant_azim), np.radians(radiant_elev), 
                        np.radians(azim), np.radians(elev))
                    ang_vel = v_init*np.sin(rad_dist)/r


                    # Compute the range correction
                    range_correction = (1e5/r)**2

                    #ang_vel_correction = ang_vel/ang_vel_mid
                    # Compute angular velocity correction relative to the nightly mean
                    ang_vel_correction = ang_vel/ang_vel_night_mid


                    ### Apply corrections

                    correction_ratio = 1.0
                    
                    # Correct the area for vignetting and extinction
                    correction_ratio *= sensitivity_ratio

                    # Correct for the range
                    correction_ratio *= range_correction

                    # Correct for the radiant elevation
                    correction_ratio *= np.sin(np.radians(radiant_elev))

                    # Correct for angular velocity
                    correction_ratio *= ang_vel_correction


                    # Add the collection area to the final estimate with the height weight
                    #   Raise the correction to the mass index power
                    collection_area += weights[ht]*area*correction_ratio**(mass_index - 1)



            # Compute the flux at the bin LM (meteors/1000km^2/h)
            flux = 1e9*len(bin_meteors)/collection_area/bin_hours

            # Compute the flux scaled to the nightly mean LM
            flux_lm_nightly_mean = flux*population_index**(lm_m_nightly_mean - lm_m)

            # Compute the flux scaled to +6.5M
            flux_lm_6_5 = flux*population_index**(6.5 - lm_m)



            print("-- Sensor information ---")
            print("Star FWHM:  {:5.2f} px".format(fwhm_bin_mean))
            print("Bkg stddev: {:4.1f} ADU".format(stddev_bin_mean))
            print("Photom ZP:  {:+6.2f} mag".format(mag_lev_bin_mean))
            print("Stellar LM: {:+.2f} mag".format(lm_s))
            print("-- Flux ---")
            print("Col area: {:d} km^2".format(int(collection_area/1e6)))
            print("Ang vel:  {:.2f} deg/s".format(np.degrees(ang_vel_mid)))
            print("LM app:   {:+.2f} mag".format(lm_m))
            print("Flux:     {:.2f} meteors/1000km^2/h".format(flux))
            print("to {:+.2f}: {:.2f} meteors/1000km^2/h".format(lm_m_nightly_mean, flux_lm_nightly_mean))
            print("to +6.50: {:.2f} meteors/1000km^2/h".format(flux_lm_6_5))


            sol_data.append(sol_mean)
            flux_lm_6_5_data.append(flux_lm_6_5)


    # Print the results
    print("Solar longitude, Flux at LM +6.5:")
    for sol, flux_lm_6_5 in zip(sol_data, flux_lm_6_5_data):
        print("{:9.5f}, {:8.4f}".format(sol, flux_lm_6_5))

    # Plot a histogram of peak magnitudes
    plt.hist(peak_mags, cumulative=True)
    plt.show()
Beispiel #2
0
    arg_parser = argparse.ArgumentParser(
        description="""Compute the FOV area given the platepar and mask files. \
        """,
        formatter_class=argparse.RawTextHelpFormatter)

    arg_parser.add_argument('platepar', metavar='PLATEPAR', type=str, \
                    help="Path to the platepar file.")

    arg_parser.add_argument('mask', metavar='MASK', type=str, nargs='?', \
                    help="Path to the mask file.")

    # Parse the command line arguments
    cml_args = arg_parser.parse_args()

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

    # Load the platepar file
    pp = Platepar()
    pp.read(cml_args.platepar)

    # Load the mask file
    if cml_args.mask is not None:
        mask = loadMask(cml_args.mask)
    else:
        mask = None

    # Compute the FOV geo points
    area_list = fovArea(pp, mask)

    for side_points in area_list:
        print(side_points)
Beispiel #3
0
def computeFlux(config, dir_path, ftpdetectinfo_path, shower_code, dt_beg, dt_end, timebin, mass_index, \
    timebin_intdt=0.25, ht_std_percent=5.0, mask=None, show_plots=True):
    """ Compute flux using measurements in the given FTPdetectinfo file. 
    
    Arguments:
        config: [Config instance]
        dir_path: [str] Path to the working directory.
        ftpdetectinfo_path: [str] Path to a FTPdetectinfo file.
        shower_code: [str] IAU shower code (e.g. ETA, PER, SDA).
        dt_beg: [Datetime] Datetime object of the observation beginning.
        dt_end: [Datetime] Datetime object of the observation end.
        timebin: [float] Time bin in hours.
        mass_index: [float] Cumulative mass index of the shower.

    Keyword arguments:
        timebin_intdt: [float] Time step for computing the integrated collection area in hours. 15 minutes by
            default. If smaller than that, only one collection are will be computed.
        ht_std_percent: [float] Meteor height standard deviation in percent.
        mask: [Mask object] Mask object, None by default.
        show_plots: [bool] Show flux plots. True by default.

    Return:
        [tuple] sol_data, flux_lm_6_5_data
            - sol_data: [list] Array of solar longitudes (in degrees) of time bins.
            - flux_lm6_5_data: [list] Array of meteoroid flux at the limiting magnitude of +6.5 in 
                meteors/1000km^2/h.
    """


    # Get a list of files in the night folder
    file_list = sorted(os.listdir(dir_path))



    # Find and load the platepar file
    if config.platepar_name in file_list:

        # Load the platepar
        platepar = Platepar.Platepar()
        platepar.read(os.path.join(dir_path, config.platepar_name), use_flat=config.use_flat)

    else:
        print("Cannot find the platepar file in the night directory: ", config.platepar_name)
        return None




    # # Load FTPdetectinfos
    # meteor_data = []
    # for ftpdetectinfo_path in ftpdetectinfo_list:

    #     if not os.path.isfile(ftpdetectinfo_path):
    #         print('No such file:', ftpdetectinfo_path)
    #         continue

    #     meteor_data += readFTPdetectinfo(*os.path.split(ftpdetectinfo_path))


    # Load meteor data from the FTPdetectinfo file
    meteor_data = readFTPdetectinfo(*os.path.split(ftpdetectinfo_path))

    if not len(meteor_data):
        print("No meteors in the FTPdetectinfo file!")
        return None




    # Find and load recalibrated platepars
    if config.platepars_recalibrated_name in file_list:
        with open(os.path.join(dir_path, config.platepars_recalibrated_name)) as f:
            recalibrated_platepars_dict = json.load(f)

            print("Recalibrated platepars loaded!")

    # If the file is not available, apply the recalibration procedure
    else:

        recalibrated_platepars_dict = applyRecalibrate(ftpdetectinfo_path, config)

        print("Recalibrated platepar file not available!")
        print("Recalibrating...")


    # Convert the dictionary of recalibrated platepars to a dictionary of Platepar objects
    recalibrated_platepars = {}
    for ff_name in recalibrated_platepars_dict:
        pp = Platepar.Platepar()
        pp.loadFromDict(recalibrated_platepars_dict[ff_name], use_flat=config.use_flat)

        recalibrated_platepars[ff_name] = pp


    # Compute nighly mean of the photometric zero point
    mag_lev_nightly_mean = np.mean([recalibrated_platepars[ff_name].mag_lev \
                                        for ff_name in recalibrated_platepars])




    # Locate and load the mask file
    if config.mask_file in file_list:
        mask_path = os.path.join(dir_path, config.mask_file)
        mask = loadMask(mask_path)
        print("Using mask:", mask_path)

    else:
        print("No mask used!")
        mask = None



    # Compute the population index using the classical equation
    population_index = 10**((mass_index - 1)/2.5) # Found to be more consistent when comparing fluxes
    #population_index = 10**((mass_index - 1)/2.3) # TEST !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1


    ### SENSOR CHARACTERIZATION ###
    # Computes FWHM of stars and noise profile of the sensor
    
    # File which stores the sensor characterization profile
    sensor_characterization_file = "flux_sensor_characterization.json"
    sensor_characterization_path = os.path.join(dir_path, sensor_characterization_file)

    # Load sensor characterization file if present, so the procedure can be skipped
    if os.path.isfile(sensor_characterization_path):

        # Load the JSON file
        with open(sensor_characterization_path) as f:
            
            data = " ".join(f.readlines())
            sensor_data = json.loads(data)

            # Remove the info entry
            if '-1' in sensor_data:
                del sensor_data['-1']

    else:

        # Run sensor characterization
        sensor_data = sensorCharacterization(config, dir_path)

        # Save to file for posterior use
        with open(sensor_characterization_path, 'w') as f:

            # Add an explanation what each entry means
            sensor_data_save = dict(sensor_data)
            sensor_data_save['-1'] = {"FF file name": ['median star FWHM', 'median background noise stddev']}

            # Convert collection areas to JSON
            out_str = json.dumps(sensor_data_save, indent=4, sort_keys=True)

            # Save to disk
            f.write(out_str)



    # Compute the nighly mean FWHM and noise stddev
    fwhm_nightly_mean = np.mean([sensor_data[key][0] for key in sensor_data])
    stddev_nightly_mean = np.mean([sensor_data[key][1] for key in sensor_data])

    ### ###



    # Perform shower association
    associations, _ = showerAssociation(config, [ftpdetectinfo_path], shower_code=shower_code, \
        show_plot=False, save_plot=False, plot_activity=False)

    # Init the flux configuration
    flux_config = FluxConfig()


    # Remove all meteors which begin below the limit height
    filtered_associations = {}
    for key in associations:
        meteor, shower = associations[key]

        if meteor.beg_alt > flux_config.elev_limit:
            print("Rejecting:", meteor.jdt_ref)
            filtered_associations[key] = [meteor, shower]

    associations = filtered_associations



    # If there are no shower association, return nothing
    if not associations:
        print("No meteors associated with the shower!")
        return None


    # Print the list of used meteors
    peak_mags = []
    for key in associations:
        meteor, shower = associations[key]

        if shower is not None:

            # Compute peak magnitude
            peak_mag = np.min(meteor.mag_array)

            peak_mags.append(peak_mag)

            print("{:.6f}, {:3s}, {:+.2f}".format(meteor.jdt_ref, shower.name, peak_mag))

    print()



    ### COMPUTE COLLECTION AREAS ###

    # Make a file name to save the raw collection areas
    col_areas_file_name = generateColAreaJSONFileName(platepar.station_code, flux_config.side_points, \
        flux_config.ht_min, flux_config.ht_max, flux_config.dht, flux_config.elev_limit)

    # Check if the collection area file exists. If yes, load the data. If not, generate collection areas
    if col_areas_file_name in os.listdir(dir_path):
        col_areas_ht = loadRawCollectionAreas(dir_path, col_areas_file_name)

        print("Loaded collection areas from:", col_areas_file_name)

    else:

        # Compute the collecting areas segments per height
        col_areas_ht = collectingArea(platepar, mask=mask, side_points=flux_config.side_points, \
            ht_min=flux_config.ht_min, ht_max=flux_config.ht_max, dht=flux_config.dht, \
            elev_limit=flux_config.elev_limit)

        # Save the collection areas to file
        saveRawCollectionAreas(dir_path, col_areas_file_name, col_areas_ht)

        print("Saved raw collection areas to:", col_areas_file_name)


    ### ###


    # Compute the raw collection area at the height of 100 km
    col_area_100km_raw = 0
    col_areas_100km_blocks = col_areas_ht[100000.0]
    for block in col_areas_100km_blocks:
        col_area_100km_raw += col_areas_100km_blocks[block][0]

    print("Raw collection area at height of 100 km: {:.2f} km^2".format(col_area_100km_raw/1e6))


    # Compute the pointing of the middle of the FOV
    _, ra_mid, dec_mid, _ = xyToRaDecPP([jd2Date(J2000_JD.days)], [platepar.X_res/2], [platepar.Y_res/2], \
        [1], platepar, extinction_correction=False)
    azim_mid, elev_mid = raDec2AltAz(ra_mid[0], dec_mid[0], J2000_JD.days, platepar.lat, platepar.lon)

    # Compute the range to the middle point
    ref_ht = 100000
    r_mid, _, _, _ = xyHt2Geo(platepar, platepar.X_res/2, platepar.Y_res/2, ref_ht, indicate_limit=True, \
        elev_limit=flux_config.elev_limit)

    print("Range at 100 km in the middle of the image: {:.2f} km".format(r_mid/1000))


    ### Compute the average angular velocity to which the flux variation throught the night will be normalized 
    #   The ang vel is of the middle of the FOV in the middle of observations

    # Middle Julian date of the night
    jd_night_mid = (datetime2JD(dt_beg) + datetime2JD(dt_end))/2

    # Compute the apparent radiant
    ra, dec, v_init = shower.computeApparentRadiant(platepar.lat, platepar.lon, jd_night_mid)

    # Compute the radiant elevation
    radiant_azim, radiant_elev = raDec2AltAz(ra, dec, jd_night_mid, platepar.lat, platepar.lon)

    # Compute the angular velocity in the middle of the FOV
    rad_dist_night_mid = angularSeparation(np.radians(radiant_azim), np.radians(radiant_elev), 
                np.radians(azim_mid), np.radians(elev_mid))
    ang_vel_night_mid = v_init*np.sin(rad_dist_night_mid)/r_mid

    ###




    # Compute the average limiting magnitude to which all flux will be normalized

    # Standard deviation of star PSF, nightly mean (px)
    star_stddev = fwhm_nightly_mean/2.355

    # # Compute the theoretical stellar limiting magnitude (nightly average)
    # star_sum = 2*np.pi*(config.k1_det*stddev_nightly_mean + config.j1_det)*star_stddev**2
    # lm_s_nightly_mean = -2.5*np.log10(star_sum) + mag_lev_nightly_mean

    # Compute the theoretical stellar limiting magnitude using an empirical model (nightly average)
    lm_s_nightly_mean = stellarLMModel(mag_lev_nightly_mean)


    # A meteor needs to be visible on at least 4 frames, thus it needs to have at least 4x the mass to produce
    #   that amount of light. 1 magnitude difference scales as -0.4 of log of mass, thus:
    # frame_min_loss = np.log10(config.line_minimum_frame_range_det)/(-0.4)
    frame_min_loss = 0.0 # TEST !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!11

    print("Frame min loss: {:.2} mag".format(frame_min_loss))

    lm_s_nightly_mean += frame_min_loss

    # Compute apparent meteor magnitude
    lm_m_nightly_mean = lm_s_nightly_mean - 5*np.log10(r_mid/1e5) - 2.5*np.log10( \
        np.degrees(platepar.F_scale*v_init*np.sin(rad_dist_night_mid)/(config.fps*r_mid*fwhm_nightly_mean)) \
        )

    #
    print("Stellar lim mag using detection thresholds:", lm_s_nightly_mean)
    print("Apparent meteor limiting magnitude:", lm_m_nightly_mean)


    ### Apply time-dependent corrections ###

    # Track values used for flux
    sol_data = []
    flux_lm_6_5_data = []
    meteor_num_data = []
    effective_collection_area_data = []
    radiant_elev_data = []
    radiant_dist_mid_data = []
    ang_vel_mid_data = []
    lm_s_data = []
    lm_m_data = []
    sensitivity_corr_data = []
    range_corr_data = []
    radiant_elev_corr_data = []
    ang_vel_corr_data = []
    total_corr_data = []


    # Go through all time bins within the observation period
    total_time_hrs = (dt_end - dt_beg).total_seconds()/3600
    nbins = int(np.ceil(total_time_hrs/timebin))
    for t_bin in range(nbins):
        for subbin in range(flux_config.sub_time_bins):

            # Compute bin start and end time
            bin_dt_beg = dt_beg + datetime.timedelta(hours=(timebin*t_bin + timebin*subbin/flux_config.sub_time_bins))
            bin_dt_end = bin_dt_beg + datetime.timedelta(hours=timebin)

            if bin_dt_end > dt_end:
                bin_dt_end = dt_end


            # Compute bin duration in hours
            bin_hours = (bin_dt_end - bin_dt_beg).total_seconds()/3600

            # Convert to Julian date
            bin_jd_beg = datetime2JD(bin_dt_beg)
            bin_jd_end = datetime2JD(bin_dt_end)

                

            jd_mean = (bin_jd_beg + bin_jd_end)/2

            # Compute the mean solar longitude
            sol_mean = np.degrees(jd2SolLonSteyaert(jd_mean))

            ### Compute the radiant elevation at the middle of the time bin ###

            # Compute the apparent radiant
            ra, dec, v_init = shower.computeApparentRadiant(platepar.lat, platepar.lon, jd_mean)

            # Compute the mean meteor height
            meteor_ht_beg = heightModel(v_init, ht_type='beg')
            meteor_ht_end = heightModel(v_init, ht_type='end')
            meteor_ht = (meteor_ht_beg + meteor_ht_end)/2

            # Compute the standard deviation of the height
            meteor_ht_std = meteor_ht*ht_std_percent/100.0

            # Init the Gaussian height distribution
            meteor_ht_gauss = scipy.stats.norm(meteor_ht, meteor_ht_std)


            # Compute the radiant elevation
            radiant_azim, radiant_elev = raDec2AltAz(ra, dec, jd_mean, platepar.lat, platepar.lon)




            # Only select meteors in this bin and not too close to the radiant
            bin_meteors = []
            bin_ffs = []
            for key in associations:
                meteor, shower = associations[key]

                if shower is not None:
                    if (shower.name == shower_code) and (meteor.jdt_ref > bin_jd_beg) \
                        and (meteor.jdt_ref <= bin_jd_end):

                        # Filter out meteors ending too close to the radiant
                        if np.degrees(angularSeparation(np.radians(radiant_azim), np.radians(radiant_elev), \
                            np.radians(meteor.end_azim), np.radians(meteor.end_alt))) >= flux_config.rad_dist_min:
                        
                            bin_meteors.append([meteor, shower])
                            bin_ffs.append(meteor.ff_name)

            ### ###

            print()
            print()
            print("-- Bin information ---")
            print("Bin beg:", bin_dt_beg)
            print("Bin end:", bin_dt_end)
            print("Sol mid: {:.5f}".format(sol_mean))
            print("Radiant elevation: {:.2f} deg".format(radiant_elev))
            print("Apparent speed: {:.2f} km/s".format(v_init/1000))

            # If the elevation of the radiant is below the limit, skip this bin
            if radiant_elev < flux_config.rad_elev_limit:
                print("!!! Mean radiant elevation below {:.2f} deg threshold, skipping time bin!".format(flux_config.rad_elev_limit))
                continue

            # The minimum duration of the time bin should be larger than 50% of the given dt
            if bin_hours < 0.5*timebin:
                print("!!! Time bin duration of {:.2f} h is shorter than 0.5x of the time bin!".format(bin_hours))
                continue


            if len(bin_meteors) >= flux_config.meteros_min:
                print("Meteors:", len(bin_meteors))


                ### Weight collection area by meteor height distribution ###

                # Determine weights for each height
                weight_sum = 0
                weights = {}
                for ht in col_areas_ht:
                    wt = meteor_ht_gauss.pdf(float(ht))
                    weight_sum += wt
                    weights[ht] = wt

                # Normalize the weights so that the sum is 1
                for ht in weights:
                    weights[ht] /= weight_sum

                ### ###


                col_area_meteor_ht_raw = 0
                for ht in col_areas_ht:
                    for block in col_areas_ht[ht]:
                        col_area_meteor_ht_raw += weights[ht]*col_areas_ht[ht][block][0]

                print("Raw collection area at meteor heights: {:.2f} km^2".format(col_area_meteor_ht_raw/1e6))

                # Compute the angular velocity in the middle of the FOV
                rad_dist_mid = angularSeparation(np.radians(radiant_azim), np.radians(radiant_elev), 
                            np.radians(azim_mid), np.radians(elev_mid))
                ang_vel_mid = v_init*np.sin(rad_dist_mid)/r_mid



                ### Compute the limiting magnitude ###

                # Compute the mean star FWHM in the given bin
                fwhm_bin_mean = np.mean([sensor_data[ff_name][0] for ff_name in bin_ffs])

                # Compute the mean background stddev in the given bin
                stddev_bin_mean = np.mean([sensor_data[ff_name][1] for ff_name in bin_ffs])

                # Compute the mean photometric zero point in the given bin
                mag_lev_bin_mean = np.mean([recalibrated_platepars[ff_name].mag_lev for ff_name in bin_ffs if ff_name in recalibrated_platepars])



                # # Standard deviation of star PSF, nightly mean (px)
                # star_stddev = fwhm_bin_mean/2.355

                # Compute the theoretical stellar limiting magnitude (bin average)
                # star_sum = 2*np.pi*(config.k1_det*stddev_bin_mean + config.j1_det)*star_stddev**2
                # lm_s = -2.5*np.log10(star_sum) + mag_lev_bin_mean
                
                # Use empirical LM calculation
                lm_s = stellarLMModel(mag_lev_bin_mean)

                lm_s += frame_min_loss


                # ### TEST !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!11

                # # Artificialy increase limiting magnitude
                # lm_s += 1.2

                # #####

                # Compute apparent meteor magnitude
                lm_m = lm_s - 5*np.log10(r_mid/1e5) - 2.5*np.log10( \
                    np.degrees(platepar.F_scale*v_init*np.sin(rad_dist_mid)/(config.fps*r_mid*fwhm_bin_mean)))

                ### ###


                # Final correction area value (height-weightned)
                collection_area = 0

                # Keep track of the corrections
                sensitivity_corr_arr = []
                range_corr_arr = []
                radiant_elev_corr_arr = []
                ang_vel_corr_arr = []
                total_corr_arr = []
                col_area_raw_arr = []
                col_area_eff_arr = []
                col_area_eff_block_dict = {}

                # Go through all heights and segment blocks
                for ht in col_areas_ht:
                    for img_coords in col_areas_ht[ht]:

                        x_mean, y_mean = img_coords

                        # Unpack precomputed values
                        area, azim, elev, sensitivity_ratio, r = col_areas_ht[ht][img_coords]


                        # Compute the angular velocity (rad/s) in the middle of this block
                        rad_dist = angularSeparation(np.radians(radiant_azim), np.radians(radiant_elev), 
                            np.radians(azim), np.radians(elev))
                        ang_vel = v_init*np.sin(rad_dist)/r


                        # If the angular distance from the radiant is less than 15 deg, don't use the block
                        #   in the effective collection area
                        if np.degrees(rad_dist) < flux_config.rad_dist_min:
                            area = 0.0


                        # Compute the range correction
                        range_correction = (1e5/r)**2

                        #ang_vel_correction = ang_vel/ang_vel_mid
                        # Compute angular velocity correction relative to the nightly mean
                        ang_vel_correction = ang_vel/ang_vel_night_mid



                        ### Apply corrections

                        correction_ratio = 1.0
                        
                        # Correct the area for vignetting and extinction
                        sensitivity_corr_arr.append(sensitivity_ratio)
                        correction_ratio *= sensitivity_ratio


                        # Correct for the range (cap to an order of magnitude correction)
                        range_correction = max(range_correction, 0.1)
                        range_corr_arr.append(range_correction)
                        correction_ratio *= range_correction

                        # Correct for the radiant elevation (cap to an order of magnitude correction)
                        radiant_elev_correction = np.sin(np.radians(radiant_elev))
                        radiant_elev_correction = max(radiant_elev_correction, 0.1)
                        radiant_elev_corr_arr.append(radiant_elev_correction)
                        correction_ratio *= radiant_elev_correction


                        # Correct for angular velocity (cap to an order of magnitude correction)
                        ang_vel_correction = min(max(ang_vel_correction, 0.1), 10)
                        correction_ratio *= ang_vel_correction
                        ang_vel_corr_arr.append(ang_vel_correction)


                        # Add the collection area to the final estimate with the height weight
                        #   Raise the correction to the mass index power
                        total_correction = correction_ratio**(mass_index - 1)
                        total_correction = min(max(total_correction, 0.1), 10)
                        collection_area += weights[ht]*area*total_correction
                        total_corr_arr.append(total_correction)

                        col_area_raw_arr.append(weights[ht]*area)
                        col_area_eff_arr.append(weights[ht]*area*total_correction)

                        if img_coords not in col_area_eff_block_dict:
                            col_area_eff_block_dict[img_coords] = []

                        col_area_eff_block_dict[img_coords].append(weights[ht]*area*total_correction)




                # Compute mean corrections
                sensitivity_corr_avg = np.mean(sensitivity_corr_arr)
                range_corr_avg = np.mean(range_corr_arr)
                radiant_elev_corr_avg = np.mean(radiant_elev_corr_arr)
                ang_vel_corr_avg = np.mean(ang_vel_corr_arr)
                total_corr_avg = np.median(total_corr_arr)
                col_area_raw_sum = np.sum(col_area_raw_arr)
                col_area_eff_sum = np.sum(col_area_eff_arr)

                print("Raw collection area at meteor heights (CHECK): {:.2f} km^2".format(col_area_raw_sum/1e6))
                print("Eff collection area at meteor heights (CHECK): {:.2f} km^2".format(col_area_eff_sum/1e6))



                # ### PLOT HOW THE CORRECTION VARIES ACROSS THE FOV
                # x_arr = []
                # y_arr = []
                # col_area_eff_block_arr = []

                # for img_coords in col_area_eff_block_dict:
                    
                #     x_mean, y_mean = img_coords

                #     #if x_mean not in x_arr:
                #     x_arr.append(x_mean)
                #     #if y_mean not in y_arr:
                #     y_arr.append(y_mean)

                #     col_area_eff_block_arr.append(np.sum(col_area_eff_block_dict[img_coords]))

                # x_unique = np.unique(x_arr)
                # y_unique = np.unique(y_arr)
                # # plt.pcolormesh(x_arr, y_arr, np.array(col_area_eff_block_arr).reshape(len(x_unique), len(y_unique)).T, shading='auto')
                # plt.title("TOTAL = " + str(np.sum(col_area_eff_block_arr)/1e6))
                # plt.scatter(x_arr, y_arr, c=np.array(col_area_eff_block_arr)/1e6)
                # #plt.pcolor(np.array(x_arr).reshape(len(x_unique), len(y_unique)), np.array(y_arr).reshape(len(x_unique), len(y_unique)), np.array(col_area_eff_block_arr).reshape(len(x_unique), len(y_unique))/1e6)
                # plt.colorbar(label="km^2")
                # plt.gca().invert_yaxis()
                # plt.show()

                # ###


                # Compute the flux at the bin LM (meteors/1000km^2/h)
                flux = 1e9*len(bin_meteors)/collection_area/bin_hours

                # Compute the flux scaled to the nightly mean LM
                flux_lm_nightly_mean = flux*population_index**(lm_m_nightly_mean - lm_m)

                # Compute the flux scaled to +6.5M
                flux_lm_6_5 = flux*population_index**(6.5 - lm_m)



                print("-- Sensor information ---")
                print("Star FWHM:  {:5.2f} px".format(fwhm_bin_mean))
                print("Bkg stddev: {:4.1f} ADU".format(stddev_bin_mean))
                print("Photom ZP:  {:+6.2f} mag".format(mag_lev_bin_mean))
                print("Stellar LM: {:+.2f} mag".format(lm_s))
                print("-- Flux ---")
                print("Meteors:  {:d}".format(len(bin_meteors)))
                print("Col area: {:d} km^2".format(int(collection_area/1e6)))
                print("Ang vel:  {:.2f} deg/s".format(np.degrees(ang_vel_mid)))
                print("LM app:   {:+.2f} mag".format(lm_m))
                print("Flux:     {:.2f} meteors/1000km^2/h".format(flux))
                print("to {:+.2f}: {:.2f} meteors/1000km^2/h".format(lm_m_nightly_mean, flux_lm_nightly_mean))
                print("to +6.50: {:.2f} meteors/1000km^2/h".format(flux_lm_6_5))


                sol_data.append(sol_mean)
                flux_lm_6_5_data.append(flux_lm_6_5)
                meteor_num_data.append(len(bin_meteors))
                effective_collection_area_data.append(collection_area)
                radiant_elev_data.append(radiant_elev)
                radiant_dist_mid_data.append(np.degrees(rad_dist_mid))
                ang_vel_mid_data.append(np.degrees(ang_vel_mid))
                lm_s_data.append(lm_s)
                lm_m_data.append(lm_m)

                sensitivity_corr_data.append(sensitivity_corr_avg)
                range_corr_data.append(range_corr_avg)
                radiant_elev_corr_data.append(radiant_elev_corr_avg)
                ang_vel_corr_data.append(ang_vel_corr_avg)
                total_corr_data.append(total_corr_avg)


    # Print the results
    print("Solar longitude, Flux at LM +6.5:")
    for sol, flux_lm_6_5 in zip(sol_data, flux_lm_6_5_data):
        print("{:9.5f}, {:8.4f}".format(sol, flux_lm_6_5))


    if show_plots and len(sol_data):

        # Plot a histogram of peak magnitudes
        plt.hist(peak_mags, cumulative=True, log=True, bins=len(peak_mags), density=True)

        # Plot population index
        r_intercept = -0.7
        x_arr = np.linspace(np.min(peak_mags), np.percentile(peak_mags, 60))
        plt.plot(x_arr, 10**(np.log10(population_index)*x_arr + r_intercept))

        plt.title("r = {:.2f}".format(population_index))

        plt.show()


        # Plot how the derived values change throughout the night
        fig, axes \
            = plt.subplots(nrows=4, ncols=2, sharex=True, figsize=(10, 8))

        ((ax_met,      ax_lm),
         (ax_rad_elev, ax_corrs),
         (ax_rad_dist, ax_col_area),
         (ax_ang_vel,  ax_flux)) = axes


        fig.suptitle("{:s}, s = {:.2f}, r = {:.2f}".format(shower_code, mass_index, population_index))


        ax_met.scatter(sol_data, meteor_num_data)
        ax_met.set_ylabel("Meteors")

        ax_rad_elev.plot(sol_data, radiant_elev_data)
        ax_rad_elev.set_ylabel("Radiant elev (deg)")

        ax_rad_dist.plot(sol_data, radiant_dist_mid_data)
        ax_rad_dist.set_ylabel("Radiant dist (deg)")

        ax_ang_vel.plot(sol_data, ang_vel_mid_data)
        ax_ang_vel.set_ylabel("Ang vel (deg/s)")
        ax_ang_vel.set_xlabel("La Sun (deg)")


        ax_lm.plot(sol_data, lm_s_data, label="Stellar")
        ax_lm.plot(sol_data, lm_m_data, label="Meteor")
        ax_lm.set_ylabel("LM")
        ax_lm.legend()

        ax_corrs.plot(sol_data, sensitivity_corr_data, label="Sensitivity")
        ax_corrs.plot(sol_data, range_corr_data, label="Range")
        ax_corrs.plot(sol_data, radiant_elev_corr_data, label="Rad elev")
        ax_corrs.plot(sol_data, ang_vel_corr_data, label="Ang vel")
        ax_corrs.plot(sol_data, total_corr_data, label="Total (median)")
        ax_corrs.set_ylabel("Corrections")
        ax_corrs.legend()

        

        ax_col_area.plot(sol_data, np.array(effective_collection_area_data)/1e6)
        ax_col_area.plot(sol_data, len(sol_data)*[col_area_100km_raw/1e6], color='k', \
            label="Raw col area at 100 km")
        ax_col_area.plot(sol_data, len(sol_data)*[col_area_meteor_ht_raw/1e6], color='k', linestyle='dashed', \
            label="Raw col area at met ht")
        ax_col_area.set_ylabel("Eff. col. area (km^2)")
        ax_col_area.legend()

        ax_flux.scatter(sol_data, flux_lm_6_5_data)
        ax_flux.set_ylabel("Flux@+6.5M (met/1000km^2/h)")
        ax_flux.set_xlabel("La Sun (deg)")

        plt.tight_layout()

        plt.show()


    return sol_data, flux_lm_6_5_data
Beispiel #4
0
def processNight(night_data_dir, config, detection_results=None, nodetect=False):
    """ Given the directory with FF files, run detection and archiving. 
    
    Arguments:
        night_data_dir: [str] Path to the directory with FF files.
        config: [Config obj]

    Keyword arguments:
        detection_results: [list] An optional list of detection. If None (default), detection will be done
            on the the files in the folder.
        nodetect: [bool] True if detection should be skipped. False by default.

    Return:
        night_archive_dir: [str] Path to the night directory in ArchivedFiles.
        archive_name: [str] Path to the archive.
        detector: [QueuedPool instance] Handle to the detector.
    """

    # Remove final slash in the night dir
    if night_data_dir.endswith(os.sep):
        night_data_dir = night_data_dir[:-1]

    # Extract the name of the night
    night_data_dir_name = os.path.basename(os.path.abspath(night_data_dir))

    platepar = None
    kml_files = []
    recalibrated_platepars = None
    
    # If the detection should be run
    if (not nodetect):

        # If no detection was performed, run it
        if detection_results is None:

            # Run detection on the given directory
            calstars_name, ftpdetectinfo_name, ff_detected, \
                detector = detectStarsAndMeteorsDirectory(night_data_dir, config)

        # Otherwise, save detection results
        else:

            # Save CALSTARS and FTPdetectinfo to disk
            calstars_name, ftpdetectinfo_name, ff_detected = saveDetections(detection_results, \
                night_data_dir, config)

            # If the files were previously detected, there is no detector
            detector = None


        # Get the platepar file
        platepar, platepar_path, platepar_fmt = getPlatepar(config, night_data_dir)


        # Run calibration check and auto astrometry refinement
        if (platepar is not None) and (calstars_name is not None):

            # Read in the CALSTARS file
            calstars_list = CALSTARS.readCALSTARS(night_data_dir, calstars_name)

            # Run astrometry check and refinement
            platepar, fit_status = autoCheckFit(config, platepar, calstars_list)

            # If the fit was sucessful, apply the astrometry to detected meteors
            if fit_status:

                log.info('Astrometric calibration SUCCESSFUL!')

                # Save the refined platepar to the night directory and as default
                platepar.write(os.path.join(night_data_dir, config.platepar_name), fmt=platepar_fmt)
                platepar.write(platepar_path, fmt=platepar_fmt)

            else:
                log.info('Astrometric calibration FAILED!, Using old platepar for calibration...')


            # # Calculate astrometry for meteor detections
            # applyAstrometryFTPdetectinfo(night_data_dir, ftpdetectinfo_name, platepar_path)

            # If a flat is used, disable vignetting correction
            if config.use_flat:
                platepar.vignetting_coeff = 0.0

            log.info("Recalibrating astrometry on FF files with detections...")

            # Recalibrate astrometry on every FF file and apply the calibration to detections
            recalibrated_platepars = recalibrateIndividualFFsAndApplyAstrometry(night_data_dir, \
                os.path.join(night_data_dir, ftpdetectinfo_name), calstars_list, config, platepar)

            

            log.info("Converting RMS format to UFOOrbit format...")

            # Convert the FTPdetectinfo into UFOOrbit input file
            FTPdetectinfo2UFOOrbitInput(night_data_dir, ftpdetectinfo_name, platepar_path)



            # Generate a calibration report
            log.info("Generating a calibration report...")
            try:
                generateCalibrationReport(config, night_data_dir, platepar=platepar)

            except Exception as e:
                log.debug('Generating calibration report failed with the message:\n' + repr(e))
                log.debug(repr(traceback.format_exception(*sys.exc_info())))


            # Perform single station shower association
            log.info("Performing single station shower association...")
            try:
                showerAssociation(config, [os.path.join(night_data_dir, ftpdetectinfo_name)], \
                    save_plot=True, plot_activity=True)

            except Exception as e:
                log.debug('Shower association failed with the message:\n' + repr(e))
                log.debug(repr(traceback.format_exception(*sys.exc_info())))



            # Generate the FOV KML file
            log.info("Generating a FOV KML file...")
            try:

                mask_path = None
                mask = None

                # Try loading the mask
                if os.path.exists(os.path.join(night_data_dir, config.mask_file)):
                    mask_path = os.path.join(night_data_dir, config.mask_file)

                # Try loading the default mask
                elif os.path.exists(config.mask_file):
                    mask_path = os.path.abspath(config.mask_file)

                # Load the mask if given
                if mask_path:
                    mask = loadMask(mask_path)

                if mask is not None:
                    log.info("Loaded mask: {:s}".format(mask_path))

                # Generate the KML (only the FOV is shown, without the station) - 100 km
                kml_file100 = fovKML(config, night_data_dir, platepar, mask=mask, plot_station=False, \
                    area_ht=100000)
                kml_files.append(kml_file100)


                # Generate the KML (only the FOV is shown, without the station) - 70 km
                kml_file70 = fovKML(config, night_data_dir, platepar, mask=mask, plot_station=False, \
                    area_ht=70000)
                kml_files.append(kml_file70)

                # Generate the KML (only the FOV is shown, without the station) - 25 km
                kml_file25 = fovKML(config, night_data_dir, platepar, mask=mask, plot_station=False, \
                    area_ht=25000)
                kml_files.append(kml_file25)



            except Exception as e:
                log.debug("Generating a FOV KML file failed with the message:\n" + repr(e))
                log.debug(repr(traceback.format_exception(*sys.exc_info())))


    else:
        ff_detected = []
        detector = None



    log.info('Plotting field sums...')

    # Plot field sums
    try:
        plotFieldsums(night_data_dir, config)

    except Exception as e:
        log.debug('Plotting field sums failed with message:\n' + repr(e))
        log.debug(repr(traceback.format_exception(*sys.exc_info())))



    # Archive all fieldsums to one archive
    archiveFieldsums(night_data_dir)


    # List for any extra files which will be copied to the night archive directory. Full paths have to be 
    #   given
    extra_files = []


    log.info('Making a flat...')

    # Make a new flat field image
    try:
        flat_img = makeFlat(night_data_dir, config)

    except Exception as e:
        log.debug('Making a flat failed with message:\n' + repr(e))
        log.debug(repr(traceback.format_exception(*sys.exc_info())))
        flat_img = None
        

    # If making flat was sucessfull, save it
    if flat_img is not None:

        # Save the flat in the night directory, to keep the operational flat updated
        flat_path = os.path.join(night_data_dir, os.path.basename(config.flat_file))
        saveImage(flat_path, flat_img)
        log.info('Flat saved to: ' + flat_path)

        # Copy the flat to the night's directory as well
        extra_files.append(flat_path)

    else:
        log.info('Making flat image FAILED!')


    ### Add extra files to archive

    # Add the config file to the archive too
    extra_files.append(config.config_file_name)

    # Add the mask
    if (not nodetect):
        if os.path.exists(config.mask_file):
            mask_path = os.path.abspath(config.mask_file)
            extra_files.append(mask_path)


    # Add the platepar to the archive if it exists
    if (not nodetect):
        if os.path.exists(platepar_path):
            extra_files.append(platepar_path)


    # Add the json file with recalibrated platepars to the archive
    if (not nodetect):
        recalibrated_platepars_path = os.path.join(night_data_dir, config.platepars_recalibrated_name)
        if os.path.exists(recalibrated_platepars_path):
            extra_files.append(recalibrated_platepars_path)

    # Add the FOV KML files
    if len(kml_files):
        extra_files += kml_files



    # If FFs are not uploaded, choose two to upload
    if config.upload_mode > 1:
    
        # If all FF files are not uploaded, add two FF files which were successfuly recalibrated
        recalibrated_ffs = []
        for ff_name in recalibrated_platepars:

            pp = recalibrated_platepars[ff_name]

            # Check if the FF was recalibrated
            if pp.auto_recalibrated:
                recalibrated_ffs.append(os.path.join(night_data_dir, ff_name))

        # Choose two files randomly
        if len(recalibrated_ffs) > 2:
            extra_files += random.sample(recalibrated_ffs, 2)

        elif len(recalibrated_ffs) > 0:
            extra_files += recalibrated_ffs


        # If no were recalibrated
        else:

            # Create a list of all FF files
            ff_list = [os.path.join(night_data_dir, ff_name) for ff_name in os.listdir(night_data_dir) \
                if validFFName(ff_name)]

            # Add any two FF files
            extra_files += random.sample(ff_list, 2)
        

    ### ###



    # If the detection should be run
    if (not nodetect):

        # Make a CAL file and a special CAMS FTPdetectinfo if full CAMS compatibility is desired
        if (config.cams_code > 0) and (platepar is not None):

            log.info('Generating a CAMS FTPdetectinfo file...')

            # Write the CAL file to disk
            cal_file_name = writeCAL(night_data_dir, config, platepar)

            # Check if the CAL file was successfully generated
            if cal_file_name is not None:

                cams_code_formatted = "{:06d}".format(int(config.cams_code))

                # Load the FTPdetectinfo
                _, fps, meteor_list = readFTPdetectinfo(night_data_dir, ftpdetectinfo_name, \
                    ret_input_format=True)

                # Replace the camera code with the CAMS code
                for met in meteor_list:

                    # Replace the station name and the FF file format
                    ff_name = met[0]
                    ff_name = ff_name.replace('.fits', '.bin')
                    ff_name = ff_name.replace(config.stationID, cams_code_formatted)
                    met[0] = ff_name


                # Write the CAMS compatible FTPdetectinfo file
                writeFTPdetectinfo(meteor_list, night_data_dir, \
                    ftpdetectinfo_name.replace(config.stationID, cams_code_formatted),\
                    night_data_dir, cams_code_formatted, fps, calibration=cal_file_name, \
                    celestial_coords_given=(platepar is not None))



    night_archive_dir = os.path.join(os.path.abspath(config.data_dir), config.archived_dir, 
        night_data_dir_name)


    log.info('Archiving detections to ' + night_archive_dir)
    
    # Archive the detections
    archive_name = archiveDetections(night_data_dir, night_archive_dir, ff_detected, config, \
        extra_files=extra_files)


    return night_archive_dir, archive_name, detector
Beispiel #5
0
            # Locate mask
            if file_name == config.mask_file:
                mask_path = os.path.join(dir_path, file_name)

        if platepar_path is None:
            print("No platepar find was found in {:s}!".format(dir_path))
            sys.exit()

        else:
            print("Found platepar!")

    # Load the platepar file
    pp = Platepar()
    pp.read(platepar_path)

    # Assign mask
    mask_path = None
    if cml_args.mask is not None:
        mask_path = cml_args.mask

    # Load the mask file
    if mask_path is not None:
        mask = loadMask(mask_path)
        print("Loading mask:", mask_path)
    else:
        mask = None

    # Generate a KML file from the platepar
    fovKML(dir_path, pp, mask, area_ht=1000*cml_args.elev, side_points=cml_args.pts, \
        plot_station=cml_args.station)
Beispiel #6
0
def trackStack(dir_path,
               config,
               border=5,
               background_compensation=True,
               hide_plot=False):
    """ Generate a stack with aligned stars, so the sky appears static. The folder should have a
        platepars_all_recalibrated.json file.

    Arguments:
        dir_path: [str] Path to the directory with image files.
        config: [Config instance]

    Keyword arguments:
        border: [int] Border around the image to exclude (px).
        background_compensation: [bool] Normalize the background by applying a median filter to avepixel and
            use it as a flat field. Slows down the procedure and may sometimes introduce artifacts. True
            by default.
    """

    # Load recalibrated platepars, if they exist ###

    # Find recalibrated platepars file per FF file
    platepars_recalibrated_file = None
    for file_name in os.listdir(dir_path):
        if file_name == config.platepars_recalibrated_name:
            platepars_recalibrated_file = file_name
            break

    # Load all recalibrated platepars if the file is available
    recalibrated_platepars = None
    if platepars_recalibrated_file is not None:
        with open(os.path.join(dir_path, platepars_recalibrated_file)) as f:
            recalibrated_platepars = json.load(f)
            print(
                'Loaded recalibrated platepars JSON file for the calibration report...'
            )

    # ###

    # If the recalib platepars is not found, stop
    if recalibrated_platepars is None:
        print("The {:s} file was not found!".format(
            config.platepars_recalibrated_name))
        return False

    # Get a list of FF files in the folder
    ff_list = []
    for file_name in os.listdir(dir_path):
        if validFFName(file_name):
            ff_list.append(file_name)

    # Take the platepar with the middle time as the reference one
    ff_found_list = []
    jd_list = []
    for ff_name_temp in recalibrated_platepars:

        if ff_name_temp in ff_list:

            # Compute the Julian date of the FF middle
            dt = getMiddleTimeFF(ff_name_temp,
                                 config.fps,
                                 ret_milliseconds=True)
            jd = date2JD(*dt)

            jd_list.append(jd)
            ff_found_list.append(ff_name_temp)

    if len(jd_list) < 2:
        print("Not more than 1 FF image!")
        return False

    # Take the FF file with the middle JD
    jd_list = np.array(jd_list)
    jd_middle = np.mean(jd_list)
    jd_mean_index = np.argmin(np.abs(jd_list - jd_middle))
    ff_mid = ff_found_list[jd_mean_index]

    # Load the middle platepar as the reference one
    pp_ref = Platepar()
    pp_ref.loadFromDict(recalibrated_platepars[ff_mid],
                        use_flat=config.use_flat)

    # Try loading the mask
    mask_path = None
    if os.path.exists(os.path.join(dir_path, config.mask_file)):
        mask_path = os.path.join(dir_path, config.mask_file)

    # Try loading the default mask
    elif os.path.exists(config.mask_file):
        mask_path = os.path.abspath(config.mask_file)

    # Load the mask if given
    mask = None
    if mask_path is not None:
        mask = loadMask(mask_path)
        print("Loaded mask:", mask_path)

    # If the shape of the mask doesn't fit, init an empty mask
    if mask is not None:
        if (mask.img.shape[0] != pp_ref.Y_res) or (mask.img.shape[1] !=
                                                   pp_ref.X_res):
            print("Mask is of wrong shape!")
            mask = None

    if mask is None:
        mask = MaskStructure(255 + np.zeros(
            (pp_ref.Y_res, pp_ref.X_res), dtype=np.uint8))

    # Compute the middle RA/Dec of the reference platepar
    _, ra_temp, dec_temp, _ = xyToRaDecPP([jd2Date(jd_middle)],
                                          [pp_ref.X_res / 2],
                                          [pp_ref.Y_res / 2], [1],
                                          pp_ref,
                                          extinction_correction=False)

    ra_mid, dec_mid = ra_temp[0], dec_temp[0]

    # Go through all FF files and find RA/Dec of image corners to find the size of the stack image ###

    # List of corners
    x_corns = [0, pp_ref.X_res, 0, pp_ref.X_res]
    y_corns = [0, 0, pp_ref.Y_res, pp_ref.Y_res]

    ra_list = []
    dec_list = []

    for ff_temp in ff_found_list:

        # Load the recalibrated platepar
        pp_temp = Platepar()
        pp_temp.loadFromDict(recalibrated_platepars[ff_temp],
                             use_flat=config.use_flat)

        for x_c, y_c in zip(x_corns, y_corns):
            _, ra_temp, dec_temp, _ = xyToRaDecPP(
                [getMiddleTimeFF(ff_temp, config.fps, ret_milliseconds=True)],
                [x_c], [y_c], [1],
                pp_ref,
                extinction_correction=False)
            ra_c, dec_c = ra_temp[0], dec_temp[0]

            ra_list.append(ra_c)
            dec_list.append(dec_c)

    # Compute the angular separation from the middle equatorial coordinates of the reference image to all
    #   RA/Dec corner coordinates
    ang_sep_list = []
    for ra_c, dec_c in zip(ra_list, dec_list):
        ang_sep = np.degrees(
            angularSeparation(np.radians(ra_mid), np.radians(dec_mid),
                              np.radians(ra_c), np.radians(dec_c)))

        ang_sep_list.append(ang_sep)

    # Find the maximum angular separation and compute the image size using the plate scale
    #   The image size will be resampled to 1/2 of the original size to avoid interpolation
    scale = 0.5
    ang_sep_max = np.max(ang_sep_list)
    img_size = int(scale * 2 * ang_sep_max * pp_ref.F_scale)

    #

    # Create the stack platepar with no distortion and a large image size
    pp_stack = copy.deepcopy(pp_ref)
    pp_stack.resetDistortionParameters()
    pp_stack.X_res = img_size
    pp_stack.Y_res = img_size
    pp_stack.F_scale *= scale
    pp_stack.refraction = False

    # Init the image
    avg_stack_sum = np.zeros((img_size, img_size), dtype=float)
    avg_stack_count = np.zeros((img_size, img_size), dtype=int)
    max_deaveraged = np.zeros((img_size, img_size), dtype=np.uint8)

    # Load individual FFs and map them to the stack
    for i, ff_name in enumerate(ff_found_list):

        print("Stacking {:s}, {:.1f}% done".format(
            ff_name, 100 * i / len(ff_found_list)))

        # Read the FF file
        ff = readFF(dir_path, ff_name)

        # Load the recalibrated platepar
        pp_temp = Platepar()
        pp_temp.loadFromDict(recalibrated_platepars[ff_name],
                             use_flat=config.use_flat)

        # Make a list of X and Y image coordinates
        x_coords, y_coords = np.meshgrid(
            np.arange(border, pp_ref.X_res - border),
            np.arange(border, pp_ref.Y_res - border))
        x_coords = x_coords.ravel()
        y_coords = y_coords.ravel()

        # Map image pixels to sky
        jd_arr, ra_coords, dec_coords, _ = xyToRaDecPP(
            len(x_coords) *
            [getMiddleTimeFF(ff_name, config.fps, ret_milliseconds=True)],
            x_coords,
            y_coords,
            len(x_coords) * [1],
            pp_temp,
            extinction_correction=False)

        # Map sky coordinates to stack image coordinates
        stack_x, stack_y = raDecToXYPP(ra_coords, dec_coords, jd_middle,
                                       pp_stack)

        # Round pixel coordinates
        stack_x = np.round(stack_x, decimals=0).astype(int)
        stack_y = np.round(stack_y, decimals=0).astype(int)

        # Cut the image to limits
        filter_arr = (stack_x > 0) & (stack_x < img_size) & (stack_y > 0) & (
            stack_y < img_size)
        x_coords = x_coords[filter_arr].astype(int)
        y_coords = y_coords[filter_arr].astype(int)
        stack_x = stack_x[filter_arr]
        stack_y = stack_y[filter_arr]

        # Apply the mask to maxpixel and avepixel
        maxpixel = copy.deepcopy(ff.maxpixel)
        maxpixel[mask.img == 0] = 0
        avepixel = copy.deepcopy(ff.avepixel)
        avepixel[mask.img == 0] = 0

        # Compute deaveraged maxpixel
        max_deavg = maxpixel - avepixel

        # Normalize the backgroud brightness by applying a large-kernel median filter to avepixel
        if background_compensation:

            # # Apply a median filter to the avepixel to get an estimate of the background brightness
            # avepixel_median = scipy.ndimage.median_filter(ff.avepixel, size=101)
            avepixel_median = cv2.medianBlur(ff.avepixel, 301)

            # Make sure to avoid zero division
            avepixel_median[avepixel_median < 1] = 1

            # Normalize the avepixel by subtracting out the background brightness
            avepixel = avepixel.astype(float)
            avepixel /= avepixel_median
            avepixel *= 50  # Normalize to a good background value, which is usually 50
            avepixel = np.clip(avepixel, 0, 255)
            avepixel = avepixel.astype(np.uint8)

            # plt.imshow(avepixel, cmap='gray', vmin=0, vmax=255)
            # plt.show()

        # Add the average pixel to the sum
        avg_stack_sum[stack_y, stack_x] += avepixel[y_coords, x_coords]

        # Increment the counter image where the avepixel is not zero
        ones_img = np.ones_like(avepixel)
        ones_img[avepixel == 0] = 0
        avg_stack_count[stack_y, stack_x] += ones_img[y_coords, x_coords]

        # Set pixel values to the stack, only take the max values
        max_deaveraged[stack_y, stack_x] = np.max(np.dstack(
            [max_deaveraged[stack_y, stack_x], max_deavg[y_coords, x_coords]]),
                                                  axis=2)

    # Compute the blended avepixel background
    stack_img = avg_stack_sum
    stack_img[avg_stack_count > 0] /= avg_stack_count[avg_stack_count > 0]
    stack_img += max_deaveraged
    stack_img = np.clip(stack_img, 0, 255)
    stack_img = stack_img.astype(np.uint8)

    # Crop image
    non_empty_columns = np.where(stack_img.max(axis=0) > 0)[0]
    non_empty_rows = np.where(stack_img.max(axis=1) > 0)[0]
    crop_box = (np.min(non_empty_rows), np.max(non_empty_rows),
                np.min(non_empty_columns), np.max(non_empty_columns))
    stack_img = stack_img[crop_box[0]:crop_box[1] + 1,
                          crop_box[2]:crop_box[3] + 1]

    # Plot and save the stack ###

    dpi = 200
    plt.figure(figsize=(stack_img.shape[1] / dpi, stack_img.shape[0] / dpi),
               dpi=dpi)

    plt.imshow(stack_img,
               cmap='gray',
               vmin=0,
               vmax=256,
               interpolation='nearest')

    plt.axis('off')
    plt.gca().get_xaxis().set_visible(False)
    plt.gca().get_yaxis().set_visible(False)

    plt.xlim([0, stack_img.shape[1]])
    plt.ylim([stack_img.shape[0], 0])

    # Remove the margins (top and right are set to 0.9999, as setting them to 1.0 makes the image blank in
    #   some matplotlib versions)
    plt.subplots_adjust(left=0,
                        bottom=0,
                        right=0.9999,
                        top=0.9999,
                        wspace=0,
                        hspace=0)

    filenam = os.path.join(dir_path,
                           os.path.basename(dir_path) + "_track_stack.jpg")
    plt.savefig(filenam, bbox_inches='tight', pad_inches=0, dpi=dpi)

    #

    if hide_plot is False:
        plt.show()