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