use super::*;
#[allow(unused_imports)]
use qtty::angular::{Degrees, Radians};
#[allow(unused_imports)]
use qtty::length::{Kilometers, Meters};
use qtty::units::Meter;
use qtty::{DEG, M};
#[derive(Debug, Copy, Clone, PartialEq)]
struct TestFrame;
impl crate::frames::ReferenceFrame for TestFrame {
fn frame_name() -> &'static str {
"TestFrame"
}
}
fn orientation() -> ConicOrientation<TestFrame> {
ConicOrientation::try_new(10.0 * DEG, 20.0 * DEG, 30.0 * DEG).unwrap()
}
#[test]
fn periapsis_try_new_rejects_zero_distance() {
assert_eq!(
PeriapsisParam::try_new(0.0 * M, 0.5),
Err(ConicValidationError::InvalidPeriapsisDistance)
);
}
#[test]
fn periapsis_try_new_rejects_negative_eccentricity() {
assert_eq!(
PeriapsisParam::try_new(1.0 * M, -0.1),
Err(ConicValidationError::InvalidEccentricity)
);
}
#[test]
fn sma_try_new_rejects_zero_axis() {
assert_eq!(
SemiMajorAxisParam::try_new(0.0 * M, 0.5),
Err(ConicValidationError::InvalidSemiMajorAxis)
);
}
#[test]
fn sma_try_new_rejects_parabolic() {
assert_eq!(
SemiMajorAxisParam::try_new(1.0 * M, 1.0),
Err(ConicValidationError::ParabolicSemiMajorAxis)
);
}
#[test]
fn orientation_try_new_rejects_nan() {
assert_eq!(
ConicOrientation::<TestFrame>::try_new(
Degrees::new(f64::NAN),
Degrees::new(0.0),
Degrees::new(0.0)
),
Err(ConicValidationError::InvalidOrientation)
);
}
#[test]
fn orientation_try_new_rejects_infinity() {
assert_eq!(
ConicOrientation::<TestFrame>::try_new(
Degrees::new(0.0),
Degrees::new(f64::INFINITY),
Degrees::new(0.0)
),
Err(ConicValidationError::InvalidOrientation)
);
}
#[test]
fn periapsis_kind_elliptic() {
let s = PeriapsisParam::try_new(1.0 * M, 0.5).unwrap();
assert_eq!(s.kind(), ConicKind::Elliptic);
}
#[test]
fn periapsis_kind_parabolic() {
let s = PeriapsisParam::try_new(1.0 * M, 1.0).unwrap();
assert_eq!(s.kind(), ConicKind::Parabolic);
}
#[test]
fn periapsis_kind_hyperbolic() {
let s = PeriapsisParam::try_new(1.0 * M, 1.5).unwrap();
assert_eq!(s.kind(), ConicKind::Hyperbolic);
}
#[test]
fn classify_periapsis_elliptic() {
let s = PeriapsisParam::try_new(1.0 * M, 0.5).unwrap();
assert!(matches!(
s.classify(),
ClassifiedPeriapsisParam::Elliptic(_)
));
}
#[test]
fn classify_periapsis_parabolic() {
let s = PeriapsisParam::try_new(1.0 * M, 1.0).unwrap();
assert!(matches!(
s.classify(),
ClassifiedPeriapsisParam::Parabolic(_)
));
}
#[test]
fn classify_periapsis_hyperbolic() {
let s = PeriapsisParam::try_new(1.0 * M, 1.5).unwrap();
assert!(matches!(
s.classify(),
ClassifiedPeriapsisParam::Hyperbolic(_)
));
}
#[test]
fn classify_sma_elliptic() {
let s = SemiMajorAxisParam::try_new(1.0 * M, 0.5).unwrap();
assert!(matches!(
s.classify(),
ClassifiedSemiMajorAxisParam::Elliptic(_)
));
}
#[test]
fn classify_sma_hyperbolic() {
let s = SemiMajorAxisParam::try_new(1.0 * M, 1.5).unwrap();
assert!(matches!(
s.classify(),
ClassifiedSemiMajorAxisParam::Hyperbolic(_)
));
}
#[test]
fn typed_elliptic_periapsis_to_sma() {
let s = PeriapsisParam::try_new(1.0 * M, 0.5).unwrap();
let ClassifiedPeriapsisParam::Elliptic(typed) = s.classify() else {
panic!()
};
let sma = typed.to_semi_major_axis().unwrap();
assert!((sma.semi_major_axis().value() - 2.0).abs() < 1e-12);
assert_eq!(sma.eccentricity(), 0.5);
assert_eq!(sma.kind(), ConicKind::Elliptic);
}
#[test]
fn typed_hyperbolic_periapsis_to_sma() {
let s = PeriapsisParam::try_new(1.0 * M, 2.0).unwrap();
let ClassifiedPeriapsisParam::Hyperbolic(typed) = s.classify() else {
panic!()
};
let sma = typed.to_semi_major_axis().unwrap();
assert!((sma.semi_major_axis().value() - 1.0).abs() < 1e-12);
assert_eq!(sma.kind(), ConicKind::Hyperbolic);
}
#[test]
fn typed_sma_to_periapsis() {
let s = SemiMajorAxisParam::try_new(2.0 * M, 0.5).unwrap();
let ClassifiedSemiMajorAxisParam::Elliptic(typed) = s.classify() else {
panic!()
};
let peri = typed.to_periapsis().unwrap();
assert!((peri.periapsis_distance().value() - 1.0).abs() < 1e-12);
assert_eq!(peri.kind(), ConicKind::Elliptic);
}
#[test]
fn periapsis_to_sma_preserves_orientation() {
let ori = orientation();
let conic = OrientedConic::new(PeriapsisParam::try_new(1.0 * M, 0.5).unwrap(), ori);
let converted = conic.to_semi_major_axis().unwrap();
assert_eq!(converted.orientation(), &ori);
}
#[test]
fn sma_to_periapsis_preserves_orientation() {
let ori = orientation();
let conic = OrientedConic::new(SemiMajorAxisParam::try_new(2.0 * M, 0.5).unwrap(), ori);
let converted = conic.to_periapsis().unwrap();
assert_eq!(converted.orientation(), &ori);
}
#[test]
fn periapsis_to_sma_parabolic_returns_none() {
let ori = orientation();
let conic = OrientedConic::new(PeriapsisParam::try_new(1.0 * M, 1.0).unwrap(), ori);
assert!(conic.to_semi_major_axis().is_none());
}
#[test]
fn oriented_conic_kind_hyperbolic() {
let conic = OrientedConic::new(
PeriapsisParam::try_new(1.0 * M, 1.5).unwrap(),
orientation(),
);
assert_eq!(conic.kind(), ConicKind::Hyperbolic);
}
#[test]
fn typed_oriented_elliptic_to_sma() {
let ori = orientation();
let ClassifiedPeriapsisParam::Elliptic(typed) =
PeriapsisParam::try_new(1.0 * M, 0.5).unwrap().classify()
else {
panic!()
};
let conic = OrientedConic::new(typed, ori);
let sma_conic = conic.to_semi_major_axis().unwrap();
assert_eq!(sma_conic.orientation(), &ori);
assert_eq!(sma_conic.kind(), ConicKind::Elliptic);
}
#[test]
fn periapsis_nextbefore_one_converts_to_sma() {
let e = f64::from_bits(1.0f64.to_bits() - 1);
assert!(e < 1.0);
let s = PeriapsisParam::try_new(1.0 * M, e).unwrap();
assert_eq!(s.kind(), ConicKind::Elliptic);
let sma = s.to_semi_major_axis();
assert!(sma.is_some(), "nextbefore(1.0) should convert to SMA");
assert_eq!(sma.unwrap().kind(), ConicKind::Elliptic);
}
#[test]
fn periapsis_nextafter_one_converts_to_sma() {
let e = f64::from_bits(1.0f64.to_bits() + 1);
assert!(e > 1.0);
let s = PeriapsisParam::try_new(1.0 * M, e).unwrap();
assert_eq!(s.kind(), ConicKind::Hyperbolic);
let sma = s.to_semi_major_axis();
assert!(sma.is_some(), "nextafter(1.0) should convert to SMA");
assert_eq!(sma.unwrap().kind(), ConicKind::Hyperbolic);
}
#[test]
fn periapsis_overflow_to_sma_returns_none() {
let e = f64::from_bits(1.0f64.to_bits() - 1);
let s = PeriapsisParam::try_new(f64::MAX * M, e).unwrap();
assert!(s.to_semi_major_axis().is_none());
}
#[test]
fn sma_overflow_to_periapsis_returns_none() {
let s = SemiMajorAxisParam::try_new(f64::MAX * M, 3.0).unwrap();
assert!(s.to_periapsis().is_none());
}
#[test]
fn typed_sma_new_rejects_wrong_kind() {
let inner = SemiMajorAxisParam::try_new(1.0 * M, 1.5).unwrap();
assert!(TypedSemiMajorAxisParam::<_, Elliptic>::new(inner).is_none());
}
#[test]
fn typed_periapsis_has_inner_layout() {
type Typed = TypedPeriapsisParam<Meter, Elliptic>;
assert_eq!(
core::mem::size_of::<Typed>(),
core::mem::size_of::<PeriapsisParam<Meter>>()
);
assert_eq!(
core::mem::align_of::<Typed>(),
core::mem::align_of::<PeriapsisParam<Meter>>()
);
}
#[test]
fn typed_sma_has_inner_layout() {
type Typed = TypedSemiMajorAxisParam<Meter, Elliptic>;
assert_eq!(
core::mem::size_of::<Typed>(),
core::mem::size_of::<SemiMajorAxisParam<Meter>>()
);
assert_eq!(
core::mem::align_of::<Typed>(),
core::mem::align_of::<SemiMajorAxisParam<Meter>>()
);
}
#[test]
fn elliptic_periapsis_alias_is_usable() {
let shape = PeriapsisParam::try_new(1.0 * M, 0.5).unwrap();
let ClassifiedPeriapsisParam::Elliptic(typed) = shape.classify() else {
panic!()
};
let conic: EllipticPeriapsis<_, TestFrame> = OrientedConic::new(typed, orientation());
assert_eq!(conic.kind(), ConicKind::Elliptic);
assert_eq!(conic.shape().eccentricity(), 0.5);
}
#[test]
fn elliptic_sma_alias_is_usable() {
let shape = SemiMajorAxisParam::try_new(2.0 * M, 0.25).unwrap();
let ClassifiedSemiMajorAxisParam::Elliptic(typed) = shape.classify() else {
panic!()
};
let conic: EllipticSemiMajorAxis<_, TestFrame> = OrientedConic::new(typed, orientation());
assert_eq!(conic.kind(), ConicKind::Elliptic);
assert_eq!(conic.shape().semi_major_axis(), 2.0 * M);
}
#[test]
fn error_display_invalid_eccentricity() {
let msg = format!("{}", ConicValidationError::InvalidEccentricity);
assert_eq!(msg, "invalid eccentricity");
}
#[test]
fn error_display_invalid_semi_major_axis() {
let msg = format!("{}", ConicValidationError::InvalidSemiMajorAxis);
assert_eq!(msg, "invalid semi-major axis");
}
#[test]
fn error_display_invalid_periapsis_distance() {
let msg = format!("{}", ConicValidationError::InvalidPeriapsisDistance);
assert_eq!(msg, "invalid periapsis distance");
}
#[test]
fn error_display_parabolic_semi_major_axis() {
let msg = format!("{}", ConicValidationError::ParabolicSemiMajorAxis);
assert!(msg.contains("parabolic"));
}
#[test]
fn error_display_invalid_orientation() {
let msg = format!("{}", ConicValidationError::InvalidOrientation);
assert_eq!(msg, "orientation angles must be finite");
}
#[test]
fn error_is_std_error() {
let e: &dyn std::error::Error = &ConicValidationError::InvalidEccentricity;
assert!(e.source().is_none());
}
#[test]
fn parabolic_conic_kind() {
assert_eq!(Parabolic::conic_kind(), ConicKind::Parabolic);
}
#[test]
fn periapsis_new_unchecked_stores_values() {
let p = PeriapsisParam::new_unchecked(2.0 * M, 0.6);
assert_eq!(p.periapsis_distance(), 2.0 * M);
assert_eq!(p.eccentricity(), 0.6);
}
#[test]
fn periapsis_to_semi_major_axis_elliptic() {
let p = PeriapsisParam::try_new(0.5 * M, 0.5).unwrap();
let sma = p.to_semi_major_axis().unwrap();
assert!((sma.semi_major_axis().value() - 1.0).abs() < 1e-10);
}
#[test]
fn periapsis_to_semi_major_axis_parabolic_returns_none() {
let p = PeriapsisParam::try_new(1.0 * M, 1.0).unwrap();
assert!(p.to_semi_major_axis().is_none());
}
#[test]
fn periapsis_shape_name() {
assert_eq!(PeriapsisParam::<Meter>::shape_name(), "periapsis_distance");
}
#[test]
fn typed_periapsis_accessors() {
let p = PeriapsisParam::try_new(3.0 * M, 0.7).unwrap();
let ClassifiedPeriapsisParam::Elliptic(typed) = p.classify() else {
panic!("expected elliptic");
};
assert_eq!(typed.periapsis_distance(), 3.0 * M);
assert_eq!(typed.eccentricity(), 0.7);
assert_eq!(typed.kind(), ConicKind::Elliptic);
let inner = typed.into_inner();
assert_eq!(inner.periapsis_distance(), 3.0 * M);
}
#[test]
fn typed_periapsis_to_semi_major_axis_elliptic() {
let p = PeriapsisParam::try_new(0.5 * M, 0.5).unwrap();
let ClassifiedPeriapsisParam::Elliptic(typed) = p.classify() else {
panic!()
};
let sma = typed.to_semi_major_axis().unwrap();
assert!((sma.semi_major_axis().value() - 1.0).abs() < 1e-10);
}
#[test]
fn typed_periapsis_to_semi_major_axis_hyperbolic() {
let p = PeriapsisParam::try_new(2.0 * M, 2.0).unwrap();
let ClassifiedPeriapsisParam::Hyperbolic(typed) = p.classify() else {
panic!()
};
let sma = typed.to_semi_major_axis().unwrap();
assert!((sma.semi_major_axis().value() - 2.0).abs() < 1e-10);
}
#[test]
fn sma_new_unchecked_stores_values() {
let s = SemiMajorAxisParam::new_unchecked(5.0 * M, 0.3);
assert_eq!(s.semi_major_axis(), 5.0 * M);
assert_eq!(s.eccentricity(), 0.3);
}
#[test]
fn sma_shape_name() {
assert_eq!(SemiMajorAxisParam::<Meter>::shape_name(), "semi_major_axis");
}
#[test]
fn sma_to_periapsis_elliptic() {
let s = SemiMajorAxisParam::try_new(1.0 * M, 0.5).unwrap();
let p = s.to_periapsis().unwrap();
assert!((p.periapsis_distance().value() - 0.5).abs() < 1e-10);
}
#[test]
fn sma_classify_elliptic() {
let s = SemiMajorAxisParam::try_new(1.0 * M, 0.3).unwrap();
assert!(matches!(
s.classify(),
ClassifiedSemiMajorAxisParam::Elliptic(_)
));
}
#[test]
fn sma_classify_hyperbolic() {
let s = SemiMajorAxisParam::try_new(1.0 * M, 1.5).unwrap();
assert!(matches!(
s.classify(),
ClassifiedSemiMajorAxisParam::Hyperbolic(_)
));
}
#[test]
fn typed_sma_new_accepts_matching_kind() {
let s = SemiMajorAxisParam::try_new(1.0 * M, 0.3).unwrap();
let typed = TypedSemiMajorAxisParam::<_, Elliptic>::new(s);
assert!(typed.is_some());
}
#[test]
fn typed_sma_new_rejects_wrong_kind_v2() {
let s = SemiMajorAxisParam::try_new(1.0 * M, 1.5).unwrap(); let typed = TypedSemiMajorAxisParam::<_, Elliptic>::new(s);
assert!(typed.is_none());
}
#[test]
fn typed_sma_accessors() {
let s = SemiMajorAxisParam::try_new(4.0 * M, 0.2).unwrap();
let ClassifiedSemiMajorAxisParam::Elliptic(typed) = s.classify() else {
panic!()
};
assert_eq!(typed.semi_major_axis(), 4.0 * M);
assert_eq!(typed.eccentricity(), 0.2);
let inner = typed.into_inner();
assert_eq!(inner.semi_major_axis(), 4.0 * M);
}
#[test]
fn typed_sma_to_periapsis_v2() {
let s = SemiMajorAxisParam::try_new(2.0 * M, 0.5).unwrap();
let ClassifiedSemiMajorAxisParam::Elliptic(typed) = s.classify() else {
panic!()
};
let p = typed.to_periapsis().unwrap();
assert!((p.periapsis_distance().value() - 1.0).abs() < 1e-10);
}
#[test]
fn oriented_conic_periapsis_to_sma() {
let shape = PeriapsisParam::try_new(0.5 * M, 0.5).unwrap();
let oc = OrientedConic::new(shape, orientation());
let sma_oc = oc.to_semi_major_axis().unwrap();
assert!((sma_oc.shape().semi_major_axis().value() - 1.0).abs() < 1e-10);
}
#[test]
fn oriented_conic_sma_to_periapsis() {
let shape = SemiMajorAxisParam::try_new(2.0 * M, 0.5).unwrap();
let oc = OrientedConic::new(shape, orientation());
let peri_oc = oc.to_periapsis().unwrap();
assert!((peri_oc.shape().periapsis_distance().value() - 1.0).abs() < 1e-10);
}
#[test]
fn oriented_conic_periapsis_parabolic_to_sma_is_none() {
let shape = PeriapsisParam::try_new(1.0 * M, 1.0).unwrap();
let oc = OrientedConic::new(shape, orientation());
assert!(oc.to_semi_major_axis().is_none());
}
#[test]
fn oriented_conic_typed_elliptic_periapsis_to_sma() {
let shape = PeriapsisParam::try_new(0.5 * M, 0.5).unwrap();
let ClassifiedPeriapsisParam::Elliptic(typed) = shape.classify() else {
panic!()
};
let oc = OrientedConic::new(typed, orientation());
let sma = oc.to_semi_major_axis().unwrap();
assert!((sma.shape().semi_major_axis().value() - 1.0).abs() < 1e-10);
}
#[test]
fn oriented_conic_typed_hyperbolic_periapsis_to_sma() {
let shape = PeriapsisParam::try_new(2.0 * M, 2.0).unwrap();
let ClassifiedPeriapsisParam::Hyperbolic(typed) = shape.classify() else {
panic!()
};
let oc = OrientedConic::new(typed, orientation());
let sma = oc.to_semi_major_axis().unwrap();
assert!((sma.shape().semi_major_axis().value() - 2.0).abs() < 1e-10);
}
#[test]
fn oriented_conic_typed_sma_to_periapsis() {
let shape = SemiMajorAxisParam::try_new(2.0 * M, 0.5).unwrap();
let ClassifiedSemiMajorAxisParam::Elliptic(typed) = shape.classify() else {
panic!()
};
let oc = OrientedConic::new(typed, orientation());
let peri = oc.to_periapsis().unwrap();
assert!((peri.shape().periapsis_distance().value() - 1.0).abs() < 1e-10);
}
#[test]
fn orientation_new_unchecked_stores_values() {
let o = ConicOrientation::<TestFrame>::new(10.0 * DEG, 20.0 * DEG, 30.0 * DEG);
assert_eq!(o.inclination(), 10.0 * DEG);
assert_eq!(o.longitude_of_ascending_node(), 20.0 * DEG);
assert_eq!(o.argument_of_periapsis(), 30.0 * DEG);
}
#[test]
fn orientation_rejects_nan_argument_of_periapsis() {
assert_eq!(
ConicOrientation::<TestFrame>::try_new(
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(f64::NAN),
),
Err(ConicValidationError::InvalidOrientation)
);
}
const ORIENTATION_EPS: f64 = 1e-12;
#[test]
fn orientation_try_new_wraps_longitude_of_ascending_node() {
let o = ConicOrientation::<TestFrame>::try_new(45.0 * DEG, 400.0 * DEG, 30.0 * DEG).unwrap();
assert!((o.longitude_of_ascending_node().value() - 40.0).abs() < ORIENTATION_EPS);
assert!((o.inclination().value() - 45.0).abs() < ORIENTATION_EPS);
assert!((o.argument_of_periapsis().value() - 30.0).abs() < ORIENTATION_EPS);
}
#[test]
fn orientation_try_new_wraps_argument_of_periapsis_negative() {
let o = ConicOrientation::<TestFrame>::try_new(45.0 * DEG, 20.0 * DEG, -30.0 * DEG).unwrap();
assert!((o.argument_of_periapsis().value() - 330.0).abs() < ORIENTATION_EPS);
}
#[test]
fn orientation_try_new_canonicalizes_negative_inclination() {
let o = ConicOrientation::<TestFrame>::try_new(-10.0 * DEG, 20.0 * DEG, 30.0 * DEG).unwrap();
assert!(o.inclination().value() >= 0.0 && o.inclination().value() <= 180.0);
assert!((o.inclination().value() - 10.0).abs() < ORIENTATION_EPS);
assert!((o.longitude_of_ascending_node().value() - 200.0).abs() < ORIENTATION_EPS);
assert!((o.argument_of_periapsis().value() - 30.0).abs() < ORIENTATION_EPS);
}
#[test]
fn orientation_try_new_canonicalizes_large_inclination() {
let raw_deg = 4.0_f64.to_degrees();
let o = ConicOrientation::<TestFrame>::try_new(Degrees::new(raw_deg), 20.0 * DEG, 30.0 * DEG)
.unwrap();
assert!(o.inclination().value() >= 0.0 && o.inclination().value() <= 180.0);
let expected_i = 360.0 - raw_deg;
assert!((o.inclination().value() - expected_i).abs() < ORIENTATION_EPS);
assert!((o.longitude_of_ascending_node().value() - 200.0).abs() < ORIENTATION_EPS);
}
#[test]
fn orientation_try_new_strict_accepts_canonical_values() {
let o =
ConicOrientation::<TestFrame>::try_new_strict(45.0 * DEG, 20.0 * DEG, 30.0 * DEG).unwrap();
assert!((o.inclination().value() - 45.0).abs() < ORIENTATION_EPS);
}
#[test]
fn orientation_try_new_strict_rejects_negative_inclination() {
let err = ConicOrientation::<TestFrame>::try_new_strict(-10.0 * DEG, 20.0 * DEG, 30.0 * DEG)
.unwrap_err();
match err {
ConicValidationError::OutOfRange { field, value } => {
assert_eq!(field, "inclination");
assert!((value - -10.0).abs() < ORIENTATION_EPS);
}
other => panic!("expected OutOfRange, got {other:?}"),
}
}
#[test]
fn orientation_try_new_strict_rejects_large_inclination() {
let raw_deg = 4.0_f64.to_degrees();
let err = ConicOrientation::<TestFrame>::try_new_strict(
Degrees::new(raw_deg),
20.0 * DEG,
30.0 * DEG,
)
.unwrap_err();
assert!(matches!(
err,
ConicValidationError::OutOfRange {
field: "inclination",
..
}
));
}
#[test]
fn orientation_try_new_strict_rejects_out_of_range_lan() {
let err = ConicOrientation::<TestFrame>::try_new_strict(45.0 * DEG, 400.0 * DEG, 30.0 * DEG)
.unwrap_err();
assert!(matches!(
err,
ConicValidationError::OutOfRange {
field: "longitude_of_ascending_node",
..
}
));
}
#[test]
fn orientation_try_new_strict_rejects_out_of_range_aop() {
let err = ConicOrientation::<TestFrame>::try_new_strict(45.0 * DEG, 20.0 * DEG, -30.0 * DEG)
.unwrap_err();
assert!(matches!(
err,
ConicValidationError::OutOfRange {
field: "argument_of_periapsis",
..
}
));
}
#[test]
fn orientation_try_new_strict_rejects_nan() {
assert_eq!(
ConicOrientation::<TestFrame>::try_new_strict(
Degrees::new(f64::NAN),
Degrees::new(0.0),
Degrees::new(0.0),
),
Err(ConicValidationError::InvalidOrientation)
);
}