Get initial guess using finite differencing from measurements

Hey all,

I am working on an OD project and I want to get the initial guess for the OD algorithm from the measurements. I want to do this by using finite differencing of the first two measurements. When I use it, there is quite a large discrepancy between the reference state and my initial guess as seen below:

Initial state: finite_differencing
State      | px           | py           | pz           | vx           | vy           | vz          
----------------------------------------------------------------------------------------------------
Reference  | -630804.69   | -2930405.89  | 6399491.22   | -3845.15     | -5715.48     | -2989.68    
Init guess | -644723.19   | -2949291.15  | 6399969.51   | -3510.15     | -4623.28     | -3879.32    
Difference | 1557.23      | 1375.81      | 21018.48     | 1379.60      | 1674.56      | 1873.50

These is some noise on the measurements however I don’t expect it to be this large in the velocity components. So my idea is to transform the measurements (range, azimuth, elevation) to cartesian ECI positions. Then I have P0 and P1, with a time difference dt. By simply saying, dx = P1 - P0 → V = dx/dt (forward differencing from T0). And then the initial state would be X0 = [P0, V].

Does anyone have any suggestions on why this is wrong? Thanks in advance!

using this code:

def topo2ECI(real_measurement, time):
    from org.hipparchus.geometry.euclidean.threed import Vector3D
    
    ran, az, el, radial_vel = real_measurement
    direction = Vector3D(float(ran * math.cos(el) * math.sin(az)), float(ran * math.cos(el) * math.cos(az)), float(ran * math.sin(el)))
    
    pv_topo = PVCoordinates(direction, Vector3D.ZERO)
    
    time = datetime_to_absolutedate(time)

    # Transform direction vector from topocentric to ECEF frame
    direction_ecef = frames['topo'].getTransformTo(frames['ecef'], time).transformPVCoordinates(pv_topo)
    
    # Convert direction vector from ECEF to ECI
    direction_eci = frames['ecef'].getTransformTo(frames['eci'], time).transformPVCoordinates(direction_ecef)
    
    return PV2state(direction_eci)

def getInitialGuess(reference, real_measurements, method='finite_differencing'):
    
    initial_state = reference[real_measurements['first_observation']][1:].astype(float)
    
    
    if method == 'finite_differencing':
        print('Finite differencing is used for the initial guess')
        measurement0 = np.array([real_measurements['range'][0], 
                                real_measurements['azimuth'][0],
                                real_measurements['elevation'][0],
                                real_measurements['range_rate'][0]])
        time0 = real_measurements['time'][0]
        
        measurement1 = np.array([real_measurements['range'][1], 
                                real_measurements['azimuth'][1],
                                real_measurements['elevation'][1],
                                real_measurements['range_rate'][1]])
        time1 = real_measurements['time'][1]
        
        pos0 = topo2ECI(measurement0, time0)
        
        pos1 = topo2ECI(measurement1, time1)
        
        dt = (time1 - time0).total_seconds()
        
        dx = pos1[:3] - pos0[:3]
        
        V = (dx[:3]/ dt) 
   
        x0 = np.array([pos0[:3], V])

Hi @Niek

I was not aware of a method based on finite differences to compute initial guess. However, to determine an initial guess based on a set of measurements, I recommend you to use an Initial Orbit Determination (IOD) algorithm.
Orekit has several algorithms. IodGauss is the best one if you only have angular measurements. If you also have som ranging measurements, you can use IodGooding.

Best regards,
Bryan

Hey Bryian, thanks for the response. I am currently trying out the IOD algorithms, i’d like to use the lamberIOD, but for this I need two position vectors. With the same analysis as the previous, my velocity error is way better but the position error is quite off. Using the Gauss IOD algorithm gives me similar results.
These are some of the results i got:

Initial state: lambert_targeting
State      | px           | py           | pz           | vx           | vy           | vz          
----------------------------------------------------------------------------------------------------
Reference  | -630804.69   | -2930405.89  | 6399491.22   | -3845.15     | -5715.48     | -2989.68    
Init guess | -641765.78   | -2951053.94  | 6397468.01   | -3836.58     | -5684.70     | -3038.15    
Difference | 10961.09     | 20648.04     | 2023.22      | 8.56         | 30.78        | 48.47     


Initial state: gauss
State      | px           | py           | pz           | vx           | vy           | vz          
----------------------------------------------------------------------------------------------------
Reference  | -650021.47   | -2958941.97  | 6384452.69   | -3841.55     | -5698.92     | -3025.72    
Init guess | -612630.83   | -2952267.08  | 6428983.29   | -3937.34     | -5943.58     | -3160.26    
Difference | 37390.64     | 6674.89      | 44530.60     | 95.80        | 244.66       | 134.54     

I think it has something to do with the way I convert the measurements to ECI. I also tried it with clean measurements (no std) but I get similar results. Are these results to be expected or am I doing something wrong? Thanks in advance!

This is my code:

def topo2ECI(real_measurement, time):
    from org.hipparchus.geometry.euclidean.threed import Vector3D
    
    ran, az, el, radial_vel = real_measurement
    
    direction = Vector3D(float(np.cos(el)*np.sin(az)), float(np.cos(el)*np.cos(az)), float(np.sin(el))).scalarMultiply(float(ran))
    
    pv_topo = PVCoordinates(direction, Vector3D.ZERO)
    
    time = datetime_to_absolutedate(time)

    direction_ecef = frames['topo'].getTransformTo(frames['ecef'], time).transformPVCoordinates(pv_topo)
    
    direction_eci = frames['ecef'].getTransformTo(frames['eci'], time).transformPVCoordinates(direction_ecef)
    
    return PV2state(direction_eci)


def getInitialGuess(reference, real_measurements, method='finite_differencing'):
    
    initial_state = reference[real_measurements['first_observation']][1:].astype(float)
    
    
    
    if method == 'lambert_targeting':
        from org.orekit.estimation.iod import IodLambert
        print('Lambert targetting is used for the initial guess')
        measurement0 = np.array([real_measurements['range'][0], 
                                real_measurements['azimuth'][0],
                                real_measurements['elevation'][0],
                                real_measurements['range_rate'][0]])
        time0 = real_measurements['time'][0]
        time0_ad = datetime_to_absolutedate(time0)
        
        measurement1 = np.array([real_measurements['range'][-1], 
                                real_measurements['azimuth'][-1],
                                real_measurements['elevation'][-1],
                                real_measurements['range_rate'][-1]])
        time1 = real_measurements['time'][-1]
        time1_ad = datetime_to_absolutedate(time1)
        
        pos0 = topo2ECI(measurement0, time0)
        pos1 = topo2ECI(measurement1, time1)
        
        vec0 = Vector3D(float(pos0[0]), float(pos0[1]), float(pos0[2]))
        vec1 = Vector3D(float(pos1[0]), float(pos1[1]), float(pos1[2]))
        
        pos_object0 = Position(time0_ad, vec0, observationSettings['std'], observationSettings['base_weight'], observationSettings['satellite'])
        
        pos_object1 = Position(time1_ad, vec1, observationSettings['std'], observationSettings['base_weight'], observationSettings['satellite'])

        
     
        lambert = IodLambert(EARTH_MU)
        
        orbit = lambert.estimate(frames['eci'], True, 0, pos_object0, pos_object1)
        
        x0 = np.array([orbit.getPVCoordinates().getPosition().x, 
                       orbit.getPVCoordinates().getPosition().y, 
                       orbit.getPVCoordinates().getPosition().z,
                       orbit.getPVCoordinates().getVelocity().x, 
                       orbit.getPVCoordinates().getVelocity().y, 
                       orbit.getPVCoordinates().getVelocity().z])
        
        
        
    elif method == 'gauss':
        from org.orekit.estimation.iod import IodGauss
        from org.orekit.propagation.analytical import KeplerianPropagator
        num = int(len(real_measurements['time'])/2)
        index = [0, num, -1]
        measurement0 = np.array([(real_measurements['azimuth'][index[0]]),(real_measurements['elevation'][index[0]])])
        time0 = datetime_to_absolutedate(real_measurements['time'][index[0]])
        
        measurement1 = np.array([(real_measurements['azimuth'][index[1]]),(real_measurements['elevation'][index[1]])])
        time1 = datetime_to_absolutedate(real_measurements['time'][index[1]])
        
        measurement2 = np.array([(real_measurements['azimuth'][index[2]]),(real_measurements['elevation'][index[2]])])
        time2 = datetime_to_absolutedate(real_measurements['time'][index[2]])
        
        azel0 = AngularAzEl(observationSettings['ground_station'], time0, [float(measurement0[0]), float(measurement0[1])],[observationSettings['std'], observationSettings['std']], [observationSettings['base_weight'], observationSettings['base_weight']], observationSettings['satellite'])       
        azel1 = AngularAzEl(observationSettings['ground_station'], time1, [float(measurement1[0]), float(measurement1[1])],[observationSettings['std'], observationSettings['std']], [observationSettings['base_weight'], observationSettings['base_weight']], observationSettings['satellite'])   
        azel2 = AngularAzEl(observationSettings['ground_station'], time2, [float(measurement2[0]), float(measurement2[1])],[observationSettings['std'], observationSettings['std']], [observationSettings['base_weight'], observationSettings['base_weight']], observationSettings['satellite'])   
        
        
        gauss = IodGauss(EARTH_MU)
        orbit = gauss.estimate(frames['eci'], azel0, azel1, azel2)
        
        prop = KeplerianPropagator(orbit)
        state = prop.propagate(time0)
        x0 = np.array([state.getPVCoordinates().getPosition().x, 
                       state.getPVCoordinates().getPosition().y, 
                       state.getPVCoordinates().getPosition().z,
                       state.getPVCoordinates().getVelocity().x, 
                       state.getPVCoordinates().getVelocity().y, 
                       state.getPVCoordinates().getVelocity().z])

Hi @Niek

I don’t understand the need of converting Az/El measurement to satellite position with topocentric to ECI transformations.
Converting station based measurements to a satellite position is the purpose of an IOD algorithme is you want an initial guess for your orbit determination.
Because you are working with Az-El, I don’t think that using a position based IOD method like Lambert or Gibbs is a good idea. These two methods are usefull for people working with on-board GNSS measurements.

In my opinion, the best candidate for your need is the Gauss IOD method. If you also have Ranging data, so you can use the Gooding method which gives better results.
In order to understand why Gauss results are bad for the estimated position, what is the time difference between azel0 and azel2?
Also, what is your orbit type? (LEO, polar, heliosynchronous, MEO)

Please find below an interesting topic close to what you want to achieve:

Thank you and best regards,
Bryan

A last question, when you instanciate the Orekit objects, are the azimuth and elevation expressed in radians?

Hey Bryan,

Thanks for the response, I’ll just use the Gauss method as you suggested!