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.forces;
18  
19  import org.hipparchus.CalculusFieldElement;
20  import org.hipparchus.Field;
21  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
22  import org.hipparchus.geometry.euclidean.threed.Rotation;
23  import org.hipparchus.geometry.euclidean.threed.Vector3D;
24  import org.hipparchus.util.Binary64;
25  import org.hipparchus.util.Binary64Field;
26  import org.hipparchus.util.FastMath;
27  import org.junit.jupiter.api.Assertions;
28  import org.junit.jupiter.api.BeforeEach;
29  import org.junit.jupiter.api.Test;
30  import org.orekit.Utils;
31  import org.orekit.attitudes.Attitude;
32  import org.orekit.attitudes.LofOffset;
33  import org.orekit.bodies.CelestialBody;
34  import org.orekit.bodies.CelestialBodyFactory;
35  import org.orekit.errors.OrekitException;
36  import org.orekit.frames.Frame;
37  import org.orekit.frames.FramesFactory;
38  import org.orekit.frames.LOFType;
39  import org.orekit.orbits.CartesianOrbit;
40  import org.orekit.orbits.CircularOrbit;
41  import org.orekit.orbits.Orbit;
42  import org.orekit.orbits.PositionAngleType;
43  import org.orekit.propagation.FieldSpacecraftState;
44  import org.orekit.propagation.Propagator;
45  import org.orekit.propagation.SpacecraftState;
46  import org.orekit.propagation.analytical.EcksteinHechlerPropagator;
47  import org.orekit.time.AbsoluteDate;
48  import org.orekit.time.DateComponents;
49  import org.orekit.time.FieldAbsoluteDate;
50  import org.orekit.time.TimeComponents;
51  import org.orekit.time.TimeScalesFactory;
52  import org.orekit.utils.Constants;
53  import org.orekit.utils.ExtendedPositionProvider;
54  import org.orekit.utils.TimeStampedAngularCoordinates;
55  import org.orekit.utils.TimeStampedPVCoordinates;
56  
57  public class PointingPanelTest {
58  
59      @Test
60      void testBestPointing() {
61  
62          AbsoluteDate initialDate = propagator.getInitialState().getDate();
63          CelestialBody sun = CelestialBodyFactory.getSun();
64          PointingPanel solarArray = new PointingPanel(Vector3D.PLUS_J, sun, 20.0, 0.0, 0.0, 0.0, 0.0);
65          for (double dt = 0; dt < 4000; dt += 60) {
66  
67              SpacecraftState state = propagator.propagate(initialDate.shiftedBy(dt));
68  
69              Vector3D sunInert = sun.getPosition(initialDate, state.getFrame());
70              Vector3D momentum = state.getPVCoordinates().getMomentum();
71              double sunElevation = FastMath.PI / 2 - Vector3D.angle(sunInert, momentum);
72              Assertions.assertEquals(15.1, FastMath.toDegrees(sunElevation), 0.1);
73  
74              Vector3D n = solarArray.getNormal(state);
75              Assertions.assertEquals(0.0, n.getY(), 1.0e-10);
76  
77              // normal misalignment should be entirely due to sun being out of orbital plane
78              Vector3D sunSat = state.getAttitude().getRotation().applyTo(sunInert);
79              double misAlignment = Vector3D.angle(sunSat, n);
80              Assertions.assertEquals(sunElevation, misAlignment, 1.0e-3);
81  
82          }
83      }
84  
85      @Test
86      void testNormalOptimalRotationDouble() {
87          AbsoluteDate initialDate = propagator.getInitialState().getDate();
88          CelestialBody sun = CelestialBodyFactory.getSun();
89          final Panel absorbingSolarArray = new PointingPanel(Vector3D.PLUS_J, sun, 20.0, 0.0, 0.0, 1.0, 0.0);
90          for (double dt = 0; dt < 4000; dt += 60) {
91              AbsoluteDate date = initialDate.shiftedBy(dt);
92              SpacecraftState state = propagator.propagate(date);
93              Vector3D normal = absorbingSolarArray.getNormal(state);
94              Assertions.assertEquals(0, Vector3D.dotProduct(normal, Vector3D.PLUS_J), 1.0e-16);
95          }
96      }
97  
98      @Test
99      void testNormalOptimalRotationField() {
100         AbsoluteDate initialDate = propagator.getInitialState().getDate();
101         CelestialBody sun = CelestialBodyFactory.getSun();
102         final Panel absorbingSolarArray = new PointingPanel(Vector3D.PLUS_J, sun, 20.0, 0.0, 0.0, 1.0, 0.0);
103         Field<Binary64> field = Binary64Field.getInstance();
104         for (double dt = 0; dt < 4000; dt += 60) {
105             AbsoluteDate date = initialDate.shiftedBy(dt);
106             FieldSpacecraftState<Binary64> fState = new FieldSpacecraftState<>(field, propagator.propagate(date));
107             FieldVector3D<Binary64> normal = absorbingSolarArray.getNormal(fState);
108             Assertions.assertEquals(0, FieldVector3D.dotProduct(normal, Vector3D.PLUS_J).getReal(), 1.0e-16);
109         }
110     }
111 
112     void testNormalSunAlignedDouble() {
113         ExtendedPositionProvider ep = new ExtendedPositionProvider() {
114             
115             @Override
116             public Vector3D getPosition(AbsoluteDate date, Frame frame) {
117                 return new Vector3D(0, 1e6, 0);
118             }
119             
120             @Override
121             public <T extends CalculusFieldElement<T>> FieldVector3D<T> getPosition(FieldAbsoluteDate<T> date,
122                                                                                     Frame frame) {
123                 // not used in this test
124                 return null;
125             }
126         };
127         final Panel panel = new PointingPanel(Vector3D.PLUS_J, ep, 20.0, 0.0, 0.0, 1.0, 0.0);
128         final Orbit orbit = new CartesianOrbit(new TimeStampedPVCoordinates(AbsoluteDate.J2000_EPOCH,
129                                                                             Vector3D.ZERO,
130                                                                             Vector3D.ZERO,
131                                                                             Vector3D.ZERO),
132                                                FramesFactory.getEME2000(), Constants.EIGEN5C_EARTH_MU);
133         final Attitude attitude = new Attitude(FramesFactory.getEME2000(),
134                                                new TimeStampedAngularCoordinates(AbsoluteDate.J2000_EPOCH,
135                                                                                  Rotation.IDENTITY,
136                                                                                  Vector3D.ZERO,
137                                                                                  Vector3D.ZERO));
138         final SpacecraftState state = new SpacecraftState(orbit, attitude, 1000.0);
139         Vector3D normal = panel.getNormal(state);
140         Assertions.assertEquals(0, Vector3D.dotProduct(normal, Vector3D.PLUS_J), 1.0e-16);
141     }
142 
143     @Test
144     void testNormalSunAlignedField() {
145         ExtendedPositionProvider ep = new ExtendedPositionProvider() {
146             
147             @Override
148             public TimeStampedPVCoordinates getPVCoordinates(AbsoluteDate date, Frame frame) {
149                 // not used in this test
150                 return null;
151             }
152             
153             @Override
154             public <T extends CalculusFieldElement<T>> FieldVector3D<T>
155                 getPosition(FieldAbsoluteDate<T> date, Frame frame) {
156                 return new FieldVector3D<>(date.getField(), new Vector3D(0, 1e6, 0));
157             }
158         };
159         final Panel panel = new PointingPanel(Vector3D.PLUS_J, ep, 20.0, 0.0, 0.0, 1.0, 0.0);
160         final Orbit orbit = new CartesianOrbit(new TimeStampedPVCoordinates(AbsoluteDate.J2000_EPOCH,
161                                                                             Vector3D.ZERO,
162                                                                             Vector3D.ZERO,
163                                                                             Vector3D.ZERO),
164                                                FramesFactory.getEME2000(), Constants.EIGEN5C_EARTH_MU);
165         final Attitude attitude = new Attitude(FramesFactory.getEME2000(),
166                                                new TimeStampedAngularCoordinates(AbsoluteDate.J2000_EPOCH,
167                                                                                  Rotation.IDENTITY,
168                                                                                  Vector3D.ZERO,
169                                                                                  Vector3D.ZERO));
170         final SpacecraftState state = new SpacecraftState(orbit, attitude, 1000.0);
171         Field<Binary64> field = Binary64Field.getInstance();
172         FieldVector3D<Binary64> normal = panel.getNormal(new FieldSpacecraftState<>(field, state));
173         Assertions.assertEquals(0, FieldVector3D.dotProduct(normal, Vector3D.PLUS_J).getReal(), 1.0e-16);
174     }
175 
176     @BeforeEach
177     public void setUp() {
178         try {
179         Utils.setDataRoot("regular-data");
180         mu  = 3.9860047e14;
181         double ae  = 6.378137e6;
182         double c20 = -1.08263e-3;
183         double c30 = 2.54e-6;
184         double c40 = 1.62e-6;
185         double c50 = 2.3e-7;
186         double c60 = -5.5e-7;
187 
188         AbsoluteDate date = new AbsoluteDate(new DateComponents(1970, 7, 1),
189                                              new TimeComponents(13, 59, 27.816),
190                                              TimeScalesFactory.getUTC());
191 
192         // Satellite position as circular parameters, raan chosen to have sun elevation with
193         // respect to orbit plane roughly evolving roughly from 15 to 15.2 degrees in the test range
194         Orbit circ =
195             new CircularOrbit(7178000.0, 0.5e-4, -0.5e-4, FastMath.toRadians(50.), FastMath.toRadians(280),
196                                    FastMath.toRadians(10.0), PositionAngleType.MEAN,
197                                    FramesFactory.getEME2000(), date, mu);
198         propagator =
199             new EcksteinHechlerPropagator(circ,
200                                           new LofOffset(circ.getFrame(), LOFType.LVLH_CCSDS),
201                                           ae, mu, c20, c30, c40, c50, c60);
202         } catch (OrekitException oe) {
203             Assertions.fail(oe.getLocalizedMessage());
204         }
205     }
206 
207     private double mu;
208     private Propagator propagator;
209 
210 }