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