Calibrating Models to Data
In this tutorial, we will showcase the tooling for fitting models to data. Let's take our favorite 2nd order Lorenz equation form as our model:
using EasyModelAnalysis
@parameters t σ ρ β
@variables x(t) y(t) z(t)
D = Differential(t)
eqs = [D(D(x)) ~ σ * (y - x),
D(y) ~ x * (ρ - z) - y,
D(z) ~ x * y - β * z]
@named sys = ODESystem(eqs)
sys = structural_simplify(sys)
u0 = [D(x) => 2.0,
x => 1.0,
y => 0.0,
z => 0.0]
p = [σ => 28.0,
ρ => 10.0,
β => 8 / 3]
tspan = (0.0, 100.0)
prob = ODEProblem(sys, u0, tspan, p, jac = true)
sol = solve(prob)
retcode: Success
Interpolation: specialized 4th order "free" interpolation, specialized 2nd order "free" stiffness-aware interpolation
t: 1495-element Vector{Float64}:
0.0
0.00014131524176735154
0.0015544676594408668
0.012956381792766277
0.038266700044138006
0.07852291796751046
0.12210864065288088
0.17155375443120538
0.2311290630460472
0.30252383872495503
⋮
99.50500396864174
99.57665667367455
99.64283640514518
99.72166966002771
99.79967562312255
99.86801143860899
99.93304884850775
99.99013919862291
100.0
u: 1495-element Vector{Vector{Float64}}:
[2.0, 0.0, 0.0, 1.0]
[1.9960454103704428, 0.0014132521265988688, 9.986094350108765e-8, 1.0002823510089436]
[1.956746201469907, 0.01555657123298723, 1.2096178746316906e-5, 1.0030752466310615]
[1.6563910957343149, 0.13029732534611171, 0.0008464888325952064, 1.023645017393648]
[1.1011942420041494, 0.3874130558030291, 0.007445311135321615, 1.0582098436702096]
[0.5557298463522196, 0.7959541011040255, 0.031202921104640697, 1.0901348104540605]
[0.44950102412404563, 1.229007863529698, 0.07390400544865926, 1.1102092717261387]
[0.9266726317320947, 1.7062381668292075, 0.14159577334379553, 1.1416871365805596]
[2.2781005968110417, 2.2773793271385414, 0.25114094267068454, 1.233177108242911]
[4.867419855792264, 3.014693593518013, 0.44047994481421116, 1.4824570579833207]
⋮
[80.19625687470715, 0.22319498068561278, 6.377998459496999, -10.92101238125288]
[93.91126047193579, -1.9435112337378833, 5.7144265707275865, -4.580907294954086]
[92.47053186268388, -2.2128845219633724, 4.966589670343976, 1.653117805222435]
[78.34378276619968, 0.2736929875090918, 3.6720087645277166, 8.448758815091098]
[59.61988722700268, 5.207887350906977, 5.364596386122552, 13.834326658988568]
[42.06608771509131, 6.848721336825014, 10.923029356998175, 17.327760613533872]
[17.746361864729497, 2.561462421673927, 14.710142641007621, 19.336065831253446]
[-13.425204706776736, -1.9987153748274389, 12.635789734327103, 19.49627693453924]
[-19.39944052157736, -2.411052627709995, 11.88863957641005, 19.334495160672596]
Let's create a dataset with some set of parameters, and show how the datafit
function can be used to discover the parameters that generated the data. To start, let's show the data format by generating a dataset. A dataset contains an array t
of the time points for the data, and maps [observable => timeseries]
where the timeseries is an array of values for the observable at the time points of t
. We can use the get_timeseries
function to generate a dataset like:
tsave = [1.0, 2.0, 3.0]
data = [x => get_timeseries(prob, x, tsave), z => get_timeseries(prob, z, tsave)]
2-element Vector{Pair{Num, Vector{Float64}}}:
x(t) => [11.352378507900013, 11.818374125301172, -10.730122690040698]
z(t) => [8.983834702143962, 4.623642052356029, 6.172956575154105]
Now let's do a datafit. We need to choose initial parameters for the fitting process and call the datafit:
psub_ini = [σ => 27.0, β => 3.0]
fit = datafit(prob, psub_ini, tsave, data)
2-element Vector{Pair{Num, Float64}}:
σ => 28.000000012195933
β => 2.66666667211938
Recall that our starting parameters, the parameters the dataset was generated from, was [σ => 28.0, ρ => 10.0, β => 8 / 3]
. Looks like this did a good job at recovering them!