packages feed

hydra-0.15.0: src/main/haskell/Hydra/Sources/Kernel/Terms/Differentiation.hs

module Hydra.Sources.Kernel.Terms.Differentiation where

-- Standard imports for kernel terms modules
import Hydra.Kernel
import Hydra.Sources.Libraries
import qualified Hydra.Dsl.Paths    as Paths
import qualified Hydra.Dsl.Annotations       as Annotations
import qualified Hydra.Dsl.Ast          as Ast
import qualified Hydra.Dsl.Bootstrap         as Bootstrap
import qualified Hydra.Dsl.Coders       as Coders
import qualified Hydra.Dsl.Util      as Util
import qualified Hydra.Dsl.Meta.Core         as Core
import qualified Hydra.Dsl.Meta.Graph        as Graph
import qualified Hydra.Dsl.Json.Model         as Json
import qualified Hydra.Dsl.Meta.Lib.Chars    as Chars
import qualified Hydra.Dsl.Meta.Lib.Eithers  as Eithers
import qualified Hydra.Dsl.Meta.Lib.Equality as Equality
import qualified Hydra.Dsl.Meta.Lib.Lists    as Lists
import qualified Hydra.Dsl.Meta.Lib.Literals as Literals
import qualified Hydra.Dsl.Meta.Lib.Logic    as Logic
import qualified Hydra.Dsl.Meta.Lib.Maps     as Maps
import qualified Hydra.Dsl.Meta.Lib.Math     as Math
import qualified Hydra.Dsl.Meta.Lib.Maybes   as Maybes
import qualified Hydra.Dsl.Meta.Lib.Pairs    as Pairs
import qualified Hydra.Dsl.Meta.Lib.Sets     as Sets
import           Hydra.Dsl.Meta.Lib.Strings  as Strings
import qualified Hydra.Dsl.Literals          as Literals
import qualified Hydra.Dsl.LiteralTypes      as LiteralTypes
import qualified Hydra.Dsl.Meta.Base         as MetaBase
import qualified Hydra.Dsl.Meta.Terms        as MetaTerms
import qualified Hydra.Dsl.Meta.Types        as MetaTypes
import qualified Hydra.Dsl.Packaging       as Packaging
import qualified Hydra.Dsl.Parsing      as Parsing
import           Hydra.Dsl.Meta.Phantoms     as Phantoms
import qualified Hydra.Dsl.Prims             as Prims
import qualified Hydra.Dsl.Meta.Tabular           as Tabular
import qualified Hydra.Dsl.Meta.Testing      as Testing
import qualified Hydra.Dsl.Terms             as Terms
import qualified Hydra.Dsl.Tests             as Tests
import qualified Hydra.Dsl.Topology     as Topology
import qualified Hydra.Dsl.Types             as Types
import qualified Hydra.Dsl.Typing       as Typing
import qualified Hydra.Dsl.Util         as Util
import qualified Hydra.Dsl.Meta.Variants     as Variants
import           Hydra.Sources.Kernel.Types.All
import           Prelude hiding ((++))
import qualified Data.Int                    as I
import qualified Data.List                   as L
import qualified Data.Map                    as M
import qualified Data.Set                    as S
import qualified Data.Maybe                  as Y

-- Deep (program-constructing) DSLs
import qualified Hydra.Dsl.Deep.Lib.Math     as DeepMath
import qualified Hydra.Dsl.Meta.DeepCore     as DeepCore

import qualified Hydra.Sources.Kernel.Terms.Strip as Strip
import qualified Hydra.Sources.Kernel.Terms.Variables as Variables


ns :: Namespace
ns = Namespace "hydra.differentiation"

define :: String -> TTerm a -> TTermDefinition a
define = definitionInNamespace ns

module_ :: Module
module_ = Module {
            moduleNamespace = ns,
            moduleDefinitions = definitions,
            moduleTermDependencies = [Strip.ns, Variables.ns],
            moduleTypeDependencies = kernelTypesNamespaces,
            moduleDescription = Just "Source-to-source automatic differentiation for Float64 terms."}
  where
    definitions = [
      toDefinition differentiateBinary,
      toDefinition differentiateFunction,
      toDefinition differentiateTerm,
      toDefinition gradient,
      toDefinition primitiveDerivative]

-- Helper: construct a float64 literal term from a Haskell Double
f64 :: Double -> TTerm Term
f64 = MetaTerms.float64


-- | Differentiate a function term (Float64 -> Float64) with respect to its parameter.
--   Given a lambda \x -> body, returns a lambda \x -> d(body)/dx.
differentiateFunction :: TTermDefinition (Term -> Term)
differentiateFunction = define "differentiateFunction" $
  doc "Differentiate a function term (Float64 -> Float64) with respect to its parameter" $
  "term" ~>
  cases _Term (var "term")
    (Just $ var "term") [  -- Non-function terms pass through unchanged
    _Term_annotated>>: "at" ~>
      differentiateFunction @@ Core.annotatedTermBody (var "at"),
    _Term_lambda>>: "l" ~>
      "paramName" <~ Core.lambdaParameter (var "l") $
      "body" <~ Core.lambdaBody (var "l") $
      DeepCore.lambdaTyped
        (var "paramName")
        (Core.lambdaDomain $ var "l")
        (differentiateTerm @@ var "paramName" @@ var "body")]

-- | Differentiate a term with respect to a named variable.
--   Implements the standard rules of differential calculus as a source-to-source
--   transformation on Hydra terms.
differentiateTerm :: TTermDefinition (Name -> Term -> Term)
differentiateTerm = define "differentiateTerm" $
  doc "Differentiate a term with respect to a named variable" $
  "dx" ~> "term" ~>
  cases _Term (var "term") Nothing [
    -- Variable: d/dx(x) = 1.0, d/dx(y) = 0.0
    _Term_variable>>: "v" ~>
      Logic.ifElse (Equality.equal (var "v") (var "dx"))
        (f64 1.0)
        (f64 0.0),

    -- Literal: d/dx(c) = 0.0
    _Term_literal>>: constant $ f64 0.0,

    -- Application: handle primitive functions and general chain rule
    _Term_application>>: "app" ~>
      "func" <~ Core.applicationFunction (var "app") $
      "arg" <~ Core.applicationArgument (var "app") $
      -- Check if the function is a variable (potentially a primitive)
      cases _Term (var "func") (Just $
        -- General case: f(g) => f'(g) * g'
        DeepMath.mulFloat64
          (differentiateTerm @@ var "dx" @@ Core.termApplication (Core.application (var "func") (var "arg")))
          (differentiateTerm @@ var "dx" @@ var "arg")) [
        _Term_variable>>: "fname" ~>
          -- Check if this is a known unary primitive
          optCases (primitiveDerivative @@ var "fname")
            -- Not a known primitive: general application derivative
            (differentiateTerm @@ var "dx" @@ Core.termApplication (Core.application (var "func") (var "arg")))
            -- Known unary primitive: chain rule f(g(x))' = f'(g(x)) * g'(x)
            ("derivTerm" ~>
              DeepMath.mulFloat64
                (Core.termApplication $ Core.application (var "derivTerm") (var "arg"))
                (differentiateTerm @@ var "dx" @@ var "arg")),
        -- Partially applied binary primitive: f a b => handle add, mul, sub, pow, etc.
        _Term_application>>: "innerApp" ~>
          "innerFunc" <~ Core.applicationFunction (var "innerApp") $
          "innerArg" <~ Core.applicationArgument (var "innerApp") $
          cases _Term (var "innerFunc")
            (Just $
              -- Not a variable: fall back to general differentiation
              DeepMath.mulFloat64
                (differentiateTerm @@ var "dx" @@ Core.termApplication (Core.application (var "func") (var "arg")))
                (differentiateTerm @@ var "dx" @@ var "arg")) [
            _Term_variable>>: "bfname" ~>
              -- Binary primitive detected: innerFunc innerArg arg = bfname(innerArg, arg)
              differentiateBinary @@ var "bfname"
                @@ var "innerArg"
                @@ var "arg"
                @@ (differentiateTerm @@ var "dx" @@ var "innerArg")
                @@ (differentiateTerm @@ var "dx" @@ var "arg")]],

    -- Function (lambda): d/dx(\y -> body) = \y -> d/dx(body)
    -- (The bound variable y is different from x, so we differentiate the body)
    _Term_lambda>>: "l" ~>
      Logic.ifElse (Equality.equal (Core.lambdaParameter $ var "l") (var "dx"))
        -- Lambda binds the differentiation variable: derivative is zero function
        (DeepCore.lambdaTyped
          (Core.lambdaParameter $ var "l")
          (Core.lambdaDomain $ var "l")
          (f64 0.0))
        -- Lambda binds a different variable: differentiate the body
        (DeepCore.lambdaTyped
          (Core.lambdaParameter $ var "l")
          (Core.lambdaDomain $ var "l")
          (differentiateTerm @@ var "dx" @@ Core.lambdaBody (var "l"))),
    _Term_cases>>: constant $ f64 0.0,
    _Term_project>>: constant $ f64 0.0,
    _Term_unwrap>>: constant $ f64 0.0,

    -- Let: differentiate bindings and body
    _Term_let>>: "l" ~>
      Core.termLet $ Core.let_
        (Lists.map ("b" ~>
          Core.binding
            (Core.bindingName $ var "b")
            (differentiateTerm @@ var "dx" @@ Core.bindingTerm (var "b"))
            nothing)
          (Core.letBindings $ var "l"))
        (differentiateTerm @@ var "dx" @@ Core.letBody (var "l")),

    -- Annotated: strip annotation and differentiate
    _Term_annotated>>: "at" ~>
      differentiateTerm @@ var "dx" @@ Core.annotatedTermBody (var "at"),

    -- List: element-wise differentiation
    _Term_list>>: "elems" ~>
      Core.termList $ Lists.map (differentiateTerm @@ var "dx") (var "elems"),

    -- Pair: component-wise differentiation
    _Term_pair>>: "p" ~>
      Core.termPair $ pair
        (differentiateTerm @@ var "dx" @@ Pairs.first (var "p"))
        (differentiateTerm @@ var "dx" @@ Pairs.second (var "p")),

    -- Record: differentiate each field
    _Term_record>>: "r" ~>
      Core.termRecord $ Core.record
        (Core.recordTypeName $ var "r")
        (Lists.map ("fld" ~>
          Core.field
            (Core.fieldName $ var "fld")
            (differentiateTerm @@ var "dx" @@ Core.fieldTerm (var "fld")))
          (Core.recordFields $ var "r")),

    -- TypeApplication: pass through
    _Term_typeApplication>>: "ta" ~>
      differentiateTerm @@ var "dx" @@ Core.typeApplicationTermBody (var "ta"),

    -- TypeLambda: pass through
    _Term_typeLambda>>: "tl" ~>
      differentiateTerm @@ var "dx" @@ Core.typeLambdaBody (var "tl"),

    -- Unit, Set, Map, Either, Maybe, Union, Wrap: derivative is zero / unsupported
    _Term_unit>>: constant $ f64 0.0,
    _Term_set>>: constant $ f64 0.0,
    _Term_map>>: constant $ f64 0.0,
    _Term_either>>: constant $ f64 0.0,
    _Term_maybe>>: constant $ f64 0.0,
    _Term_inject>>: constant $ f64 0.0,
    _Term_wrap>>: constant $ f64 0.0]

-- | Differentiate a binary primitive application.
--   bfname is the primitive name, a and b are the two arguments,
--   da and db are their derivatives with respect to x.
--   Returns d/dx(bfname(a, b)).
differentiateBinary :: TTermDefinition (Name -> Term -> Term -> Term -> Term -> Term)
differentiateBinary = define "differentiateBinary" $
  doc "Differentiate a binary primitive application given both arguments and their derivatives" $
  "bfname" ~> "a" ~> "b" ~> "da" ~> "db" ~>
    -- d/dx(a + b) = da + db (both Int32 and Float64 names)
    Logic.ifElse (Logic.or (Equality.equal (var "bfname") (encodedName _math_add))
                           (Equality.equal (var "bfname") (encodedName _math_addFloat64)))
        (DeepMath.addFloat64 (var "da") (var "db")) $
      -- d/dx(a - b) = da - db
      Logic.ifElse (Logic.or (Equality.equal (var "bfname") (encodedName _math_sub))
                             (Equality.equal (var "bfname") (encodedName _math_subFloat64)))
        (DeepMath.subFloat64 (var "da") (var "db")) $
      -- d/dx(a * b) = a*db + b*da  (product rule)
      Logic.ifElse (Logic.or (Equality.equal (var "bfname") (encodedName _math_mul))
                             (Equality.equal (var "bfname") (encodedName _math_mulFloat64)))
        (DeepMath.addFloat64
          (DeepMath.mulFloat64 (var "a") (var "db"))
          (DeepMath.mulFloat64 (var "b") (var "da"))) $
      -- d/dx(a ^ b) = a^b * (b*da/a + db*ln(a))  (general power rule)
      Logic.ifElse (Equality.equal (var "bfname") (encodedName _math_pow))
        (DeepMath.mulFloat64
          (DeepMath.pow (var "a") (var "b"))
          (DeepMath.addFloat64
            (DeepMath.mulFloat64 (var "db") (DeepMath.log (var "a")))
            (DeepMath.mulFloat64
              (DeepMath.mulFloat64 (var "b") (var "da"))
              (DeepMath.pow (var "a") (f64 (-1.0)))))) $
      -- d/dx(atan2(a, b)) = (b*da - a*db) / (a^2 + b^2)
      Logic.ifElse (Equality.equal (var "bfname") (encodedName _math_atan2))
        (DeepMath.mulFloat64
          (DeepMath.subFloat64
            (DeepMath.mulFloat64 (var "b") (var "da"))
            (DeepMath.mulFloat64 (var "a") (var "db")))
          (DeepMath.pow
            (DeepMath.addFloat64
              (DeepMath.mulFloat64 (var "a") (var "a"))
              (DeepMath.mulFloat64 (var "b") (var "b")))
            (f64 (-1.0)))) $
      -- d/dx(logBase(a, b)) = d/dx(ln(b)/ln(a))
      -- = (da*ln(b) is wrong)... use: logBase(a,b) = ln(b)/ln(a)
      -- d/dx = (ln(a)*db/b - ln(b)*da/a) / (ln(a))^2
      Logic.ifElse (Equality.equal (var "bfname") (encodedName _math_logBase))
        (DeepMath.mulFloat64
          (DeepMath.subFloat64
            (DeepMath.mulFloat64
              (DeepMath.log (var "a"))
              (DeepMath.mulFloat64 (var "db") (DeepMath.pow (var "b") (f64 (-1.0)))))
            (DeepMath.mulFloat64
              (DeepMath.log (var "b"))
              (DeepMath.mulFloat64 (var "da") (DeepMath.pow (var "a") (f64 (-1.0))))))
          (DeepMath.pow
            (DeepMath.mulFloat64 (DeepMath.log (var "a")) (DeepMath.log (var "a")))
            (f64 (-1.0)))) $
      -- Unknown binary primitive: return 0
      f64 0.0

-- | Compute the gradient of a term with respect to a list of named variables.
--   Returns a record term where each field is the partial derivative of the term
--   with respect to the corresponding variable.
gradient :: TTermDefinition (Name -> [Name] -> Term -> Term)
gradient = define "gradient" $
  doc "Compute the gradient of a term as a record of partial derivatives" $
  "typeName" ~> "vars" ~> "term" ~>
  Core.termRecord $ Core.record
    (var "typeName")
    (Lists.map ("v" ~>
      Core.field
        (var "v")
        (differentiateTerm @@ var "v" @@ var "term"))
      (var "vars"))

-- | Look up the derivative of a unary Float64 primitive by name.
--   Returns Just a term representing the derivative function (a lambda),
--   or Nothing if the primitive is not differentiable.
primitiveDerivative :: TTermDefinition (Name -> Maybe Term)
primitiveDerivative = define "primitiveDerivative" $
  doc "Look up the derivative of a unary Float64 primitive" $
  "name" ~>
  -- d/dx(sin(x)) = cos(x)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_sin))
    (just $ DeepMath.ref _math_cos) $
  -- d/dx(cos(x)) = -sin(x)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_cos))
    (just $ DeepCore.lambda "_x" (DeepMath.negateFloat64 (DeepMath.sin (DeepCore.var "_x")))) $
  -- d/dx(tan(x)) = 1/cos(x)^2
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_tan))
    (just $ DeepCore.lambda "_x" (DeepMath.pow (DeepMath.cos (DeepCore.var "_x")) (f64 (-2.0)))) $
  -- d/dx(exp(x)) = exp(x)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_exp))
    (just $ DeepMath.ref _math_exp) $
  -- d/dx(log(x)) = 1/x
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_log))
    (just $ DeepCore.lambda "_x" (DeepMath.pow (DeepCore.var "_x") (f64 (-1.0)))) $
  -- d/dx(sqrt(x)) = 1/(2*sqrt(x))
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_sqrt))
    (just $ DeepCore.lambda "_x" (DeepMath.mulFloat64 (f64 0.5)
        (DeepMath.pow (DeepMath.sqrt (DeepCore.var "_x")) (f64 (-1.0))))) $
  -- d/dx(asin(x)) = 1/sqrt(1 - x^2)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_asin))
    (just $ DeepCore.lambda "_x" (DeepMath.pow
        (DeepMath.sqrt
          (DeepMath.subFloat64 (f64 1.0) (DeepMath.mulFloat64
            (DeepCore.var "_x")
            (DeepCore.var "_x"))))
        (f64 (-1.0)))) $
  -- d/dx(acos(x)) = -1/sqrt(1 - x^2)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_acos))
    (just $ DeepCore.lambda "_x" (DeepMath.negateFloat64 $ DeepMath.pow
        (DeepMath.sqrt
          (DeepMath.subFloat64 (f64 1.0) (DeepMath.mulFloat64
            (DeepCore.var "_x")
            (DeepCore.var "_x"))))
        (f64 (-1.0)))) $
  -- d/dx(atan(x)) = 1/(1 + x^2)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_atan))
    (just $ DeepCore.lambda "_x" (DeepMath.pow
        (DeepMath.addFloat64 (f64 1.0) (DeepMath.mulFloat64
          (DeepCore.var "_x")
          (DeepCore.var "_x")))
        (f64 (-1.0)))) $
  -- d/dx(sinh(x)) = cosh(x)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_sinh))
    (just $ DeepMath.ref _math_cosh) $
  -- d/dx(cosh(x)) = sinh(x)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_cosh))
    (just $ DeepMath.ref _math_sinh) $
  -- d/dx(tanh(x)) = 1 - tanh(x)^2
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_tanh))
    (just $ DeepCore.lambda "_x" (DeepMath.subFloat64 (f64 1.0)
        (DeepMath.mulFloat64
          (DeepMath.tanh (DeepCore.var "_x"))
          (DeepMath.tanh (DeepCore.var "_x"))))) $
  -- d/dx(asinh(x)) = 1/sqrt(x^2 + 1)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_asinh))
    (just $ DeepCore.lambda "_x" (DeepMath.pow
        (DeepMath.sqrt
          (DeepMath.addFloat64
            (DeepMath.mulFloat64
              (DeepCore.var "_x")
              (DeepCore.var "_x"))
            (f64 1.0)))
        (f64 (-1.0)))) $
  -- d/dx(acosh(x)) = 1/sqrt(x^2 - 1)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_acosh))
    (just $ DeepCore.lambda "_x" (DeepMath.pow
        (DeepMath.sqrt
          (DeepMath.subFloat64
            (DeepMath.mulFloat64
              (DeepCore.var "_x")
              (DeepCore.var "_x"))
            (f64 1.0)))
        (f64 (-1.0)))) $
  -- d/dx(atanh(x)) = 1/(1 - x^2)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_atanh))
    (just $ DeepCore.lambda "_x" (DeepMath.pow
        (DeepMath.subFloat64 (f64 1.0) (DeepMath.mulFloat64
          (DeepCore.var "_x")
          (DeepCore.var "_x")))
        (f64 (-1.0)))) $
  -- d/dx(negate(x)) = -1
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_negate))
    (just $ DeepCore.lambda "_x" (f64 (-1.0))) $
  -- d/dx(abs(x)) = signum(x)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_abs))
    (just $ DeepMath.ref _math_signum) $
  -- d/dx(ceiling(x)) = 0  (piecewise constant)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_ceiling))
    (just $ DeepCore.lambda "_x" (f64 0.0)) $
  -- d/dx(floor(x)) = 0
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_floor))
    (just $ DeepCore.lambda "_x" (f64 0.0)) $
  -- d/dx(round(x)) = 0
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_round))
    (just $ DeepCore.lambda "_x" (f64 0.0)) $
  -- d/dx(truncate(x)) = 0
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_truncate))
    (just $ DeepCore.lambda "_x" (f64 0.0)) $
  -- d/dx(signum(x)) = 0  (piecewise constant)
  Logic.ifElse (Equality.equal (var "name") (encodedName _math_signum))
    (just $ DeepCore.lambda "_x" (f64 0.0)) $
  -- Unknown primitive
  nothing