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