Skip to content

Commit 970f1cf

Browse files
committed
updates
1 parent 97cec76 commit 970f1cf

4 files changed

Lines changed: 166 additions & 12 deletions

File tree

‎ext/ControlSystemIdentificationLSOptExt.jl‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,13 @@ function _inner_pem(
178178
resid = zeros(T * ny)
179179
J = ForwardDiff.jacobian(residuals!, resid, res.minimizer)
180180
residuals!(resid, res.minimizer)
181-
(T - length(p_guess)) / dot(resid, resid) * Symmetric(J' * J)
181+
Σout = vec(sum(abs2, reshape(resid, ny, T), dims=2)) ./ (T - length(p_guess))
182+
inds = range(1, step=ny, length=T)
183+
for i = 1:ny
184+
@views J[inds, :] ./= sqrt(Σout[i])
185+
inds = inds .+ 1
186+
end
187+
Symmetric(J' * J)
182188
end
183189

184190
ukf = get_ukf(res.minimizer)

‎src/pem.jl‎

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -642,10 +642,10 @@ using ChainRules
642642
using ForwardDiffChainRules
643643
@ForwardDiff_frule LinearAlgebra.exp!(x1::AbstractMatrix{<:ForwardDiff.Dual}) true
644644

645-
function inner_constructor(p, constructor, Ts)
645+
function inner_constructor(p, constructor, Ts, method)
646646
sys = constructor(p)
647647
if iscontinuous(sys)
648-
return c2d(sys, Ts, :zoh), p.K, p.x0
648+
return c2d(sys, Ts, method), p.K, p.x0
649649
end
650650
return sys, p.K, p.x0
651651
end
@@ -710,6 +710,7 @@ function structured_pem(
710710
# alphaguess = LineSearches.InitialStatic(alpha = 0.95),
711711
linesearch = LineSearches.BackTracking(),
712712
),
713+
method = :zoh,
713714
store_trace = true,
714715
show_trace = true,
715716
show_every = 50,
@@ -742,22 +743,34 @@ function structured_pem(
742743
end
743744
p0ca = ComponentArray(p = p0, K = T.(K0), x0 = x0i)
744745
function predloss(p)
745-
sysi, Ki, x0 = inner_constructor(p, constructor, d.Ts)
746+
sysi, Ki, x0 = inner_constructor(p, constructor, d.Ts, method)
746747
syso = PredictionStateSpace(sysi, Ki, 0, 0)
747748
Pyh = ControlSystemsBase.observer_predictor(syso; h)
748749
simres = lsim(Pyh, pd; x0=Vector(x0))
749-
yh = simres.y
750+
yh = simres.y
750751
yh .= metric.(yh .- d.y)
751752
c1 = sum(yh)
752753
c1 + inner_regularizer(regularizer, p, syso, simres)
753754
end
754755
function simloss(p)
755-
syssim, _, x0 = inner_constructor(p, constructor, d.Ts)
756+
syssim, _, x0 = inner_constructor(p, constructor, d.Ts, method)
756757
simres = lsim(syssim, d; x0)
757-
y = simres.y
758+
y = simres.y
758759
y .= metric.(y .- d.y)
759760
sum(y) + inner_regularizer(regularizer, p, syssim, simres)
760761
end
762+
function simresiduals(p)
763+
syssim, _, x0 = inner_constructor(p, constructor, d.Ts, method)
764+
simres = lsim(syssim, d; x0)
765+
vec(simres.y .- d.y)
766+
end
767+
function predresiduals(p)
768+
sysi, Ki, x0 = inner_constructor(p, constructor, d.Ts, method)
769+
syso = PredictionStateSpace(sysi, Ki, 0, 0)
770+
Pyh = ControlSystemsBase.observer_predictor(syso; h)
771+
simres = lsim(Pyh, pd; x0=Vector(x0))
772+
vec(simres.y .- d.y[:, 1:size(simres.y, 2)])
773+
end
761774
res = Optim.optimize(
762775
pred ? predloss : simloss,
763776
p0ca,
@@ -767,7 +780,7 @@ function structured_pem(
767780
time_limit, x_abstol, f_abstol, g_tol, f_calls_limit, g_calls_limit);
768781
autodiff = AutoForwardDiff(),
769782
)
770-
sys_opt, K_opt, x0_opt = inner_constructor(res.minimizer, constructor, d.Ts)
783+
sys_opt, K_opt, x0_opt = inner_constructor(res.minimizer, constructor, d.Ts, method)
771784
sysp_opt = PredictionStateSpace(
772785
sys_opt,
773786
pred ? K_opt : zeros(T, nx, ny),
@@ -776,5 +789,20 @@ function structured_pem(
776789
zeros(T, nx, ny),
777790
)
778791
pred && !isstable(observer_predictor(sysp_opt)) && @warn("Estimated predictor dynamics A-KC is unstable")
779-
(; sys=sysp_opt, x0=x0_opt, res)
792+
function Λ()
793+
residuals = pred ? predresiduals : simresiduals
794+
resid = residuals(res.minimizer)
795+
J = ForwardDiff.jacobian(residuals, res.minimizer)
796+
Tn = length(resid) ÷ ny
797+
npar = length(res.minimizer)
798+
Σout = vec(sum(abs2, reshape(resid, ny, Tn), dims=2)) ./ (Tn - npar)
799+
inds = range(1, step=ny, length=Tn)
800+
for i = 1:ny
801+
@views J[inds, :] ./= sqrt(Σout[i])
802+
inds = inds .+ 1
803+
end
804+
Symmetric(J' * J)
805+
end
806+
ax = ComponentArrays.getaxes(res.minimizer)
807+
(; sys=sysp_opt, x0=x0_opt, res, Λ, constructor=p->inner_constructor(ComponentArray(p, ax), constructor, d.Ts, method))
780808
end

‎test/test_nonlinear_pem.jl‎

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ using ControlSystemIdentification, SeeToDee, LowLevelParticleFilters
22
using LeastSquaresOptim
33
using StaticArrays
44
using LinearAlgebra
5+
using Random, Statistics
56
using Test
67

78
function quadtank(h, u, p, t)
@@ -110,4 +111,73 @@ modelγ = ControlSystemIdentification.nonlinear_pem(d, discrete_dynamics, measur
110111

111112
γ = 1e4*ones(8) # Strong regularization to more or less force the optimum to be at the initial guess
112113
modelγ = ControlSystemIdentification.nonlinear_pem(d, discrete_dynamics, measurement, p0, x00, R1, 100R2, nu; γ, iterations=300, show_every=50, optimizer=Dogleg())
113-
@test norm(modelγ.p - p0) < 0.01norm(model.p - p0)
114+
@test norm(modelγ.p - p0) < 0.01norm(model.p - p0)
115+
116+
117+
## Covariance scaling vs bootstrap (ny=1 and ny=3)
118+
119+
@testset "nonlinear_pem covariance scaling" begin
120+
@info "Testing nonlinear_pem covariance scaling against bootstrap (ny=1 and ny=3)"
121+
122+
Ts_b = 1.0
123+
nT_b = 200
124+
σ_b = 0.3
125+
N_reps = 30
126+
127+
dyn_bs(x, u, p, t) = SA[p[1] * x[1] + p[2] * u[1]]
128+
meas_1(x, u, p, t) = SA[x[1]]
129+
meas_3(x, u, p, t) = SA[x[1], 0.5 * x[1], -0.3 * x[1]]
130+
131+
p_true_bs = [0.9, 1.0]
132+
x0_bs = SA[1.0]
133+
134+
Random.seed!(0)
135+
u_vec = [SA[0.5 * randn()] for _ in 1:nT_b]
136+
U_bs = reduce(hcat, u_vec)
137+
138+
function rollout_manual(f, x0, uvec, p)
139+
x = x0
140+
traj = Vector{typeof(x0)}(undef, length(uvec))
141+
for k in eachindex(uvec)
142+
traj[k] = x
143+
x = f(x, uvec[k], p, 0)
144+
end
145+
traj
146+
end
147+
x_traj = rollout_manual(dyn_bs, x0_bs, u_vec, p_true_bs)
148+
149+
function bootstrap_compare(meas, ny, seed_offset)
150+
y_clean_vec = meas.(x_traj, u_vec, Ref(p_true_bs), 0)
151+
Y_clean = reduce(hcat, y_clean_vec)
152+
153+
R1_bs = Matrix(1e-6 * I, 1, 1)
154+
R2_bs = Matrix(σ_b^2 * I, ny, ny)
155+
156+
p_ests = zeros(2, N_reps)
157+
var_ests = zeros(2, N_reps)
158+
159+
redirect_stdout(devnull) do
160+
for rep in 1:N_reps
161+
Random.seed!(seed_offset + rep)
162+
Y_noisy = Y_clean .+ σ_b .* randn(ny, nT_b)
163+
d_rep = iddata(Y_noisy, U_bs, Ts_b)
164+
model_b = ControlSystemIdentification.nonlinear_pem(
165+
d_rep, dyn_bs, meas, p_true_bs, [x0_bs...], R1_bs, R2_bs, 1,
166+
)
167+
p_ests[:, rep] = model_b.p
168+
var_ests[:, rep] = diag(inv(model_b.Λ()))[1:2]
169+
end
170+
end
171+
vec(mean(var_ests, dims=2)), vec(var(p_ests, dims=2))
172+
end
173+
174+
# ny = 1
175+
mean_est_1, emp_var_1 = bootstrap_compare(meas_1, 1, 100)
176+
@test 0.6 < mean_est_1[1] / emp_var_1[1] < 1.7
177+
@test 0.6 < mean_est_1[2] / emp_var_1[2] < 1.7
178+
179+
# ny = 3 — catches the (T·ny vs T) bug in the previous master formula
180+
mean_est_3, emp_var_3 = bootstrap_compare(meas_3, 3, 200)
181+
@test 0.6 < mean_est_3[1] / emp_var_3[1] < 1.7
182+
@test 0.6 < mean_est_3[2] / emp_var_3[2] < 1.7
183+
end

‎test/test_pem.jl‎

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using ControlSystemIdentification, Optim, ControlSystemsBase
22
using ControlSystemsBase.DemoSystems: resonant
3-
using Random, Test
3+
using Random, Test, Statistics, LinearAlgebra
44
Random.seed!(1)
55
T = 1000
66
sys = c2d(resonant(ω0 = 0.1) * tf(1, [0.1, 1]), 1)# generate_system(nx, nu, ny)
@@ -358,7 +358,57 @@ end
358358
res = ControlSystemIdentification.structured_pem(d, nx; focus=:simulation, p0, constructor, regularizer, show_trace = false, show_every = 1, iterations=300)
359359
p_est = res.res.minimizer.p
360360
@test norm(p_est - p_true) < 1e-6
361-
361+
362+
end
363+
364+
365+
@testset "structured_pem covariance scaling" begin
366+
@info "Testing structured_pem covariance scaling against bootstrap (ny=1 and ny=3)"
367+
368+
Ts_b = 0.1
369+
nT_b = 200
370+
t_b = 0:Ts_b:Ts_b*(nT_b-1)
371+
σ_b = 0.3
372+
N_reps = 50
373+
374+
Random.seed!(0)
375+
u_b = randn(1, nT_b)
376+
377+
function bootstrap_compare(constructor, p_true, seed_offset)
378+
y_clean = lsim(c2d(constructor(p_true), Ts_b), u_b, t_b).y
379+
p_ests = zeros(length(p_true), N_reps)
380+
var_ests = zeros(length(p_true), N_reps)
381+
for rep in 1:N_reps
382+
Random.seed!(seed_offset + rep)
383+
d_rep = iddata(y_clean .+ σ_b .* randn(size(y_clean)), u_b, Ts_b)
384+
res = ControlSystemIdentification.structured_pem(
385+
d_rep, 1; focus=:simulation, p0=p_true, constructor,
386+
show_trace=false, iterations=100,
387+
)
388+
p_ests[:, rep] = collect(res.res.minimizer.p)
389+
var_ests[:, rep] = diag(inv(res.Λ()))[1:length(p_true)]
390+
end
391+
vec(mean(var_ests, dims=2)), vec(var(p_ests, dims=2))
392+
end
393+
394+
# --- ny = 1 (catches the missing factor of ½ — buggy formula would give ratio ≈ 0.5) ---
395+
constructor_1 = function (p)
396+
a, b = p
397+
ss(reshape([a], 1, 1), reshape([b], 1, 1), reshape([1.0], 1, 1), 0)
398+
end
399+
mean_est_1, emp_var_1 = bootstrap_compare(constructor_1, [-1.0, 1.0], 100)
400+
@test 0.6 < mean_est_1[1] / emp_var_1[1] < 1.7
401+
@test 0.6 < mean_est_1[2] / emp_var_1[2] < 1.7
402+
403+
# --- ny = 3 (catches the T vs T·ny error — buggy formula would give ratio ≈ 1.5) ---
404+
C_3 = reshape([1.0, 0.5, -0.3], 3, 1)
405+
constructor_3 = function (p)
406+
a, b = p
407+
ss(reshape([a], 1, 1), reshape([b], 1, 1), C_3, zeros(3, 1))
408+
end
409+
mean_est_3, emp_var_3 = bootstrap_compare(constructor_3, [-1.0, 1.0], 200)
410+
@test 0.6 < mean_est_3[1] / emp_var_3[1] < 1.7
411+
@test 0.6 < mean_est_3[2] / emp_var_3[2] < 1.7
362412
end
363413

364414

0 commit comments

Comments
 (0)