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