1
1
# Let's try parameter estimation for an ODE, here the famous Lorentz attractor.
2
2
# We base our implementation on the code from Paulo Marques available at:
3
3
# https://github.com/pjpmarques/Julia-Modeling-the-World/blob/master/Lorenz%20Attractor.ipynb
4
- function lorentz_equations (params:: Vector{Float64} , r:: Vector{Float64} )
4
+
5
+ function lorentz_ode! (dr:: AbstractVector{Float64} ,
6
+ params:: AbstractVector{Float64} ,
7
+ r:: AbstractVector{Float64} )
8
+ @assert length (dr) == length (r) == 3
9
+
5
10
# Get individual params for this ODE
6
- sigma, rho, beta = params
11
+ @inbounds sigma, rho, beta = params
7
12
8
13
# Extract the coordinates from the r vector
9
- x, y, z = r
14
+ @inbounds x, y, z = r
10
15
11
- # The Lorenz equations
12
- dx_dt = sigma* (y - x)
13
- dy_dt = x* (rho - z) - y
14
- dz_dt = x* y - beta* z
16
+ # The Lorenz equations, put the derivative into dr
17
+ @inbounds dr[ 1 ] = sigma* (y - x) # dx/dt
18
+ @inbounds dr[ 2 ] = x* (rho - z) - y # dy/dt
19
+ @inbounds dr[ 3 ] = x* y - beta* z # dz/dt
15
20
16
- # Return the derivatives as a vector
17
- Float64[dx_dt, dy_dt, dz_dt]
21
+ return dr
18
22
end
19
23
20
24
# Define time vector and interval grid.
21
- dt = 0.001
22
- tf = 100.0
23
- tinterval = 0 : dt: tf
24
- t = collect (tinterval)
25
-
25
+ t = range (0.0 , step= 0.001 , stop= 100.0 )
26
26
# But in order to compare to [Xiang2015] paper we use their choice instead:
27
- h = 0.01
28
- M = 300
29
- tstart = 0.0
30
- tstop = tstart + M * h
31
- tinterval_Xiang2015 = 0 : h: tstop
32
- t_Xiang2015 = collect (tinterval_Xiang2015)
27
+ t_Xiang2015 = range (0.0 , step= 0.01 , length= 301 )
33
28
34
29
# Initial position in space
35
- r0 = [0.1 ; 0.0 ; 0.0 ]
30
+ r0 = [0.1 , 0.0 , 0.0 ]
36
31
37
32
# Constants sigma, rho and beta. In a real ODE problem these would not be known
38
33
# and would be estimated.
@@ -42,65 +37,80 @@ beta = 8.0/3.0
42
37
real_params = [sigma, rho, beta]
43
38
44
39
# "Play" an ODE from a starting point and into the future given a sequence of time steps.
45
- function calc_state_vectors (params:: Vector{Float64} , odefunc:: Function ,
46
- startx:: Vector{Float64} , times:: Vector{Float64} ; states = nothing )
40
+ # Put the restults into `states`
41
+ function calc_states! (states:: AbstractMatrix{Float64} ,
42
+ params:: AbstractVector{Float64} , odefunc!:: Function ,
43
+ startx:: AbstractVector{Float64} , times:: AbstractVector{Float64} )
47
44
48
45
numsamples = length (times)
49
- if states == nothing
50
- states = Array (Float64, length (startx), numsamples)
51
- end
46
+ @assert size (states) == (length (startx), numsamples)
52
47
53
48
states[:, 1 ] = startx
54
49
tprev = times[1 ]
50
+ deriv = similar (startx)
55
51
for i in 2 : numsamples
56
- @inbounds derivatives = odefunc (params, states[:, (i- 1 )])
57
- @inbounds tnow = times[i]
58
- @inbounds states[:, i] = states[:, (i- 1 )] .+ derivatives * (tnow - tprev)
52
+ tnow = times[i]
53
+ stateprev = view (states, :, i- 1 )
54
+ odefunc! (deriv, params, stateprev)
55
+ @inbounds states[:, i] .= stateprev .+ deriv .* (tnow - tprev)
59
56
tprev = tnow
60
57
end
61
58
62
59
return states
63
60
end
64
61
62
+ calc_states (params:: AbstractVector{Float64} , odefunc!:: Function ,
63
+ startx:: AbstractVector{Float64} , times:: AbstractVector{Float64} ) =
64
+ calc_states! (Matrix {Float64} (undef, length (startx), length (times)),
65
+ params, odefunc!, startx, times)
66
+
65
67
# RSS = Residual Sum of Squares, columnwise
66
- function rss (actual:: Array{Float64, 2} , estimated:: Array{Float64, 2} )
67
- M = size (actual, 2 )
68
- sumsq = 0.0
69
- for i in 1 : M
70
- @inbounds sumsq += sumabs2 (actual[:, i] .- estimated[:, i])
71
- end
72
- sumsq
73
- end
68
+ rss (actual:: AbstractMatrix{Float64} , estimated:: AbstractMatrix{Float64} ) =
69
+ @inbounds sum (i -> abs2 (actual[i] - estimated[i]), eachindex (actual, estimated))
74
70
75
71
# Calculate the actual/original state vectors that we will use for parameter
76
72
# estimation:
77
- origstates = calc_state_vectors (real_params, lorentz_equations , r0, t)
78
- origstates_Xiang2015 = calc_state_vectors (real_params, lorentz_equations , r0, t_Xiang2015)
73
+ origstates = calc_states (real_params, lorentz_ode! , r0, t)
74
+ origstates_Xiang2015 = calc_states (real_params, lorentz_ode! , r0, t_Xiang2015)
79
75
80
- function subsample (origstates:: Array{Float64, 2} , times:: Vector{Float64} , lenratio = 0.25 )
81
- @assert size (origstates, 2 ) == length (times)
76
+ function subsample (origstates:: AbstractMatrix{Float64} ,
77
+ times:: AbstractVector{Float64} ;
78
+ lenratio = 0.25 )
82
79
N = length (times)
80
+ @assert size (origstates, 2 ) == N
83
81
stopidx = round (Int, lenratio* N)
84
82
indexes = 1 : stopidx
85
83
return origstates[:, indexes], times[indexes]
86
84
end
87
85
88
86
# The [Xiang2015] paper, https://www.hindawi.com/journals/ddns/2015/740721/,
89
87
# used these param bounds:
90
- Xiang2015Bounds = Tuple{Float64, Float64} [(9 , 11 ), (20 , 30 ), (2 , 3 )]
88
+ Xiang2015Bounds = [(9. , 11. ), (20. , 30. ), (2. , 3. )]
91
89
92
90
# Now we can optimize using BlackBoxOptim
93
91
using BlackBoxOptim
94
92
95
- function lorentz_fitness (params:: Vector{Float64} , origstates:: Array{Float64, 2} , times:: Vector{Float64} )
96
- states = calc_state_vectors (params, lorentz_equations, r0, times)
93
+ # store temporary states for fitness calculation
94
+ const tmpstates_pool = Dict {NTuple{2, Int}, Vector{Matrix{Float64}}} ()
95
+
96
+ # get the RSS between the origstates trajectory and ODE solution for given params
97
+ function lorentz_fitness (params:: AbstractVector{Float64} ,
98
+ origstates:: AbstractMatrix{Float64} ,
99
+ times:: AbstractVector{Float64} )
100
+ # get the state matrix from the pool of the proper size
101
+ statesize = size (origstates)
102
+ states_pool = get! (() -> Vector {Matrix{Float64}} (), tmpstates_pool, statesize)
103
+ states = ! isempty (states_pool) ? pop! (states_pool) : Matrix {Float64} (undef, statesize... )
104
+ # solve ODE for given params
105
+ calc_states! (states, params, lorentz_ode!, r0, times)
106
+ push! (states_pool, states) # return states matrix back to the pool
97
107
return rss (origstates, states)
98
108
end
99
109
100
110
# But optimizing all states in each optimization step is too much so lets
101
111
# sample a small subset and use for first opt iteration.
102
- origstates1, times1 = subsample (origstates, t, 0.04 ); # Sample only first 4%
103
- origstates1_Xiang2015, times1_Xiang2015 = subsample (origstates_Xiang2015, t_Xiang2015, 1.00 );
112
+ origstates1, times1 = subsample (origstates, t; lenratio = 0.04 ); # Sample only first 4%
113
+ origstates1_Xiang2015, times1_Xiang2015 = subsample (origstates_Xiang2015, t_Xiang2015; lenratio = 1.00 );
104
114
105
115
res1 = bboptimize (params -> lorentz_fitness (params, origstates1, times1);
106
116
SearchRange = Xiang2015Bounds, MaxSteps = 8e3 )
@@ -109,24 +119,24 @@ res2 = bboptimize(params -> lorentz_fitness(params, origstates1_Xiang2015, times
109
119
SearchRange = Xiang2015Bounds, MaxSteps = 11e3 ) # They allow 12k fitness evals for 3-param estimation
110
120
111
121
# But lets also try with less tight bounds
112
- LooserBounds = Tuple{Float64, Float64} [(0 , 22 ), (0 , 60 ), (1 , 6 )]
122
+ LooserBounds = [(0. , 22. ), (0. , 60. ), (1. , 6. )]
113
123
res3 = bboptimize (params -> lorentz_fitness (params, origstates1_Xiang2015, times1_Xiang2015);
114
124
SearchRange = LooserBounds, MaxSteps = 11e3 ) # They allow 12k fitness evals for 3-param estimation
115
125
116
- println ( " Results on the long time sequence from Paulo Marques:" )
126
+ @info " Results on the long time sequence from Paulo Marques:"
117
127
estfitness = lorentz_fitness (best_candidate (res1), origstates, t)
118
- @show ( estfitness, best_candidate (res1), best_fitness (res1) )
128
+ @show estfitness best_candidate (res1) best_fitness (res1)
119
129
origfitness = lorentz_fitness (real_params, origstates, t)
120
- @show ( origfitness, real_params)
130
+ @show origfitness real_params
121
131
122
- println ( " Results on the short time sequence used in [Xiang2015] paper:" )
132
+ @info " Results on the short time sequence used in [Xiang2015] paper:"
123
133
estfitness = lorentz_fitness (best_candidate (res2), origstates_Xiang2015, t_Xiang2015)
124
- @show ( estfitness, best_candidate (res2), best_fitness (res2) )
134
+ @show estfitness best_candidate (res2) best_fitness (res2)
125
135
origfitness = lorentz_fitness (real_params, origstates_Xiang2015, t_Xiang2015)
126
- @show ( origfitness, real_params)
136
+ @show origfitness real_params
127
137
128
- println ( " Results on the short time sequence used in [Xiang2015] paper, but with looser bounds:" )
138
+ @info " Results on the short time sequence used in [Xiang2015] paper, but with looser bounds:"
129
139
estfitness = lorentz_fitness (best_candidate (res3), origstates_Xiang2015, t_Xiang2015)
130
- @show ( estfitness, best_candidate (res3), best_fitness (res3) )
140
+ @show estfitness best_candidate (res3) best_fitness (res3)
131
141
origfitness = lorentz_fitness (real_params, origstates_Xiang2015, t_Xiang2015)
132
- @show ( origfitness, real_params)
142
+ @show origfitness real_params
0 commit comments