EllipticalFieldOfView.java

  1. /* Copyright 2002-2024 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.geometry.fov;

  18. import org.hipparchus.CalculusFieldElement;
  19. import org.hipparchus.analysis.differentiation.DSFactory;
  20. import org.hipparchus.analysis.differentiation.DerivativeStructure;
  21. import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
  22. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  23. import org.hipparchus.util.FastMath;
  24. import org.hipparchus.util.SinCos;
  25. import org.orekit.propagation.events.VisibilityTrigger;

  26. /** Class representing a spacecraft sensor Field Of View with elliptical shape.
  27.  * <p>
  28.  * Without loss of generality, one can assume that with a suitable rotation
  29.  * the ellipse center is along the Z<sub>ell</sub> axis and the ellipse principal axes
  30.  * are along the X<sub>ell</sub> and Y<sub>ell</sub> axes. The first defining
  31.  * elements for an ellipse are these canonical axes. This class allows specifying
  32.  * them by giving directly the Z<sub>ell</sub> axis as the {@code center} of
  33.  * the ellipse, and giving a {@code primaryMeridian} vector in the (+X<sub>ell</sub>,
  34.  * Z<sub>ell</sub>) half-plane. It is allowed to have {@code primaryMeridian} not
  35.  * orthogonal to {@code center} as orthogonality will be fixed internally (i.e
  36.  * {@code primaryMeridian} may be different from X<sub>ell</sub>).
  37.  * </p>
  38.  * <p>
  39.  * We can define angular coordinates \((\alpha, \beta)\) as dihedra angles around the
  40.  * +Y<sub>ell</sub> and -X<sub>ell</sub> axes respectively to specify points on the
  41.  * unit sphere. The corresponding Cartesian coordinates will be
  42.  * \[P_{\alpha,\beta}\left(\begin{gather*}
  43.  *   \frac{\sin\alpha\cos\beta}{\sqrt{1-\sin^2\alpha\sin^2\beta}}\\
  44.  *   \frac{\cos\alpha\sin\beta}{\sqrt{1-\sin^2\alpha\sin^2\beta}}\\
  45.  *   \frac{\cos\alpha\cos\beta}{\sqrt{1-\sin^2\alpha\sin^2\beta}}
  46.  * \end{gather*}\right)\]
  47.  * which shows that angle \(\beta=0\) corresponds to the (X<sub>ell</sub>, Z<sub>ell</sub>)
  48.  * plane and that angle \(\alpha=0\) corresponds to the (Y<sub>ell</sub>, Z<sub>ell</sub>)
  49.  * plane. Note that at least one of the angles must be different from \(\pm\frac{\pi}{2}\),
  50.  * which means that the expression above is singular for points in the (X<sub>ell</sub>,
  51.  * Y<sub>ell</sub>) plane.
  52.  * </p>
  53.  * <p>
  54.  * The size of the ellipse is defined by its half aperture angles \(\lambda\) along the
  55.  * X<sub>ell</sub> axis and \(\mu\) along the Y<sub>ell</sub> axis.
  56.  * For points belonging to the ellipse, we always have \(-\lambda \le \alpha \le +\lambda\)
  57.  * and \(-\mu \le \beta \le +\mu\), equalities being reached at the end of principal axes.
  58.  * An ellipse defined on the sphere is not a planar ellipse because the four endpoints
  59.  * \((\alpha=\pm\lambda, \beta=0)\) and \((\alpha=0, \beta=\pm\mu)\) are not coplanar
  60.  * when \(\lambda\neq\mu\).
  61.  * </p>
  62.  * <p>
  63.  * We define an ellipse on the sphere as the locus of points \(P\) such that the sum of
  64.  * their angular distance to two foci \(F_+\) and \(F_-\) is constant, all points being on
  65.  * the sphere. The relationship between the foci and the two half aperture angles \(\lambda\)
  66.  * and \(\mu\) is:
  67.  * \[\lambda \ge \mu \Rightarrow F_\pm\left(\begin{gather*}
  68.  *   \pm\sin\delta\\
  69.  *   0\\
  70.  *   \cos\delta
  71.  * \end{gather*}\right)
  72.  * \quad\text{with}\quad
  73.  * \cos\delta = \frac{\cos\lambda}{\cos\mu}\]
  74.  * </p>
  75.  * <p>
  76.  * and
  77.  * \[\mu \ge \lambda \Rightarrow F_\pm\left(\begin{gather*}
  78.  *   0\\
  79.  *   \pm\sin\delta\\
  80.  *   \cos\delta
  81.  * \end{gather*}\right)
  82.  * \quad\text{with}\quad
  83.  * \cos\delta = \frac{\cos\mu}{\cos\lambda}\]
  84.  * </p>
  85.  * <p>
  86.  * It can be shown that the previous definition is equivalent to define first a regular
  87.  * planar ellipse drawn on a plane \(z = z_0\) (\(z_0\) being an arbitrary strictly positive
  88.  * number, \(z_0=1\) being the simplest choice) with semi major axis \(a=z_0\tan\lambda\)
  89.  * and semi minor axis \(b=z_0\tan\mu\) and then to project it onto the sphere using a
  90.  * central projection:
  91.  * \[\left\{\begin{align*}
  92.  * \left(\frac{x}{z_0\tan\lambda}\right)^2 + \left(\frac{y}{z_0\tan\mu}\right)^2 &amp;= \left(\frac{z}{z_0}\right)^2\\
  93.  * x^2 + y^2 + z^2 &amp;= 1
  94.  * \end{align*}\right.\]
  95.  * </p>
  96.  * <p>
  97.  * Simplifying first equation by \(z_0\) and eliminating \(z^2\) in it using the second equation gives:
  98.  * \[\left\{\begin{align*}
  99.  * \left(\frac{x}{\sin\lambda}\right)^2 + \left(\frac{y}{\sin\mu}\right)^2 &amp;= 1\\
  100.  * x^2 + y^2 + z^2 &amp;= 1
  101.  * \end{align*}\right.\]
  102.  * which shows that the previous definition is also equivalent to define first a
  103.  * dimensionless planar ellipse on the \((x, y)\) plane and to project it onto the sphere
  104.  * using a projection along \(z\).
  105.  * </p>
  106.  * <p>
  107.  * Note however that despite the ellipse on the sphere can be computed as a projection
  108.  * of an ellipse on the \((x, y)\) plane, the foci of one ellipse are not the projection of the
  109.  * foci of the other ellipse. The foci on the plane are closer to each other by a factor
  110.  * \(\cos\mu\) than the projection of the foci \(F_+\) and \(F_-\)).
  111.  * </p>
  112.  * @author Luc Maisonobe
  113.  * @since 10.1
  114.  */
  115. public class EllipticalFieldOfView extends SmoothFieldOfView {

  116.     /** Factory for derivatives. */
  117.     private static final DSFactory FACTORY = new DSFactory(1, 3);

  118.     /** FOV half aperture angle for spreading along X (i.e. rotation around +Y). */
  119.     private final double halfApertureAlongX;

  120.     /** FOV half aperture angle for spreading along Y (i.e. rotation around -X). */
  121.     private final double halfApertureAlongY;

  122.     /** tan(halfApertureAlongX). */
  123.     private final double   tanX;

  124.     /** tan(halfApertureAlongX). */
  125.     private final double   tanY;

  126.     /** Unit vector along major axis. */
  127.     private final Vector3D u;

  128.     /** First focus. */
  129.     private final Vector3D focus1;

  130.     /** Second focus. */
  131.     private final Vector3D focus2;

  132.     /** Cross product of foci. */
  133.     private final Vector3D crossF1F2;

  134.     /** Dot product of foci. */
  135.     private final double dotF1F2;

  136.     /** Half angle between foci. */
  137.     private final double gamma;

  138.     /** Scaling factor for normalizing ellipse points. */
  139.     private final double d;

  140.     /** Angular semi major axis. */
  141.     private double a;

  142.     /** Build a new instance.
  143.      * <p>
  144.      * Using a suitable rotation, an elliptical Field Of View can be oriented such
  145.      * that the ellipse center is along the Z<sub>ell</sub> axis, one of its principal
  146.      * axes is in the (X<sub>ell</sub>, Z<sub>ell</sub>) plane and the other principal
  147.      * axis is in the (Y<sub>ell</sub>, Z<sub>ell</sub>) plane. Beware that the ellipse
  148.      * principal axis that spreads along the Y<sub>ell</sub> direction corresponds to a
  149.      * rotation around -X<sub>ell</sub> axis and that the ellipse principal axis that
  150.      * spreads along the X<sub>ell</sub> direction corresponds to a rotation around
  151.      * +Y<sub>ell</sub> axis. The naming convention used here is that the angles are
  152.      * named after the spreading axis.
  153.      * </p>
  154.      * @param center direction of the FOV center (i.e. Z<sub>ell</sub>),
  155.      * in spacecraft frame
  156.      * @param primaryMeridian vector defining the (+X<sub>ell</sub>, Z<sub>ell</sub>)
  157.      * half-plane (it is allowed to have {@code primaryMeridian} not orthogonal to
  158.      * {@code center} as orthogonality will be fixed internally)
  159.      * @param halfApertureAlongX FOV half aperture angle defining the ellipse spreading
  160.      * along X<sub>ell</sub> (i.e. it corresponds to a rotation around +Y<sub>ell</sub>)
  161.      * @param halfApertureAlongY FOV half aperture angle defining the ellipse spreading
  162.      * along Y<sub>ell</sub> (i.e. it corresponds to a rotation around -X<sub>ell</sub>)
  163.      * @param margin angular margin to apply to the zone (if positive,
  164.      * the Field Of View will consider points slightly outside of the
  165.      * zone are still visible)
  166.      */
  167.     public EllipticalFieldOfView(final Vector3D center, final Vector3D primaryMeridian,
  168.                                  final double halfApertureAlongX, final double halfApertureAlongY,
  169.                                  final double margin) {

  170.         super(center, primaryMeridian, margin);

  171.         final Vector3D v;
  172.         final double b;
  173.         if (halfApertureAlongX >= halfApertureAlongY) {
  174.             u = getX();
  175.             v = getY();
  176.             a = halfApertureAlongX;
  177.             b = halfApertureAlongY;
  178.         } else {
  179.             u = getY();
  180.             v = getX().negate();
  181.             a = halfApertureAlongY;
  182.             b = halfApertureAlongX;
  183.         }

  184.         final double cos = FastMath.cos(a) / FastMath.cos(b);
  185.         final double sin = FastMath.sqrt(1 - cos * cos);

  186.         this.halfApertureAlongX = halfApertureAlongX;
  187.         this.halfApertureAlongY = halfApertureAlongY;
  188.         this.tanX               = FastMath.tan(halfApertureAlongX);
  189.         this.tanY               = FastMath.tan(halfApertureAlongY);
  190.         this.focus1             = new Vector3D(+sin, u, cos, getZ());
  191.         this.focus2             = new Vector3D(-sin, u, cos, getZ());
  192.         this.crossF1F2          = new Vector3D(-2 * sin * cos, v);
  193.         this.dotF1F2            = 2 * cos * cos - 1;
  194.         this.gamma              = FastMath.acos(cos);
  195.         this.d                  = 1.0 / (1 - dotF1F2 * dotF1F2);

  196.     }

  197.     /** get the FOV half aperture angle for spreading along X<sub>ell</sub> (i.e. rotation around +Y<sub>ell</sub>).
  198.      * @return FOV half aperture angle for spreading along X<sub>ell</sub> (i.e. rotation around +Y<sub>ell</sub>
  199.      */
  200.     public double getHalfApertureAlongX() {
  201.         return halfApertureAlongX;
  202.     }

  203.     /** get the FOV half aperture angle for spreading along Y<sub>ell</sub> (i.e. rotation around -X<sub>ell</sub>).
  204.      * @return FOV half aperture angle for spreading along Y<sub>ell</sub> (i.e. rotation around -X<sub>ell</sub>)
  205.      */
  206.     public double getHalfApertureAlongY() {
  207.         return halfApertureAlongY;
  208.     }

  209.     /** Get first focus in spacecraft frame.
  210.      * @return first focus in spacecraft frame
  211.      */
  212.     public Vector3D getFocus1() {
  213.         return focus1;
  214.     }

  215.     /** Get second focus in spacecraft frame.
  216.      * @return second focus in spacecraft frame
  217.      */
  218.     public Vector3D getFocus2() {
  219.         return focus2;
  220.     }

  221.     /** {@inheritDoc} */
  222.     @Override
  223.     public double offsetFromBoundary(final Vector3D lineOfSight, final double angularRadius,
  224.                                      final VisibilityTrigger trigger) {

  225.         final double  margin          = getMargin();
  226.         final double  correctedRadius = trigger.radiusCorrection(angularRadius);
  227.         final double  deadBand        = margin + angularRadius;

  228.         // for faster computation, we start using only the surrounding cap, to filter out
  229.         // far away points (which correspond to most of the points if the Field Of View is small)
  230.         final double crudeDistance = Vector3D.angle(getZ(), lineOfSight) - a;
  231.         if (crudeDistance > deadBand + 0.01) {
  232.             // we know we are strictly outside of the zone,
  233.             // use the crude distance to compute the (positive) return value
  234.             return crudeDistance + correctedRadius - margin;
  235.         }

  236.         // we are close, we need to compute carefully the exact offset;
  237.         // we project the point to the closest zone boundary
  238.         final double   d1      = Vector3D.angle(lineOfSight, focus1);
  239.         final double   d2      = Vector3D.angle(lineOfSight, focus2);
  240.         final Vector3D closest = projectToBoundary(lineOfSight, d1, d2);

  241.         // compute raw offset as an accurate signed angle
  242.         final double rawOffset = FastMath.copySign(Vector3D.angle(lineOfSight, closest),
  243.                                                    d1 + d2 - 2 * a);

  244.         return rawOffset + correctedRadius - getMargin();

  245.     }

  246.     /** {@inheritDoc} */
  247.     @Override
  248.     public Vector3D projectToBoundary(final Vector3D lineOfSight) {
  249.         final double d1 = Vector3D.angle(lineOfSight, focus1);
  250.         final double d2 = Vector3D.angle(lineOfSight, focus2);
  251.         return projectToBoundary(lineOfSight, d1, d2);
  252.     }

  253.     /** Find the direction on Field Of View Boundary closest to a line of sight.
  254.      * @param lineOfSight line of sight from the center of the Field Of View support
  255.      * unit sphere to the target in spacecraft frame
  256.      * @param d1 distance to first focus
  257.      * @param d2 distance to second focus
  258.      * @return direction on Field Of View Boundary closest to a line of sight
  259.      */
  260.     private Vector3D projectToBoundary(final Vector3D lineOfSight, final double d1, final double d2) {

  261.         final Vector3D los  = lineOfSight.normalize();
  262.         final double   side = Vector3D.dotProduct(los, crossF1F2);
  263.         if (FastMath.abs(side) < 1.0e-12) {
  264.             // the line of sight is almost along the major axis
  265.             return directionAt(Vector3D.dotProduct(los, u) > 0 ? 0.0 : FastMath.PI);
  266.         }

  267.         // find an initial point on ellipse, that approximates closest point
  268.         final double offset0 = 0.5 * (d1 - d2);
  269.         double minOffset = -gamma;
  270.         double maxOffset = +gamma;

  271.         // find closest ellipse point
  272.         DerivativeStructure offset = FACTORY.variable(0, offset0);
  273.         for (int i = 0; i < 100; i++) { // this loop usually converges in 1-4 iterations

  274.             // distance function we want to minimize
  275.             final FieldVector3D<DerivativeStructure> pn = directionAt(offset.add(a), offset.subtract(a).negate(), side);
  276.             final DerivativeStructure                yn = FieldVector3D.angle(pn, los);
  277.             if (yn.getValue() < 1.0e-12) {
  278.                 // the query point is almost on the ellipse boundary
  279.                 break;
  280.             }

  281.             // Halley's iteration on the derivative (since we want the minimum of the distance function)
  282.             final double f0 = yn.getPartialDerivative(1);
  283.             final double f1 = yn.getPartialDerivative(2);
  284.             final double f2 = yn.getPartialDerivative(3);
  285.             double dx = -2 * f0 * f1 / (2 * f1 * f1 - f0 * f2);
  286.             if (dx * f0 > 0) {
  287.                 // the Halley's iteration is going towards maximum, not minimum
  288.                 // try to go past inflection point
  289.                 dx = -1.5 * f2 / f1;
  290.             }

  291.             // manage bounds
  292.             if (dx < 0) {
  293.                 maxOffset = offset.getValue();
  294.                 if (offset.getValue() + dx <= minOffset) {
  295.                     // we overshoot limit, fall back to bisection
  296.                     dx = 0.5 * (minOffset - offset.getValue());
  297.                 }
  298.             } else {
  299.                 minOffset = offset.getValue();
  300.                 if (offset.getValue() + dx >= maxOffset) {
  301.                     // we overshoot limit, fall back to bisection
  302.                     dx = 0.5 * (maxOffset - offset.getValue());
  303.                 }
  304.             }

  305.             // apply offset change
  306.             offset = offset.add(dx);

  307.             // check convergence
  308.             if (FastMath.abs(dx) < 1.0e-12) {
  309.                 break;
  310.             }

  311.         }

  312.         return directionAt(a + offset.getReal(), a - offset.getReal(), side);

  313.     }

  314.     /** {@inheritDoc} */
  315.     @Override
  316.     protected Vector3D directionAt(final double angle) {
  317.         final SinCos   sce  = FastMath.sinCos(angle);
  318.         final Vector3D dEll = new Vector3D(tanX * sce.cos(), tanY * sce.sin(), 1.0).normalize();
  319.         return new Vector3D(dEll.getX(), getX(), dEll.getY(), getY(), dEll.getZ(), getZ());
  320.     }

  321.     /** Get a direction from distances to foci.
  322.      * <p>
  323.      * if {@code d1} + {@code d2} = 2 max({@link #getHalfApertureAlongX()}, {@link #getHalfApertureAlongY()}),
  324.      * then the point is on the ellipse boundary
  325.      * </p>
  326.      * @param d1 distance to focus 1
  327.      * @param d2 distance to focus 2
  328.      * @param sign sign of the ellipse point with respect to F1 ^ F2
  329.      * @return direction
  330.      */
  331.     private Vector3D directionAt(final double d1, final double d2, final double sign) {
  332.         final double cos1 = FastMath.cos(d1);
  333.         final double cos2 = FastMath.cos(d2);
  334.         final double a1   = (cos1 - cos2 * dotF1F2) * d;
  335.         final double a2   = (cos2 - cos1 * dotF1F2) * d;
  336.         final double ac   = FastMath.sqrt((1 - (a1 * a1 + 2 * a1 * a2 * dotF1F2 + a2 * a2)) * d);
  337.         return new Vector3D(a1, focus1, a2, focus2, FastMath.copySign(ac, sign), crossF1F2);
  338.     }

  339.     /** Get a direction from distances to foci.
  340.      * <p>
  341.      * if {@code d1} + {@code d2} = 2 max({@link #getHalfApertureAlongX()}, {@link #getHalfApertureAlongY()}),
  342.      * then the point is on the ellipse boundary
  343.      * </p>
  344.      * @param d1 distance to focus 1
  345.      * @param d2 distance to focus 2
  346.      * @param sign sign of the ellipse point with respect to F1 ^ F2
  347.      * @param <T> type of the field element
  348.      * @return direction
  349.      */
  350.     private <T extends CalculusFieldElement<T>> FieldVector3D<T> directionAt(final T d1, final T d2, final double sign) {
  351.         final T cos1 = FastMath.cos(d1);
  352.         final T cos2 = FastMath.cos(d2);
  353.         final T a1   = cos1.subtract(cos2.multiply(dotF1F2)).multiply(d);
  354.         final T a2   = cos2.subtract(cos1.multiply(dotF1F2)).multiply(d);
  355.         final T ac   = FastMath.sqrt(a1.multiply(a1.add(a2.multiply(2 * dotF1F2))).add(a2.square()).negate().add(1).multiply(d));
  356.         return new FieldVector3D<>(a1, focus1, a2, focus2, FastMath.copySign(ac, sign), crossF1F2);
  357.     }

  358. }