Skip to content

Commit 9039ef4

Browse files
Merge pull request #2 from TUM-ITR/bugfix
Updated Kernel Definition
2 parents b65e72d + 98bf35c commit 9039ef4

11 files changed

Lines changed: 308 additions & 282 deletions

CITATION.cff

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ authors:
1313
given-names: "Sandra"
1414
orcid: "https://orcid.org/0000-0001-7819-5926"
1515
title: "PGopt"
16-
version: 0.2.1
17-
doi: 10.5281/zenodo.13737628
18-
date-released: 2024-05-23
16+
version: 0.2.2
17+
doi: "10.5281/zenodo.13737628"
18+
date-released: 2024-12-19
1919
url: "https://github.com/TUM-ITR/PGopt"
2020
preferred-citation:
2121
type: conference-paper

Julia/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,20 @@ For the results in Table II of the paper, this script is repeated with seeds 1:1
5454
This script is similar to `PG_OCP_known_basis_functions_Altro.jl`, but the solver IPOPT is used instead of Altro. Due to the different solver, the results differ slightly from the ones presented in the paper.
5555

5656
### PG_OCP_generic_basis_functions_Altro.jl
57-
This script reproduces the results of the optimal control approach with generic basis functions (Figure 3) given in Section V-C of the paper.
57+
This script reproduces the results of the optimal control approach with generic basis functions (Figure 3) given in Section V-C of the paper. Due to small adjustments to the code (see Erratum in the [general README](../README.md)) and resulting numerical differences, the results from **v0.2.2** and later differ slightly from those presented in the paper. If you wish to reproduce the exact results from the paper, you can pull an earlier release of the code before version **v0.2.2**.
5858

5959
<p align = "left">
6060
 <img src="figures/PG_OCP_generic_basis_functions.svg" alt="PG_OCP_generic_basis_functions" style="width:80%;"/>
6161
</p>
6262

63-
The method presented in the paper ["A flexible state–space model for learning nonlinear dynamical systems"](https://doi.org/10.1016/j.automatica.2017.02.030) is utilized to systematically derive basis functions and priors for the parameters based on a reduced-rank GP approximation. Afterward, by calling the function `particle_Gibbs()`, samples are drawn from the posterior distribution using particle Gibbs sampling. These samples are then passed to the function `solve_PG_OCP_Altro()`, which solves the scenario OCP using Altro.
63+
In this example, the method presented in the paper ["A flexible state–space model for learning nonlinear dynamical systems"](https://doi.org/10.1016/j.automatica.2017.02.030) is utilized to systematically derive basis functions and priors for the parameters based on a reduced-rank GP approximation. Afterward, by calling the function `particle_Gibbs()`, samples are drawn from the posterior distribution using particle Gibbs sampling. These samples are then passed to the function `solve_PG_OCP_Altro()`, which solves the scenario OCP using Altro.
6464

65-
The runtime of the script is about 2 hours on a standard laptop. Using an improved phi function can reduce the runtime to about 50 minutes, but the results change slightly due to numerical reasons. Further explanations can be found in the source code.
65+
The runtime of the script is about 1 hour on a standard laptop.
6666

6767
For the results in Table IV of the paper, this script is repeated with seeds 1:100.
6868

6969
### PG_OCP_generic_basis_functions_Ipopt.jl
70-
This script is similar to `PG_OCP_generic_basis_functions_Altro.jl`, but the solver IPOPT is used instead of Altro. Due to the different solver, the results differ slightly from the ones presented in the paper.
70+
This script is similar to `PG_OCP_generic_basis_functions_Altro.jl`, but the solver IPOPT is used instead of Altro.
7171

7272
## List of source code files
7373
The following files in the folder [src](src) contain the PGopt source code.

Julia/docs/src/assets/PG_OCP_generic_basis_functions.svg

Lines changed: 45 additions & 47 deletions
Loading

Julia/docs/src/examples/PG_OCP_generic_basis_functions.md

Lines changed: 57 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Optimal control with generic basis functions
22

3-
This example reproduces the results of the optimal control approach with generic basis functions (Figure 3) given in Section V-C of the [paper](../reference.md).
3+
This example reproduces the results of the optimal control approach with generic basis functions (Figure 3) given in Section V-C of the [paper](../reference.md). Due to small adjustments to the code (see Erratum in the [Introduction](../index.md)) and resulting numerical differences, the results from **v0.2.2** and later differ slightly from those presented in the paper. If you wish to reproduce the exact results from the paper, you can pull an earlier release of the code before version **v0.2.2**.
44

55
![autocorrelation](../assets/PG_OCP_generic_basis_functions.svg)
66

77
The method presented in the paper ["A flexible state–space model for learning nonlinear dynamical systems"](https://doi.org/10.1016/j.automatica.2017.02.030) is utilized to systematically derive basis functions and priors for the parameters based on a reduced-rank GP approximation. Afterward, by calling the function `particle_Gibbs()`, samples are drawn from the posterior distribution using particle Gibbs sampling. These samples are then passed to the function `solve_PG_OCP_Altro()` (or `solve_PG_OCP_Ipopt()` in case IPOPT is used), which solves the scenario OCP using the solver Altro.
88

9-
A Julia script that contains all the steps described in the following and exactly reproduces Figure 2 of the paper can be found at `PGopt/Julia/examples/PG_OCP_generic_basis_functions_Altro.jl`. For the results in Table IV of the paper, this script is repeated with seeds 1:100. The runtime of the script is about 2 hours on a standard laptop. Using an improved function `phi()`, can reduce the runtime to about 50 minutes, but the results change slightly due to numerical reasons. Further explanations are given below.
9+
A Julia script that contains all the steps described in the following and reproduces Figure 2 of the paper can be found at `PGopt/Julia/examples/PG_OCP_generic_basis_functions_Altro.jl`. For the results in Table IV of the paper, this script is repeated with seeds 1:100. The runtime of the script is about 2 hours on a standard laptop.
1010

1111
A similar example that utilizes the solver IPOPT can be found at `PGopt/Julia/examples/PG_OCP_generic_basis_functions_Ipopt.jl`. Due to the different solver, the results differ slightly from the ones presented in the paper.
1212

@@ -43,6 +43,9 @@ n_y = 1 # number of outputs
4343
Then, generate generic basis functions and priors based on a reduced-rank GP approximation.
4444
The approach is described in the paper ["A flexible state–space model for learning nonlinear dynamical systems"](https://doi.org/10.1016/j.automatica.2017.02.030).
4545
The equation numbers given in the following refer to this paper.
46+
In this example, a GP with a squared exponential kernel
47+
$k(z, z') = s_f \exp (-\frac{1}{2} (z - z')^\top \Lambda^{-1} (z - z'))$,
48+
where $\Lambda = l^2 I$ and $I$ is the identity matrix, is approximated.
4649
```julia
4750
n_phi_x = [5 5] # number of basis functions for each state
4851
n_phi_u = 5 # number of basis functions for the control input
@@ -54,10 +57,6 @@ L_u = 10 # interval length for u
5457
L = zeros(1, 1, n_z) # array containing the interval lengths
5558
L[1, 1, :] = [L_u L_x]
5659

57-
# Hyperparameters of the squared exponential kernel
58-
l = [2] # length scale
59-
sf = 100 # scale factor
60-
6160
# Initialize.
6261
j_vec = zeros(n_phi, 1, n_z) # contains all possible vectors j; j_vec[i, 1, :] corresponds to the vector j in eq. (5) for basis function i
6362
lambda = zeros(n_phi, n_z) # lambda[i, :] corresponds to the vector λ in eq. (9) (right-hand side) for basis function i
@@ -66,71 +65,65 @@ In the following, all possible vectors ``j`` are constructed (i.e., `j_vec`). Th
6665
```julia
6766
cart_prod_sets = Array{Any}(undef, n_z) # array of arrays; cart_prod_sets[i] corresponds to the i-th set to be considered for the Cartesian product, i.e., [1 : n_basis[i]].
6867
for i = 1:n_z
69-
  cart_prod_sets[i] = Array(1:n_phi_dims[i])
68+
cart_prod_sets[i] = Array(1:n_phi_dims[i])
7069
end
7170

7271
subscript_values = Array{Int64}(undef, n_z) # contains the equivalent subscript values corresponding to a given single index i
7372
variants = [1; cumprod(n_phi_dims[1:end-1])] # required to convert the single index i to the equivalent subscript value
7473

7574
# Construct Cartesian product and calculate spectral densities.
7675
for i in 1:n_phi
77-
  # Convert the single index i to the equivalent subscript values.
78-
  remaining = i - 1
79-
  for j in n_z:-1:1
80-
    subscript_values[j] = floor(remaining / variants[j]) + 1
81-
    remaining = mod(remaining, variants[j])
82-
  end
83-
84-
  # Fill j_vec with the values belonging to the respective subscript indices.
85-
  for j in 1:n_z
86-
    j_vec[i, 1, j] = cart_prod_sets[j][subscript_values[j]]
87-
  end
88-
89-
  # Calculate the eigenvalue of the Laplace operator corresponding to the vector j_vec[i, 1, :] - see eq. (9) (right-hand side).
90-
  lambda[i, :] = (pi .* j_vec[i, 1, :] ./ (2 * dropdims(L, dims=tuple(findall(size(L) .== 1)...)))) .^ 2
76+
# Convert the single index i to the equivalent subscript values.
77+
remaining = i - 1
78+
for j in n_z:-1:1
79+
subscript_values[j] = floor(remaining / variants[j]) + 1
80+
remaining = mod(remaining, variants[j])
81+
end
82+
83+
# Fill j_vec with the values belonging to the respective subscript indices.
84+
for j in 1:n_z
85+
j_vec[i, 1, j] = cart_prod_sets[j][subscript_values[j]]
86+
end
87+
88+
# Calculate the eigenvalue of the Laplace operator corresponding to the vector j_vec[i, 1, :] - see eq. (9) (right-hand side).
89+
lambda[i, :] = (pi .* j_vec[i, 1, :] ./ (2 * dropdims(L, dims=tuple(findall(size(L) .== 1)...)))) .^ 2
9190
end
9291

9392
# Reverse j_vec.
9493
j_vec = reverse(j_vec, dims=3)
9594
```
9695
Then, define basis functions phi. This function evaluates ``\phi_{1 : n_x+n_u}`` according to eq. (5).
97-
There are two implementations of the phi function.
98-
### Original implementation
99-
This implementation is the original implementation, which is slow but exactly reproduces the results given in the paper.
100-
```julia
101-
function phi_sampling(x, u)
102-
  # Initialize.
103-
  z = vcat(u, x) # augmented state
104-
  phi = Array{Float64}(undef, n_phi, size(z, 2))
105-
106-
  Threads.@threads for i in axes(z, 2)
107-
    phi_temp = ones(n_phi)
108-
    for k in axes(z, 1)
109-
      phi_temp .= phi_temp .* ((1 ./ (sqrt.(L[:, :, k]))) .* sin.((pi .* j_vec[:, :, k] .* (z[k, i] .+ L[:, :, k])) ./ (2 .* L[:, :, k])))
110-
    end
111-
    phi[:, i] .= phi_temp
112-
  end
113-
  return phi
114-
end
115-
```
116-
### Efficient implementation
117-
After the paper's publication, a much more efficient implementation was found, which, for numerical reasons, produces slightly different results (differences in the range ``1\cdot 10^{-16}`` when called once).
118-
However, these minimal deviations lead to noticeably different results since the function phi is called recursively ``T \cdot N \cdot K_{\mathrm{total}}`` times (= very often).
119-
This is the improved implementation, which is significantly faster but yields slightly different results.
12096
```julia
12197
# Precompute.
12298
L_sqrt_inv = 1 ./ sqrt.(L)
12399
pi_j_over_2L = pi .* j_vec ./ (2 .* L)
124100
function phi_sampling(x, u)
125-
  # Initialize.
126-
  z = vcat(u, x) # augmented state
127-
  phi = ones(n_phi, size(z, 2))
101+
# Initialize.
102+
z = vcat(u, x) # augmented state
103+
phi = ones(n_phi, size(z, 2))
128104

129-
  for k in axes(z, 1)
130-
    phi .= phi .* (L_sqrt_inv[:, :, k] .* sin.(pi_j_over_2L[:, :, k] * (z[k, :] .+ L[:, :, k])'))
131-
  end
105+
for k in axes(z, 1)
106+
phi .= phi .* (L_sqrt_inv[:, :, k] .* sin.(pi_j_over_2L[:, :, k] * (z[k, :] .+ L[:, :, k])'))
107+
end
132108

133-
  return phi
109+
return phi
110+
end
111+
```
112+
Since the optimization cannot deal with multithreading or in-place computations, a less efficient definition of phi is required for the subsequent optimization.
113+
```julia
114+
function phi_opt(x, u)
115+
# Initialize.
116+
z = vcat(u, x) # augmented state
117+
phi = Array{Any}(undef, n_phi, size(z, 2))
118+
119+
for i in axes(z, 2)
120+
phi_temp = ones(n_phi)
121+
for k in axes(z, 1)
122+
phi_temp = phi_temp .* ((1 ./ (sqrt.(L[:, :, k]))) .* sin.((pi .* j_vec[:, :, k] .* (z[k, i] .+ L[:, :, k])) ./ (2 .* L[:, :, k])))
123+
end
124+
phi[:, i] = phi_temp
125+
end
126+
return phi
134127
end
135128
```
136129
## Define prior
@@ -140,13 +133,18 @@ ell_Q = 10 # degrees of freedom
140133
Lambda_Q = 100 * I(n_x) # scale matrix
141134
```
142135
Determine the parameters of the matrix normal prior (with mean matrix ``0``, right covariance matrix ``Q`` (see above), and left covariance matrix ``V``) for ``A``.
143-
``V`` is derived from the GP approximation according to eq. (8b), (11a), and (9).
136+
``V`` is derived from the GP approximation according to eq. (8b), (9).
137+
The spectral density of the anisotropic squared exponential kernel
138+
$k(z, z') = s_f \exp (-\frac{1}{2} (z - z')^\top \Lambda^{-1} (z - z'))$
139+
is
140+
$S(\omega) = s_f (2 \pi)^{\frac{d}{2}} |\Lambda|^{\frac{1}{2}} \exp(-\frac{\omega^\top \Lambda \omega}{2})$;
141+
see eq. (68) in the paper ["Hilbert space methods for reduced-rank Gaussian process regression"](https://doi.org/10.1007/s11222-019-09886-w).
144142
```julia
145-
V_diagonal = Array{Float64}(undef, size(lambda, 1)) # diagonal of V
143+
V_diag = Array{Float64}(undef, size(lambda, 1)) # diagonal of V
146144
for i in axes(lambda, 1)
147-
  V_diagonal[i] = sf^2 * sqrt(opnorm(2 * pi * Diagonal(repeat(l, trunc(Int, n_z / size(l, 1))) .^ 2))) * exp.(-(pi^2 * transpose(sqrt.(lambda[i, :])) * Diagonal(repeat(l, trunc(Int, n_z / size(l, 1))) .^ 2) * sqrt.(lambda[i, :])) / 2)
145+
V_diag[i] = sf * ((2 * pi)^(n_z / 2)) * prod(l) * exp(-0.5 * sum((l .^ 2) .* lambda[i, :]))
148146
end
149-
V = Diagonal(V_diagonal)
147+
V = Diagonal(V_diag)
150148
```
151149
Provide an initial guess for the parameters.
152150
```julia
@@ -207,10 +205,11 @@ y_test = y[:, T+1:end]
207205
## Infer model
208206
Run the particle Gibbs sampler to jointly estimate the model parameters and the latent state trajectory.
209207
```julia
210-
PG_samples = particle_Gibbs(u_training, y_training, K, K_b, k_d, N, phi, Lambda_Q, ell_Q, Q_init, V, A_init, x_init_dist, g, R)
208+
PG_samples = particle_Gibbs(u_training, y_training, K, K_b, k_d, N, phi_sampling, Lambda_Q, ell_Q, Q_init, V, A_init, x_init_dist, g, R)
211209

212210
time_sampling = time() - sampling_timer
213211
```
212+
214213
## Define and solve optimal control problem using Altro
215214
In the following, the optimal control problem is defined and solved using the solver Altro. An example using the solver IPOPT is given in the next section.
216215
With the Altro solver, problems of the following form can be solved
@@ -229,7 +228,7 @@ u_t &\leq u_{\mathrm{max},\ t}.
229228
\end{aligned}
230229
```
231230

232-
(Note that the output constraints imply the measurement function ``y_t^{[k]} = x_{t, 1:n_y}^{[k]}``.)
231+
(Note that the output constraints imply the measurement function ``y_t^{[k]} = x_{t, 1:n_y}^{[k]}``)
233232
```julia
234233
# Horizon
235234
H = 41
@@ -388,7 +387,7 @@ Ipopt_options = Dict("max_iter" => 10000, "tol" => 1e-8, "hsllib" => HSL_jll.lib
388387
Solve the optimal control problem using the solver IPOPT. In this case, no formal guarantees for the constraint satisfaction can be derived since Assumption 1 is not satisfied as the employed basis functions cannot represent the actual dynamics with arbitrary precision.
389388
Since the cost function depends only on the control inputs ``u_{0:H}``, the optional argument `J_u` is set to `true`.
390389
```julia
391-
u_opt, x_opt, y_opt, J_opt, solve_successful, iterations, mu = solve_PG_OCP_Ipopt(PG_samples, phi, g, R, H, cost_function, bounded_output, bounded_input; J_u=true, K_pre_solve=20, solver_opts=Ipopt_options)
390+
u_opt, x_opt, y_opt, J_opt, solve_successful, iterations, mu = solve_PG_OCP_Ipopt(PG_samples, phi_opt, g, R, H, cost_function, bounded_output, bounded_input; J_u=true, K_pre_solve=20, solver_opts=Ipopt_options)
392391
```
393392
Finally, apply the input trajectory to the actual system and plot the output trajectories.
394393
```julia

Julia/docs/src/index.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**PGopt** is a software for determining optimal input trajectories with probabilistic performance and constraint satisfaction guarantees for unknown systems with latent states based on input-output measurements. In order to quantify uncertainties, which is crucial for deriving formal guarantees, a Bayesian approach is employed and a prior over the unknown dynamics and the system trajectory is formulated in state-space representation. Since for practical applicability, the prior must be updated based on input-output measurements, but the corresponding posterior distribution is analytically intractable, particle Gibbs (PG) sampling is utilized to draw samples from this distribution. Based on these samples, a scenario optimal control problem (OCP) is formulated and probabilistic performance and constraint satisfaction guarantees are inferred via a greedy constraint removal.
44

5-
The approach is explained in the paper "Learning-Based Optimal Control with Performance Guarantees for Unknown Systems with Latent States", available as a preprint on [arXiv](https://arxiv.org/abs/2303.17963).
5+
The approach is explained in the paper "Learning-Based Optimal Control with Performance Guarantees for Unknown Systems with Latent States", available on [IEEExplore](https://doi.org/10.23919/ECC64448.2024.10590972) and as a preprint on [arXiv](https://arxiv.org/abs/2303.17963).
66

77
## Versions
88
This document describes the Julia implementation of `PGopt`. In this version, the solvers [Altro](https://github.com/RoboticExplorationLab/Altro.jl) and [IPOPT](https://coin-or.github.io/Ipopt/) can be employed to solve the optimal control problem (OCP).
@@ -13,6 +13,20 @@ The solver IPOPT is more general than Altro, and this implementation allows arbi
1313

1414
Besides the Julia implementation, there is also a MATLAB implementation that utilizes [CasADi](https://web.casadi.org/) and [IPOPT](https://coin-or.github.io/Ipopt/). Further information can be found [here](https://github.com/TUM-ITR/PGopt/tree/main/MATLAB).
1515

16+
## Erratum: Hyperparameters in Section V-C
17+
It has come to our attention that the hyperparameters reported in the example with generic basis functions in Section V-C of the paper were unclear due to a notational inconsistency in the kernel representation. While the theoretical conclusions of the paper remain unaffected, the code for the example with generic basis functions has been updated for consistency with the general literature in version **v0.2.2** and later.
18+
19+
### Corrected Kernel Definition
20+
The squared exponential kernel is now defined as
21+
$k(z, z') = s_f \exp (-\frac{1}{2} (z - z')^\top \Lambda^{-1} (z - z'))$,
22+
where $\Lambda = l^2 I$, and $I$ is the identity matrix.
23+
24+
This updated definition ensures consistency with the general literature. Using this definition, the hyperparameters for the example in Section V-C (Table III) of the paper are:
25+
* $l = 2\pi \approx 6.28$
26+
* $s_f = \frac{100^2}{8\pi^4} \approx 12.83$
27+
28+
The updated definition may result in small numerical differences (e.g., due to rounding or recalculation) compared to the original results presented in the paper. These differences are minor and do not impact the overall findings or conclusions.
29+
1630
## Installation
1731
`PGopt` can be installed using the Julia package manager. Start a Pkg REPL (press `]` in a Julia REPL), and install `PGopt` via
1832
```

0 commit comments

Comments
 (0)