Skip to main content

nyx_space/md/opti/
raphson_finite_diff.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use super::solution::TargeterSolution;
20use super::targeter::Targeter;
21use crate::cosmic::{AstroAlmanacSnafu, AstroPhysicsSnafu};
22use crate::dynamics::guidance::{GuidanceError, LocalFrame, Maneuver, MnvrRepr};
23use crate::errors::TargetingError;
24use crate::linalg::{SMatrix, SVector, Vector6};
25use crate::md::{AstroSnafu, GuidanceSnafu, UnderdeterminedProblemSnafu, prelude::*};
26use crate::md::{PropSnafu, StateParameter};
27pub use crate::md::{Variable, Vary};
28use crate::polyfit::CommonPolynomial;
29use crate::pseudo_inverse;
30use anise::astro::orbit_gradient::OrbitGrad;
31use hifitime::TimeUnits;
32use log::{debug, error, info};
33use rayon::prelude::*;
34use snafu::{ResultExt, ensure};
35#[cfg(not(target_arch = "wasm32"))]
36use std::time::Instant;
37
38impl<const V: usize, const O: usize> Targeter<'_, V, O> {
39    /// Differential correction using finite differencing
40    #[allow(clippy::comparison_chain)]
41    pub fn try_achieve_fd(
42        &self,
43        initial_state: Spacecraft,
44        correction_epoch: Epoch,
45        achievement_epoch: Epoch,
46        almanac: Arc<Almanac>,
47    ) -> Result<TargeterSolution<V, O>, TargetingError> {
48        ensure!(!self.objectives.is_empty(), UnderdeterminedProblemSnafu);
49
50        let mut is_bplane_tgt = false;
51        for obj in &self.objectives {
52            if obj.parameter.is_b_plane() {
53                is_bplane_tgt = true;
54                break;
55            }
56        }
57
58        // Now we know that the problem is correctly defined, so let's propagate as is to the epoch
59        // where the correction should be applied.
60        let xi_start = self
61            .prop
62            .with(initial_state, almanac.clone())
63            .until_epoch(correction_epoch)
64            .context(PropSnafu)?;
65
66        debug!("initial_state = {initial_state}");
67        debug!("xi_start = {xi_start}");
68
69        let mut xi = xi_start;
70        // We'll store the initial state correction here.
71        let mut state_correction = Vector6::<f64>::zeros();
72
73        // Store the total correction in Vector3
74        let mut total_correction = SVector::<f64, V>::zeros();
75
76        let mut mnvr = Maneuver {
77            start: correction_epoch,
78            end: achievement_epoch,
79            thrust_prct: 1.0,
80            representation: MnvrRepr::Angles {
81                azimuth: CommonPolynomial::Quadratic {
82                    a: 0.0,
83                    b: 0.0,
84                    c: 0.0,
85                },
86                elevation: CommonPolynomial::Quadratic {
87                    a: 0.0,
88                    b: 0.0,
89                    c: 0.0,
90                },
91            },
92            frame: LocalFrame::RCN,
93        };
94
95        let mut finite_burn_target = false;
96
97        // Apply the initial guess: first accumulate, then apply once (matching
98        // the pattern used in the iteration loop).
99        for (i, var) in self.variables.iter().enumerate() {
100            // Check the validity (this function will report to log and raise an error)
101            var.valid()?;
102            // Check that there is no attempt to target a position in a local frame
103            if let Some(correction_frame) = self.correction_frame
104                && var.component.vec_index() < 3
105            {
106                // Then this is a position correction, which is not allowed if a frame is provided!
107                let msg = format!(
108                    "Variable is in frame {correction_frame:?} but that frame cannot be used for a {:?} correction",
109                    var.component
110                );
111                error!("{msg}");
112                return Err(TargetingError::FrameError { msg });
113            }
114
115            // Check that a thruster is provided since we'll be changing that and the burn duration
116            if var.component.is_finite_burn() {
117                if xi_start.thruster.is_none() {
118                    return Err(TargetingError::GuidanceError {
119                        source: GuidanceError::NoThrustersDefined,
120                    });
121                }
122
123                finite_burn_target = true;
124                // Modify the default maneuver
125                match var.component {
126                    Vary::Duration => mnvr.end = mnvr.start + var.init_guess.seconds(),
127                    Vary::EndEpoch => mnvr.end += var.init_guess.seconds(),
128                    Vary::StartEpoch => mnvr.start += var.init_guess.seconds(),
129                    Vary::MnvrAlpha | Vary::MnvrAlphaDot | Vary::MnvrAlphaDDot => {
130                        match mnvr.representation {
131                            MnvrRepr::Angles { azimuth, elevation } => {
132                                let azimuth = azimuth
133                                    .add_val_in_order(var.init_guess, var.component.vec_index())
134                                    .unwrap();
135                                mnvr.representation = MnvrRepr::Angles { azimuth, elevation };
136                            }
137                            _ => unreachable!(),
138                        };
139                    }
140                    Vary::MnvrDelta | Vary::MnvrDeltaDot | Vary::MnvrDeltaDDot => {
141                        match mnvr.representation {
142                            MnvrRepr::Angles { azimuth, elevation } => {
143                                let elevation = elevation
144                                    .add_val_in_order(var.init_guess, var.component.vec_index())
145                                    .unwrap();
146                                mnvr.representation = MnvrRepr::Angles { azimuth, elevation };
147                            }
148                            _ => unreachable!(),
149                        };
150                    }
151                    Vary::ThrustX | Vary::ThrustY | Vary::ThrustZ => {
152                        let mut vector = mnvr.direction();
153                        vector[var.component.vec_index()] += var.perturbation;
154                        mnvr.set_direction(vector).context(GuidanceSnafu)?;
155                    }
156                    Vary::ThrustRateX | Vary::ThrustRateY | Vary::ThrustRateZ => {
157                        let mut vector = mnvr.rate();
158                        vector[(var.component.vec_index() - 1) % 3] += var.perturbation;
159                        mnvr.set_rate(vector).context(GuidanceSnafu)?;
160                    }
161                    Vary::ThrustAccelX | Vary::ThrustAccelY | Vary::ThrustAccelZ => {
162                        let mut vector = mnvr.accel();
163                        vector[(var.component.vec_index() - 1) % 3] += var.perturbation;
164                        mnvr.set_accel(vector).context(GuidanceSnafu)?;
165                    }
166                    Vary::ThrustLevel => {
167                        mnvr.thrust_prct += var.perturbation;
168                        mnvr.thrust_prct = mnvr.thrust_prct.clamp(0.0, 1.0);
169                    }
170                    _ => unreachable!(),
171                }
172                info!("Initial maneuver guess: {mnvr}");
173            } else {
174                state_correction[var.component.vec_index()] += var.init_guess;
175            }
176
177            total_correction[i] += var.init_guess;
178        }
179
180        // Apply the accumulated initial guess to xi (once, after the loop)
181        if !finite_burn_target {
182            if let Some(frame) = self.correction_frame {
183                let dcm_vnc2inertial = xi
184                    .orbit
185                    .dcm_to_inertial(frame)
186                    .context(AstroPhysicsSnafu)
187                    .context(AstroSnafu)?
188                    .rot_mat;
189
190                let velocity_correction = dcm_vnc2inertial * state_correction.fixed_rows::<3>(3);
191                xi.orbit.apply_dv_km_s(velocity_correction);
192            } else {
193                xi.orbit.radius_km += state_correction.fixed_rows::<3>(0).to_owned();
194                xi.orbit.velocity_km_s += state_correction.fixed_rows::<3>(3).to_owned();
195            }
196        }
197
198        let mut prev_err_norm = f64::INFINITY;
199
200        // Determine padding in debugging info
201        // For the width, we find the largest desired values and multiply it by the order of magnitude of its tolerance
202        let max_obj_val = self
203            .objectives
204            .iter()
205            .map(|obj| {
206                obj.desired_value.abs().ceil() as i32
207                    * 10_i32.pow(obj.tolerance.abs().log10().ceil() as u32)
208            })
209            .max()
210            .unwrap();
211
212        let max_obj_tol = self
213            .objectives
214            .iter()
215            .map(|obj| obj.tolerance.log10().abs().ceil() as usize)
216            .max()
217            .unwrap();
218
219        let width = f64::from(max_obj_val).log10() as usize + 2 + max_obj_tol;
220
221        #[cfg(not(target_arch = "wasm32"))]
222        let start_instant = Instant::now();
223
224        for it in 0..=self.iterations {
225            // Modify each variable by the desired perturbation, propagate, compute the final parameter, and store how modifying that variable affects the final parameter
226            let cur_xi = xi;
227
228            // If we are targeting a finite burn, let's set propagate in several steps to make sure we don't miss the burn
229            let xf = if finite_burn_target {
230                info!("#{it} {mnvr}");
231                let mut prop = self.prop.clone();
232                let prop_opts = prop.opts;
233                let pre_mnvr = prop
234                    .with(cur_xi, almanac.clone())
235                    .until_epoch(mnvr.start)
236                    .context(PropSnafu)?;
237                prop.dynamics = prop.dynamics.with_guidance_law(Arc::new(mnvr));
238                prop.set_max_step(mnvr.duration());
239                let post_mnvr = prop
240                    .with(
241                        pre_mnvr.with_guidance_mode(GuidanceMode::Thrust),
242                        almanac.clone(),
243                    )
244                    .until_epoch(mnvr.end)
245                    .context(PropSnafu)?;
246                // Reset the propagator options to their previous configuration
247                prop.opts = prop_opts;
248                // And propagate until the achievement epoch
249                prop.with(post_mnvr, almanac.clone())
250                    .until_epoch(achievement_epoch)
251                    .context(PropSnafu)?
252                    .orbit
253            } else {
254                self.prop
255                    .with(cur_xi, almanac.clone())
256                    .until_epoch(achievement_epoch)
257                    .context(PropSnafu)?
258                    .orbit
259            };
260
261            let xf_dual_obj_frame = match &self.objective_frame {
262                Some(frame) => {
263                    let orbit_obj_frame = almanac
264                        .transform_to(xf, *frame, None)
265                        .context(AstroAlmanacSnafu)
266                        .context(AstroSnafu)?;
267
268                    OrbitGrad::from(orbit_obj_frame)
269                }
270                None => OrbitGrad::from(xf),
271            };
272
273            // Build the error vector
274            let mut err_vector = SVector::<f64, O>::zeros();
275            let mut converged = true;
276
277            // Build the B-Plane once, if needed, and always in the objective frame
278            let b_plane = if is_bplane_tgt {
279                Some(BPlane::from_dual(xf_dual_obj_frame).context(AstroSnafu)?)
280            } else {
281                None
282            };
283
284            // Build debugging information
285            let mut objmsg = Vec::with_capacity(self.objectives.len());
286
287            // The Jacobian includes the sensitivity of each objective with respect to each variable for the whole trajectory.
288            // As such, it includes the STM of that variable for the whole propagation arc.
289            let mut jac = SMatrix::<f64, O, V>::zeros();
290
291            for (i, obj) in self.objectives.iter().enumerate() {
292                let partial = if obj.parameter.is_b_plane() {
293                    match obj.parameter {
294                        StateParameter::BdotR() => b_plane.unwrap().b_r_km,
295                        StateParameter::BdotT() => b_plane.unwrap().b_t_km,
296                        StateParameter::BLTOF() => b_plane.unwrap().ltof_s,
297                        _ => unreachable!(),
298                    }
299                } else if let StateParameter::Element(oe) = obj.parameter {
300                    xf_dual_obj_frame
301                        .partial_for(oe)
302                        .context(AstroPhysicsSnafu)
303                        .context(AstroSnafu)?
304                } else {
305                    unreachable!()
306                };
307
308                let achieved = partial.real();
309
310                let (ok, param_err) = obj.assess_value(achieved);
311                if !ok {
312                    converged = false;
313                }
314                err_vector[i] = param_err;
315
316                objmsg.push(format!(
317                    "\t{:?}: achieved = {:>width$.prec$}\t desired = {:>width$.prec$}\t scaled error = {:>width$.prec$}",
318                    obj.parameter,
319                    achieved,
320                    obj.desired_value,
321                    param_err, width=width, prec=max_obj_tol
322                ));
323
324                let mut pert_calc: Vec<_> = self
325                    .variables
326                    .iter()
327                    .enumerate()
328                    .map(|(j, var)| (j, var, 0.0_f64))
329                    .collect();
330
331                pert_calc.par_iter_mut().for_each(|(_, var, jac_val)| {
332                    let mut this_xi = xi;
333
334                    let mut this_prop = self.prop.clone();
335                    let mut this_mnvr = mnvr;
336
337                    let mut opposed_pert = false;
338
339                    if var.component.is_finite_burn() {
340                        // Modify the burn itself
341                        let pert = var.perturbation;
342                        // Modify the maneuver, but do not change the epochs of the maneuver unless the change is greater than one millisecond
343                        match var.component {
344                            Vary::Duration => {
345                                if pert.abs() > 1e-3 {
346                                    this_mnvr.end = mnvr.start + pert.seconds()
347                                }
348                            }
349                            Vary::EndEpoch => {
350                                if pert.abs() > 1e-3 {
351                                    this_mnvr.end = mnvr.end + pert.seconds()
352                                }
353                            }
354                            Vary::StartEpoch => {
355                                if pert.abs() > 1e-3 {
356                                    this_mnvr.start = mnvr.start + pert.seconds()
357                                }
358                            }
359                            Vary::MnvrAlpha | Vary::MnvrAlphaDot | Vary::MnvrAlphaDDot => {
360                                match mnvr.representation {
361                                    MnvrRepr::Angles { azimuth, elevation } => {
362                                        let azimuth = azimuth
363                                            .add_val_in_order(pert, var.component.vec_index())
364                                            .unwrap();
365                                        this_mnvr.representation =
366                                            MnvrRepr::Angles { azimuth, elevation };
367                                    }
368                                    _ => unreachable!(),
369                                };
370                            }
371                            Vary::MnvrDelta | Vary::MnvrDeltaDot | Vary::MnvrDeltaDDot => {
372                                match mnvr.representation {
373                                    MnvrRepr::Angles { azimuth, elevation } => {
374                                        let elevation = elevation
375                                            .add_val_in_order(pert, var.component.vec_index())
376                                            .unwrap();
377                                        this_mnvr.representation =
378                                            MnvrRepr::Angles { azimuth, elevation };
379                                    }
380                                    _ => unreachable!(),
381                                };
382                            }
383                            Vary::ThrustX | Vary::ThrustY | Vary::ThrustZ => {
384                                let mut vector = this_mnvr.direction();
385                                vector[var.component.vec_index()] += var.perturbation;
386                                if !var.check_bounds(vector[var.component.vec_index()]).1 {
387                                    // Oops, bound was hit, go the other way
388                                    vector[var.component.vec_index()] -= 2.0 * var.perturbation;
389                                    opposed_pert = true;
390                                }
391                                this_mnvr.set_direction(vector).unwrap();
392                            }
393                            Vary::ThrustRateX | Vary::ThrustRateY | Vary::ThrustRateZ => {
394                                let mut vector = this_mnvr.rate();
395                                vector[(var.component.vec_index() - 1) % 3] += var.perturbation;
396                                if !var
397                                    .check_bounds(vector[(var.component.vec_index() - 1) % 3])
398                                    .1
399                                {
400                                    // Oops, bound was hit, go the other way
401                                    vector[(var.component.vec_index() - 1) % 3] -=
402                                        2.0 * var.perturbation;
403                                    opposed_pert = true;
404                                }
405                                this_mnvr.set_rate(vector).unwrap();
406                            }
407                            Vary::ThrustAccelX | Vary::ThrustAccelY | Vary::ThrustAccelZ => {
408                                let mut vector = this_mnvr.accel();
409                                vector[(var.component.vec_index() - 1) % 3] += var.perturbation;
410                                if !var
411                                    .check_bounds(vector[(var.component.vec_index() - 1) % 3])
412                                    .1
413                                {
414                                    // Oops, bound was hit, go the other way
415                                    vector[(var.component.vec_index() - 1) % 3] -=
416                                        2.0 * var.perturbation;
417                                    opposed_pert = true;
418                                }
419                                this_mnvr.set_accel(vector).unwrap();
420                            }
421                            Vary::ThrustLevel => {
422                                this_mnvr.thrust_prct += var.perturbation;
423                                this_mnvr.thrust_prct = this_mnvr.thrust_prct.clamp(0.0, 1.0);
424                            }
425                            _ => unreachable!(),
426                        }
427                    } else {
428                        let mut state_correction = Vector6::<f64>::zeros();
429                        state_correction[var.component.vec_index()] += var.perturbation;
430                        // Now, let's apply the correction to the initial state
431                        if let Some(frame) = self.correction_frame {
432                            // The following will error if the frame is not local
433                            let dcm_vnc2inertial = this_xi
434                                .orbit
435                                .dcm_to_inertial(frame)
436                                .context(AstroPhysicsSnafu)
437                                .context(AstroSnafu)
438                                .unwrap()
439                                .rot_mat;
440
441                            let velocity_correction =
442                                dcm_vnc2inertial * state_correction.fixed_rows::<3>(3);
443                            this_xi.orbit.apply_dv_km_s(velocity_correction);
444                        } else {
445                            this_xi = xi + state_correction;
446                        }
447                    }
448
449                    let this_xf = if finite_burn_target {
450                        // Propagate normally until start of maneuver
451                        let pre_mnvr = this_prop
452                            .with(cur_xi, almanac.clone())
453                            .until_epoch(this_mnvr.start)
454                            .unwrap();
455                        // Add this maneuver to the dynamics, make sure that we don't over-step this maneuver
456                        let prop_opts = this_prop.opts;
457                        this_prop.set_max_step(this_mnvr.duration());
458                        this_prop.dynamics =
459                            this_prop.dynamics.with_guidance_law(Arc::new(this_mnvr));
460                        let post_mnvr = this_prop
461                            .with(
462                                pre_mnvr.with_guidance_mode(GuidanceMode::Thrust),
463                                almanac.clone(),
464                            )
465                            .until_epoch(this_mnvr.end)
466                            .unwrap();
467                        // Reset the propagator options to their previous configuration
468                        this_prop.opts = prop_opts;
469                        // And propagate until the achievement epoch
470                        this_prop
471                            .with(post_mnvr, almanac.clone())
472                            .until_epoch(achievement_epoch)
473                            .unwrap()
474                            .orbit
475                    } else {
476                        this_prop
477                            .with(this_xi, almanac.clone())
478                            .until_epoch(achievement_epoch)
479                            .unwrap()
480                            .orbit
481                    };
482
483                    let xf_dual_obj_frame = match &self.objective_frame {
484                        Some(frame) => {
485                            let orbit_obj_frame = almanac
486                                .transform_to(this_xf, *frame, None)
487                                .context(AstroAlmanacSnafu)
488                                .context(AstroSnafu)
489                                .unwrap();
490
491                            OrbitGrad::from(orbit_obj_frame)
492                        }
493                        None => OrbitGrad::from(this_xf),
494                    };
495
496                    let b_plane = if is_bplane_tgt {
497                        Some(BPlane::from_dual(xf_dual_obj_frame).unwrap())
498                    } else {
499                        None
500                    };
501
502                    let partial = if obj.parameter.is_b_plane() {
503                        match obj.parameter {
504                            StateParameter::BdotR() => b_plane.unwrap().b_r_km,
505                            StateParameter::BdotT() => b_plane.unwrap().b_t_km,
506                            StateParameter::BLTOF() => b_plane.unwrap().ltof_s,
507                            _ => unreachable!(),
508                        }
509                    } else if let StateParameter::Element(oe) = obj.parameter {
510                        xf_dual_obj_frame.partial_for(oe).unwrap()
511                    } else {
512                        unreachable!()
513                    };
514
515                    let this_achieved = partial.real();
516                    *jac_val = (this_achieved - achieved) / var.perturbation;
517                    if opposed_pert {
518                        // We opposed the perturbation to ensure we don't over step a min/max bound
519                        *jac_val = -*jac_val;
520                    }
521                });
522
523                for (j, var, jac_val) in &pert_calc {
524                    // If this is a thrust level, we oppose the value so that the correction can still be positive.
525                    jac[(i, *j)] = if var.component == Vary::ThrustLevel {
526                        -*jac_val
527                    } else {
528                        *jac_val
529                    };
530                }
531            }
532
533            if converged {
534                #[cfg(not(target_arch = "wasm32"))]
535                let conv_dur = Instant::now() - start_instant;
536                #[cfg(target_arch = "wasm32")]
537                let conv_dur = Duration::ZERO.into();
538                let mut corrected_state = xi_start;
539
540                let mut state_correction = Vector6::<f64>::zeros();
541                if !finite_burn_target {
542                    for (i, var) in self.variables.iter().enumerate() {
543                        state_correction[var.component.vec_index()] += total_correction[i];
544                    }
545                }
546                // Now, let's apply the correction to the initial state
547                if let Some(frame) = self.correction_frame {
548                    let dcm_vnc2inertial = corrected_state
549                        .orbit
550                        .dcm_to_inertial(frame)
551                        .context(AstroPhysicsSnafu)
552                        .context(AstroSnafu)?
553                        .rot_mat;
554
555                    let velocity_correction =
556                        dcm_vnc2inertial * state_correction.fixed_rows::<3>(3);
557                    corrected_state.orbit.apply_dv_km_s(velocity_correction);
558                } else {
559                    corrected_state.orbit.radius_km +=
560                        state_correction.fixed_rows::<3>(0).to_owned();
561                    corrected_state.orbit.velocity_km_s +=
562                        state_correction.fixed_rows::<3>(3).to_owned();
563                }
564
565                let sol = TargeterSolution {
566                    corrected_state,
567                    achieved_state: xi_start.with_orbit(xf),
568                    correction: total_correction,
569                    computation_dur: conv_dur,
570                    variables: self.variables,
571                    achieved_errors: err_vector,
572                    achieved_objectives: self.objectives,
573                    iterations: it,
574                };
575                // Log success as info
576                if it == 1 {
577                    info!("Targeter -- CONVERGED in 1 iteration");
578                } else {
579                    info!("Targeter -- CONVERGED in {it} iterations");
580                }
581                for obj in &objmsg {
582                    info!("{obj}");
583                }
584                return Ok(sol);
585            }
586
587            // We haven't converged yet, so let's build t
588            if (err_vector.norm() - prev_err_norm).abs() < 1e-10 {
589                return Err(TargetingError::CorrectionIneffective {
590                    prev_val: prev_err_norm,
591                    cur_val: err_vector.norm(),
592                    action: "Raphson targeter",
593                });
594            }
595            prev_err_norm = err_vector.norm();
596
597            debug!("Jacobian {jac}");
598
599            // Perform the pseudo-inverse if needed, else just inverse
600            let jac_inv = pseudo_inverse!(&jac)?;
601
602            debug!("Inverse Jacobian {jac_inv}");
603
604            let mut delta = jac_inv * err_vector;
605
606            debug!(
607                "Error vector (norm = {}): {}\nRaw correction: {}",
608                err_vector.norm(),
609                err_vector,
610                delta
611            );
612
613            // And finally apply it to the xi
614            let mut state_correction = Vector6::<f64>::zeros();
615            for (i, var) in self.variables.iter().enumerate() {
616                debug!(
617                    "Correction {:?}{} (element {}): {}",
618                    var.component,
619                    match self.correction_frame {
620                        Some(f) => format!(" in {f:?}"),
621                        None => String::new(),
622                    },
623                    i,
624                    delta[i]
625                );
626
627                let corr = delta[i];
628
629                if var.component.is_finite_burn() {
630                    // Modify the maneuver, but do not change the epochs of the maneuver unless the change is greater than one millisecond
631                    match var.component {
632                        Vary::Duration => {
633                            if corr.abs() > 1e-3 {
634                                // Check that we are within the bounds
635                                let init_duration_s =
636                                    (correction_epoch - achievement_epoch).to_seconds();
637                                let acceptable_corr = var.apply_bounds(init_duration_s).seconds();
638                                mnvr.end = mnvr.start + acceptable_corr;
639                            }
640                        }
641                        Vary::EndEpoch => {
642                            if corr.abs() > 1e-3 {
643                                // Check that we are within the bounds
644                                let total_end_corr =
645                                    (mnvr.end + corr.seconds() - achievement_epoch).to_seconds();
646                                let acceptable_corr = var.apply_bounds(total_end_corr).seconds();
647                                mnvr.end += acceptable_corr;
648                            }
649                        }
650                        Vary::StartEpoch => {
651                            if corr.abs() > 1e-3 {
652                                // Check that we are within the bounds
653                                let total_start_corr =
654                                    (mnvr.start + corr.seconds() - correction_epoch).to_seconds();
655                                let acceptable_corr = var.apply_bounds(total_start_corr).seconds();
656                                mnvr.end += acceptable_corr;
657
658                                mnvr.start += corr.seconds()
659                            }
660                        }
661                        Vary::MnvrAlpha | Vary::MnvrAlphaDot | Vary::MnvrAlphaDDot => {
662                            match mnvr.representation {
663                                MnvrRepr::Angles { azimuth, elevation } => {
664                                    let azimuth = azimuth
665                                        .add_val_in_order(corr, var.component.vec_index())
666                                        .unwrap();
667                                    mnvr.representation = MnvrRepr::Angles { azimuth, elevation };
668                                }
669                                _ => unreachable!(),
670                            };
671                        }
672                        Vary::MnvrDelta | Vary::MnvrDeltaDot | Vary::MnvrDeltaDDot => {
673                            match mnvr.representation {
674                                MnvrRepr::Angles { azimuth, elevation } => {
675                                    let elevation = elevation
676                                        .add_val_in_order(corr, var.component.vec_index())
677                                        .unwrap();
678                                    mnvr.representation = MnvrRepr::Angles { azimuth, elevation };
679                                }
680                                _ => unreachable!(),
681                            };
682                        }
683                        Vary::ThrustX | Vary::ThrustY | Vary::ThrustZ => {
684                            let mut vector = mnvr.direction();
685                            vector[var.component.vec_index()] += corr;
686                            var.ensure_bounds(&mut vector[var.component.vec_index()]);
687                            mnvr.set_direction(vector).context(GuidanceSnafu)?;
688                        }
689                        Vary::ThrustRateX | Vary::ThrustRateY | Vary::ThrustRateZ => {
690                            let mut vector = mnvr.rate();
691                            let idx = (var.component.vec_index() - 1) % 3;
692                            vector[idx] += corr;
693                            var.ensure_bounds(&mut vector[idx]);
694                            mnvr.set_rate(vector).context(GuidanceSnafu)?;
695                        }
696                        Vary::ThrustAccelX | Vary::ThrustAccelY | Vary::ThrustAccelZ => {
697                            let mut vector = mnvr.accel();
698                            let idx = (var.component.vec_index() - 1) % 3;
699                            vector[idx] += corr;
700                            var.ensure_bounds(&mut vector[idx]);
701                            mnvr.set_accel(vector).context(GuidanceSnafu)?;
702                        }
703                        Vary::ThrustLevel => {
704                            mnvr.thrust_prct += corr;
705                            var.ensure_bounds(&mut mnvr.thrust_prct);
706                        }
707                        _ => unreachable!(),
708                    }
709                } else {
710                    // Choose the minimum step between the provided max step and the correction.
711                    if delta[i].abs() > var.max_step.abs() {
712                        delta[i] = var.max_step.abs() * delta[i].signum();
713                    } else if delta[i] > var.max_value {
714                        delta[i] = var.max_value;
715                    } else if delta[i] < var.min_value {
716                        delta[i] = var.min_value;
717                    }
718                    state_correction[var.component.vec_index()] += delta[i];
719                }
720            }
721
722            // Now, let's apply the correction to the initial state
723            if let Some(frame) = self.correction_frame {
724                let dcm_vnc2inertial = xi
725                    .orbit
726                    .dcm_to_inertial(frame)
727                    .context(AstroPhysicsSnafu)
728                    .context(AstroSnafu)?
729                    .rot_mat;
730
731                let velocity_correction = dcm_vnc2inertial * state_correction.fixed_rows::<3>(3);
732                xi.orbit.apply_dv_km_s(velocity_correction);
733            } else {
734                xi = xi + state_correction;
735            }
736            total_correction += delta;
737            debug!("Total correction: {total_correction:e}");
738
739            // Log progress to debug
740            info!("Targeter -- Iteration #{it} -- {achievement_epoch}");
741            for obj in &objmsg {
742                info!("{obj}");
743            }
744        }
745
746        Err(TargetingError::TooManyIterations)
747    }
748}