def fitGC(self): """ Fits great circle to observations. """ self.cartesian_points = [] self.ra_array = np.array(self.ra_array) self.dec_array = np.array(self.dec_array) for ra, dec in zip(self.ra_array, self.dec_array): vect = vectNorm(raDec2Vector(ra, dec)) self.cartesian_points.append(vect) self.cartesian_points = np.array(self.cartesian_points) # Set begin and end pointing vectors self.beg_vect = self.cartesian_points[0] self.end_vect = self.cartesian_points[-1] # Compute alt of the begining and the last point self.beg_azim, self.beg_alt = raDec2AltAz(self.ra_array[0], self.dec_array[0], self.jd_array[0], \ self.lat, self.lon) self.end_azim, self.end_alt = raDec2AltAz(self.ra_array[-1], self.dec_array[-1], self.jd_array[-1], \ self.lat, self.lon) # Fit a great circle through observations x_arr, y_arr, z_arr = self.cartesian_points.T coeffs, self.theta0, self.phi0 = fitGreatCircle(x_arr, y_arr, z_arr) # Calculate the plane normal self.normal = np.array([coeffs[0], coeffs[1], -1.0]) # Norm the normal vector to unit length self.normal = vectNorm(self.normal) # Compute RA/Dec of the normal direction self.normal_ra, self.normal_dec = vector2RaDec(self.normal) # Take pointing directions of the beginning and the end of the meteor self.meteor_begin_cartesian = vectNorm(self.cartesian_points[0]) self.meteor_end_cartesian = vectNorm(self.cartesian_points[-1]) # Compute angular distance between begin and end (radians) self.ang_be = angularSeparationVect(self.beg_vect, self.end_vect) # Compute meteor duration in seconds self.duration = (self.jd_array[-1] - self.jd_array[0])*86400.0 # Set the reference JD as the JD of the beginning self.jdt_ref = self.jd_array[0] # Compute the solar longitude of the beginning (degrees) self.lasun = np.degrees(jd2SolLonSteyaert(self.jdt_ref))
def vector2RaDec(eci): """ Convert Earth-centered intertial vector to right ascension and declination. Arguments: eci: [3 element ndarray] Vector coordinates in Earth-centered inertial system Return: (ra, dec): [tuple of floats] right ascension and declinaton (degrees) """ # Normalize the ECI coordinates eci = vectNorm(eci) # Calculate declination dec = np.arcsin(eci[2]) # Calculate right ascension ra = np.arctan2(eci[1], eci[0]) % (2 * np.pi) return np.degrees(ra), np.degrees(dec)
def getThresholdedStripe3DPoints(config, img_handle, frame_min, frame_max, rho, theta, mask, flat_struct, \ dark, stripe_width_factor=1.0, centroiding=False, point1=None, point2=None, debug=False): """ Threshold the image and get a list of pixel positions and frames of threshold passers. This function handles all input types of data. Arguments; config: [config object] configuration object (loaded from the .config file). img_handle: [FrameInterface instance] Object which has a common interface to various input files. frame_min: [int] First frame to process. frame_max: [int] Last frame to process. rho: [float] Line distance from the center in HT space (pixels). theta: [float] Angle in degrees in HT space. mask: [ndarray] Image mask. flat_struct: [Flat struct] Structure containing the flat field. None by default. dark: [ndarray] Dark frame. Keyword arguments: stripe_width_factor: [float] Multipler by which the default stripe width will be multiplied. Default is 1.0 centroiding: [bool] If True, the indices will be returned in the centroiding mode, which means that point1 and point2 arguments must be given. point1: [list] (x, y, frame) Of the first reference point of the detection. point2: [list] (x, y, frame) Of the second reference point of the detection. debug: [bool] If True, extra debug messages and plots will be shown. Return: xs, ys, zs: [tuple of lists] Indices of (x, y, frame) of threshold passers for every frame. """ # Get indices of stripe pixels around the line of the meteor img_h, img_w = img_handle.ff.maxpixel.shape stripe_indices = getStripeIndices( rho, theta, stripe_width_factor * config.stripe_width, img_h, img_w) # If centroiding should be done, prepare everything for cutting out parts of the image for photometry if centroiding: # Compute the unit vector which describes the motion of the meteor in the image domain point1 = np.array(point1) point2 = np.array(point2) motion_vect = point2[:2] - point1[:2] motion_vect_unit = vectNorm(motion_vect) # Get coordinates of 2 points that describe the line x1, y1, z1 = point1 x2, y2, z2 = point2 # Compute the average angular velocity in px per frame ang_vel = np.sqrt((x2 - x1)**2 + (y2 - y1)**2) / (z2 - z1) # Compute the vector describing the length and direction of the meteor per frame motion_vect = ang_vel * motion_vect_unit # If the FF files is given, extract the points from FF after threshold if img_handle.input_type == 'ff': # Threshold the FF file img_thres = Image.thresholdFF(img_handle.ff, config.k1_det, config.j1_det, mask=mask, \ mask_ave_bright=False) # Extract the thresholded image by min and max frames from FF file img = selectFFFrames(np.copy(img_thres), img_handle.ff, frame_min, frame_max) # Remove lonely pixels img = morph.clean(img) # Extract the stripe from the thresholded image stripe = np.zeros(img.shape, img.dtype) stripe[stripe_indices] = img[stripe_indices] # Show stripe # show2("stripe", stripe*255) # Show 3D could # show3DCloud(ff, stripe) # Get stripe positions (x, y, frame) stripe_positions = stripe.nonzero() xs = stripe_positions[1] ys = stripe_positions[0] zs = img_handle.ff.maxframe[stripe_positions] return xs, ys, zs # If video frames are available, extract indices on all frames in the given range else: xs_array = [] ys_array = [] zs_array = [] # Go through all frames in the frame range for fr in range(frame_min, frame_max + 1): # Break the loop if outside frame size if fr == (img_handle.total_frames - 1): break # Set the frame number img_handle.setFrame(fr) # Load the frame fr_img = img_handle.loadFrame() # Apply the dark frame if dark is not None: fr_img = Image.applyDark(fr_img, dark) # Apply the flat to frame if flat_struct is not None: fr_img = Image.applyFlat(fr_img, flat_struct) # Mask the image fr_img = MaskImage.applyMask(fr_img, mask) # Threshold the frame img_thres = Image.thresholdImg(fr_img, img_handle.ff.avepixel, img_handle.ff.stdpixel, \ config.k1_det, config.j1_det, mask=mask, mask_ave_bright=False) # Remove lonely pixels img_thres = morph.clean(img_thres) # Extract the stripe from the thresholded image stripe = np.zeros(img_thres.shape, img_thres.dtype) stripe[stripe_indices] = img_thres[stripe_indices] # Include more pixels for centroiding and photometry and mask out per frame pixels if centroiding: # Dilate the pixels in the stripe twice, to include more pixels for photometry stripe = morph.dilate(stripe) stripe = morph.dilate(stripe) # Get indices of the stripe that is perpendicular to the meteor, and whose thickness is the # length of the meteor on this particular frame - this is called stripe_indices_motion # Compute the previous, current, and the next linear model position of the meteor on the # image model_pos_prev = point1[:2] + (fr - 1 - z1) * motion_vect model_pos = point1[:2] + (fr - z1) * motion_vect model_pos_next = point1[:2] + (fr + 1 - z1) * motion_vect # Get the rho, theta of the line perpendicular to the meteor line x_inters, y_inters = model_pos # Check if the previous, current or the next centroids are outside bounds, and if so, skip the # frame if (not checkCentroidBounds(model_pos_prev, img_w, img_h)) or \ (not checkCentroidBounds(model_pos, img_w, img_h)) or \ (not checkCentroidBounds(model_pos_next, img_w, img_h)): continue # Get parameters of the perpendicular line to the meteor line rho2, theta2 = htLinePerpendicular(rho, theta, x_inters, y_inters, img_h, img_w) # Compute the image indices of this position which will be the intersection with the stripe # The width of the line will be 2x the angular velocity stripe_length = 6 * ang_vel if stripe_length < stripe_width_factor * config.stripe_width: stripe_length = stripe_width_factor * config.stripe_width stripe_indices_motion = getStripeIndices( rho2, theta2, stripe_length, img_h, img_w) # Mark only those parts which overlap both lines, which effectively creates a mask for # photometry an centroiding, excluding other influences stripe_new = np.zeros_like(stripe) stripe_new[stripe_indices_motion] = stripe[ stripe_indices_motion] stripe = stripe_new if debug: # Show the extracted stripe img_stripe = np.zeros_like(stripe) img_stripe[stripe_indices] = 1 final_stripe = np.zeros_like(stripe) final_stripe[stripe_indices_motion] = img_stripe[ stripe_indices_motion] plt.imshow(final_stripe) plt.show() if debug and centroiding: print(fr) print('mean stdpixel3:', np.mean(img_handle.ff.stdpixel)) print('mean avepixel3:', np.mean(img_handle.ff.avepixel)) print('mean frame:', np.mean(fr_img)) fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True, sharey=True) fr_img_noavg = Image.applyDark(fr_img, img_handle.ff.avepixel) #fr_img_noavg = fr_img # Auto levels min_lvl = np.percentile(fr_img_noavg[2:, :], 1) max_lvl = np.percentile(fr_img_noavg[2:, :], 99.0) # Adjust levels fr_img_autolevel = Image.adjustLevels(fr_img_noavg, min_lvl, 1.0, max_lvl) ax1.imshow(stripe, cmap='gray') ax2.imshow(fr_img_autolevel, cmap='gray') plt.show() pass # Get stripe positions (x, y, frame) stripe_positions = stripe.nonzero() xs = stripe_positions[1] ys = stripe_positions[0] zs = np.zeros_like(xs) + fr # Add the points to the list xs_array.append(xs) ys_array.append(ys) zs_array.append(zs) if debug: print('---') print(stripe.nonzero()) print(xs, ys, zs) if len(xs_array) > 0: # Flatten the arrays xs_array = np.concatenate(xs_array) ys_array = np.concatenate(ys_array) zs_array = np.concatenate(zs_array) else: xs_array = np.array(xs_array) ys_array = np.array(ys_array) zs_array = np.array(zs_array) return xs_array, ys_array, zs_array
def geocentricToApparentRadiantAndVelocity(ra_g, dec_g, vg, lat, lon, elev, jd, include_rotation=True): """ Converts the geocentric into apparent meteor radiant and velocity. The conversion is not perfect as the zenith attraction correction should be done after the radiant has been derotated for Earth's velocity, but it's precise to about 0.1 deg. Arguments: ra_g: [float] Geocentric right ascension (deg). dec_g: [float] Declination (deg). vg: [float] Geocentric velocity (m/s). lat: [float] State vector latitude (deg) lon: [float] State vector longitude (deg). ele: [float] State vector elevation (meters). jd: [float] Julian date. Keyword arguments: include_rotation: [bool] Whether the velocity should be corrected for Earth's rotation. True by default. Return: (ra, dec, v_init): Apparent radiant (deg) and velocity (m/s). """ # Compute ECI coordinates of the meteor state vector state_vector = geo2Cartesian(lat, lon, elev, jd) eci_x, eci_y, eci_z = state_vector # Assume that the velocity at infinity corresponds to the initial velocity v_init = np.sqrt(vg**2 + (2 * 6.67408 * 5.9722) * 1e13 / vectMag(state_vector)) # Calculate the geocentric latitude (latitude which considers the Earth as an elipsoid) of the reference # trajectory point lat_geocentric = np.degrees( math.atan2(eci_z, math.sqrt(eci_x**2 + eci_y**2))) ### Uncorrect for zenith attraction ### # Compute the radiant in the local coordinates #eta, rho = raDec2EtaRho(ra_g, dec_g, lat_geocentric, lon, jd) azim, elev = raDec2AltAz(ra_g, dec_g, jd, lat_geocentric, lon) # Compute the zenith angle eta = np.radians(90.0 - elev) # Numerically correct for zenith attraction diff = 10e-5 zc = eta while diff > 10e-6: # Update the zenith distance zc -= diff # Calculate the zenith attraction correction delta_zc = 2 * math.atan( (v_init - vg) * math.tan(zc / 2.0) / (v_init + vg)) diff = zc + delta_zc - eta # Compute the uncorrected geocentric radiant for zenith attraction ra, dec = altAz2RADec(azim, 90.0 - np.degrees(zc), jd, lat_geocentric, lon) ### ### # Apply the rotation correction if include_rotation: # Calculate the velocity of the Earth rotation at the position of the reference trajectory point (m/s) v_e = 2 * math.pi * vectMag(state_vector) * math.cos( np.radians(lat_geocentric)) / 86164.09053 # Calculate the equatorial coordinates of east from the reference position on the trajectory azimuth_east = 90.0 altitude_east = 0 ra_east, dec_east = altAz2RADec(azimuth_east, altitude_east, jd, lat, lon) # Compute the radiant vector in ECI coordinates of the apparent radiant v_ref_vect = v_init * np.array(raDec2Vector(ra, dec)) v_ref_nocorr = np.zeros(3) # Calculate the derotated reference velocity vector/radiant v_ref_nocorr[0] = v_ref_vect[0] + v_e * np.cos(np.radians(ra_east)) v_ref_nocorr[1] = v_ref_vect[1] + v_e * np.sin(np.radians(ra_east)) v_ref_nocorr[2] = v_ref_vect[2] # Compute the radiant without Earth's rotation included ra_norot, dec_norot = vector2RaDec(vectNorm(v_ref_nocorr)) v_init_norot = vectMag(v_ref_nocorr) ra = ra_norot dec = dec_norot v_init = v_init_norot return ra, dec, v_init
def getThresholdedStripe3DPoints(config, img_handle, frame_min, frame_max, rho, theta, mask, flat_struct, \ dark, stripe_width_factor=1.0, centroiding=False, point1=None, point2=None, debug=False): """ Threshold the image and get a list of pixel positions and frames of threshold passers. This function handles all input types of data. Arguments; config: [config object] configuration object (loaded from the .config file). img_handle: [FrameInterface instance] Object which has a common interface to various input files. frame_min: [int] First frame to process. frame_max: [int] Last frame to process. rho: [float] Line distance from the center in HT space (pixels). theta: [float] Angle in degrees in HT space. mask: [ndarray] Image mask. flat_struct: [Flat struct] Structure containing the flat field. None by default. dark: [ndarray] Dark frame. Keyword arguments: stripe_width_factor: [float] Multipler by which the default stripe width will be multiplied. Default is 1.0 centroiding: [bool] If True, the indices will be returned in the centroiding mode, which means that point1 and point2 arguments must be given. point1: [list] (x, y, frame) Of the first reference point of the detection. point2: [list] (x, y, frame) Of the second reference point of the detection. debug: [bool] If True, extra debug messages and plots will be shown. Return: xs, ys, zs: [tuple of lists] Indices of (x, y, frame) of threshold passers for every frame. """ # Get indices of stripe pixels around the line of the meteor img_h, img_w = img_handle.ff.maxpixel.shape stripe_indices = getStripeIndices(rho, theta, stripe_width_factor*config.stripe_width, img_h, img_w) # If centroiding should be done, prepare everything for cutting out parts of the image for photometry if centroiding: # Compute the unit vector which describes the motion of the meteor in the image domain point1 = np.array(point1) point2 = np.array(point2) motion_vect = point2[:2] - point1[:2] motion_vect_unit = vectNorm(motion_vect) # Get coordinates of 2 points that describe the line x1, y1, z1 = point1 x2, y2, z2 = point2 # Compute the average angular velocity in px per frame ang_vel = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)/(z2 - z1) # Compute the vector describing the length and direction of the meteor per frame motion_vect = ang_vel*motion_vect_unit # If the FF files is given, extract the points from FF after threshold if img_handle.input_type == 'ff': # Threshold the FF file img_thres = Image.thresholdFF(img_handle.ff, config.k1_det, config.j1_det, mask=mask, \ mask_ave_bright=False) # Extract the thresholded image by min and max frames from FF file img = selectFFFrames(np.copy(img_thres), img_handle.ff, frame_min, frame_max) # Remove lonely pixels img = morph.clean(img) # Extract the stripe from the thresholded image stripe = np.zeros(img.shape, img.dtype) stripe[stripe_indices] = img[stripe_indices] # Show stripe # show2("stripe", stripe*255) # Show 3D could # show3DCloud(ff, stripe) # Get stripe positions (x, y, frame) stripe_positions = stripe.nonzero() xs = stripe_positions[1] ys = stripe_positions[0] zs = img_handle.ff.maxframe[stripe_positions] return xs, ys, zs # If video frames are available, extract indices on all frames in the given range else: xs_array = [] ys_array = [] zs_array = [] # Go through all frames in the frame range for fr in range(frame_min, frame_max + 1): # Break the loop if outside frame size if fr == (img_handle.total_frames - 1): break # Set the frame number img_handle.setFrame(fr) # Load the frame fr_img = img_handle.loadFrame() # Apply the dark frame if dark is not None: fr_img = Image.applyDark(fr_img, dark) # Apply the flat to frame if flat_struct is not None: fr_img = Image.applyFlat(fr_img, flat_struct) # Mask the image fr_img = MaskImage.applyMask(fr_img, mask) # Threshold the frame img_thres = Image.thresholdImg(fr_img, img_handle.ff.avepixel, img_handle.ff.stdpixel, \ config.k1_det, config.j1_det, mask=mask, mask_ave_bright=False) # Remove lonely pixels img_thres = morph.clean(img_thres) # Extract the stripe from the thresholded image stripe = np.zeros(img_thres.shape, img_thres.dtype) stripe[stripe_indices] = img_thres[stripe_indices] # Include more pixels for centroiding and photometry and mask out per frame pixels if centroiding: # Dilate the pixels in the stripe twice, to include more pixels for photometry stripe = morph.dilate(stripe) stripe = morph.dilate(stripe) # Get indices of the stripe that is perpendicular to the meteor, and whose thickness is the # length of the meteor on this particular frame - this is called stripe_indices_motion # Compute the previous, current, and the next linear model position of the meteor on the # image model_pos_prev = point1[:2] + (fr - 1 - z1)*motion_vect model_pos = point1[:2] + (fr - z1)*motion_vect model_pos_next = point1[:2] + (fr + 1 - z1)*motion_vect # Get the rho, theta of the line perpendicular to the meteor line x_inters, y_inters = model_pos # Check if the previous, current or the next centroids are outside bounds, and if so, skip the # frame if (not checkCentroidBounds(model_pos_prev, img_w, img_h)) or \ (not checkCentroidBounds(model_pos, img_w, img_h)) or \ (not checkCentroidBounds(model_pos_next, img_w, img_h)): continue # Get parameters of the perpendicular line to the meteor line rho2, theta2 = htLinePerpendicular(rho, theta, x_inters, y_inters, img_h, img_w) # Compute the image indices of this position which will be the intersection with the stripe # The width of the line will be 2x the angular velocity stripe_length = 6*ang_vel if stripe_length < stripe_width_factor*config.stripe_width: stripe_length = stripe_width_factor*config.stripe_width stripe_indices_motion = getStripeIndices(rho2, theta2, stripe_length, img_h, img_w) # Mark only those parts which overlap both lines, which effectively creates a mask for # photometry an centroiding, excluding other influences stripe_new = np.zeros_like(stripe) stripe_new[stripe_indices_motion] = stripe[stripe_indices_motion] stripe = stripe_new if debug: # Show the extracted stripe img_stripe = np.zeros_like(stripe) img_stripe[stripe_indices] = 1 final_stripe = np.zeros_like(stripe) final_stripe[stripe_indices_motion] = img_stripe[stripe_indices_motion] plt.imshow(final_stripe) plt.show() if debug and centroiding: print(fr) print('mean stdpixel3:', np.mean(img_handle.ff.stdpixel)) print('mean avepixel3:', np.mean(img_handle.ff.avepixel)) print('mean frame:', np.mean(fr_img)) fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True, sharey=True) fr_img_noavg = Image.applyDark(fr_img, img_handle.ff.avepixel) #fr_img_noavg = fr_img # Auto levels min_lvl = np.percentile(fr_img_noavg[2:, :], 1) max_lvl = np.percentile(fr_img_noavg[2:, :], 99.0) # Adjust levels fr_img_autolevel = Image.adjustLevels(fr_img_noavg, min_lvl, 1.0, max_lvl) ax1.imshow(stripe, cmap='gray') ax2.imshow(fr_img_autolevel, cmap='gray') plt.show() pass # Get stripe positions (x, y, frame) stripe_positions = stripe.nonzero() xs = stripe_positions[1] ys = stripe_positions[0] zs = np.zeros_like(xs) + fr # Add the points to the list xs_array.append(xs) ys_array.append(ys) zs_array.append(zs) if debug: print('---') print(stripe.nonzero()) print(xs, ys, zs) if len(xs_array) > 0: # Flatten the arrays xs_array = np.concatenate(xs_array) ys_array = np.concatenate(ys_array) zs_array = np.concatenate(zs_array) else: xs_array = np.array(xs_array) ys_array = np.array(ys_array) zs_array = np.array(zs_array) return xs_array, ys_array, zs_array
def showerAssociation(config, ftpdetectinfo_list, shower_code=None, show_plot=False, save_plot=False, \ plot_activity=False): """ Do single station shower association based on radiant direction and height. Arguments: config: [Config instance] ftpdetectinfo_list: [list] A list of paths to FTPdetectinfo files. Keyword arguments: shower_code: [str] Only use this one shower for association (e.g. ETA, PER, SDA). None by default, in which case all active showers will be associated. show_plot: [bool] Show the plot on the screen. False by default. save_plot: [bool] Save the plot in the folder with FTPdetectinfos. False by default. plot_activity: [bool] Whether to plot the shower activity plot of not. False by default. Return: associations, shower_counts: [tuple] - associations: [dict] A dictionary where the FF name and the meteor ordinal number on the FF file are keys, and the associated Shower object are values. - shower_counts: [list] A list of shower code and shower count pairs. """ # Load the list of meteor showers shower_list = loadShowers(config.shower_path, config.shower_file_name) # 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)) if not len(meteor_data): return {}, [] # Dictionary which holds FF names as keys and meteor measurements + associated showers as values associations = {} for meteor in meteor_data: ff_name, cam_code, meteor_No, n_segments, fps, hnr, mle, binn, px_fm, rho, phi, meteor_meas = meteor # Skip very short meteors if len(meteor_meas) < 4: continue # Check if the data is calibrated if not meteor_meas[0][0]: print( 'Data is not calibrated! Meteors cannot be associated to showers!' ) break # Init container for meteor observation meteor_obj = MeteorSingleStation(cam_code, config.latitude, config.longitude, ff_name) # Infill the meteor structure for entry in meteor_meas: calib_status, frame_n, x, y, ra, dec, azim, elev, inten, mag = entry # Compute the Julian data of every point jd = datetime2JD( filenameToDatetime(ff_name) + datetime.timedelta(seconds=float(frame_n) / fps)) meteor_obj.addPoint(jd, ra, dec, mag) # Fit the great circle and compute the geometrical parameters meteor_obj.fitGC() # Skip all meteors with beginning heights below 15 deg if meteor_obj.beg_alt < 15: continue # Go through all showers in the list and find the best match best_match_shower = None best_match_dist = np.inf for shower_entry in shower_list: # Extract shower parameters shower = Shower(shower_entry) # If the shower code was given, only check this one shower if shower_code is not None: if shower.name.lower() != shower_code.lower(): continue ### Solar longitude filter # If the shower doesn't have a stated beginning or end, check if the meteor is within a preset # threshold solar longitude difference if np.any(np.isnan([shower.lasun_beg, shower.lasun_end])): shower.lasun_beg = (shower.lasun_max - config.shower_lasun_threshold) % 360 shower.lasun_end = (shower.lasun_max + config.shower_lasun_threshold) % 360 # Filter out all showers which are not active if not isAngleBetween(np.radians(shower.lasun_beg), np.radians(meteor_obj.lasun), np.radians(shower.lasun_end)): continue ### ### ### Radiant filter ### # Assume a fixed meteor height for an approximate apparent radiant meteor_fixed_ht = 100000 # 100 km shower.computeApparentRadiant(config.latitude, config.longitude, meteor_obj.jdt_ref, \ meteor_fixed_ht=meteor_fixed_ht) # Compute the angle between the meteor radiant and the great circle normal radiant_separation = meteor_obj.angularSeparationFromGC( shower.ra, shower.dec) # Make sure the meteor is within the radiant distance threshold if radiant_separation > config.shower_max_radiant_separation: continue # Compute angle between the meteor's beginning and end, and the shower radiant shower.radiant_vector = vectNorm( raDec2Vector(shower.ra, shower.dec)) begin_separation = np.degrees(angularSeparationVect(shower.radiant_vector, \ meteor_obj.meteor_begin_cartesian)) end_separation = np.degrees(angularSeparationVect(shower.radiant_vector, \ meteor_obj.meteor_end_cartesian)) # Make sure the beginning of the meteor is closer to the radiant than it's end if begin_separation > end_separation: continue ### ### ### Height filter ### # Estimate the limiting meteor height from the velocity (meters) filter_beg_ht = heightModel(shower.v_init, ht_type='beg') filter_end_ht = heightModel(shower.v_init, ht_type='end') ### Estimate the meteor beginning height with +/- 1 frame, otherwise some short meteor may get ### rejected meteor_obj_orig = copy.deepcopy(meteor_obj) # Shorter meteor_obj_m1 = copy.deepcopy(meteor_obj_orig) meteor_obj_m1.duration -= 1.0 / config.fps meteor_beg_ht_m1 = estimateMeteorHeight(config, meteor_obj_m1, shower) # Nominal meteor_beg_ht = estimateMeteorHeight(config, meteor_obj_orig, shower) # Longer meteor_obj_p1 = copy.deepcopy(meteor_obj_orig) meteor_obj_p1.duration += 1.0 / config.fps meteor_beg_ht_p1 = estimateMeteorHeight(config, meteor_obj_p1, shower) meteor_obj = meteor_obj_orig ### ### # If all heights (even those with +/- 1 frame) are outside the height range, reject the meteor if ((meteor_beg_ht_p1 < filter_end_ht) or (meteor_beg_ht_p1 > filter_beg_ht)) and \ ((meteor_beg_ht < filter_end_ht) or (meteor_beg_ht > filter_beg_ht)) and \ ((meteor_beg_ht_m1 < filter_end_ht) or (meteor_beg_ht_m1 > filter_beg_ht)): continue ### ### # Compute the radiant elevation above the horizon shower.azim, shower.elev = raDec2AltAz(shower.ra, shower.dec, meteor_obj.jdt_ref, \ config.latitude, config.longitude) # Take the shower that's closest to the great circle if there are multiple candidates if radiant_separation < best_match_dist: best_match_dist = radiant_separation best_match_shower = copy.deepcopy(shower) # If a shower is given and the match is not this shower, skip adding the meteor to the list # If no specific shower is give for association, add all meteors if ((shower_code is not None) and (best_match_shower is not None)) or (shower_code is None): # Store the associated shower associations[(ff_name, meteor_No)] = [meteor_obj, best_match_shower] # Find shower frequency and sort by count shower_name_list_temp = [] shower_list_temp = [] for key in associations: _, shower = associations[key] if shower is None: shower_name = '...' else: shower_name = shower.name shower_name_list_temp.append(shower_name) shower_list_temp.append(shower) _, unique_showers_indices = np.unique(shower_name_list_temp, return_index=True) unique_shower_names = np.array( shower_name_list_temp)[unique_showers_indices] unique_showers = np.array(shower_list_temp)[unique_showers_indices] shower_counts = [[shower_obj, shower_name_list_temp.count(shower_name)] for shower_obj, \ shower_name in zip(unique_showers, unique_shower_names)] shower_counts = sorted(shower_counts, key=lambda x: x[1], reverse=True) # Create a plot of showers if show_plot or save_plot: # Generate consistent colours colors_by_name = makeShowerColors(shower_list) def get_shower_color(shower): try: return colors_by_name[shower.name] if shower else "0.4" except KeyError: return 'gray' # Init the figure plt.figure() # Init subplots depending on if the activity plot is done as well if plot_activity: gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1]) ax_allsky = plt.subplot(gs[0], facecolor='black') ax_activity = plt.subplot(gs[1], facecolor='black') else: ax_allsky = plt.subplot(111, facecolor='black') # Init the all-sky plot allsky_plot = AllSkyPlot(ax_handle=ax_allsky) # Plot all meteors for key in associations: meteor_obj, shower = associations[key] ### Plot the observed meteor points ### color = get_shower_color(shower) allsky_plot.plot(meteor_obj.ra_array, meteor_obj.dec_array, color=color, linewidth=1, zorder=4) # Plot the peak of shower meteors a different color peak_color = 'blue' if shower is not None: peak_color = 'tomato' allsky_plot.scatter(meteor_obj.ra_array[-1], meteor_obj.dec_array[-1], c=peak_color, marker='+', \ s=5, zorder=5) ### ### ### Plot fitted great circle points ### # Find the GC phase angle of the beginning of the meteor gc_beg_phase = meteor_obj.findGCPhase( meteor_obj.ra_array[0], meteor_obj.dec_array[0])[0] % 360 # If the meteor belongs to a shower, find the GC phase which ends at the shower if shower is not None: gc_end_phase = meteor_obj.findGCPhase(shower.ra, shower.dec)[0] % 360 # Fix 0/360 wrap if abs(gc_end_phase - gc_beg_phase) > 180: if gc_end_phase > gc_beg_phase: gc_end_phase -= 360 else: gc_beg_phase -= 360 gc_alpha = 1.0 else: # If it's a sporadic, find the direction to which the meteor should extend gc_end_phase = meteor_obj.findGCPhase(meteor_obj.ra_array[-1], \ meteor_obj.dec_array[-1])[0]%360 # Find the correct direction if (gc_beg_phase - gc_end_phase) % 360 > (gc_end_phase - gc_beg_phase) % 360: gc_end_phase = gc_beg_phase - 170 else: gc_end_phase = gc_beg_phase + 170 gc_alpha = 0.7 # Store great circle beginning and end phase meteor_obj.gc_beg_phase = gc_beg_phase meteor_obj.gc_end_phase = gc_end_phase # Get phases 180 deg before the meteor phase_angles = np.linspace(gc_end_phase, gc_beg_phase, 100) % 360 # Compute RA/Dec of points on the great circle ra_gc, dec_gc = meteor_obj.sampleGC(phase_angles) # Cull all points below the horizon azim_gc, elev_gc = raDec2AltAz(ra_gc, dec_gc, meteor_obj.jdt_ref, config.latitude, \ config.longitude) temp_arr = np.c_[ra_gc, dec_gc] temp_arr = temp_arr[elev_gc > 0] ra_gc, dec_gc = temp_arr.T # Plot the great circle fitted on the radiant gc_color = get_shower_color(shower) allsky_plot.plot(ra_gc, dec_gc, linestyle='dotted', color=gc_color, alpha=gc_alpha, linewidth=1) # Plot the point closest to the shower radiant if shower is not None: allsky_plot.plot(ra_gc[0], dec_gc[0], color='r', marker='+', ms=5, mew=1) # Store shower radiant point meteor_obj.radiant_ra = ra_gc[0] meteor_obj.radiant_dec = dec_gc[0] ### ### ### Plot all showers ### # Find unique showers and their apparent radiants computed at highest radiant elevation # (otherwise the apparent radiants can be quite off) shower_dict = {} for key in associations: meteor_obj, shower = associations[key] if shower is None: continue # If the shower name is in dict, find the shower with the highest radiant elevation if shower.name in shower_dict: if shower.elev > shower_dict[shower.name].elev: shower_dict[shower.name] = shower else: shower_dict[shower.name] = shower # Plot the location of shower radiants for shower_name in shower_dict: shower = shower_dict[shower_name] heading_arr = np.linspace(0, 360, 50) # Compute coordinates on a circle around the given RA, Dec ra_circle, dec_circle = sphericalPointFromHeadingAndDistance(shower.ra, shower.dec, \ heading_arr, config.shower_max_radiant_separation) # Plot the shower circle allsky_plot.plot(ra_circle, dec_circle, color=colors_by_name[shower_name]) # Plot the shower name x_text, y_text = allsky_plot.raDec2XY(shower.ra, shower.dec) allsky_plot.ax.text(x_text, y_text, shower.name, color='w', size=8, va='center', \ ha='center', zorder=6) # Plot station name and solar longiutde range allsky_plot.ax.text(-180, 89, "{:s}".format(cam_code), color='w', family='monospace') # Get a list of JDs of meteors jd_list = [associations[key][0].jdt_ref for key in associations] if len(jd_list): # Get the range of solar longitudes jd_min = min(jd_list) sol_min = np.degrees(jd2SolLonSteyaert(jd_min)) jd_max = max(jd_list) sol_max = np.degrees(jd2SolLonSteyaert(jd_max)) # Plot the date and solar longitude range date_sol_beg = u"Beg: {:s} (sol = {:.2f}\u00b0)".format( jd2Date(jd_min, dt_obj=True).strftime("%Y%m%d %H:%M:%S"), sol_min) date_sol_end = u"End: {:s} (sol = {:.2f}\u00b0)".format( jd2Date(jd_max, dt_obj=True).strftime("%Y%m%d %H:%M:%S"), sol_max) allsky_plot.ax.text(-180, 85, date_sol_beg, color='w', family='monospace') allsky_plot.ax.text(-180, 81, date_sol_end, color='w', family='monospace') allsky_plot.ax.text(-180, 77, "-" * len(date_sol_end), color='w', family='monospace') # Plot shower counts for i, (shower, count) in enumerate(shower_counts): if shower is not None: shower_name = shower.name else: shower_name = "..." allsky_plot.ax.text(-180, 73 - i*4, "{:s}: {:d}".format(shower_name, count), color='w', \ family='monospace') ### ### # Plot yearly meteor shower activity if plot_activity: # Plot the activity diagram generateActivityDiagram(config, shower_list, ax_handle=ax_activity, \ sol_marker=[sol_min, sol_max], colors=colors_by_name) # Save plot and text file if save_plot: dir_path, ftpdetectinfo_name = os.path.split(ftpdetectinfo_path) ftpdetectinfo_base_name = ftpdetectinfo_name.replace( 'FTPdetectinfo_', '').replace('.txt', '') plot_name = ftpdetectinfo_base_name + '_radiants.png' # Increase figure size allsky_plot.fig.set_size_inches(18, 9, forward=True) allsky_plot.beautify() plt.savefig(os.path.join(dir_path, plot_name), dpi=100, facecolor='k') # Save the text file with shower info if len(jd_list): with open( os.path.join(dir_path, ftpdetectinfo_base_name + "_radiants.txt"), 'w') as f: # Print station code f.write("# RMS single station association\n") f.write("# \n") f.write("# Station: {:s}\n".format(cam_code)) # Print date range f.write( "# Beg | End \n" ) f.write( "# -----------------------------------------------------\n" ) f.write("# Date | {:24s} | {:24s} \n".format(jd2Date(jd_min, \ dt_obj=True).strftime("%Y%m%d %H:%M:%S.%f"), jd2Date(jd_max, \ dt_obj=True).strftime("%Y%m%d %H:%M:%S.%f"))) f.write("# Sol | {:>24.2f} | {:>24.2f} \n".format( sol_min, sol_max)) # Write shower counts f.write("# \n") f.write("# Shower counts:\n") f.write("# --------------\n") f.write("# Code, Count, IAU link\n") for i, (shower, count) in enumerate(shower_counts): if shower is not None: shower_name = shower.name # Create link to the IAU database of showers iau_link = "https://www.ta3.sk/IAUC22DB/MDC2007/Roje/pojedynczy_obiekt.php?kodstrumienia={:05d}".format( shower.iau_code) else: shower_name = "..." iau_link = "None" f.write("# {:>4s}, {:>5d}, {:s}\n".format( shower_name, count, iau_link)) f.write("# \n") f.write("# Meteor parameters:\n") f.write("# ------------------\n") f.write( "# Date And Time, Beg Julian date, La Sun, Shower, RA beg, Dec beg, RA end, Dec end, RA rad, Dec rad, GC theta0, GC phi0, GC beg phase, GC end phase, Mag\n" ) # Create a sorted list of meteor associations by time associations_list = [ associations[key] for key in associations ] associations_list = sorted(associations_list, key=lambda x: x[0].jdt_ref) # Write out meteor parameters for meteor_obj, shower in associations_list: # Find peak magnitude if np.any(meteor_obj.mag_array): peak_mag = "{:+.1f}".format( np.min(meteor_obj.mag_array)) else: peak_mag = "None" if shower is not None: f.write("{:24s}, {:20.12f}, {:>10.6f}, {:>6s}, {:6.2f}, {:+7.2f}, {:6.2f}, {:+7.2f}, {:6.2f}, {:+7.2f}, {:9.3f}, {:8.3f}, {:12.3f}, {:12.3f}, {:4s}\n".format(jd2Date(meteor_obj.jdt_ref, dt_obj=True).strftime("%Y%m%d %H:%M:%S.%f"), \ meteor_obj.jdt_ref, meteor_obj.lasun, shower.name, \ meteor_obj.ra_array[0]%360, meteor_obj.dec_array[0], \ meteor_obj.ra_array[-1]%360, meteor_obj.dec_array[-1], \ meteor_obj.radiant_ra%360, meteor_obj.radiant_dec, \ np.degrees(meteor_obj.theta0), np.degrees(meteor_obj.phi0), \ meteor_obj.gc_beg_phase, meteor_obj.gc_end_phase, peak_mag)) else: f.write("{:24s}, {:20.12f}, {:>10.6f}, {:>6s}, {:6.2f}, {:+7.2f}, {:6.2f}, {:+7.2f}, {:>6s}, {:>7s}, {:9.3f}, {:8.3f}, {:12.3f}, {:12.3f}, {:4s}\n".format(jd2Date(meteor_obj.jdt_ref, dt_obj=True).strftime("%Y%m%d %H:%M:%S.%f"), \ meteor_obj.jdt_ref, meteor_obj.lasun, '...', meteor_obj.ra_array[0]%360, \ meteor_obj.dec_array[0], meteor_obj.ra_array[-1]%360, \ meteor_obj.dec_array[-1], "None", "None", np.degrees(meteor_obj.theta0), \ np.degrees(meteor_obj.phi0), meteor_obj.gc_beg_phase, \ meteor_obj.gc_end_phase, peak_mag)) if show_plot: allsky_plot.show() else: plt.clf() plt.close() return associations, shower_counts
def estimateMeteorHeight(config, meteor_obj, shower): """ Estimate the height of a meteor from single station give a candidate shower. Arguments: config: [Config instance] meteor_obj: [MeteorSingleStation instance] shower: [Shower instance] Return: ht: [float] Estimated height in meters. """ ### Compute all needed values in alt/az coordinates ### # Compute beginning point vector in alt/az beg_ra, beg_dec = vector2RaDec(meteor_obj.beg_vect) beg_azim, beg_alt = raDec2AltAz(beg_ra, beg_dec, meteor_obj.jdt_ref, meteor_obj.lat, meteor_obj.lon) beg_vect_horiz = raDec2Vector(beg_azim, beg_alt) # Compute end point vector in alt/az end_ra, end_dec = vector2RaDec(meteor_obj.end_vect) end_azim, end_alt = raDec2AltAz(end_ra, end_dec, meteor_obj.jdt_ref, meteor_obj.lat, meteor_obj.lon) end_vect_horiz = raDec2Vector(end_azim, end_alt) # Compute radiant vector in alt/az radiant_azim, radiant_alt = raDec2AltAz(shower.ra, shower.dec, meteor_obj.jdt_ref, meteor_obj.lat, \ meteor_obj.lon) radiant_vector_horiz = raDec2Vector(radiant_azim, radiant_alt) # Reject the pairing if the radiant is below the horizon if radiant_alt < 0: return -1 # Get distance from Earth's centre to the position given by geographical coordinates for the # observer's latitude earth_radius = EARTH.EQUATORIAL_RADIUS / np.sqrt( 1.0 - (EARTH.E**2) * np.sin(np.radians(config.latitude))**2) # Compute the distance from Earth's centre to the station (including the sea level height of the station) re_dist = earth_radius + config.elevation ### ### # Compute the distance the meteor traversed during its duration (meters) dist = shower.v_init * meteor_obj.duration # Compute the angle between the begin and the end point of the meteor (rad) ang_beg_end = np.arccos( np.dot(vectNorm(beg_vect_horiz), vectNorm(end_vect_horiz))) # Compute the angle between the radiant vector and the begin point (rad) ang_beg_rad = np.arccos( np.dot(vectNorm(radiant_vector_horiz), -vectNorm(beg_vect_horiz))) # Compute the distance from the station to the begin point (meters) dist_beg = dist * np.sin(ang_beg_rad) / np.sin(ang_beg_end) # Compute the height using the law of cosines ht = np.sqrt(dist_beg**2 + re_dist**2 - 2 * dist_beg * re_dist * np.cos(np.radians(90 + meteor_obj.beg_alt))) ht -= earth_radius ht = abs(ht) return ht
def estimateMeteorHeight(meteor_obj, shower): """ Estimate the height of a meteor from single station give a candidate shower. Arguments: meteor_obj: [MeteorSingleStation instance] shower: [Shower instance] Return: ht: [float] Estimated height in meters. """ ### Compute all needed values in alt/az coordinates ### # Compute beginning point vector in alt/az beg_ra, beg_dec = vector2RaDec(meteor_obj.beg_vect) beg_azim, beg_alt = raDec2AltAz(beg_ra, beg_dec, meteor_obj.jdt_ref, meteor_obj.lat, meteor_obj.lon) beg_vect_horiz = raDec2Vector(beg_azim, beg_alt) # Compute end point vector in alt/az end_ra, end_dec = vector2RaDec(meteor_obj.end_vect) end_azim, end_alt = raDec2AltAz(end_ra, end_dec, meteor_obj.jdt_ref, meteor_obj.lat, meteor_obj.lon) end_vect_horiz = raDec2Vector(end_azim, end_alt) # Compute normal vector in alt/az normal_azim, normal_alt = raDec2AltAz(meteor_obj.normal_ra, meteor_obj.normal_dec, meteor_obj.jdt_ref, \ meteor_obj.lat, meteor_obj.lon) normal_horiz = raDec2Vector(normal_azim, normal_alt) # Compute radiant vector in alt/az radiant_azim, radiant_alt = raDec2AltAz(shower.ra, shower.dec, meteor_obj.jdt_ref, meteor_obj.lat, \ meteor_obj.lon) radiant_vector_horiz = raDec2Vector(radiant_azim, radiant_alt) # Reject the pairing if the radiant is below the horizon if radiant_alt < 0: return -1 ### ### # Compute cartesian coordinates of the pointing at the beginning of the meteor pt = vectNorm(beg_vect_horiz) # Compute reference vector perpendicular to the plane normal and the radiant vec = vectNorm(np.cross(normal_horiz, radiant_vector_horiz)) # Compute angles between the reference vector and the pointing dot_vb = np.dot(vec, beg_vect_horiz) dot_ve = np.dot(vec, end_vect_horiz) dot_vp = np.dot(vec, pt) # Compute distance to the radiant intersection line r_mag = 1.0 / (dot_vb**2) r_mag += 1.0 / (dot_ve**2) r_mag += -2 * np.cos(meteor_obj.ang_be) / (dot_vb * dot_ve) r_mag = np.sqrt(r_mag) r_mag = shower.v_init * meteor_obj.duration / r_mag pt_mag = r_mag / dot_vp # Compute the height ht = pt_mag**2 + EARTH.EQUATORIAL_RADIUS**2 \ - 2*pt_mag*EARTH.EQUATORIAL_RADIUS*np.cos(np.radians(90 - meteor_obj.beg_alt)) ht = np.sqrt(ht) ht -= EARTH.EQUATORIAL_RADIUS ht = abs(ht) return ht