1   /* Copyright 2013-2016 CS Systèmes d'Information
2    * Licensed to CS Systèmes d'Information (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  
19  import org.apache.commons.math3.analysis.UnivariateFunction;
20  import org.apache.commons.math3.analysis.differentiation.DerivativeStructure;
21  import org.apache.commons.math3.analysis.solvers.BracketingNthOrderBrentSolver;
22  import org.apache.commons.math3.analysis.solvers.UnivariateSolver;
23  import org.apache.commons.math3.exception.NoBracketingException;
24  import org.apache.commons.math3.geometry.euclidean.threed.FieldVector3D;
25  import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
26  import org.apache.commons.math3.linear.Array2DRowRealMatrix;
27  import org.apache.commons.math3.linear.ArrayRealVector;
28  import org.apache.commons.math3.linear.DecompositionSolver;
29  import org.apache.commons.math3.linear.MatrixUtils;
30  import org.apache.commons.math3.linear.QRDecomposition;
31  import org.apache.commons.math3.linear.RealMatrix;
32  import org.apache.commons.math3.linear.RealVector;
33  import org.apache.commons.math3.linear.SingularMatrixException;
34  import org.apache.commons.math3.linear.SingularValueDecomposition;
35  import org.apache.commons.math3.util.FastMath;
36  import org.apache.commons.math3.util.Precision;
37  import org.orekit.frames.Transform;
38  import org.orekit.rugged.errors.RuggedException;
39  import org.orekit.rugged.errors.RuggedExceptionWrapper;
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  
45  /** Class dedicated to find when ground point crosses mean sensor plane.
46   * <p>
47   * This class is used in the first stage of inverse location.
48   * </p>
49   * @author Luc Maisonobe
50   */
51  public class SensorMeanPlaneCrossing {
52  
53      /** Number of cached results for guessing the line number. */
54      private static final int CACHED_RESULTS = 6;
55  
56      /** Converter between spacecraft and body. */
57      private final SpacecraftToObservedBody scToBody;
58  
59      /** Middle line. */
60      private final double midLine;
61  
62      /** Body to inertial transform for middle line. */
63      private final Transform midBodyToInert;
64  
65      /** Spacecraft to inertial transform for middle line. */
66      private final Transform midScToInert;
67  
68      /** Minimum line number in the search interval. */
69      private final int minLine;
70  
71      /** Maximum line number in the search interval. */
72      private final int maxLine;
73  
74      /** Flag for light time correction. */
75      private final boolean lightTimeCorrection;
76  
77      /** Flag for aberration of light correction. */
78      private final boolean aberrationOfLightCorrection;
79  
80      /** Line sensor. */
81      private final LineSensor sensor;
82  
83      /** Sensor mean plane normal. */
84      private final Vector3D meanPlaneNormal;
85  
86      /** Maximum number of evaluations. */
87      private final int maxEval;
88  
89      /** Accuracy to use for finding crossing line number. */
90      private final double accuracy;
91  
92      /** Cached results. */
93      private final CrossingResult[] cachedResults;
94  
95      /** Simple constructor.
96       * @param sensor sensor to consider
97       * @param scToBody converter between spacecraft and body
98       * @param minLine minimum line number
99       * @param maxLine maximum line number
100      * @param lightTimeCorrection flag for light time correction
101      * @param aberrationOfLightCorrection flag for aberration of light correction.
102      * @param maxEval maximum number of evaluations
103      * @param accuracy accuracy to use for finding crossing line number
104      * @exception RuggedException if some frame conversion fails
105      */
106     public SensorMeanPlaneCrossing(final LineSensor sensor,
107                                    final SpacecraftToObservedBody scToBody,
108                                    final int minLine, final int maxLine,
109                                    final boolean lightTimeCorrection,
110                                    final boolean aberrationOfLightCorrection,
111                                    final int maxEval, final double accuracy)
112         throws RuggedException {
113         this(sensor, scToBody, minLine, maxLine, lightTimeCorrection, aberrationOfLightCorrection,
114              maxEval, accuracy, computeMeanPlaneNormal(sensor, minLine, maxLine), new CrossingResult[0]);
115     }
116 
117     /** Simple constructor.
118      * @param sensor sensor to consider
119      * @param scToBody converter between spacecraft and body
120      * @param minLine minimum line number
121      * @param maxLine maximum line number
122      * @param lightTimeCorrection flag for light time correction
123      * @param aberrationOfLightCorrection flag for aberration of light correction.
124      * @param maxEval maximum number of evaluations
125      * @param accuracy accuracy to use for finding crossing line number
126      * @param meanPlaneNormal mean plane normal
127      * @param cachedResults cached results
128      * @exception RuggedException if some frame conversion fails
129      */
130     public SensorMeanPlaneCrossing(final LineSensor sensor,
131                                    final SpacecraftToObservedBody scToBody,
132                                    final int minLine, final int maxLine,
133                                    final boolean lightTimeCorrection,
134                                    final boolean aberrationOfLightCorrection,
135                                    final int maxEval, final double accuracy,
136                                    final Vector3D meanPlaneNormal,
137                                    final CrossingResult[] cachedResults)
138         throws RuggedException {
139 
140         this.sensor                      = sensor;
141         this.minLine                     = minLine;
142         this.maxLine                     = maxLine;
143         this.lightTimeCorrection         = lightTimeCorrection;
144         this.aberrationOfLightCorrection = aberrationOfLightCorrection;
145         this.maxEval                     = maxEval;
146         this.accuracy                    = accuracy;
147         this.scToBody                    = scToBody;
148 
149         this.midLine                     = 0.5 * (minLine + maxLine);
150         final AbsoluteDate midDate       = sensor.getDate(midLine);
151         this.midBodyToInert              = scToBody.getBodyToInertial(midDate);
152         this.midScToInert                = scToBody.getScToInertial(midDate);
153 
154         this.meanPlaneNormal             = meanPlaneNormal;
155 
156         this.cachedResults               = new CrossingResult[CACHED_RESULTS];
157         System.arraycopy(cachedResults, 0, this.cachedResults, 0, cachedResults.length);
158 
159     }
160 
161     /** Compute the plane containing origin that best fits viewing directions point cloud.
162      * @param sensor line sensor
163      * @param minLine minimum line number
164      * @param maxLine maximum line number
165      * <p>
166      * The normal is oriented such that traversing pixels in increasing indices
167      * order corresponds to trigonometric order (i.e. counterclockwise).
168      * </p>
169      * @return normal of the mean plane
170      * @exception RuggedException if mid date cannot be handled
171      */
172     private static Vector3D computeMeanPlaneNormal(final LineSensor sensor, final int minLine, final int maxLine)
173         throws RuggedException {
174 
175         final AbsoluteDate midDate = sensor.getDate(0.5 * (minLine + maxLine));
176 
177         // build a centered data matrix
178         // (for each viewing direction, we add both the direction and its
179         //  opposite, thus ensuring the plane will contain origin)
180         final RealMatrix matrix = MatrixUtils.createRealMatrix(3, 2 * sensor.getNbPixels());
181         for (int i = 0; i < sensor.getNbPixels(); ++i) {
182             final Vector3D l = sensor.getLos(midDate, i);
183             matrix.setEntry(0, 2 * i,      l.getX());
184             matrix.setEntry(1, 2 * i,      l.getY());
185             matrix.setEntry(2, 2 * i,      l.getZ());
186             matrix.setEntry(0, 2 * i + 1, -l.getX());
187             matrix.setEntry(1, 2 * i + 1, -l.getY());
188             matrix.setEntry(2, 2 * i + 1, -l.getZ());
189         }
190 
191         // compute Singular Value Decomposition
192         final SingularValueDecomposition svd = new SingularValueDecomposition(matrix);
193 
194         // extract the left singular vector corresponding to least singular value
195         // (i.e. last vector since Apache Commons Math returns the values
196         //  in non-increasing order)
197         final Vector3D singularVector = new Vector3D(svd.getU().getColumn(2)).normalize();
198 
199         // check rotation order
200         final Vector3D first = sensor.getLos(midDate, 0);
201         final Vector3D last  = sensor.getLos(midDate, sensor.getNbPixels() - 1);
202         if (Vector3D.dotProduct(singularVector, Vector3D.crossProduct(first, last)) >= 0) {
203             return singularVector;
204         } else {
205             return singularVector.negate();
206         }
207 
208     }
209 
210     /** Get the underlying sensor.
211      * @return underlying sensor
212      */
213     public LineSensor getSensor() {
214         return sensor;
215     }
216 
217     /** Get converter between spacecraft and body.
218      * @return converter between spacecraft and body
219      */
220     public SpacecraftToObservedBody getScToBody() {
221         return scToBody;
222     }
223 
224     /** Get the minimum line number in the search interval.
225      * @return minimum line number in the search interval
226      */
227     public int getMinLine() {
228         return minLine;
229     }
230 
231     /** Get the maximum line number in the search interval.
232      * @return maximum line number in the search interval
233      */
234     public int getMaxLine() {
235         return maxLine;
236     }
237 
238     /** Get the maximum number of evaluations.
239      * @return maximum number of evaluations
240      */
241     public int getMaxEval() {
242         return maxEval;
243     }
244 
245     /** Get the accuracy to use for finding crossing line number.
246      * @return accuracy to use for finding crossing line number
247      */
248     public double getAccuracy() {
249         return accuracy;
250     }
251 
252     /** Get the mean plane normal.
253      * <p>
254      * The normal is oriented such traversing pixels in increasing indices
255      * order corresponds is consistent with trigonometric order (i.e.
256      * counterclockwise).
257      * </p>
258      * @return mean plane normal
259      */
260     public Vector3D getMeanPlaneNormal() {
261         return meanPlaneNormal;
262     }
263 
264     /** Get the cached previous results.
265      * @return mean plane normal
266      */
267     public CrossingResult[] getCachedResults() {
268         return cachedResults;
269     }
270 
271     /** Container for mean plane crossing result. */
272     public static class CrossingResult {
273 
274         /** Crossing date. */
275         private final AbsoluteDate crossingDate;
276 
277         /** Crossing line. */
278         private final double crossingLine;
279 
280         /** Target ground point. */
281         private final Vector3D target;
282 
283         /** Target direction in spacecraft frame. */
284         private final FieldVector3D<DerivativeStructure> targetDirection;
285 
286         /** Simple constructor.
287          * @param crossingDate crossing date
288          * @param crossingLine crossing line
289          * @param target target ground point
290          * @param targetDirection target direction in spacecraft frame
291          */
292         public CrossingResult(final AbsoluteDate crossingDate, final double crossingLine,
293                               final Vector3D target,
294                               final FieldVector3D<DerivativeStructure> targetDirection) {
295             this.crossingDate    = crossingDate;
296             this.crossingLine    = crossingLine;
297             this.target          = target;
298             this.targetDirection = targetDirection;
299         }
300 
301         /** Get the crossing date.
302          * @return crossing date
303          */
304         public AbsoluteDate getDate() {
305             return crossingDate;
306         }
307 
308         /** Get the crossing line.
309          * @return crossing line
310          */
311         public double getLine() {
312             return crossingLine;
313         }
314 
315         /** Get the target ground point.
316          * @return target ground point
317          */
318         public Vector3D getTarget() {
319             return target;
320         }
321 
322         /** Get the normalized target direction in spacecraft frame at crossing.
323          * @return normalized target direction in spacecraft frame at crossing, with its first derivative
324          * with respect to line number
325          */
326         public FieldVector3D<DerivativeStructure> getTargetDirection() {
327             return targetDirection;
328         }
329 
330     }
331 
332     /** Find mean plane crossing.
333      * @param target target ground point
334      * @return line number and target direction at mean plane crossing,
335      * or null if search interval does not bracket a solution
336      * @exception RuggedException if geometry cannot be computed for some line or
337      * if the maximum number of evaluations is exceeded
338      */
339     public CrossingResult find(final Vector3D target)
340         throws RuggedException {
341 
342         double crossingLine     = midLine;
343         Transform bodyToInert   = midBodyToInert;
344         Transform scToInert     = midScToInert;
345 
346         // count the number of available results
347         int n = 0;
348         while (n < cachedResults.length && cachedResults[n] != null) {
349             ++n;
350         }
351         if (n >= 4) {
352             // we already have computed at lest 4 values, we attempt to build a linear
353             // model to guess a better start line
354             final double guessedCrossingLine = guessStartLine(n, target);
355             if (guessedCrossingLine >= minLine && guessedCrossingLine <= maxLine) {
356                 crossingLine = guessedCrossingLine;
357                 final AbsoluteDate date = sensor.getDate(crossingLine);
358                 bodyToInert = scToBody.getBodyToInertial(date);
359                 scToInert   = scToBody.getScToInertial(date);
360             }
361         }
362 
363         final PVCoordinates targetPV = new PVCoordinates(target, Vector3D.ZERO);
364 
365         // we don't use an Apache Commons Math solver here because we are more
366         // interested in reducing the number of evaluations than being accurate,
367         // as we know the solution is improved in the second stage of inverse location.
368         // We expect two or three evaluations only. Each new evaluation shows up quickly in
369         // the performances as it involves frames conversions
370         final double[]               crossingLineHistory = new double[maxEval];
371         final DerivativeStructure[]  betaHistory         = new DerivativeStructure[maxEval];
372         boolean atMin           = false;
373         boolean atMax           = false;
374         for (int i = 0; i < maxEval; ++i) {
375 
376             crossingLineHistory[i] = crossingLine;
377             final FieldVector3D<DerivativeStructure> targetDirection =
378                     evaluateLine(crossingLine, targetPV, bodyToInert, scToInert);
379             betaHistory[i] = FieldVector3D.angle(targetDirection, meanPlaneNormal);
380 
381             final double deltaL;
382             if (i == 0) {
383                 // simple Newton iteration
384                 deltaL        = (0.5 * FastMath.PI - betaHistory[i].getValue()) / betaHistory[i].getPartialDerivative(1);
385                 crossingLine += deltaL;
386             } else {
387                 // inverse cubic iteration
388                 final double a0    = betaHistory[i - 1].getValue() - 0.5 * FastMath.PI;
389                 final double l0    = crossingLineHistory[i - 1];
390                 final double d0    = betaHistory[i - 1].getPartialDerivative(1);
391                 final double a1    = betaHistory[i].getValue()     - 0.5 * FastMath.PI;
392                 final double l1    = crossingLineHistory[i];
393                 final double d1    = betaHistory[i].getPartialDerivative(1);
394                 final double a1Ma0 = a1 - a0;
395                 crossingLine = ((l0 * (a1 - 3 * a0) - a0 * a1Ma0 / d0) * a1 * a1 +
396                                 (l1 * (3 * a1 - a0) - a1 * a1Ma0 / d1) * a0 * a0
397                                ) / (a1Ma0 * a1Ma0 * a1Ma0);
398                 deltaL = crossingLine - l1;
399             }
400             if (FastMath.abs(deltaL) <= accuracy) {
401                 // return immediately, without doing any additional evaluation!
402                 for (int k = cachedResults.length - 1; k > 0; --k) {
403                     cachedResults[k] = cachedResults[k - 1];
404                 }
405                 cachedResults[0] = new CrossingResult(sensor.getDate(crossingLine), crossingLine, target, targetDirection);
406                 return cachedResults[0];
407             }
408             for (int j = 0; j < i; ++j) {
409                 if (FastMath.abs(crossingLine - crossingLineHistory[j]) <= 1.0) {
410                     // rare case: we are stuck in a loop!
411                     // switch to a more robust (but slower) algorithm in this case
412                     final CrossingResult slowResult = slowFind(targetPV, crossingLine);
413                     if (slowResult == null) {
414                         return null;
415                     }
416                     for (int k = cachedResults.length - 1; k > 0; --k) {
417                         cachedResults[k] = cachedResults[k - 1];
418                     }
419                     cachedResults[0] = slowResult;
420                     return cachedResults[0];
421                 }
422             }
423 
424             if (crossingLine < minLine) {
425                 if (atMin) {
426                     // we were already trying at minLine and we need to go below that
427                     // give up as the solution is out of search interval
428                     return null;
429                 }
430                 atMin        = true;
431                 crossingLine = minLine;
432             } else if (crossingLine > maxLine) {
433                 if (atMax) {
434                     // we were already trying at maxLine and we need to go above that
435                     // give up as the solution is out of search interval
436                     return null;
437                 }
438                 atMax        = true;
439                 crossingLine = maxLine;
440             } else {
441                 // the next evaluation will be a regular point
442                 atMin = false;
443                 atMax = false;
444             }
445 
446             final AbsoluteDate date = sensor.getDate(crossingLine);
447             bodyToInert = scToBody.getBodyToInertial(date);
448             scToInert   = scToBody.getScToInertial(date);
449         }
450 
451         return null;
452 
453     }
454 
455     /** Guess a start line using the last four results.
456      * @param n number of cached results available
457      * @param target target ground point
458      * @return guessed start line
459      */
460     private double guessStartLine(final int n, final Vector3D target) {
461         try {
462 
463             // assume a linear model of the form: l = ax + by + cz + d
464             final RealMatrix m = new Array2DRowRealMatrix(n, 4);
465             final RealVector v = new ArrayRealVector(n);
466             for (int i = 0; i < n; ++i) {
467                 m.setEntry(i, 0, cachedResults[i].getTarget().getX());
468                 m.setEntry(i, 1, cachedResults[i].getTarget().getY());
469                 m.setEntry(i, 2, cachedResults[i].getTarget().getZ());
470                 m.setEntry(i, 3, 1.0);
471                 v.setEntry(i, cachedResults[i].getLine());
472             }
473 
474             final DecompositionSolver solver = new QRDecomposition(m, Precision.SAFE_MIN).getSolver();
475             final RealVector coefficients = solver.solve(v);
476 
477             // apply the linear model
478             return target.getX() * coefficients.getEntry(0) +
479                    target.getY() * coefficients.getEntry(1) +
480                    target.getZ() * coefficients.getEntry(2) +
481                    coefficients.getEntry(3);
482 
483         } catch (SingularMatrixException sme) {
484             // the points are not independent
485             return Double.NaN;
486         }
487     }
488 
489     /** Find mean plane crossing using a slow but robust method.
490      * @param targetPV target ground point
491      * @param initialGuess initial guess for the crossing line
492      * @return line number and target direction at mean plane crossing,
493      * or null if search interval does not bracket a solution
494      * @exception RuggedException if geometry cannot be computed for some line or
495      * if the maximum number of evaluations is exceeded
496      */
497     public CrossingResult slowFind(final PVCoordinates targetPV, final double initialGuess)
498         throws RuggedException {
499         try {
500 
501             // safety check
502             final double startValue;
503             if (initialGuess < minLine || initialGuess > maxLine) {
504                 startValue = midLine;
505             } else {
506                 startValue = initialGuess;
507             }
508 
509             final UnivariateSolver solver = new BracketingNthOrderBrentSolver(accuracy, 5);
510             double crossingLine = solver.solve(maxEval, new UnivariateFunction() {
511                 /** {@inheritDoc} */
512                 @Override
513                 public double value(final double x) throws RuggedExceptionWrapper {
514                     try {
515                         final AbsoluteDate date = sensor.getDate(x);
516                         final FieldVector3D<DerivativeStructure> targetDirection =
517                                 evaluateLine(x, targetPV, scToBody.getBodyToInertial(date), scToBody.getScToInertial(date));
518                         final DerivativeStructure beta = FieldVector3D.angle(targetDirection, meanPlaneNormal);
519                         return 0.5 * FastMath.PI - beta.getValue();
520                     } catch (RuggedException re) {
521                         throw new RuggedExceptionWrapper(re);
522                     }
523                 }
524             }, minLine, maxLine, startValue);
525 
526             final AbsoluteDate date = sensor.getDate(crossingLine);
527             final FieldVector3D<DerivativeStructure> targetDirection =
528                     evaluateLine(crossingLine, targetPV, scToBody.getBodyToInertial(date), scToBody.getScToInertial(date));
529             return new CrossingResult(sensor.getDate(crossingLine), crossingLine, targetPV.getPosition(), targetDirection);
530 
531         } catch (NoBracketingException nbe) {
532             return null;
533         } catch (RuggedExceptionWrapper rew) {
534             throw rew.getException();
535         }
536     }
537 
538     /** Evaluate geometry for a given line number.
539      * @param lineNumber current line number
540      * @param targetPV target ground point
541      * @param bodyToInert transform from observed body to inertial frame, for current line
542      * @param scToInert transform from inertial frame to spacecraft frame, for current line
543      * @return target direction in spacecraft frame, with its first derivative
544      * with respect to line number
545      */
546     private FieldVector3D<DerivativeStructure> evaluateLine(final double lineNumber, final PVCoordinates targetPV,
547                                                             final Transform bodyToInert, final Transform scToInert) {
548 
549         // compute the transform between spacecraft and observed body
550         final PVCoordinates refInert =
551                 scToInert.transformPVCoordinates(new PVCoordinates(sensor.getPosition(), Vector3D.ZERO));
552 
553         final PVCoordinates targetInert;
554         if (lightTimeCorrection) {
555             // apply light time correction
556             final Vector3D iT     = bodyToInert.transformPosition(targetPV.getPosition());
557             final double   deltaT = refInert.getPosition().distance(iT) / Constants.SPEED_OF_LIGHT;
558             targetInert           = bodyToInert.shiftedBy(-deltaT).transformPVCoordinates(targetPV);
559         } else {
560             // don't apply light time correction
561             targetInert = bodyToInert.transformPVCoordinates(targetPV);
562         }
563 
564         final PVCoordinates lInert    = new PVCoordinates(refInert, targetInert);
565         final Transform     inertToSc = scToInert.getInverse();
566         final Vector3D obsLInert;
567         final Vector3D obsLInertDot;
568         if (aberrationOfLightCorrection) {
569 
570             // apply aberration of light correction
571             // as the spacecraft velocity is small with respect to speed of light,
572             // we use classical velocity addition and not relativistic velocity addition
573             // we have: c * lInert + vsat = k * obsLInert
574             final PVCoordinates spacecraftPV = scToInert.transformPVCoordinates(PVCoordinates.ZERO);
575             final Vector3D  l        = lInert.getPosition().normalize();
576             final Vector3D  lDot     = normalizedDot(lInert.getPosition(), lInert.getVelocity());
577             final Vector3D  kObs     = new Vector3D(Constants.SPEED_OF_LIGHT, l, +1.0, spacecraftPV.getVelocity());
578             obsLInert = kObs.normalize();
579 
580             // the following derivative is computed under the assumption the spacecraft velocity
581             // is constant in inertial frame ... It is obviously not true, but as this velocity
582             // is very small with respect to speed of light, the error is expected to remain small
583             obsLInertDot = normalizedDot(kObs, new Vector3D(Constants.SPEED_OF_LIGHT, lDot));
584 
585         } else {
586 
587             // don't apply aberration of light correction
588             obsLInert    = lInert.getPosition().normalize();
589             obsLInertDot = normalizedDot(lInert.getPosition(), lInert.getVelocity());
590 
591         }
592         final Vector3D dir    = inertToSc.transformVector(obsLInert);
593         final Vector3D dirDot = new Vector3D(+1.0, inertToSc.transformVector(obsLInertDot),
594                                              -1.0, Vector3D.crossProduct(inertToSc.getRotationRate(), dir));
595 
596         // combine vector value and derivative
597         final double rate = sensor.getRate(lineNumber);
598         return new FieldVector3D<DerivativeStructure>(new DerivativeStructure(1, 1, dir.getX(), dirDot.getX() / rate),
599                                                       new DerivativeStructure(1, 1, dir.getY(), dirDot.getY() / rate),
600                                                       new DerivativeStructure(1, 1, dir.getZ(), dirDot.getZ() / rate));
601 
602     }
603 
604     /** Compute the derivative of normalized vector.
605      * @param u base vector
606      * @param uDot derivative of the base vector
607      * @return vDot, where v = u / ||u||
608      */
609     private Vector3D normalizedDot(final Vector3D u, final Vector3D uDot) {
610         final double n = u.getNorm();
611         return new Vector3D(1.0 / n, uDot, -Vector3D.dotProduct(u, uDot) / (n * n * n), u);
612     }
613 
614 }