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