1use 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 #[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 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 let mut state_correction = Vector6::<f64>::zeros();
71
72 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 for (i, var) in self.variables.iter().enumerate() {
90 var.valid()?;
92 if self.correction_frame.is_some() && var.component.vec_index() < 3 {
94 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 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 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 if let Some(frame) = self.correction_frame {
166 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 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 let cur_xi = xi;
214
215 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 prop.opts = prop_opts;
235 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 let mut err_vector = SVector::<f64, O>::zeros();
262 let mut converged = true;
263
264 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 let mut objmsg = Vec::with_capacity(self.objectives.len());
273
274 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 let pert = var.perturbation;
326 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 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 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 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 if let Some(frame) = self.correction_frame {
416 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 let pre_mnvr = this_prop
435 .with(cur_xi, almanac.clone())
436 .until_epoch(this_mnvr.start)
437 .unwrap();
438 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 this_prop.opts = prop_opts;
452 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 *jac_val = -*jac_val;
501 }
502 });
503
504 for (j, var, jac_val) in &pert_calc {
505 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 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 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 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 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 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 match var.component {
613 Vary::Duration => {
614 if corr.abs() > 1e-3 {
615 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 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 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 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 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 info!("Targeter -- Iteration #{it} -- {achievement_epoch}");
721 for obj in &objmsg {
722 info!("{obj}");
723 }
724 }
725
726 Err(TargetingError::TooManyIterations)
727 }
728}