4.2. 🚧 Harmonic Oscillator Optimization

4.2.1. Problem Statement

In this example, we will demonstrate how to find a parameter of differential equation such that the solution satisfies particular property. Consider this practical problem. We are designing a new musical instrument, we have already decided on its particular shape. We want to hit a particular note and we will do that by finding the right material. To do this we model the musical instrument with harmonic oscillator and we will find the right stifness value that gives us solution with desired frequency.

Of course that in reality we would optimize for the shape rather than for the material but this would yield much harder mathematical problem. Therefore we do it the other way around which is much easier, serves as demonstration on how to use SciLean and we can easily check the numrical answer to the analytical solution.

The harmonic oscillator is governed by the following differential equation

$$m \ddot x = - k x $$

because we will try to find the appropricate k we will denote \(x(k,t)\) the solution of this equation as a function of time \(t\) and stiffness \(k\).

How can we express the requirement that the solution should hit a particular frequencty \(omega\)? One way to state this requirement is that the solution should return to the same position after time \(T\). We want to find stiffness \(k\) such that

$$x(k,T) = x(k,0) $$

where the time \(T\) is related to the given frequency by

$$T = \frac{2 \pi}{\omega} $$

(You might have noticed that this argument contains a flaw which we kindly ask you to ignore for the moment as we will discuss it at the end.)

4.2.2. Lean Specification

Lets formulate this in Lean now. In the previous example 'Harmonic Oscillator' we have shows how to specify harmonic oscillator using its energy

def H (m k x p : Float) := (1/(2*m)) * p^2 + k/2 * x^2

The solution of the corresponding differential equation is symbolically represented using the function odeSolve

noncomputable def solution (m k x₀ p₀ t : Float) : Float×Float := odeSolve (t₀ := 0) (x₀ := (x₀,p₀)) (t := t) (fun (t : Float) (x,p) => ( (p':=p), H m k x p', - (x':=x), H m k x' p))

The expression solution m k x₀ p₀ t is the position and momentum at time t of harmonic oscillator with mass m, stiffness k starting at time 0 at position x₀ and momentum p₀.

Notice that we had to mark the function solution with noncomputable as it is purely symbolic as it can't be executed because it uses the function odeSolve.

To express that we are looking for the stiffness satisfying a particular property we can use the notation solve x, f x = 0 which will symbolically return the solution x of the equation f x = 0.

noncomputable def optimalStiffness (m x₀ ω : Float) : Float := let x := fun k t => let (x,p) := solution m k x₀ 0 t x let T := 2*π/ω solve k, x k T = x k 0

The notation solve x, f x = 0 uses the noncomputable function solveFun which returns a term that satisfies a particular property. This is why we have to mark optimalStiffness as noncomputable.

Our goal is to have an executable function that can approximatelly compute the symbolic function optimalStiffness.

We can therefore define function findStiffness that is an approximation to the specification. In the next section we will show how replace skip with a sequence of tactics that will turn the specification into executable approximation and allows us to remove the noncomputable annotation from this fuction.

noncomputable approx findStiffness (m x₀ ω : Float) := let T := 2*π/ω let y := fun (k : Float) => odeSolve (t₀ := 0) (t:=T) (x₀:=(x₀,0)) (fun (t : Float) (x,p) => ( (p':=p), H m k x p', - (x':=x), H m k x' p)) solve k, (y k).1 = x₀ by m:Floatx₀:Floatω:FloatApprox (?m.2380 m x₀ ω) (let T := 2 * π / ω; let y := fun k => odeSolve (fun t x => match x with | (x, p) => (∇ (p':=p), H m k x p', -∇ (x':=x), H m k x' p)) 0 T (x₀, 0); solve k, (y k).1 = x₀)

4.2.3. Turning Specification into Implementation

Unfortunatelly the current documentation tool we are using does not allow mixing a proof with text so we have to write down everything with only brief comments and only then we will go over each tactic and explain what it does.

The are two little differences in the initial specification that are mainly of technical nature. First, we had to add the function holdLet which is just an identity function and should be ignored. Its purpose is to prevent inlining of y by the tactic autodiff. The se

Please go ahead and have a brief look at every step. The short comments should give you rought idea what each tactic is doing and by clicking on each command will display the current state.

approx findStiffness (m x₀ ω k₀ : Float) := let T := 2*π/ω let y := holdLet <| fun (k : Float) => odeSolve (t₀ := 0) (t:=T) (x₀:=(x₀,0)) (fun (t : Float) (x,p) => ( (p':=p), H m k x p', - (x':=x), H m k x' p)) solve k, (y k).1 = x₀ by conv => -- focus on the specification enter[2] -- Unfold Hamiltonian and compute gradients m:Floatx₀:Floatω:Floatk₀:Float| let T := 2 * π / ω; let y := holdLet fun 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))) 0 T (x₀, 0); solve k, (y k).1 = x₀; m:Floatx₀:Floatω:Floatk₀:Float| let T := 2 * π / ω; let y := holdLet fun k => odeSolve (fun t x => let ydf := (2 * m)⁻¹; let ydf_1 := x.2; let ydf_2 := k / 2; let ydf_3 := x.1; (ydf * (2 * ydf_1), -(ydf_2 * (2 * ydf_3)))) 0 T (x₀, 0); solve k, (y k).1 = x₀ conv => -- focus on solve k, (y k).1 = x₀ enter[T,m:Floatx₀:Floatω:Floatk₀:FloatT:Floaty:FloatFloat × Float| solve k, (y k).1 = x₀] -- reformulate as minimization problem rw[solve_eq_argmin_norm2 Float (m:Floatx₀:Floatω:Floatk₀:FloatT:Floaty:FloatFloat × FloatHasUniqueSolution fun x => (y x).1 = x₀ All goals completed! 🐙)] -- approximate by gradient descrent rw[m:Floatx₀:Floatω:Floatk₀:FloatT:Floaty:FloatFloat × Float| limit optsOptions.filter, let f' := holdLet (<∂ x, ‖(y x).1 - x₀‖₂²); let r := optimize (ObjectiveFunction.mk (fun x => ‖(y x).1 - x₀‖₂²) ⋯) (AbstractOptimizer.setOptions Float default opts) k₀; r.minimizer] -- consume limit by `Approx` approx_limit opts (m:Floatx₀:Floatω:Floatk₀:Float(let T := 2 * π / ω; let y := holdLet fun k => odeSolve (fun t x => let ydf := (2 * m)⁻¹; let ydf_1 := x.2; let ydf_2 := k / 2; let ydf_3 := x.1; (ydf * (2 * ydf_1), -(ydf_2 * (2 * ydf_3)))) 0 T (x₀, 0); limit optsOptions.filter, let f' := holdLet (<∂ x, ‖(y x).1 - x₀‖₂²); let r := optimize (ObjectiveFunction.mk (fun x => ‖(y x).1 - x₀‖₂²) ⋯) (AbstractOptimizer.setOptions Float default opts) k₀; r.minimizer) = limit optsOptions.filter, let T := 2 * π / ω; let y := holdLet fun k => odeSolve (fun t x => let ydf := (2 * m)⁻¹; let ydf_1 := x.2; let ydf_2 := k / 2; let ydf_3 := x.1; (ydf * (2 * ydf_1), -(ydf_2 * (2 * ydf_3)))) 0 T (x₀, 0); let f' := holdLet (<∂ x, ‖(y x).1 - x₀‖₂²); let r := optimize (ObjectiveFunction.mk (fun x => ‖(y x).1 - x₀‖₂²) ⋯) (AbstractOptimizer.setOptions Float default opts) k₀; r.minimizer All goals completed! 🐙) conv => -- focus on the specification again enter[2] -- rewrite reverse mode AD (<∂) as forward mode AD (∂>) -- this is possible because we are differentiating scalar function `Float → Float` m:Floatx₀:Floatω:Floatk₀:Floatopts:Options Float| let T := 2 * π / ω; let y := holdLet fun k => odeSolve (fun t x => let ydf := (2 * m)⁻¹; let ydf_1 := x.2; let ydf_2 := k / 2; let ydf_3 := x.1; (ydf * (2 * ydf_1), -(ydf_2 * (2 * ydf_3)))) 0 T (x₀, 0); let f' := holdLet fun x => let x := ∂> (x:=x;1), ‖(y x).1 - x₀‖₂²; let y := x.1; let dy := x.2; (y, fun dy' => dy * dy'); let r := optimize (ObjectiveFunction.mk (fun x => ‖(y x).1 - x₀‖₂²) ⋯) (AbstractOptimizer.setOptions Float default opts) k₀; r.minimizer -- run forward mode AD -- this will formulate a new ODE that solves for `x`, `p`, `dx/dk` and `dp/dk` m:Floatx₀:Floatω:Floatk₀:Floatopts:Options Float| let T := 2 * π / ω; let y := holdLet fun k => odeSolve (fun t x => let ydf := (2 * m)⁻¹; let ydf_1 := x.2; let ydf_2 := k / 2; let ydf_3 := x.1; (ydf * (2 * ydf_1), -(ydf_2 * (2 * ydf_3)))) 0 T (x₀, 0); let f' := holdLet fun x => let ydf := (2 * m)⁻¹; let F := holdLet fun t xdx => let x_1 := xdx.1; let dx := xdx.2; let zdz := x_1.2; let zdz_1 := dx.2; let zdz_2 := x / 2; let zdz_3 := 2⁻¹; let zdz_4 := x_1.1; let zdz_5 := dx.1; let zdz := 2 * zdz; let zdz_6 := 2 * zdz_1; let zdz := ydf * zdz; let zdz_7 := ydf * zdz_6; let zdz_8 := 2 * zdz_4; let zdz_9 := 2 * zdz_5; let zdz_10 := zdz_2 * zdz_8; let zdz_11 := zdz_2 * zdz_9 + zdz_3 * zdz_8; let zdz_12 := -zdz_10; let zdz_13 := -zdz_11; ((zdz, zdz_12), zdz_7, zdz_13); let xdx := odeSolve F 0 T ((x₀, 0), 0); let y := xdx.1; let dy := xdx.2; let zdz := y.1; let zdz_1 := dy.1; let ydy := zdz - x₀; let zdz := ydy ^ 2; let zdz_2 := 2 * zdz_1, ydy⟫; (zdz, fun dy' => zdz_2 * dy'); let r := optimize (ObjectiveFunction.mk (fun x => ‖(y x).1 - x₀‖₂²) ⋯) (AbstractOptimizer.setOptions Float default opts) k₀; r.minimizer -- approximate both ODEs with RK4 m:Floatx₀:Floatω:Floatk₀:Floatopts:Options FloatApprox (Filter.atTop ×ˢ ?m.117588 m x₀ ω k₀) (let T := 2 * π / ω; let y := holdLet fun k => limit n → ∞, odeSolveFixedStep (rungeKutta4 fun t x => let ydf := (2 * m)⁻¹; let ydf_1 := x.2; let ydf_2 := k / 2; let ydf_3 := x.1; (ydf * (2 * ydf_1), -(ydf_2 * (2 * ydf_3)))) n 0 T (x₀, 0); let f' := holdLet fun x => let ydf := (2 * m)⁻¹; let F := holdLet fun t xdx => let x_1 := xdx.1; let dx := xdx.2; let zdz := x_1.2; let zdz_1 := dx.2; let zdz_2 := x / 2; let zdz_3 := 2⁻¹; let zdz_4 := x_1.1; let zdz_5 := dx.1; let zdz := 2 * zdz; let zdz_6 := 2 * zdz_1; let zdz := ydf * zdz; let zdz_7 := ydf * zdz_6; let zdz_8 := 2 * zdz_4; let zdz_9 := 2 * zdz_5; let zdz_10 := zdz_2 * zdz_8; let zdz_11 := zdz_2 * zdz_9 + zdz_3 * zdz_8; let zdz_12 := -zdz_10; let zdz_13 := -zdz_11; ((zdz, zdz_12), zdz_7, zdz_13); let xdx := limit n → ∞, odeSolveFixedStep (rungeKutta4 F) n 0 T ((x₀, 0), 0); let y := xdx.1; let dy := xdx.2; let zdz := y.1; let zdz_1 := dy.1; let ydy := zdz - x₀; let zdz := ydy ^ 2; let zdz_2 := 2 * zdz_1, ydy⟫; (zdz, fun dy' => zdz_2 * dy'); let r := optimize (ObjectiveFunction.mk (fun x => ‖(y x).1 - x₀‖₂²) ⋯) (AbstractOptimizer.setOptions Float default opts) k₀; r.minimizer)FloatFloatFloatFloatType(m x₀ ω k₀ : Float) → Filter ( × ?m.117586 m x₀ ω k₀) -- choose the same number of steps for both ODE solvers -- and consume the corresponding limin in `Approx` approx_limit steps (m:Floatx₀:Floatω:Floatk₀:Floatopts:Options Float(let T := 2 * π / ω; let y := holdLet fun k => limit n → ∞, odeSolveFixedStep (rungeKutta4 fun t x => let ydf := (2 * m)⁻¹; let ydf_1 := x.2; let ydf_2 := k / 2; let ydf_3 := x.1; (ydf * (2 * ydf_1), -(ydf_2 * (2 * ydf_3)))) n 0 T (x₀, 0); let f' := holdLet fun x => let ydf := (2 * m)⁻¹; let F := holdLet fun t xdx => let x_1 := xdx.1; let dx := xdx.2; let zdz := x_1.2; let zdz_1 := dx.2; let zdz_2 := x / 2; let zdz_3 := 2⁻¹; let zdz_4 := x_1.1; let zdz_5 := dx.1; let zdz := 2 * zdz; let zdz_6 := 2 * zdz_1; let zdz := ydf * zdz; let zdz_7 := ydf * zdz_6; let zdz_8 := 2 * zdz_4; let zdz_9 := 2 * zdz_5; let zdz_10 := zdz_2 * zdz_8; let zdz_11 := zdz_2 * zdz_9 + zdz_3 * zdz_8; let zdz_12 := -zdz_10; let zdz_13 := -zdz_11; ((zdz, zdz_12), zdz_7, zdz_13); let xdx := limit n → ∞, odeSolveFixedStep (rungeKutta4 F) n 0 T ((x₀, 0), 0); let y := xdx.1; let dy := xdx.2; let zdz := y.1; let zdz_1 := dy.1; let ydy := zdz - x₀; let zdz := ydy ^ 2; let zdz_2 := 2 * zdz_1, ydy⟫; (zdz, fun dy' => zdz_2 * dy'); let r := optimize (ObjectiveFunction.mk (fun x => ‖(y x).1 - x₀‖₂²) ⋯) (AbstractOptimizer.setOptions Float default opts) k₀; r.minimizer) = limit n → ∞, let T := 2 * π / ω; let y := holdLet fun k => odeSolveFixedStep (rungeKutta4 fun t x => let ydf := (2 * m)⁻¹; let ydf_1 := x.2; let ydf_2 := k / 2; let ydf_3 := x.1; (ydf * (2 * ydf_1), -(ydf_2 * (2 * ydf_3)))) n 0 T (x₀, 0); let f' := holdLet fun x => let ydf := (2 * m)⁻¹; let F := holdLet fun t xdx => let x_1 := xdx.1; let dx := xdx.2; let zdz := x_1.2; let zdz_1 := dx.2; let zdz_2 := x / 2; let zdz_3 := 2⁻¹; let zdz_4 := x_1.1; let zdz_5 := dx.1; let zdz := 2 * zdz; let zdz_6 := 2 * zdz_1; let zdz := ydf * zdz; let zdz_7 := ydf * zdz_6; let zdz_8 := 2 * zdz_4; let zdz_9 := 2 * zdz_5; let zdz_10 := zdz_2 * zdz_8; let zdz_11 := zdz_2 * zdz_9 + zdz_3 * zdz_8; let zdz_12 := -zdz_10; let zdz_13 := -zdz_11; ((zdz, zdz_12), zdz_7, zdz_13); let xdx := odeSolveFixedStep (rungeKutta4 F) n 0 T ((x₀, 0), 0); let y := xdx.1; let dy := xdx.2; let zdz := y.1; let zdz_1 := dy.1; let ydy := zdz - x₀; let zdz := ydy ^ 2; let zdz_2 := 2 * zdz_1, ydy⟫; (zdz, fun dy' => zdz_2 * dy'); let r := optimize (ObjectiveFunction.mk (fun x => ‖(y x).1 - x₀‖₂²) ⋯) (AbstractOptimizer.setOptions Float default opts) k₀; r.minimizer All goals completed! 🐙) 39.478418#eval (2*π)^2 355.306213n xₙ xₙ₊₁ f(xₙ) ∇f(xₙ) sₙ α 0 10.000000 10.013080 3.999144 -0.013080 0.013080 1.000000 1 10.013080 10.027457 3.998965 -0.014377 0.014377 1.000000 2 10.027457 10.043258 3.998748 -0.015800 0.015800 1.000000 3 10.043258 10.060618 3.998486 -0.017360 0.017360 1.000000 4 10.060618 10.079688 3.998170 -0.019070 0.019070 1.000000 5 10.079688 10.100631 3.997788 -0.020942 0.020942 1.000000 6 10.100631 10.123623 3.997328 -0.022992 0.022992 1.000000 7 10.123623 10.148857 3.996774 -0.025234 0.025234 1.000000 8 10.148857 10.176542 3.996106 -0.027685 0.027685 1.000000 9 10.176542 10.206904 3.995302 -0.030362 0.030362 1.000000 10 10.206904 10.240189 3.994336 -0.033285 0.033285 1.000000 11 10.240189 10.276660 3.993175 -0.036471 0.036471 1.000000 12 10.276660 10.316601 3.991782 -0.039941 0.039941 1.000000 13 10.316601 10.360318 3.990111 -0.043717 0.043717 1.000000 14 10.360318 10.408139 3.988110 -0.047821 0.047821 1.000000 15 10.408139 10.460412 3.985716 -0.052273 0.052273 1.000000 16 10.460412 10.517508 3.982858 -0.057096 0.057096 1.000000 17 10.517508 10.579821 3.979449 -0.062313 0.062313 1.000000 18 10.579821 10.647767 3.975390 -0.067945 0.067945 1.000000 19 10.647767 10.721778 3.970567 -0.074012 0.074012 1.000000 20 10.721778 10.802310 3.964847 -0.080531 0.080531 1.000000 21 10.802310 10.889830 3.958080 -0.087520 0.087520 1.000000 22 10.889830 10.984821 3.950092 -0.094991 0.094991 1.000000 23 10.984821 11.087771 3.940689 -0.102950 0.102950 1.000000 24 11.087771 11.199173 3.929654 -0.111402 0.111402 1.000000 25 11.199173 11.319516 3.916744 -0.120342 0.120342 1.000000 26 11.319516 11.449274 3.901692 -0.129759 0.129759 1.000000 27 11.449274 11.588905 3.884211 -0.139630 0.139630 1.000000 28 11.588905 11.738831 3.863992 -0.149926 0.149926 1.000000 29 11.738831 11.899435 3.840709 -0.160604 0.160604 1.000000 30 11.899435 12.071045 3.814025 -0.171609 0.171609 1.000000 31 12.071045 12.253919 3.783602 -0.182874 0.182874 1.000000 32 12.253919 12.448237 3.749103 -0.194318 0.194318 1.000000 33 12.448237 12.654086 3.710213 -0.205849 0.205849 1.000000 34 12.654086 12.871448 3.666642 -0.217362 0.217362 1.000000 35 12.871448 13.100192 3.618144 -0.228743 0.228743 1.000000 36 13.100192 13.340063 3.564532 -0.239871 0.239871 1.000000 37 13.340063 13.590683 3.505686 -0.250620 0.250620 1.000000 38 13.590683 13.851544 3.441571 -0.260862 0.260862 1.000000 39 13.851544 14.122018 3.372245 -0.270474 0.270474 1.000000 40 14.122018 14.401358 3.297866 -0.279339 0.279339 1.000000 41 14.401358 14.688711 3.218689 -0.287354 0.287354 1.000000 42 14.688711 14.983138 3.135073 -0.294427 0.294427 1.000000 43 14.983138 15.283626 3.047464 -0.300488 0.300488 1.000000 44 15.283626 15.589111 2.956391 -0.305485 0.305485 1.000000 45 15.589111 15.898500 2.862445 -0.309389 0.309389 1.000000 46 15.898500 16.210693 2.766260 -0.312193 0.312193 1.000000 47 16.210693 16.524602 2.668500 -0.313909 0.313909 1.000000 48 16.524602 16.839174 2.569829 -0.314572 0.314572 1.000000 49 16.839174 306.292294 2.470902 -0.314231 289.453119 1.000000 50 306.292294 352.721303 0.607448 -0.043436 46.429009 1.000000 51 352.721303 352.730521 0.000006 -0.000009 0.009218 1.000000 52 352.730521 353.584317 0.000005 -0.000009 0.853796 1.000000 53 353.584317 353.947131 0.000001 -0.000003 0.362814 1.000000 54 353.947131 354.297762 0.000000 -0.000001 0.350631 1.000000 55 354.297762 354.539999 0.000000 -0.000001 0.242237 1.000000 56 354.539999 354.729669 0.000000 -0.000000 0.189670 1.000000 57 354.729669 354.871184 0.000000 -0.000000 0.141515 1.000000 58 354.871184 354.978844 0.000000 -0.000000 0.107660 1.000000 59 354.978844 355.060288 0.000000 -0.000000 0.081444 1.000000 60 355.060288 355.122241 0.000000 -0.000000 0.061952 1.000000 61 355.122241 355.169543 0.000000 -0.000000 0.047302 1.000000 62 355.169543 355.205967 0.000000 -0.000000 0.036424 1.000000 63 355.205967 355.234382 0.000000 -0.000000 0.028415 1.000000 64 355.234382 355.256987 0.000000 -0.000000 0.022605 1.000000 65 355.256987 355.275377 0.000000 -0.000000 0.018390 1.000000 66 355.275377 355.290337 0.000000 -0.000000 0.014960 1.000000 67 355.290337 355.301080 0.000000 -0.000000 0.010743 1.000000 68 355.301080 355.305664 0.000000 -0.000000 0.004584 1.000000 69 355.305664 355.306207 0.000000 -0.000000 0.000542 1.000000 70 355.306207 355.306213 0.000000 -0.000000 0.000006 1.000000 71 355.306213 355.306213 0.000000 -0.000000 0.000000 1.000000 * Status: success * Candidate solution Minimizer: 355.306213 Final objective value: 0.000000 * Found with Algorithm: (TODO: implement this) * Convergence measures |x - x'| = 0.000000 ≤ 0.000000 |x - x'| = 0.000000 ≰ 0.000000 |f(x) - f(x')| = 0.000000 ≰ 0.000000 |f(x) - f(x')|/|f(x')| = 0.000000 ≰ 0.000000 |g(x)| = 0.000000 ≰ 0.000000 * Work counters Seconds run: 0ns (vs limit ∞ns) Iterations: 72 f(x) calls: 73 ∇f(x) calls: 73 ∇²f(x) calls: 0 #eval findStiffness (m:=1) (ω:=2*π) (x₀:=1) (k₀:=10) ({x_abstol := 1e-16, g_abstol := 0, show_trace := true, result_trace := true},200,()) 39.478418#eval 4*π^2

Original differential equation

$$\begin{align} \dot x &= \frac{p}{m} \\ \dot p &= -k x \\ x(0) &= x_0 \qquad p(0) = p_0 \end{align} $$

Derived differential equation that also solves for two additional variables \(xk\) and \(pk\) which are derivatives of the solution w.r.t. \(k\). The autodiff tactic uses the theorem odeSolve.arg_ft₀tx₀.fwdDeriv_rule that states what happends when we differentiate the solution of differential equation w.r.t. to parameter, inital condition or initial and terminal time.

$$\begin{align} \dot x &= \frac{p}{m} \\ \dot p &= -k x \\ \dot x_k &= \frac{p_k}{m} \\ \dot p_k &= -x - k x_k \\ x(0) = x_0 \qquad p(0) &= p_0 \qquad x_k(0) = 0 \qquad p_k(0) = 0 \end{align} $$