BoxAndSolarArraySpacecraft.java

  1. /* Copyright 2002-2025 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.forces;

  18. import java.util.ArrayList;
  19. import java.util.Collections;
  20. import java.util.List;
  21. import java.util.stream.Collectors;

  22. import org.hipparchus.CalculusFieldElement;
  23. import org.hipparchus.Field;
  24. import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
  25. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  26. import org.hipparchus.util.FastMath;
  27. import org.hipparchus.util.Precision;
  28. import org.orekit.errors.OrekitException;
  29. import org.orekit.errors.OrekitInternalError;
  30. import org.orekit.forces.drag.DragSensitive;
  31. import org.orekit.forces.radiation.RadiationSensitive;
  32. import org.orekit.propagation.FieldSpacecraftState;
  33. import org.orekit.propagation.SpacecraftState;
  34. import org.orekit.utils.ExtendedPositionProvider;
  35. import org.orekit.utils.ParameterDriver;

  36. /** Class representing the features of a classical satellite with a convex body shape.
  37.  * <p>
  38.  * The body can be either a simple parallelepipedic box aligned with
  39.  * spacecraft axes or a set of panels defined by their area and normal vector.
  40.  * Some panels may be moving to model solar arrays (or antennas that could
  41.  * point anywhere). This should handle accurately most spacecraft shapes. This model
  42.  * does not take cast shadows into account.
  43.  * </p>
  44.  * <p>
  45.  * The lift component of the drag force can be optionally considered. It should
  46.  * probably only be used for reentry computation, with much denser atmosphere
  47.  * than in regular orbit propagation. The lift component is computed using a
  48.  * ratio of molecules that experience specular reflection instead of diffuse
  49.  * reflection (absorption followed by outgassing at negligible velocity).
  50.  * Without lift (i.e. when the lift ratio is set to 0), drag force is along
  51.  * atmosphere relative velocity. With lift (i.e. when the lift ratio is set to any
  52.  * value between 0 and 1), the drag force depends on both relative velocity direction
  53.  * and panels normal orientation. For a single panel, if the relative velocity is
  54.  * head-on (i.e. aligned with the panel normal), the force will be in the same
  55.  * direction with and without lift, but the magnitude with lift ratio set to 1.0 will
  56.  * be twice the magnitude with lift ratio set to 0.0 (because atmosphere molecules
  57.  * bounces backward at same velocity in case of specular reflection).
  58.  * </p>
  59.  * <p>
  60.  * Each {@link Panel panel} has its own set of radiation and drag coefficients. In
  61.  * orbit determination context, it would not be possible to estimate each panel
  62.  * individually, therefore {@link #getDragParametersDrivers()} returns a single
  63.  * {@link ParameterDriver parameter driver} representing a {@link DragSensitive#GLOBAL_DRAG_FACTOR
  64.  * global drag multiplicative factor} that applies to all panels drag coefficients
  65.  * and the {@link #getRadiationParametersDrivers()} returns a single
  66.  * {@link ParameterDriver parameter driver} representing a
  67.  * {@link RadiationSensitive#GLOBAL_RADIATION_FACTOR global radiation multiplicative factor}
  68.  * that applies to all panels radiation coefficients.
  69.  * </p>
  70.  *
  71.  * @author Luc Maisonobe
  72.  * @author Pascal Parraud
  73.  */
  74. public class BoxAndSolarArraySpacecraft implements RadiationSensitive, DragSensitive {

  75.     /** Parameters scaling factor.
  76.      * <p>
  77.      * We use a power of 2 to avoid numeric noise introduction
  78.      * in the multiplications/divisions sequences.
  79.      * </p>
  80.      */
  81.     private final double SCALE = FastMath.scalb(1.0, -3);

  82.     /** Driver for drag multiplicative factor parameter. */
  83.     private final ParameterDriver dragFactorParameterDriver;

  84.     /** Driver for radiation pressure multiplicative factor parameter. */
  85.     private final ParameterDriver radiationFactorParameterDriver;

  86.     /** Panels composing the spacecraft. */
  87.     private final List<Panel> panels;

  88.     /** Build a spacecraft model.
  89.      * @param panels panels composing the body, solar arrays and antennas
  90.      * (only the panels with strictly positive area will be stored)
  91.      * @since 12.0
  92.      */
  93.     public BoxAndSolarArraySpacecraft(final List<Panel> panels) {

  94.         try {
  95.             dragFactorParameterDriver      = new ParameterDriver(DragSensitive.GLOBAL_DRAG_FACTOR,
  96.                                                                  1.0, SCALE, 0.0, Double.POSITIVE_INFINITY);
  97.             radiationFactorParameterDriver = new ParameterDriver(RadiationSensitive.GLOBAL_RADIATION_FACTOR,
  98.                                                                  1.0, SCALE, 0.0, Double.POSITIVE_INFINITY);
  99.         } catch (OrekitException oe) {
  100.             // this should never happen
  101.             throw new OrekitInternalError(oe);
  102.         }

  103.         // remove spurious panels
  104.         this.panels = panels.stream().filter(p -> p.getArea() > 0).collect(Collectors.toList());

  105.     }

  106.     /** Build a spacecraft model with best lighting of solar array.
  107.      * <p>
  108.      * Solar arrays orientation will be such that at each time the Sun direction
  109.      * will always be in the solar array meridian plane defined by solar array
  110.      * rotation axis and solar array normal vector.
  111.      * </p>
  112.      * @param xLength length of the body along its X axis (m)
  113.      * @param yLength length of the body along its Y axis (m)
  114.      * @param zLength length of the body along its Z axis (m)
  115.      * @param sun sun model
  116.      * @param solarArrayArea area of the solar array (m²)
  117.      * @param solarArrayAxis solar array rotation axis in satellite frame
  118.      * @param dragCoeff drag coefficient (used only for drag)
  119.      * @param liftRatio lift ratio (proportion between 0 and 1 of atmosphere modecules
  120.      * that will experience specular reflection when hitting spacecraft instead
  121.      * of experiencing diffuse reflection, hence producing lift)
  122.      * @param absorptionCoeff absorption coefficient between 0.0 an 1.0
  123.      * (used only for radiation pressure)
  124.      * @param reflectionCoeff specular reflection coefficient between 0.0 an 1.0
  125.      * (used only for radiation pressure)
  126.      * @since 12.0
  127.      */
  128.     public BoxAndSolarArraySpacecraft(final double xLength, final double yLength, final double zLength,
  129.                                       final ExtendedPositionProvider sun,
  130.                                       final double solarArrayArea, final Vector3D solarArrayAxis,
  131.                                       final double dragCoeff, final double liftRatio,
  132.                                       final double absorptionCoeff, final double reflectionCoeff) {
  133.         this(buildPanels(xLength, yLength, zLength,
  134.                          sun, solarArrayArea, solarArrayAxis,
  135.                          dragCoeff, liftRatio, absorptionCoeff, reflectionCoeff));
  136.     }

  137.     /** Get the panels composing the body.
  138.      * @return unmodifiable view of the panels composing the body
  139.      * @since 12.0
  140.      */
  141.     public List<Panel> getPanels() {
  142.         return Collections.unmodifiableList(panels);
  143.     }

  144.     /** {@inheritDoc} */
  145.     @Override
  146.     public List<ParameterDriver> getDragParametersDrivers() {
  147.         return Collections.singletonList(dragFactorParameterDriver);
  148.     }

  149.     /** {@inheritDoc} */
  150.     @Override
  151.     public List<ParameterDriver> getRadiationParametersDrivers() {
  152.         return Collections.singletonList(radiationFactorParameterDriver);
  153.     }

  154.     /** {@inheritDoc} */
  155.     @Override
  156.     public Vector3D dragAcceleration(final SpacecraftState state,
  157.                                      final double density, final Vector3D relativeVelocity,
  158.                                      final double[] parameters) {

  159.         final double dragFactor = parameters[0];

  160.         // relative velocity in spacecraft frame
  161.         final double   vNorm2 = relativeVelocity.getNormSq();
  162.         final double   vNorm  = FastMath.sqrt(vNorm2);
  163.         final Vector3D vDir   = state.getAttitude().getRotation().applyTo(relativeVelocity.scalarMultiply(1.0 / vNorm));
  164.         final double   coeff  = density * dragFactor * vNorm2 / (2.0 * state.getMass());

  165.         // panels contribution
  166.         Vector3D acceleration = Vector3D.ZERO;
  167.         for (final Panel panel : panels) {
  168.             Vector3D normal = panel.getNormal(state);
  169.             double dot = Vector3D.dotProduct(normal, vDir);
  170.             if (panel.isDoubleSided() && dot > 0) {
  171.                 // the flux comes from the back side
  172.                 normal = normal.negate();
  173.                 dot    = -dot;
  174.             }
  175.             if (dot < 0) {
  176.                 // the panel intercepts the incoming flux
  177.                 final double f         = coeff * panel.getDrag() * panel.getArea() * dot;
  178.                 final double liftRatio = panel.getLiftRatio();
  179.                 acceleration = new Vector3D(1,                                 acceleration,
  180.                                             (1 - liftRatio) * FastMath.abs(f), vDir,
  181.                                             liftRatio * f * 2,                 normal);
  182.             }
  183.         }

  184.         // convert back to inertial frame
  185.         return state.getAttitude().getRotation().applyInverseTo(acceleration);

  186.     }

  187.     /** {@inheritDoc} */
  188.     @Override
  189.     public <T extends CalculusFieldElement<T>> FieldVector3D<T>
  190.         dragAcceleration(final FieldSpacecraftState<T> state,
  191.                          final  T density, final FieldVector3D<T> relativeVelocity,
  192.                          final T[] parameters) {

  193.         final Field<T> field = state.getDate().getField();
  194.         final T dragFactor = parameters[0];

  195.         // relative velocity in spacecraft frame
  196.         final T                vNorm2 = relativeVelocity.getNormSq();
  197.         final T                vNorm  = FastMath.sqrt(vNorm2);
  198.         final FieldVector3D<T> vDir   = state.getAttitude().getRotation().applyTo(relativeVelocity.scalarMultiply(vNorm.reciprocal()));
  199.         final T                coeff  = density.multiply(dragFactor).multiply(vNorm2).divide(state.getMass().multiply(2.0));

  200.         // panels contribution
  201.         FieldVector3D<T> acceleration = FieldVector3D.getZero(field);
  202.         for (final Panel panel : panels) {
  203.             FieldVector3D<T> normal = panel.getNormal(state);
  204.             T dot = FieldVector3D.dotProduct(normal, vDir);
  205.             if (panel.isDoubleSided() && dot.getReal() > 0) {
  206.                 // the flux comes from the back side
  207.                 normal = normal.negate();
  208.                 dot    = dot.negate();
  209.             }
  210.             if (panel.isDoubleSided() || dot.getReal() < 0) {
  211.                 // the panel intercepts the incoming flux
  212.                 final T      f         = coeff.multiply(panel.getDrag() * panel.getArea()).multiply(dot);
  213.                 final double liftRatio = panel.getLiftRatio();
  214.                 acceleration = new FieldVector3D<>(field.getOne(),                         acceleration,
  215.                                                   FastMath.abs(f).multiply(1 - liftRatio), vDir,
  216.                                                   f.multiply(2 * liftRatio),               normal);
  217.             }
  218.         }

  219.         // convert back to inertial frame
  220.         return state.getAttitude().getRotation().applyInverseTo(acceleration);

  221.     }

  222.     /** {@inheritDoc} */
  223.     @Override
  224.     public Vector3D radiationPressureAcceleration(final SpacecraftState state,
  225.                                                   final Vector3D flux,
  226.                                                   final double[] parameters) {

  227.         if (flux.getNormSq() < Precision.SAFE_MIN) {
  228.             // null illumination (we are probably in umbra)
  229.             return Vector3D.ZERO;
  230.         }

  231.         // radiation flux in spacecraft frame
  232.         final double   radiationFactor = parameters[0];
  233.         final Vector3D fluxSat         = state.getAttitude().getRotation().applyTo(flux).
  234.                                          scalarMultiply(radiationFactor);

  235.         // panels contribution
  236.         Vector3D force = Vector3D.ZERO;
  237.         for (final Panel panel : panels) {
  238.             Vector3D normal = panel.getNormal(state);
  239.             double dot = Vector3D.dotProduct(normal, fluxSat);
  240.             if (panel.isDoubleSided() && dot > 0) {
  241.                 // the flux comes from the back side
  242.                 normal = normal.negate();
  243.                 dot    = -dot;
  244.             }
  245.             if (dot < 0) {
  246.                 // the panel intercepts the incoming flux

  247.                 final double absorptionCoeff         = panel.getAbsorption();
  248.                 final double specularReflectionCoeff = panel.getReflection();
  249.                 final double diffuseReflectionCoeff  = 1 - (absorptionCoeff + specularReflectionCoeff);
  250.                 final double psr                     = fluxSat.getNorm();

  251.                 // Vallado's equation 8-44 uses different parameters which are related to our parameters as:
  252.                 // cos (phi) = -dot / (psr * area)
  253.                 // n         = panel / area
  254.                 // s         = -fluxSat / psr
  255.                 final double cN = 2 * panel.getArea() * dot * (diffuseReflectionCoeff / 3 - specularReflectionCoeff * dot / psr);
  256.                 final double cS = (panel.getArea() * dot / psr) * (specularReflectionCoeff - 1);
  257.                 force = new Vector3D(1, force, cN, normal, cS, fluxSat);

  258.             }
  259.         }

  260.         // convert to inertial frame
  261.         return state.getAttitude().getRotation().applyInverseTo(new Vector3D(1.0 / state.getMass(), force));

  262.     }

  263.     /** {@inheritDoc}
  264.      * <p>This method implements equation 8-44 from David A. Vallado's
  265.      * Fundamentals of Astrodynamics and Applications, third edition,
  266.      * 2007, Microcosm Press.</p>
  267.      */
  268.     @Override
  269.     public <T extends CalculusFieldElement<T>> FieldVector3D<T>
  270.         radiationPressureAcceleration(final FieldSpacecraftState<T> state,
  271.                                       final FieldVector3D<T> flux,
  272.                                       final T[] parameters) {

  273.         final Field<T> field = state.getDate().getField();
  274.         if (flux.getNormSq().getReal() < Precision.SAFE_MIN) {
  275.             // null illumination (we are probably in umbra)
  276.             return FieldVector3D.getZero(field);
  277.         }

  278.         // radiation flux in spacecraft frame
  279.         final T                radiationFactor = parameters[0];
  280.         final FieldVector3D<T> fluxSat         = state.getAttitude().getRotation().applyTo(flux).
  281.                                                  scalarMultiply(radiationFactor);

  282.         // panels contribution
  283.         FieldVector3D<T> force = FieldVector3D.getZero(field);
  284.         for (final Panel panel : panels) {
  285.             FieldVector3D<T> normal = panel.getNormal(state);
  286.             T dot = FieldVector3D.dotProduct(normal, fluxSat);
  287.             if (panel.isDoubleSided() && dot.getReal() > 0) {
  288.                 // the flux comes from the back side
  289.                 normal = normal.negate();
  290.                 dot    = dot.negate();
  291.             }
  292.             if (dot.getReal() < 0) {
  293.                 // the panel intercepts the incoming flux

  294.                 final double absorptionCoeff         = panel.getAbsorption();
  295.                 final double specularReflectionCoeff = panel.getReflection();
  296.                 final double diffuseReflectionCoeff  = 1 - (absorptionCoeff + specularReflectionCoeff);
  297.                 final T      psr                     = fluxSat.getNorm();

  298.                 // Vallado's equation 8-44 uses different parameters which are related to our parameters as:
  299.                 // cos (phi) = -dot / (psr * area)
  300.                 // n         = panel / area
  301.                 // s         = -fluxSat / psr
  302.                 final T cN = dot.multiply(-2 * panel.getArea()).multiply(dot.multiply(specularReflectionCoeff).divide(psr).subtract(diffuseReflectionCoeff / 3));
  303.                 final T cS = dot.multiply(panel.getArea()).multiply(specularReflectionCoeff - 1).divide(psr);
  304.                 force = new FieldVector3D<>(field.getOne(), force, cN, normal, cS, fluxSat);
  305.             }
  306.         }

  307.         // convert to inertial frame
  308.         return state.getAttitude().getRotation().applyInverseTo(new FieldVector3D<>(state.getMass().reciprocal(), force));

  309.     }

  310.     /** Build the panels of a simple parallelepipedic box.
  311.      * @param xLength length of the body along its X axis (m)
  312.      * @param yLength length of the body along its Y axis (m)
  313.      * @param zLength length of the body along its Z axis (m)
  314.      * @param drag drag coefficient
  315.      * @param liftRatio drag lift ratio (proportion between 0 and 1 of atmosphere modecules
  316.      * that will experience specular reflection when hitting spacecraft instead
  317.      * of experiencing diffuse reflection, hence producing lift)
  318.      * @param absorption radiation pressure absorption coefficient (between 0 and 1)
  319.      * @param reflection radiation pressure specular reflection coefficient (between 0 and 1)
  320.      * @return surface vectors array
  321.      * @since 12.0
  322.      */
  323.     public static List<Panel> buildBox(final double xLength, final double yLength, final double zLength,
  324.                                        final double drag, final double liftRatio,
  325.                                        final double absorption, final double reflection) {

  326.         final List<Panel> panels = new ArrayList<>(6);

  327.         // spacecraft body, composed of single-sided panels
  328.         panels.add(new FixedPanel(Vector3D.MINUS_I, yLength * zLength, false, drag, liftRatio, absorption, reflection));
  329.         panels.add(new FixedPanel(Vector3D.PLUS_I,  yLength * zLength, false, drag, liftRatio, absorption, reflection));
  330.         panels.add(new FixedPanel(Vector3D.MINUS_J, xLength * zLength, false, drag, liftRatio, absorption, reflection));
  331.         panels.add(new FixedPanel(Vector3D.PLUS_J,  xLength * zLength, false, drag, liftRatio, absorption, reflection));
  332.         panels.add(new FixedPanel(Vector3D.MINUS_K, xLength * yLength, false, drag, liftRatio, absorption, reflection));
  333.         panels.add(new FixedPanel(Vector3D.PLUS_K,  xLength * yLength, false, drag, liftRatio, absorption, reflection));

  334.         return panels;

  335.     }

  336.     /** Build the panels of a simple parallelepiped box plus one solar array panel.
  337.      * @param xLength length of the body along its X axis (m)
  338.      * @param yLength length of the body along its Y axis (m)
  339.      * @param zLength length of the body along its Z axis (m)
  340.      * @param sun sun model
  341.      * @param solarArrayArea area of the solar array (m²)
  342.      * @param solarArrayAxis solar array rotation axis in satellite frame
  343.      * @param drag drag coefficient
  344.      * @param liftRatio drag lift ratio (proportion between 0 and 1 of atmosphere modecules
  345.      * that will experience specular reflection when hitting spacecraft instead
  346.      * of experiencing diffuse reflection, hence producing lift)
  347.      * @param absorption radiation pressure absorption coefficient (between 0 and 1)
  348.      * @param reflection radiation pressure specular reflection coefficient (between 0 and 1)
  349.      * @return surface vectors array
  350.      * @since 12.0
  351.      */
  352.     public static List<Panel> buildPanels(final double xLength, final double yLength, final double zLength,
  353.                                           final ExtendedPositionProvider sun,
  354.                                           final double solarArrayArea, final Vector3D solarArrayAxis,
  355.                                           final double drag, final double liftRatio,
  356.                                           final double absorption, final double reflection) {

  357.         // spacecraft body
  358.         final List<Panel> panels = buildBox(xLength, yLength, zLength, drag, liftRatio, absorption, reflection);

  359.         // solar array
  360.         panels.add(new PointingPanel(solarArrayAxis, sun, solarArrayArea, drag, liftRatio, absorption, reflection));

  361.         return panels;

  362.     }

  363. }