1use crate::error::ensure_finite;
15use crate::integration::{
16 DifferentiableDynamicsModel, Dop853, DynamicsModel, InitialValueProblem, IntegrationStats,
17 IntegratorOptions, Propagation, SensitivityProblem, SensitivityPropagation, Termination,
18};
19use crate::{PykepError, Result};
20
21#[derive(Clone, Debug, PartialEq)]
28pub struct ControlSchedule<const C: usize> {
29 boundaries: Vec<f64>,
30 controls: Vec<[f64; C]>,
31}
32
33impl<const C: usize> ControlSchedule<C> {
34 pub fn new(boundaries: Vec<f64>, controls: Vec<[f64; C]>) -> Result<Self> {
42 if C == 0 {
43 return Err(PykepError::InvalidInput {
44 parameter: "control_dimension",
45 reason: "must be greater than zero".into(),
46 });
47 }
48 if controls.is_empty() {
49 return Err(PykepError::InvalidInput {
50 parameter: "controls",
51 reason: "must contain at least one segment".into(),
52 });
53 }
54 if boundaries.len() != controls.len() + 1 {
55 return Err(PykepError::DimensionMismatch {
56 expected: controls.len() + 1,
57 actual: boundaries.len(),
58 });
59 }
60 for &boundary in &boundaries {
61 ensure_finite("boundaries", boundary)?;
62 }
63 if boundaries.windows(2).any(|pair| pair[0] >= pair[1]) {
64 return Err(PykepError::InvalidInput {
65 parameter: "boundaries",
66 reason: "must be strictly increasing".into(),
67 });
68 }
69 for control in &controls {
70 for &value in control {
71 ensure_finite("controls", value)?;
72 }
73 }
74 Ok(Self {
75 boundaries,
76 controls,
77 })
78 }
79
80 pub fn boundaries(&self) -> &[f64] {
82 &self.boundaries
83 }
84
85 pub fn controls(&self) -> &[[f64; C]] {
87 &self.controls
88 }
89
90 pub fn len(&self) -> usize {
92 self.controls.len()
93 }
94
95 pub fn is_empty(&self) -> bool {
100 self.controls.is_empty()
101 }
102
103 pub fn initial_time(&self) -> f64 {
105 self.boundaries[0]
106 }
107
108 pub fn final_time(&self) -> f64 {
110 self.boundaries[self.boundaries.len() - 1]
111 }
112
113 pub fn control_at(&self, time: f64) -> Result<[f64; C]> {
123 ensure_finite("time", time)?;
124 if time < self.initial_time() || time > self.final_time() {
125 return Err(PykepError::InvalidInput {
126 parameter: "time",
127 reason: "must lie inside the control schedule".into(),
128 });
129 }
130 if time == self.final_time() {
131 return Ok(self.controls[self.controls.len() - 1]);
132 }
133 let index = self
134 .boundaries
135 .partition_point(|boundary| *boundary <= time)
136 - 1;
137 Ok(self.controls[index])
138 }
139}
140
141#[derive(Clone, Debug, PartialEq)]
147pub struct ZohSensitivitySeeds<const N: usize, const C: usize, const K: usize, const W: usize> {
148 pub initial_state: [[f64; W]; N],
150 pub segment_controls: Vec<[[f64; W]; C]>,
152 pub constants: [[f64; W]; K],
154}
155
156pub trait ZeroOrderHoldModel<const N: usize, const C: usize, const K: usize, const P: usize>:
158 DifferentiableDynamicsModel<N, P>
159{
160 fn parameters(control: [f64; C], constants: [f64; K]) -> [f64; P];
163
164 fn parameter_seeds<const W: usize>(
167 control: [[f64; W]; C],
168 constants: [[f64; W]; K],
169 ) -> [[f64; W]; P];
170}
171
172pub fn propagate_schedule<M, const N: usize, const C: usize, const K: usize, const P: usize>(
182 model: &M,
183 schedule: &ControlSchedule<C>,
184 initial_state: [f64; N],
185 constants: [f64; K],
186 options: IntegratorOptions,
187) -> Result<Propagation<N>>
188where
189 M: ZeroOrderHoldModel<N, C, K, P>,
190{
191 propagate_schedule_direction(model, schedule, initial_state, constants, options, false)
192}
193
194pub fn propagate_schedule_backward<
200 M,
201 const N: usize,
202 const C: usize,
203 const K: usize,
204 const P: usize,
205>(
206 model: &M,
207 schedule: &ControlSchedule<C>,
208 final_state: [f64; N],
209 constants: [f64; K],
210 options: IntegratorOptions,
211) -> Result<Propagation<N>>
212where
213 M: ZeroOrderHoldModel<N, C, K, P>,
214{
215 propagate_schedule_direction(model, schedule, final_state, constants, options, true)
216}
217
218pub fn propagate_schedule_with_sensitivities<
228 M,
229 const N: usize,
230 const C: usize,
231 const K: usize,
232 const P: usize,
233 const W: usize,
234>(
235 model: &M,
236 schedule: &ControlSchedule<C>,
237 initial_state: [f64; N],
238 constants: [f64; K],
239 seeds: &ZohSensitivitySeeds<N, C, K, W>,
240 options: IntegratorOptions,
241) -> Result<SensitivityPropagation<N, W>>
242where
243 M: ZeroOrderHoldModel<N, C, K, P>,
244{
245 propagate_schedule_sensitivities_direction(
246 model,
247 schedule,
248 initial_state,
249 constants,
250 seeds,
251 options,
252 false,
253 )
254}
255
256pub fn propagate_schedule_with_sensitivities_backward<
263 M,
264 const N: usize,
265 const C: usize,
266 const K: usize,
267 const P: usize,
268 const W: usize,
269>(
270 model: &M,
271 schedule: &ControlSchedule<C>,
272 final_state: [f64; N],
273 constants: [f64; K],
274 seeds: &ZohSensitivitySeeds<N, C, K, W>,
275 options: IntegratorOptions,
276) -> Result<SensitivityPropagation<N, W>>
277where
278 M: ZeroOrderHoldModel<N, C, K, P>,
279{
280 propagate_schedule_sensitivities_direction(
281 model,
282 schedule,
283 final_state,
284 constants,
285 seeds,
286 options,
287 true,
288 )
289}
290
291fn propagate_schedule_direction<M, const N: usize, const C: usize, const K: usize, const P: usize>(
292 model: &M,
293 schedule: &ControlSchedule<C>,
294 initial_state: [f64; N],
295 constants: [f64; K],
296 options: IntegratorOptions,
297 backward: bool,
298) -> Result<Propagation<N>>
299where
300 M: ZeroOrderHoldModel<N, C, K, P>,
301{
302 let mut state = initial_state;
303 let mut statistics = IntegrationStats::default();
304 if backward {
305 for index in (0..schedule.len()).rev() {
306 let result = Dop853.propagate(
307 model,
308 InitialValueProblem::new(
309 schedule.boundaries[index + 1],
310 state,
311 schedule.boundaries[index],
312 M::parameters(schedule.controls[index], constants),
313 ),
314 options,
315 )?;
316 state = result.state;
317 add_statistics(&mut statistics, result.stats);
318 }
319 } else {
320 for index in 0..schedule.len() {
321 let result = Dop853.propagate(
322 model,
323 InitialValueProblem::new(
324 schedule.boundaries[index],
325 state,
326 schedule.boundaries[index + 1],
327 M::parameters(schedule.controls[index], constants),
328 ),
329 options,
330 )?;
331 state = result.state;
332 add_statistics(&mut statistics, result.stats);
333 }
334 }
335 Ok(Propagation {
336 time: if backward {
337 schedule.initial_time()
338 } else {
339 schedule.final_time()
340 },
341 state,
342 stats: statistics,
343 termination: Termination::FinalTime,
344 })
345}
346
347#[allow(clippy::too_many_arguments)]
348fn propagate_schedule_sensitivities_direction<
349 M,
350 const N: usize,
351 const C: usize,
352 const K: usize,
353 const P: usize,
354 const W: usize,
355>(
356 model: &M,
357 schedule: &ControlSchedule<C>,
358 initial_state: [f64; N],
359 constants: [f64; K],
360 seeds: &ZohSensitivitySeeds<N, C, K, W>,
361 options: IntegratorOptions,
362 backward: bool,
363) -> Result<SensitivityPropagation<N, W>>
364where
365 M: ZeroOrderHoldModel<N, C, K, P>,
366{
367 if seeds.segment_controls.len() != schedule.len() {
368 return Err(PykepError::DimensionMismatch {
369 expected: schedule.len(),
370 actual: seeds.segment_controls.len(),
371 });
372 }
373 let mut state = initial_state;
374 let mut sensitivities = seeds.initial_state;
375 let mut statistics = IntegrationStats::default();
376 let mut run_segment = |index: usize, start: f64, end: f64| -> Result<()> {
377 let result = Dop853.propagate_with_sensitivities(
378 model,
379 SensitivityProblem {
380 nominal: InitialValueProblem::new(
381 start,
382 state,
383 end,
384 M::parameters(schedule.controls[index], constants),
385 ),
386 initial_sensitivities: sensitivities,
387 parameter_seeds: M::parameter_seeds(seeds.segment_controls[index], seeds.constants),
388 },
389 options,
390 )?;
391 state = result.state;
392 sensitivities = result.sensitivities;
393 add_statistics(&mut statistics, result.stats);
394 Ok(())
395 };
396 if backward {
397 for index in (0..schedule.len()).rev() {
398 run_segment(
399 index,
400 schedule.boundaries[index + 1],
401 schedule.boundaries[index],
402 )?;
403 }
404 } else {
405 for index in 0..schedule.len() {
406 run_segment(
407 index,
408 schedule.boundaries[index],
409 schedule.boundaries[index + 1],
410 )?;
411 }
412 }
413 Ok(SensitivityPropagation {
414 time: if backward {
415 schedule.initial_time()
416 } else {
417 schedule.final_time()
418 },
419 state,
420 sensitivities,
421 stats: statistics,
422 })
423}
424
425fn add_statistics(total: &mut IntegrationStats, segment: IntegrationStats) {
426 total.rhs_evaluations += segment.rhs_evaluations;
427 total.accepted_steps += segment.accepted_steps;
428 total.rejected_steps += segment.rejected_steps;
429}
430
431#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
437pub struct ZohKeplerDynamics;
438
439impl DynamicsModel<7, 5> for ZohKeplerDynamics {
440 const NAME: &'static str = "ZOH Kepler dynamics";
441
442 fn validate(&self, time: f64, state: &[f64; 7], parameters: &[f64; 5]) -> Result<()> {
443 validate_finite(time, state, parameters)?;
444 validate_positive_state(state[6], "mass")?;
445 validate_radius(&state[..3], "ZOH Kepler radius")?;
446 Ok(())
447 }
448
449 fn rhs(
450 &self,
451 time: f64,
452 state: &[f64; 7],
453 parameters: &[f64; 5],
454 derivative: &mut [f64; 7],
455 ) -> Result<()> {
456 self.validate(time, state, parameters)?;
457 let radius_squared = squared_norm(&state[..3]);
458 let gravity = -1.0 / (radius_squared * radius_squared.sqrt());
459 let [thrust, ix, iy, iz, c] = *parameters;
460 *derivative = [
461 state[3],
462 state[4],
463 state[5],
464 gravity * state[0] + thrust * ix / state[6],
465 gravity * state[1] + thrust * iy / state[6],
466 gravity * state[2] + thrust * iz / state[6],
467 regularized_mass_flow(state[6], thrust, c),
468 ];
469 validate_rhs(Self::NAME, derivative)
470 }
471}
472
473impl DifferentiableDynamicsModel<7, 5> for ZohKeplerDynamics {
474 fn jacobians(
475 &self,
476 time: f64,
477 state: &[f64; 7],
478 parameters: &[f64; 5],
479 state_jacobian: &mut [[f64; 7]; 7],
480 parameter_jacobian: &mut [[f64; 5]; 7],
481 ) -> Result<()> {
482 numerical_jacobians(
483 self,
484 time,
485 state,
486 parameters,
487 state_jacobian,
488 parameter_jacobian,
489 &[6],
490 )
491 }
492}
493
494impl ZeroOrderHoldModel<7, 4, 1, 5> for ZohKeplerDynamics {
495 fn parameters(control: [f64; 4], constants: [f64; 1]) -> [f64; 5] {
496 [control[0], control[1], control[2], control[3], constants[0]]
497 }
498
499 fn parameter_seeds<const W: usize>(
500 control: [[f64; W]; 4],
501 constants: [[f64; W]; 1],
502 ) -> [[f64; W]; 5] {
503 [control[0], control[1], control[2], control[3], constants[0]]
504 }
505}
506
507#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
513pub struct ZohCr3bpDynamics;
514
515impl DynamicsModel<7, 6> for ZohCr3bpDynamics {
516 const NAME: &'static str = "ZOH CR3BP dynamics";
517
518 fn validate(&self, time: f64, state: &[f64; 7], parameters: &[f64; 6]) -> Result<()> {
519 validate_finite(time, state, parameters)?;
520 validate_positive_state(state[6], "mass")?;
521 validate_mass_fraction(parameters[5])?;
522 let mu = parameters[5];
523 validate_radius(
524 &[state[0] + mu, state[1], state[2]],
525 "ZOH CR3BP primary-one distance",
526 )?;
527 validate_radius(
528 &[state[0] + mu - 1.0, state[1], state[2]],
529 "ZOH CR3BP primary-two distance",
530 )?;
531 Ok(())
532 }
533
534 fn rhs(
535 &self,
536 time: f64,
537 state: &[f64; 7],
538 parameters: &[f64; 6],
539 derivative: &mut [f64; 7],
540 ) -> Result<()> {
541 self.validate(time, state, parameters)?;
542 let [thrust, ix, iy, iz, c, mu] = *parameters;
543 let d1 = [state[0] + mu, state[1], state[2]];
544 let d2 = [state[0] + mu - 1.0, state[1], state[2]];
545 let r1_squared = squared_norm(&d1);
546 let r2_squared = squared_norm(&d2);
547 let primary = (1.0 - mu) / (r1_squared * r1_squared.sqrt());
548 let secondary = mu / (r2_squared * r2_squared.sqrt());
549 *derivative = [
550 state[3],
551 state[4],
552 state[5],
553 2.0 * state[4] + state[0] - primary * d1[0] - secondary * d2[0]
554 + thrust * ix / state[6],
555 -2.0 * state[3] + state[1] - (primary + secondary) * state[1] + thrust * iy / state[6],
556 -(primary + secondary) * state[2] + thrust * iz / state[6],
557 regularized_mass_flow(state[6], thrust, c),
558 ];
559 validate_rhs(Self::NAME, derivative)
560 }
561}
562
563impl DifferentiableDynamicsModel<7, 6> for ZohCr3bpDynamics {
564 fn jacobians(
565 &self,
566 time: f64,
567 state: &[f64; 7],
568 parameters: &[f64; 6],
569 state_jacobian: &mut [[f64; 7]; 7],
570 parameter_jacobian: &mut [[f64; 6]; 7],
571 ) -> Result<()> {
572 numerical_jacobians(
573 self,
574 time,
575 state,
576 parameters,
577 state_jacobian,
578 parameter_jacobian,
579 &[6],
580 )
581 }
582}
583
584impl ZeroOrderHoldModel<7, 4, 2, 6> for ZohCr3bpDynamics {
585 fn parameters(control: [f64; 4], constants: [f64; 2]) -> [f64; 6] {
586 [
587 control[0],
588 control[1],
589 control[2],
590 control[3],
591 constants[0],
592 constants[1],
593 ]
594 }
595
596 fn parameter_seeds<const W: usize>(
597 control: [[f64; W]; 4],
598 constants: [[f64; W]; 2],
599 ) -> [[f64; W]; 6] {
600 [
601 control[0],
602 control[1],
603 control[2],
604 control[3],
605 constants[0],
606 constants[1],
607 ]
608 }
609}
610
611#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
619pub struct ZohEquinoctialDynamics;
620
621impl DynamicsModel<7, 5> for ZohEquinoctialDynamics {
622 const NAME: &'static str = "ZOH equinoctial dynamics";
623
624 fn validate(&self, time: f64, state: &[f64; 7], parameters: &[f64; 5]) -> Result<()> {
625 validate_finite(time, state, parameters)?;
626 validate_positive_state(state[0], "semilatus_rectum")?;
627 validate_positive_state(state[6], "mass")?;
628 let w = 1.0 + state[1] * state[5].cos() + state[2] * state[5].sin();
629 if w == 0.0 {
630 return Err(PykepError::SingularGeometry {
631 operation: "ZOH equinoctial radial denominator",
632 });
633 }
634 Ok(())
635 }
636
637 fn rhs(
638 &self,
639 time: f64,
640 state: &[f64; 7],
641 parameters: &[f64; 5],
642 derivative: &mut [f64; 7],
643 ) -> Result<()> {
644 self.validate(time, state, parameters)?;
645 let [p, f, g, h, k, longitude, mass] = *state;
646 let [thrust, radial, transverse, normal, c] = *parameters;
647 let sine = longitude.sin();
648 let cosine = longitude.cos();
649 let w = 1.0 + f * cosine + g * sine;
650 let s2 = 1.0 + h * h + k * k;
651 let hsk = h * sine - k * cosine;
652 let sqrt_p = p.sqrt();
653 let radial_thrust = radial * thrust;
654 let transverse_thrust = transverse * thrust;
655 let normal_thrust = normal * thrust;
656 *derivative = [
657 sqrt_p * (2.0 * p / w) * transverse_thrust / mass,
658 sqrt_p
659 * (radial_thrust * sine + ((1.0 + w) * cosine + f) / w * transverse_thrust
660 - g / w * hsk * normal_thrust)
661 / mass,
662 sqrt_p
663 * (-radial_thrust * cosine
664 + ((1.0 + w) * sine + g) / w * transverse_thrust
665 + f / w * hsk * normal_thrust)
666 / mass,
667 sqrt_p * (s2 / w / 2.0) * cosine * normal_thrust / mass,
668 sqrt_p * (s2 / w / 2.0) * sine * normal_thrust / mass,
669 sqrt_p * hsk / w * normal_thrust / mass + w * w / p.powf(1.5),
670 regularized_mass_flow(mass, thrust, c),
671 ];
672 validate_rhs(Self::NAME, derivative)
673 }
674}
675
676impl DifferentiableDynamicsModel<7, 5> for ZohEquinoctialDynamics {
677 fn jacobians(
678 &self,
679 time: f64,
680 state: &[f64; 7],
681 parameters: &[f64; 5],
682 state_jacobian: &mut [[f64; 7]; 7],
683 parameter_jacobian: &mut [[f64; 5]; 7],
684 ) -> Result<()> {
685 numerical_jacobians(
686 self,
687 time,
688 state,
689 parameters,
690 state_jacobian,
691 parameter_jacobian,
692 &[0, 6],
693 )
694 }
695}
696
697impl ZeroOrderHoldModel<7, 4, 1, 5> for ZohEquinoctialDynamics {
698 fn parameters(control: [f64; 4], constants: [f64; 1]) -> [f64; 5] {
699 [control[0], control[1], control[2], control[3], constants[0]]
700 }
701
702 fn parameter_seeds<const W: usize>(
703 control: [[f64; W]; 4],
704 constants: [[f64; W]; 1],
705 ) -> [[f64; W]; 5] {
706 [control[0], control[1], control[2], control[3], constants[0]]
707 }
708}
709
710#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
717pub struct ZohSolarSailDynamics;
718
719impl DynamicsModel<6, 3> for ZohSolarSailDynamics {
720 const NAME: &'static str = "ZOH solar-sail dynamics";
721
722 fn validate(&self, time: f64, state: &[f64; 6], parameters: &[f64; 3]) -> Result<()> {
723 validate_finite(time, state, parameters)?;
724 validate_radius(&state[..3], "ZOH solar-sail radius")?;
725 let angular_momentum = cross(&state[..3], &state[3..]);
726 validate_radius(&angular_momentum, "ZOH solar-sail angular momentum")?;
727 Ok(())
728 }
729
730 fn rhs(
731 &self,
732 time: f64,
733 state: &[f64; 6],
734 parameters: &[f64; 3],
735 derivative: &mut [f64; 6],
736 ) -> Result<()> {
737 self.validate(time, state, parameters)?;
738 let [alpha, beta, c] = *parameters;
739 let radius_squared = squared_norm(&state[..3]);
740 let radius = radius_squared.sqrt();
741 let angular_momentum = cross(&state[..3], &state[3..]);
742 let angular_norm = squared_norm(&angular_momentum).sqrt();
743 let radial = [state[0] / radius, state[1] / radius, state[2] / radius];
744 let normal = [
745 angular_momentum[0] / angular_norm,
746 angular_momentum[1] / angular_norm,
747 angular_momentum[2] / angular_norm,
748 ];
749 let transverse = cross(&normal, &radial);
750 let thrust = c / radius_squared * alpha.cos().powi(2);
751 let radial_acceleration = alpha.cos() * thrust;
752 let transverse_acceleration = alpha.sin() * beta.sin() * thrust;
753 let normal_acceleration = alpha.sin() * beta.cos() * thrust;
754 let gravity = -1.0 / (radius_squared * radius);
755 *derivative = [
756 state[3],
757 state[4],
758 state[5],
759 gravity * state[0]
760 + radial_acceleration * radial[0]
761 + transverse_acceleration * transverse[0]
762 + normal_acceleration * normal[0],
763 gravity * state[1]
764 + radial_acceleration * radial[1]
765 + transverse_acceleration * transverse[1]
766 + normal_acceleration * normal[1],
767 gravity * state[2]
768 + radial_acceleration * radial[2]
769 + transverse_acceleration * transverse[2]
770 + normal_acceleration * normal[2],
771 ];
772 validate_rhs(Self::NAME, derivative)
773 }
774}
775
776impl DifferentiableDynamicsModel<6, 3> for ZohSolarSailDynamics {
777 fn jacobians(
778 &self,
779 time: f64,
780 state: &[f64; 6],
781 parameters: &[f64; 3],
782 state_jacobian: &mut [[f64; 6]; 6],
783 parameter_jacobian: &mut [[f64; 3]; 6],
784 ) -> Result<()> {
785 numerical_jacobians(
786 self,
787 time,
788 state,
789 parameters,
790 state_jacobian,
791 parameter_jacobian,
792 &[],
793 )
794 }
795}
796
797impl ZeroOrderHoldModel<6, 2, 1, 3> for ZohSolarSailDynamics {
798 fn parameters(control: [f64; 2], constants: [f64; 1]) -> [f64; 3] {
799 [control[0], control[1], constants[0]]
800 }
801
802 fn parameter_seeds<const W: usize>(
803 control: [[f64; W]; 2],
804 constants: [[f64; W]; 1],
805 ) -> [[f64; W]; 3] {
806 [control[0], control[1], constants[0]]
807 }
808}
809
810fn validate_finite<const N: usize, const P: usize>(
811 time: f64,
812 state: &[f64; N],
813 parameters: &[f64; P],
814) -> Result<()> {
815 ensure_finite("time", time)?;
816 for &value in state {
817 ensure_finite("state", value)?;
818 }
819 for &value in parameters {
820 ensure_finite("parameters", value)?;
821 }
822 Ok(())
823}
824
825fn validate_positive_state(value: f64, parameter: &'static str) -> Result<()> {
826 if value > 0.0 {
827 Ok(())
828 } else {
829 Err(PykepError::InvalidInput {
830 parameter,
831 reason: "must be greater than zero".into(),
832 })
833 }
834}
835
836fn validate_mass_fraction(mu: f64) -> Result<()> {
837 if (0.0..=1.0).contains(&mu) {
838 Ok(())
839 } else {
840 Err(PykepError::InvalidInput {
841 parameter: "mu",
842 reason: "must lie in the closed interval [0, 1]".into(),
843 })
844 }
845}
846
847fn validate_radius(vector: &[f64], operation: &'static str) -> Result<()> {
848 let squared = squared_norm(vector);
849 if squared == 0.0 {
850 Err(PykepError::SingularGeometry { operation })
851 } else if squared.is_finite() {
852 Ok(())
853 } else {
854 Err(PykepError::NumericalOverflow { operation })
855 }
856}
857
858fn squared_norm(vector: &[f64]) -> f64 {
859 vector.iter().map(|value| value * value).sum()
860}
861
862fn cross(left: &[f64], right: &[f64]) -> [f64; 3] {
863 [
864 left[1] * right[2] - left[2] * right[1],
865 left[2] * right[0] - left[0] * right[2],
866 left[0] * right[1] - left[1] * right[0],
867 ]
868}
869
870fn regularized_mass_flow(mass: f64, thrust: f64, coefficient: f64) -> f64 {
871 -coefficient * thrust * (-1.0 / mass / 1e16).exp()
872}
873
874fn validate_rhs<const N: usize>(operation: &'static str, values: &[f64; N]) -> Result<()> {
875 if values.iter().all(|value| value.is_finite()) {
876 Ok(())
877 } else {
878 Err(PykepError::NumericalOverflow { operation })
879 }
880}
881
882fn numerical_jacobians<M, const N: usize, const P: usize>(
883 model: &M,
884 time: f64,
885 state: &[f64; N],
886 parameters: &[f64; P],
887 state_jacobian: &mut [[f64; N]; N],
888 parameter_jacobian: &mut [[f64; P]; N],
889 positive_state_indices: &[usize],
890) -> Result<()>
891where
892 M: DynamicsModel<N, P>,
893{
894 *state_jacobian = [[0.0; N]; N];
895 *parameter_jacobian = [[0.0; P]; N];
896 for column in 0..N {
897 let mut step = 3e-6 * state[column].abs().max(1.0);
898 if positive_state_indices.contains(&column) {
899 step = step.min(state[column] * 0.25);
900 }
901 let mut plus = *state;
902 let mut minus = *state;
903 plus[column] += step;
904 minus[column] -= step;
905 let mut rhs_plus = [0.0; N];
906 let mut rhs_minus = [0.0; N];
907 model.rhs(time, &plus, parameters, &mut rhs_plus)?;
908 model.rhs(time, &minus, parameters, &mut rhs_minus)?;
909 for row in 0..N {
910 state_jacobian[row][column] = (rhs_plus[row] - rhs_minus[row]) / (2.0 * step);
911 }
912 }
913 for column in 0..P {
914 let step = 3e-6 * parameters[column].abs().max(1.0);
915 let mut plus = *parameters;
916 let mut minus = *parameters;
917 plus[column] += step;
918 minus[column] -= step;
919 let mut rhs_plus = [0.0; N];
920 let mut rhs_minus = [0.0; N];
921 model.rhs(time, state, &plus, &mut rhs_plus)?;
922 model.rhs(time, state, &minus, &mut rhs_minus)?;
923 for row in 0..N {
924 parameter_jacobian[row][column] = (rhs_plus[row] - rhs_minus[row]) / (2.0 * step);
925 }
926 }
927 Ok(())
928}