1   /* Copyright 2020-2025 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  package org.orekit.frames;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  
21  import java.time.Instant;
22  import java.time.LocalDateTime;
23  import java.time.ZoneId;
24  import java.util.HashMap;
25  import java.util.Map;
26  
27  import org.hipparchus.Field;
28  import org.hipparchus.geometry.euclidean.threed.FieldRotation;
29  import org.hipparchus.geometry.euclidean.threed.Rotation;
30  import org.hipparchus.geometry.euclidean.threed.RotationConvention;
31  import org.hipparchus.geometry.euclidean.threed.RotationOrder;
32  import org.hipparchus.geometry.euclidean.threed.Vector3D;
33  import org.hipparchus.util.Binary64;
34  import org.hipparchus.util.Binary64Field;
35  import org.hipparchus.util.FastMath;
36  import org.hipparchus.util.Pair;
37  import org.junit.jupiter.api.Assertions;
38  import org.junit.jupiter.api.BeforeEach;
39  import org.junit.jupiter.api.Test;
40  import org.mockito.Mockito;
41  import org.orekit.Utils;
42  import org.orekit.attitudes.Attitude;
43  import org.orekit.attitudes.AttitudeProvider;
44  import org.orekit.attitudes.LofOffset;
45  import org.orekit.models.earth.GeoMagneticField;
46  import org.orekit.models.earth.GeoMagneticFieldFactory;
47  import org.orekit.orbits.KeplerianOrbit;
48  import org.orekit.orbits.Orbit;
49  import org.orekit.orbits.PositionAngleType;
50  import org.orekit.propagation.SpacecraftState;
51  import org.orekit.time.AbsoluteDate;
52  import org.orekit.time.DateComponents;
53  import org.orekit.time.FieldAbsoluteDate;
54  import org.orekit.time.TimeComponents;
55  import org.orekit.time.TimeScalesFactory;
56  import org.orekit.utils.Constants;
57  import org.orekit.utils.FieldPVCoordinates;
58  import org.orekit.utils.IERSConventions;
59  import org.orekit.utils.PVCoordinates;
60  
61  public class LocalMagneticFieldFrameTest {
62  
63      @BeforeEach
64      public void setUp() {
65          Utils.setDataRoot("regular-data:earth");
66      }
67  
68      @Test
69      void testDefaultConstructorAndName() {
70          // GIVEN
71          final Frame            inertialFrameMock    = Mockito.mock(Frame.class);
72          final Frame            bodyFrameMock        = Mockito.mock(Frame.class);
73          final GeoMagneticField geoMagneticFieldMock = Mockito.mock(GeoMagneticField.class);
74  
75          final LocalMagneticFieldFrame localMagneticFieldFrame =
76                  new LocalMagneticFieldFrame(inertialFrameMock, geoMagneticFieldMock, bodyFrameMock);
77  
78          // WHEN
79          final String computedName = localMagneticFieldFrame.getName();
80  
81          // THEN
82          // Assert name
83          Assertions.assertEquals("LOCAL_MAGNETIC_FIELD_FRAME", computedName);
84  
85          // Assert getters
86          Assertions.assertEquals(inertialFrameMock, localMagneticFieldFrame.getInertialFrame());
87          Assertions.assertEquals(geoMagneticFieldMock, localMagneticFieldFrame.getMagneticField());
88      }
89  
90  
91      @Test
92      void testFieldRotation() {
93          // GIVEN
94          // Create local magnetic field frame instance
95          final Frame            inertialFrameMock    = Mockito.mock(Frame.class);
96          final Frame            bodyFrameMock        = Mockito.mock(Frame.class);
97          final GeoMagneticField geoMagneticFieldMock = Mockito.mock(GeoMagneticField.class);
98  
99          final LocalMagneticFieldFrame localMagneticFieldFrame =
100                 new LocalMagneticFieldFrame(inertialFrameMock, geoMagneticFieldMock, bodyFrameMock);
101 
102         // Create a spy to stub non field rotationFromInertial method
103         final LocalMagneticFieldFrame spy = Mockito.spy(localMagneticFieldFrame);
104 
105         // Create returned non field rotation from non field rotationFromInertial method
106         final double   q0       = 0;
107         final double   q1       = 0.707;
108         final double   q2       = 0;
109         final double   q3       = 0.707;
110         final Rotation rotation = new Rotation(q0, q1, q2, q3, true);
111         Mockito.doReturn(rotation).when(spy).rotationFromInertial(Mockito.any(AbsoluteDate.class),
112                                                                   Mockito.any(PVCoordinates.class));
113 
114         // Create fielded date
115         final AbsoluteDate date = Mockito.mock(AbsoluteDate.class);
116         @SuppressWarnings("unchecked")
117         final FieldAbsoluteDate<Binary64> fieldDate = Mockito.mock(FieldAbsoluteDate.class);
118         Mockito.when(fieldDate.toAbsoluteDate()).thenReturn(date);
119 
120         // Create fielded pv
121         final PVCoordinates pv = Mockito.mock(PVCoordinates.class);
122         @SuppressWarnings("unchecked")
123         final FieldPVCoordinates<Binary64> fieldPV = Mockito.mock(FieldPVCoordinates.class);
124         Mockito.when(fieldPV.toPVCoordinates()).thenReturn(pv);
125 
126         // Create Binary64 field instance
127         final Field<Binary64> binary64Field = Binary64Field.getInstance();
128 
129         // WHEN
130         final FieldRotation<Binary64> computedRotation = spy.rotationFromInertial(binary64Field, fieldDate, fieldPV);
131 
132         // THEN
133         Assertions.assertEquals(rotation.getQ0(), computedRotation.getQ0().getReal());
134         Assertions.assertEquals(rotation.getQ1(), computedRotation.getQ1().getReal());
135         Assertions.assertEquals(rotation.getQ2(), computedRotation.getQ2().getReal());
136         Assertions.assertEquals(rotation.getQ3(), computedRotation.getQ3().getReal());
137     }
138 
139     @Test
140     void testAligningJDirection() {
141         // GIVEN
142         // Create mock pv
143         final Vector3D positionMock      = Mockito.mock(Vector3D.class);
144         final Vector3D velocityMock      = Mockito.mock(Vector3D.class);
145         final Vector3D momentumMock      = Mockito.mock(Vector3D.class);
146         final Vector3D minusPositionMock = Mockito.mock(Vector3D.class);
147         final Vector3D minusVelocityMock = Mockito.mock(Vector3D.class);
148         final Vector3D minusMomentumMock = Mockito.mock(Vector3D.class);
149 
150         Mockito.when(positionMock.negate()).thenReturn(minusPositionMock);
151         Mockito.when(velocityMock.negate()).thenReturn(minusVelocityMock);
152         Mockito.when(momentumMock.negate()).thenReturn(minusMomentumMock);
153 
154         Mockito.when(Vector3D.crossProduct(positionMock, velocityMock)).thenReturn(momentumMock);
155 
156         final PVCoordinates pv = new PVCoordinates(positionMock, velocityMock);
157 
158         // Extract enum values
159         final LocalMagneticFieldFrame.LOFBuilderVector plusPos = LocalMagneticFieldFrame.LOFBuilderVector.PLUS_POSITION;
160         final LocalMagneticFieldFrame.LOFBuilderVector plusVel = LocalMagneticFieldFrame.LOFBuilderVector.PLUS_VELOCITY;
161         final LocalMagneticFieldFrame.LOFBuilderVector plusMom = LocalMagneticFieldFrame.LOFBuilderVector.PLUS_MOMENTUM;
162         final LocalMagneticFieldFrame.LOFBuilderVector minusPos =
163                 LocalMagneticFieldFrame.LOFBuilderVector.MINUS_POSITION;
164         final LocalMagneticFieldFrame.LOFBuilderVector minusVel =
165                 LocalMagneticFieldFrame.LOFBuilderVector.MINUS_VELOCITY;
166         final LocalMagneticFieldFrame.LOFBuilderVector minusMom =
167                 LocalMagneticFieldFrame.LOFBuilderVector.MINUS_MOMENTUM;
168 
169         // WHEN
170         final Vector3D computedPlusPos  = plusPos.getVector(pv);
171         final Vector3D computedPlusVel  = plusVel.getVector(pv);
172         final Vector3D computedPlusMom  = plusMom.getVector(pv);
173         final Vector3D computedMinusPos = minusPos.getVector(pv);
174         final Vector3D computedMinusVel = minusVel.getVector(pv);
175         final Vector3D computedMinusMom = minusMom.getVector(pv);
176 
177         // THEN
178         Assertions.assertEquals(positionMock, computedPlusPos);
179         Assertions.assertEquals(velocityMock, computedPlusVel);
180         Assertions.assertEquals(momentumMock, computedPlusMom);
181         Assertions.assertEquals(minusPositionMock, computedMinusPos);
182         Assertions.assertEquals(minusVelocityMock, computedMinusVel);
183         Assertions.assertEquals(minusMomentumMock, computedMinusMom);
184     }
185 
186     @Test
187     public void testBotPointing() {
188 
189         final Map<String, Pair<Rotation, Double[]>> rotationAnglesMap = new HashMap<>();
190         rotationAnglesMap.put("+x", new Pair<>(new Rotation(1.0, 0.0, 0.0, 0.0, false),
191                                                new Double[] { FastMath.toRadians(0.0), FastMath.toRadians(0.0) }));
192         rotationAnglesMap.put("+y", new Pair<>(new Rotation(0.7071068, 0.0, 0.0, 0.7071068, false),
193                                                new Double[] { FastMath.toRadians(90.0), FastMath.toRadians(0.0) }));
194         rotationAnglesMap.put("+z", new Pair<>(new Rotation(Vector3D.PLUS_K, Vector3D.PLUS_I),
195                                                new Double[] { FastMath.toRadians(90.0), FastMath.toRadians(90.0) }));
196         rotationAnglesMap.put("-x", new Pair<>(new Rotation(0.0, 0.0, 1.0, 0.0, false),
197                                                new Double[] { FastMath.toRadians(180.0), FastMath.toRadians(0.0) }));
198         rotationAnglesMap.put("-y", new Pair<>(new Rotation(Vector3D.MINUS_J, Vector3D.PLUS_I),
199                                                new Double[] { FastMath.toRadians(90.0), FastMath.toRadians(180.0) }));
200         rotationAnglesMap.put("-z", new Pair<>(new Rotation(Vector3D.MINUS_K, Vector3D.PLUS_I),
201                                                new Double[] { FastMath.toRadians(90.0), FastMath.toRadians(-90.0) }));
202 
203         // UTC time zone
204         final ZoneId utcTime = ZoneId.of("UTC");
205 
206         // ITRF frame
207         final Frame itrf = FramesFactory.getITRF(IERSConventions.IERS_2010, false);
208 
209         // get GeoMagnetic model
210         final LocalDateTime    utcDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(1532785820000L), utcTime);
211         final GeoMagneticField model   = GeoMagneticFieldFactory.getIGRF(utcDate.getYear());
212 
213         final Frame frame = FramesFactory.getEME2000();
214         final AbsoluteDate date = new AbsoluteDate(new DateComponents(2018, 7, 28),
215                                                    new TimeComponents(13, 50, 20.00),
216                                                    TimeScalesFactory.getTAI());
217 
218         for (Map.Entry<String, Pair<Rotation, Double[]>> entry : rotationAnglesMap.entrySet()) {
219             String   axis             = entry.getKey();
220             Rotation rotate           = entry.getValue().getFirst();
221             Double[] expectedThetaPhi = entry.getValue().getSecond();
222 
223             // test Bdot pointing, with body axis oriented to magnetic north
224             final double[] rotationAngles = rotate.getAngles(RotationOrder.XYZ, RotationConvention.VECTOR_OPERATOR);
225             final LOF lof =
226                     new LocalMagneticFieldFrame(frame, model, LocalMagneticFieldFrame.LOFBuilderVector.PLUS_MOMENTUM,
227                                                 itrf);
228 
229             final AttitudeProvider bdotPointing = new LofOffset(frame, lof, RotationOrder.XYZ,
230                                                                 rotationAngles[0], rotationAngles[1], rotationAngles[2]);
231 
232             // get orbit, attitude and magnetic field at equator
233             KeplerianOrbit orbit = new KeplerianOrbit(6866719.5237, 0.003, FastMath.toRadians(89.7),
234                                                       0, 0, 0, PositionAngleType.TRUE, frame, date, 3.986004415e14);
235             Attitude        equatorialAttitude        = bdotPointing.getAttitude(orbit, date, frame);
236             SpacecraftState spacecraftStateEquatorial = new SpacecraftState(orbit, equatorialAttitude);
237 
238             // move from the equator to poles with 1/4th of the orbit
239             double h = orbit.getKeplerianPeriod() / 4.0;
240 
241             // get orbit, attitude and magnetic field at south pole
242             Orbit           southPoleOrbit           = orbit.shiftedBy(h);
243             AbsoluteDate    southPoleDate            = southPoleOrbit.getDate();
244             Attitude        southPoleAttitude        = bdotPointing.getAttitude(southPoleOrbit, southPoleDate, frame);
245             SpacecraftState spacecraftStateSouthPole = new SpacecraftState(southPoleOrbit, southPoleAttitude);
246 
247             // get orbit, attitude and magnetic field at north pole
248             Orbit           northPoleOrbit           = orbit.shiftedBy(-h);
249             AbsoluteDate    northPoleDate            = northPoleOrbit.getDate();
250             Attitude        northPoleAttitude        = bdotPointing.getAttitude(northPoleOrbit, northPoleDate, frame);
251             SpacecraftState spacecraftStateNorthPole = new SpacecraftState(northPoleOrbit, northPoleAttitude);
252 
253             // check attitude aligned with magnetic field, projecting magnetic field vector to satellite body frame
254             // x = cos(theta)
255             // y = sin(theta) cos(phi)
256             // z = sin(theta) sin(phi)
257             checkMagneticFieldAngles(axis, expectedThetaPhi, itrf, model, spacecraftStateEquatorial);
258             checkMagneticFieldAngles(axis, expectedThetaPhi, itrf, model, spacecraftStateSouthPole);
259             checkMagneticFieldAngles(axis, expectedThetaPhi, itrf, model, spacecraftStateNorthPole);
260         }
261     }
262 
263     private void checkMagneticFieldAngles(final String axis, final Double[] expectedThetaPhi, final Frame itrf,
264                                           final GeoMagneticField model, SpacecraftState s) {
265         // calculate magnetic field intensity
266         Vector3D posItrf = s.getPosition(itrf);
267         double   lat     = posItrf.getDelta();
268         double   lng     = posItrf.getAlpha();
269         double   alt     = posItrf.getNorm() - Constants.WGS84_EARTH_EQUATORIAL_RADIUS;
270         Vector3D magneticFieldVector = model.calculateField(lat, lng, alt).getFieldVector();
271         Vector3D magneticFieldVectorInertial = itrf.getTransformTo(s.getFrame(), s.getDate())
272                                                    .transformVector(magneticFieldVector);
273 
274         // calculate projection of magnetic field vector into spacecraft body frame
275         double[] magneticThetaPhi = getProjectionAngles(s, magneticFieldVectorInertial);
276 
277         double toll = FastMath.toRadians(1e-3);
278         assertEquals(expectedThetaPhi[0], magneticThetaPhi[0], toll,
279                      axis + " Theta angle, lat:" + FastMath.toDegrees(lat) + ", lng:" + FastMath.toDegrees(lng));
280         if (FastMath.sin(expectedThetaPhi[0]) > 1e-5) {
281             assertEquals(expectedThetaPhi[1], magneticThetaPhi[1], toll,
282                          axis + " Phi angle, lat:" + FastMath.toDegrees(lat) + ", lng:" + FastMath.toDegrees(lng));
283         }
284     }
285 
286     /**
287      * Get projected angles (theta, phi) of arbitrary vector on satellite body frame, considering attitude.
288      *
289      * @param s spacecraft state
290      * @param vector vector to project, on arbitrary frame
291      *
292      * @return projected angles theta, phi [rad]
293      */
294     private double[] getProjectionAngles(final SpacecraftState s, final Vector3D vector) {
295         //Get J2000 vector in Spacecraft body reference frame (consider the attitude)
296         Transform toSpacecraftBody          = s.toTransform();
297         Vector3D  vectorInCurrentStateFrame = toSpacecraftBody.transformVector(vector);
298         double elevation = FastMath.abs(vectorInCurrentStateFrame.getDelta()) < 1e-5 ?
299                 0.0 : vectorInCurrentStateFrame.getDelta();
300         double azimuth = FastMath.abs(vectorInCurrentStateFrame.getAlpha()) < 1e-5 ?
301                 0.0 : vectorInCurrentStateFrame.getAlpha();
302         double theta = Math.acos(Math.cos(elevation) * Math.cos(azimuth));
303         double phi   = Math.atan2(Math.tan(elevation), Math.sin(azimuth));
304         return new double[] { theta, phi };
305     }
306 
307 }
308