def continue_condition(state_i):

    # Define the rough ground level
    lat = ECEF2LLH(np.vstack((state_i[:3])))[0]
    R_sealevel = EarthRadius(lat)
    r_end = R_sealevel  #+ h_ground
    r_beg = R_sealevel + 120e3

    # brightness = fn()
    # if brightness < threshold: # Not luminous anymore
    if norm(state_i[3:6]) < 2e3:  # Get a better measure
        print('Successfully found the end of the trajectory: v < 2km/s.')
        return False  # Ends the integration
    elif norm(state_i[:3]) > r_beg:  # Escaped the atmosphere
        print('Successfully found the start of the trajectory: h > 120km.')
        return False  # Ends the integration
    elif norm(state_i[:3]) < r_end:  # Reached ground
        print('Your meteoroid became a meteorite!!')
        return False  # Ends the integration
    elif state_i[6] < 1e-3:  # Lost all mass [<1g]
        print('Your meteoroid is dust!')
        return False  # Ends the integration
    else:
        return True  # Continues integration
Ejemplo n.º 2
0
def PointTriangulation(data, directory=None, mc=1):

    # Find the times with multiple data points
    [dt_vals, dt_counts] = np.unique(data['time'], return_counts=True)
    dt_vals_plus = dt_vals[dt_counts > 1]

    # Triangulate all paired points
    x = np.zeros((3, len(dt_vals_plus)))
    cov = np.zeros((3, 3, len(dt_vals_plus)))

    for k, dt_val in enumerate(dt_vals_plus):

        # Get all the observation data
        obs_ECEF_all = []
        for i in range(dt_counts[dt_vals == dt_val][0]):

            Times = np.array(data['time'])

            obs_lat = data['obs_lat'][Times == dt_val][i]
            obs_lon = data['obs_lon'][Times == dt_val][i]
            obs_hei = data['obs_hei'][Times == dt_val][i]

            obs_LLH = np.vstack((obs_lat, obs_lon, obs_hei))
            obs_ECEF = LLH2ECEF(obs_LLH)

            obs_ECEF_all.append(obs_ECEF)

        x_particles = np.zeros((3, mc))
        for particle in range(mc):

            # Calculate each line of sight in the paired points
            UV_ECEF_all = []
            for i in range(dt_counts[dt_vals == dt_val][0]):

                Times = np.array(data['time'])

                # Extract some raw data
                alt = np.deg2rad(data['altitude'][Times == dt_val][i])
                azi = np.deg2rad(data['azimuth'][Times == dt_val][i])

                if mc != 1:
                    alt_err = np.deg2rad(
                        data['err_plus_altitude'][Times == dt_val][i])
                    azi_err = np.deg2rad(
                        data['err_plus_azimuth'][Times == dt_val][i])
                    alt += np.random.normal(0, alt_err)
                    azi += np.random.normal(0, azi_err)

                # Convert from spherical to cartesian
                UV_ENU = np.vstack(
                    (np.cos(alt) * np.sin(azi), np.cos(alt) * np.cos(azi),
                     np.sin(alt)))

                # Convert from ENU to ECEF coordinates
                UV_ECEF = ENU2ECEF(
                    data['obs_lon'][Times == dt_val][i],
                    data['obs_lat'][Times == dt_val][i]).dot(UV_ENU)

                UV_ECEF_all.append(UV_ECEF)

            # Calculate the best position between the first two lines
            x_est = ShortestMidPoint(obs_ECEF_all, UV_ECEF_all)

            # Minimizing angular separation between all the lines
            result = minimize(TotalAngSep,
                              x_est,
                              args=(obs_ECEF_all, UV_ECEF_all),
                              method='Nelder-Mead')

            x_particles[:, particle] = result.x

        if mc > 1:
            PointsTable = Table()
            PointsTable['datetime'] = [data['datetime'][Times == dt_val][0]
                                       ] * mc
            PointsTable['X_geo'] = x_particles[0]
            PointsTable['Y_geo'] = x_particles[1]
            PointsTable['Z_geo'] = x_particles[2]
            partices_LLH = ECEF2LLH(x_particles)
            PointsTable['latitude'] = np.rad2deg(partices_LLH[0])
            PointsTable['longitude'] = np.rad2deg(partices_LLH[1])
            PointsTable['height'] = partices_LLH[2]
            PointsFile = os.path.join(directory,
                                      '{:.3f}_particles.ecsv'.format(dt_val))
            PointsTable.write(PointsFile, format='ascii.ecsv', delimiter=',')
            print('\Points file written to: ' + PointsFile)
            Points(PointsFile)

        x[:, k] = np.mean(x_particles, axis=1)
        cov[:, :, k] = np.cov(x_particles)

        print('\nTime step: {0:.2f} sec'.format(dt_val))
        # print('Normalised Residuals Before: {0:8.4f}'.format(
        #     np.rad2deg(TotalAngSep(x_est, obs_ECEF_all, UV_ECEF_all))*3600))
        # print('Normalised Residuals After:  {0:8.4f}'.format(
        #     np.rad2deg(result.fun)*3600))

    if mc == 1:
        return x, dt_vals_plus, dt_counts[dt_counts > 1]
    else:
        return x, dt_vals_plus, dt_counts[dt_counts > 1], cov
Ejemplo n.º 3
0
         24 * 60 * 60), 2)

    if errors:
        mc = 1000  # Number of particles
        ParticleFolder = os.path.join(PointDir,
                                      str(mc) + '_particles_per_timestep')
        os.mkdir(ParticleFolder)

        [Pos_ECEF, t_rel, N_cams,
         Cov_ECEF] = PointTriangulation(AllData, ParticleFolder, mc)
    else:
        [Pos_ECEF, t_rel, N_cams] = PointTriangulation(AllData)

    # Coordinate transforms
    T_jd = t0.jd + t_rel / (24 * 60 * 60)
    Pos_LLH = ECEF2LLH(Pos_ECEF)
    Pos_ECI = ECEF2ECI(Pos_ECEF, Pos_ECEF, T_jd)[0]

    # Raw velocity calculations
    vel_eci = norm(Pos_ECI[:, 1:] - Pos_ECI[:, :-1],
                   axis=0) / (t_rel[1:] - t_rel[:-1])
    vel_geo = norm(Pos_ECEF[:, 1:] - Pos_ECEF[:, :-1],
                   axis=0) / (t_rel[1:] - t_rel[:-1])
    gamma = np.arcsin((Pos_LLH[2][1:] - Pos_LLH[2][:-1]) /
                      (vel_geo * (t_rel[1:] - t_rel[:-1])))
    vel_eci = np.hstack((vel_eci, np.nan))
    vel_geo = np.hstack((vel_geo, np.nan))
    gamma = np.hstack((gamma, np.nan))

    t_isot_col = Column(name='datetime',
                        data=Time(T_jd, format='jd', scale='utc').isot)
def NRLMSISE_00(pos, time, pos_type='eci'):
    ''' Courtesy of Ellie Sansom '''
    """
    Inputs: inertial position and time
    Outputs: [altitude, temp, atm_pres, atm density, sos, dyn_vis]
    """

    from nrlmsise_00_header import nrlmsise_input, nrlmsise_output, nrlmsise_flags
    from nrlmsise_00 import gtd7

    time = Time(time, format='jd', scale='utc')

    # Convert ECI to LLH coordinates
    if pos_type == 'eci':
        Pos_LLH = ECEF2LLH(ECI2ECEF_pos(pos, time))
    elif pos_type == 'ecef':
        Pos_LLH = ECEF2LLH(pos)
    elif pos_type == 'llh':
        Pos_LLH = pos
    else:
        print('NRLMSISE_00 error: Invalid pos_type')
        exit()
    g_lat = np.rad2deg(Pos_LLH[0][0])
    g_long = np.rad2deg(Pos_LLH[1][0])
    alt = Pos_LLH[2][0]

    # Break up time into year, day of year, and seconds of the day
    yDay = time.yday.split(':')
    yr = float(yDay[0])
    doy = float(yDay[1])
    sec = float(yDay[2]) * 60 * 60 + float(yDay[3]) * 60 + float(yDay[4])

    # Assign our variables into the nrmsise inputs
    Input = nrlmsise_input(yr, doy, sec, alt / 1000, g_lat, g_long)
    Output = nrlmsise_output()
    Flags = nrlmsise_flags()

    # Switches
    for i in range(1, 24):
        Flags.switches[i] = 1

    # GTD7 atmospheric model subroutine
    gtd7(Input, Flags, Output)

    # Temperature at alt [deg K]
    T = Output.t[1]

    # Molecular number densities [m-3]
    He = Output.d[0]  # He
    O = Output.d[1]  # O
    N2 = Output.d[2]  # N2
    O2 = Output.d[3]  # O2
    Ar = Output.d[4]  # Ar
    H = Output.d[6]  # H
    N = Output.d[7]  # N
    #    ano_O  = Output.d[8] # Anomalous oxygen
    sum_mass = He + O + N2 + O2 + Ar + H + N

    # Molar mass
    He_mass = 4.0026  # g/mol
    O_mass = 15.9994  # g/mol
    N2_mass = 28.013  # g/mol
    O2_mass = 31.998  # g/mol
    Ar_mass = 39.948  # g/mol
    H_mass = 1.0079  # g/mol
    N_mass = 14.0067  # g/mol

    # Molecular weight of air [kg/mol]
    mol_mass_air = (He_mass * He + O_mass * O + N2_mass * N2 + O2_mass * O2 +
                    Ar_mass * Ar + H_mass * H + N_mass * N) / (1000 * sum_mass)

    # Total mass density [kg*m-3]
    po = Output.d[5] * 1000

    Ru = 8.3144621  # Universal gas constant [J/(K*mol)]
    R = Ru / mol_mass_air  # Individual gas constant [J/(kg*K)] #287.058

    # Ideal gas law
    atm_pres = po * T * R

    # Speed of sound in atm
    sos = 331.3 * np.sqrt(1 + T / 273.15)

    # Dynamic viscosity (http://en.wikipedia.org/wiki/Viscosity)
    C = 120  #Sutherland's constant for air [deg K]
    mu_ref = 18.27e-6  # Reference viscosity [[mu_Pa s] * e-6]
    T_ref = 291.15  # Reference temperature [deg K]

    dyn_vis = mu_ref * (T_ref + C) / (T + C) * (T / T_ref)**1.5

    return T, atm_pres, po, sos, dyn_vis
def EarthDynamics(t, X, WindData, t0, return_abs_mag=False):
    '''
    The state rate dynamics are used in Runge-Kutta integration method to 
    calculate the next set of equinoctial element values.
    '''
    ''' State Rates '''
    # State parameter vector decomposed
    Pos_ECI = np.vstack((X[:3]))
    Vel_ECI = np.vstack((X[3:6]))
    M = X[6]
    rho = X[7]
    A = X[8]
    c_ml = X[9]  # c_massloss = sigma*cd_hyp
    t_jd = t0 + t / (24 * 60 * 60)  # Absolute time [jd]
    ''' Primary Gravitational Acceleration '''
    a_grav = gravity_vector(Pos_ECI)
    ''' Atmospheric Drag Perturbation - Better Model Needed '''
    Pos_ECEF = ECI2ECEF_pos(Pos_ECI, t_jd)
    Pos_LLH = ECEF2LLH(Pos_ECEF)
    # Atmospheric velocity
    if type(WindData) == Table:  #1D vertical profile
        maxwindheight = max(WindData['# Height'])
        if float(Pos_LLH[2]) > maxwindheight:
            [v_atm, rho_a, temp] = AtmosphericModel([], Pos_ECI, t_jd)
        else:
            [v_atm, rho_a, temp] = AtmosphericModel(WindData, Pos_ECI, t_jd)

    else:  #3D wind profile
        maxwindheight = max(WindData[2, :, :, :])
        if float(Pos_LLH[2]) > maxwindheight:
            [v_atm, rho_a, temp] = AtmosphericModel([], Pos_ECI, t_jd)
        else:
            [Wind_ENU, rho_a, temp] = WRF3D(WindData, Pos_LLH)
            Wind_ECEF = ENU2ECEF(Pos_LLH[1], Pos_LLH[0]).dot(Wind_ENU)
            v_atm = ECEF2ECI(Pos_ECEF, Wind_ECEF, t_jd)[1]

    # Velocity relative to the atmosphere
    v_rel = Vel_ECI - v_atm
    v = norm(v_rel)

    # New drag equations - function that fits the literature
    cd = dragcoeff(v, temp, rho_a, A)[0]

    # Total drag perturbation
    a_drag = -cd * A * rho_a * v * v_rel / (2 * M**(1. / 3) * rho**(2. / 3))
    ''' Total Perturbing Acceleration '''
    a_tot = a_grav + a_drag

    # Mass-loss equation
    dm_dt = -c_ml * A * rho_a * v**3 * M**(2. / 3) / (2 * rho**(2. / 3))

    # See (Sansom, 2019) as reference
    if return_abs_mag:  # sigma = c_ml / cd
        lum = -X[10] * (v**2 / 2 + cd / c_ml) * dm_dt * 1e7
        return -2.5 * np.log10(lum / 1.5e10)
    ''' State Rate Equation '''
    X_dot = np.zeros(X.shape)
    X_dot[:3] = Vel_ECI.flatten()
    X_dot[3:6] = a_tot.flatten()
    X_dot[6] = dm_dt

    return X_dot
def Propagate(state0, args):
    '''
    Inputs: Initial ECI position (m), ECI velocity (m/s), and mass (kgs).
    Outputs: ECI position (m), ECI velocity (m/s) throughout the dark flight.
    '''
    # Calculate the meteor's initial state parameters
    Pos_ECI = np.vstack((state0[1:4]))
    t0 = float(state0[0])
    X = state0.flatten()[1:]  # Add mass to the state
    [WindData, h_ground, windspd_err, winddir_err, mc] = args

    newWindData = copy.deepcopy(WindData)
    if mc:  # Randomise every layer independently
        if type(newWindData) == Table:  #wind profile
            #    newWindData['Wind'] = WindData['Wind'] + np.random.normal( 0.0, windspd_err, size = len(WindData['Wind']) )
            #    newWindData['WDir'] = WindData['WDir'] + np.random.normal( 0.0, winddir_err, size = len(WindData['Wind']) )
            newWindData['Wind'] = WindData['Wind'] + np.random.uniform(
                -windspd_err, windspd_err, size=len(WindData['Wind']))
            newWindData['WDir'] = WindData['WDir'] + np.random.uniform(
                -windspd_err, winddir_err, size=len(WindData['Wind']))
            newWindData['WDir'] = np.mod(newWindData['WDir'],
                                         360.0)  #mod the wind to 0-360a
        else:  #3d wind ###TODO untested
            #winddata[3] wind north
            newWindData[3, :, :, :] = WindData[3, :, :, :] + np.random.uniform(
                -windspd_err, windspd_err, size=np.shape(WindData[3, :, :, :]))
            #winddata[4] wind east
            newWindData[4, :, :, :] = WindData[4, :, :, :] + np.random.uniform(
                -windspd_err, windspd_err, size=np.shape(WindData[4, :, :, :]))
            #winddata[5] wind up
            #left untouched

    elevation_data = srtm.get_data(local_cache_dir=SRTM_cache)

    # Initialise the time step
    #    R_sealevel = EarthRadius(ECEF2LLH(Pos_ECI)[0])
    #    r_end = R_sealevel + h_ground

    state, T_rel = [], []

    def solout(t, X):
        state.extend([X.copy()])
        T_rel.extend([t])
        Pos_ECI = np.vstack((X[:3]))
        Pos_ECF = ECI2ECEF_pos(Pos_ECI, t0 + (t / (24 * 60 * 60)))
        lt, ln, ht = ECEF2LLH(Pos_ECF)
        R_sealevel = EarthRadius(lt)
        if h_ground == 'a':
            #            print( 'lat,lon, time, ', np.rad2deg(lt).item(), np.rad2deg(ln).item(), str(t+t0) )
            h_gnd = elevation_data.get_elevation(
                np.rad2deg(lt).item(),
                np.rad2deg(ln).item())
            if h_gnd == None:  #in case of voids or geo file not found
                print('WARNING elevation file issue, ',
                      np.rad2deg(lt).item(),
                      np.rad2deg(ln).item(), ht, X[6])
                h_gnd = 0.0
        else:
            h_gnd = float(h_ground)
        r_end = R_sealevel + h_gnd
        if norm(X[:3]) < r_end:  # Reached ground or below
            return -1  # Ends the integration
        elif X[6] < 1e-3:  # Lost all mass [<1g]
            print('Your meteoroid is dust!')
            return -1  # Ends the integration
        else:
            return 0  # Continues integration

    # Setup integrator
    dt0 = 0.1
    dt_max = 3  #60 # sec
    solver = ode(EarthDynamics).set_integrator('dopri5', \
        first_step=dt0, max_step=dt_max, rtol=1e-4) #'dop853',
    solver.set_solout(solout)
    solver.set_initial_value(X, 0).set_f_params(newWindData, t0)

    # Integrate with RK4 until impact
    t_max = np.inf
    solver.integrate(t_max)

    # Assign the variables
    T = np.array(T_rel) / (24 * 60 * 60) + t0
    X_all = np.array(state).T

    # Make sure we end precisely on the ground, rather than below
    #so we backtrack a fraction of a step
    Pos_ECF_final = ECI2ECEF_pos(X_all[:3, -1], T[-1])
    lt, ln, ht = ECEF2LLH(Pos_ECF_final)
    if h_ground == 'a':
        h_gnd = elevation_data.get_elevation(
            np.rad2deg(lt).item(),
            np.rad2deg(ln).item())
        if h_gnd == None:
            h_gnd = 0.0
            print('WARNING final ground issue, ',
                  np.rad2deg(lt).item(),
                  np.rad2deg(ln).item(), ht, X_all[6])
    else:
        h_gnd = float(h_ground)
    r_end = EarthRadius(lt) + h_gnd
    fraction = (norm(X_all[:3, -2:-1]) - r_end) / (norm(X_all[:3, -2:-1]) -
                                                   norm(X_all[:3, -1:]))
    if len(X_all[0]) > 1:
        X_all[:, -1:] = X_all[:, -2:-1] + fraction * (X_all[:, -1:] -
                                                      X_all[:, -2:-1])
        T[-1] = T[-2] + fraction * (T[-1] - T[-2])

    Pos_ECI = X_all[:3]
    Vel_ECI = X_all[3:6]
    M = X_all[6]
    rho = X_all[7]
    A = X_all[8]
    c_ml = X_all[9]

    return {
        'time_jd': T,
        'pos_eci': Pos_ECI,
        'vel_eci': Vel_ECI,
        'mass': M,
        'rho': rho,
        'shape': A,
        'c_ml': c_ml
    }
Ejemplo n.º 7
0
def AtmosphericModel(WindData, Pos_ECI, t_jd):

    # The rotational component of the wind in the ECI frame
    north_pole_ecef = np.vstack((0, 0, 6356.75231e3))
    north_pole_eci = ECEF2ECI_pos(north_pole_ecef, t_jd)
    earth_ang_rot = OMEGA_EARTH * north_pole_eci / norm(north_pole_eci)
    v_rot = np.cross(earth_ang_rot, Pos_ECI, axis=0)

    if len(WindData):

        #        Pos_LLH = ECEF2LLH(Pos_ECI) # There is a max of +-40m height error here
        Pos_ECEF = ECEF2LLH(Pos_ECI)
        Pos_LLH = ECEF2LLH(Pos_ECEF)
        h = float(Pos_LLH[2])  # (m)

        # Get the relevent wind data from that height
        if h > max(
                WindData['# Height']):  # Think about using a model for +30km
            i = np.argmax(WindData['# Height'])
            Wind = WindData['Wind'][i]  # (m/s)
            WDir = WindData['WDir'][i]  # (deg)
            TempK = WindData['TempK'][i]  # (deg K)
            Press = WindData['Press'][i]  # (Pa)
            RHum = WindData['RHum'][i]  # (%)

        elif h <= max(WindData['# Height']) and h >= min(WindData['# Height']):
            Wind = interp1d(WindData['# Height'],
                            WindData['Wind'],
                            kind='cubic')(h)
            WDir = interp1d(WindData['# Height'],
                            WindData['WDir'],
                            kind='cubic')(h)
            TempK = interp1d(WindData['# Height'],
                             WindData['TempK'],
                             kind='cubic')(h)
            Press = interp1d(WindData['# Height'],
                             WindData['Press'],
                             kind='cubic')(h)
            RHum = interp1d(WindData['# Height'],
                            WindData['RHum'],
                            kind='cubic')(h)

        elif h < min(WindData['# Height']):
            i = np.argmin(WindData['# Height'])
            Wind = WindData['Wind'][i]  # (m/s)
            WDir = WindData['WDir'][i]  # (deg)
            TempK = WindData['TempK'][i]  # (deg K)
            Press = WindData['Press'][i]  # (Pa)
            RHum = WindData['RHum'][i]  # (%)
        else:
            input('There is a problem in AtomosphericModel.')

        # Calculate the atmospheric density
        rho_a = density_from_pressure(TempK, Press, RHum)  # (kg/m3)

        # Construct the wind vector (start with clockwise N=0, coming from!!)
        Wind_ENU = -np.vstack((Wind * np.sin(np.deg2rad(WDir)),
                               Wind * np.cos(np.deg2rad(WDir)), 0))

        # Convert the ENU to ECI coordinates (using lat/lon from ECI frame)
        lat = float(Pos_LLH[0])
        lon = float(Pos_LLH[1])  # (rad)
        C_ENU2ECI = np.array([[
            -np.sin(lon), -np.sin(lat) * np.cos(lon),
            np.cos(lat) * np.cos(lon)
        ], [
            np.cos(lon), -np.sin(lat) * np.sin(lon),
            np.cos(lat) * np.sin(lon)
        ], [0, np.cos(lat), np.sin(lat)]])

        Wind_ECI = np.dot(C_ENU2ECI, Wind_ENU) + v_rot

    else:
        #no winds, use NRLMSISE model
        Pos_ECEF = ECI2ECEF_pos(Pos_ECI, t_jd)
        Pos_LLH = ECEF2LLH(Pos_ECEF)
        h = float(Pos_LLH[2])  # (m)
        [TempK, Press, rho_a] = NRLMSISE_00(Pos_LLH, t_jd, pos_type='llh')[:3]
        Wind_ENU = np.vstack((0, 0, 0))
        Wind_ECI = v_rot

    # Save the variables
    WRF_history.append(np.vstack((h, Wind_ENU, rho_a, TempK)))

    return Wind_ECI, rho_a, TempK
def Propagate(state0, args):
    '''
    Inputs: Initial ECI position (m), ECI velocity (m/s), and mass (kgs).
    Outputs: ECI position (m), ECI velocity (m/s) throughout the dark flight.
    '''
    # Calculate the meteor's initial state parameters
    Pos_ECI = np.vstack((state0[1:4]))
    t0 = float(state0[0])
    X = state0.flatten()[1:]  # Add mass to the state
    [WindData, h_ground] = args

    # Initialise the time step
    R_sealevel = EarthRadius(ECEF2LLH(Pos_ECI)[0])
    r_end = R_sealevel + h_ground

    state, T_rel = [], []

    def solout(t, X):
        state.extend([X.copy()])
        T_rel.extend([t])
        if norm(X[:3]) < r_end:  # Reached ground
            return -1  # Ends the integration
        elif X[6] < 1e-3:  # Lost all mass [<1g]
            print('Your meteoroid is dust!')
            return -1  # Ends the integration
        else:
            return 0  # Continues integration

    # Setup integrator
    dt0 = 0.1
    dt_max = 5  #60 # sec
    solver = ode(EarthDynamics).set_integrator('dopri5', \
        first_step=dt0, max_step=dt_max, rtol=1e-4) #'dop853',
    solver.set_solout(solout)
    solver.set_initial_value(X, 0).set_f_params(WindData, t0)

    # Integrate with RK4 until impact
    t_max = np.inf
    solver.integrate(t_max)

    # Assign the variables
    T = np.array(T_rel) / (24 * 60 * 60) + t0
    X_all = np.array(state).T

    # Make sure we end on the ground
    r_end = EarthRadius(ECEF2LLH(X_all[:3, -1:])[0]) + h_ground
    fraction = (norm(X_all[:3, -2:-1]) - r_end) / (norm(X_all[:3, -2:-1]) -
                                                   norm(X_all[:3, -1:]))

    if len(X_all[0]) > 1:
        X_all[:, -1:] = X_all[:, -2:-1] + fraction * (X_all[:, -1:] -
                                                      X_all[:, -2:-1])
        T[-1] = T[-2] + fraction * (T[-1] - T[-2])

    Pos_ECI = X_all[:3]
    Vel_ECI = X_all[3:6]
    M = X_all[6]
    rho = X_all[7]
    A = X_all[8]
    c_ml = X_all[9]

    return {
        'time_jd': T,
        'pos_eci': Pos_ECI,
        'vel_eci': Vel_ECI,
        'mass': M,
        'rho': rho,
        'shape': A,
        'c_ml': c_ml
    }
def EarthDynamics(t, X, WindData, t0, return_abs_mag=False):
    '''
    The state rate dynamics are used in Runge-Kutta integration method to 
    calculate the next set of equinoctial element values.
    '''
    ''' State Rates '''
    # State parameter vector decomposed
    Pos_ECI = np.vstack((X[:3]))
    Vel_ECI = np.vstack((X[3:6]))
    M = X[6]
    rho = X[7]
    A = X[8]
    c_ml = X[9]  # c_massloss = sigma*cd_hyp
    t_jd = t0 + t / (24 * 60 * 60)  # Absolute time [jd]
    ''' Primary Gravitational Acceleration '''
    a_grav = gravity_vector(Pos_ECI)
    ''' Atmospheric Drag Perturbation - Better Model Needed '''
    # Atmospheric velocity
    if type(WindData) == Table:  #1D vertical profile
        [v_atm, rho_a, temp] = AtmosphericModel(WindData, Pos_ECI, t_jd)

    else:  #3D wind profile
        Pos_ECEF = ECI2ECEF_pos(Pos_ECI, t_jd)
        Pos_LLH = ECEF2LLH(Pos_ECEF)

        [Wind_ENU, rho_a, temp] = WRF3D(WindData, Pos_LLH)

        Wind_ECEF = ENU2ECEF(Pos_LLH[1], Pos_LLH[0]).dot(Wind_ENU)
        v_atm = ECEF2ECI(Pos_ECEF, Wind_ECEF, t_jd)[1]

    # Velocity relative to the atmosphere
    v_rel = Vel_ECI - v_atm
    v = norm(v_rel)

    # # Constants for drag coeff
    # d = 2 * np.sqrt(A / rho**(2./3) * M**(2./3))
    # mu_a = viscosity(temp) # Air Viscosity (Pa.s)
    # mach = v / SoS(temp) # Mach Number
    # re = reynolds(v, rho_a, mu_a, d) # Reynolds Number
    # kn = knudsen(mach, re) # Knudsen Number
    # cd = dragcoef(re, mach, kn)#, A) # Drag Coefficient
    # # cd = 2.0 # Approximation

    # New drag equations - function that fits the literature
    cd = dragcoeff(v, temp, rho_a, A)[0]
    # cd = 1

    # # New drag equations 2
    # mach = v / SoS(temp) # Mach Number
    # cd = dragcoefff(mach, A)

    # Total drag perturbation
    a_drag = -cd * A * rho_a * v * v_rel / (2 * M**(1. / 3) * rho**(2. / 3))
    ''' Total Perturbing Acceleration '''
    a_tot = a_grav + a_drag

    # Mass-loss equation
    dm_dt = -c_ml * A * rho_a * v**3 * M**(2. / 3) / (2 * rho**(2. / 3))

    # See (Sansom, 2019) as reference
    if return_abs_mag:  # sigma = c_ml / cd
        lum = -X[10] * (v**2 / 2 + cd / c_ml) * dm_dt * 1e7
        return -2.5 * np.log10(lum / 1.5e10)
    ''' State Rate Equation '''
    X_dot = np.zeros(X.shape)
    X_dot[:3] = Vel_ECI.flatten()
    X_dot[3:6] = a_tot.flatten()
    X_dot[6] = dm_dt

    return X_dot
def write_rays_kml(obs_lat,
                   obs_lon,
                   obs_hei,
                   alt,
                   azi,
                   dist,
                   cam_name,
                   outputname,
                   colour='33ff0000'):

    # Convert from spherical to cartesian
    UV_ENU = np.vstack(
        (np.cos(alt) * np.sin(azi), np.cos(alt) * np.cos(azi), np.sin(alt)))

    UV_ECEF = ENU2ECEF(obs_lon, obs_lat).dot(UV_ENU)
    Obs_ECEF = LLH2ECEF(np.vstack((obs_lat, obs_lon, obs_hei)))

    Proj_ECEF = Obs_ECEF + dist * UV_ECEF
    Proj_LLH = ECEF2LLH(Proj_ECEF)

    [Proj_lat, Proj_lon, Proj_hei] = [Proj_LLH[0], Proj_LLH[1], Proj_LLH[2]]

    # Open the file to be written.
    f = open(outputname, 'w')

    f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
    f.write('<kml xmlns="http://earth.google.com/kml/2.1">\n')
    f.write('<Document>\n')
    f.write('<open>1</open>\n')
    f.write('<Placemark>\n')
    f.write('    <Style id="camera">\n')
    f.write('        <LineStyle>\n')
    f.write('            <width>1</width>\n')
    f.write('        </LineStyle>\n')
    f.write('        <PolyStyle>\n')
    f.write('            <color>' + str(colour) + '</color>\n')
    f.write('        </PolyStyle>\n')
    f.write('    </Style>\n')
    f.write('    <styleUrl>#camera</styleUrl>\n')
    f.write('<name>' + str(cam_name) + '</name>\n')
    f.write('<Polygon>\n')
    f.write('    <extrude>0</extrude>\n')
    f.write('    <altitudeMode>absolute</altitudeMode>\n')
    f.write('    <outerBoundaryIs>\n')
    f.write('        <LinearRing>\n')
    f.write('        <coordinates>\n')
    f.write('            ' + str(np.rad2deg(obs_lon)) + ',' +
            str(np.rad2deg(obs_lat)) + ',' + str(obs_hei) + '\n')
    for i in range(len(Proj_lat)):
        f.write('                ' + str(np.rad2deg(Proj_lon[i])) + "," +
                str(np.rad2deg(Proj_lat[i])) + "," + str(Proj_hei[i]) + '\n')
        f.write('                ' + str(np.rad2deg(obs_lon)) + ',' +
                str(np.rad2deg(obs_lat)) + ',' + str(obs_hei) + '\n')
    f.write('        </coordinates>\n')
    f.write('        </LinearRing>\n')
    f.write('    </outerBoundaryIs>\n')
    f.write('</Polygon>\n')
    f.write('</Placemark>\n')
    f.write('</Document>\n')
    f.write('</kml>\n')

    f.close()

    return {'fname': outputname, 'kml_type': 'rays', 'camera': cam_name}