1 /* Copyright 2002-2021 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 19 import org.hipparchus.CalculusFieldElement; 20 import org.hipparchus.analysis.differentiation.DSFactory; 21 import org.hipparchus.analysis.differentiation.DerivativeStructure; 22 import org.hipparchus.geometry.euclidean.threed.FieldVector3D; 23 import org.hipparchus.geometry.euclidean.threed.Vector3D; 24 import org.hipparchus.util.FastMath; 25 import org.hipparchus.util.SinCos; 26 import org.orekit.propagation.events.VisibilityTrigger; 27 28 /** Class representing a spacecraft sensor Field Of View with elliptical shape. 29 * <p> 30 * Without loss of generality, one can assume that with a suitable rotation 31 * the ellipse center is along the Z<sub>ell</sub> axis and the ellipse principal axes 32 * are along the X<sub>ell</sub> and Y<sub>ell</sub> axes. The first defining 33 * elements for an ellipse are these canonical axes. This class allows specifying 34 * them by giving directly the Z<sub>ell</sub> axis as the {@code center} of 35 * the ellipse, and giving a {@code primaryMeridian} vector in the (+X<sub>ell</sub>, 36 * Z<sub>ell</sub>) half-plane. It is allowed to have {@code primaryMeridian} not 37 * orthogonal to {@code center} as orthogonality will be fixed internally (i.e 38 * {@code primaryMeridian} may be different from X<sub>ell</sub>). 39 * </p> 40 * <p> 41 * We can define angular coordinates \((\alpha, \beta)\) as dihedra angles around the 42 * +Y<sub>ell</sub> and -X<sub>ell</sub> axes respectively to specify points on the 43 * unit sphere. The corresponding Cartesian coordinates will be 44 * \[P_{\alpha,\beta}\left(\begin{gather*} 45 * \frac{\sin\alpha\cos\beta}{\sqrt{1-\sin^2\alpha\sin^2\beta}}\\ 46 * \frac{\cos\alpha\sin\beta}{\sqrt{1-\sin^2\alpha\sin^2\beta}}\\ 47 * \frac{\cos\alpha\cos\beta}{\sqrt{1-\sin^2\alpha\sin^2\beta}} 48 * \end{gather*}\right)\] 49 * which shows that angle \(\beta=0\) corresponds to the (X<sub>ell</sub>, Z<sub>ell</sub>) 50 * plane and that angle \(\alpha=0\) corresponds to the (Y<sub>ell</sub>, Z<sub>ell</sub>) 51 * plane. Note that at least one of the angles must be different from \(\pm\frac{\pi}{2}\), 52 * which means that the expression above is singular for points in the (X<sub>ell</sub>, 53 * Y<sub>ell</sub>) plane. 54 * </p> 55 * <p> 56 * The size of the ellipse is defined by its half aperture angles \(\lambda\) along the 57 * X<sub>ell</sub> axis and \(\mu\) along the Y<sub>ell</sub> axis. 58 * For points belonging to the ellipse, we always have \(-\lambda \le \alpha \le +\lambda\) 59 * and \(-\mu \le \beta \le +\mu\), equalities being reached at the end of principal axes. 60 * An ellipse defined on the sphere is not a planar ellipse because the four endpoints 61 * \((\alpha=\pm\lambda, \beta=0)\) and \((\alpha=0, \beta=\pm\mu)\) are not coplanar 62 * when \(\lambda\neq\mu\). 63 * </p> 64 * <p> 65 * We define an ellipse on the sphere as the locus of points \(P\) such that the sum of 66 * their angular distance to two foci \(F_+\) and \(F_-\) is constant, all points being on 67 * the sphere. The relationship between the foci and the two half aperture angles \(\lambda\) 68 * and \(\mu\) is: 69 * \[\lambda \ge \mu \Rightarrow F_\pm\left(\begin{gather*} 70 * \pm\sin\delta\\ 71 * 0\\ 72 * \cos\delta 73 * \end{gather*}\right) 74 * \quad\text{with}\quad 75 * \cos\delta = \frac{\cos\lambda}{\cos\mu}\] 76 * </p> 77 * <p> 78 * and 79 * \[\mu \ge \lambda \Rightarrow F_\pm\left(\begin{gather*} 80 * 0\\ 81 * \pm\sin\delta\\ 82 * \cos\delta 83 * \end{gather*}\right) 84 * \quad\text{with}\quad 85 * \cos\delta = \frac{\cos\mu}{\cos\lambda}\] 86 * </p> 87 * <p> 88 * It can be shown that the previous definition is equivalent to define first a regular 89 * planar ellipse drawn on a plane \(z = z_0\) (\(z_0\) being an arbitrary strictly positive 90 * number, \(z_0=1\) being the simplest choice) with semi major axis \(a=z_0\tan\lambda\) 91 * and semi minor axis \(b=z_0\tan\mu\) and then to project it onto the sphere using a 92 * central projection: 93 * \[\left\{\begin{align*} 94 * \left(\frac{x}{z_0\tan\lambda}\right)^2 + \left(\frac{y}{z_0\tan\mu}\right)^2 &= \left(\frac{z}{z_0}\right)^2\\ 95 * x^2 + y^2 + z^2 &= 1 96 * \end{align*}\right.\] 97 * </p> 98 * <p> 99 * Simplifying first equation by \(z_0\) and eliminating \(z^2\) in it using the second equation gives: 100 * \[\left\{\begin{align*} 101 * \left(\frac{x}{\sin\lambda}\right)^2 + \left(\frac{y}{\sin\mu}\right)^2 &= 1\\ 102 * x^2 + y^2 + z^2 &= 1 103 * \end{align*}\right.\] 104 * which shows that the previous definition is also equivalent to define first a 105 * dimensionless planar ellipse on the \((x, y)\) plane and to project it onto the sphere 106 * using a projection along \(z\). 107 * </p> 108 * <p> 109 * Note however that despite the ellipse on the sphere can be computed as a projection 110 * of an ellipse on the \((x, y)\) plane, the foci of one ellipse are not the projection of the 111 * foci of the other ellipse. The foci on the plane are closer to each other by a factor 112 * \(\cos\mu\) than the projection of the foci \(F_+\) and \(F_-\)). 113 * </p> 114 * @author Luc Maisonobe 115 * @since 10.1 116 */ 117 public class EllipticalFieldOfView extends SmoothFieldOfView { 118 119 /** Factory for derivatives. */ 120 private static final DSFactory FACTORY = new DSFactory(1, 3); 121 122 /** FOV half aperture angle for spreading along X (i.e. rotation around +Y). */ 123 private final double halfApertureAlongX; 124 125 /** FOV half aperture angle for spreading along Y (i.e. rotation around -X). */ 126 private final double halfApertureAlongY; 127 128 /** tan(halfApertureAlongX). */ 129 private final double tanX; 130 131 /** tan(halfApertureAlongX). */ 132 private final double tanY; 133 134 /** Unit vector along major axis. */ 135 private final Vector3D u; 136 137 /** First focus. */ 138 private final Vector3D focus1; 139 140 /** Second focus. */ 141 private final Vector3D focus2; 142 143 /** Cross product of foci. */ 144 private final Vector3D crossF1F2; 145 146 /** Dot product of foci. */ 147 private final double dotF1F2; 148 149 /** Half angle between foci. */ 150 private final double gamma; 151 152 /** Scaling factor for normalizing ellipse points. */ 153 private final double d; 154 155 /** Angular semi major axis. */ 156 private double a; 157 158 /** Build a new instance. 159 * <p> 160 * Using a suitable rotation, an elliptical Field Of View can be oriented such 161 * that the ellipse center is along the Z<sub>ell</sub> axis, one of its principal 162 * axes is in the (X<sub>ell</sub>, Z<sub>ell</sub>) plane and the other principal 163 * axis is in the (Y<sub>ell</sub>, Z<sub>ell</sub>) plane. Beware that the ellipse 164 * principal axis that spreads along the Y<sub>ell</sub> direction corresponds to a 165 * rotation around -X<sub>ell</sub> axis and that the ellipse principal axis that 166 * spreads along the X<sub>ell</sub> direction corresponds to a rotation around 167 * +Y<sub>ell</sub> axis. The naming convention used here is that the angles are 168 * named after the spreading axis. 169 * </p> 170 * @param center direction of the FOV center (i.e. Z<sub>ell</sub>), 171 * in spacecraft frame 172 * @param primaryMeridian vector defining the (+X<sub>ell</sub>, Z<sub>ell</sub>) 173 * half-plane (it is allowed to have {@code primaryMeridian} not orthogonal to 174 * {@code center} as orthogonality will be fixed internally) 175 * @param halfApertureAlongX FOV half aperture angle defining the ellipse spreading 176 * along X<sub>ell</sub> (i.e. it corresponds to a rotation around +Y<sub>ell</sub>) 177 * @param halfApertureAlongY FOV half aperture angle defining the ellipse spreading 178 * along Y<sub>ell</sub> (i.e. it corresponds to a rotation around -X<sub>ell</sub>) 179 * @param margin angular margin to apply to the zone (if positive, 180 * the Field Of View will consider points slightly outside of the 181 * zone are still visible) 182 */ 183 public EllipticalFieldOfView(final Vector3D center, final Vector3D primaryMeridian, 184 final double halfApertureAlongX, final double halfApertureAlongY, 185 final double margin) { 186 187 super(center, primaryMeridian, margin); 188 189 final Vector3D v; 190 final double b; 191 if (halfApertureAlongX >= halfApertureAlongY) { 192 u = getX(); 193 v = getY(); 194 a = halfApertureAlongX; 195 b = halfApertureAlongY; 196 } else { 197 u = getY(); 198 v = getX().negate(); 199 a = halfApertureAlongY; 200 b = halfApertureAlongX; 201 } 202 203 final double cos = FastMath.cos(a) / FastMath.cos(b); 204 final double sin = FastMath.sqrt(1 - cos * cos); 205 206 this.halfApertureAlongX = halfApertureAlongX; 207 this.halfApertureAlongY = halfApertureAlongY; 208 this.tanX = FastMath.tan(halfApertureAlongX); 209 this.tanY = FastMath.tan(halfApertureAlongY); 210 this.focus1 = new Vector3D(+sin, u, cos, getZ()); 211 this.focus2 = new Vector3D(-sin, u, cos, getZ()); 212 this.crossF1F2 = new Vector3D(-2 * sin * cos, v); 213 this.dotF1F2 = 2 * cos * cos - 1; 214 this.gamma = FastMath.acos(cos); 215 this.d = 1.0 / (1 - dotF1F2 * dotF1F2); 216 217 } 218 219 /** get the FOV half aperture angle for spreading along X<sub>ell</sub> (i.e. rotation around +Y<sub>ell</sub>). 220 * @return FOV half aperture angle for spreading along X<sub>ell</sub> (i.e. rotation around +Y<sub>ell</sub> 221 */ 222 public double getHalfApertureAlongX() { 223 return halfApertureAlongX; 224 } 225 226 /** get the FOV half aperture angle for spreading along Y<sub>ell</sub> (i.e. rotation around -X<sub>ell</sub>). 227 * @return FOV half aperture angle for spreading along Y<sub>ell</sub> (i.e. rotation around -X<sub>ell</sub>) 228 */ 229 public double getHalfApertureAlongY() { 230 return halfApertureAlongY; 231 } 232 233 /** Get first focus in spacecraft frame. 234 * @return first focus in spacecraft frame 235 */ 236 public Vector3D getFocus1() { 237 return focus1; 238 } 239 240 /** Get second focus in spacecraft frame. 241 * @return second focus in spacecraft frame 242 */ 243 public Vector3D getFocus2() { 244 return focus2; 245 } 246 247 /** {@inheritDoc} */ 248 @Override 249 public double offsetFromBoundary(final Vector3D lineOfSight, final double angularRadius, 250 final VisibilityTrigger trigger) { 251 252 final double margin = getMargin(); 253 final double correctedRadius = trigger.radiusCorrection(angularRadius); 254 final double deadBand = margin + angularRadius; 255 256 // for faster computation, we start using only the surrounding cap, to filter out 257 // far away points (which correspond to most of the points if the Field Of View is small) 258 final double crudeDistance = Vector3D.angle(getZ(), lineOfSight) - a; 259 if (crudeDistance > deadBand + 0.01) { 260 // we know we are strictly outside of the zone, 261 // use the crude distance to compute the (positive) return value 262 return crudeDistance + correctedRadius - margin; 263 } 264 265 // we are close, we need to compute carefully the exact offset; 266 // we project the point to the closest zone boundary 267 final double d1 = Vector3D.angle(lineOfSight, focus1); 268 final double d2 = Vector3D.angle(lineOfSight, focus2); 269 final Vector3D closest = projectToBoundary(lineOfSight, d1, d2); 270 271 // compute raw offset as an accurate signed angle 272 final double rawOffset = FastMath.copySign(Vector3D.angle(lineOfSight, closest), 273 d1 + d2 - 2 * a); 274 275 return rawOffset + correctedRadius - getMargin(); 276 277 } 278 279 /** {@inheritDoc} */ 280 @Override 281 public Vector3D projectToBoundary(final Vector3D lineOfSight) { 282 final double d1 = Vector3D.angle(lineOfSight, focus1); 283 final double d2 = Vector3D.angle(lineOfSight, focus2); 284 return projectToBoundary(lineOfSight, d1, d2); 285 } 286 287 /** Find the direction on Field Of View Boundary closest to a line of sight. 288 * @param lineOfSight line of sight from the center of the Field Of View support 289 * unit sphere to the target in spacecraft frame 290 * @param d1 distance to first focus 291 * @param d2 distance to second focus 292 * @return direction on Field Of View Boundary closest to a line of sight 293 */ 294 private Vector3D projectToBoundary(final Vector3D lineOfSight, final double d1, final double d2) { 295 296 final Vector3D los = lineOfSight.normalize(); 297 final double side = Vector3D.dotProduct(los, crossF1F2); 298 if (FastMath.abs(side) < 1.0e-12) { 299 // the line of sight is almost along the major axis 300 return directionAt(Vector3D.dotProduct(los, u) > 0 ? 0.0 : FastMath.PI); 301 } 302 303 // find an initial point on ellipse, that approximates closest point 304 final double offset0 = 0.5 * (d1 - d2); 305 double minOffset = -gamma; 306 double maxOffset = +gamma; 307 308 // find closest ellipse point 309 DerivativeStructure offset = FACTORY.variable(0, offset0); 310 for (int i = 0; i < 100; i++) { // this loop usually converges in 1-4 iterations 311 312 // distance function we want to minimize 313 final FieldVector3D<DerivativeStructure> pn = directionAt(offset.add(a), offset.subtract(a).negate(), side); 314 final DerivativeStructure yn = FieldVector3D.angle(pn, los); 315 if (yn.getValue() < 1.0e-12) { 316 // the query point is almost on the ellipse boundary 317 break; 318 } 319 320 // Halley's iteration on the derivative (since we want the minimum of the distance function) 321 final double f0 = yn.getPartialDerivative(1); 322 final double f1 = yn.getPartialDerivative(2); 323 final double f2 = yn.getPartialDerivative(3); 324 double dx = -2 * f0 * f1 / (2 * f1 * f1 - f0 * f2); 325 if (dx * f0 > 0) { 326 // the Halley's iteration is going towards maximum, not minimum 327 // try to go past inflection point 328 dx = -1.5 * f2 / f1; 329 } 330 331 // manage bounds 332 if (dx < 0) { 333 maxOffset = offset.getValue(); 334 if (offset.getValue() + dx <= minOffset) { 335 // we overshoot limit, fall back to bisection 336 dx = 0.5 * (minOffset - offset.getValue()); 337 } 338 } else { 339 minOffset = offset.getValue(); 340 if (offset.getValue() + dx >= maxOffset) { 341 // we overshoot limit, fall back to bisection 342 dx = 0.5 * (maxOffset - offset.getValue()); 343 } 344 } 345 346 // apply offset change 347 offset = offset.add(dx); 348 349 // check convergence 350 if (FastMath.abs(dx) < 1.0e-12) { 351 break; 352 } 353 354 } 355 356 return directionAt(a + offset.getReal(), a - offset.getReal(), side); 357 358 } 359 360 /** {@inheritDoc} */ 361 @Override 362 protected Vector3D directionAt(final double angle) { 363 final SinCos sce = FastMath.sinCos(angle); 364 final Vector3D dEll = new Vector3D(tanX * sce.cos(), tanY * sce.sin(), 1.0).normalize(); 365 return new Vector3D(dEll.getX(), getX(), dEll.getY(), getY(), dEll.getZ(), getZ()); 366 } 367 368 /** Get a direction from distances to foci. 369 * <p> 370 * if {@code d1} + {@code d2} = 2 max({@link #getHalfApertureAlongX()}, {@link #getHalfApertureAlongY()}), 371 * then the point is on the ellipse boundary 372 * </p> 373 * @param d1 distance to focus 1 374 * @param d2 distance to focus 2 375 * @param sign sign of the ellipse point with respect to F1 ^ F2 376 * @return direction 377 */ 378 private Vector3D directionAt(final double d1, final double d2, final double sign) { 379 final double cos1 = FastMath.cos(d1); 380 final double cos2 = FastMath.cos(d2); 381 final double a1 = (cos1 - cos2 * dotF1F2) * d; 382 final double a2 = (cos2 - cos1 * dotF1F2) * d; 383 final double ac = FastMath.sqrt((1 - (a1 * a1 + 2 * a1 * a2 * dotF1F2 + a2 * a2)) * d); 384 return new Vector3D(a1, focus1, a2, focus2, FastMath.copySign(ac, sign), crossF1F2); 385 } 386 387 /** Get a direction from distances to foci. 388 * <p> 389 * if {@code d1} + {@code d2} = 2 max({@link #getHalfApertureAlongX()}, {@link #getHalfApertureAlongY()}), 390 * then the point is on the ellipse boundary 391 * </p> 392 * @param d1 distance to focus 1 393 * @param d2 distance to focus 2 394 * @param sign sign of the ellipse point with respect to F1 ^ F2 395 * @param <T> type of the field element 396 * @return direction 397 */ 398 private <T extends CalculusFieldElement<T>> FieldVector3D<T> directionAt(final T d1, final T d2, final double sign) { 399 final T cos1 = FastMath.cos(d1); 400 final T cos2 = FastMath.cos(d2); 401 final T a1 = cos1.subtract(cos2.multiply(dotF1F2)).multiply(d); 402 final T a2 = cos2.subtract(cos1.multiply(dotF1F2)).multiply(d); 403 final T ac = FastMath.sqrt(a1.multiply(a1.add(a2.multiply(2 * dotF1F2))).add(a2.multiply(a2)).negate().add(1).multiply(d)); 404 return new FieldVector3D<>(a1, focus1, a2, focus2, FastMath.copySign(ac, sign), crossF1F2); 405 } 406 407 }