SensorMeanPlaneCrossing.java

  1. /* Copyright 2013-2022 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.rugged.linesensor;

  18. import java.util.ArrayList;
  19. import java.util.List;
  20. import java.util.stream.Stream;

  21. import org.hipparchus.analysis.UnivariateFunction;
  22. import org.hipparchus.analysis.solvers.BracketingNthOrderBrentSolver;
  23. import org.hipparchus.analysis.solvers.UnivariateSolver;
  24. import org.hipparchus.exception.MathIllegalArgumentException;
  25. import org.hipparchus.exception.MathIllegalStateException;
  26. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  27. import org.hipparchus.linear.Array2DRowRealMatrix;
  28. import org.hipparchus.linear.ArrayRealVector;
  29. import org.hipparchus.linear.DecompositionSolver;
  30. import org.hipparchus.linear.MatrixUtils;
  31. import org.hipparchus.linear.QRDecomposition;
  32. import org.hipparchus.linear.RealMatrix;
  33. import org.hipparchus.linear.RealVector;
  34. import org.hipparchus.linear.SingularValueDecomposition;
  35. import org.hipparchus.util.FastMath;
  36. import org.hipparchus.util.Precision;
  37. import org.orekit.frames.Transform;
  38. import org.orekit.rugged.errors.RuggedException;
  39. import org.orekit.rugged.errors.RuggedInternalError;
  40. import org.orekit.rugged.utils.SpacecraftToObservedBody;
  41. import org.orekit.time.AbsoluteDate;
  42. import org.orekit.utils.Constants;
  43. import org.orekit.utils.PVCoordinates;

  44. /** Class dedicated to find when ground point crosses mean sensor plane.
  45.  * <p>
  46.  * This class is used in the first stage of inverse location.
  47.  * </p>
  48.  * @author Luc Maisonobe
  49.  */
  50. public class SensorMeanPlaneCrossing {

  51.     /** Number of cached results for guessing the line number. */
  52.     private static final int CACHED_RESULTS = 6;

  53.     /** Converter between spacecraft and body. */
  54.     private final SpacecraftToObservedBody scToBody;

  55.     /** Middle line. */
  56.     private final double midLine;

  57.     /** Body to inertial transform for middle line. */
  58.     private final Transform midBodyToInert;

  59.     /** Spacecraft to inertial transform for middle line. */
  60.     private final Transform midScToInert;

  61.     /** Minimum line number in the search interval. */
  62.     private final int minLine;

  63.     /** Maximum line number in the search interval. */
  64.     private final int maxLine;

  65.     /** Flag for light time correction. */
  66.     private final boolean lightTimeCorrection;

  67.     /** Flag for aberration of light correction. */
  68.     private final boolean aberrationOfLightCorrection;

  69.     /** Line sensor. */
  70.     private final LineSensor sensor;

  71.     /** Sensor mean plane normal. */
  72.     private final Vector3D meanPlaneNormal;

  73.     /** Maximum number of evaluations. */
  74.     private final int maxEval;

  75.     /** Accuracy to use for finding crossing line number. */
  76.     private final double accuracy;

  77.     /** Cached results. */
  78.     private final List<CrossingResult> cachedResults;

  79.     /** Simple constructor.
  80.      * @param sensor sensor to consider
  81.      * @param scToBody converter between spacecraft and body
  82.      * @param minLine minimum line number
  83.      * @param maxLine maximum line number
  84.      * @param lightTimeCorrection flag for light time correction
  85.      * @param aberrationOfLightCorrection flag for aberration of light correction.
  86.      * @param maxEval maximum number of evaluations
  87.      * @param accuracy accuracy to use for finding crossing line number
  88.      */
  89.     public SensorMeanPlaneCrossing(final LineSensor sensor,
  90.                                    final SpacecraftToObservedBody scToBody,
  91.                                    final int minLine, final int maxLine,
  92.                                    final boolean lightTimeCorrection,
  93.                                    final boolean aberrationOfLightCorrection,
  94.                                    final int maxEval, final double accuracy) {
  95.         this(sensor, scToBody, minLine, maxLine, lightTimeCorrection, aberrationOfLightCorrection,
  96.              maxEval, accuracy, computeMeanPlaneNormal(sensor, minLine, maxLine),
  97.              Stream.<CrossingResult>empty());
  98.     }

  99.     /** Simple constructor.
  100.      * @param sensor sensor to consider
  101.      * @param scToBody converter between spacecraft and body
  102.      * @param minLine minimum line number
  103.      * @param maxLine maximum line number
  104.      * @param lightTimeCorrection flag for light time correction
  105.      * @param aberrationOfLightCorrection flag for aberration of light correction.
  106.      * @param maxEval maximum number of evaluations
  107.      * @param accuracy accuracy to use for finding crossing line number
  108.      * @param meanPlaneNormal mean plane normal
  109.      * @param cachedResults cached results
  110.      */
  111.     public SensorMeanPlaneCrossing(final LineSensor sensor,
  112.                                    final SpacecraftToObservedBody scToBody,
  113.                                    final int minLine, final int maxLine,
  114.                                    final boolean lightTimeCorrection,
  115.                                    final boolean aberrationOfLightCorrection,
  116.                                    final int maxEval, final double accuracy,
  117.                                    final Vector3D meanPlaneNormal,
  118.                                    final Stream<CrossingResult> cachedResults) {

  119.         this.sensor                      = sensor;
  120.         this.minLine                     = minLine;
  121.         this.maxLine                     = maxLine;
  122.         this.lightTimeCorrection         = lightTimeCorrection;
  123.         this.aberrationOfLightCorrection = aberrationOfLightCorrection;
  124.         this.maxEval                     = maxEval;
  125.         this.accuracy                    = accuracy;
  126.         this.scToBody                    = scToBody;

  127.         this.midLine                     = 0.5 * (minLine + maxLine);
  128.         final AbsoluteDate midDate       = sensor.getDate(midLine);
  129.         this.midBodyToInert              = scToBody.getBodyToInertial(midDate);
  130.         this.midScToInert                = scToBody.getScToInertial(midDate);

  131.         this.meanPlaneNormal             = meanPlaneNormal;

  132.         this.cachedResults               = new ArrayList<>(CACHED_RESULTS);
  133.         cachedResults.forEach(crossingResult -> {
  134.             if (crossingResult != null && this.cachedResults.size() < CACHED_RESULTS) {
  135.                 this.cachedResults.add(crossingResult);
  136.             }
  137.         });

  138.     }

  139.     /** Compute the plane containing origin that best fits viewing directions point cloud.
  140.      * @param sensor line sensor
  141.      * @param minLine minimum line number
  142.      * @param maxLine maximum line number
  143.      * <p>
  144.      * The normal is oriented such that traversing pixels in increasing indices
  145.      * order corresponds to trigonometric order (i.e. counterclockwise).
  146.      * </p>
  147.      * @return normal of the mean plane
  148.      */
  149.     private static Vector3D computeMeanPlaneNormal(final LineSensor sensor, final int minLine, final int maxLine) {

  150.         final AbsoluteDate midDate = sensor.getDate(0.5 * (minLine + maxLine));

  151.         // build a centered data matrix
  152.         // (for each viewing direction, we add both the direction and its
  153.         //  opposite, thus ensuring the plane will contain origin)
  154.         final RealMatrix matrix = MatrixUtils.createRealMatrix(3, 2 * sensor.getNbPixels());
  155.         for (int i = 0; i < sensor.getNbPixels(); ++i) {
  156.             final Vector3D l = sensor.getLOS(midDate, i);
  157.             matrix.setEntry(0, 2 * i,      l.getX());
  158.             matrix.setEntry(1, 2 * i,      l.getY());
  159.             matrix.setEntry(2, 2 * i,      l.getZ());
  160.             matrix.setEntry(0, 2 * i + 1, -l.getX());
  161.             matrix.setEntry(1, 2 * i + 1, -l.getY());
  162.             matrix.setEntry(2, 2 * i + 1, -l.getZ());
  163.         }

  164.         // compute Singular Value Decomposition
  165.         final SingularValueDecomposition svd = new SingularValueDecomposition(matrix);

  166.         // extract the left singular vector corresponding to least singular value
  167.         // (i.e. last vector since Hipparchus returns the values
  168.         //  in non-increasing order)
  169.         final Vector3D singularVector = new Vector3D(svd.getU().getColumn(2)).normalize();

  170.         // check rotation order
  171.         final Vector3D first = sensor.getLOS(midDate, 0);
  172.         final Vector3D last  = sensor.getLOS(midDate, sensor.getNbPixels() - 1);
  173.         if (Vector3D.dotProduct(singularVector, Vector3D.crossProduct(first, last)) >= 0) {
  174.             return singularVector;
  175.         } else {
  176.             return singularVector.negate();
  177.         }

  178.     }

  179.     /** Get the underlying sensor.
  180.      * @return underlying sensor
  181.      */
  182.     public LineSensor getSensor() {
  183.         return sensor;
  184.     }

  185.     /** Get converter between spacecraft and body.
  186.      * @return converter between spacecraft and body
  187.      */
  188.     public SpacecraftToObservedBody getScToBody() {
  189.         return scToBody;
  190.     }

  191.     /** Get the minimum line number in the search interval.
  192.      * @return minimum line number in the search interval
  193.      */
  194.     public int getMinLine() {
  195.         return minLine;
  196.     }

  197.     /** Get the maximum line number in the search interval.
  198.      * @return maximum line number in the search interval
  199.      */
  200.     public int getMaxLine() {
  201.         return maxLine;
  202.     }

  203.     /** Get the maximum number of evaluations.
  204.      * @return maximum number of evaluations
  205.      */
  206.     public int getMaxEval() {
  207.         return maxEval;
  208.     }

  209.     /** Get the accuracy to use for finding crossing line number.
  210.      * @return accuracy to use for finding crossing line number
  211.      */
  212.     public double getAccuracy() {
  213.         return accuracy;
  214.     }

  215.     /** Get the mean plane normal.
  216.      * <p>
  217.      * The normal is oriented such traversing pixels in increasing indices
  218.      * order corresponds is consistent with trigonometric order (i.e.
  219.      * counterclockwise).
  220.      * </p>
  221.      * @return mean plane normal
  222.      */
  223.     public Vector3D getMeanPlaneNormal() {
  224.         return meanPlaneNormal;
  225.     }

  226.     /** Get cached previous results.
  227.      * @return cached previous results
  228.      */
  229.     public Stream<CrossingResult> getCachedResults() {
  230.         return cachedResults.stream();
  231.     }

  232.     /** Container for mean plane crossing result. */
  233.     public static class CrossingResult {

  234.         /** Crossing date. */
  235.         private final AbsoluteDate crossingDate;

  236.         /** Crossing line. */
  237.         private final double crossingLine;

  238.         /** Target ground point. */
  239.         private final Vector3D target;

  240.         /** Target direction in spacecraft frame. */
  241.         private final Vector3D targetDirection;

  242.         /** Derivative of the target direction in spacecraft frame with respect to line number. */
  243.         private final Vector3D targetDirectionDerivative;

  244.         /** Simple constructor.
  245.          * @param crossingDate crossing date
  246.          * @param crossingLine crossing line
  247.          * @param target target ground point
  248.          * @param targetDirection target direction in spacecraft frame
  249.          * @param targetDirectionDerivative derivative of the target direction in spacecraft frame with respect to line number
  250.          */
  251.         public CrossingResult(final AbsoluteDate crossingDate, final double crossingLine,
  252.                               final Vector3D target,
  253.                               final Vector3D targetDirection,
  254.                               final Vector3D targetDirectionDerivative) {
  255.             this.crossingDate              = crossingDate;
  256.             this.crossingLine              = crossingLine;
  257.             this.target                    = target;
  258.             this.targetDirection           = targetDirection;
  259.             this.targetDirectionDerivative = targetDirectionDerivative;
  260.         }

  261.         /** Get the crossing date.
  262.          * @return crossing date
  263.          */
  264.         public AbsoluteDate getDate() {
  265.             return crossingDate;
  266.         }

  267.         /** Get the crossing line.
  268.          * @return crossing line
  269.          */
  270.         public double getLine() {
  271.             return crossingLine;
  272.         }

  273.         /** Get the target ground point.
  274.          * @return target ground point
  275.          */
  276.         public Vector3D getTarget() {
  277.             return target;
  278.         }

  279.         /** Get the normalized target direction in spacecraft frame at crossing.
  280.          * @return normalized target direction in spacecraft frame at crossing
  281.          * with respect to line number
  282.          */
  283.         public Vector3D getTargetDirection() {
  284.             return targetDirection;
  285.         }

  286.         /** Get the derivative of the normalized target direction in spacecraft frame at crossing.
  287.          * @return derivative of the normalized target direction in spacecraft frame at crossing
  288.          * with respect to line number
  289.          * @since 2.0
  290.          */
  291.         public Vector3D getTargetDirectionDerivative() {
  292.             return targetDirectionDerivative;
  293.         }

  294.     }

  295.     /** Find mean plane crossing.
  296.      * @param target target ground point
  297.      * @return line number and target direction at mean plane crossing,
  298.      * or null if search interval does not bracket a solution
  299.      */
  300.     public CrossingResult find(final Vector3D target) {

  301.         double crossingLine     = midLine;
  302.         Transform bodyToInert   = midBodyToInert;
  303.         Transform scToInert     = midScToInert;

  304.         // count the number of available results
  305.         if (cachedResults.size() >= 4) {
  306.             // we already have computed at lest 4 values, we attempt to build a linear
  307.             // model to guess a better start line
  308.             final double guessedCrossingLine = guessStartLine(target);
  309.             if (guessedCrossingLine >= minLine && guessedCrossingLine <= maxLine) {
  310.                 crossingLine = guessedCrossingLine;
  311.                 final AbsoluteDate date = sensor.getDate(crossingLine);
  312.                 bodyToInert = scToBody.getBodyToInertial(date);
  313.                 scToInert   = scToBody.getScToInertial(date);
  314.             }
  315.         }

  316.         final PVCoordinates targetPV = new PVCoordinates(target, Vector3D.ZERO);

  317.         // we don't use an Hipparchus solver here because we are more
  318.         // interested in reducing the number of evaluations than being accurate,
  319.         // as we know the solution is improved in the second stage of inverse location.
  320.         // We expect two or three evaluations only. Each new evaluation shows up quickly in
  321.         // the performances as it involves frames conversions
  322.         final double[]  crossingLineHistory = new double[maxEval];
  323.         final double[]  betaHistory         = new double[maxEval];
  324.         final double[]  betaDerHistory      = new double[maxEval];
  325.         boolean         atMin               = false;
  326.         boolean         atMax               = false;
  327.         for (int i = 0; i < maxEval; ++i) {

  328.             crossingLineHistory[i] = crossingLine;
  329.             final Vector3D[] targetDirection =
  330.                     evaluateLine(crossingLine, targetPV, bodyToInert, scToInert);
  331.             betaHistory[i] = FastMath.acos(Vector3D.dotProduct(targetDirection[0], meanPlaneNormal));
  332.             final double s = Vector3D.dotProduct(targetDirection[1], meanPlaneNormal);
  333.             betaDerHistory[i] = -s / FastMath.sqrt(1 - s * s);

  334.             final double deltaL;
  335.             if (i == 0) {
  336.                 // simple Newton iteration
  337.                 deltaL        = (0.5 * FastMath.PI - betaHistory[i]) / betaDerHistory[i];
  338.                 crossingLine += deltaL;
  339.             } else {
  340.                 // inverse cubic iteration
  341.                 final double a0    = betaHistory[i - 1] - 0.5 * FastMath.PI;
  342.                 final double l0    = crossingLineHistory[i - 1];
  343.                 final double d0    = betaDerHistory[i - 1];
  344.                 final double a1    = betaHistory[i]     - 0.5 * FastMath.PI;
  345.                 final double l1    = crossingLineHistory[i];
  346.                 final double d1    = betaDerHistory[i];
  347.                 final double a1Ma0 = a1 - a0;
  348.                 crossingLine = ((l0 * (a1 - 3 * a0) - a0 * a1Ma0 / d0) * a1 * a1 +
  349.                                 (l1 * (3 * a1 - a0) - a1 * a1Ma0 / d1) * a0 * a0
  350.                                ) / (a1Ma0 * a1Ma0 * a1Ma0);
  351.                 deltaL = crossingLine - l1;
  352.             }
  353.             if (FastMath.abs(deltaL) <= accuracy) {
  354.                 // return immediately, without doing any additional evaluation!
  355.                 final CrossingResult crossingResult =
  356.                     new CrossingResult(sensor.getDate(crossingLine), crossingLine, target,
  357.                                        targetDirection[0], targetDirection[1]);
  358.                 boolean isNew = true;
  359.                 for (final CrossingResult existing : cachedResults) {
  360.                     isNew = isNew && FastMath.abs(crossingLine - existing.crossingLine) > accuracy;
  361.                 }
  362.                 if (isNew) {
  363.                     // this result is different from the existing ones,
  364.                     // it brings new sampling data to the cache
  365.                     if (cachedResults.size() >= CACHED_RESULTS) {
  366.                         cachedResults.remove(cachedResults.size() - 1);
  367.                     }
  368.                     cachedResults.add(0, crossingResult);
  369.                 }
  370.                 return crossingResult;
  371.             }
  372.             for (int j = 0; j < i; ++j) {
  373.                 if (FastMath.abs(crossingLine - crossingLineHistory[j]) <= 1.0) {
  374.                     // rare case: we are stuck in a loop!
  375.                     // switch to a more robust (but slower) algorithm in this case
  376.                     final CrossingResult slowResult = slowFind(targetPV, crossingLine);
  377.                     if (slowResult == null) {
  378.                         return null;
  379.                     }
  380.                     if (cachedResults.size() >= CACHED_RESULTS) {
  381.                         cachedResults.remove(cachedResults.size() - 1);
  382.                     }
  383.                     cachedResults.add(0, slowResult);
  384.                     return cachedResults.get(0);
  385.                 }
  386.             }

  387.             if (crossingLine < minLine) {
  388.                 if (atMin) {
  389.                     // we were already trying at minLine and we need to go below that
  390.                     // give up as the solution is out of search interval
  391.                     return null;
  392.                 }
  393.                 atMin        = true;
  394.                 crossingLine = minLine;
  395.             } else if (crossingLine > maxLine) {
  396.                 if (atMax) {
  397.                     // we were already trying at maxLine and we need to go above that
  398.                     // give up as the solution is out of search interval
  399.                     return null;
  400.                 }
  401.                 atMax        = true;
  402.                 crossingLine = maxLine;
  403.             } else {
  404.                 // the next evaluation will be a regular point
  405.                 atMin = false;
  406.                 atMax = false;
  407.             }

  408.             final AbsoluteDate date = sensor.getDate(crossingLine);
  409.             bodyToInert = scToBody.getBodyToInertial(date);
  410.             scToInert   = scToBody.getScToInertial(date);
  411.         }

  412.         return null;

  413.     }

  414.     /** Guess a start line using the last four results.
  415.      * @param target target ground point
  416.      * @return guessed start line
  417.      */
  418.     private double guessStartLine(final Vector3D target) {
  419.         try {

  420.             // assume a linear model of the form: l = ax + by + cz + d
  421.             final int        n = cachedResults.size();
  422.             final RealMatrix m = new Array2DRowRealMatrix(n, 4);
  423.             final RealVector v = new ArrayRealVector(n);
  424.             for (int i = 0; i < n; ++i) {
  425.                 final CrossingResult crossingResult = cachedResults.get(i);
  426.                 m.setEntry(i, 0, crossingResult.getTarget().getX());
  427.                 m.setEntry(i, 1, crossingResult.getTarget().getY());
  428.                 m.setEntry(i, 2, crossingResult.getTarget().getZ());
  429.                 m.setEntry(i, 3, 1.0);
  430.                 v.setEntry(i, crossingResult.getLine());
  431.             }

  432.             final DecompositionSolver solver = new QRDecomposition(m, Precision.SAFE_MIN).getSolver();
  433.             final RealVector coefficients = solver.solve(v);

  434.             // apply the linear model
  435.             return target.getX() * coefficients.getEntry(0) +
  436.                    target.getY() * coefficients.getEntry(1) +
  437.                    target.getZ() * coefficients.getEntry(2) +
  438.                    coefficients.getEntry(3);

  439.         } catch (MathIllegalStateException sme) {
  440.             // the points are not independent
  441.             return Double.NaN;
  442.         }
  443.     }

  444.     /** Find mean plane crossing using a slow but robust method.
  445.      * @param targetPV target ground point
  446.      * @param initialGuess initial guess for the crossing line
  447.      * @return line number and target direction at mean plane crossing,
  448.      * or null if search interval does not bracket a solution
  449.      */
  450.     private CrossingResult slowFind(final PVCoordinates targetPV, final double initialGuess) {

  451.         try {
  452.             // safety check
  453.             final double startValue;
  454.             if (initialGuess < minLine || initialGuess > maxLine) {
  455.                 startValue = midLine;
  456.             } else {
  457.                 startValue = initialGuess;
  458.             }

  459.             final UnivariateSolver solver = new BracketingNthOrderBrentSolver(accuracy, 5);
  460.             final double crossingLine = solver.solve(maxEval, new UnivariateFunction() {
  461.                 /** {@inheritDoc} */
  462.                 @Override
  463.                 public double value(final double x) {
  464.                     try {
  465.                         final AbsoluteDate date = sensor.getDate(x);
  466.                         final Vector3D[] targetDirection =
  467.                                 evaluateLine(x, targetPV, scToBody.getBodyToInertial(date), scToBody.getScToInertial(date));
  468.                         return 0.5 * FastMath.PI - FastMath.acos(Vector3D.dotProduct(targetDirection[0], meanPlaneNormal));
  469.                     } catch (RuggedException re) {
  470.                         throw new RuggedInternalError(re);
  471.                     }
  472.                 }
  473.             }, minLine, maxLine, startValue);

  474.             final AbsoluteDate date = sensor.getDate(crossingLine);
  475.             final Vector3D[] targetDirection =
  476.                     evaluateLine(crossingLine, targetPV, scToBody.getBodyToInertial(date), scToBody.getScToInertial(date));
  477.             return new CrossingResult(sensor.getDate(crossingLine), crossingLine, targetPV.getPosition(),
  478.                                       targetDirection[0], targetDirection[1]);

  479.         } catch (MathIllegalArgumentException nbe) {
  480.             return null;
  481.         }
  482.     }

  483.     /** Evaluate geometry for a given line number.
  484.      * @param lineNumber current line number
  485.      * @param targetPV target ground point
  486.      * @param bodyToInert transform from observed body to inertial frame, for current line
  487.      * @param scToInert transform from inertial frame to spacecraft frame, for current line
  488.      * @return target direction in spacecraft frame, with its first derivative
  489.      * with respect to line number
  490.      */
  491.     private Vector3D[] evaluateLine(final double lineNumber, final PVCoordinates targetPV,
  492.                                     final Transform bodyToInert, final Transform scToInert) {

  493.         // compute the transform between spacecraft and observed body
  494.         final PVCoordinates refInert =
  495.                 scToInert.transformPVCoordinates(new PVCoordinates(sensor.getPosition(), Vector3D.ZERO));

  496.         final PVCoordinates targetInert;
  497.         if (lightTimeCorrection) {
  498.             // apply light time correction
  499.             final Vector3D iT     = bodyToInert.transformPosition(targetPV.getPosition());
  500.             final double   deltaT = refInert.getPosition().distance(iT) / Constants.SPEED_OF_LIGHT;
  501.             targetInert           = bodyToInert.shiftedBy(-deltaT).transformPVCoordinates(targetPV);
  502.         } else {
  503.             // don't apply light time correction
  504.             targetInert = bodyToInert.transformPVCoordinates(targetPV);
  505.         }

  506.         final PVCoordinates lInert    = new PVCoordinates(refInert, targetInert);
  507.         final Transform     inertToSc = scToInert.getInverse();
  508.         final Vector3D obsLInert;
  509.         final Vector3D obsLInertDot;
  510.         if (aberrationOfLightCorrection) {

  511.             // apply aberration of light correction
  512.             // as the spacecraft velocity is small with respect to speed of light,
  513.             // we use classical velocity addition and not relativistic velocity addition
  514.             // we have: c * lInert + vsat = k * obsLInert
  515.             final PVCoordinates spacecraftPV = scToInert.transformPVCoordinates(PVCoordinates.ZERO);
  516.             final Vector3D  l        = lInert.getPosition().normalize();
  517.             final Vector3D  lDot     = normalizedDot(lInert.getPosition(), lInert.getVelocity());
  518.             final Vector3D  kObs     = new Vector3D(Constants.SPEED_OF_LIGHT, l, +1.0, spacecraftPV.getVelocity());
  519.             obsLInert = kObs.normalize();

  520.             // the following derivative is computed under the assumption the spacecraft velocity
  521.             // is constant in inertial frame ... It is obviously not true, but as this velocity
  522.             // is very small with respect to speed of light, the error is expected to remain small
  523.             obsLInertDot = normalizedDot(kObs, new Vector3D(Constants.SPEED_OF_LIGHT, lDot));

  524.         } else {

  525.             // don't apply aberration of light correction
  526.             obsLInert    = lInert.getPosition().normalize();
  527.             obsLInertDot = normalizedDot(lInert.getPosition(), lInert.getVelocity());

  528.         }
  529.         final Vector3D dir    = inertToSc.transformVector(obsLInert);
  530.         final Vector3D dirDot = new Vector3D(+1.0, inertToSc.transformVector(obsLInertDot),
  531.                                              -1.0, Vector3D.crossProduct(inertToSc.getRotationRate(), dir));

  532.         // combine vector value and derivative
  533.         final double rate = sensor.getRate(lineNumber);
  534.         return new Vector3D[] {
  535.             dir, dirDot.scalarMultiply(1.0 / rate)
  536.         };

  537.     }

  538.     /** Compute the derivative of normalized vector.
  539.      * @param u base vector
  540.      * @param uDot derivative of the base vector
  541.      * @return vDot, where v = u / ||u||
  542.      */
  543.     private Vector3D normalizedDot(final Vector3D u, final Vector3D uDot) {
  544.         final double n = u.getNorm();
  545.         return new Vector3D(1.0 / n, uDot, -Vector3D.dotProduct(u, uDot) / (n * n * n), u);
  546.     }

  547. }