1   /* Copyright 2002-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.files.general;
18  
19  import java.io.BufferedWriter;
20  import java.io.IOException;
21  import java.nio.charset.StandardCharsets;
22  import java.nio.file.Files;
23  import java.nio.file.Paths;
24  import java.util.ArrayList;
25  import java.util.List;
26  
27  import org.hipparchus.geometry.euclidean.threed.Rotation;
28  import org.junit.jupiter.api.Assertions;
29  import org.junit.jupiter.api.BeforeEach;
30  import org.junit.jupiter.api.Test;
31  import org.orekit.Utils;
32  import org.orekit.attitudes.Attitude;
33  import org.orekit.attitudes.AttitudeProvider;
34  import org.orekit.attitudes.FrameAlignedProvider;
35  import org.orekit.bodies.CelestialBody;
36  import org.orekit.bodies.CelestialBodyFactory;
37  import org.orekit.data.DataSource;
38  import org.orekit.errors.OrekitIllegalArgumentException;
39  import org.orekit.errors.OrekitMessages;
40  import org.orekit.files.ccsds.definitions.CelestialBodyFrame;
41  import org.orekit.files.ccsds.definitions.FrameFacade;
42  import org.orekit.files.ccsds.definitions.SpacecraftBodyFrame;
43  import org.orekit.files.ccsds.definitions.TimeSystem;
44  import org.orekit.files.ccsds.ndm.ParserBuilder;
45  import org.orekit.files.ccsds.ndm.WriterBuilder;
46  import org.orekit.files.ccsds.ndm.adm.AdmHeader;
47  import org.orekit.files.ccsds.ndm.adm.AttitudeType;
48  import org.orekit.files.ccsds.ndm.adm.aem.AemMetadata;
49  import org.orekit.files.ccsds.ndm.adm.aem.AemSegment;
50  import org.orekit.files.ccsds.ndm.adm.aem.AttitudeWriter;
51  import org.orekit.files.ccsds.utils.FileFormat;
52  import org.orekit.files.general.AttitudeEphemerisFile.AttitudeEphemerisSegment;
53  import org.orekit.files.general.OrekitAttitudeEphemerisFile.OrekitSatelliteAttitudeEphemeris;
54  import org.orekit.frames.Frame;
55  import org.orekit.frames.FramesFactory;
56  import org.orekit.orbits.KeplerianOrbit;
57  import org.orekit.orbits.PositionAngleType;
58  import org.orekit.propagation.SpacecraftState;
59  import org.orekit.propagation.analytical.KeplerianPropagator;
60  import org.orekit.time.AbsoluteDate;
61  import org.orekit.utils.AngularDerivativesFilter;
62  import org.orekit.utils.Constants;
63  import org.orekit.utils.TimeStampedAngularCoordinates;
64  
65  public class OrekitAttitudeEphemerisFileTest {
66  
67      @BeforeEach
68      public void setUp() throws Exception {
69          Utils.setDataRoot("regular-data");
70      }
71  
72      @Test
73      public void testGetSatellites() {
74          final String id1 = "ID1";
75          final String id2 = "ID2";
76          OrekitAttitudeEphemerisFile file = new OrekitAttitudeEphemerisFile();
77          OrekitSatelliteAttitudeEphemeris ephem1 = file.addSatellite(id1);
78          Assertions.assertNotNull(ephem1);
79          OrekitSatelliteAttitudeEphemeris ephem2 = file.addSatellite(id2);
80          Assertions.assertNotNull(ephem2);
81      }
82  
83      @Test
84      public void testWritingToAEM() throws IOException {
85          final double quaternionTolerance = 1e-5;
86          final String satId = "SATELLITE1";
87          final double sma = 10000000;
88          final double inc = Math.toRadians(45.0);
89          final double ecc = 0.1;
90          final double raan = 0.0;
91          final double pa = 0.0;
92          final double ta = 0.0;
93          final AbsoluteDate date = new AbsoluteDate();
94          final Frame frame = FramesFactory.getEME2000();
95          final CelestialBody body = CelestialBodyFactory.getEarth();
96          final double mu = body.getGM();
97          KeplerianOrbit initialOrbit = new KeplerianOrbit(sma, ecc, inc, pa, raan, ta, PositionAngleType.TRUE,
98                                                           frame, date, mu);
99  
100         // Initialize a Keplerian propagator with an Inertial attitude provider
101         // It is expected that all attitude data lines will have the same value
102         final Rotation refRot = new Rotation(0.72501, -0.64585, 0.018542, -0.23854, false);
103         AttitudeProvider inertialPointing = new FrameAlignedProvider(refRot);
104         KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit, inertialPointing);
105 
106         final double propagationDurationSeconds = 1200.0;
107         final double stepSizeSeconds = 60.0;
108         List<SpacecraftState> states = new ArrayList<SpacecraftState>();
109 
110         for (double dt = 0.0; dt < propagationDurationSeconds; dt += stepSizeSeconds) {
111             states.add(propagator.propagate(date.shiftedBy(dt)));
112         }
113 
114         OrekitAttitudeEphemerisFile ephemerisFile = new OrekitAttitudeEphemerisFile();
115         OrekitSatelliteAttitudeEphemeris satellite = ephemerisFile.addSatellite(satId);
116         satellite.addNewSegment(states,
117                                 OrekitSatelliteAttitudeEphemeris.DEFAULT_INTERPOLATION_METHOD,
118                                 OrekitSatelliteAttitudeEphemeris.DEFAULT_INTERPOLATION_SIZE,
119                                 AngularDerivativesFilter.USE_RR);
120 
121         // Test of all getters for OrekitSatelliteAttitudeEphemeris
122         Assertions.assertEquals(satId, satellite.getId());
123         Assertions.assertEquals(0.0, states.get(0).getDate().durationFrom(satellite.getStart()), 1.0e-15);
124         Assertions.assertEquals(0.0, states.get(states.size() - 1).getDate().durationFrom(satellite.getStop()), 1.0e-15);
125 
126         // Test of all getters for OrekitAttitudeEphemerisSegment
127         AttitudeEphemerisSegment<TimeStampedAngularCoordinates> segment = satellite.getSegments().get(0);
128         Assertions.assertEquals(OrekitSatelliteAttitudeEphemeris.DEFAULT_INTERPOLATION_METHOD, segment.getInterpolationMethod());
129         Assertions.assertEquals(OrekitSatelliteAttitudeEphemeris.DEFAULT_INTERPOLATION_SIZE, segment.getInterpolationSamples());
130         Assertions.assertEquals(0.0, states.get(0).getDate().durationFrom(segment.getStart()), 1.0e-15);
131         Assertions.assertEquals(0.0, states.get(states.size() - 1).getDate().durationFrom(segment.getStop()), 1.0e-15);
132         Assertions.assertEquals(AngularDerivativesFilter.USE_RR, segment.getAvailableDerivatives());
133 
134         // Verify attitude
135         final Attitude attitude = segment.getAttitudeProvider().getAttitude(initialOrbit, date, frame);
136         Assertions.assertEquals(frame, attitude.getReferenceFrame());
137         Assertions.assertEquals(refRot.getQ0(), attitude.getRotation().getQ0(), quaternionTolerance);
138         Assertions.assertEquals(refRot.getQ1(), attitude.getRotation().getQ1(), quaternionTolerance);
139         Assertions.assertEquals(refRot.getQ2(), attitude.getRotation().getQ2(), quaternionTolerance);
140         Assertions.assertEquals(refRot.getQ3(), attitude.getRotation().getQ3(), quaternionTolerance);
141 
142         String tempAem = Files.createTempFile("OrekitAttitudeEphemerisFileTest", ".aem").toString();
143         try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(tempAem), StandardCharsets.UTF_8)) {
144             final AdmHeader header = new AdmHeader();
145             header.setFormatVersion(1.0);
146             new AttitudeWriter(new WriterBuilder().buildAemWriter(),
147                                header, dummyMetadata(), FileFormat.KVN, "", Constants.JULIAN_DAY, 60).
148             write(writer, ephemerisFile);
149         }
150 
151         AttitudeEphemerisFile<TimeStampedAngularCoordinates, AemSegment> ephemerisFrom =
152                         new ParserBuilder().buildAemParser().parseMessage(new DataSource(tempAem));
153         Files.delete(Paths.get(tempAem));
154 
155         segment = ephemerisFrom.getSatellites().get(satId).getSegments().get(0);
156         Assertions.assertEquals(states.get(0).getDate(), segment.getStart());
157         Assertions.assertEquals(states.get(states.size() - 1).getDate(), segment.getStop());
158         Assertions.assertEquals(states.size(), segment.getAngularCoordinates().size());
159         for (int i = 0; i < states.size(); i++) {
160             TimeStampedAngularCoordinates expected = states.get(i).getAttitude().getOrientation();
161             TimeStampedAngularCoordinates actual = segment.getAngularCoordinates().get(i);
162             Assertions.assertEquals(expected.getDate(), actual.getDate());
163             Assertions.assertEquals(0.0, Rotation.distance(refRot, actual.getRotation()), quaternionTolerance);
164         }
165 
166     }
167 
168     @Test
169     public void testNoStates() {
170 
171         // Satellite ID
172         final String satId = "SATELLITE1";
173 
174         // Create an empty list of states
175         List<SpacecraftState> states = new ArrayList<SpacecraftState>();
176 
177         // Create a new satellite attitude ephemeris
178         OrekitAttitudeEphemerisFile ephemerisFile = new OrekitAttitudeEphemerisFile();
179         OrekitSatelliteAttitudeEphemeris satellite = ephemerisFile.addSatellite(satId);
180 
181         // Try to add a new segment
182         try {
183             satellite.addNewSegment(states,
184                                     OrekitSatelliteAttitudeEphemeris.DEFAULT_INTERPOLATION_METHOD,
185                                     OrekitSatelliteAttitudeEphemeris.DEFAULT_INTERPOLATION_SIZE,
186                                     AngularDerivativesFilter.USE_RR);
187         } catch (OrekitIllegalArgumentException oiae) {
188             Assertions.assertEquals(OrekitMessages.NULL_ARGUMENT, oiae.getSpecifier());
189         }
190 
191 
192     }
193 
194     @Test
195     public void testNoEnoughDataForInterpolation() {
196 
197         // Create a spacecraft state
198         final String satId = "SATELLITE1";
199         final double sma = 10000000;
200         final double inc = Math.toRadians(45.0);
201         final double ecc = 0.1;
202         final double raan = 0.0;
203         final double pa = 0.0;
204         final double ta = 0.0;
205         final AbsoluteDate date = new AbsoluteDate();
206         final Frame frame = FramesFactory.getEME2000();
207         final CelestialBody body = CelestialBodyFactory.getEarth();
208         final double mu = body.getGM();
209         KeplerianOrbit initialOrbit = new KeplerianOrbit(sma, ecc, inc, pa, raan, ta, PositionAngleType.TRUE,
210                                                          frame, date, mu);
211         SpacecraftState state = new SpacecraftState(initialOrbit);
212 
213         // Add the state to the list of spacecraft states
214         List<SpacecraftState> states = new ArrayList<SpacecraftState>();
215         states.add(state);
216 
217         // Create a new satellite attitude ephemeris
218         OrekitAttitudeEphemerisFile ephemerisFile = new OrekitAttitudeEphemerisFile();
219         OrekitSatelliteAttitudeEphemeris satellite = ephemerisFile.addSatellite(satId);
220 
221         // Try to add a new segment
222         try {
223             satellite.addNewSegment(states, "LINEAR", 1, AngularDerivativesFilter.USE_R);
224         } catch (OrekitIllegalArgumentException oiae) {
225             Assertions.assertEquals(OrekitMessages.NOT_ENOUGH_DATA, oiae.getSpecifier());
226         }
227     }
228 
229     private AemMetadata dummyMetadata() {
230         AemMetadata metadata = new AemMetadata(4);
231         metadata.setTimeSystem(TimeSystem.TT);
232         metadata.setObjectID("SATELLITE1");
233         metadata.setObjectName("transgalactic");
234         metadata.getEndpoints().setFrameA(new FrameFacade(FramesFactory.getGCRF(), CelestialBodyFrame.GCRF,
235                                                           null, null, "GCRF"));
236         metadata.getEndpoints().setFrameB(new FrameFacade(null, null, null,
237                                                           new SpacecraftBodyFrame(SpacecraftBodyFrame.BaseEquipment.GYRO_FRAME, "1"),
238                                                           "GYRO FRAME 1"));
239         metadata.getEndpoints().setA2b(true);
240         metadata.setStartTime(AbsoluteDate.J2000_EPOCH.shiftedBy(80 * Constants.JULIAN_CENTURY));
241         metadata.setStopTime(metadata.getStartTime().shiftedBy(Constants.JULIAN_YEAR));
242         metadata.setAttitudeType(AttitudeType.QUATERNION_DERIVATIVE);
243         metadata.setIsFirst(true);
244         return metadata;
245     }
246 
247 }
248