packages feed

telescope-0.4.1: src/Telescope/Asdf/GWCS.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DefaultSignatures #-}

module Telescope.Asdf.GWCS where

import Data.Foldable (toList)
import Data.Kind (Type)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Massiv.Array (Array, Ix2)
import Data.Massiv.Array qualified as M
import Data.String (IsString)
import Data.Text (Text, pack)
import Data.Text qualified as T
import Data.Time.LocalTime (LocalTime)
import Effectful
import GHC.Generics
import Telescope.Asdf
import Telescope.Asdf.Core
import Telescope.Data.Parser
import Telescope.Data.WCS (WCSAxis (..))
import Text.Casing (quietSnake)


-- | GWCS pipelines consist of an input and output 'GWCSStep'
data GWCS inp out = GWCS (GWCSStep inp) (GWCSStep out)


instance (ToAsdf inp, ToAsdf out) => ToAsdf (GWCS inp out) where
  schema _ = "tag:stsci.edu:gwcs/wcs-1.2.0"
  toValue (GWCS inp out) =
    Object
      [ ("name", toNode $ String "")
      , ("steps", toNode $ Array [toNode inp, toNode out])
      ]


instance (FromAsdf inp, FromAsdf out) => FromAsdf (GWCS inp out) where
  parseValue = \case
    Object o -> do
      steps :: [Value] <- o .: "steps"
      case steps of
        [inpv, outv] -> do
          inp <- parseValue inpv
          out <- parseValue outv
          pure $ GWCS inp out
        other -> expected "GWCS steps: [input,output]" other
    val -> expected "GWCS" val


-- | A step contains a frame (like 'CelestialFrame') and a 'Transform a b'
data GWCSStep frame = GWCSStep
  { frame :: frame
  , transform :: Maybe Transformation
  }
  deriving (Generic, Show)


instance (ToAsdf frame) => ToAsdf (GWCSStep frame) where
  schema _ = "tag:stsci.edu:gwcs/step-1.1.0"
instance (FromAsdf frame) => FromAsdf (GWCSStep frame)


newtype AxisName = AxisName Text
  deriving newtype (IsString, ToAsdf, FromAsdf, Show, Semigroup, Eq)


newtype AxisType = AxisType Text
  deriving newtype (IsString)
instance ToAsdf AxisType where
  toValue (AxisType t) = String t


data Pix a
data Rot a


instance (ToAxes a) => ToAxes (Pix a) where
  toAxes = toAxes @a
instance (ToAxes a) => ToAxes (Scale a) where
  toAxes = fmap ("*" <>) (toAxes @a)
instance (ToAxes a) => ToAxes (Shift a) where
  toAxes = fmap ("+" <>) (toAxes @a)
instance (ToAxes a) => ToAxes (Rot a) where
  toAxes = fmap ("rot_" <>) (toAxes @a)
instance (ToAxes a) => ToAxes (Linear a) where
  toAxes = fmap ("lin_" <>) (toAxes @a)


newtype Lon = Lon Double
  deriving newtype (ToAsdf)
newtype Lat = Lat Double
  deriving newtype (ToAsdf)
newtype LonPole = LonPole Double
  deriving newtype (ToAsdf)


-- | A 'Tranform' with the types stripped, and the axes recorded
data Transformation = Transformation
  { inputs :: [AxisName]
  , outputs :: [AxisName]
  , forward :: Forward
  }
  deriving (Show, Eq)


instance ToAsdf Transformation where
  schema t = schema t.forward
  toValue t =
    toValue t.forward
      <> Object
        [ ("inputs", toNode t.inputs)
        , ("outputs", toNode t.outputs)
        ]


instance FromAsdf Transformation where
  parseNode (Node sch _ val) = do
    case val of
      Object o -> do
        inps <- o .: "inputs"
        outs <- o .: "outputs"
        frwd <- parseNode (Node sch Nothing (Object $ filter (not . isDirectKey) o))
        pure $ Transformation inps outs frwd
      other -> expected "Transformation" other
   where
    isDirectKey (k, _) =
      k == "inputs" || k == "outputs"


  -- parse a direct forward transformation using the logic defined in parseNode
  parseValue val = parseNode (Node mempty Nothing val)


data Forward
  = Compose Transformation Transformation
  | Concat Transformation Transformation
  | Direct Node
  deriving (Show, Eq)


instance ToAsdf Forward where
  schema (Compose _ _) = "!transform/compose-1.2.0"
  schema (Concat _ _) = "!transform/concatenate-1.2.0"
  schema (Direct node) = node.schema


  toValue = \case
    Compose a b -> Object [("forward", toNode [a, b])]
    Concat a b -> Object [("forward", toNode [a, b])]
    Direct node -> node.value


instance FromAsdf Forward where
  parseNode (Node sch _ val) = do
    case sch of
      "!transform/compose-1.2.0" -> parseCompose val
      "!transform/concatenate-1.2.0" -> parseConcat val
      _ -> parseDirect sch val


  parseValue v =
    -- parse it blindly: compose and concat might match the same one!
    runParserAlts (expected "Forward" v) $ do
      tryParserEmpty (parseCompose v) <|> tryParserEmpty (parseConcat v) <|> tryParserEmpty (parseDirect "..." v)


parseCompose :: (Parser :> es) => Value -> Eff es Forward
parseCompose = \case
  Object o -> do
    res <- o .: "forward"
    case res of
      [a, b] -> pure $ Compose a b
      fwd -> expected "Compose a b" fwd
  val -> expected "Compose a b" val


parseConcat :: (Parser :> es) => Value -> Eff es Forward
parseConcat = \case
  Object o -> do
    res <- o .: "forward"
    case res of
      [a, b] -> pure $ Concat a b
      fwd -> expected "Concat a b" fwd
  val -> expected "Concat a b" val


parseDirect :: (Parser :> es) => SchemaTag -> Value -> Eff es Forward
parseDirect sch val = pure $ Direct $ Node sch Nothing val


{- | A Transform specifies how we manipulate a type in a pipeline

> spatialTransform :: WCSAxis s X -> WCSAxis s Y -> Transform (Pix X, PixY) (Scale X, Scale Y)
> spatialTransform wcsx wcsy =
>   let dx = shift wcsx.crpix :: Transform (Pix X) (Shift X)
>       dy = shift wcsy.crpix :: Transform (Pix Y) (Shift Y)
>       xx = scale wcsx.cdelt :: Transform (Shift X) (Scale X)
>       xy = scale wcsy.cdelt :: Transform (Shift Y) (Scale Y)
>   in dx |> xx <&> dy |> xy
-}
data Transform b c = Transform
  { transformation :: Transformation
  }
  deriving (Show)


-- | Convert a type into a 'Transform' via 'ToAsdf' and 'ToAxes'
transform :: forall a bs cs. (ToAsdf a, ToAxes bs, ToAxes cs) => a -> Transform bs cs
transform a =
  Transform
    $ Transformation
      (toAxes @bs)
      (toAxes @cs)
    $ Direct (toNode a)


-- | Compose two transforms
(|>) :: forall b c d. (ToAxes b, ToAxes d) => Transform b c -> Transform c d -> Transform b d
(Transform s) |> (Transform t) =
  Transform
    $ Transformation
      (toAxes @b)
      (toAxes @d)
    $ Compose s t


infixr 5 |>


-- | Concatent two transforms
(<&>)
  :: forall (a :: Type) (b :: Type) (cs :: Type) (ds :: Type)
   . (ToAxes (TConcat a cs), ToAxes (TConcat b ds))
  => Transform a b
  -> Transform cs ds
  -> Transform (TConcat a cs) (TConcat b ds)
Transform s <&> Transform t =
  Transform
    $ Transformation
      (toAxes @(TConcat a cs))
      (toAxes @(TConcat b ds))
    $ Concat s t


infixr 4 <&>


data Direction
  = Pix2Sky
  | Native2Celestial
  deriving (Show)


instance ToAsdf Direction where
  toValue = String . T.toLower . pack . show


data Shift a = Shift Double deriving (Show, Eq)
data Scale a = Scale Double deriving (Show, Eq)
data Identity = Identity deriving (Show, Eq)
data Intercept = Intercept Double deriving (Show, Eq)
data Affine = Affine {matrix :: Array M.D Ix2 Double, translation :: (Double, Double)}
data Projection = Projection Direction
data Rotate3d = Rotate3d {direction :: Direction, phi :: Lon, theta :: Lat, psi :: LonPole}
  deriving (Generic)


-- TODO: this should be a quantity!
data Linear a = Linear1d {intercept :: Double, slope :: Double}
  deriving (Generic)
data Mapping = Mapping {mapping :: [Int]}
data Const1D = Const1D Quantity


instance ToAsdf Identity where
  schema _ = "!transform/identity-1.2.0"
  toValue _ = Object []


instance ToAsdf (Linear a) where
  schema _ = "!transform/linear1d-1.0.0"


instance ToAsdf (Shift a) where
  schema _ = "!transform/shift-1.2.0"
  toValue (Shift d) =
    Object [("offset", toNode d)]


instance ToAsdf (Scale a) where
  schema _ = "!transform/scale-1.2.0"
  toValue (Scale d) =
    Object [("factor", toNode d)]


instance ToAsdf Projection where
  schema _ = "!transform/gnomonic-1.2.0"
  toValue (Projection d) =
    Object [("direction", toNode d)]


instance ToAsdf Rotate3d where
  schema _ = "!transform/rotate3d-1.3.0"


instance ToAsdf Affine where
  schema _ = "!transform/affine-1.3.0"
  toValue a =
    let (tx, ty) = a.translation
     in Object
          [ ("matrix", toNode $ M.toLists a.matrix)
          , ("translation", toNode [tx, ty])
          ]


instance ToAsdf Mapping where
  schema _ = "!transform/remap_axes-1.3.0"
  toValue m =
    Object [("mapping", toNode $ toList m.mapping)]


instance ToAsdf Const1D where
  schema _ = "!transform/constant-1.2.0"
  toValue (Const1D q) =
    Object
      [ ("dimensions", toNode $ Integer 1)
      , ("value", toNode q)
      ]
instance FromAsdf Const1D where
  parseValue val = do
    o <- parseValue @Object val
    Const1D <$> o .: "value"


-- Frames -----------------------------------------------

data CoordinateFrame = CoordinateFrame
  { name :: Text
  , axes :: NonEmpty FrameAxis
  }
instance ToAsdf CoordinateFrame where
  schema _ = "tag:stsci.edu:gwcs/frame-1.0.0"
  toValue f =
    Object $
      [ ("name", toNode f.name)
      , ("axes_type", toNode $ fmap (.axisType) f.axes)
      ]
        <> frameAxesObject f.axes


data StokesFrame = StokesFrame
  { name :: Text
  , axisOrder :: Int
  }
instance ToAsdf StokesFrame where
  schema _ = "tag:stsci.edu:gwcs/stokes_frame-1.0.0"
  toValue f =
    Object
      [ ("name", toNode f.name)
      , ("axes_order", toNode [f.axisOrder])
      ]


data SpectralFrame = SpectralFrame
  { name :: Text
  , axisOrder :: Int
  }
instance ToAsdf SpectralFrame where
  schema _ = "tag:stsci.edu:gwcs/spectral_frame-1.0.0"
  toValue f =
    Object
      [ ("name", toNode f.name)
      , ("axes_names", toNode [String "wavelength"])
      , ("axes_order", toNode [f.axisOrder])
      , ("axis_physical_types", toNode [String "em.wl"])
      , ("unit", toNode [Nanometers])
      ]


data TemporalFrame = TemporalFrame
  { name :: Text
  , time :: LocalTime
  , axisOrder :: Int
  }
instance ToAsdf TemporalFrame where
  schema _ = "tag:stsci.edu:gwcs/temporal_frame-1.0.0"
  toValue f =
    Object
      [ ("name", toNode f.name)
      , ("axis_names", toNode [String "time"])
      , ("axes_order", toNode [f.axisOrder])
      , ("axis_physical_types", toNode [String "time"])
      , ("reference_frame", toNode f.time)
      , ("unit", toNode [Seconds])
      ]


data CelestialFrame ref = CelestialFrame
  { name :: Text
  , axes :: NonEmpty FrameAxis
  , referenceFrame :: ref
  }
instance (ToAsdf ref) => ToAsdf (CelestialFrame ref) where
  schema _ = "tag:stsci.edu:gwcs/celestial_frame-1.0.0"
  toValue f =
    Object $
      [ ("name", toNode f.name)
      , ("reference_frame", toNode f.referenceFrame)
      ]
        <> frameAxesObject f.axes


frameAxesObject :: NonEmpty FrameAxis -> Object
frameAxesObject as =
  -- doesn't include axes_type, only on CoorindateFrame
  [ ("naxes", toNode $ NE.length as)
  , ("axes_names", toNode axesNames)
  , ("axes_order", toNode axesOrders)
  , ("axis_physical_types", toNode axesPhysicalTypes)
  , ("unit", toNode units)
  ]
 where
  axesNames = fmap (.axisName) as
  axesOrders = fmap (.axisOrder) as
  axesPhysicalTypes = fmap (physicalType . (.axisType)) as
  units = fmap (.unit) as
  physicalType = toValue


-- numAxes = NE.length as

data ICRSFrame = ICRSFrame
instance ToAsdf ICRSFrame where
  schema _ = "tag:astropy.org:astropy/coordinates/frames/icrs-1.1.0"
  toValue _ = Object [("frame_attributes", toNode $ Object mempty)]


data HelioprojectiveFrame = HelioprojectiveFrame
  { coordinates :: Cartesian3D
  , observation :: HelioObservation
  }
instance ToAsdf HelioprojectiveFrame where
  schema _ = "tag:sunpy.org:sunpy/coordinates/frames/helioprojective-1.0.0"
  toValue frame =
    Object
      [("frame_attributes", fromValue attributes)]
   where
    observer = HelioObserver (CartesianRepresentation frame.coordinates) frame.observation
    attributes =
      Object [("observer", toNode observer)] <> toValue frame.observation
instance FromAsdf HelioprojectiveFrame where
  parseValue = \case
    Object o -> do
      atts :: Object <- o .: "frame_attributes"
      observer :: HelioObserver <- atts .: "observer"
      observation <- parseValue (Object atts)
      let CartesianRepresentation coords = observer.coordinates
      pure $ HelioprojectiveFrame coords observation
    other -> expected "helioprojective frame" other


-- reference_frame: !<tag:sunpy.org:sunpy/coordinates/frames/helioprojective-1.0.0>
--   frame_attributes:
--     observer: !<tag:sunpy.org:sunpy/coordinates/frames/heliographic_stonyhurst-1.1.0>
--       data: !<tag:astropy.org:astropy/coordinates/representation-1.0.0>
--         components:
--           x: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 m, value: 151741639088.53842}
--           y: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 m, value: 5050819.209579468}
--           z: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 m, value: -968701275.8656464}
--         type: CartesianRepresentation
--       frame_attributes:
--         obstime: !time/time-1.1.0 2022-06-03T17:52:20.031
--         rsun: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 km, value: 695700.0}
--     obstime: !time/time-1.1.0 2022-06-03T17:52:20.031
--     rsun: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 km, value: 695700.0}

data HelioObserver = HelioObserver
  { coordinates :: CartesianRepresentation Cartesian3D
  , observation :: HelioObservation
  }
instance ToAsdf HelioObserver where
  --     observer: !<tag:sunpy.org:sunpy/coordinates/frames/heliographic_stonyhurst-1.1.0>
  --       data: !<tag:astropy.org:astropy/coordinates/representation-1.0.0>
  --         components:
  --           x: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 m, value: 151741639088.53842}
  --           y: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 m, value: 5050819.209579468}
  --           z: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 m, value: -968701275.8656464}
  --         type: CartesianRepresentation
  --       frame_attributes:
  --         obstime: !time/time-1.1.0 2022-06-03T17:52:20.031
  --         rsun: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 km, value: 695700.0}
  schema _ = "tag:sunpy.org:sunpy/coordinates/frames/heliographic_stonyhurst-1.1.0"
  toValue obs =
    Object
      [ ("data", toNode obs.coordinates)
      , ("frame_attributes", toNode obs.observation)
      ]
instance FromAsdf HelioObserver where
  parseValue = \case
    Object o -> do
      d <- o .: "data"
      atts <- o .: "frame_attributes"
      pure $ HelioObserver d atts
    other -> expected "Helioobserver" other


data HelioObservation = HelioObservation
  { obstime :: LocalTime
  , rsun :: Quantity
  }
  deriving (Generic, ToAsdf, FromAsdf)


data Cartesian3D = Cartesian3D
  { x :: Quantity
  , y :: Quantity
  , z :: Quantity
  }
  deriving (Generic, ToAsdf, FromAsdf)


data CartesianRepresentation dims = CartesianRepresentation dims
instance (ToAsdf dims) => ToAsdf (CartesianRepresentation dims) where
  -- data: !<tag:astropy.org:astropy/coordinates/representation-1.0.0>
  --   components:
  --     x: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 m, value: 151741639088.53842}
  --     y: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 m, value: 5050819.209579468}
  --     z: !unit/quantity-1.1.0 {unit: !unit/unit-1.0.0 m, value: -968701275.8656464}
  --   type: CartesianRepresentation
  schema _ = "tag:astropy.org:astropy/coordinates/representation-1.0.0"
  toValue (CartesianRepresentation dims) =
    Object [("components", toNode dims), ("type", "CartesianRepresentation")]
instance (FromAsdf dims) => FromAsdf (CartesianRepresentation dims) where
  parseValue = \case
    Object o -> do
      CartesianRepresentation <$> o .: "components"
    other -> expected "CartesianRepresentation" other


data FrameAxis = FrameAxis
  { axisOrder :: Int
  , axisName :: AxisName
  , axisType :: AxisType
  , unit :: Unit
  }


data CompositeFrame as = CompositeFrame {frames :: as}
instance (ToAsdf as) => ToAsdf (CompositeFrame as) where
  schema _ = "tag:stsci.edu:gwcs/composite_frame-1.0.0"
  toValue (CompositeFrame as) =
    Object
      [ ("name", toNode $ String "CompositeFrame")
      , ("frames", toNode as)
      ]
instance (FromAsdf as) => FromAsdf (CompositeFrame as) where
  parseValue = \case
    Object o -> do
      CompositeFrame <$> o .: "name"
    other -> expected "CompositeFrame" other


-- ToAxes -----------------------------------------------

{- | Convert a type to named axes

> data X deriving (Generic, ToAxes)
> data Y
> instance ToAxes Y where
>   toAxes = ["y"]
-}
class ToAxes (as :: Type) where
  toAxes :: [AxisName]
  default toAxes :: (Generic as, GTypeName (Rep as)) => [AxisName]
  toAxes = [AxisName $ pack $ quietSnake $ gtypeName (from (undefined :: as))]


instance ToAxes () where
  toAxes = []
instance (ToAxes a, ToAxes b) => ToAxes (a, b) where
  toAxes = mconcat [toAxes @a, toAxes @b]
instance (ToAxes a, ToAxes b, ToAxes c) => ToAxes (a, b, c) where
  toAxes = mconcat [toAxes @a, toAxes @b, toAxes @c]
instance (ToAxes a, ToAxes b, ToAxes c, ToAxes d) => ToAxes (a, b, c, d) where
  toAxes = mconcat [toAxes @a, toAxes @b, toAxes @c, toAxes @d]
instance (ToAxes a, ToAxes b, ToAxes c, ToAxes d, ToAxes e) => ToAxes (a, b, c, d, e) where
  toAxes = mconcat [toAxes @a, toAxes @b, toAxes @c, toAxes @d, toAxes @e]
instance (ToAxes a, ToAxes b, ToAxes c, ToAxes d, ToAxes e, ToAxes f) => ToAxes (a, b, c, d, e, f) where
  toAxes = mconcat [toAxes @a, toAxes @b, toAxes @c, toAxes @d, toAxes @e, toAxes @f]


-- Transforms -----------------------------------------------

shift :: forall a f. (ToAxes (f a), ToAxes (Shift a)) => Double -> Transform (f a) (Shift a)
shift d = transform $ Shift d


scale :: forall a f. (ToAxes (f a), ToAxes (Scale a)) => Double -> Transform (f a) (Scale a)
scale d = transform $ Scale d


linear :: forall a. (ToAxes a) => Intercept -> Scale a -> Transform (Pix a) (Linear a)
linear (Intercept dlt) (Scale scl) = transform $ Linear1d{intercept = dlt, slope = scl}


rotate :: (ToAxes x, ToAxes y) => Array M.D Ix2 Double -> Transform (Linear x, Linear y) (Rot (x, y))
rotate arr =
  transform $ Affine arr (0, 0)


project :: (ToAxes x, ToAxes y) => Direction -> Transform (Rot (x, y)) (Phi, Theta)
project dir =
  transform $ Projection dir


celestial :: Lat -> Lon -> LonPole -> Transform (Phi, Theta) (Alpha, Delta)
celestial lat lon pole =
  transform $ Rotate3d{direction = Native2Celestial, theta = lat, phi = lon, psi = pole}


data Phi deriving (Generic, ToAxes)
data Theta deriving (Generic, ToAxes)
data Alpha deriving (Generic, ToAxes)
data Delta deriving (Generic, ToAxes)


identity :: (ToAxes a) => Transform a a
identity = transform Identity


-- WCS Transforms ---------------------------------------------------------

wcsLinear :: (ToAxes axis) => WCSAxis alt axis -> Transform (Pix axis) (Linear axis)
wcsLinear wcs = linear (wcsIntercept wcs) (Scale wcs.cdelt)


-- the Y intercept
wcsIntercept :: WCSAxis alt axis -> Intercept
wcsIntercept w =
  -- crpix is 1-indexed, need to switch to zero
  Intercept $ w.crval - w.cdelt * (w.crpix - 1)


-- | Generic NodeName
class GTypeName f where
  gtypeName :: f p -> String


instance (Datatype d) => GTypeName (D1 d f) where
  gtypeName = datatypeName


type family TConcat a b where
  TConcat a (b, c, d, e, f) = (a, b, c, d, e, f)
  TConcat (a, b) (c, d, e, f) = (a, b, c, d, e, f)
  TConcat (a, b, c) (d, e, f) = (a, b, c, d, e, f)
  TConcat (a, b, c, d) (e, f) = (a, b, c, d, e, f)
  TConcat (a, b, c, d, e) f = (a, b, c, d, e, f)
  TConcat a (b, c, d, e) = (a, b, c, d, e)
  TConcat (a, b) (c, d, e) = (a, b, c, d, e)
  TConcat (a, b, c) (d, e) = (a, b, c, d, e)
  TConcat (a, b, c, d) e = (a, b, c, d, e)
  TConcat a (b, c, d) = (a, b, c, d)
  TConcat (a, b) (c, d) = (a, b, c, d)
  TConcat (a, b, c) d = (a, b, c, d)
  TConcat a (b, c) = (a, b, c)
  TConcat (a, b) c = (a, b, c)
  TConcat a () = a
  TConcat () b = b
  TConcat a b = (a, b)