{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Physics.Orbit
(
Orbit(..)
, InclinationSpecifier(..)
, PeriapsisSpecifier(..)
, Classification(..)
, isValid
, classify
, apoapsis
, meanMotion
, period
, arealVelocity
, semiMajorAxis
, semiMinorAxis
, semiLatusRectum
, hyperbolicApproachAngle
, hyperbolicDepartureAngle
, timeAtMeanAnomaly
, timeAtEccentricAnomaly
, timeAtTrueAnomaly
, meanAnomalyAtTime
, meanAnomalyAtEccentricAnomaly
, meanAnomalyAtTrueAnomaly
, eccentricAnomalyAtTime
, eccentricAnomalyAtMeanAnomaly
, eccentricAnomalyAtMeanAnomalyFloat
, eccentricAnomalyAtTrueAnomaly
, trueAnomalyAtTime
, trueAnomalyAtMeanAnomaly
, trueAnomalyAtEccentricAnomaly
, Time
, Distance
, Speed
, Mass
, Angle
, Unitless
, Converge
) where
import Control.Monad ( (<=<) )
import Data.Bifunctor ( bimap
, second
)
import Data.CReal.Converge ( Converge
, convergeErr
)
import Data.Constants.Mechanics.Extra
import Data.Metrology
import Data.Metrology.Extra
import Data.Metrology.Show ( )
import Data.Metrology.Unsafe ( UnsafeQu(..) )
import Data.Units.SI.Parser
import Numeric.AD ( Mode
, Scalar
, auto
)
import Numeric.AD.Halley ( findZero
, findZeroNoEq
)
import Numeric.AD.Internal.Identity ( Id(..) )
type Quantity u = MkQu_ULN u 'DefaultLCSU
type Time = Quantity [si|s|]
type Distance = Quantity [si| m |]
type Speed = Quantity [si| m s^-1 |]
type Mass = Quantity [si| kg |]
type Angle = Quantity [si| rad |]
type Unitless = Quantity [si||]
data Orbit a = Orbit {
eccentricity :: !(Unitless a)
, periapsis :: !(Distance a)
, inclinationSpecifier :: !(InclinationSpecifier a)
, periapsisSpecifier :: !(PeriapsisSpecifier a)
, primaryGravitationalParameter :: !(Quantity [si| m^3 s^-2 |] a)
}
deriving (Show, Eq)
data InclinationSpecifier a =
Inclined {
longitudeOfAscendingNode :: !(Angle a)
, inclination :: !(Angle a)
}
| NonInclined
deriving (Show, Eq)
data PeriapsisSpecifier a =
Eccentric {
argumentOfPeriapsis :: !(Angle a) }
| Circular
deriving (Show, Eq)
data Classification =
Elliptic
| Parabolic
| Hyperbolic
deriving (Show, Read, Eq)
unsafeMapUnit :: (a -> b) -> Qu u l a -> Qu u l b
unsafeMapUnit f = qu . fmap f . UnsafeQu
unsafeMapOrbit :: (a -> b) -> Orbit a -> Orbit b
unsafeMapOrbit f (Orbit e q i p μ) = Orbit (unsafeMapUnit f e)
(unsafeMapUnit f q)
(unsafeMapInclinationSpecifier f i)
(unsafeMapPeriapsisSpecifier f p)
(unsafeMapUnit f μ)
unsafeMapInclinationSpecifier :: (a -> b)
-> InclinationSpecifier a -> InclinationSpecifier b
unsafeMapInclinationSpecifier f s = case s of
Inclined _Ω i -> Inclined (unsafeMapUnit f _Ω) (unsafeMapUnit f i)
NonInclined -> NonInclined
unsafeMapPeriapsisSpecifier :: (a -> b)
-> PeriapsisSpecifier a -> PeriapsisSpecifier b
unsafeMapPeriapsisSpecifier f p = case p of
Circular -> Circular
Eccentric a -> Eccentric (unsafeMapUnit f a)
isValid :: (Ord a, Num a) => Orbit a -> Bool
isValid o = e >= 0 &&
((e == 0) `iff` (periapsisSpecifier o == Circular)) &&
q > zero &&
μ > zero
where
iff = (==) :: Bool -> Bool -> Bool
e = eccentricity o
q = periapsis o
μ = primaryGravitationalParameter o
classify :: (Num a, Ord a) => Orbit a -> Classification
classify o
| e < 1 = Elliptic
| e == 1 = Parabolic
| e > 1 = Hyperbolic
| otherwise = error "classify"
where
e = eccentricity o
semiMajorAxis :: (Fractional a, Ord a) => Orbit a -> Maybe (Distance a)
semiMajorAxis o =
case classify o of
Parabolic -> Nothing
_ -> Just $ q |/| (1 |-| e)
where
q = periapsis o
e = eccentricity o
semiMinorAxis :: (Floating a, Ord a) => Orbit a -> Distance a
semiMinorAxis o =
case classify o of
Elliptic -> a |*| qSqrt (1 |-| e ^ (2::Int))
Parabolic -> zero
Hyperbolic -> a |*| qSqrt (e ^ (2::Int) |-| 1)
where
e = eccentricity o
Just a = semiMajorAxis o
semiLatusRectum :: (Num a) => Orbit a -> Distance a
semiLatusRectum orbit = e |*| q |+| q
where q = periapsis orbit
e = eccentricity orbit
apoapsis :: (Fractional a, Ord a) => Orbit a -> Maybe (Distance a)
apoapsis o =
case classify o of
Elliptic -> Just $ a |*| (1 |+| e)
_ -> Nothing
where
Just a = semiMajorAxis o
e = eccentricity o
meanMotion :: (Floating a, Ord a) => Orbit a -> Quantity [si|rad/s|] a
meanMotion o =
case classify o of
Elliptic -> addRad $ qSqrt (μ |/| qCube a)
Hyperbolic -> addRad $ qSqrt (μ |/| qNegate (qCube a))
Parabolic -> addRad $ 2 |*| qSqrt (μ |/| qCube l)
where
Just a = semiMajorAxis o
μ = primaryGravitationalParameter o
l = semiLatusRectum o
period :: (Floating a, Ord a) => Orbit a -> Maybe (Time a)
period o =
case classify o of
Elliptic -> Just p
_ -> Nothing
where
n = meanMotion o
p = turn |/| n
arealVelocity :: (Ord a, Floating a) => Orbit a -> Quantity [si|m^2/s|] a
arealVelocity o = qSqrt (l |*| μ) |/| 2
where l = semiLatusRectum o
μ = primaryGravitationalParameter o
hyperbolicDepartureAngle :: (Floating a, Ord a) => Orbit a -> Maybe (Angle a)
hyperbolicDepartureAngle o =
case classify o of
Hyperbolic ->
let e = eccentricity o
θ = addRad $ acos (-1 / e)
in Just θ
Parabolic -> Just (turn |/| 2)
_ -> Nothing
hyperbolicApproachAngle :: (Floating a, Ord a) => Orbit a -> Maybe (Angle a)
hyperbolicApproachAngle = fmap qNegate . hyperbolicDepartureAngle
timeAtMeanAnomaly :: (Floating a, Ord a) => Orbit a -> Angle a -> Time a
timeAtMeanAnomaly o _M = _M |/| n
where n = meanMotion o
timeAtEccentricAnomaly :: (Floating a, Ord a) => Orbit a -> Angle a -> Maybe (Time a)
timeAtEccentricAnomaly o = fmap (timeAtMeanAnomaly o) . meanAnomalyAtEccentricAnomaly o
timeAtTrueAnomaly :: (Real a, Floating a) => Orbit a -> Angle a -> Maybe (Time a)
timeAtTrueAnomaly o = fmap (timeAtMeanAnomaly o) . meanAnomalyAtTrueAnomaly o
meanAnomalyAtTime :: (Floating a, Ord a) => Orbit a -> Time a -> Angle a
meanAnomalyAtTime o t = t |*| n
where n = meanMotion o
meanAnomalyAtEccentricAnomaly :: (Floating a, Ord a) => Orbit a -> Angle a -> Maybe (Angle a)
meanAnomalyAtEccentricAnomaly o _E = case classify o of
Elliptic -> Just _M
_ -> Nothing
where e = eccentricity o
untypedE = delRad _E
_M = addRad (untypedE |-| e |*| sin untypedE)
meanAnomalyAtTrueAnomaly :: (Real a, Floating a)
=> Orbit a -> Angle a -> Maybe (Angle a)
meanAnomalyAtTrueAnomaly o = case classify o of
Elliptic -> meanAnomalyAtEccentricAnomaly o <=<
eccentricAnomalyAtTrueAnomaly o
_ -> error "TODO: meanAnomalyAtTrueAnomaly"
eccentricAnomalyAtTime :: (Converge [a], Floating a, Real a)
=> Orbit a -> Time a -> Maybe (Angle a)
eccentricAnomalyAtTime o t = case classify o of
Elliptic -> eccentricAnomalyAtMeanAnomaly o . meanAnomalyAtTime o $ t
_ -> Nothing
eccentricAnomalyAtMeanAnomaly :: forall a. (Converge [a], Floating a, Real a)
=> Orbit a -> Angle a -> Maybe (Angle a)
eccentricAnomalyAtMeanAnomaly o _M = case classify o of
Elliptic -> _E
_ -> Nothing
where (n, wrappedM) = second (# [si|rad|]) (_M `divMod'` turn)
e = eccentricity o # [si||]
_MFloat = rad . realToFrac $ wrappedM
oFloat = unsafeMapOrbit realToFrac o
initialGuessFloat :: Angle Float
Just initialGuessFloat = eccentricAnomalyAtMeanAnomalyFloat oFloat _MFloat
initialGuess = realToFrac . (# [si|rad|]) $ initialGuessFloat
err :: (Mode b, Floating b, Scalar b ~ a) => b -> b
err _E = auto wrappedM - (_E - auto e * sin _E)
wrappedE = fmap rad . convergeErr (runId . abs . err . Id) $
findZeroNoEq err initialGuess
_E = (|+| (unsafeMapUnit fromInteger n |*| turn)) <$> wrappedE
eccentricAnomalyAtMeanAnomalyFloat :: Orbit Float -> Angle Float -> Maybe (Angle Float)
eccentricAnomalyAtMeanAnomalyFloat o _M = case classify o of
Elliptic -> Just _E
_ -> Nothing
where wrappedM = (_M `mod'` turn) # [si|rad|]
e = eccentricity o # [si||]
sinM = sin wrappedM
cosM = cos wrappedM
initialGuess = wrappedM +
e * sinM +
e * e * sinM * cosM +
0.5 * e * e * e * sinM * (3 * cosM * cosM - 1)
_E :: Angle Float
_E = rad . last . take 5 $
findZero (\_E -> auto wrappedM - (_E - auto e * sin _E))
initialGuess
eccentricAnomalyAtTrueAnomaly :: (Floating a, Real a)
=> Orbit a -> Angle a -> Maybe (Angle a)
eccentricAnomalyAtTrueAnomaly o ν = case classify o of
Elliptic -> Just _E
_ -> Nothing
where (n, wrappedν) = ν `divMod'` turn
cosν = cos (ν # [si|rad|])
e = eccentricity o # [si||]
wrappedE = rad $ acos ((e + cosν) / (1 + e * cosν))
_E = if wrappedν < halfTurn
then (unsafeMapUnit fromInteger n |*| turn) |+| wrappedE
else (unsafeMapUnit fromInteger (n+1) |*| turn) |-| wrappedE
trueAnomalyAtTime :: (Converge [a], RealFloat a)
=> Orbit a -> Time a -> Maybe (Angle a)
trueAnomalyAtTime o = trueAnomalyAtMeanAnomaly o . meanAnomalyAtTime o
trueAnomalyAtMeanAnomaly :: (Converge [a], RealFloat a)
=> Orbit a -> Angle a -> Maybe (Angle a)
trueAnomalyAtMeanAnomaly o = trueAnomalyAtEccentricAnomaly o <=<
eccentricAnomalyAtMeanAnomaly o
trueAnomalyAtEccentricAnomaly :: RealFloat a
=> Orbit a
-> Angle a
-> Maybe (Angle a)
trueAnomalyAtEccentricAnomaly o _E = case classify o of
Elliptic -> Just ν
_ -> Nothing
where (n, wrappedE) = bimap (unsafeMapUnit fromInteger) (# [si|rad|]) $
_E `divMod'` turn
e = eccentricity o # [si||]
wrappedν = rad $ 2 * atan2 (sqrt (1 + e) * sin (wrappedE / 2))
(sqrt (1 - e) * cos (wrappedE / 2))
ν = turn |*| n |+| wrappedν
rad :: Fractional a => a -> Angle a
rad = (% [si|rad|])