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