Switching solar panel behavior during eclipse

I want to implement a custom panel for my BoxAndSolarArraySpacecraft that behaves like this:

  • When the spacecraft is not in eclipse , the panel tracks the Sun.
  • When it enters eclipse , the panel rotates back to a fixed angle.

below is a very basic custom Java class I wrote (I amm still pretty new to Java). The plan is to compile this into a JAR and use it with orekit_jpype.

import org.orekit.forces.Panel;
import org.orekit.propagation.SpacecraftState;
import org.orekit.propagation.FieldSpacecraftState;
import org.orekit.time.AbsoluteDate;
import org.orekit.utils.Constants;
import org.orekit.utils.PVCoordinatesProvider;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.CalculusFieldElement;
import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
import org.orekit.utils.ExtendedPositionProvider;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.Precision;


public class CustomPanel extends Panel {

    public enum Mode {
        SUN_TRACKING,
        REVERSE_ROTATION,
        HOLD
    }

    private Mode mode = Mode.SUN_TRACKING;

    private final Vector3D rotationAxis;
    private final Vector3D referenceNormal;

    private final double reverseRate;     // rad/s
    private double holdAngle;             // rad

    public double currentAngle = 0.0;

    private AbsoluteDate lastDate = null;

    private final ExtendedPositionProvider target;

    public SmartPanel(Vector3D referenceNormal,
                      Vector3D rotationAxis,
                      double area,
                      boolean doubleSided,
                      double drag,
                      double liftRatio,
                      double absorption,
                      double reflection,
                      double reverseRate,
                      double holdAngle,
                      final ExtendedPositionProvider target) {

        super(area, doubleSided, drag, liftRatio, absorption, reflection);

        this.referenceNormal = referenceNormal.normalize();
        this.rotationAxis = rotationAxis.normalize();

        this.reverseRate = Math.abs(reverseRate); // speed of wing rotating back to holdAngle
        this.holdAngle = holdAngle; // angle of rest when in eclipse

        this.target = target;
        
    }

    // Event handler will call these when an EclipseDetector triggered
    public void enterEclipse() {
        mode = Mode.REVERSE_ROTATION;
    }

    public void exitEclipse() {
        mode = Mode.SUN_TRACKING;
    }

    public void setHoldAngle(double newAngle) {
        this.holdAngle = newAngle;
    }

    // update current angle if in eclipse - REVERSE_ROTATION
    private void updateAngle(AbsoluteDate date) {

        if (lastDate == null) {
            lastDate = date;
            return;
        }

        double dt = date.durationFrom(lastDate);
        lastDate = date;

        if (mode == Mode.REVERSE_ROTATION) {

            currentAngle -= reverseRate * dt;

            if (currentAngle <= holdAngle) {
                currentAngle = holdAngle;
                mode = Mode.HOLD;
            }
        }
    }


    @Override
    public Vector3D getNormal(SpacecraftState state) {

        updateAngle(state.getDate());

        switch (mode) {

            case SUN_TRACKING:

                return getNormalForPointing(state);

            case REVERSE_ROTATION:
            case HOLD:
                return getNormalForFixed(referenceNormal, rotationAxis, currentAngle);

            default:
                return referenceNormal;
        }
    }

    @Override
    public <T extends CalculusFieldElement<T>>
    FieldVector3D<T> getNormal(FieldSpacecraftState<T> state) {

        // just dummy implementation
        Vector3D normal = getNormal(state.toSpacecraftState());
        return new FieldVector3D<>(state.getDate().getField(), normal);
    }


    private Vector3D getNormalForFixed(Vector3D v, Vector3D axis, double angle) {

        double c = Math.cos(angle);
        double s = Math.sin(angle);

        return v.scalarMultiply(c)
                .add(axis.crossProduct(v).scalarMultiply(s))
                .add(axis.scalarMultiply(axis.dotProduct(v) * (1 - c)));
    }

    private Vector3D getNormalForPointing(SpacecraftState state) {
        // Same code as in the orekit PointingPanel
        final Vector3D targetInert = target.getPosition(state.getDate(), state.getFrame()).
                                     subtract(state.getPosition()).normalize();
        final Vector3D targetSpacecraft = state.getAttitude().getRotation().applyTo(targetInert);
        final double d = Vector3D.dotProduct(targetSpacecraft, rotationAxis);
        final double f = 1 - d * d;
        if (f < Precision.EPSILON) {
            // extremely rare case: the target is along panel rotation axis
            // (there will not be much output power if it is a solar array…)
            // we set up an arbitrary normal
            return rotationAxis.orthogonal();
        }

        final double s = 1.0 / FastMath.sqrt(f);
        return new Vector3D(s, targetSpacecraft, -s * d, rotationAxis);
    }
}

I also set up an EventHandler that calls enterEclipse and exitEclipse when the corresponding events fire. Although I am not sure this is correct or not, so I’d appreciate any feedback on the implementation. Another question: if I use this panel with a NumericalPropagator as aSolarRadiationPressure force, is overriding getNormal enough, or are there other methods I should also override?

Any suggestions or guidance would be greatly appreciated.

Hi @dominic-mt,

I think you have a good plan to achieve what you want to do. Unless I missed something.

getNormal should be enough, as long as your panel is part of the list in BoxAndSolarArraySpacecraft.

Small question: why didn’t you extend PointingPanel and call super.getNormal(state) in getNormalForPointing ?

Cheers,
Maxime

1 Like

Thanks for the reply, Inheriting from PointingPanel makes sense since I need the same thing. Thanks for the suggestion :smiley:

As the panel API is called by force models which themselves are called by ordinary differential equation integrators, you should be aware that calls are not always chronological or reverse-chronological. There are two different reasons for this.

  1. when the integrator evaluates a step, it evaluates the derivatives at several places in the step, sampling forward and backward (see for example DormandPrince853Integrator.getC() method, which gives the times steps of the Butcher array, the coefficients are not all in increasing order)
  2. once a step has been fully evaluated, it may be necessary to truncate it if an event occurs (for example at eclipse entry/exit, in order to re-evaluate the forces when dynamics change)

Your use case will typically be hindered by the second point. If you set up the mode when the force model is evaluated, it will change at events and force models will be re-evaluated at events. This is a very classical effect.

In order to deal with that, instead of just storing one value for the mode, you should store it in a TimeSpanMap and the event handler should register the validity range of each mode as it encounters them and its eventOccurred method is called (calling addValidaAfterif propagating forward in time, or calling addValidBefore if propagating backward in time). The panel, should then apply the mode that is valid at current time. Then, if the integrator does call the force model at some times around the event, almost randomly, it will properly use the correct mode regardless of the time.

2 Likes