Skip to content

Q-Law orbit raising #503

@ChristopherRabotin

Description

@ChristopherRabotin

High level description

I've tried this in the past, but failed. Gemini proposes a solution which I might as well try and debug.

Requirements

Implement Q-Law \o/

Test plans

The Q Law paper test cases are already in the code, so I'll just need to clone these for the new Q-Law implementation

Design

Document, discuss, and optionally upload design diagram into this section.

use nalgebra::Vector3;
use std::sync::Arc;

pub struct QLaw {
    pub objectives: Vec<Option<GuidanceObjective>>,
    pub weights: Vec<f64>,
    /// Scaling factors (tolerances) for each element, often x_i,tol in literature
    pub tolerances: Vec<f64>,
    pub max_eclipse_prct: Option<f64>,
}

impl GuidanceLaw for QLaw {
    fn achieved(&self, state: &Spacecraft) -> Result<bool, GuidanceError> {
        for obj in self.objectives.iter().flatten() {
            if !obj.assess_value(state.value(obj.parameter).context(GuidStateSnafu)?).0 {
                return Ok(false);
            }
        }
        Ok(true)
    }

    fn direction(&self, sc: &Spacecraft) -> Result<Vector3<f64>, GuidanceError> {
        if sc.mode() != GuidanceMode::Thrust {
            return Ok(Vector3::zeros());
        }

        let osc = sc.orbit;
        
        // Retrieve MEE elements: p, f, g, h, k, L
        // We'll use these to build the GVE matrix B(x)
        let p = osc.semi_parameter_km().context(GuidancePhysicsSnafu { action: "Q-Law" })?;
        let f = osc.mee_f().context(GuidancePhysicsSnafu { action: "Q-Law" })?;
        let g = osc.mee_g().context(GuidancePhysicsSnafu { action: "Q-Law" })?;
        let h = osc.mee_h().context(GuidancePhysicsSnafu { action: "Q-Law" })?;
        let k = osc.mee_k().context(GuidancePhysicsSnafu { action: "Q-Law" })?;
        let l = osc.true_longitude_rad().context(GuidancePhysicsSnafu { action: "Q-Law" })?;

        let (sin_l, cos_l) = l.sin_cos();
        let w = 1.0 + f * cos_l + g * sin_l;
        let s2 = 1.0 + h.powi(2) + k.powi(2);
        let sqrt_p_mu = (p / osc.gm_km_s2()).sqrt();

        // D is the direction vector: D = sum( dQ/dx_i * G_i )
        // where G_i is the row of the GVE matrix corresponding to element i
        let mut d_vector = Vector3::zeros();

        for (i, obj_opt) in self.objectives.iter().enumerate() {
            if let Some(obj) = obj_opt {
                let weight = self.weights[i];
                let tol = self.tolerances[i];
                let current_val = sc.value(obj.parameter).context(GuidStateSnafu)?;
                let target_val = obj.target; // Assuming GuidanceObjective stores a target
                
                // Partial of Q with respect to element x_i: 2 * W_i * (x_i - x_target) / tol^2
                let dq_dx = 2.0 * weight * (current_val - target_val) / tol.powi(2);

                // GVE components for the RTN frame (Radial, Transverse, Normal)
                let g_i = match obj.parameter {
                    StateParameter::Element(OrbitalElement::SemiParameter) => {
                        Vector3::new(0.0, 2.0 * p / w * sqrt_p_mu, 0.0)
                    }
                    StateParameter::Element(OrbitalElement::EquinoctialF) => {
                        Vector3::new(
                            sqrt_p_mu * sin_l,
                            sqrt_p_mu / w * ((w + 1.0) * cos_l + f),
                            -sqrt_p_mu * g / w * (h * sin_l - k * cos_l)
                        )
                    }
                    StateParameter::Element(OrbitalElement::EquinoctialG) => {
                        Vector3::new(
                            -sqrt_p_mu * cos_l,
                            sqrt_p_mu / w * ((w + 1.0) * sin_l + g),
                            sqrt_p_mu * f / w * (h * sin_l - k * cos_l)
                        )
                    }
                    StateParameter::Element(OrbitalElement::EquinoctialH) => {
                        Vector3::new(0.0, 0.0, sqrt_p_mu * s2 * cos_l / (2.0 * w))
                    }
                    StateParameter::Element(OrbitalElement::EquinoctialK) => {
                        Vector3::new(0.0, 0.0, sqrt_p_mu * s2 * sin_l / (2.0 * w))
                    }
                    _ => Vector3::zeros(),
                };

                d_vector += dq_dx * g_i;
            }
        }

        // To minimize Q, we thrust in the direction opposite to the gradient
        // Steering direction u = -D / |D|
        if d_vector.norm() > 0.0 {
            let steering_rtn = -d_vector.normalize();
            
            // Convert RTN to Inertial
            Ok(osc.dcm_from_rcn_to_inertial().context(GuidancePhysicsSnafu { action: "RTN to Inertial" })? * steering_rtn)
        } else {
            Ok(Vector3::zeros())
        }
    }

    fn throttle(&self, sc: &Spacecraft) -> Result<f64, GuidanceError> {
        // Q-law often includes an "effectivity" check. 
        // If the best dQ/dt is very small, it's better to coast.
        // For simplicity, we trigger full thrust if a direction is found.
        if sc.mode() == GuidanceMode::Thrust { Ok(1.0) } else { Ok(0.0) }
    }

    fn next(&self, sc: &mut Spacecraft, almanac: Arc<Almanac>) {
        // Reuse logic from previous implementations for eclipse and mode switching
    }
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions