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