hydra-0.15.0: src/main/haskell/Hydra/Demos/Grad/Demo.hs
-- | Haskell driver for the automatic differentiation demo.
--
-- Demonstrates source-to-source automatic differentiation on Hydra terms.
-- Builds mathematical functions as Hydra Term values, differentiates them
-- symbolically, and evaluates both original and derivative at specific points.
--
-- Usage: runhaskell Demo.hs <output-dir>
module Hydra.Demos.Grad.Demo where
import Hydra.Kernel
import qualified Hydra.Core as Core
import qualified Hydra.Differentiation as Diff
import qualified Hydra.Variables as Vars
import qualified Hydra.Reduction as Reduction
import qualified Hydra.Show.Core as ShowCore
import qualified Hydra.Sources.Libraries as Lib
import qualified Hydra.Lib.Math as HMath
import Control.Monad (when)
import qualified Data.List as L
import qualified Data.Map as M
import System.Directory (createDirectoryIfMissing)
import System.Environment (getArgs)
import System.Exit (exitFailure)
import System.FilePath ((</>))
import System.IO (hPutStrLn, stderr, hFlush, stdout)
import Text.Printf (printf)
main :: IO ()
main = do
args <- getArgs
case args of
[outDir] -> runDemo outDir
_ -> do
hPutStrLn stderr "Usage: GradDemo <output-dir>"
exitFailure
-- | Build a Graph with all standard library primitives registered.
primGraph :: Graph
primGraph = let
allPrims = L.concatMap libraryPrimitives Lib.standardLibraries
primsMap = M.fromList $ fmap (\p -> (primitiveName p, p)) allPrims
in emptyGraph { graphPrimitives = primsMap }
-- | Evaluate a Term to a reduced form, or return an error message.
eval :: Term -> Either String Term
eval term = case Reduction.reduceTerm emptyContext primGraph True term of
Left err -> Left $ show err
Right t -> Right t
-- | Evaluate a Term and show the result as a string.
evalShow :: Term -> String
evalShow term = case eval term of
Left err -> "<<error: " ++ err ++ ">>"
Right t -> ShowCore.term t
-- | Extract a Double from a reduced Term, if possible.
evalDouble :: Term -> Maybe Double
evalDouble term = case eval term of
Right (TermLiteral (LiteralFloat (FloatValueFloat64 d))) -> Just d
_ -> Nothing
-- | Build a variable term: x
var :: String -> Term
var = TermVariable . Name
-- | Build a Float64 literal term
lit :: Double -> Term
lit = TermLiteral . LiteralFloat . FloatValueFloat64
-- | Build a unary primitive application: f(x)
app1 :: String -> Term -> Term
app1 name arg = TermApplication $ Application
(TermVariable (Name name)) arg
-- | Build a binary primitive application: f(x, y)
app2 :: String -> Term -> Term -> Term
app2 name a b = TermApplication $ Application
(TermApplication $ Application (TermVariable (Name name)) a) b
-- | Substitute a variable with a value in a term, then evaluate.
evalAt :: String -> Double -> Term -> Maybe Double
evalAt varName xVal term =
let substituted = Vars.replaceFreeTermVariable (Name varName) (lit xVal) term
in evalDouble substituted
-- | Central finite difference approximation of the derivative.
finiteDiff :: (Double -> Double) -> Double -> Double -> Double
finiteDiff f x h = (f (x + h) - f (x - h)) / (2 * h)
-- | A demo function: name, Hydra term representation, and Haskell evaluation function.
data DemoFunction = DemoFunction
{ dfName :: String
, dfTerm :: Term
, dfHaskell :: Double -> Double
}
-- | The collection of demo functions.
demoFunctions :: [DemoFunction]
demoFunctions =
[ DemoFunction
{ dfName = "x^2"
, dfTerm = app2 "hydra.lib.math.mulFloat64" (var "x") (var "x")
, dfHaskell = \x -> x * x
}
, DemoFunction
{ dfName = "x^3"
, dfTerm = app2 "hydra.lib.math.pow" (var "x") (lit 3.0)
, dfHaskell = \x -> x ** 3
}
, DemoFunction
{ dfName = "sin(x)"
, dfTerm = app1 "hydra.lib.math.sin" (var "x")
, dfHaskell = sin
}
, DemoFunction
{ dfName = "exp(x)"
, dfTerm = app1 "hydra.lib.math.exp" (var "x")
, dfHaskell = exp
}
, DemoFunction
{ dfName = "log(x)"
, dfTerm = app1 "hydra.lib.math.log" (var "x")
, dfHaskell = log
}
, DemoFunction
{ dfName = "sqrt(x)"
, dfTerm = app1 "hydra.lib.math.sqrt" (var "x")
, dfHaskell = sqrt
}
, DemoFunction
{ dfName = "sin(cos(x))"
, dfTerm = app1 "hydra.lib.math.sin" (app1 "hydra.lib.math.cos" (var "x"))
, dfHaskell = \x -> sin (cos x)
}
, DemoFunction
{ dfName = "x * sin(x)"
, dfTerm = app2 "hydra.lib.math.mulFloat64" (var "x") (app1 "hydra.lib.math.sin" (var "x"))
, dfHaskell = \x -> x * sin x
}
, DemoFunction
{ dfName = "exp(x^2)"
, dfTerm = app1 "hydra.lib.math.exp"
(app2 "hydra.lib.math.mulFloat64" (var "x") (var "x"))
, dfHaskell = \x -> exp (x * x)
}
]
-- | Test points for evaluation.
testPoints :: [Double]
testPoints = [0.5, 1.0, 2.0]
runDemo :: FilePath -> IO ()
runDemo outDir = do
createDirectoryIfMissing True outDir
putStrLn "╔══════════════════════════════════════════════════════════════════╗"
putStrLn "║ Automatic Differentiation Demo for Hydra ║"
putStrLn "║ ║"
putStrLn "║ Source-to-source symbolic differentiation on typed lambda ║"
putStrLn "║ calculus terms, compiled identically across all Hydra ║"
putStrLn "║ target languages. ║"
putStrLn "╚══════════════════════════════════════════════════════════════════╝"
putStrLn ""
-- Phase 1: Symbolic differentiation
putStrLn "═══ Phase 1: Symbolic Differentiation ═══"
putStrLn ""
derivs <- mapM showSymbolic demoFunctions
putStrLn ""
-- Phase 2: Numerical evaluation
putStrLn "═══ Phase 2: Numerical Evaluation ═══"
putStrLn ""
mapM_ (evaluateFunction outDir) (zip demoFunctions derivs)
putStrLn ""
-- Phase 3: Gradient check
putStrLn "═══ Phase 3: Gradient Check (AD vs Finite Differences) ═══"
putStrLn ""
results <- mapM gradientCheck demoFunctions
let allPass = and results
putStrLn ""
if allPass
then putStrLn "All gradient checks PASSED."
else putStrLn "Some gradient checks FAILED."
putStrLn ""
-- Phase 4: Gradient descent optimization
putStrLn "═══ Phase 4: Gradient Descent Optimization ═══"
putStrLn ""
optimizationDemo outDir
putStrLn ""
-- Phase 5: Curve fitting with two parameters
putStrLn "═══ Phase 5: Linear Regression via Gradient Descent ═══"
putStrLn ""
curveFittingDemo outDir
putStrLn ""
-- Write summary to file
let summaryFile = outDir </> "summary.txt"
writeFile summaryFile $ unlines
[ "Automatic Differentiation Demo Summary"
, "======================================"
, ""
, "Functions differentiated: " ++ show (length demoFunctions)
, "Test points: " ++ show testPoints
, "Gradient checks: " ++ (if allPass then "ALL PASSED" else "SOME FAILED")
, "Optimization: single-variable and linear regression demos included"
]
putStrLn $ "Summary written to " ++ summaryFile
showSymbolic :: DemoFunction -> IO Term
showSymbolic df = do
let deriv = Diff.differentiateTerm (Name "x") (dfTerm df)
putStrLn $ " f(x) = " ++ dfName df
putStrLn $ " term: " ++ ShowCore.term (dfTerm df)
putStrLn $ " derivative: " ++ ShowCore.term deriv
putStrLn ""
return deriv
evaluateFunction :: FilePath -> (DemoFunction, Term) -> IO ()
evaluateFunction outDir (df, deriv) = do
putStrLn $ " f(x) = " ++ dfName df
printf " %-8s %-14s %-14s %-14s %-14s\n"
("x" :: String) ("f(x)" :: String) ("f'(x) AD" :: String) ("f'(x) exact" :: String) ("error" :: String)
printf " %-8s %-14s %-14s %-14s %-14s\n"
("---" :: String) ("----" :: String) ("--------" :: String) ("----------" :: String) ("-----" :: String)
mapM_ (evalPoint df deriv) testPoints
putStrLn ""
-- Write derivative terms to file
let derivFile = outDir </> (sanitize (dfName df) ++ "_derivative.txt")
writeFile derivFile $ unlines
[ "f(x) = " ++ dfName df
, ""
, "Original term:"
, " " ++ ShowCore.term (dfTerm df)
, ""
, "Derivative term:"
, " " ++ ShowCore.term deriv
]
where
sanitize = map (\c -> if c `elem` ("*^() " :: String) then '_' else c)
evalPoint :: DemoFunction -> Term -> Double -> IO ()
evalPoint df deriv x = do
let fVal = dfHaskell df x
let adVal = evalAt "x" x deriv
let exactDeriv = finiteDiff (dfHaskell df) x 1.0e-10
case adVal of
Just ad -> do
let err = abs (ad - exactDeriv)
printf " %-8.4f %-14.8f %-14.8f %-14.8f %-14.2e\n" x fVal ad exactDeriv err
Nothing ->
printf " %-8.4f %-14.8f %-14s %-14.8f %-14s\n" x fVal ("<<error>>" :: String) exactDeriv ("—" :: String)
gradientCheck :: DemoFunction -> IO Bool
gradientCheck df = do
let deriv = Diff.differentiateTerm (Name "x") (dfTerm df)
let h = 1.0e-7
let results = map (checkPoint deriv h) testPoints
let allOk = all id results
let status = if allOk then "PASS" else "FAIL"
printf " %-20s %s\n" (dfName df) status
return allOk
where
checkPoint deriv h x =
case evalAt "x" x deriv of
Nothing -> False
Just ad ->
let fd = finiteDiff (dfHaskell df) x h
relErr = if abs fd < 1e-12
then abs (ad - fd)
else abs (ad - fd) / abs fd
in relErr < 1e-4
-- ============================================================================
-- Phase 4: Single-variable gradient descent optimization
-- ============================================================================
-- | Minimize f(x) = (x - 3)^2 + 2*sin(x), starting from x = 0.
-- The minimum is near x ≈ 2.81 where f'(x) = 0.
optimizationDemo :: FilePath -> IO ()
optimizationDemo outDir = do
putStrLn " Problem: minimize f(x) = (x - 3)^2 + 2*sin(x)"
putStrLn " Method: gradient descent with AD-computed derivatives"
putStrLn " Start: x = 0.0, learning rate = 0.1"
putStrLn ""
-- Build the loss function as a Hydra term:
-- f(x) = (x - 3)^2 + 2*sin(x)
-- = mulFloat64(subFloat64(x, 3), subFloat64(x, 3)) + mulFloat64(2, sin(x))
let xMinus3 = app2 "hydra.lib.math.subFloat64" (var "x") (lit 3.0)
let lossTerm = app2 "hydra.lib.math.addFloat64"
(app2 "hydra.lib.math.mulFloat64" xMinus3 xMinus3)
(app2 "hydra.lib.math.mulFloat64" (lit 2.0) (app1 "hydra.lib.math.sin" (var "x")))
-- Differentiate once — this produces the gradient term
let gradTerm = Diff.differentiateTerm (Name "x") lossTerm
putStrLn $ " Loss term: " ++ ShowCore.term lossTerm
putStrLn $ " Gradient term: " ++ ShowCore.term gradTerm
putStrLn ""
-- Gradient descent loop
let lr = 0.1
let numSteps = 30
let haskellLoss x = (x - 3)**2 + 2 * sin x
printf " %-6s %-12s %-14s %-14s\n"
("step" :: String) ("x" :: String) ("f(x)" :: String) ("f'(x)" :: String)
printf " %-6s %-12s %-14s %-14s\n"
("----" :: String) ("---" :: String) ("----" :: String) ("-----" :: String)
let go :: Int -> Double -> IO Double
go step x
| step > numSteps = return x
| otherwise = do
let fVal = haskellLoss x
case evalAt "x" x gradTerm of
Nothing -> do
printf " %-6d %-12.6f %-14.8f <<error>>\n" step x fVal
return x
Just grad -> do
when (step `mod` 5 == 0 || step <= 2 || step == numSteps) $
printf " %-6d %-12.6f %-14.8f %-14.8f\n" step x fVal grad
go (step + 1) (x - lr * grad)
xFinal <- go 0 0.0
let fFinal = haskellLoss xFinal
putStrLn ""
printf " Result: x* = %.8f, f(x*) = %.8f\n" xFinal fFinal
printf " Verification: f'(x*) ≈ %.2e (should be near zero)\n"
(case evalAt "x" xFinal gradTerm of Just g -> g; Nothing -> 999.0)
-- Write optimization trace to file
let optFile = outDir </> "optimization.txt"
writeFile optFile $ unlines
[ "Single-variable optimization"
, "f(x) = (x - 3)^2 + 2*sin(x)"
, "Method: gradient descent, lr=0.1, 30 steps"
, "Result: x* = " ++ show xFinal ++ ", f(x*) = " ++ show fFinal
]
-- ============================================================================
-- Phase 5: Linear regression (2-parameter curve fitting)
-- ============================================================================
-- | Fit y = a*x + b to noisy data using coordinate-wise gradient descent.
-- Since we have single-variable differentiation, we compute dL/da and dL/db
-- separately and update each parameter per step.
curveFittingDemo :: FilePath -> IO ()
curveFittingDemo outDir = do
-- Generate "observed" data: y = 2.5*x + 0.7 + noise
let trueA = 2.5
let trueB = 0.7
let dataPoints = [(x, trueA * x + trueB + noise x) | x <- [0.0, 0.5 .. 4.0]]
where noise x = 0.1 * sin (7 * x) -- deterministic pseudo-noise
putStrLn $ " Problem: fit y = a*x + b to " ++ show (length dataPoints) ++ " data points"
putStrLn $ " True parameters: a = " ++ show trueA ++ ", b = " ++ show trueB
putStrLn $ " Method: coordinate-wise gradient descent with AD"
putStrLn ""
-- Build the loss function L(a, b) = sum_i (a*x_i + b - y_i)^2
-- We construct this as a Hydra term with free variables "a" and "b".
let residual xi yi = app2 "hydra.lib.math.subFloat64"
(app2 "hydra.lib.math.addFloat64"
(app2 "hydra.lib.math.mulFloat64" (var "a") (lit xi))
(var "b"))
(lit yi)
let squaredResidual xi yi =
let r = residual xi yi
in app2 "hydra.lib.math.mulFloat64" r r
let lossTerm = foldl1 (\acc t -> app2 "hydra.lib.math.addFloat64" acc t)
[squaredResidual xi yi | (xi, yi) <- dataPoints]
-- Differentiate w.r.t. "a" and "b" separately
let dLda = Diff.differentiateTerm (Name "a") lossTerm
let dLdb = Diff.differentiateTerm (Name "b") lossTerm
putStrLn " Loss function: L(a,b) = sum_i (a*x_i + b - y_i)^2"
putStrLn ""
-- Gradient descent
let lr = 0.001 -- small learning rate (sum of squares can have large gradients)
let numSteps = 200
printf " %-6s %-12s %-12s %-14s\n"
("step" :: String) ("a" :: String) ("b" :: String) ("loss" :: String)
printf " %-6s %-12s %-12s %-14s\n"
("----" :: String) ("---" :: String) ("---" :: String) ("----" :: String)
let evalLoss a b = sum [(a * xi + b - yi)**2 | (xi, yi) <- dataPoints]
let evalGrad gradTerm aVal bVal =
let t1 = Vars.replaceFreeTermVariable (Name "a") (lit aVal) gradTerm
t2 = Vars.replaceFreeTermVariable (Name "b") (lit bVal) t1
in evalDouble t2
let go :: Int -> Double -> Double -> IO (Double, Double)
go step a b
| step > numSteps = return (a, b)
| otherwise = do
let loss = evalLoss a b
case (evalGrad dLda a b, evalGrad dLdb a b) of
(Just ga, Just gb) -> do
when (step `mod` 40 == 0 || step <= 2 || step == numSteps) $
printf " %-6d %-12.6f %-12.6f %-14.8f\n" step a b loss
go (step + 1) (a - lr * ga) (b - lr * gb)
_ -> do
printf " %-6d %-12.6f %-12.6f <<gradient error>>\n" step a b
return (a, b)
(aFinal, bFinal) <- go 0 0.0 0.0
let finalLoss = evalLoss aFinal bFinal
putStrLn ""
printf " Result: a = %.6f, b = %.6f\n" aFinal bFinal
printf " True: a = %.6f, b = %.6f\n" trueA trueB
printf " Error: |a - a*| = %.6f, |b - b*| = %.6f\n"
(abs (aFinal - trueA)) (abs (bFinal - trueB))
printf " Final loss: %.8f\n" finalLoss
-- Write results
let fitFile = outDir </> "curve_fitting.txt"
writeFile fitFile $ unlines
[ "Linear regression via gradient descent"
, "Model: y = a*x + b"
, "True: a = " ++ show trueA ++ ", b = " ++ show trueB
, "Fitted: a = " ++ show aFinal ++ ", b = " ++ show bFinal
, "Final loss: " ++ show finalLoss
]