An similar example is available in a codeocean capsule.
This example considers the linear control system already introduced in the Example 2 of [LTJ20]; see also Example 2 of [LRJ20]: xk+1=xk+uk/2uk+1=u′k
The system is xk+1=Axk+Buk where B=(0,1) and A=[11/200].
[LRJ20] Legat, Benoît, Saša V. Raković, and Raphaël M. Jungers. Piecewise semi-ellipsoidal control invariant sets. IEEE Control Systems Letters 5.3 (2020): 755-760.
[LTJ18] B. Legat, P. Tabuada and R. M. Jungers. Computing controlled invariant sets for hybrid systems with applications to model-predictive control. 6th IFAC Conference on Analysis and Design of Hybrid Systems ADHS (2018).
We need to pick an LP and an SDP solver, see here for a list of available ones.
using SetProg
import GLPK
lp_solver = optimizer_with_attributes(GLPK.Optimizer, MOI.Silent() => true)
import CSDP
sdp_solver = optimizer_with_attributes(CSDP.Optimizer, MOI.Silent() => true)
A = [1.0 0.5]
E = [1.0 0.0]
using Polyhedra
lib = DefaultLibrary{Float64}(lp_solver)
h = HalfSpace([1, 0], 1.0) ∩ HalfSpace([-1, 0], 1) ∩ HalfSpace([0, 1], 1) ∩ HalfSpace([0, -1], 1)
□ = polyhedron(h, lib) # [0, 1]^2
v = convexhull([1.0, 0], [0, 1], [-1, 0], [0, -1])
◇ = polyhedron(v, lib); # polar of [0, 1]^2
This section shows that the maximal control invariant set of this simple control system is polyhedral. Moreover, this polyhedron is obtained after only one fixed point iteration of the standard viability kernel algorithm. We implement the fixed point iteration with the following function:
function fixed_point_iteration(set::Polyhedron)
new_set = set ∩ (A \ (E * set))
removehredundancy!(new_set)
return new_set
end
fixed_point_iteration (generic function with 1 method)
We start with [−1,1]2 and obtain the polytope mci
after one iteration.
mci = fixed_point_iteration(□)
Polyhedron DefaultPolyhedron{Float64, Polyhedra.Intersection{Float64, Vector{Float64}, Int64}, Polyhedra.Hull{Float64, Vector{Float64}, Int64}}: 6-element iterator of HalfSpace{Float64, Vector{Float64}}: HalfSpace([1.0, 0.0], 1.0) HalfSpace([-1.0, 0.0], 1.0) HalfSpace([0.0, 1.0], 1.0) HalfSpace([0.0, -1.0], 1.0) HalfSpace([1.0, 0.5], 1.0) HalfSpace([-0.5, -0.25], 0.5)
One additional iteration gives the same polytope, showing that this polytope is control invariant.
fixed_point_iteration(mci)
Polyhedron DefaultPolyhedron{Float64, Polyhedra.Intersection{Float64, Vector{Float64}, Int64}, Polyhedra.Hull{Float64, Vector{Float64}, Int64}}: 6-element iterator of HalfSpace{Float64, Vector{Float64}}: HalfSpace([1.0, 0.0], 1.0) HalfSpace([-1.0, 0.0], 1.0) HalfSpace([0.0, 1.0], 1.0) HalfSpace([0.0, -1.0], 1.0) HalfSpace([-1.0, -0.5], 1.0) HalfSpace([0.5, 0.25], 0.5)
We plot this polytope mci
in yellow below along with the safe set [−1,1]2 in green.
using Plots
function hexcolor(rgb::UInt32)
r = ((0xff0000 & rgb) >> 16) / 255
g = ((0x00ff00 & rgb) >> 8) / 255
b = ((0x0000ff & rgb) ) / 255
Plots.RGBA(r, g, b)
end # Values taken from http://www.toutes-les-couleurs.com/code-couleur-rvb.php
lichen = hexcolor(0x85c17e)
canard = hexcolor(0x048b9a)
aurore = hexcolor(0xffcb60)
plot(ratio=:equal, tickfont=Plots.font(12))
plot!(□, color=lichen)
plot!(mci, color=aurore)
We also plot their respective polars. Note that the inclusion is reversed in the polar space.
polar_mci = polar(mci)
plot(ratio=:equal, tickfont=Plots.font(12))
plot!(polar_mci, color=aurore)
plot!(◇, color=lichen)
function maximal_invariant(template, heuristic::Function)
solver = if template isa Polytope
lp_solver
else
sdp_solver
end
model = Model(solver)
@variable(model, S, template)
@constraint(model, S ⊆ □)
@constraint(model, A * S ⊆ E * S)
@objective(model, Max, heuristic(volume(S)))
optimize!(model)
@show solve_time(model)
@show termination_status(model)
@show objective_value(model)
return value(S)
end
function primal_plot(set; npoints=256, args...)
plot(ratio=:equal, tickfont=Plots.font(12); args...)
plot!(□, color=lichen)
plot!(mci, color=aurore)
plot!(set, color=canard, npoints=npoints)
end
function polar_plot(set; npoints=256, args...)
plot(ratio=:equal, tickfont=Plots.font(12); args...)
plot!(SetProg.Sets.polar(set), color=canard, npoints=npoints)
plot!(polar_mci, color=aurore)
plot!(◇, color=lichen)
end
polar_plot (generic function with 1 method)
As introduced in [R21], given a fixed polyhedral conic partition of the state-space, we can search over all polyhedra for which the gauge or support function is linear over each piece of the partition. The partition can be defined by a polytope as described in [Eq. (4.6), R21]. In this example, because the sets are represented with the support function, the pieces will be used for the support function/polar set and the pieces of the gauge function/primal will depend on the value of the support function on each piece.
[R21] Raković, S. V. Control Minkowski–Lyapunov functions Automatica, Elsevier BV, 2021, 128, 109598
Let's start with setting the partition using the cross polytope ◇
.
sol_polytope_◇ = maximal_invariant(Polytope(symmetric=true, piecewise=◇), L1_heuristic)
SetProg.Sets.print_support_function(sol_polytope_◇)
solve_time(model) = 5.4836273193359375e-5 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 1.8856180831641272 h(S, x) = x[1] if x[2] ≥ 0, x[1] ≥ 0 x[1] if -x[2] ≥ 0, x[1] ≥ 0 -x[1] if -x[1] ≥ 0, x[2] ≥ 0 -x[1] if -x[1] ≥ 0, -x[2] ≥ 0
We can see that the polar set is unbounded so the primal set has zero volume. More precisely, the polar is [−1,1]×[−∞,∞] and the primal set is [−1,1]×{0}.
Let's now try setting the partition using the square □
.
sol_polytope_□ = maximal_invariant(Polytope(symmetric=true, piecewise=□), L1_heuristic)
SetProg.Sets.print_support_function(sol_polytope_□)
solve_time(model) = 4.601478576660156e-5 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 2.6666666666666665 h(S, x) = x[1] if -x[2] + x[1] ≥ 0, x[2] + x[1] ≥ 0 -x[1] if -x[2] - x[1] ≥ 0, x[2] - x[1] ≥ 0 x[2] if x[2] - x[1] ≥ 0, x[2] + x[1] ≥ 0 -x[2] if -x[2] - x[1] ≥ 0, -x[2] + x[1] ≥ 0
The primal plot is below:
primal_plot(sol_polytope_□, xlim=(-1.05, 1.05), ylim=(-1.05, 1.05))
And the polar plot is below:
polar_plot(sol_polytope_□, xlim=(-1.05, 1.05), ylim=(-1.05, 1.05))
Let's now use a partition with 8 pieces.
pieces8 = convexhull(□, 1.25 * ◇)
sol_polytope_8 = maximal_invariant(Polytope(symmetric=true, piecewise=pieces8), L1_heuristic)
SetProg.Sets.print_support_function(sol_polytope_8)
solve_time(model) = 0.0001270771026611328 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 2.4636787533634337 h(S, x) = x[1] if 0.4*x[2] ≥ 0, -0.6*x[2] + 0.6*x[1] ≥ 0 -x[2] + x[1] if -0.4*x[2] ≥ 0, 0.6*x[2] + 0.6*x[1] ≥ 0 x[2] if 0.6*x[2] - 0.6*x[1] ≥ 0, 0.4*x[1] ≥ 0 x[2] - x[1] if -0.4*x[1] ≥ 0, 0.6*x[2] + 0.6*x[1] ≥ 0 x[2] - x[1] if -0.6*x[2] - 0.6*x[1] ≥ 0, x[2] ≥ 0 -x[1] if -x[2] ≥ 0, 0.6*x[2] - 0.6*x[1] ≥ 0 -x[2] + x[1] if -0.6*x[2] - 0.6*x[1] ≥ 0, 0.4*x[1] ≥ 0 -x[2] if -0.6*x[2] + 0.6*x[1] ≥ 0, -0.4*x[1] ≥ 0
The primal plot is below:
primal_plot(sol_polytope_8, xlim=(-1.05, 1.05), ylim=(-1.05, 1.05))
And the polar plot is below:
polar_plot(sol_polytope_8, xlim=(-1.05, 1.05), ylim=(-1.05, 1.05))
We can use the polar of the polytope resulting from the first fixed point iteration to generate a refined conic partition. With this partition, the maximal piecewise semi-ellipsoidal control invariant set matches the maximal control invariant set.
sol_polytope_mci = maximal_invariant(Polytope(symmetric=true, piecewise=polar_mci), L1_heuristic)
SetProg.Sets.print_support_function(sol_polytope_mci)
solve_time(model) = 7.700920104980469e-5 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 3.1095432575413913 h(S, x) = x[1] if x[2] ≥ 0, -x[2] + 0.5*x[1] ≥ 0 -x[2] + x[1] if -x[2] ≥ 0, x[1] ≥ 0 x[2] + 0.5*x[1] if x[2] - 0.5*x[1] ≥ 0, x[1] ≥ 0 x[2] - x[1] if -x[1] ≥ 0, x[2] ≥ 0 -x[1] if -x[2] ≥ 0, x[2] - 0.5*x[1] ≥ 0 -x[2] - 0.5*x[1] if -x[1] ≥ 0, -x[2] + 0.5*x[1] ≥ 0
The primal plot is below:
primal_plot(sol_polytope_mci)
And the polar plot is below:
polar_plot(sol_polytope_mci)
We now compute the maximal ellipsoidal control invariant set. Here we can either maximize its volume (which corresponds to log(det(Q)) or n√det(Q) in the objective function) or minimize the sum of the squares of the length of its semi-axes of the polar (which corresponds to the trace of Q in the objective function).
We can see below that the control invariant set of maximal volume has support function h2(S,x)=x21−12x1x2+x22.
sol_ell_vol = maximal_invariant(Ellipsoid(symmetric=true, dimension=2), nth_root)
SetProg.Sets.print_support_function(sol_ell_vol)
solve_time(model) = 0.00457000732421875 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 0.9682458357350057 h(S, x) = x[2]^2 - 0.5*x[1]*x[2] + x[1]^2
We can see below the ellipsoid in blue along with the maximal control invariant set in yellow and the safe set in green.
primal_plot(sol_ell_vol)
Below is are the corresponding sets in the polar space.
polar_plot(sol_ell_vol)
We can see below that the control invariant set of minimal sum of squares of the length of the semi-axes of the polar has support function h2(S,x)=x21−αx1x2+x22.
L1_heuristic(vol, ones(2))
as objective which means taking the integral over the hyperrectangle with vertex ones(2) = [1, 1]
.
sol_ell_L1 = maximal_invariant(Ellipsoid(symmetric=true, dimension=2), vol -> L1_heuristic(vol, ones(2)))
SetProg.Sets.print_support_function(sol_ell_L1)
solve_time(model) = 0.0017478466033935547 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 0.666666665518142 h(S, x) = x[2]^2 - 1.204768*x[1]*x[2] + x[1]^2
The primal plot is below:
primal_plot(sol_ell_L1)
And the polar plot is below:
polar_plot(sol_ell_L1)
We now study the maximal piecewise semi-ellipsoidal control invariant sets of a given conic partition. The volume is not directly maximized. Instead, for each cone, we compute the sum s of the normalized rays and consider the polytope obtained by intersecting the cone with the halfspace s⊤x≤‖s‖22. We integrate the quadratic form corresponding to this cone, i.e. h2(S,x) over the polytope. The sum of the integrals over each polytope is the objective function we use. This can be seen as the generalization of the sum of the squares of the semi-axes of the polar of the ellipsoid.
Note that the constraint (29) of Program 1 of [LRJ20] is implemented with Proposition 2 of [LRJ20] for all results of this example.
The conic partition are obtained by considering the conic hull of each facets of a given polytope. We first consider the conic partition corresponding to the polar of the safe set [−1,1]2. This gives the four quadrants as cones of the conic partition. The maximal piecewise semi-ellipsoidal control invariant set with this partition has the following support function: h2(S,x)={(x1−x2)2 if x1x2≤0,x21−x1x2/2+x22 if x1x2≥0.
sol_piece_◇ = maximal_invariant(Ellipsoid(symmetric=true, piecewise=◇), L1_heuristic)
SetProg.Sets.print_support_function(sol_piece_◇)
solve_time(model) = 0.005764961242675781 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 3.1666666550367832 h(S, x) = x[2]^2 - 0.5*x[1]*x[2] + x[1]^2 if x[2] ≥ 0, x[1] ≥ 0 x[2]^2 - 2.0*x[1]*x[2] + x[1]^2 if -x[2] ≥ 0, x[1] ≥ 0 x[2]^2 - 2.0*x[1]*x[2] + x[1]^2 if -x[1] ≥ 0, x[2] ≥ 0 x[2]^2 - 0.5*x[1]*x[2] + x[1]^2 if -x[1] ≥ 0, -x[2] ≥ 0
The primal plot is below:
primal_plot(sol_piece_◇, xlim=(-1.05, 1.05), ylim=(-1.05, 1.05))
And the polar plot is below:
polar_plot(sol_piece_◇, xlim=(-1.05, 1.05), ylim=(-1.05, 1.05))
Let's now try setting the partition with the square □
.
sol_piece_□ = maximal_invariant(Ellipsoid(symmetric=true, piecewise=□), L1_heuristic)
SetProg.Sets.print_support_function(sol_piece_□)
solve_time(model) = 0.0057239532470703125 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 2.666666664667396 h(S, x) = x[2]^2 - 1.52044*x[1]*x[2] + x[1]^2 if -x[2] + x[1] ≥ 0, x[2] + x[1] ≥ 0 x[2]^2 - 1.52044*x[1]*x[2] + x[1]^2 if -x[2] - x[1] ≥ 0, x[2] - x[1] ≥ 0 x[2]^2 - 1.52044*x[1]*x[2] + x[1]^2 if x[2] - x[1] ≥ 0, x[2] + x[1] ≥ 0 x[2]^2 - 1.52044*x[1]*x[2] + x[1]^2 if -x[2] - x[1] ≥ 0, -x[2] + x[1] ≥ 0
The primal plot is below:
primal_plot(sol_piece_□, xlim=(-1.05, 1.05), ylim=(-1.05, 1.05))
And the polar plot is below:
polar_plot(sol_piece_□, xlim=(-1.6, 1.6), ylim=(-1.6, 1.6))
Let's now try with the partition of 8 pieces.
sol_piece_8 = maximal_invariant(Ellipsoid(symmetric=true, piecewise=pieces8), L1_heuristic)
SetProg.Sets.print_support_function(sol_piece_8)
solve_time(model) = 0.011782169342041016 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 2.2831717475906466 h(S, x) = 1.849901*x[2]^2 - 0.92495*x[1]*x[2] + x[1]^2 if 0.4*x[2] ≥ 0, -0.6*x[2] + 0.6*x[1] ≥ 0 x[2]^2 - 2.0*x[1]*x[2] + x[1]^2 if -0.4*x[2] ≥ 0, 0.6*x[2] + 0.6*x[1] ≥ 0 x[2]^2 + 0.774852*x[1]*x[2] + 0.150099*x[1]^2 if 0.6*x[2] - 0.6*x[1] ≥ 0, 0.4*x[1] ≥ 0 x[2]^2 - 2.0*x[1]*x[2] + x[1]^2 if -0.4*x[1] ≥ 0, 0.6*x[2] + 0.6*x[1] ≥ 0 x[2]^2 - 2.0*x[1]*x[2] + x[1]^2 if -0.6*x[2] - 0.6*x[1] ≥ 0, x[2] ≥ 0 1.849901*x[2]^2 - 0.92495*x[1]*x[2] + x[1]^2 if -x[2] ≥ 0, 0.6*x[2] - 0.6*x[1] ≥ 0 x[2]^2 - 2.0*x[1]*x[2] + x[1]^2 if -0.6*x[2] - 0.6*x[1] ≥ 0, 0.4*x[1] ≥ 0 x[2]^2 + 0.774852*x[1]*x[2] + 0.150099*x[1]^2 if -0.6*x[2] + 0.6*x[1] ≥ 0, -0.4*x[1] ≥ 0
The primal plot is below:
primal_plot(sol_piece_8, xlim=(-1.05, 1.05), ylim=(-1.05, 1.05))
And the polar plot is below:
polar_plot(sol_piece_8, xlim=(-1.1, 1.1), ylim=(-1.05, 1.05))
We can use the polar of the polytope resulting from the first fixed point iteration to generate a refined conic partition. With this partition, the maximal piecewise semi-ellipsoidal control invariant set matches the maximal control invariant set. The support function is h2(S,x)={(x1−x2)2 if x1x2≤0,x21 if x2(x1−2x2)≥0,(x1/2+x2)2 if x1(2x2−x1)≥0.
sol_piece_mci = maximal_invariant(Ellipsoid(symmetric=true, piecewise=polar_mci), L1_heuristic)
SetProg.Sets.print_support_function(sol_piece_mci)
solve_time(model) = 0.009195089340209961 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 2.990943448940364 h(S, x) = x[1]^2 if x[2] ≥ 0, -x[2] + 0.5*x[1] ≥ 0 x[2]^2 - 2.0*x[1]*x[2] + x[1]^2 if -x[2] ≥ 0, x[1] ≥ 0 x[2]^2 + x[1]*x[2] + 0.25*x[1]^2 if x[2] - 0.5*x[1] ≥ 0, x[1] ≥ 0 x[2]^2 - 2.0*x[1]*x[2] + x[1]^2 if -x[1] ≥ 0, x[2] ≥ 0 x[1]^2 if -x[2] ≥ 0, x[2] - 0.5*x[1] ≥ 0 x[2]^2 + x[1]*x[2] + 0.25*x[1]^2 if -x[1] ≥ 0, -x[2] + 0.5*x[1] ≥ 0
The primal plot is below:
primal_plot(sol_piece_mci)
And the polar plot is below:
polar_plot(sol_piece_mci)
We now use the polyset templates. As details in [LRJ20], for inclusion
constraints of the form A * S ⊆ E * S
, the polar representation is used
so we need the set to be convex. For this, we set the argument convex=true
.
We directly try degree=4
because degree=2
would simply give the same as
sol_ell_L1
.
sol4 = maximal_invariant(PolySet(symmetric=true, degree=4, convex=true), vol -> L1_heuristic(vol, ones(2)))
SetProg.Sets.print_support_function(sol4)
solve_time(model) = 0.0025229454040527344 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 0.39999999806931175 h(S, x) = x[2]^4 - 2.992549*x[1]*x[2]^3 + 6.0*x[1]^2*x[2]^2 - 2.992579*x[1]^3*x[2] + x[1]^4
The primal plot is below:
primal_plot(sol4)
And the polar plot is below:
polar_plot(sol4)
Let's now try degree 6.
sol6 = maximal_invariant(PolySet(symmetric=true, degree=6, convex=true), vol -> L1_heuristic(vol, ones(2)))
SetProg.Sets.print_support_function(sol6)
solve_time(model) = 0.004295825958251953 termination_status(model) = MathOptInterface.OPTIMAL objective_value(model) = 0.2857142833526061 h(S, x) = x[2]^6 - 5.206527*x[1]*x[2]^5 + 15.0*x[1]^2*x[2]^4 - 17.355089*x[1]^3*x[2]^3 + 15.0*x[1]^4*x[2]^2 - 5.206526*x[1]^5*x[2] + x[1]^6
The primal plot is below:
primal_plot(sol6)
And the polar plot is below:
polar_plot(sol6)
This notebook was generated using Literate.jl.