AbstractRadiationForceModel.java
/* Copyright 2002-2024 CS GROUP
* Licensed to CS GROUP (CS) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* CS licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.orekit.forces.radiation;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
import org.hipparchus.CalculusFieldElement;
import org.hipparchus.Field;
import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.ode.events.Action;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.MathArrays;
import org.orekit.bodies.OneAxisEllipsoid;
import org.orekit.forces.ForceModel;
import org.orekit.frames.Frame;
import org.orekit.propagation.events.EclipseDetector;
import org.orekit.propagation.events.EventDetector;
import org.orekit.propagation.events.FieldEclipseDetector;
import org.orekit.propagation.events.FieldEventDetector;
import org.orekit.utils.Constants;
import org.orekit.utils.ExtendedPVCoordinatesProvider;
import org.orekit.utils.ExtendedPVCoordinatesProviderAdapter;
import org.orekit.utils.OccultationEngine;
/**
* Base class for radiation force models.
* @see SolarRadiationPressure
* @see ECOM2
* @since 10.2
*/
public abstract class AbstractRadiationForceModel implements ForceModel {
/** Margin to force recompute lighting ratio derivatives when we are really inside penumbra. */
private static final double ANGULAR_MARGIN = 1.0e-10;
/** Max check interval for eclipse detectors. */
private static final double ECLIPSE_MAX_CHECK = 60.0;
/** Threshold for eclipse detectors. */
private static final double ECLIPSE_THRESHOLD = 1.0e-7; // this is consistent with ANGULAR_MARGIN = 10⁻¹⁰ rad for LEO
/** Flatness for spherical bodies. */
private static final double SPHERICAL_BODY_FLATNESS = 0.0;
/** Prefix for occulting bodies frames names. */
private static final String OCCULTING_PREFIX = "occulting-";
/** Occulting bodies (central body is always the first one).
* @since 12.0
*/
private final List<OccultationEngine> occultingBodies;
/**
* Default constructor.
* Only central body is considered.
* @param sun Sun model
* @param centralBody central body shape model (for umbra/penumbra computation)
* @since 12.0
*/
protected AbstractRadiationForceModel(final ExtendedPVCoordinatesProvider sun, final OneAxisEllipsoid centralBody) {
// in most cases, there will be only Earth, sometimes also Moon so an initial capacity of 2 is appropriate
occultingBodies = new ArrayList<>(2);
occultingBodies.add(new OccultationEngine(sun, Constants.SUN_RADIUS, centralBody));
}
/** {@inheritDoc} */
@Override
public boolean dependsOnPositionOnly() {
return false;
}
/** {@inheritDoc} */
@Override
public Stream<EventDetector> getEventDetectors() {
final EventDetector[] detectors = new EventDetector[2 * occultingBodies.size()];
for (int i = 0; i < occultingBodies.size(); ++i) {
final OccultationEngine occulting = occultingBodies.get(i);
detectors[2 * i] = new EclipseDetector(occulting).
withUmbra().
withMargin(-ANGULAR_MARGIN).
withMaxCheck(ECLIPSE_MAX_CHECK).
withThreshold(ECLIPSE_THRESHOLD).
withHandler((state, detector, increasing) -> Action.RESET_DERIVATIVES);
detectors[2 * i + 1] = new EclipseDetector(occulting).
withPenumbra().
withMargin(ANGULAR_MARGIN).
withMaxCheck(ECLIPSE_MAX_CHECK).
withThreshold(ECLIPSE_THRESHOLD).
withHandler((state, detector, increasing) -> Action.RESET_DERIVATIVES);
}
// Fusion between Date detector for parameter driver span change and
// Detector for umbra / penumbra events
return Stream.concat(Stream.of(detectors), ForceModel.super.getEventDetectors());
}
/** {@inheritDoc} */
@Override
public <T extends CalculusFieldElement<T>> Stream<FieldEventDetector<T>> getFieldEventDetectors(final Field<T> field) {
final T zero = field.getZero();
@SuppressWarnings("unchecked")
final FieldEventDetector<T>[] detectors = (FieldEventDetector<T>[]) Array.newInstance(FieldEventDetector.class,
2 * occultingBodies.size());
for (int i = 0; i < occultingBodies.size(); ++i) {
final OccultationEngine occulting = occultingBodies.get(i);
detectors[2 * i] = new FieldEclipseDetector<>(field, occulting).
withUmbra().
withMargin(zero.newInstance(-ANGULAR_MARGIN)).
withMaxCheck(ECLIPSE_MAX_CHECK).
withThreshold(zero.newInstance(ECLIPSE_THRESHOLD)).
withHandler((state, detector, increasing) -> Action.RESET_DERIVATIVES);
detectors[2 * i + 1] = new FieldEclipseDetector<>(field, occulting).
withPenumbra().
withMargin(zero.newInstance(ANGULAR_MARGIN)).
withMaxCheck(ECLIPSE_MAX_CHECK).
withThreshold(zero.newInstance(ECLIPSE_THRESHOLD)).
withHandler((state, detector, increasing) -> Action.RESET_DERIVATIVES);
}
return Stream.concat(Stream.of(detectors), ForceModel.super.getFieldEventDetectors(field));
}
/**
* Get the useful angles for eclipse computation.
* @param position the satellite's position in the selected frame
* @param occultingPosition Oculting body position in the selected frame
* @param occultingRadius Occulting body mean radius
* @param occultedPosition Occulted body position in the selected frame
* @param occultedRadius Occulted body mean radius
* @return the 3 angles {(satOcculting, satOcculted), Occulting body apparent radius, Occulted body apparent radius}
*/
protected double[] getGeneralEclipseAngles(final Vector3D position, final Vector3D occultingPosition, final double occultingRadius,
final Vector3D occultedPosition, final double occultedRadius) {
final double[] angle = new double[3];
final Vector3D satOccultedVector = occultedPosition.subtract(position);
final Vector3D satOccultingVector = occultingPosition.subtract(position);
// Sat-Occulted / Sat-Occulting angle
angle[0] = Vector3D.angle(satOccultedVector, satOccultingVector);
// Occulting body apparent radius
angle[1] = FastMath.asin(occultingRadius / satOccultingVector.getNorm());
// Occulted body apparent radius
angle[2] = FastMath.asin(occultedRadius / satOccultedVector.getNorm());
return angle;
}
/**
* Get the useful angles for eclipse computation.
* @param occultingPosition Oculting body position in the selected frame
* @param occultingRadius Occulting body mean radius
* @param occultedPosition Occulted body position in the selected frame
* @param occultedRadius Occulted body mean radius
* @param position the satellite's position in the selected frame
* @param <T> extends RealFieldElement
* @return the 3 angles {(satOcculting, satOcculted), Occulting body apparent radius, Occulted body apparent radius}
*/
protected <T extends CalculusFieldElement<T>> T[] getGeneralEclipseAngles(final FieldVector3D<T> position,
final FieldVector3D<T> occultingPosition, final T occultingRadius,
final FieldVector3D<T> occultedPosition, final T occultedRadius) {
final T[] angle = MathArrays.buildArray(position.getX().getField(), 3);
final FieldVector3D<T> satOccultedVector = occultedPosition.subtract(position);
final FieldVector3D<T> satOccultingVector = occultingPosition.subtract(position);
// Sat-Occulted / Sat-Occulting angle
angle[0] = FieldVector3D.angle(satOccultedVector, satOccultingVector);
// Occulting body apparent radius
angle[1] = occultingRadius.divide(satOccultingVector.getNorm()).asin();
// Occulted body apparent radius
angle[2] = occultedRadius.divide(satOccultedVector.getNorm()).asin();
return angle;
}
/**
* Add a new occulting body.
* <p>
* Central body is already considered, it shall not be added this way.
* </p>
* @param provider body PV provider
* @param radius body mean radius
* @see #addOccultingBody(OneAxisEllipsoid)
*/
public void addOccultingBody(final ExtendedPVCoordinatesProvider provider, final double radius) {
// as parent frame for occulting body frame,
// we select the first inertial frame in central body hierarchy
Frame parent = occultingBodies.get(0).getOcculting().getBodyFrame();
while (!parent.isPseudoInertial()) {
parent = parent.getParent();
}
// as the occulting body will be spherical, we can use an inertially oriented body frame
final Frame inertiallyOrientedBodyFrame =
new ExtendedPVCoordinatesProviderAdapter(parent,
provider,
OCCULTING_PREFIX + occultingBodies.size());
// create the spherical occulting body
final OneAxisEllipsoid sphericalOccultingBody =
new OneAxisEllipsoid(radius, SPHERICAL_BODY_FLATNESS, inertiallyOrientedBodyFrame);
addOccultingBody(sphericalOccultingBody);
}
/**
* Add a new occulting body.
* <p>
* Central body is already considered, it shall not be added this way.
* </p>
* @param occulting occulting body to add
* @see #addOccultingBody(ExtendedPVCoordinatesProvider, double)
* @since 12.0
*/
public void addOccultingBody(final OneAxisEllipsoid occulting) {
// retrieve Sun from the central occulting body engine
final OccultationEngine central = occultingBodies.get(0);
// create a new occultation engine for the new occulting body
final OccultationEngine additional =
new OccultationEngine(central.getOcculted(), central.getOccultedRadius(), occulting);
// add it to the list
occultingBodies.add(additional);
}
/**
* Get all occulting bodies to consider.
* <p>
* The list always contains at least one element: the central body
* which is always the first one in the list.
* </p>
* @return immutable list of all occulting bodies to consider, including the central body
* @since 12.0
*/
public List<OccultationEngine> getOccultingBodies() {
return Collections.unmodifiableList(occultingBodies);
}
}