1   /* Copyright 2002-2026 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.orekit.estimation.iod;
19  
20  import java.util.List;
21  
22  import org.hipparchus.geometry.euclidean.threed.Vector3D;
23  import org.hipparchus.util.FastMath;
24  import org.junit.jupiter.api.Assertions;
25  import org.junit.jupiter.api.Test;
26  import org.orekit.bodies.GeodeticPoint;
27  import org.orekit.bodies.OneAxisEllipsoid;
28  import org.orekit.estimation.Context;
29  import org.orekit.estimation.EstimationTestUtils;
30  import org.orekit.estimation.measurements.AngularAzEl;
31  import org.orekit.estimation.measurements.AngularAzElMeasurementCreator;
32  import org.orekit.estimation.measurements.AngularRaDec;
33  import org.orekit.estimation.measurements.AngularRaDecMeasurementCreator;
34  import org.orekit.estimation.measurements.GroundStation;
35  import org.orekit.estimation.measurements.ObservableSatellite;
36  import org.orekit.estimation.measurements.ObservedMeasurement;
37  import org.orekit.estimation.measurements.PVMeasurementCreator;
38  import org.orekit.frames.Frame;
39  import org.orekit.frames.FramesFactory;
40  import org.orekit.frames.TopocentricFrame;
41  import org.orekit.orbits.KeplerianOrbit;
42  import org.orekit.orbits.Orbit;
43  import org.orekit.orbits.OrbitType;
44  import org.orekit.orbits.PositionAngleType;
45  import org.orekit.propagation.Propagator;
46  import org.orekit.propagation.SpacecraftState;
47  import org.orekit.propagation.conversion.NumericalPropagatorBuilder;
48  import org.orekit.time.AbsoluteDate;
49  import org.orekit.time.Month;
50  import org.orekit.time.TimeScalesFactory;
51  import org.orekit.utils.Constants;
52  import org.orekit.utils.IERSConventions;
53  
54  /**
55   *
56   * Source: <a href="http://ccar.colorado.edu/asen5050/projects/projects_2012/kemble/gibbs_derivation.html">...</a>
57   *
58   * @author Joris Olympio
59   * @since 7.1
60   *
61   */
62  class IodGoodingTest extends AbstractIodTest {
63  
64      /** Based on example provided in forum thread:
65       * <a href="https://forum.orekit.org/t/iodgooging-orbit-got-from-three-angular-observations/2749">IodGooding</a> */
66      @Test
67      void testIssue1166RaDec() {
68          AbsoluteDate t1 = new AbsoluteDate(2023, Month.JUNE, 9, 17, 4,59.10, TimeScalesFactory.getUTC());
69          AbsoluteDate t2 = new AbsoluteDate(2023, Month.JUNE, 9, 17, 10,50.66, TimeScalesFactory.getUTC());
70          AbsoluteDate t3 = new AbsoluteDate(2023, Month.JUNE, 9, 17, 16,21.09, TimeScalesFactory.getUTC());
71  
72          Vector3D RA = new Vector3D((15.* (16. + 5./60. + 51.20/3600.)),
73                  (15.*(16.+ 11./60. + 43.73/3600.)), (15.*(16.+ 17./60. + 15.1/3600. )));
74  
75          Vector3D DEC = new Vector3D((-(6.+ 31./60. + 44.22/3600.)),
76                  (-(6. + 31./60. + 52.36/3600.)),
77                  (-(6. +32./60. + 0.03/3600.)));
78  
79          Frame ITRF = FramesFactory.getITRF(IERSConventions.IERS_2010, true);
80          OneAxisEllipsoid earth = new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS ,Constants.WGS84_EARTH_FLATTENING, ITRF);
81          GeodeticPoint stationCoord = new GeodeticPoint(FastMath.toRadians(43.05722), FastMath.toRadians(76.971667), 2735.0);
82          TopocentricFrame stationFrame = new TopocentricFrame(earth, stationCoord, "N42");
83          GroundStation ground_station = new GroundStation(stationFrame);
84  
85          double[] angular1 = {FastMath.toRadians(RA.getX()), FastMath.toRadians(DEC.getX())};
86          double[] angular2 = {FastMath.toRadians(RA.getY()), FastMath.toRadians(DEC.getY())};
87          double[] angular3 = {FastMath.toRadians(RA.getZ()), FastMath.toRadians(DEC.getZ())};
88  
89          AngularRaDec raDec1 = new AngularRaDec(ground_station, FramesFactory.getEME2000(), t1,
90                  angular1, new double[] {1.0, 1.0}, new double[] {1.0, 1.0}, new ObservableSatellite(1));
91          AngularRaDec raDec2 = new AngularRaDec(ground_station, FramesFactory.getEME2000(), t2,
92                  angular2, new double[] {1.0, 1.0}, new double[] {1.0, 1.0}, new ObservableSatellite(1));
93          AngularRaDec raDec3 = new AngularRaDec(ground_station, FramesFactory.getEME2000(), t3,
94                  angular3, new double[] {1.0, 1.0}, new double[] {1.0, 1.0}, new ObservableSatellite(1));
95  
96          // Gauss: {a: 4.238973764054024E7; e: 0.004324857593564294; i: 0.09157752601786696; pa: 170.725916897286; raan: 91.00902931155805; v: -19.971524129451392;}
97          // Laplace: {a: 4.2394495034863256E7; e: 0.004440883687182993; i: 0.09000218139994348; pa: 173.17005925268154; raan: 91.20208239937111; v: -22.60862919684909;}
98          // BEFORE the fix -> Gooding: {a: 6.993021221010809E7; e: 0.3347390725866758; i: 0.5890565053278204; pa: -108.07120996868652; raan: -12.64337508041537; v: 2.587189785272028;}
99          // Values changed slightly after updating to new Lambert solver
100         // BEFORE the update -> Gooding {a: 4.242929828622434E7; e: 0.005085550484861005; i: 0.09455751549021724; pa: 162.64799060142445; raan: 90.00027281152558; v: -10.884841988914873;}
101         // AFTER the update (still forcing an intermediate planar solution) -> Gooding: Keplerian parameters: {a: 4.2400558079418406E7; e: 0.004504568857387906; i: 0.09137634766893886; pa: 170.1160849868576; raan: 90.77888177347695; v: -19.13147024478354;}
102         Orbit estimatedOrbitGooding = new IodGooding(mu).estimate(eme2000, raDec1, raDec2, raDec3);
103         KeplerianOrbit orbitGooding = new KeplerianOrbit(estimatedOrbitGooding);
104         System.out.println(orbitGooding);
105         Assertions.assertEquals(4.2400558079418406E7, orbitGooding.getA(), 1.0e-6);
106         Assertions.assertEquals(0.004504568857387906, orbitGooding.getE(), 1.0e-10);
107         Assertions.assertEquals(FastMath.toRadians(0.09137634766893886), orbitGooding.getI(), 1.0e-10);
108         Assertions.assertEquals(FastMath.toRadians(170.1160849868576), orbitGooding.getPerigeeArgument(), 1.0e-10);
109         Assertions.assertEquals(FastMath.toRadians(90.77888177347695), orbitGooding.getRightAscensionOfAscendingNode(), 1.0e-10);
110         Assertions.assertEquals(FastMath.toRadians(-19.13147024478354), orbitGooding.getTrueAnomaly(), 1.0e-10);
111     }
112 
113     @Test
114     void testIssue1166AzEl() {
115         // Generate measurements
116         final Context context = EstimationTestUtils.eccentricContext("regular-data:potential:tides");
117         final NumericalPropagatorBuilder propagatorBuilder = context.createBuilder(OrbitType.KEPLERIAN, PositionAngleType.TRUE, true, 1.0e-6, 60.0, 0.001);
118         final Propagator propagator = EstimationTestUtils.createPropagator(context.initialOrbit,  propagatorBuilder);
119         final List<ObservedMeasurement<?>> measurements = EstimationTestUtils.createMeasurements(propagator,
120                                                                                                  new AngularAzElMeasurementCreator(context),
121                                                                                                  0.0, 1.0, 60.0);
122         final AngularAzEl azEl1 = (AngularAzEl) measurements.get(0);
123         final AngularAzEl azEl2 = (AngularAzEl) measurements.get(20);
124         final AngularAzEl azEl3 = (AngularAzEl) measurements.get(40);
125 
126         // Gauss: {a: 1.4240687661878748E7; e: 0.16505257340554763; i: 71.54945520547201; pa: 21.27193872599194; raan: 78.78440298193975; v: -163.45049044435925;}
127         Orbit orbitGooding = new IodGooding(mu).estimate(eme2000, azEl1,azEl2,azEl3);
128 
129         // IOD should be close to original
130         SpacecraftState midPoint = propagator.propagate(azEl2.getDate());
131         Assertions.assertTrue(midPoint.getPosition().subtract(orbitGooding.getPosition()).getNorm() < 170.0);
132         Assertions.assertTrue(midPoint.getVelocity().subtract(orbitGooding.getVelocity()).getNorm() < 0.06);
133     }
134 
135     @Test
136     void testGooding() {
137         final Context context = EstimationTestUtils.eccentricContext("regular-data:potential:tides");
138 
139         final double mu = context.initialOrbit.getMu();
140         final Frame frame = context.initialOrbit.getFrame();
141 
142         final NumericalPropagatorBuilder propagatorBuilder =
143                         context.createBuilder(OrbitType.KEPLERIAN, PositionAngleType.TRUE, true,
144                                               1.0e-6, 60.0, 0.001);
145 
146         // create perfect range measurements
147         final Propagator propagator = EstimationTestUtils.createPropagator(context.initialOrbit,
148                                                                            propagatorBuilder);
149 
150         final List<ObservedMeasurement<?>> measurements =
151                 EstimationTestUtils.createMeasurements(propagator,
152                                                        new PVMeasurementCreator(),
153                                                        0.0, 1.0, 60.0);
154 
155         // measurement data 1
156         final int idMeasure1 = 0;
157         final AbsoluteDate date1 = measurements.get(idMeasure1).getDate();
158         final Vector3D stapos1 = Vector3D.ZERO;/*context.stations.get(0)  // FIXME we need to access the station of the measurement
159                                     .getBaseFrame()
160                                     .getPVCoordinates(date1, frame)
161                                     .getPosition();*/
162         final Vector3D position1 = new Vector3D(measurements.get(idMeasure1).getObservedValue()[0],
163                                                 measurements.get(idMeasure1).getObservedValue()[1],
164                                                 measurements.get(idMeasure1).getObservedValue()[2]);
165         final double r1 = position1.getNorm();
166         final Vector3D lineOfSight1 = position1.normalize();
167 
168         // measurement data 2
169         final int idMeasure2 = 20;
170         final AbsoluteDate date2 = measurements.get(idMeasure2).getDate();
171         final Vector3D stapos2 = Vector3D.ZERO;/*context.stations.get(0)  // FIXME we need to access the station of the measurement
172                         .getBaseFrame()
173                         .getPVCoordinates(date2, frame)
174                         .getPosition();*/
175         final Vector3D position2 = new Vector3D(
176                                                 measurements.get(idMeasure2).getObservedValue()[0],
177                                                 measurements.get(idMeasure2).getObservedValue()[1],
178                                                 measurements.get(idMeasure2).getObservedValue()[2]);
179         final Vector3D lineOfSight2 = position2.normalize();
180 
181         // measurement data 3
182         final int idMeasure3 = 40;
183         final AbsoluteDate date3 = measurements.get(idMeasure3).getDate();
184         final Vector3D stapos3 = Vector3D.ZERO;/*context.stations.get(0)  // FIXME we need to access the station of the measurement
185                         .getBaseFrame()
186                         .getPVCoordinates(date3, frame)
187                         .getPosition();*/
188         final Vector3D position3 = new Vector3D(
189                                                 measurements.get(idMeasure3).getObservedValue()[0],
190                                                 measurements.get(idMeasure3).getObservedValue()[1],
191                                                 measurements.get(idMeasure3).getObservedValue()[2]);
192         final double r3 = position3.getNorm();
193         final Vector3D lineOfSight3 = position3.normalize();
194 
195         // instantiate the IOD method
196         final IodGooding iod = new IodGooding(mu);
197 
198         // the problem is very sensitive, and unless one can provide the exact
199         // initial range estimate, the estimate may be far off the truth...
200         final Orbit orbit = iod.estimate(frame,
201                                          stapos1, stapos2, stapos3,
202                                          lineOfSight1, date1,
203                                          lineOfSight2, date2,
204                                          lineOfSight3, date3,
205                                          r1 - 1e3, r3 + 1e3);
206 
207         // IOD should be close to original
208         SpacecraftState midPoint = propagator.propagate(date2);
209         Assertions.assertTrue(midPoint.getPosition().subtract(orbit.getPosition()).getNorm() < 1100.0);
210         Assertions.assertTrue(midPoint.getVelocity().subtract(orbit.getVelocity()).getNorm() < 0.47);
211     }
212 
213     @Test
214     void testIssue756() {
215         final Context context = EstimationTestUtils.eccentricContext("regular-data:potential:tides");
216 
217         final double mu = context.initialOrbit.getMu();
218         final Frame frame = context.initialOrbit.getFrame();
219 
220         final NumericalPropagatorBuilder propagatorBuilder =
221                         context.createBuilder(OrbitType.KEPLERIAN, PositionAngleType.TRUE, true,
222                                               1.0e-6, 60.0, 0.001);
223 
224         // create perfect range measurements
225         final Propagator propagator = EstimationTestUtils.createPropagator(context.initialOrbit,
226                                                                            propagatorBuilder);
227 
228         final List<ObservedMeasurement<?>> measurements =
229                 EstimationTestUtils.createMeasurements(propagator,
230                                                        new AngularRaDecMeasurementCreator(context),
231                                                        0.0, 1.0, 60.0);
232 
233         // Angular measurements
234         final AngularRaDec raDec1 = (AngularRaDec) measurements.get(0);
235         final AngularRaDec raDec2 = (AngularRaDec) measurements.get(20);
236         final AngularRaDec raDec3 = (AngularRaDec) measurements.get(40);
237 
238         // Range estimations
239         final double rhoInit1 = 1.3127847998082995E7;
240         final double rhoInit3 = 1.3950296648518201E7;
241 
242         // instantiate the IOD method
243         final IodGooding iod = new IodGooding(mu);
244 
245         final KeplerianOrbit orbit1 = new KeplerianOrbit(iod.estimate(frame, raDec1, raDec2, raDec3, rhoInit1, rhoInit3));
246         final KeplerianOrbit orbit2 = new KeplerianOrbit(iod.estimate(frame,
247                                                          raDec1.getGroundStationPosition(frame),
248                                                          raDec2.getGroundStationPosition(frame),
249                                                          raDec3.getGroundStationPosition(frame),
250                                                          raDec1.getObservedLineOfSight(frame), raDec1.getDate(),
251                                                          raDec2.getObservedLineOfSight(frame), raDec2.getDate(),
252                                                          raDec3.getObservedLineOfSight(frame), raDec3.getDate(),
253                                                          rhoInit1, rhoInit3));
254 
255         // Orbit 1 and 2 should be identical
256         Assertions.assertEquals(0.0, orbit1.getPosition().subtract(orbit2.getPosition()).getNorm());
257         Assertions.assertEquals(0.0, orbit1.getVelocity().subtract(orbit2.getVelocity()).getNorm());
258 
259         // IOD should be close to original
260         SpacecraftState midPoint = propagator.propagate(raDec2.getDate());
261         Assertions.assertTrue(midPoint.getPosition().subtract(orbit1.getPosition()).getNorm() < 170.0);
262         Assertions.assertTrue(midPoint.getVelocity().subtract(orbit1.getVelocity()).getNorm() < 0.06);
263     }
264 
265     @Test
266     void testIssue1216() {
267         final Context context = EstimationTestUtils.eccentricContext("regular-data:potential:tides");
268 
269         final double mu = context.initialOrbit.getMu();
270         final Frame frame = context.initialOrbit.getFrame();
271 
272         final NumericalPropagatorBuilder propagatorBuilder =
273             context.createBuilder(OrbitType.KEPLERIAN, PositionAngleType.TRUE, true,
274                 1.0e-6, 60.0, 0.001);
275 
276         // create perfect range measurements
277         final Propagator propagator = EstimationTestUtils.createPropagator(context.initialOrbit,
278             propagatorBuilder);
279 
280         final List<ObservedMeasurement<?>> measurements =
281             EstimationTestUtils.createMeasurements(propagator,
282                 new AngularAzElMeasurementCreator(context),
283                 0.0, 1.0, 60.0);
284 
285         // Angular measurements
286         final AngularAzEl azEl1 = (AngularAzEl) measurements.get(0);
287         final AngularAzEl azEl2 = (AngularAzEl) measurements.get(20);
288         final AngularAzEl azEl3 = (AngularAzEl) measurements.get(40);
289 
290         // Range estimations
291         final double rhoInit1 = 1.3127847998082995E7;
292         final double rhoInit3 = 1.3950296648518201E7;
293 
294         // instantiate the IOD method
295         final IodGooding iod = new IodGooding(mu);
296 
297         final KeplerianOrbit orbit1 = new KeplerianOrbit(iod.estimate(frame, azEl1, azEl2, azEl3, rhoInit1, rhoInit3));
298         final KeplerianOrbit orbit2 = new KeplerianOrbit(iod.estimate(frame,
299             azEl1.getGroundStationPosition(frame),
300             azEl2.getGroundStationPosition(frame),
301             azEl3.getGroundStationPosition(frame),
302             azEl1.getObservedLineOfSight(frame), azEl1.getDate(),
303             azEl2.getObservedLineOfSight(frame), azEl2.getDate(),
304             azEl3.getObservedLineOfSight(frame), azEl3.getDate(),
305             rhoInit1, rhoInit3));
306 
307         // Orbit 1 and 2 should be identical
308         Assertions.assertEquals(0.0, orbit1.getPosition().subtract(orbit2.getPosition()).getNorm());
309         Assertions.assertEquals(0.0, orbit1.getVelocity().subtract(orbit2.getVelocity()).getNorm());
310 
311         // IOD should be close to original
312         SpacecraftState midPoint = propagator.propagate(azEl2.getDate());
313         Assertions.assertTrue(midPoint.getPosition().subtract(orbit1.getPosition()).getNorm() < 170.0);
314         Assertions.assertTrue(midPoint.getVelocity().subtract(orbit1.getVelocity()).getNorm() < 0.06);
315     }
316 
317 }