1   /* Copyright 2002-2025 Mark Rutten
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    * Mark Rutten 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.estimation.measurements.modifiers;
18  
19  import java.util.Collections;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  
24  import org.hipparchus.Field;
25  import org.hipparchus.analysis.differentiation.Gradient;
26  import org.hipparchus.analysis.differentiation.GradientField;
27  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
28  import org.hipparchus.geometry.euclidean.threed.Vector3D;
29  import org.hipparchus.util.FastMath;
30  import org.hipparchus.util.MathUtils;
31  import org.orekit.annotation.DefaultDataContext;
32  import org.orekit.data.DataContext;
33  import org.orekit.errors.OrekitException;
34  import org.orekit.errors.OrekitMessages;
35  import org.orekit.estimation.measurements.AngularRaDec;
36  import org.orekit.estimation.measurements.EstimatedMeasurement;
37  import org.orekit.estimation.measurements.EstimatedMeasurementBase;
38  import org.orekit.estimation.measurements.EstimationModifier;
39  import org.orekit.estimation.measurements.GroundStation;
40  import org.orekit.frames.FieldTransform;
41  import org.orekit.frames.Frame;
42  import org.orekit.time.AbsoluteDate;
43  import org.orekit.time.FieldAbsoluteDate;
44  import org.orekit.utils.Constants;
45  import org.orekit.utils.FieldPVCoordinates;
46  import org.orekit.utils.PVCoordinates;
47  import org.orekit.utils.ParameterDriver;
48  import org.orekit.utils.TimeSpanMap;
49  import org.orekit.utils.TimeStampedFieldPVCoordinates;
50  
51  
52  /**
53   * Class modifying theoretical angular measurement with (the inverse of) stellar aberration.
54   * <p>
55   * This class implements equation 3.252-3 from Seidelmann, "Explanatory Supplement to the Astronmical Almanac", 1992.
56   *
57   * @author Mark Rutten
58   */
59  public class AberrationModifier implements EstimationModifier<AngularRaDec> {
60  
61      /** Data context. */
62      private final DataContext dataContext;
63  
64      /** Empty constructor.
65       * <p>
66       * This constructor uses the {@link DefaultDataContext default data context}
67       * </p>
68       * @since 12.0
69       * @see #AberrationModifier(DataContext)
70       */
71      @DefaultDataContext
72      public AberrationModifier() {
73          this(DataContext.getDefault());
74      }
75  
76      /** Constructor.
77       * @param dataContext data context
78       * @since 12.0.1
79       */
80      public AberrationModifier(final DataContext dataContext) {
81          this.dataContext = dataContext;
82      }
83  
84      /** {@inheritDoc} */
85      @Override
86      public String getEffectName() {
87          return "aberration";
88      }
89  
90      /** Natural to proper correction for aberration of light.
91       * @param naturalRaDec the "natural" direction (in barycentric coordinates)
92       * @param station      the observer ground station
93       * @param date         the date of the measurement
94       * @param frame        the frame of the measurement
95       * @return the "proper" direction (station-relative coordinates)
96       */
97      @DefaultDataContext
98      public static double[] naturalToProper(final double[] naturalRaDec, final GroundStation station,
99                                             final AbsoluteDate date, final Frame frame) {
100         return naturalToProper(naturalRaDec, station, date, frame, DataContext.getDefault());
101     }
102 
103     /**
104      * Natural to proper correction for aberration of light.
105      *
106      * @param naturalRaDec the "natural" direction (in barycentric coordinates)
107      * @param station      the observer ground station
108      * @param date         the date of the measurement
109      * @param frame        the frame of the measurement
110      * @param context      the data context
111      * @return the "proper" direction (station-relative coordinates)
112      * @since 12.0.1
113      */
114     public static double[] naturalToProper(final double[] naturalRaDec, final GroundStation station,
115                                            final AbsoluteDate date, final Frame frame, final DataContext context) {
116 
117         ensureFrameIsPseudoInertial(frame);
118 
119         // Velocity of station relative to barycentre (units of c)
120         final PVCoordinates baryPV = context.getCelestialBodies().getSolarSystemBarycenter().getPVCoordinates(date, frame);
121         final PVCoordinates stationPV = station.getBaseFrame().getPVCoordinates(date, frame);
122         final Vector3D stationBaryVel = stationPV.getVelocity()
123                 .subtract(baryPV.getVelocity())
124                 .scalarMultiply(1.0 / Constants.SPEED_OF_LIGHT);
125 
126         // Delegate to private method
127         return lorentzVelocitySum(naturalRaDec, stationBaryVel);
128     }
129 
130     /**
131      * Proper to natural correction for aberration of light.
132      *
133      * @param properRaDec the "proper" direction (station-relative coordinates)
134      * @param station     the observer ground station
135      * @param date        the date of the measurement
136      * @param frame       the frame of the measurement
137      * @return the "natural" direction (in barycentric coordinates)
138      */
139     @DefaultDataContext
140     public static double[] properToNatural(final double[] properRaDec, final GroundStation station,
141                                            final AbsoluteDate date, final Frame frame) {
142         return properToNatural(properRaDec, station, date, frame, DataContext.getDefault());
143     }
144 
145     /**
146      * Proper to natural correction for aberration of light.
147      *
148      * @param properRaDec the "proper" direction (station-relative coordinates)
149      * @param station     the observer ground station
150      * @param date        the date of the measurement
151      * @param frame       the frame of the measurement
152      * @param context     the data context
153      * @return the "natural" direction (in barycentric coordinates)
154      * @since 12.0.1
155      */
156     public static double[] properToNatural(final double[] properRaDec, final GroundStation station,
157                                            final AbsoluteDate date, final Frame frame, final DataContext context) {
158 
159         // Check measurement frame is inertial
160         ensureFrameIsPseudoInertial(frame);
161 
162         // Velocity of barycentre relative to station (units of c)
163         final PVCoordinates baryPV = context.getCelestialBodies().getSolarSystemBarycenter().getPVCoordinates(date, frame);
164         final PVCoordinates stationPV = station.getBaseFrame().getPVCoordinates(date, frame);
165         final Vector3D baryStationVel = baryPV.getVelocity()
166                 .subtract(stationPV.getVelocity())
167                 .scalarMultiply(1.0 / Constants.SPEED_OF_LIGHT);
168 
169         // Delegate to private method
170         return lorentzVelocitySum(properRaDec, baryStationVel);
171     }
172 
173     /**
174      * Relativistic sum of velocities.
175      * This is based on equation 3.252-3 from Seidelmann, "Explanatory Supplement to the Astronmical Almanac", 1992.
176      *
177      * @param raDec    the direction to transform
178      * @param velocity the velocity (units of c)
179      * @return the transformed direction
180      */
181     private static double[] lorentzVelocitySum(final double[] raDec, final Vector3D velocity) {
182 
183         // Measurement as unit vector
184         final Vector3D direction = new Vector3D(raDec[0], raDec[1]);
185 
186         // Coefficients for calculations
187         final double inverseBeta = FastMath.sqrt(1.0 - velocity.getNormSq());
188         final double velocityScale = 1.0 + direction.dotProduct(velocity) / (1.0 + inverseBeta);
189 
190         // From Seidelmann, equation 3.252-3 (unnormalised)
191         final Vector3D transformDirection = (direction.scalarMultiply(inverseBeta))
192                 .add(velocity.scalarMultiply(velocityScale));
193         return new double[] {transformDirection.getAlpha(), transformDirection.getDelta()};
194     }
195 
196     /**
197      * Natural to proper correction for aberration of light.
198      *
199      * @param naturalRaDec      the "natural" direction (in barycentric coordinates)
200      * @param stationToInertial the transform from station to inertial coordinates
201      * @param frame             the frame of the measurement
202      * @return the "proper" direction (station-relative coordinates)
203      */
204     @DefaultDataContext
205     public static Gradient[] fieldNaturalToProper(final Gradient[] naturalRaDec,
206                                                   final FieldTransform<Gradient> stationToInertial,
207                                                   final Frame frame) {
208         return fieldNaturalToProper(naturalRaDec, stationToInertial, frame, DataContext.getDefault());
209     }
210 
211     /**
212      * Natural to proper correction for aberration of light.
213      *
214      * @param naturalRaDec      the "natural" direction (in barycentric coordinates)
215      * @param stationToInertial the transform from station to inertial coordinates
216      * @param frame             the frame of the measurement
217      * @param context           the data context
218      * @return the "proper" direction (station-relative coordinates)
219      * @since 12.0.1
220      */
221     public static Gradient[] fieldNaturalToProper(final Gradient[] naturalRaDec,
222                                                   final FieldTransform<Gradient> stationToInertial,
223                                                   final Frame frame,
224                                                   final DataContext context) {
225 
226         // Check measurement frame is inertial
227         ensureFrameIsPseudoInertial(frame);
228 
229         // Set up field
230         final Field<Gradient> field = naturalRaDec[0].getField();
231         final FieldVector3D<Gradient> zero = FieldVector3D.getZero(field);
232         final FieldAbsoluteDate<Gradient> date = stationToInertial.getFieldDate();
233 
234         // Barycentre in inertial coordinates
235         final FieldPVCoordinates<Gradient> baryPV = context.getCelestialBodies().getSolarSystemBarycenter().getPVCoordinates(date, frame);
236 
237         // Station in inertial coordinates
238         final TimeStampedFieldPVCoordinates<Gradient> stationPV =
239                 stationToInertial.transformPVCoordinates(new TimeStampedFieldPVCoordinates<>(date, zero, zero, zero));
240 
241         // Velocity of station relative to barycentre (units of c)
242         final FieldVector3D<Gradient> stationBaryVel = stationPV.getVelocity()
243                 .subtract(baryPV.getVelocity())
244                 .scalarMultiply(1.0 / Constants.SPEED_OF_LIGHT);
245 
246         return fieldLorentzVelocitySum(naturalRaDec, stationBaryVel);
247     }
248 
249     /**
250      * Proper to natural correction for aberration of light.
251      *
252      * @param properRaDec       the "proper" direction (station-relative coordinates)
253      * @param stationToInertial the transform from station to inertial coordinates
254      * @param frame             the frame of the measurement
255      * @return the "natural" direction (in barycentric coordinates)
256      */
257     @DefaultDataContext
258     public static Gradient[] fieldProperToNatural(final Gradient[] properRaDec,
259                                                   final FieldTransform<Gradient> stationToInertial,
260                                                   final Frame frame) {
261         return fieldProperToNatural(properRaDec, stationToInertial, frame, DataContext.getDefault());
262     }
263 
264     /**
265      * Proper to natural correction for aberration of light.
266      *
267      * @param properRaDec       the "proper" direction (station-relative coordinates)
268      * @param stationToInertial the transform from station to inertial coordinates
269      * @param frame             the frame of the measurement
270      * @param context           the data context
271      * @return the "natural" direction (in barycentric coordinates)
272      * @since 12.0.1
273      */
274     public static Gradient[] fieldProperToNatural(final Gradient[] properRaDec,
275                                                   final FieldTransform<Gradient> stationToInertial,
276                                                   final Frame frame,
277                                                   final DataContext context) {
278 
279         // Check measurement frame is inertial
280         ensureFrameIsPseudoInertial(frame);
281 
282         // Set up field
283         final Field<Gradient> field = properRaDec[0].getField();
284         final FieldVector3D<Gradient> zero = FieldVector3D.getZero(field);
285         final FieldAbsoluteDate<Gradient> date = stationToInertial.getFieldDate();
286 
287         // Barycentre in inertial coordinates
288         final FieldPVCoordinates<Gradient> baryPV = context.getCelestialBodies().getSolarSystemBarycenter().getPVCoordinates(date, frame);
289 
290         // Station in inertial coordinates
291         final TimeStampedFieldPVCoordinates<Gradient> stationPV =
292                 stationToInertial.transformPVCoordinates(new TimeStampedFieldPVCoordinates<>(date, zero, zero, zero));
293 
294         // Velocity of barycentre relative to station (units of c)
295         final FieldVector3D<Gradient> stationBaryVel = stationPV.getVelocity()
296                 .negate()
297                 .add(baryPV.getVelocity())
298                 .scalarMultiply(1.0 / Constants.SPEED_OF_LIGHT);
299 
300         return fieldLorentzVelocitySum(properRaDec, stationBaryVel);
301     }
302 
303     /**
304      * Relativistic sum of velocities.
305      * This is based on equation 3.252-3 from Seidelmann, "Explanatory Supplement to the Astronmical Almanac", 1992.
306      *
307      * @param raDec    the direction to transform
308      * @param velocity the velocity (units of c)
309      * @return the transformed direction
310      */
311     private static Gradient[] fieldLorentzVelocitySum(final Gradient[] raDec,
312                                                       final FieldVector3D<Gradient> velocity) {
313 
314         // Measurement as unit vector
315         final FieldVector3D<Gradient> direction = new FieldVector3D<>(raDec[0], raDec[1]);
316 
317         // Coefficients for calculations
318         final Gradient inverseBeta = (velocity.getNormSq().negate().add(1.0)).sqrt();
319         final Gradient velocityScale = (direction.dotProduct(velocity)).divide(inverseBeta.add(1.0)).add(1.0);
320 
321         // From Seidelmann, equation 3.252-3 (unnormalised)
322         final FieldVector3D<Gradient> transformDirection = (direction.scalarMultiply(inverseBeta))
323                 .add(velocity.scalarMultiply(velocityScale));
324         return new Gradient[] {transformDirection.getAlpha(), transformDirection.getDelta()};
325     }
326 
327 
328     /** {@inheritDoc} */
329     @Override
330     public List<ParameterDriver> getParametersDrivers() {
331         return Collections.emptyList();
332     }
333 
334     /** {@inheritDoc} */
335     @Override
336     public void modifyWithoutDerivatives(final EstimatedMeasurementBase<AngularRaDec> estimated) {
337 
338         // Observation date
339         final AbsoluteDate date = estimated.getDate();
340 
341         // Observation station
342         final GroundStation station = estimated.getObservedMeasurement().getStation();
343 
344         // Observation frame
345         final Frame frame = estimated.getObservedMeasurement().getReferenceFrame();
346 
347         // Convert measurement to natural direction
348         final double[] estimatedRaDec = estimated.getEstimatedValue();
349         final double[] naturalRaDec = properToNatural(estimatedRaDec, station, date, frame, dataContext);
350 
351         // Normalise RA
352         final double[] observed           = estimated.getObservedValue();
353         final double   baseRightAscension = naturalRaDec[0];
354         final double   twoPiWrap          = MathUtils.normalizeAngle(baseRightAscension, observed[0]) - baseRightAscension;
355         final double   rightAscension     = baseRightAscension + twoPiWrap;
356 
357         // New estimated values
358         estimated.modifyEstimatedValue(this, rightAscension, naturalRaDec[1]);
359 
360     }
361 
362     /** {@inheritDoc} */
363     @Override
364     public void modify(final EstimatedMeasurement<AngularRaDec> estimated) {
365 
366         // Observation date
367         final AbsoluteDate date = estimated.getDate();
368 
369         // Observation frame
370         final Frame frame = estimated.getObservedMeasurement().getReferenceFrame();
371 
372         // Extract RA/Dec parameters (no state derivatives)
373         int nbParams = 6;
374         final Map<String, Integer> indices = new HashMap<>();
375         for (ParameterDriver driver : estimated.getObservedMeasurement().getParametersDrivers()) {
376             if (driver.isSelected()) {
377                 for (TimeSpanMap.Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
378                     if (!indices.containsKey(span.getData())) {
379                         indices.put(span.getData(), nbParams++);
380                     }
381                 }
382             }
383         }
384         final Field<Gradient> field = GradientField.getField(nbParams);
385 
386         // Observation location
387         final FieldTransform<Gradient> stationToInertial =
388                 estimated.getObservedMeasurement().getStation().getOffsetToInertial(frame, date, nbParams, indices);
389 
390         // Convert measurement to natural direction
391         final double[] estimatedRaDec = estimated.getEstimatedValue();
392         final Gradient[] estimatedRaDecDS = new Gradient[] {
393                 field.getZero().add(estimatedRaDec[0]),
394                 field.getZero().add(estimatedRaDec[1])
395         };
396         final Gradient[] naturalRaDec = fieldProperToNatural(estimatedRaDecDS, stationToInertial, frame, dataContext);
397 
398         // Normalise RA
399         final double[] observed = estimated.getObservedValue();
400         final Gradient baseRightAscension = naturalRaDec[0];
401         final double twoPiWrap = MathUtils.normalizeAngle(baseRightAscension.getReal(),
402                 observed[0]) - baseRightAscension.getReal();
403         final Gradient rightAscension = baseRightAscension.add(twoPiWrap);
404 
405         // New estimated values
406         estimated.modifyEstimatedValue(this, rightAscension.getValue(), naturalRaDec[1].getValue());
407 
408         // Derivatives (only parameter, no state)
409         final double[] raDerivatives = rightAscension.getGradient();
410         final double[] decDerivatives = naturalRaDec[1].getGradient();
411 
412         for (final ParameterDriver driver : estimated.getObservedMeasurement().getParametersDrivers()) {
413             for (TimeSpanMap.Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
414                 final Integer index = indices.get(span.getData());
415                 if (index != null) {
416                     final double[] parameterDerivative = estimated.getParameterDerivatives(driver);
417                     parameterDerivative[0] += raDerivatives[index];
418                     parameterDerivative[1] += decDerivatives[index];
419                     estimated.setParameterDerivatives(driver, span.getStart(), parameterDerivative[0], parameterDerivative[1]);
420                 }
421             }
422         }
423     }
424 
425     /**
426      * Check that given frame is pseudo-inertial. Throws an error otherwise.
427      *
428      * @param frame to check
429      *
430      * @throws OrekitException if given frame is not pseudo-inertial
431      */
432     private static void ensureFrameIsPseudoInertial(final Frame frame) {
433         // Check measurement frame is inertial
434         if (!frame.isPseudoInertial()) {
435             throw new OrekitException(OrekitMessages.NON_PSEUDO_INERTIAL_FRAME, frame.getName());
436         }
437     }
438 
439 }