Harmonic Oscillator
Let's demonstrate basic capabilities of SciLean on a simple program simulating harmonic oscillator. The energy/Hamiltonian of harmonic oscillator is
def H (m k : Float) (x p : Float) := (1/(2*m)) * p^2 + k/2 * x^2
Once we have an energy of a physical system we can derive its dynamics using Hamilton's equations
$$\begin{align} \dot x &= \frac{\partial H}{\partial p} \\ \dot p &= -\frac{\partial H}{\partial x} \end{align} $$
In SciLean, we define function solver
that approximatelly solves these equations with the following command
approx solver (m k : Float) := odeSolve (fun (t : Float) (x,p) => ( β (p':=p), H m k x p', -β (x':=x), H m k x' p)) by ...
The expression after :=
is just a specification of what the function solver
is supposed to approximate and the actual definition is given where the dots ...
are.
The general idea behind SciLean is that we first specify what we want to compute and only then we describe how we do it. In this case, the function SciLean.odeSolve
is non-computable function that can't be executed but has well defined meaning of returning the solution of given differential equation. In the place of the dots ...
we will manipulate this function symbolically and transform the whole specification into an expression that can be evaluated approximatelly.
Let's have a look how we can turn the specification into implementation
open SciLean
approx solver (m k : Float)
:= odeSolve (fun (t : Float) (x,p) => ( β (p':=p), H m k x p',
-β (x':=x), H m k x' p))
by
m : Float k : Float
β’ Approx (Filter.atTop.prod (?m.11758 m k))
(odeSolve fun t x =>
match x with
| (x, p) => (β (p':=p), (1 / (2 * m) * p' ^ 2 + k / 2 * x ^ 2), -β (x':=x), (1 / (2 * m) * p ^ 2 + k / 2 * x' ^ 2))) -- unfold definition of H
m : Float k : Float
β’ let ydf := (2 * m)β»ΒΉ;
let ydf_1 := k / 2;
Approx (Filter.atTop.prod (?m.11758 m k))
(odeSolve fun t x =>
let ydf_2 := x.2;
let ydf_3 := x.1;
(ydf * (2 * ydf_2), -(ydf_1 * (2 * ydf_3)))) -- run automatic differentiation
-- apply RK4 method
simp_rw (config:={zeta:=false}) [m : Float k : Float
β’ let ydf := (2 * m)β»ΒΉ;
let ydf_1 := k / 2;
Approx (?m.3965 m k) fun tβ t xβ =>
limit n β β,
odeSolveFixedStep
(rungeKutta4 fun t x =>
let ydf_2 := x.2;
let ydf_3 := x.1;
(ydf * (2 * ydf_2), -(ydf_1 * (2 * ydf_3))))
n tβ t xβ]
-- approximate the limit with a fixed `n`
m : Float k : Float n : β
β’ Approx (?m.11758 m k) fun tβ t xβ =>
odeSolveFixedStep
(rungeKutta4 fun t x =>
let ydf := x.2;
let ydf_1 := x.1;
((2 * m)β»ΒΉ * (2 * ydf), -(k / 2 * (2 * ydf_1))))
n tβ t xββ’ Float β Float β Typeβ’ (m k : Float) β Filter (?m.11756 m k)
There are two main steps in turning the specification into implementation. We need to compute the derivatives β (p':=p), H m k x p'
and β (x':=x), H m k x' p
and we need to approximate the function SciLean.odeSolve
using some time stepping scheme.
To compute the derivatives, we unfold the definition of the Hamiltonian H
and call autodiff
tactic.
To approximate SciLean.odeSolve
we rewrite the specification using the theorem SciLean.odeSolve_fixed_dt
saying that SciLean.odeSolve
can be approximated by a time stepping scheme, we choose Runge-Kutta 4 SciLean.rungeKutta4
.
j
The theorem SciLean.odeSolve_fixed_dt
produces a specification of the form limit n β β, ...
. We can decide to approximate this limit by taking a concrete value n
and hope it is sufficiently large such that the approximation is good enough. To do this we call the tactic approx_limit
and we end up with a goal that fully computable expression.
Now we have a function solver
that takes mass m
, stiffness k
, number of substeps substep
, initial time tβ
, final time t
, initial position x
and momentum p
and produces new position and momentum at time t
.
#check fun m k substep tβ t (x,p) => solver m k substep tβ t (x,p)
If we check solver
directly
#check solver
we see that its type contains @Approx ?l ?spec
where ?l
specifies what are the parameters controling the approximation and ?spec
is the specification of what is solver
approximating.
Here is an example how to run solver
def sim : IO Unit := do
let substeps := 1
let m := 1.0
let k := 10.0
let Ξt := 0.1
let xβ := 1.0
let pβ := 0.0
let mut t := 0
let mut (x,p) := (xβ, pβ)
for _ in [0:50] do
(x, p) := solver m k substeps t (t+Ξt) (x, p)
t += Ξt
-- poor man's visualization
for (j : Nat) in [0:20] do
if j.toFloat < 10*(x+1.0) then
IO.print "o"
IO.println ""
When we execute it we obtain expected sinusoidal wave
#eval sim