def rectangular2EclipticCoord(x, y, z): """ Calculate ecliptic coordinats from given rectangular coordinates. Rectangular coordinates must be in the ecliptic reference frame, J2000 equinox, and in kilometers. Arguments: x y z Return: L, B, r_au """ # Calculate the distance from the Sun to the Earth in km r = vectMag(np.array([x, y, z])) # Calculate the ecliptic latitude B = np.arcsin(z / r) # Calculate ecliptic longitude L = np.arctan2(y, x) # Convert the distance to AU r_au = r / AU return L, B, r_au
def geocentricRadiantToApparent(ra_g, dec_g, v_g, state_vector, jd_ref): """ Numerically converts the given geocentric radiant to the apparent radiant. Arguments: ra_g: [float] Geocentric right ascension (radians). dec_g: [float] Geocentric declination (radians). v_g: [float] Geocentric velocity (m/s). state_vector: [ndarray of 3 elemens] (x, y, z) ECI coordinates of the initial state vector (meters). jd_ref: [float] reference Julian date of the event. Return: (ra_a, dec_a, v_init): [list] - ra_a: [float] Apparent R.A. (radians). - dec_a: [float] Apparent declination (radians). - v_init: [float] Initial velocity (m/s). """ def _radiantDiff(radiant_eq, ra_g, dec_g, v_init, state_vector, jd_ref): ra_a, dec_a = radiant_eq # Convert the given RA and Dec to ECI coordinates radiant_eci = np.array(raDec2ECI(ra_a, dec_a)) # Estimate the orbit with the given apparent radiant orbit = calcOrbit(radiant_eci, v_init, v_init, state_vector, jd_ref, stations_fixed=False, \ reference_init=True) if orbit.ra_g is None: return None # Compare the difference between the calculated and the reference geocentric radiant return angleBetweenSphericalCoords(orbit.dec_g, orbit.ra_g, dec_g, ra_g) # Assume that the velocity at infinity corresponds to the initial velocity v_init = np.sqrt(v_g**2 + (2 * 6.67408 * 5.9722) * 1e13 / vectMag(state_vector)) # Numerically find the apparent radiant res = scipy.optimize.minimize(_radiantDiff, x0=[ra_g, dec_g], args=(ra_g, dec_g, v_init, state_vector, jd_ref), \ bounds=[(0, 2*np.pi), (-np.pi, np.pi)], tol=1e-13, method='SLSQP') ra_a, dec_a = res.x # Calculate all orbital parameters with the best estimation of apparent RA and Dec orb = calcOrbit(np.array(raDec2ECI(ra_a, dec_a)), v_init, v_init, state_vector, jd_ref, stations_fixed=False, \ reference_init=True) return ra_a, dec_a, v_init, orb
def waveReleasePoint(stat_coord, x0, y0, t0, v, azim, zangle, v_sound): """ Calculate the point on the trajectory from which the balistic wave was released and heard by the given station. Arguments: stat_coord: [3 element ndarray] Coordinates of the station in the local coordinate system. x0: [float] Intersection with the X axis in the local coordinate system (meters). y0: [float] Intersection with the Y axis in the local coordinate system (meters). t0: [float] Time when the trajectory intersected the reference XY plane (seconds), offset from some reference time. v: [float] Velocity of the fireball (m/s). azim: [float] Fireball azimuth (+E of due N). zangle: [float] Zenith angle. v_sound: [float] Average speed of sound (m/s). Return: traj_point: [3 element ndarray] Location of the release point in the local coordinate system. """ # back azimuth #azim = (np.pi - azim)%(2*np.pi) # Calculate the mach angle beta = np.arcsin(v_sound / v) # Trajectory vector u = np.array([ np.sin(azim) * np.sin(zangle), np.cos(azim) * np.sin(zangle), -np.cos(zangle) ]) # Difference from the reference point on the trajectory and the station b = stat_coord - np.array([x0, y0, 0]) # Calculate the distance along the trajectory dt = np.abs(np.dot(b, -u)) # Calculate the distance perpendicular to the trajectory dp = np.sqrt(vectMag(b)**2 - dt**2) # Vector from (x0, y0) to the point of wave release r = -u * (dt + dp * np.tan(beta)) #r = -u*dt # Position of the wave release in the local coordinate system traj_point = np.array([x0, y0, 0]) + r return traj_point
def correctedEclipticCoord(L_g, B_g, v_g, earth_vel): """ Calculates the corrected ecliptic coordinates using the method of Sato and Watanabe (2014). Arguments: L_g: [float] Geocentric ecliptic longitude (radians). B_g: [float] Geocentric ecliptic latitude (radians). v_g: [float] Geocentric velocity (km/s). earth_vel: [3 element ndarray] Earh velocity vector (km/s) Return: L_h, B_h, met_v_h: L_h: [float] Corrected ecliptic longitude (radians). B_h: [float] Corrected ecliptic latitude (radians). met_v_h: [3 element ndarray] Heliocentric velocity vector of the meteoroid (km/s). """ # Calculate velocity components of the meteor xm, ym, zm = eclipticToRectangularVelocityVect(L_g, B_g, v_g) # Calculate the heliocentric velocity vector magnitude v_h = vectMag(np.array(earth_vel) + np.array([xm, ym, zm])) # Calculate the corrected meteoroid velocity vector xm_c = (xm + earth_vel[0]) / v_h ym_c = (ym + earth_vel[1]) / v_h zm_c = (zm + earth_vel[2]) / v_h # Calculate corrected radiant in ecliptic coordinates # NOTE: 180 deg had to be added to L and B had to be negative arcsin to get the right results L_h = (np.arctan2(ym_c, xm_c) + np.pi) % (2 * np.pi) B_h = -np.arcsin(zm_c) # Calculate the heliocentric velocity vector of the meteoroid xh, yh, zh = eclipticToRectangularVelocityVect(L_h, B_h, v_h) return L_h, B_h, np.array([xh, yh, zh])
def calcSpatialResidual(jd, state_vect, radiant_eci, stat, meas): """ Calculate horizontal and vertical residuals from the radiant line, for the given observed point. Arguments: jd: [float] Julian date state_vect: [3 element ndarray] ECI position of the state vector radiant_eci: [3 element ndarray] radiant direction vector in ECI stat: [3 element ndarray] position of the station in ECI meas: [3 element ndarray] line of sight from the station, in ECI Return: (hres, vres): [tuple of floats] residuals in horitontal and vertical direction from the radiant line """ meas = vectNorm(meas) # Calculate closest points of approach (observed line of sight to radiant line) from the state vector obs_cpa, rad_cpa, d = findClosestPoints(stat, meas, state_vect, radiant_eci) # Vector pointing from the point on the trajectory to the point on the line of sight p = obs_cpa - rad_cpa # Calculate geographical coordinates of the state vector lat, lon, elev = cartesian2Geo(jd, *state_vect) # Calculate ENU (East, North, Up) vector at the position of the state vector, and direction of the radiant nn = np.array(ecef2ENU(lat, lon, *radiant_eci)) # Convert the vector to polar coordinates theta = np.arctan2(nn[1], nn[0]) phi = np.arccos(nn[2] / vectMag(nn)) # Local reference frame unit vectors hx = np.array([-np.cos(theta), np.sin(theta), 0.0]) vz = np.array([ -np.cos(phi) * np.sin(theta), -np.cos(phi) * np.cos(theta), np.sin(phi) ]) hy = np.array([ np.sin(phi) * np.sin(theta), np.sin(phi) * np.cos(theta), np.cos(phi) ]) # Calculate local reference frame unit vectors in ECEF coordinates ehorzx = enu2ECEF(lat, lon, *hx) ehorzy = enu2ECEF(lat, lon, *hy) evert = enu2ECEF(lat, lon, *vz) ehx = np.dot(p, ehorzx) ehy = np.dot(p, ehorzy) # Calculate vertical residuals vres = np.sign(ehx) * np.hypot(ehx, ehy) # Calculate horizontal residuals hres = np.dot(p, evert) return hres, vres
def calcOrbit(radiant_eci, v_init, v_avg, eci_ref, jd_ref, stations_fixed=False, reference_init=True, \ rotation_correction=False): """ Calculate the meteor's orbit from the given meteor trajectory. The orbit of the meteoroid is defined relative to the centre of the Sun (heliocentric). Arguments: radiant_eci: [3 element ndarray] Radiant vector in ECI coordinates (meters). v_init: [float] Initial velocity (m/s). v_avg: [float] Average velocity of a meteor (m/s). eci_ref: [float] reference ECI coordinates in the epoch of date (meters, in the epoch of date) of the meteor trajectory. They can be calculated with the geo2Cartesian function. Ceplecha (1987) assumes this to the the average point on the trajectory, while Jennsikens et al. (2011) assume this to be the first point on the trajectory as that point is not influenced by deceleration. NOTE: If the stations are not fixed, the reference ECI coordinates should be the ones of the initial point on the trajectory, NOT of the average point! jd_ref: [float] reference Julian date of the meteor trajectory. Ceplecha (1987) takes this as the average time of the trajectory, while Jenniskens et al. (2011) take this as the the first point on the trajectory. Keyword arguments: stations_fixed: [bool] If True, the correction for Earth's rotation will be performed on the radiant, but not the velocity. This should be True ONLY in two occasions: - if the ECEF coordinate system was used for trajectory estimation - if the ECI coordinate system was used for trajectory estimation, BUT the stations were not moved in time, but were kept fixed at one point, regardless of the trajectory estimation method. It is necessary to perform this correction for the intersecting planes method, but not for the lines of sight method ONLY when the stations are not fixed. Of course, if one is using the lines of sight method with fixed stations, one should perform this correction! reference_init: [bool] If True (default), the initial point on the trajectory is given as the reference one, i.e. the reference ECI coordinates are the ECI coordinates of the initial point on the trajectory, where the meteor has the velocity v_init. If False, then the reference point is the average point on the trajectory, and the average velocity will be used to do the corrections. rotation_correction: [bool] If True, the correction of the initial velocity for Earth's rotation will be performed. False by default. This should ONLY be True if the coordiante system for trajectory estimation was ECEF, i.e. did not rotate with the Earth. In all other cases it should be False, even if fixed station coordinates were used in the ECI coordinate system! Return: orb: [Orbit object] Object containing the calculated orbit. """ ### Correct the velocity vector for the Earth's rotation if the stations are fixed ### ########################################################################################################## eci_x, eci_y, eci_z = eci_ref # Calculate the geocentric latitude (latitude which considers the Earth as an elipsoid) of the reference # trajectory point lat_geocentric = np.arctan2(eci_z, np.sqrt(eci_x**2 + eci_y**2)) # Calculate the dynamical JD jd_dyn = jd2DynamicalTimeJD(jd_ref) # Calculate the geographical coordinates of the reference trajectory ECI position lat_ref, lon_ref, ht_ref = cartesian2Geo(jd_ref, *eci_ref) # Initialize a new orbit structure and assign calculated parameters orb = Orbit() # Calculate the velocity of the Earth rotation at the position of the reference trajectory point (m/s) v_e = 2 * np.pi * vectMag(eci_ref) * np.cos(lat_geocentric) / 86164.09053 # Calculate the equatorial coordinates of east from the reference position on the trajectory azimuth_east = np.pi / 2 altitude_east = 0 ra_east, dec_east = altAz2RADec(azimuth_east, altitude_east, jd_ref, lat_ref, lon_ref) # Compute velocity components of the state vector if reference_init: # If the initial velocity was the reference velocity, use it for the correction v_ref_vect = v_init * radiant_eci else: # Calculate reference velocity vector using the average point on the trajectory and the average # velocity v_ref_vect = v_avg * radiant_eci # Apply the Earth rotation correction if the station coordinates are fixed (a MUST for the # intersecting planes method!) if stations_fixed: ### Set fixed stations radiant info ### # If the stations are fixed, then the input state vector is already fixed to the ground orb.ra_norot, orb.dec_norot = eci2RaDec(radiant_eci) # Apparent azimuth and altitude (no rotation) orb.azimuth_apparent_norot, orb.elevation_apparent_norot = raDec2AltAz(orb.ra_norot, orb.dec_norot, \ jd_ref, lat_ref, lon_ref) # Estimated average velocity (no rotation) orb.v_avg_norot = v_avg # Estimated initial velocity (no rotation) orb.v_init_norot = v_init ### ### v_ref_corr = np.zeros(3) # Calculate the corrected reference velocity vector/radiant v_ref_corr[0] = v_ref_vect[0] - v_e * np.cos(ra_east) v_ref_corr[1] = v_ref_vect[1] - v_e * np.sin(ra_east) v_ref_corr[2] = v_ref_vect[2] else: # MOVING STATIONS # Velocity vector will remain unchanged if the stations were moving if reference_init: v_ref_corr = v_init * radiant_eci else: v_ref_corr = v_avg * radiant_eci ### ### # If the rotation correction does not have to be applied, meaning that the rotation is already # included, compute a version of the radiant and the velocity without Earth's rotation # (REPORTING PURPOSES ONLY, THESE VALUES ARE NOT USED IN THE CALCULATION) 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(ra_east) v_ref_nocorr[1] = v_ref_vect[1] + v_e * np.sin(ra_east) v_ref_nocorr[2] = v_ref_vect[2] # Compute the radiant without Earth's rotation included orb.ra_norot, orb.dec_norot = eci2RaDec(vectNorm(v_ref_nocorr)) orb.azimuth_apparent_norot, orb.elevation_apparent_norot = raDec2AltAz(orb.ra_norot, orb.dec_norot, \ jd_ref, lat_ref, lon_ref) orb.v_init_norot = vectMag(v_ref_nocorr) orb.v_avg_norot = orb.v_init_norot - v_init + v_avg ### ### ########################################################################################################## ### Correct velocity for Earth's gravity ### ########################################################################################################## # If the reference velocity is the initial velocity if reference_init: # Use the corrected velocity for Earth's rotation (when ECEF coordinates are used) if rotation_correction: v_init_corr = vectMag(v_ref_corr) else: # IMPORTANT NOTE: The correction in this case is only done on the radiant (even if the stations # were fixed, but NOT on the initial velocity!). Thus, correction from Ceplecha 1987, # equation (35) is not needed. If the initial velocity is determined from time vs. length and in # ECI coordinates, whose coordinates rotate with the Earth, the moving stations play no role in # biasing the velocity. v_init_corr = v_init else: if rotation_correction: # Calculate the corrected initial velocity if the reference velocity is the average velocity v_init_corr = vectMag(v_ref_corr) + v_init - v_avg else: v_init_corr = v_init # Calculate apparent RA and Dec from radiant state vector orb.ra, orb.dec = eci2RaDec(radiant_eci) orb.v_init = v_init orb.v_avg = v_avg # Calculate the apparent azimuth and altitude (geodetic latitude, because ra/dec are calculated from ECI, # which is calculated from WGS84 coordinates) orb.azimuth_apparent, orb.elevation_apparent = raDec2AltAz( orb.ra, orb.dec, jd_ref, lat_ref, lon_ref) orb.jd_ref = jd_ref orb.lon_ref = lon_ref orb.lat_ref = lat_ref orb.ht_ref = ht_ref orb.lat_geocentric = lat_geocentric # Assume that the velocity in infinity is the same as the initial velocity (after rotation correction, if # it was needed) orb.v_inf = v_init_corr # Make sure the velocity of the meteor is larger than the escape velocity if v_init_corr**2 > (2 * 6.67408 * 5.9722) * 1e13 / vectMag(eci_ref): # Calculate the geocentric velocity (sqrt of squared inital velocity minus the square of the Earth escape # velocity at the height of the trajectory), units are m/s. # Square of the escape velocity is: 2GM/r, where G is the 2014 CODATA-recommended value of # 6.67408e-11 m^3/(kg s^2), and the mass of the Earth is M = 5.9722e24 kg v_g = np.sqrt(v_init_corr**2 - (2 * 6.67408 * 5.9722) * 1e13 / vectMag(eci_ref)) # Calculate the radiant corrected for Earth's rotation (ONLY if the stations were fixed, otherwise it # is the same as the apparent radiant) ra_corr, dec_corr = eci2RaDec(vectNorm(v_ref_corr)) # Calculate the Local Sidreal Time of the reference trajectory position lst_ref = np.radians(jd2LST(jd_ref, np.degrees(lon_ref))[0]) # Calculate the apparent zenith angle zc = np.arccos(np.sin(dec_corr)*np.sin(lat_geocentric) \ + np.cos(dec_corr)*np.cos(lat_geocentric)*np.cos(lst_ref - ra_corr)) # Calculate the zenith attraction correction delta_zc = 2 * np.arctan2( (v_init_corr - v_g) * np.tan(zc / 2), v_init_corr + v_g) # Zenith distance of the geocentric radiant zg = zc + np.abs(delta_zc) ########################################################################################################## ### Calculate the geocentric radiant ### ########################################################################################################## # Get the azimuth from the corrected RA and Dec azimuth_corr, _ = raDec2AltAz(ra_corr, dec_corr, jd_ref, lat_geocentric, lon_ref) # Calculate the geocentric radiant ra_g, dec_g = altAz2RADec(azimuth_corr, np.pi / 2 - zg, jd_ref, lat_geocentric, lon_ref) ### Precess ECI coordinates to J2000 ### # Convert rectangular to spherical coordiantes re, delta_e, alpha_e = cartesianToSpherical(*eci_ref) # Precess coordinates to J2000 alpha_ej, delta_ej = equatorialCoordPrecession(jd_ref, J2000_JD.days, alpha_e, delta_e) # Convert coordinates back to rectangular eci_ref = sphericalToCartesian(re, delta_ej, alpha_ej) eci_ref = np.array(eci_ref) ###### # Precess the geocentric radiant to J2000 ra_g, dec_g = equatorialCoordPrecession(jd_ref, J2000_JD.days, ra_g, dec_g) # Calculate the ecliptic latitude and longitude of the geocentric radiant (J2000 epoch) L_g, B_g = raDec2Ecliptic(J2000_JD.days, ra_g, dec_g) # Load the JPL ephemerids data jpl_ephem_data = SPK.open(config.jpl_ephem_file) # Get the position of the Earth (km) and its velocity (km/s) at the given Julian date (J2000 epoch) # The position is given in the ecliptic coordinates, origin of the coordinate system is in the centre # of the Sun earth_pos, earth_vel = calcEarthRectangularCoordJPL( jd_dyn, jpl_ephem_data, sun_centre_origin=True) # print('Earth position:') # print(earth_pos) # print('Earth velocity:') # print(earth_vel) # Convert the Earth's position to rectangular equatorial coordinates (FK5) earth_pos_eq = rotateVector(earth_pos, np.array([1, 0, 0]), J2000_OBLIQUITY) # print('Earth position (FK5):') # print(earth_pos_eq) # print('Meteor ECI:') # print(eci_ref) # Add the position of the meteor's trajectory to the position of the Earth to calculate the # equatorial coordinates of the meteor (in kilometers) meteor_pos = earth_pos_eq + eci_ref / 1000 # print('Meteor position (FK5):') # print(meteor_pos) # Convert the position of the trajectory from FK5 to heliocentric ecliptic coordinates meteor_pos = rotateVector(meteor_pos, np.array([1, 0, 0]), -J2000_OBLIQUITY) # print('Meteor position:') # print(meteor_pos) ########################################################################################################## # Calculate components of the heliocentric velocity of the meteor (km/s) v_h = np.array(earth_vel) + np.array( eclipticToRectangularVelocityVect(L_g, B_g, v_g / 1000)) # Calculate the heliocentric velocity in km/s v_h_mag = vectMag(v_h) # Calculate the corrected heliocentric ecliptic coordinates of the meteoroid using the method of # Sato and Watanabe (2014). L_h, B_h, met_v_h = correctedEclipticCoord(L_g, B_g, v_g / 1000, earth_vel) # Calculate the solar longitude la_sun = jd2SolLonJPL(jd_dyn) # Calculations below done using Dave Clark's Master thesis equations # Specific orbital energy epsilon = (vectMag(v_h)**2) / 2 - SUN_MU / vectMag(meteor_pos) # Semi-major axis in AU a = -SUN_MU / (2 * epsilon * AU) # Calculate mean motion in rad/day n = np.sqrt(G * SUN_MASS / ((np.abs(a) * AU * 1000.0)**3)) * 86400.0 # Calculate the orbital period in years T = 2 * np.pi * np.sqrt( ((a * AU)**3) / SUN_MU) / (86400 * SIDEREAL_YEAR) # Calculate the orbit angular momentum h_vect = np.cross(meteor_pos, v_h) # Calculate inclination incl = np.arccos(h_vect[2] / vectMag(h_vect)) # Calculate eccentricity e_vect = np.cross(v_h, h_vect) / SUN_MU - vectNorm(meteor_pos) eccentricity = vectMag(e_vect) # Calculate perihelion distance (source: Jenniskens et al., 2011, CAMS overview paper) if eccentricity == 1: q = (vectMag(meteor_pos) + np.dot(e_vect, meteor_pos)) / (1 + vectMag(e_vect)) else: q = a * (1.0 - eccentricity) # Calculate the aphelion distance Q = a * (1.0 + eccentricity) # Normal vector to the XY reference frame k_vect = np.array([0, 0, 1]) # Vector from the Sun pointing to the ascending node n_vect = np.cross(k_vect, h_vect) # Calculate node if vectMag(n_vect) == 0: node = 0 else: node = np.arctan2(n_vect[1], n_vect[0]) node = node % (2 * np.pi) # Calculate argument of perihelion if vectMag(n_vect) != 0: peri = np.arccos( np.dot(n_vect, e_vect) / (vectMag(n_vect) * vectMag(e_vect))) if e_vect[2] < 0: peri = 2 * np.pi - peri else: peri = np.arccos(e_vect[0] / vectMag(e_vect)) peri = peri % (2 * np.pi) # Calculate the longitude of perihelion pi = (node + peri) % (2 * np.pi) ### Calculate true anomaly true_anomaly = np.arccos( np.dot(e_vect, meteor_pos) / (vectMag(e_vect) * vectMag(meteor_pos))) if np.dot(meteor_pos, v_h) < 0: true_anomaly = 2 * np.pi - true_anomaly true_anomaly = true_anomaly % (2 * np.pi) ### # Calculate eccentric anomaly eccentric_anomaly = np.arctan2(np.sqrt(1 - eccentricity**2)*np.sin(true_anomaly), eccentricity \ + np.cos(true_anomaly)) # Calculate mean anomaly mean_anomaly = eccentric_anomaly - eccentricity * np.sin( eccentric_anomaly) mean_anomaly = mean_anomaly % (2 * np.pi) # Calculate the time in days since the last perihelion passage of the meteoroid dt_perihelion = (mean_anomaly * a**(3.0 / 2)) / 0.01720209895 if not np.isnan(dt_perihelion): # Calculate the date and time of the last perihelion passage last_perihelion = jd2Date(jd_dyn - dt_perihelion, dt_obj=True) else: last_perihelion = None # Calculate Tisserand's parameter with respect to Jupiter Tj = 2 * np.sqrt( (1 - eccentricity**2) * a / 5.204267) * np.cos(incl) + 5.204267 / a # Assign calculated parameters orb.lst_ref = lst_ref orb.jd_dyn = jd_dyn orb.v_g = v_g orb.ra_g = ra_g orb.dec_g = dec_g orb.meteor_pos = meteor_pos orb.L_g = L_g orb.B_g = B_g orb.v_h_x, orb.v_h_y, orb.v_h_z = met_v_h orb.L_h = L_h orb.B_h = B_h orb.zc = zc orb.zg = zg orb.v_h = v_h_mag * 1000 orb.la_sun = la_sun orb.a = a orb.e = eccentricity orb.i = incl orb.peri = peri orb.node = node orb.pi = pi orb.q = q orb.Q = Q orb.true_anomaly = true_anomaly orb.eccentric_anomaly = eccentric_anomaly orb.mean_anomaly = mean_anomaly orb.last_perihelion = last_perihelion orb.n = n orb.T = T orb.Tj = Tj return orb
# Values from Tsuchiya paper: # Lh: 329.61 # Bh: 0.82 # vh: 38.71 ########### # Load the JPL ephemerids data jpl_ephem_data = SPK.open(config.jpl_ephem_file) # Get the position of the Earth (km) and its velocity (km/s) at the given Julian date (J2000 epoch) # The position is given in the ecliptic coordinates, origin of the coordinate system is in the Solar # system barycentre earth_pos, earth_vel = wmpl.Utils.Earth.calcEarthRectangularCoordJPL( jd, jpl_ephem_data) # Calculate corrected heliocentrc coordinates L_h, B_h, met_v_h = correctedEclipticCoord(L_g, B_g, v_g, earth_vel) print('Lh:', np.degrees(L_h)) print('Bh:', np.degrees(B_h)) print('Vh:', vectMag(met_v_h)) print() jd = 2455843.314521576278 print('JD: {:.10f}'.format(jd)) print('JDdyn: {:.10f}'.format(jd2DynamicalTimeJD(jd))) ###########
def timeOfArrival(stat_coord, traj, bam, prefs, points, ref_loc=Position(0, 0, 0)): """ Calculate the time of arrival at given coordinates in the local coordinate system for the given parameters of the fireball. Arguments: stat_coord: [3 element ndarray] Coordinates of the station in the local coordinate system. x0: [float] Intersection with the X axis in the local coordinate system (meters). y0: [float] Intersection with the Y axis in the local coordinate system (meters). t0: [float] Time when the trajectory intersected the reference XY plane (seconds), offset from some reference time. v: [float] Velocity of the fireball (m/s). azim: [float] Fireball azimuth (+E of due N). (radians) zangle: [float] Zenith angle. (radians) setup: [Object] Object containing all user-defined parameters sounding: [ndarray] atmospheric profile of the search area travel: [boolean] switch to only return the travel time Return: ti: [float] Balistic shock time of arrival to the given coordinates (seconds). """ #azim = (np.pi - azim)%(2*np.pi) # Calculate the mach angle #cos(arcsin(x)) = sqrt(1 - x^2) #x0, y0, t0, v, azim, zangle beta = math.sqrt(1 - (prefs.avg_sp_sound / traj.v_avg / 1000)**2) # Difference from the reference point on the trajectory and the station g = traj.findGeo(0) g.pos_loc(ref_loc) b = stat_coord - g.xyz u = traj.getTrajVect() # Calculate the distance along the trajectory dt = abs(np.dot(b, -u)) # Calculate the distance perpendicular to the trajectory dp = math.sqrt(abs(vectMag(b)**2 - dt**2)) R = waveReleasePointWinds(stat_coord, bam, prefs, ref_loc, points, u) # if travel: # # travel from trajectory only # ti = R[3]*beta # else: # v = traj.getVelAtHeight(R[2]) # if theo: # # Calculate time of arrival # ti = traj.t + dt/v + R[3]*beta # else: # Calculate time of arrival # ti = traj.t - dt/traj.v_avg + R[0]*beta ti = R[0] ti_pert = [] for pert_R in R[1]: ti_pert.append(traj.t - dt / traj.v_avg + pert_R * beta) return ti, ti_pert
def quickTrajectorySolution(self, obs1, obs2): """ Perform an intersecting planes solution and check if it satisfies specified sanity checks. """ # Do the plane intersection solution plane_intersection = PlaneIntersection(obs1, obs2) ra_cand, dec_cand = plane_intersection.radiant_eq print("Candidate radiant: RA = {:.3f}, Dec = {:+.3f}".format(np.degrees(ra_cand), \ np.degrees(dec_cand))) ### Compute meteor begin and end points eci1_beg, lat1_beg, lon1_beg, ht1_beg = self.projectPointToTrajectory( 0, obs1, plane_intersection) eci1_end, lat1_end, lon1_end, ht1_end = self.projectPointToTrajectory( -1, obs1, plane_intersection) eci2_beg, lat2_beg, lon2_beg, ht2_beg = self.projectPointToTrajectory( 0, obs2, plane_intersection) eci2_end, lat2_end, lon2_end, ht2_end = self.projectPointToTrajectory( -1, obs2, plane_intersection) # Convert heights to kilometers ht1_beg /= 1000 ht1_end /= 1000 ht2_beg /= 1000 ht2_end /= 1000 ### ### ### Check if the meteor begin and end points are within the specified range ### # Check the end height is lower than begin height if (ht1_end > ht1_beg) or (ht2_end > ht2_beg): print("Begin height lower than the end height!") return None # Check if begin height are within the specified range if (ht1_beg > self.traj_constraints.max_begin_ht) \ or (ht1_beg < self.traj_constraints.min_begin_ht) \ or (ht2_beg > self.traj_constraints.max_begin_ht) \ or (ht2_beg < self.traj_constraints.min_begin_ht) \ or (ht1_end > self.traj_constraints.max_end_ht) \ or (ht1_end < self.traj_constraints.min_end_ht) \ or (ht2_end > self.traj_constraints.max_end_ht) \ or (ht2_end < self.traj_constraints.min_end_ht): print("Meteor heights outside allowed range!") print("H1_beg: {:.2f}, H1_end: {:.2f}".format(ht1_beg, ht1_end)) print("H2_beg: {:.2f}, H2_end: {:.2f}".format(ht2_beg, ht2_end)) return None ### ### ### Check if the velocity is consistent ### # Compute the average velocity from both stations (km/s) vel1 = vectMag(eci1_end - eci1_beg) / (obs1.time_data[-1] - obs1.time_data[0]) / 1000 vel2 = vectMag(eci2_end - eci2_beg) / (obs2.time_data[-1] - obs2.time_data[0]) / 1000 # Check if they are within a certain percentage difference percent_diff = 100 * abs(vel1 - vel2) / max(vel1, vel2) if percent_diff > self.traj_constraints.max_vel_percent_diff: print("Velocity difference too high: {:.2f} vs {:.2f} km/s".format( vel1 / 1000, vel2 / 1000)) return None # Check the velocity range v_avg = (vel1 + vel2) / 2 if (v_avg < self.traj_constraints.v_avg_min) or ( v_avg > self.traj_constraints.v_avg_max): print("Average veocity outside velocity bounds: {:.1f} < {:.1f} < {:.1f}".format(self.traj_constraints.v_avg_min, \ v_avg, self.traj_constraints.v_avg_max)) return None ### ### return plane_intersection
def sampleTrajectory(dir_path, file_name, beg_ht, end_ht, sample_step): """ Given the trajectory, beginning, end and step in km, this function will interpolate the fireball height vs. distance and return the coordinates of sampled positions and compute the azimuth and elevation for every point. Arguments: Return: """ # Load the trajectory file traj = loadPickle(dir_path, file_name) # Set begin and end heights, if not given if beg_ht < 0: beg_ht = traj.rbeg_ele / 1000 if end_ht < 0: end_ht = traj.rend_ele / 1000 # Convert heights to meters beg_ht *= 1000 end_ht *= 1000 sample_step *= 1000 # Generate heights for sampling height_array = np.flipud( np.arange(end_ht, beg_ht + sample_step, sample_step)) ### Fit time vs. height time_data = [] height_data = [] for obs in traj.observations: time_data += obs.time_data.tolist() height_data += obs.model_ht.tolist() # Plot the station data plt.scatter(obs.time_data, obs.model_ht / 1000, label=obs.station_id, marker='x', zorder=3) height_data = np.array(height_data) time_data = np.array(time_data) # Sort the arrays by decreasing time arr_sort_indices = np.argsort(time_data)[::-1] height_data = height_data[arr_sort_indices] time_data = time_data[arr_sort_indices] # Plot the non-smoothed time vs. height #plt.scatter(time_data, height_data/1000, label='Data') # Apply Savitzky-Golay to smooth out the height change height_data = scipy.signal.savgol_filter(height_data, 21, 5) plt.scatter(time_data, height_data / 1000, label='Savitzky-Golay filtered', marker='+', zorder=3) # Sort the arrays by increasing heights (needed for interpolation) arr_sort_indices = np.argsort(height_data) height_data = height_data[arr_sort_indices] time_data = time_data[arr_sort_indices] # Interpolate height vs. time ht_vs_time_interp = scipy.interpolate.PchipInterpolator( height_data, time_data) # Plot the interpolation ht_arr = np.linspace(np.min(height_data), np.max(height_data), 1000) time_arr = ht_vs_time_interp(ht_arr) plt.plot(time_arr, ht_arr / 1000, label='Interpolation', zorder=3) plt.legend() plt.xlabel('Time (s)') plt.ylabel('Height (km)') plt.grid() plt.show() ### # Take the ground above the state vector as the reference distance from the surface of the Earth ref_radius = vectMag(traj.state_vect_mini) - np.max(height_data) # Compute distance from the centre of the Earth to each height radius_array = ref_radius + height_array print('Beginning coordinates (observed):') print(' Lat: {:.6f}'.format(np.degrees(traj.rbeg_lat))) print(' Lon: {:.6f}'.format(np.degrees(traj.rbeg_lon))) print(' Elev: {:.1f}'.format(traj.rbeg_ele)) print() print("Ground-fixed azimuth and altitude:") print( ' Time(s), Sample ht (m), Lat (deg), Lon (deg), Height (m), Azim (deg), Elev (deg)' ) # Go through every distance from the Earth centre and compute the geo coordinates at the given distance, # as well as the point-to-point azimuth and elevation prev_eci = None for ht, radius in zip(height_array, radius_array): # If the height is lower than the eng height, use a fixed velocity of 3 km/s if ht < traj.rend_ele: t_est = ht_vs_time_interp( traj.rend_ele) + abs(ht - traj.rend_ele) / 3000 time_marker = "*" else: # Estimate the fireball time at the given height using interpolated values t_est = ht_vs_time_interp(ht) time_marker = " " # Compute the intersection between the trajectory line and the sphere of radius at the given height intersections = lineAndSphereIntersections(np.array([0, 0, 0]), radius, traj.state_vect_mini, traj.radiant_eci_mini) # Choose the intersection that is closer to the state vector inter_min_dist_indx = np.argmin( [vectMag(inter - traj.state_vect_mini) for inter in intersections]) height_eci = intersections[inter_min_dist_indx] # Compute the Julian date at the given height jd = traj.jdt_ref + t_est / 86400.0 # Compute geographical coordinates lat, lon, ele_geo = cartesian2Geo(jd, *height_eci) # Compute azimuth and elevation if prev_eci is not None: # Compute the vector pointing from the previous point to the current point direction_vect = vectNorm(prev_eci - height_eci) ### Compute the ground-fixed alt/az eci_x, eci_y, eci_z = height_eci # Calculate the geocentric latitude (latitude which considers the Earth as an elipsoid) of the reference # trajectory point lat_geocentric = np.arctan2(eci_z, np.sqrt(eci_x**2 + eci_y**2)) # Calculate the velocity of the Earth rotation at the position of the reference trajectory point (m/s) v_e = 2 * np.pi * vectMag(height_eci) * np.cos( lat_geocentric) / 86164.09053 # Calculate the equatorial coordinates of east from the reference position on the trajectory azimuth_east = np.pi / 2 altitude_east = 0 ra_east, dec_east = altAz2RADec(azimuth_east, altitude_east, jd, lat, lon) # The reference velocity vector has the average velocity and the given direction # Note that ideally this would be the instantaneous velocity v_ref_vect = traj.orbit.v_avg_norot * direction_vect 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(ra_east) v_ref_nocorr[1] = v_ref_vect[1] + v_e * np.sin(ra_east) v_ref_nocorr[2] = v_ref_vect[2] # Compute the radiant without Earth's rotation included ra_norot, dec_norot = eci2RaDec(vectNorm(v_ref_nocorr)) azim_norot, elev_norot = raDec2AltAz(ra_norot, dec_norot, jd, lat, lon) ### else: azim_norot = -np.inf elev_norot = -np.inf prev_eci = np.copy(height_eci) print( "{:s}{:7.3f}, {:13.1f}, {:10.6f}, {:11.6f}, {:10.1f}, {:10.6f}, {:10.6f}" .format(time_marker, t_est, ht, np.degrees(lat), np.degrees(lon), ele_geo, np.degrees(azim_norot), np.degrees(elev_norot))) print( 'The star * denotes heights extrapolated after the end of the fireball, with the fixed velocity of 3 km/s.' ) print('End coordinates (observed):') print(' Lat: {:.6f}'.format(np.degrees(traj.rend_lat))) print(' Lon: {:.6f}'.format(np.degrees(traj.rend_lon))) print(' Elev: {:.1f}'.format(traj.rend_ele))
def projectNarrowPicks(dir_path, met, traj, traj_uncert, metal_mags, frag_info): """ Projects picks done in the narrow-field to the given trajectory. """ # Adjust initial velocity frag_v_init = traj.v_init + frag_info.v_init_adjust # List for computed values to be stored in a file computed_values = [] # Generate the file name prefix from the time (take from trajectory) file_name_prefix = traj.file_name # List that holds datetimes of fragmentations, used for the light curve plot fragmentations_datetime = [] # Go through picks from all sites for site_no in met.picks: # Extract site exact plate exact = met.exact_plates[site_no] # Extract site picks picks = np.array(met.picks[site_no]) # Skip the site if there are no picks if not len(picks): continue print() print('Processing site:', site_no) # Find unique fragments fragments = np.unique(picks[:, 1]) # If the fragmentation dictionary is empty, generate one if frag_info.frag_dict is None: frag_info.frag_dict = { float(i): i + 1 for i in range(len(fragments)) } # A list with results of finding the closest point on the trajectory cpa_list = [] # Go thorugh all fragments and calculate the coordinates of the closest points on the trajectory and # the line of sight for frag in fragments: # Take only those picks from current fragment frag_picks = picks[picks[:, 1] == frag] # Sort by frame frag_picks = frag_picks[np.argsort(frag_picks[:, 0])] # Extract Unix timestamp ts = frag_picks[:, 11] tu = frag_picks[:, 12] # Extract theta, phi theta = np.radians(frag_picks[:, 4]) phi = np.radians(frag_picks[:, 5]) # Calculate azimuth +E of N azim = (np.pi / 2.0 - phi) % (2 * np.pi) # Calculate elevation elev = np.pi / 2.0 - theta # Calculate Julian date from Unix timestamp jd_data = np.array([unixTime2JD(s, u) for s, u in zip(ts, tu)]) # Convert azim/elev to RA/Dec ra, dec = altAz2RADec_vect(azim, elev, jd_data, exact.lat, exact.lon) # Convert RA/Dec to ECI direction vector x_eci, y_eci, z_eci = raDec2ECI(ra, dec) # Convert station geocoords to ECEF coordinates x_stat_vect, y_stat_vect, z_stat_vect = geo2Cartesian_vect(exact.lat, exact.lon, exact.elev, \ jd_data) # Find closest points of aproach for all measurements for jd, x, y, z, x_stat, y_stat, z_stat in np.c_[jd_data, x_eci, y_eci, z_eci, x_stat_vect, \ y_stat_vect, z_stat_vect]: # Find the closest point of approach of every narrow LoS to the wide trajectory obs_cpa, rad_cpa, d = findClosestPoints(np.array([x_stat, y_stat, z_stat]), \ np.array([x, y, z]), traj.state_vect_mini, traj.radiant_eci_mini) # Calculate the height of each fragment for the given time rad_lat, rad_lon, height = cartesian2Geo(jd, *rad_cpa) cpa_list.append( [frag, jd, obs_cpa, rad_cpa, d, rad_lat, rad_lon, height]) # Find the coordinates of the first point in time on the trajectory and the first JD first_jd_indx = np.argmin([entry[1] for entry in cpa_list]) jd_ref = cpa_list[first_jd_indx][1] rad_cpa_ref = cpa_list[first_jd_indx][3] print(jd_ref) # Set the beginning time to the beginning of the widefield trajectory ref_beg_time = (traj.jdt_ref - jd_ref) * 86400 length_list = [] decel_list = [] # Go through all fragments and calculate the length from the reference point for frag in fragments: # Select only the data points of the current fragment cpa_data = [entry for entry in cpa_list if entry[0] == frag] # Lengths of the current fragment length_frag = [] # Go through all projected points on the trajectory for entry in cpa_data: jd = entry[1] rad_cpa = entry[3] rad_lat = entry[5] rad_lon = entry[6] height = entry[7] # Calculate the distance from the first point on the trajectory and the given point dist = vectMag(rad_cpa - rad_cpa_ref) # Calculate the time in seconds time_sec = (jd - jd_ref) * 24 * 3600 length_frag.append([time_sec, dist, rad_lat, rad_lon, height]) length_list.append( [frag, time_sec, dist, rad_lat, rad_lon, height]) ### Fit the deceleration model to the length ### ################################################################################################## length_frag = np.array(length_frag) # Extract JDs and lengths into individual arrays time_data, length_data, lat_data, lon_data, height_data = length_frag.T if frag_info.fit_full_exp_model: # Fit the full exp deceleration model # First guess of the lag parameters p0 = [ frag_v_init, 0, 0, traj.jacchia_fit[0], traj.jacchia_fit[1] ] # Length residuals function def _lenRes(params, time_data, length_data): return np.sum( (length_data - exponentialDeceleration(time_data, *params))**2) # Fit an exponential to the data res = scipy.optimize.basinhopping(_lenRes, p0, \ minimizer_kwargs={"method": "BFGS", 'args':(time_data, length_data)}, \ niter=1000) decel_fit = res.x else: # Fit only the deceleration parameters # First guess of the lag parameters p0 = [0, 0, traj.jacchia_fit[0], traj.jacchia_fit[1]] # Length residuals function def _lenRes(params, time_data, length_data, v_init): return np.sum((length_data - exponentialDeceleration( time_data, v_init, *params))**2) # Fit an exponential to the data res = scipy.optimize.basinhopping(_lenRes, p0, \ minimizer_kwargs={"method": "Nelder-Mead", 'args':(time_data, length_data, frag_v_init)}, \ niter=100) decel_fit = res.x # Add the velocity to the deceleration fit decel_fit = np.append(np.array([frag_v_init]), decel_fit) decel_list.append(decel_fit) print('---------------') print('Fragment', frag_info.frag_dict[frag], 'fit:') print(decel_fit) # plt.plot(time_data, length_data, label='Observed') # plt.plot(time_data, exponentialDeceleration(time_data, *decel_fit), label='fit') # plt.legend() # plt.xlabel('Time (s)') # plt.ylabel('Length (m)') # plt.title('Fragment {:d} fit'.format(frag_info.frag_dict[frag])) # plt.show() # # Plot the residuals # plt.plot(time_data, length_data - exponentialDeceleration(time_data, *decel_fit)) # plt.xlabel('Time (s)') # plt.ylabel('Length O - C (m)') # plt.title('Fragment {:d} fit residuals'.format(frag_info.frag_dict[frag])) # plt.show() ################################################################################################## # Generate a unique color for every fragment colors = plt.cm.rainbow(np.linspace(0, 1, len(fragments))) # Create a dictionary for every fragment-color pair colors_frags = {frag: color for frag, color in zip(fragments, colors)} # Make sure lags start at 0 offset_vel_max = 0 # Plot the positions of fragments from the beginning to the end # Calculate and plot the lag of all fragments for frag, decel_fit in zip(fragments, decel_list): # Select only the data points of the current fragment length_frag = [entry for entry in length_list if entry[0] == frag] # Find the last time of the fragment appearance last_time = max([entry[1] for entry in length_frag]) # Extract the observed data _, time_data, length_data, lat_data, lon_data, height_data = np.array( length_frag).T # Plot the positions of fragments from the first time to the end, using fitted parameters # The lag is calculated by subtracting an "average" velocity length from the observed length time_array = np.linspace(ref_beg_time, last_time, 1000) plt.plot(exponentialDeceleration(time_array, *decel_fit) - exponentialDeceleration(time_array, \ frag_v_init, 0, offset_vel_max, 0, 0), time_array, linestyle='--', color=colors_frags[frag], \ linewidth=0.75) # Plot the observed data fake_lag = length_data - exponentialDeceleration( time_data, frag_v_init, 0, offset_vel_max, 0, 0) plt.plot(fake_lag, time_data, color=colors_frags[frag], linewidth=0.75) # Plot the fragment number at the end of each lag plt.text(fake_lag[-1] - 10, time_data[-1] + 0.02, str(frag_info.frag_dict[frag]), color=colors_frags[frag], \ size=7, va='center', ha='right') # Check if the fragment has a fragmentation point and plot it if site_no in frag_info.fragmentation_points: if frag_info.frag_dict[frag] in frag_info.fragmentation_points[ site_no]: # Get the lag of the fragmentation point frag_point_time, fragments_list = frag_info.fragmentation_points[ site_no][frag_info.frag_dict[frag]] frag_point_lag = exponentialDeceleration(frag_point_time, *decel_fit) \ - exponentialDeceleration(frag_point_time, frag_v_init, 0, offset_vel_max, 0, 0) fragments_list = list(map(str, fragments_list)) # Save the fragmentation time in the list for light curve plot fragmentations_datetime.append([jd2Date(jd_ref + frag_point_time/86400, dt_obj=True), \ fragments_list]) # Plot the fragmentation point plt.scatter(frag_point_lag, frag_point_time, s=20, zorder=4, color=colors_frags[frag], \ edgecolor='k', linewidth=0.5, label='Fragmentation: ' + ",".join(fragments_list)) # Plot reference time plt.title('Reference time: ' + str(jd2Date(jd_ref, dt_obj=True))) plt.gca().invert_yaxis() plt.grid(color='0.9') plt.xlabel('Lag (m)') plt.ylabel('Time (s)') plt.ylim(ymax=ref_beg_time) plt.legend() plt.savefig(os.path.join(dir_path, file_name_prefix \ + '_fragments_deceleration_site_{:s}.png'.format(str(site_no))), dpi=300) plt.show() time_min = np.inf time_max = -np.inf ht_min = np.inf ht_max = -np.inf ### PLOT DYNAMIC PRESSURE FOR EVERY FRAGMENT for frag, decel_fit in zip(fragments, decel_list): # Select only the data points of the current fragment length_frag = [entry for entry in length_list if entry[0] == frag] # Extract the observed data _, time_data, length_data, lat_data, lon_data, height_data = np.array( length_frag).T # Fit a linear dependance of time vs. height line_fit, _ = scipy.optimize.curve_fit(lineFunc, time_data, height_data) # Get the time and height limits time_min = min(time_min, min(time_data)) time_max = max(time_max, max(time_data)) ht_min = min(ht_min, min(height_data)) ht_max = max(ht_max, max(height_data)) ### CALCULATE OBSERVED DYN PRESSURE # Get the velocity at every point in time velocities = exponentialDecelerationVel(time_data, *decel_fit) # Calculate the dynamic pressure dyn_pressure = dynamicPressure(lat_data, lon_data, height_data, jd_ref, velocities) ### # Plot Observed height vs. dynamic pressure plt.plot(dyn_pressure / 10**3, height_data / 1000, color=colors_frags[frag], zorder=3, linewidth=0.75) # Plot the fragment number at the end of each lag plt.text(dyn_pressure[-1]/10**3, height_data[-1]/1000 - 0.02, str(frag_info.frag_dict[frag]), \ color=colors_frags[frag], size=7, va='top', zorder=3) ### CALCULATE MODELLED DYN PRESSURE time_array = np.linspace(ref_beg_time, max(time_data), 1000) # Calculate the modelled height height_array = lineFunc(time_array, *line_fit) # Get the time and height limits time_min = min(time_min, min(time_array)) time_max = max(time_max, max(time_array)) ht_min = min(ht_min, min(height_array)) ht_max = max(ht_max, max(height_array)) # Get the atmospheric densities at every heights atm_dens_model = getAtmDensity_vect(np.zeros_like(time_array) + np.mean(lat_data), \ np.zeros_like(time_array) + np.mean(lon_data), height_array, jd_ref) # Get the velocity at every point in time velocities_model = exponentialDecelerationVel( time_array, *decel_fit) # Calculate the dynamic pressure dyn_pressure_model = atm_dens_model * DRAG_COEFF * velocities_model**2 ### # Plot Modelled height vs. dynamic pressure plt.plot(dyn_pressure_model/10**3, height_array/1000, color=colors_frags[frag], zorder=3, \ linewidth=0.75, linestyle='--') # Check if the fragment has a fragmentation point and plot it if site_no in frag_info.fragmentation_points: if frag_info.frag_dict[frag] in frag_info.fragmentation_points[ site_no]: # Get the lag of the fragmentation point frag_point_time, fragments_list = frag_info.fragmentation_points[ site_no][frag_info.frag_dict[frag]] # Get the fragmentation height frag_point_height = lineFunc(frag_point_time, *line_fit) # Calculate the velocity at fragmentation frag_point_velocity = exponentialDecelerationVel( frag_point_time, *decel_fit) # Calculate the atm. density at the fragmentation point frag_point_atm_dens = getAtmDensity(np.mean(lat_data), np.mean(lon_data), frag_point_height, \ jd_ref) # Calculate the dynamic pressure at fragmentation in kPa frag_point_dyn_pressure = frag_point_atm_dens * DRAG_COEFF * frag_point_velocity**2 frag_point_dyn_pressure /= 10**3 # Compute height in km frag_point_height_km = frag_point_height / 1000 fragments_list = map(str, fragments_list) # Plot the fragmentation point plt.scatter(frag_point_dyn_pressure, frag_point_height_km, s=20, zorder=5, \ color=colors_frags[frag], edgecolor='k', linewidth=0.5, \ label='Fragmentation: ' + ",".join(fragments_list)) ### Plot the errorbar # Compute the lower veloicty estimate stddev_multiplier = 2.0 # Check if the uncertainty exists if traj_uncert.v_init is None: v_init_uncert = 0 else: v_init_uncert = traj_uncert.v_init # Compute the range of velocities lower_vel = frag_point_velocity - stddev_multiplier * v_init_uncert higher_vel = frag_point_velocity + stddev_multiplier * v_init_uncert # Assume the atmosphere density can vary +/- 25% (Gunther's analysis) lower_atm_dens = 0.75 * frag_point_atm_dens higher_atm_dens = 1.25 * frag_point_atm_dens # Compute lower and higher range for dyn pressure in kPa lower_frag_point_dyn_pressure = ( lower_atm_dens * DRAG_COEFF * lower_vel**2) / 10**3 higher_frag_point_dyn_pressure = ( higher_atm_dens * DRAG_COEFF * higher_vel**2) / 10**3 # Compute errors lower_error = abs(frag_point_dyn_pressure - lower_frag_point_dyn_pressure) higher_error = abs(frag_point_dyn_pressure - higher_frag_point_dyn_pressure) print(frag_point_dyn_pressure, frag_point_height_km, [ lower_frag_point_dyn_pressure, higher_frag_point_dyn_pressure ]) # Plot the errorbar plt.errorbar(frag_point_dyn_pressure, frag_point_height_km, \ xerr=[[lower_error], [higher_error]], fmt='--', capsize=5, zorder=4, \ color=colors_frags[frag], label='+/- 25% $\\rho_{atm}$, 2$\\sigma_v$ ') # Save the computed fragmentation values to list # Site, Reference JD, Relative time, Fragment ID, Height, Dyn pressure, Dyn pressure lower \ # bound, Dyn pressure upper bound computed_values.append([site_no, jd_ref, frag_point_time, frag_info.frag_dict[frag], \ frag_point_height_km, frag_point_dyn_pressure, lower_frag_point_dyn_pressure, \ higher_frag_point_dyn_pressure]) ###### # Plot reference time plt.title('Reference time: ' + str(jd2Date(jd_ref, dt_obj=True))) plt.xlabel('Dynamic pressure (kPa)') plt.ylabel('Height (km)') plt.ylim([ht_min / 1000, ht_max / 1000]) # Remove repeating labels and plot the legend handles, labels = plt.gca().get_legend_handles_labels() by_label = OrderedDict(zip(labels, handles)) plt.legend(by_label.values(), by_label.keys()) plt.grid(color='0.9') # Create the label for seconds ax2 = plt.gca().twinx() ax2.set_ylim([time_max, time_min]) ax2.set_ylabel('Time (s)') plt.savefig(os.path.join(dir_path, file_name_prefix \ + '_fragments_dyn_pressures_site_{:s}.png'.format(str(site_no))), dpi=300) plt.show() ### PLOT DYNAMICS MASSES FOR ALL FRAGMENTS for frag, decel_fit in zip(fragments, decel_list): # Select only the data points of the current fragment length_frag = [entry for entry in length_list if entry[0] == frag] # Extract the observed data _, time_data, length_data, lat_data, lon_data, height_data = np.array( length_frag).T # Fit a linear dependance of time vs. height line_fit, _ = scipy.optimize.curve_fit(lineFunc, time_data, height_data) ### CALCULATE OBSERVED DYN MASS # Get the velocity at every point in time velocities = exponentialDecelerationVel(time_data, *decel_fit) decelerations = np.abs( exponentialDecelerationDecel(time_data, *decel_fit)) # Calculate the dynamic mass dyn_mass = dynamicMass(frag_info.bulk_density, lat_data, lon_data, height_data, jd_ref, \ velocities, decelerations) ### # Plot Observed height vs. dynamic pressure plt.plot(dyn_mass * 1000, height_data / 1000, color=colors_frags[frag], zorder=3, linewidth=0.75) # Plot the fragment number at the end of each lag plt.text(dyn_mass[-1]*1000, height_data[-1]/1000 - 0.02, str(frag_info.frag_dict[frag]), \ color=colors_frags[frag], size=7, va='top', zorder=3) ### CALCULATE MODELLED DYN MASS time_array = np.linspace(ref_beg_time, max(time_data), 1000) # Calculate the modelled height height_array = lineFunc(time_array, *line_fit) # Get the velocity at every point in time velocities_model = exponentialDecelerationVel( time_array, *decel_fit) # Get the deceleration decelerations_model = np.abs( exponentialDecelerationDecel(time_array, *decel_fit)) # Calculate the modelled dynamic mass dyn_mass_model = dynamicMass(frag_info.bulk_density, np.zeros_like(time_array) + np.mean(lat_data), np.zeros_like(time_array) + np.mean(lon_data), height_array, jd_ref, \ velocities_model, decelerations_model) ### # Plot Modelled height vs. dynamic mass plt.plot(dyn_mass_model*1000, height_array/1000, color=colors_frags[frag], zorder=3, \ linewidth=0.75, linestyle='--', \ label='Frag {:d} initial dyn mass = {:.1e} g'.format(frag_info.frag_dict[frag], \ 1000*dyn_mass_model[0])) # Plot reference time plt.title('Reference time: ' + str(jd2Date(jd_ref, dt_obj=True)) \ + ', $\\rho_m = ${:d} $kg/m^3$'.format(frag_info.bulk_density)) plt.xlabel('Dynamic mass (g)') plt.ylabel('Height (km)') plt.ylim([ht_min / 1000, ht_max / 1000]) # Remove repeating labels and plot the legend handles, labels = plt.gca().get_legend_handles_labels() by_label = OrderedDict(zip(labels, handles)) plt.legend(by_label.values(), by_label.keys()) plt.grid(color='0.9') # Create the label for seconds ax2 = plt.gca().twinx() ax2.set_ylim([time_max, time_min]) ax2.set_ylabel('Time (s)') plt.savefig(os.path.join(dir_path, file_name_prefix \ + '_fragments_dyn_mass_site_{:s}.png'.format(str(site_no))), dpi=300) plt.show() # Plot the light curve if the METAL .met file was given if (metal_mags is not None): # Make sure there are lightcurves in the data if len(metal_mags): lc_min = np.inf lc_max = -np.inf # Plot the lightcurves for site_entry in metal_mags: site_id, time, mags = site_entry # Track the minimum and maximum magnitude lc_min = np.min([lc_min, np.min(mags)]) lc_max = np.max([lc_max, np.max(mags)]) plt.plot(time, mags, marker='+', label='Site: ' + str(site_id), zorder=4, linewidth=1) # Plot times of fragmentation for frag_dt, fragments_list in fragmentations_datetime: # Plot the lines of fragmentation y_arr = np.linspace(lc_min, lc_max, 10) x_arr = [frag_dt] * len(y_arr) plt.plot(x_arr, y_arr, linestyle='--', zorder=4, \ label='Fragmentation: ' + ",".join(fragments_list)) plt.xlabel('Time (UTC)') plt.ylabel('Absolute magnitude (@100km)') plt.grid() plt.gca().invert_yaxis() plt.legend() ### Format the X axis datetimes import matplotlib def formatDT(x, pos=None): x = matplotlib.dates.num2date(x) # Add date to the first tick if pos == 0: fmt = '%D %H:%M:%S.%f' else: fmt = '%H:%M:%S.%f' label = x.strftime(fmt)[:-3] label = label.rstrip("0") label = label.rstrip(".") return label from matplotlib.ticker import FuncFormatter plt.gca().xaxis.set_major_formatter(FuncFormatter(formatDT)) plt.gca().xaxis.set_minor_formatter(FuncFormatter(formatDT)) ### plt.tight_layout() # Save the figure plt.savefig(os.path.join(dir_path, file_name_prefix + '_fragments_light_curve_comparison.png'), \ dpi=300) plt.show() # Save the computed values to file with open( os.path.join(dir_path, file_name_prefix + "_fragments_dyn_pressure_info.txt"), 'w') as f: # Site, Reference JD, Relative time, Fragment ID, Height, Dyn pressure, Dyn pressure lower \ # bound, Dyn pressure upper bound # Write the header f.write( "# Site, Ref JD, Rel time, Frag ID, Ht (km), DP (kPa), DP low, DP high\n" ) # Write computed values for every fragment for entry in computed_values: f.write( " {:>5s}, {:20.12f}, {:+8.6f}, {:7d}, {:7.3f}, {:9.2f}, {:8.2f}, {:8.2f}\n" .format(*entry))