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