packages feed

orbits-0.4: test/Test/StateVectors.hs

{-# language QuasiQuotes #-}

module Test.StateVectors where

import           Control.Lens.Operators         ( (^.) )
import           Data.CReal                     ( CReal )
import           Data.CReal.QuickCheck          ( )
import           Data.Constants.Mechanics.Extra
import           Data.Metrology
import           Data.Metrology.Extra
import           Data.Metrology.QuickCheck
import           Data.Units.SI.Parser
import           Linear.Metric
import           Linear.QuickCheck              ( )
import           Linear.V3
import           Test.QuickCheck.Checkers
import           Test.QuickCheck.Extra
import           Test.Tasty
import           Test.Tasty.QuickCheck
import           Test.Tasty.TH                  ( testGroupGenerator )

import           Physics.Orbit
import           Physics.Orbit.QuickCheck
import           Physics.Orbit.StateVectors

-- | The type used for tests which require exact arithmetic. They are compared
-- at a resolution of 2^16
type Exact = CReal 16

test_planeRotation :: [TestTree]
test_planeRotation =
  [ testProperty
      "plane rotation inverse"
      (\o -> inverse @(Position Exact) (rotateToPlane o) (rotateFromPlane o))
  ]

test_stateVectorInverse :: [TestTree]
test_stateVectorInverse =
  [ testProperty
    "state vector elements inverse"
    (\(PositiveQuantity μ) sv ->
      let (o, ν) = elementsFromStateVectors @Exact μ sv
          sv'    = stateVectorsAtTrueAnomaly o ν
      in  sv' === sv
    )
  , slowTest $ testProperty
    "elements state vector inverse"
    (\(CanonicalOrbit o) (PositiveQuantity ((`mod'` turn) -> ν)) ->
      let μ        = primaryGravitationalParameter @Exact o
          sv       = stateVectorsAtTrueAnomaly o ν
          (o', ν') = elementsFromStateVectors μ sv
      in  validTrueAnomaly o ν ==> (o', ν') === (o, ν)
    )
  , slowTest $ testProperty
    "elements state vector inverse 2"
    (\(normalizeOrbit -> o) (PositiveQuantity ((`mod'` turn) -> ν)) ->
      let μ        = primaryGravitationalParameter @Exact o
          sv       = stateVectorsAtTrueAnomaly o ν
          (o', ν') = elementsFromStateVectors μ sv
      in  validTrueAnomaly o ν ==> normalizeν (o', ν') === normalizeν (o, ν)
    )
  ]
 where
  normalizeν (o, ν) = case periapsisSpecifier o of
    Eccentric ω | eccentricity o == 0 ->
      (o { periapsisSpecifier = Circular }, (ν |+| ω) `mod'` turn)
    _ -> (o, ν `mod'` turn)

test_normalize :: [TestTree]
test_normalize =
  [ testProperty
    "state vectors invariant over normalize"
    (\o ν ->
      let oN = normalizeOrbit @Exact o
      in  stateVectorsAtTrueAnomaly o ν === stateVectorsAtTrueAnomaly oN ν
    )
  , testProperty
    "plane quaternion invariant over normalize"
    (\o ->
      let q1 = orbitalPlaneQuaternion @Exact o
          q2 = orbitalPlaneQuaternion (normalizeOrbit o)
      in  q1 === q2 .||. q1 === negate q2
    )
  ]

test_positionVelocity :: [TestTree]
test_positionVelocity =
  [ testProperty
    "position magnitude"
    (\o ν ->
      let r1 = fmap (# [si|m|]) . positionAtTrueAnomaly @Exact o $ ν
          r2 = (# [si|m|]) . radiusAtTrueAnomaly o $ ν
      in  r2 * r2 === quadrance r1
    )
  , testProperty
    "position in plane z"
    (\o ν ->
      let r = positionInPlaneAtTrueAnomaly @Float o ν in r ^. _z === zero
    )
  , testProperty
    "velocity magnitude"
    (\o ν ->
      let r1 = fmap (# [si|m/s|]) . velocityAtTrueAnomaly @Exact o $ ν
          r2 = (# [si|m/s|]) . speedAtTrueAnomaly o $ ν
      in  r2 * r2 === quadrance r1
    )
  , testProperty
    "velocity in plane z"
    (\o ν ->
      let v = velocityInPlaneAtTrueAnomaly @Float o ν in v ^. _z === zero
    )
  , testProperty
    "velocity at ν=0"
    (\o ->
      let v     = velocityInPlaneAtTrueAnomaly @Exact o zero
          speed = speedAtTrueAnomaly o zero
      in  v === V3 zero speed zero
    )
  , testProperty
    "velocity in circular orbit"
    (\(CircularOrbit o) ν ->
      let v     = velocityInPlaneAtTrueAnomaly @Exact o ν
          speed = speedAtTrueAnomaly o zero
      in  qNorm v === speed
    )
  , testProperty
    "velocity perpendicular to radius in circular orbit"
    (\(CircularOrbit o) ν ->
      let v = velocityInPlaneAtTrueAnomaly @Exact o ν
          r = positionInPlaneAtTrueAnomaly o ν
      in  v `qDot` r === zero
    )
  ]

test_flightPathAngle :: [TestTree]
test_flightPathAngle =
  [ testProperty
    "fpa circular orbit "
    (\(CircularOrbit o) ν ->
      let φ = flightPathAngleAtTrueAnomaly @Exact o ν in φ === zero
    )
  , testProperty
    "fpa angular momentum"
    (\o ν ->
      let φ = flightPathAngleAtTrueAnomaly @Exact o ν
          h = specificAngularMomentum o
          r = radiusAtTrueAnomaly o ν
          v = speedAtTrueAnomaly o ν
      in  h === r |*| v |*| qCos φ
    )
  , testProperty
    "fpa velocity direction"
    (\o ν ->
      let φ = flightPathAngleAtTrueAnomaly @Exact o ν
          r = (# [si|m|]) <$> positionInPlaneAtTrueAnomaly o ν
          v = (# [si|m/s|]) <$> velocityInPlaneAtTrueAnomaly o ν
      in  validTrueAnomaly o ν
            ==>   sin (φ # [si|rad|])
            ===   normalize r
            `dot` normalize v
    )
  ]

test_specificAngularMomentum :: [TestTree]
test_specificAngularMomentum =
  [ testProperty
    "momentum from vectors"
    (\o -> specificAngularMomentum @Exact o === specificAngularMomentumSV o)
  , testProperty
    "momentum vector length"
    (\o ν ->
      let sv = stateVectorsAtTrueAnomaly @Exact o ν
          h1 = specificAngularMomentumVector sv
          h2 = specificAngularMomentum o
      in  qNorm h1 === h2
    )
  ]

prop_specificAngularMomentum :: Orbit Exact -> Property
prop_specificAngularMomentum o =
  specificAngularMomentum o === specificAngularMomentumSV o

specificAngularMomentumSV
  :: (Ord a, Floating a) => Orbit a -> Quantity [si|m^2 s^-1|] a
specificAngularMomentumSV o = rx |*| vy |-| ry |*| vx
   where
    ν          = zero
    V3 rx ry _ = positionInPlaneAtTrueAnomaly o ν
    V3 vx vy _ = velocityInPlaneAtTrueAnomaly o ν

tests :: TestTree
tests = $(testGroupGenerator)

----------------------------------------------------------------
-- Orbit utils
----------------------------------------------------------------

validTrueAnomaly :: (Floating a, Ord a) => Orbit a -> Angle a -> Bool
validTrueAnomaly o ν = case hyperbolicDepartureAngle o of
  Nothing -> True
  Just d  -> qAbs ν < d