A modified energy that every step of symplectic Euler leaves exactly unchanged on the harmonic oscillator, stated and proved in Lean 4, with the step size under which it stays bounded, and the reason the energy of the method oscillates and does not grow.
With scripts, this page keeps a record in your browser of whether you completed the lesson. Nothing is stored without them.
With scripts, this place suggests what to do next, with the reason, from the progress saved in your browser. Without them, follow the learning path.
The numerical integrators lesson found that symplectic Euler does what explicit Euler cannot: on the harmonic oscillator its energy does not drift away. It swings a little above and below its starting value, over and over, for as long as the run lasts. The first Lean lesson proved the opposite for explicit Euler, a factor greater than one at every step. A run shows the bounded energy of symplectic Euler for the step sizes it was given and not for any other, and it does not say why.
The reason is a quantity that each step of symplectic Euler leaves exactly where it was. It is not the energy of the spring but a neighbour of it: the energy minus a small term. This lesson states that quantity in Lean and proves that it is conserved, for any state and any step size, and then proves that it is not negative when the step is small. The energy of the spring differs from it by a term that stays small, which is the oscillation the simulation shows.
Assumptions
Every assumption of the model is a hypothesis of a theorem or a definition; the build fails on sorry and on any axiom the project declares.
Newton’s second law with Hooke’s law. The mass on a spring accelerates as : the hypothesis hnewton.
The update rule of symplectic Euler. The velocity changes first, with the acceleration at the old position (hv'), and the particle moves with the new velocity (hx'). Moving with the old velocity is explicit Euler, which the first Lean lesson covers and which has no such conserved quantity.
A positive mass.hm : 0 < m lets the proof divide by the mass.
For the bound: a spring that pulls back, and a small step. The non-negativity of the modified energy needs hk : 0 < k and hdt : k * dt ^ 2 < 4 * m. The condition says that the step is shorter than a fraction of the period; for a longer step the quantity is still conserved but can be negative, and the bound is lost.
Real numbers, not floating-point numbers. Lean’s ℝ has no round-off. The program computes with doubles, and the conservation holds for them only up to the round-off of every step.
No derivatives. The facts are algebraic. The theorem does not mention the motion in continuous time, and it does not say how close the modified energy is to the true one.
What the theorems say
Write for the squared angular frequency. One step of symplectic Euler is
The energy of the spring is . The modified energy subtracts a cross term:
One step leaves unchanged. The proof expands the two sides in and and compares them: the terms in that the new velocity inside the position update introduces are exactly what the cross term removes.
Formal theorem (symplectic Euler on a mass of positive mass on a spring, any step size, real arithmetic): One step of symplectic Euler on the harmonic oscillator leaves the modified energy unchanged.
Formal theorem (symplectic Euler on the oscillator, any step size and any number of steps, real arithmetic): After any number of steps of symplectic Euler, the modified energy equals its initial value.
Formal theorem (the step sizes below the stated limit, real arithmetic): For a spring constant that is positive and a step with , the modified energy is never negative.
The third statement is what turns a conserved number into a bound. With a non-negative conserved quantity of this shape, the motion stays inside an ellipse in : position and velocity cannot grow without limit. The cross term is small when the step is small, so the true energy stays near the modified one, and the simulation sees it oscillate.
The proofs
The listings are read from the Lean files of the repository when the site is built, and they are the text that lake build compiled.
Compiled with Lean 4.34.1 and Mathlib v4.34.1: ./scripts/check-lean.sh found no sorry and no axiom declared by the project before this page was built.
./scripts/check-lean.sh ran lake build on every module of the project and found no sorry and no axiom declared by the project before this page was built. CI runs the same check on every pull request and on main.
Lean 4.34.1, from the toolchain pinned in lean/lean-toolchain.
The file in the repository at that commit and the result of the CI run are authoritative. The Lean web editor is a convenience: it runs its own version of Mathlib, which can differ from the one above, so a proof that compiled here may report an error there.
The definition. It reuses springEnergy from the first Lean lesson’s file and subtracts the cross term.
/-- The modified energy of symplectic Euler with the time step `dt`: theenergy of the spring minus `k dt x v / 2`. -/noncomputabledef modifiedEnergy (m k dt x v : ℝ) : ℝ := springEnergy m k x v - k * dt * x * v / 2
One step. The hypotheses hv' and hx' are the update rule, and hnewton is the law of the spring. Compare hx' here with the one of explicit Euler: the position uses v'.
/-- **One symplectic Euler step leaves the modified energy unchanged.**`hv'` updates the velocity with the acceleration at the old position, and`hx'` moves the particle with the *new* velocity. `hnewton` is Newton's lawfor the spring, `m a = -k x`. -/theorem symplectic_modified_energy_step (m k x v a dt x' v' : ℝ) (hm : 0 < m) (hnewton : m * a = -k * x) (hv' : v' = v + dt * a) (hx' : x' = x + dt * v') : modifiedEnergy m k dt x' v' = modifiedEnergy m k dt x v := byhave ha : a = -k * x / m := byfield_simplinarith [mul_comm m a]subst hx' hv' haunfold modifiedEnergy springEnergyfield_simpring
Any number of steps, by induction. The base case is the initial state. The step rewrites the energy after one more step with the one-step theorem, then uses the hypothesis of the induction.
/-- **After any number of steps the modified energy is the initial one.**By induction on the number of steps, from the one-step theorem. -/theorem symplectic_modified_energy_after_steps (m k dt : ℝ) (hm : 0 < m) (x v a : ℕ → ℝ) (hnewton : ∀ n, m * a n = -k * x n) (hv : ∀ n, v (n + 1) = v n + dt * a n) (hx : ∀ n, x (n + 1) = x n + dt * v (n + 1)) (n : ℕ) : modifiedEnergy m k dt (x n) (v n) = modifiedEnergy m k dt (x 0) (v 0) := byinduction n with | zero => rfl | succ n ih =>rw [symplectic_modified_energy_step m k (x n) (v n) (a n) dt _ _ hm (hnewton n) (hv n) (hx n), ih]
The bound. The proof completes a square: the quadratic form in and is a square plus a positive multiple of whenever the step is short.
/-- **The modified energy is not negative when the step is small.** For`k Δt² < 4 m` the quadratic form `m v² - k Δt x v + k x²` has no negativevalue, so the conserved quantity bounds the motion: `x` and `v` cannot growwithout limit. -/theorem modified_energy_nonneg (m k dt x v : ℝ) (hm : 0 < m) (hk : 0 < k) (hdt : k * dt ^ 2 < 4 * m) :0 ≤ modifiedEnergy m k dt x v := byunfold modifiedEnergy springEnergyhave h1 : 0 ≤ (m * v - k * dt * x / 2) ^ 2 := sq_nonneg _have h2 : 0 ≤ k * (4 * m - k * dt ^ 2) * x ^ 2 := byhave : 0 < 4 * m - k * dt ^ 2 := bylinarithpositivityhave h3 : 0 ≤ m * (m * v ^ 2 / 2 + k * x ^ 2 / 2 - k * dt * x * v / 2) := bynlinarithexact nonneg_of_mul_nonneg_right h3 hm
Half a kilogram on a spring of two newtons per metre, pulled out one metre and released, run with symplectic Euler for three periods at sixteen steps per period. The cell measures the energy of the spring and the modified energy along the run.
mass =0.5# kgk =2.0# N/mperiod =2* np.pi / angular_frequency(mass, k)steps_per_period =16dt = period / steps_per_periodn_steps =3* steps_per_period
def modified_energy(trajectory):"""The energy of the spring minus k dt x v / 2, at every instant (J).""" cross = np.sum(trajectory.x * trajectory.v, axis=1)return energy(trajectory, mass, k) -0.5* k * dt * cross
def three_periods(experiment: Experiment):"""Three periods with symplectic Euler: the energy of the spring and the modified energy; record the table.""" symplectic = simulate( State(t=0.0, x=1.0, v=0.0), newton(mass, spring(k)), dt, n_steps, step=symplectic_euler_step, ) spring_energy = energy(symplectic, mass, k) modified = modified_energy(symplectic) drift = np.max(np.abs(modified / modified[0] -1)) swing = spring_energy.max() / spring_energy.min() -1 experiment.table("three-periods", [ ("energy of the spring", spring_energy.min(), spring_energy.max()), ("modified energy", modified.min(), modified.max()), ], [("quantity", "", ""), ("smallest", "J", ".4f"), ("largest", "J", ".4f")],"The energy of the spring and the modified energy over the run, symplectic ""Euler.", )return symplectic, spring_energy, modified, drift, swing
The energy of the spring and the modified energy over the run, symplectic Euler.
quantity
smallest (J)
largest (J)
energy of the spring
0.8403
1.2390
modified energy
1.0000
1.0000
The modified energy is the same at every step to round-off (the relative change over the run is 8e-16), while the energy of the spring swings by a relative 0.47 between its smallest and largest value. That is the theorem on a run, and the swing is the cross term at work.
def draw_modified_energy(symplectic, spring_energy, modified) -> Figure:"""The energy of the spring and the modified energy against time.""" fig, ax = plt.subplots(figsize=(6.4, 3.6)) ax.plot(symplectic.t, spring_energy, color="#93003a", label="energy of the spring") ax.plot( symplectic.t, modified, color="#00429d", linestyle="--", label="modified energy" ) ax.set_xlabel("time (s)") ax.set_ylabel("energy (J)") ax.legend()return fig
Figure 1: simulated The energy of the spring swings above and below its start under symplectic Euler, while the modified energy stays on the line.
The same quantity under explicit Euler
The theorem is about the rule of symplectic Euler. Under explicit Euler the same quantity, and the energy of the spring too, are not conserved.
def explicit_euler():"""Exercise 1: the modified energy along explicit Euler.""" explicit = simulate( State(t=0.0, x=1.0, v=0.0), newton(mass, spring(k)), dt, n_steps, step=euler_step, ) explicit_modified = modified_energy(explicit) growth = explicit_modified[-1] / explicit_modified[0]print("modified energy after three periods of explicit Euler, relative to the"f" start: {growth:.2f}" )
modified energy after three periods of explicit Euler, relative to the start: 788.88
The modified energy of explicit Euler grows with the energy. The quantity is a property of the update rule and not of the oscillator alone.
The bound and the step size
The bound needs , that is, a step shorter than the period divided by . Above it the quadratic form is indefinite. The cell evaluates the smallest value of the modified energy over the unit circle in for several step sizes.
def positive_or_not(experiment: Experiment) ->None:"""Exercise 2: the smallest modified energy on the unit circle for four steps; record the table.""" angles = np.linspace(0, 2* np.pi, 3601) rows = []for per_period in (64, 16, 4, 3): step = period / per_period x, v = np.cos(angles), np.sin(angles) value =0.5* mass * v**2+0.5* k * x**2-0.5* k * step * x * v rows.append((per_period, k * step**2/ (4* mass), value.min())) experiment.table("positive-or-not", rows, [ ("steps per period", "", "d"), ("k dt^2 / 4m", "", ".3f"), ("smallest modified energy on the circle", "J", ".4f"), ],"The smallest value of the modified energy on the unit circle in position ""and velocity, for four step sizes.", )
The smallest value of the modified energy on the unit circle in position and velocity, for four step sizes.
steps per period
k dt^2 / 4m
smallest modified energy on the circle (J)
64
0.002
0.2492
16
0.039
0.2374
4
0.617
0.0820
3
1.097
-0.0190
While the ratio is below one the minimum is positive, which is the theorem. The last row has a ratio above one, and its minimum is no longer positive: the bound is lost there.
Limits
A statement about the method, not the physics. The modified energy is conserved by the update rule. That the energy of the spring stays near it is what the cross term, small for a small step, says, and it is an estimate that is not proved here.
Only the harmonic oscillator. The conserved quantity was found for the linear force of a spring. For other forces, symplectic methods conserve a modified energy only approximately, a result that needs more than algebra.
The Python code. Nothing here is a statement about pbc.mechanics. The theorem is about the rule written in Lean; the table above checks, for one run, that the program follows it.
Floating-point arithmetic.ℝ is exact. A double carries a rounding error in every step, and the modified energy of a long run of the program drifts by a random walk of those errors.
Stability of the method. The bound is on the quantity. A step that violates hdt is not shown unstable by this lesson, only left without the bound.
Self-check. The theorem says the modified energy is the same after every step. Why does the ordinary energy of the spring still change?
With scripts, you can record in this browser that you answered this question.
Solution to the self-check
Because the conserved quantity is the energy plus a cross term that changes during the oscillation: it is positive when the particle moves away from the centre and negative when it returns. The sum is constant, so the energy of the spring changes by the opposite of the cross term, which is bounded and small for a small step.
Exercises
Exercise 1.Why the cross term? Suppose the conserved quantity had the form for some constant . Find so that the coefficient of after a step is again .
Solution to exercise 1
After one step the quantity is with . Collecting the coefficient of gives , which equals only for . The coefficient of is then , and substituting turns the whole expression back into the quantity before the step. The cell checks it on a random state.
def quantity(x, v, c):return0.5* mass * v**2+0.5* k * x**2+ c * x * v
def the_value_of_c():"""Exercise 3: the value of c, checked on a random state.""" rng = np.random.default_rng(3) x0, v0 = rng.normal(size=2) v1 = v0 - k / mass * x0 * dt x1 = x0 + dt * v1 c =-0.5* k * dtprint("change of the quantity over a step with c = -k dt / 2: "f"{quantity(x1, v1, c) - quantity(x0, v0, c):.1e}" )
The experiment writes every figure of this page into results/, as <label>.png, and every number and table of the page, as the page shows it, into results/results.json. Its numbers match this page to the digits shown, and its figure files are the ones the page shows; digits beyond those can differ between machines, because floating-point libraries differ. Change the code of the experiment and run it again to see what changes.
lake build compiles the proofs of this page with the Lean toolchain that elan installs; the first run fetches the Mathlib build cache, which is large. The setup page says how to install elan.