|
| 1 | +# # Outer approximation of Julia set |
| 2 | + |
| 3 | +#md # [](@__BINDER_ROOT_URL__/generated/Systems and Control/julia_set.ipynb) |
| 4 | +#md # [](@__NBVIEWER_ROOT_URL__/generated/Systems and Control/julia_set.ipynb) |
| 5 | +# **Adapted from**: Section 7.1.3 of [KHJ14] |
| 6 | +# |
| 7 | +# [KHJ14] Milan Korda, Didier Henrion, and Colin N. Jones. |
| 8 | +# *Convex computation of the maximum controlled invariant set for polynomial control systems*. |
| 9 | +# SIAM Journal on Control and Optimization 52.5 (2014): 2944-2969. |
| 10 | + |
| 11 | +# The Julia map is defined as follows: |
| 12 | + |
| 13 | +using Test #src |
| 14 | +function julia_map(point, c) |
| 15 | + a, b = point |
| 16 | + return [a^2 - b^2 + real(c), 2a * b + imag(c)] |
| 17 | +end |
| 18 | + |
| 19 | + |
| 20 | +# The *escape radius" is the radius `r` such that `r^2 ≥ r + abs(c)`. |
| 21 | +# Ouside of the circle of that radius, all points diverge so we know the Julia set belongs to that circle. |
| 22 | + |
| 23 | +escape_radius(c) = (1 + √(1 + 4 * abs(c))) / 2 |
| 24 | + |
| 25 | +# To check whether a point is in the Julia set, we can iterate and once the point leaves the circle of escape radius, |
| 26 | +# we consider that it is not in the Julia set, if it stays in the set, we consider that it is in the Julia set. |
| 27 | +# This gives an outer approximation that converges to the Julia set when `m` increases. |
| 28 | + |
| 29 | +using LinearAlgebra |
| 30 | +function in_set(x, c, m=2000) |
| 31 | + r = escape_radius(c) |
| 32 | + for i in 1:m |
| 33 | + if norm(x) > r |
| 34 | + return false |
| 35 | + end |
| 36 | + x = julia_map(x, c) |
| 37 | + end |
| 38 | + return true |
| 39 | +end |
| 40 | + |
| 41 | +# To sort of minimize a level set of a polynomial we minimize integral of that polynomial. |
| 42 | +# We borrow the following from https://doi.org/10.1080/00029890.2001.11919774 |
| 43 | + |
| 44 | +using SpecialFunctions |
| 45 | +using DynamicPolynomials |
| 46 | +β(α) = (α + 1) / 2 |
| 47 | +function circle_integral(mono::AbstractMonomial) |
| 48 | + if any(isodd, exponents(mono)) |
| 49 | + return 0.0 |
| 50 | + else |
| 51 | + return 2 * prod(gamma ∘ β, exponents(mono)) / gamma(sum(β, exponents(mono))) |
| 52 | + end |
| 53 | +end |
| 54 | +function disk_integral(mono::AbstractMonomial, r) |
| 55 | + d = degree(mono) + nvariables(mono) |
| 56 | + return circle_integral(mono) * r^d / d |
| 57 | +end |
| 58 | +function disk_integral(p::AbstractPolynomialLike, r) |
| 59 | + return sum(MultivariatePolynomials.coefficient(t) * disk_integral(monomial(t), r) for t in terms(p)) |
| 60 | +end |
| 61 | + |
| 62 | +# The following function implements [KHJ14, (8)]. |
| 63 | + |
| 64 | +using SumOfSquares |
| 65 | +function outer_approximation(solver, d::Int, c; α = 1/2) |
| 66 | + @polyvar x[1:2] |
| 67 | + model = SOSModel(solver) |
| 68 | + r = escape_radius(c) |
| 69 | + S = @set sum(x.^2) <= r^2 |
| 70 | + @variable(model, v, Poly(monomials(x, 0:2d))) |
| 71 | + @variable(model, w0, SOSPoly(monomials(x, 0:d))) |
| 72 | + @variable(model, w1, SOSPoly(monomials(x, 0:(d - 1)))) |
| 73 | + @constraint(model, α * v(x => julia_map(x, c)) <= v, domain = S) |
| 74 | + w = w0 + w1 * (r^2 - sum(x.^2)) |
| 75 | + @constraint(model, w >= v + 1, domain = S) |
| 76 | + @objective(model, Min, disk_integral(w, r)) |
| 77 | + optimize!(model) |
| 78 | + @test termination_status(model) in [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL] #src |
| 79 | + @test primal_status(model) in [MOI.FEASIBLE_POINT, MOI.NEARLY_FEASIBLE_POINT] #src |
| 80 | + if primal_status(model) == MOI.NO_SOLUTION |
| 81 | + return |
| 82 | + end |
| 83 | + return model |
| 84 | +end |
| 85 | + |
| 86 | +# The following function plots the Julia set with the outer approximation. |
| 87 | + |
| 88 | +using ImplicitPlots |
| 89 | +using Plots |
| 90 | +function julia_plot(poly, c, n=200, m=1000; tol=1e-6, res = 1000) |
| 91 | + r = escape_radius(c) |
| 92 | + p = implicit_plot(poly; xlims=(-r, r) .* 1.1, ylims=(-r, r), resolution = res, label="") |
| 93 | + θ = range(0, stop=2π, length=100) |
| 94 | + points = Vector{Float64}[] |
| 95 | + as = range(-r, r, length=n) |
| 96 | + bs = range(-r, r, length=n) |
| 97 | + for a in as, b in bs |
| 98 | + point = [a, b] |
| 99 | + if in_set(point, c, m) |
| 100 | + push!(points, point) |
| 101 | + end |
| 102 | + end |
| 103 | + xs = [point[1] for point in points] |
| 104 | + ys = [point[2] for point in points] |
| 105 | + scatter!(p, xs, ys, label="", markerstrokewidth=0, markersize=1.5, m=:pixel) |
| 106 | + return p |
| 107 | +end |
| 108 | + |
| 109 | +# We need to pick an SDP solver, see [here](https://jump.dev/JuMP.jl/v1.8/installation/#Supported-solvers) for a list of the available choices. |
| 110 | + |
| 111 | +import CSDP |
| 112 | +solver = optimizer_with_attributes(CSDP.Optimizer, MOI.Silent() => true) |
| 113 | + |
| 114 | +# Let's start with the value of `c` corresponding to the left image of [KHJ14, Figure 3] and with degree 2. |
| 115 | + |
| 116 | +c = -0.7 + 0.2im |
| 117 | +model = outer_approximation(solver, 2, c) |
| 118 | +solution_summary(model) |
| 119 | + |
| 120 | +# We visualize below: |
| 121 | + |
| 122 | +julia_plot(value(model[:v]), c) |
| 123 | + |
| 124 | +# Let's now look at degree 4. |
| 125 | + |
| 126 | +model = outer_approximation(solver, 4, c) |
| 127 | +solution_summary(model) |
| 128 | + |
| 129 | +# We visualize below: |
| 130 | + |
| 131 | +julia_plot(value(model[:v]), c) |
| 132 | + |
| 133 | +# Let's now use the value of `c` corresponding to the right image of [KHJ14, Figure 3] and with degree 2. |
| 134 | + |
| 135 | +c = -0.9 + 0.2im |
| 136 | +model = outer_approximation(solver, 2, c) |
| 137 | +solution_summary(model) |
| 138 | + |
| 139 | +# We visualize below: |
| 140 | + |
| 141 | +julia_plot(value(model[:v]), c) |
| 142 | + |
| 143 | +# Let's now look at degree 4. |
| 144 | + |
| 145 | +model = outer_approximation(solver, 4, c) |
| 146 | +solution_summary(model) |
| 147 | + |
| 148 | +# We visualize below: |
| 149 | + |
| 150 | +julia_plot(value(model[:v]), c) |
0 commit comments