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::{AstroSnafu, GuidanceSnafu, UnderdeterminedProblemSnafu, prelude::*};
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::{ResultExt, ensure};
35#[cfg(not(target_arch = "wasm32"))]
36use std::time::Instant;
37
38impl<const V: usize, const O: usize> Targeter<'_, V, O> {
39 #[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 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 let mut state_correction = Vector6::<f64>::zeros();
72
73 let mut total_correction = SVector::<f64, V>::zeros();
75
76 let mut mnvr = Maneuver {
77 start: correction_epoch,
78 end: achievement_epoch,
79 thrust_prct: 1.0,
80 representation: MnvrRepr::Angles {
81 azimuth: CommonPolynomial::Quadratic {
82 a: 0.0,
83 b: 0.0,
84 c: 0.0,
85 },
86 elevation: CommonPolynomial::Quadratic {
87 a: 0.0,
88 b: 0.0,
89 c: 0.0,
90 },
91 },
92 frame: LocalFrame::RCN,
93 };
94
95 let mut finite_burn_target = false;
96
97 for (i, var) in self.variables.iter().enumerate() {
100 var.valid()?;
102 if let Some(correction_frame) = self.correction_frame
104 && var.component.vec_index() < 3
105 {
106 let msg = format!(
108 "Variable is in frame {correction_frame:?} but that frame cannot be used for a {:?} correction",
109 var.component
110 );
111 error!("{msg}");
112 return Err(TargetingError::FrameError { msg });
113 }
114
115 if var.component.is_finite_burn() {
117 if xi_start.thruster.is_none() {
118 return Err(TargetingError::GuidanceError {
119 source: GuidanceError::NoThrustersDefined,
120 });
121 }
122
123 finite_burn_target = true;
124 match var.component {
126 Vary::Duration => mnvr.end = mnvr.start + var.init_guess.seconds(),
127 Vary::EndEpoch => mnvr.end += var.init_guess.seconds(),
128 Vary::StartEpoch => mnvr.start += var.init_guess.seconds(),
129 Vary::MnvrAlpha | Vary::MnvrAlphaDot | Vary::MnvrAlphaDDot => {
130 match mnvr.representation {
131 MnvrRepr::Angles { azimuth, elevation } => {
132 let azimuth = azimuth
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::MnvrDelta | Vary::MnvrDeltaDot | Vary::MnvrDeltaDDot => {
141 match mnvr.representation {
142 MnvrRepr::Angles { azimuth, elevation } => {
143 let elevation = elevation
144 .add_val_in_order(var.init_guess, var.component.vec_index())
145 .unwrap();
146 mnvr.representation = MnvrRepr::Angles { azimuth, elevation };
147 }
148 _ => unreachable!(),
149 };
150 }
151 Vary::ThrustX | Vary::ThrustY | Vary::ThrustZ => {
152 let mut vector = mnvr.direction();
153 vector[var.component.vec_index()] += var.perturbation;
154 mnvr.set_direction(vector).context(GuidanceSnafu)?;
155 }
156 Vary::ThrustRateX | Vary::ThrustRateY | Vary::ThrustRateZ => {
157 let mut vector = mnvr.rate();
158 vector[(var.component.vec_index() - 1) % 3] += var.perturbation;
159 mnvr.set_rate(vector).context(GuidanceSnafu)?;
160 }
161 Vary::ThrustAccelX | Vary::ThrustAccelY | Vary::ThrustAccelZ => {
162 let mut vector = mnvr.accel();
163 vector[(var.component.vec_index() - 1) % 3] += var.perturbation;
164 mnvr.set_accel(vector).context(GuidanceSnafu)?;
165 }
166 Vary::ThrustLevel => {
167 mnvr.thrust_prct += var.perturbation;
168 mnvr.thrust_prct = mnvr.thrust_prct.clamp(0.0, 1.0);
169 }
170 _ => unreachable!(),
171 }
172 info!("Initial maneuver guess: {mnvr}");
173 } else {
174 state_correction[var.component.vec_index()] += var.init_guess;
175 }
176
177 total_correction[i] += var.init_guess;
178 }
179
180 if !finite_burn_target {
182 if let Some(frame) = self.correction_frame {
183 let dcm_vnc2inertial = xi
184 .orbit
185 .dcm_to_inertial(frame)
186 .context(AstroPhysicsSnafu)
187 .context(AstroSnafu)?
188 .rot_mat;
189
190 let velocity_correction = dcm_vnc2inertial * state_correction.fixed_rows::<3>(3);
191 xi.orbit.apply_dv_km_s(velocity_correction);
192 } else {
193 xi.orbit.radius_km += state_correction.fixed_rows::<3>(0).to_owned();
194 xi.orbit.velocity_km_s += state_correction.fixed_rows::<3>(3).to_owned();
195 }
196 }
197
198 let mut prev_err_norm = f64::INFINITY;
199
200 let max_obj_val = self
203 .objectives
204 .iter()
205 .map(|obj| {
206 obj.desired_value.abs().ceil() as i32
207 * 10_i32.pow(obj.tolerance.abs().log10().ceil() as u32)
208 })
209 .max()
210 .unwrap();
211
212 let max_obj_tol = self
213 .objectives
214 .iter()
215 .map(|obj| obj.tolerance.log10().abs().ceil() as usize)
216 .max()
217 .unwrap();
218
219 let width = f64::from(max_obj_val).log10() as usize + 2 + max_obj_tol;
220
221 #[cfg(not(target_arch = "wasm32"))]
222 let start_instant = Instant::now();
223
224 for it in 0..=self.iterations {
225 let cur_xi = xi;
227
228 let xf = if finite_burn_target {
230 info!("#{it} {mnvr}");
231 let mut prop = self.prop.clone();
232 let prop_opts = prop.opts;
233 let pre_mnvr = prop
234 .with(cur_xi, almanac.clone())
235 .until_epoch(mnvr.start)
236 .context(PropSnafu)?;
237 prop.dynamics = prop.dynamics.with_guidance_law(Arc::new(mnvr));
238 prop.set_max_step(mnvr.duration());
239 let post_mnvr = prop
240 .with(
241 pre_mnvr.with_guidance_mode(GuidanceMode::Thrust),
242 almanac.clone(),
243 )
244 .until_epoch(mnvr.end)
245 .context(PropSnafu)?;
246 prop.opts = prop_opts;
248 prop.with(post_mnvr, almanac.clone())
250 .until_epoch(achievement_epoch)
251 .context(PropSnafu)?
252 .orbit
253 } else {
254 self.prop
255 .with(cur_xi, almanac.clone())
256 .until_epoch(achievement_epoch)
257 .context(PropSnafu)?
258 .orbit
259 };
260
261 let xf_dual_obj_frame = match &self.objective_frame {
262 Some(frame) => {
263 let orbit_obj_frame = almanac
264 .transform_to(xf, *frame, None)
265 .context(AstroAlmanacSnafu)
266 .context(AstroSnafu)?;
267
268 OrbitGrad::from(orbit_obj_frame)
269 }
270 None => OrbitGrad::from(xf),
271 };
272
273 let mut err_vector = SVector::<f64, O>::zeros();
275 let mut converged = true;
276
277 let b_plane = if is_bplane_tgt {
279 Some(BPlane::from_dual(xf_dual_obj_frame).context(AstroSnafu)?)
280 } else {
281 None
282 };
283
284 let mut objmsg = Vec::with_capacity(self.objectives.len());
286
287 let mut jac = SMatrix::<f64, O, V>::zeros();
290
291 for (i, obj) in self.objectives.iter().enumerate() {
292 let partial = if obj.parameter.is_b_plane() {
293 match obj.parameter {
294 StateParameter::BdotR() => b_plane.unwrap().b_r_km,
295 StateParameter::BdotT() => b_plane.unwrap().b_t_km,
296 StateParameter::BLTOF() => b_plane.unwrap().ltof_s,
297 _ => unreachable!(),
298 }
299 } else if let StateParameter::Element(oe) = obj.parameter {
300 xf_dual_obj_frame
301 .partial_for(oe)
302 .context(AstroPhysicsSnafu)
303 .context(AstroSnafu)?
304 } else {
305 unreachable!()
306 };
307
308 let achieved = partial.real();
309
310 let (ok, param_err) = obj.assess_value(achieved);
311 if !ok {
312 converged = false;
313 }
314 err_vector[i] = param_err;
315
316 objmsg.push(format!(
317 "\t{:?}: achieved = {:>width$.prec$}\t desired = {:>width$.prec$}\t scaled error = {:>width$.prec$}",
318 obj.parameter,
319 achieved,
320 obj.desired_value,
321 param_err, width=width, prec=max_obj_tol
322 ));
323
324 let mut pert_calc: Vec<_> = self
325 .variables
326 .iter()
327 .enumerate()
328 .map(|(j, var)| (j, var, 0.0_f64))
329 .collect();
330
331 pert_calc.par_iter_mut().for_each(|(_, var, jac_val)| {
332 let mut this_xi = xi;
333
334 let mut this_prop = self.prop.clone();
335 let mut this_mnvr = mnvr;
336
337 let mut opposed_pert = false;
338
339 if var.component.is_finite_burn() {
340 let pert = var.perturbation;
342 match var.component {
344 Vary::Duration => {
345 if pert.abs() > 1e-3 {
346 this_mnvr.end = mnvr.start + pert.seconds()
347 }
348 }
349 Vary::EndEpoch => {
350 if pert.abs() > 1e-3 {
351 this_mnvr.end = mnvr.end + pert.seconds()
352 }
353 }
354 Vary::StartEpoch => {
355 if pert.abs() > 1e-3 {
356 this_mnvr.start = mnvr.start + pert.seconds()
357 }
358 }
359 Vary::MnvrAlpha | Vary::MnvrAlphaDot | Vary::MnvrAlphaDDot => {
360 match mnvr.representation {
361 MnvrRepr::Angles { azimuth, elevation } => {
362 let azimuth = azimuth
363 .add_val_in_order(pert, var.component.vec_index())
364 .unwrap();
365 this_mnvr.representation =
366 MnvrRepr::Angles { azimuth, elevation };
367 }
368 _ => unreachable!(),
369 };
370 }
371 Vary::MnvrDelta | Vary::MnvrDeltaDot | Vary::MnvrDeltaDDot => {
372 match mnvr.representation {
373 MnvrRepr::Angles { azimuth, elevation } => {
374 let elevation = elevation
375 .add_val_in_order(pert, var.component.vec_index())
376 .unwrap();
377 this_mnvr.representation =
378 MnvrRepr::Angles { azimuth, elevation };
379 }
380 _ => unreachable!(),
381 };
382 }
383 Vary::ThrustX | Vary::ThrustY | Vary::ThrustZ => {
384 let mut vector = this_mnvr.direction();
385 vector[var.component.vec_index()] += var.perturbation;
386 if !var.check_bounds(vector[var.component.vec_index()]).1 {
387 vector[var.component.vec_index()] -= 2.0 * var.perturbation;
389 opposed_pert = true;
390 }
391 this_mnvr.set_direction(vector).unwrap();
392 }
393 Vary::ThrustRateX | Vary::ThrustRateY | Vary::ThrustRateZ => {
394 let mut vector = this_mnvr.rate();
395 vector[(var.component.vec_index() - 1) % 3] += var.perturbation;
396 if !var
397 .check_bounds(vector[(var.component.vec_index() - 1) % 3])
398 .1
399 {
400 vector[(var.component.vec_index() - 1) % 3] -=
402 2.0 * var.perturbation;
403 opposed_pert = true;
404 }
405 this_mnvr.set_rate(vector).unwrap();
406 }
407 Vary::ThrustAccelX | Vary::ThrustAccelY | Vary::ThrustAccelZ => {
408 let mut vector = this_mnvr.accel();
409 vector[(var.component.vec_index() - 1) % 3] += var.perturbation;
410 if !var
411 .check_bounds(vector[(var.component.vec_index() - 1) % 3])
412 .1
413 {
414 vector[(var.component.vec_index() - 1) % 3] -=
416 2.0 * var.perturbation;
417 opposed_pert = true;
418 }
419 this_mnvr.set_accel(vector).unwrap();
420 }
421 Vary::ThrustLevel => {
422 this_mnvr.thrust_prct += var.perturbation;
423 this_mnvr.thrust_prct = this_mnvr.thrust_prct.clamp(0.0, 1.0);
424 }
425 _ => unreachable!(),
426 }
427 } else {
428 let mut state_correction = Vector6::<f64>::zeros();
429 state_correction[var.component.vec_index()] += var.perturbation;
430 if let Some(frame) = self.correction_frame {
432 let dcm_vnc2inertial = this_xi
434 .orbit
435 .dcm_to_inertial(frame)
436 .context(AstroPhysicsSnafu)
437 .context(AstroSnafu)
438 .unwrap()
439 .rot_mat;
440
441 let velocity_correction =
442 dcm_vnc2inertial * state_correction.fixed_rows::<3>(3);
443 this_xi.orbit.apply_dv_km_s(velocity_correction);
444 } else {
445 this_xi = xi + state_correction;
446 }
447 }
448
449 let this_xf = if finite_burn_target {
450 let pre_mnvr = this_prop
452 .with(cur_xi, almanac.clone())
453 .until_epoch(this_mnvr.start)
454 .unwrap();
455 let prop_opts = this_prop.opts;
457 this_prop.set_max_step(this_mnvr.duration());
458 this_prop.dynamics =
459 this_prop.dynamics.with_guidance_law(Arc::new(this_mnvr));
460 let post_mnvr = this_prop
461 .with(
462 pre_mnvr.with_guidance_mode(GuidanceMode::Thrust),
463 almanac.clone(),
464 )
465 .until_epoch(this_mnvr.end)
466 .unwrap();
467 this_prop.opts = prop_opts;
469 this_prop
471 .with(post_mnvr, almanac.clone())
472 .until_epoch(achievement_epoch)
473 .unwrap()
474 .orbit
475 } else {
476 this_prop
477 .with(this_xi, almanac.clone())
478 .until_epoch(achievement_epoch)
479 .unwrap()
480 .orbit
481 };
482
483 let xf_dual_obj_frame = match &self.objective_frame {
484 Some(frame) => {
485 let orbit_obj_frame = almanac
486 .transform_to(this_xf, *frame, None)
487 .context(AstroAlmanacSnafu)
488 .context(AstroSnafu)
489 .unwrap();
490
491 OrbitGrad::from(orbit_obj_frame)
492 }
493 None => OrbitGrad::from(this_xf),
494 };
495
496 let b_plane = if is_bplane_tgt {
497 Some(BPlane::from_dual(xf_dual_obj_frame).unwrap())
498 } else {
499 None
500 };
501
502 let partial = if obj.parameter.is_b_plane() {
503 match obj.parameter {
504 StateParameter::BdotR() => b_plane.unwrap().b_r_km,
505 StateParameter::BdotT() => b_plane.unwrap().b_t_km,
506 StateParameter::BLTOF() => b_plane.unwrap().ltof_s,
507 _ => unreachable!(),
508 }
509 } else if let StateParameter::Element(oe) = obj.parameter {
510 xf_dual_obj_frame.partial_for(oe).unwrap()
511 } else {
512 unreachable!()
513 };
514
515 let this_achieved = partial.real();
516 *jac_val = (this_achieved - achieved) / var.perturbation;
517 if opposed_pert {
518 *jac_val = -*jac_val;
520 }
521 });
522
523 for (j, var, jac_val) in &pert_calc {
524 jac[(i, *j)] = if var.component == Vary::ThrustLevel {
526 -*jac_val
527 } else {
528 *jac_val
529 };
530 }
531 }
532
533 if converged {
534 #[cfg(not(target_arch = "wasm32"))]
535 let conv_dur = Instant::now() - start_instant;
536 #[cfg(target_arch = "wasm32")]
537 let conv_dur = Duration::ZERO.into();
538 let mut corrected_state = xi_start;
539
540 let mut state_correction = Vector6::<f64>::zeros();
541 if !finite_burn_target {
542 for (i, var) in self.variables.iter().enumerate() {
543 state_correction[var.component.vec_index()] += total_correction[i];
544 }
545 }
546 if let Some(frame) = self.correction_frame {
548 let dcm_vnc2inertial = corrected_state
549 .orbit
550 .dcm_to_inertial(frame)
551 .context(AstroPhysicsSnafu)
552 .context(AstroSnafu)?
553 .rot_mat;
554
555 let velocity_correction =
556 dcm_vnc2inertial * state_correction.fixed_rows::<3>(3);
557 corrected_state.orbit.apply_dv_km_s(velocity_correction);
558 } else {
559 corrected_state.orbit.radius_km +=
560 state_correction.fixed_rows::<3>(0).to_owned();
561 corrected_state.orbit.velocity_km_s +=
562 state_correction.fixed_rows::<3>(3).to_owned();
563 }
564
565 let sol = TargeterSolution {
566 corrected_state,
567 achieved_state: xi_start.with_orbit(xf),
568 correction: total_correction,
569 computation_dur: conv_dur,
570 variables: self.variables,
571 achieved_errors: err_vector,
572 achieved_objectives: self.objectives,
573 iterations: it,
574 };
575 if it == 1 {
577 info!("Targeter -- CONVERGED in 1 iteration");
578 } else {
579 info!("Targeter -- CONVERGED in {it} iterations");
580 }
581 for obj in &objmsg {
582 info!("{obj}");
583 }
584 return Ok(sol);
585 }
586
587 if (err_vector.norm() - prev_err_norm).abs() < 1e-10 {
589 return Err(TargetingError::CorrectionIneffective {
590 prev_val: prev_err_norm,
591 cur_val: err_vector.norm(),
592 action: "Raphson targeter",
593 });
594 }
595 prev_err_norm = err_vector.norm();
596
597 debug!("Jacobian {jac}");
598
599 let jac_inv = pseudo_inverse!(&jac)?;
601
602 debug!("Inverse Jacobian {jac_inv}");
603
604 let mut delta = jac_inv * err_vector;
605
606 debug!(
607 "Error vector (norm = {}): {}\nRaw correction: {}",
608 err_vector.norm(),
609 err_vector,
610 delta
611 );
612
613 let mut state_correction = Vector6::<f64>::zeros();
615 for (i, var) in self.variables.iter().enumerate() {
616 debug!(
617 "Correction {:?}{} (element {}): {}",
618 var.component,
619 match self.correction_frame {
620 Some(f) => format!(" in {f:?}"),
621 None => String::new(),
622 },
623 i,
624 delta[i]
625 );
626
627 let corr = delta[i];
628
629 if var.component.is_finite_burn() {
630 match var.component {
632 Vary::Duration => {
633 if corr.abs() > 1e-3 {
634 let init_duration_s =
636 (correction_epoch - achievement_epoch).to_seconds();
637 let acceptable_corr = var.apply_bounds(init_duration_s).seconds();
638 mnvr.end = mnvr.start + acceptable_corr;
639 }
640 }
641 Vary::EndEpoch => {
642 if corr.abs() > 1e-3 {
643 let total_end_corr =
645 (mnvr.end + corr.seconds() - achievement_epoch).to_seconds();
646 let acceptable_corr = var.apply_bounds(total_end_corr).seconds();
647 mnvr.end += acceptable_corr;
648 }
649 }
650 Vary::StartEpoch => {
651 if corr.abs() > 1e-3 {
652 let total_start_corr =
654 (mnvr.start + corr.seconds() - correction_epoch).to_seconds();
655 let acceptable_corr = var.apply_bounds(total_start_corr).seconds();
656 mnvr.end += acceptable_corr;
657
658 mnvr.start += corr.seconds()
659 }
660 }
661 Vary::MnvrAlpha | Vary::MnvrAlphaDot | Vary::MnvrAlphaDDot => {
662 match mnvr.representation {
663 MnvrRepr::Angles { azimuth, elevation } => {
664 let azimuth = azimuth
665 .add_val_in_order(corr, var.component.vec_index())
666 .unwrap();
667 mnvr.representation = MnvrRepr::Angles { azimuth, elevation };
668 }
669 _ => unreachable!(),
670 };
671 }
672 Vary::MnvrDelta | Vary::MnvrDeltaDot | Vary::MnvrDeltaDDot => {
673 match mnvr.representation {
674 MnvrRepr::Angles { azimuth, elevation } => {
675 let elevation = elevation
676 .add_val_in_order(corr, var.component.vec_index())
677 .unwrap();
678 mnvr.representation = MnvrRepr::Angles { azimuth, elevation };
679 }
680 _ => unreachable!(),
681 };
682 }
683 Vary::ThrustX | Vary::ThrustY | Vary::ThrustZ => {
684 let mut vector = mnvr.direction();
685 vector[var.component.vec_index()] += corr;
686 var.ensure_bounds(&mut vector[var.component.vec_index()]);
687 mnvr.set_direction(vector).context(GuidanceSnafu)?;
688 }
689 Vary::ThrustRateX | Vary::ThrustRateY | Vary::ThrustRateZ => {
690 let mut vector = mnvr.rate();
691 let idx = (var.component.vec_index() - 1) % 3;
692 vector[idx] += corr;
693 var.ensure_bounds(&mut vector[idx]);
694 mnvr.set_rate(vector).context(GuidanceSnafu)?;
695 }
696 Vary::ThrustAccelX | Vary::ThrustAccelY | Vary::ThrustAccelZ => {
697 let mut vector = mnvr.accel();
698 let idx = (var.component.vec_index() - 1) % 3;
699 vector[idx] += corr;
700 var.ensure_bounds(&mut vector[idx]);
701 mnvr.set_accel(vector).context(GuidanceSnafu)?;
702 }
703 Vary::ThrustLevel => {
704 mnvr.thrust_prct += corr;
705 var.ensure_bounds(&mut mnvr.thrust_prct);
706 }
707 _ => unreachable!(),
708 }
709 } else {
710 if delta[i].abs() > var.max_step.abs() {
712 delta[i] = var.max_step.abs() * delta[i].signum();
713 } else if delta[i] > var.max_value {
714 delta[i] = var.max_value;
715 } else if delta[i] < var.min_value {
716 delta[i] = var.min_value;
717 }
718 state_correction[var.component.vec_index()] += delta[i];
719 }
720 }
721
722 if let Some(frame) = self.correction_frame {
724 let dcm_vnc2inertial = xi
725 .orbit
726 .dcm_to_inertial(frame)
727 .context(AstroPhysicsSnafu)
728 .context(AstroSnafu)?
729 .rot_mat;
730
731 let velocity_correction = dcm_vnc2inertial * state_correction.fixed_rows::<3>(3);
732 xi.orbit.apply_dv_km_s(velocity_correction);
733 } else {
734 xi = xi + state_correction;
735 }
736 total_correction += delta;
737 debug!("Total correction: {total_correction:e}");
738
739 info!("Targeter -- Iteration #{it} -- {achievement_epoch}");
741 for obj in &objmsg {
742 info!("{obj}");
743 }
744 }
745
746 Err(TargetingError::TooManyIterations)
747 }
748}