packages feed

proarrow-0.4.0.0: src/Proarrow/Tools/Optic.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

-- | Compile resource-aware SMC programs of optics to forward/backward computations.
-- A wire @'(a, da)@ carries its forward and backward objects. 'optic' and 'opticLegs'
-- expand each primitive to two boxes joined by its residual; 'runOptics' connects them
-- by graph substitution and reads the computation into the target symmetric monoidal category.
-- No trace or Frobenius structure is required of the target.
--
-- For example, an SMC program on @Wires '[ '(Double, Double)]@ can compose the
-- primitive @opticLegs \@Double (\x -> (x, x*x)) (\(x, dy) -> 2*x*dy)@ with
-- @opticLegs \@Double (\x -> (cos x, sin x)) (uncurry (*))@. Applying 'runOptics'
-- gives @(x, dy) -> (sin (x*x), 2*x*cos (x*x)*dy)@. A primitive with several output
-- wires supports an ordinary SMC tuple pattern. An explicit duplication optic can
-- copy a forward value and add its two cotangents on the backward pass.
--
-- This is the optic translation in §10 of Wilson and Zanasi,
-- <https://arxiv.org/abs/2305.01041 Data-Parallel Algorithms for String Diagrams>.
-- The source program may compose, tensor and permute its wires. Copying, discarding,
-- merging and feedback are rejected when their translated wiring needs stronger structure.
module Proarrow.Tools.Optic (OPTICS, Forward, Backward, optic, opticLegs, runOptics) where

import Data.Map.Strict qualified as M
import Prelude hiding (id, (**), (.))

import Proarrow.Category.Instance.DecoratedCospan (DECCOSPAN (..), DecCospan (..))
import Proarrow.Category.Instance.FinHask (FinHask (..))
import Proarrow.Category.Instance.OpenHypergraph
  ( Box (..)
  , Boxes (..)
  , OPENHG
  , Port (..)
  , Prim
  , SomeHypergraph (..)
  , SomeSort
  , SortList
  , SortOb
  , Sorts
  , WireSorts
  , Wires
  , box
  , permutations
  , reifySorts
  , simplify
  , someArrow
  , sortList
  , sorts
  , unsafeOpenHypergraph
  , unsafePrim
  , unsafeSubstituteWith
  )
import Proarrow.Category.Instance.Product (Fst, Snd)
import Proarrow.Category.Instance.Sub (Sub (..))
import Proarrow.Category.Monoidal (Monoidal (..), MonoidalProfunctor (..), SymMonoidal, Tensor)
import Proarrow.Category.Monoidal.Strictified
  ( Fold
  , Strictified (..)
  , concatMany
  , splitMany
  , withIsListOf
  , type (++)
  )
import Proarrow.Core (CategoryOf (..), Kind, Promonad (..), obj)
import Proarrow.Functor (FunctorForRep (type (@)))
import Proarrow.Object (ListOf (..), SomeOf (..), withKnownAppend, withKnownListOf, pattern Objs)
import Proarrow.Optic (ExOptic, Optic, withLegs)
import Proarrow.Optic.Action (ActFl (..))

-- | Graphs of typed optic primitives. Build boxes with 'optic' or 'opticLegs'; reattaching
-- their hidden labels to other sorts breaks the type agreement used by 'runOptics'.
type OPTICS :: Kind -> Kind
type OPTICS k = OPENHG (k, k) (OpticPrim k)

newtype OpticPrim k = OpticPrim (SomeHypergraph k (Prim k))

-- | The forward objects of a list of optic wires, in boundary order.
type Forward :: [(k, k)] -> [k]
type family Forward xs where
  Forward '[] = '[]
  Forward ('(a, da) ': xs) = a ': Forward xs

-- | The backward objects of a list of optic wires, in boundary order.
type Backward :: [(k, k)] -> [k]
type family Backward xs where
  Backward '[] = '[]
  Backward ('(a, da) ': xs) = da ': Backward xs

-- | A primitive from an existing tensor-action optic, in any encoding supported by 'withLegs'.
optic
  :: forall {k} c (xs :: [(k, k)]) ys
   . ( SymMonoidal k
     , SortList xs
     , SortList ys
     , (Ob (Fold (Forward ys)), Ob (Fold (Backward ys))) => c (ExOptic (ActFl Tensor) (Fold (Forward ys)) (Fold (Backward ys)))
     )
  => Optic c (Fold (Forward xs)) (Fold (Backward xs)) (Fold (Forward ys)) (Fold (Backward ys))
  -> (Wires xs :: OPTICS k) ~> Wires ys
optic o@Objs =
  withLegs @(ActFl Tensor) o \l r -> withActP @Tensor l r \ @m f g -> opticLegs @m @xs @ys f g

-- | A primitive from its two legs, with residual @m@. The residual is produced once by
-- the forward leg and consumed by the backward leg; it needs no comonoid structure.
-- The forward and backward lists of ports are folded with the target's tensor.
opticLegs
  :: forall {k} (m :: k) (xs :: [(k, k)]) ys
   . (SymMonoidal k, Ob m, SortList xs, SortList ys)
  => (Fold (Forward xs) ~> m ** Fold (Forward ys))
  -> (m ** Fold (Backward ys) ~> Fold (Backward xs))
  -> (Wires xs :: OPTICS k) ~> Wires ys
opticLegs f g = withHalves (sorts @_ @xs) $
  withHalves (sorts @_ @ys) $
    case (concatMany @(Forward xs), splitMany @(Forward ys), concatMany @(Backward ys), splitMany @(Backward xs)) of
      (joinForward@Objs, splitForward@Objs, joinBackward@Objs, splitBackward@Objs) ->
        let inputs = concatMap expand (sortList @_ @xs)
            outputs = concatMap expand (sortList @_ @ys)
            ni = length inputs
            no = length outputs
            residual = ni + no
            forward =
              (obj @'[m] ** splitForward) . Str @'[Fold (Forward xs)] @'[m, Fold (Forward ys)] f . joinForward
            backward =
              splitBackward
                . Str @'[m, Fold (Backward ys)] @'[Fold (Backward xs)] g
                . (obj @'[m] ** joinBackward)
            boxes =
              [ Box (unsafePrim (someArrow forward)) [0, 2 .. ni - 1] (residual : [ni, ni + 2 .. residual - 1])
              , Box (unsafePrim (someArrow backward)) (residual : [ni + 1, ni + 3 .. residual - 1]) [1, 3 .. ni - 1]
              ]
            replacement = reifySorts inputs \ @as -> reifySorts outputs \ @bs ->
              SomeHypergraph
                (unsafeOpenHypergraph @(Prim k) @as @bs (inputs ++ outputs ++ [Some @m]) [0 .. ni - 1] [ni .. residual - 1] boxes)
        in box (OpticPrim replacement)

-- | Expand the primitives, move the external backward wires to the other boundary, and
-- read back using 'permutations'. The inputs are the forward source and backward target;
-- the outputs are the forward target and backward source. Empty boundaries fold to the unit.
runOptics
  :: forall {k} (xs :: [(k, k)]) ys
   . (SymMonoidal k)
  => (Wires xs :: OPTICS k) ~> Wires ys
  -> Either String ((Fold (Forward xs) ** Fold (Backward ys)) ~> (Fold (Forward ys) ** Fold (Backward xs)))
runOptics graph@Objs =
  withHalves (sorts @_ @xs) $
    withHalves (sorts @_ @ys) $
      withKnownAppend @SortOb @(Forward xs) @(Backward ys) $
        withKnownAppend @SortOb @(Forward ys) @(Backward xs) $ do
          SomeHypergraph (DecCospan @c (Sub (FinHask ins)) (Sub (FinHask outs)) (Boxes boxes)) <-
            unsafeSubstituteWith expand (\(OpticPrim g) -> g) graph
          let unPort (Port i) = i
              inputs = map unPort (M.elems ins)
              outputs = map unPort (M.elems outs)
              evenPorts = everyOther
              oddPorts = everyOther . drop 1
              computation =
                unsafeOpenHypergraph @(Prim k)
                  @(Forward xs ++ Backward ys)
                  @(Forward ys ++ Backward xs)
                  (sortList @_ @(WireSorts (DC c)))
                  (evenPorts inputs ++ oddPorts outputs)
                  (evenPorts outputs ++ oddPorts inputs)
                  (map (fmap unPort) boxes)
          result <- simplify permutations computation
          pure $
            unStr
              ((concatMany @(Forward ys) ** concatMany @(Backward xs)) . result . (splitMany @(Forward xs) ** splitMany @(Backward ys)))
  where
    everyOther [] = []
    everyOther (x : rest) = x : everyOther (drop 1 rest)

expand :: (CategoryOf k) => SomeSort (k, k) -> [SomeSort k]
expand (Some @p) = [Some @(Fst @ p), Some @(Snd @ p)]

withHalves
  :: forall {k} xs r
   . (CategoryOf k)
  => Sorts (xs :: [(k, k)])
  -> ( ( SortList (Forward xs)
       , SortList (Backward xs)
       , Ob (Forward xs)
       , Ob (Backward xs)
       )
       => r
     )
  -> r
withHalves xs k = go xs \as ds ->
  withKnownListOf as $
    withKnownListOf ds $
      withIsListOf as $
        withIsListOf ds k
  where
    go :: Sorts (zs :: [(k, k)]) -> (Sorts (Forward zs) -> Sorts (Backward zs) -> r) -> r
    go Nil next = next Nil Nil
    go (Cons @p rest) next = go rest \as ds -> next (Cons @(Fst @ p) as) (Cons @(Snd @ p) ds)