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