|
| 1 | ++++ |
| 2 | +title = "Implement a custom homotopy" |
| 3 | +description = "How to set up customized homotopies" |
| 4 | +weight = 100 |
| 5 | +draft = false |
| 6 | +toc = true |
| 7 | +bref = "How to set up a custom homotopy" |
| 8 | +group = "feature-guide" |
| 9 | ++++ |
| 10 | + |
| 11 | + |
| 12 | +This guide explains how to implement a homotopy of the form |
| 13 | +$$H(x,t).$$ |
| 14 | +In `HomotopyContinuation.jl` a homotopy is a `struct` for which one must implement a few (in-place) functions, namely |
| 15 | + |
| 16 | +```julia |
| 17 | +ModelKit.evaluate! |
| 18 | +ModelKit.evaluate_and_jacobian! |
| 19 | +ModelKit.taylor! |
| 20 | +``` |
| 21 | + |
| 22 | +As the names suggest, these functions |
| 23 | + |
| 24 | +* evaluate $H(x,t)$ at $(x,t)$, |
| 25 | +* evaluate $H(x,t)$ and compute the jacobian at $(x,t)$, and |
| 26 | +* compute the taylor expansion of $H(x,t)$ with respect to $t$ at $(x,t)$. |
| 27 | + |
| 28 | +In fact, $H(x,t)$ does not even need to be a polynomial function, neither in $x$ nor $t$. The algorithm only depends the implementation of these three functions and tracks $t$ from $t=1$ to $t=0$. |
| 29 | + |
| 30 | +<h3 class="section-head" id="homotopies"><a href="#homotopies">Implementing an example homotopy</a></h3> |
| 31 | + |
| 32 | +Let us illustrate how this works in an example. We want to implement a homotopy that, given systems of polynomials $F(x)$ and $G(x)$, is given by |
| 33 | + |
| 34 | +$$H(x,t) = \gamma\cdot \sin(t\cdot \pi/2)\cdot G(x) + \cos(t\cdot \pi/2) \cdot F(x), \quad 0\leq t\leq 1,$$ |
| 35 | + |
| 36 | +where $\gamma \in\mathbb C$. |
| 37 | + |
| 38 | +In this case, we have $H(x,1)=G(x)$ and $H(x,0)=F(x)$. That is, $G(x)$ is our start system and $F(x)$ our target system. |
| 39 | + |
| 40 | +This is similar to a [straight line homotopy](https://www.juliahomotopycontinuation.org/HomotopyContinuation.jl/stable/homotopies/#StraightLineHomotopy) $H(x,t) = \gamma \cdot t \cdot G(x) + (1-t)\cdot F(x)$. |
| 41 | +We copy the [code](https://github.com/JuliaHomotopyContinuation/HomotopyContinuation.jl/blob/main/src/homotopies/straight_line_homotopy.jl) for a `StraightLineHomotopy`, adapt it to our setting and call the result `TrigonometricHomotopy`. |
| 42 | + |
| 43 | +In the following, we explain some of the implemented functions, but not all. The complete code is at the end of this guide. |
| 44 | + |
| 45 | +The `struct` that defines our `TrigonometricHomotopy` could look like this: |
| 46 | + |
| 47 | +```julia |
| 48 | +using HomotopyContinuation |
| 49 | + |
| 50 | +struct TrigonometricHomotopy{S<:AbstractSystem, T<:AbstractSystem} <: AbstractHomotopy |
| 51 | + start::S |
| 52 | + target::T |
| 53 | + γ::ComplexF64 |
| 54 | + half_pi::Float64 |
| 55 | + |
| 56 | + u::Vector{ComplexF64} |
| 57 | + ū::Vector{ComplexF64} |
| 58 | + v̄::Vector{ComplexF64} |
| 59 | + U::Matrix{ComplexF64} |
| 60 | + |
| 61 | + dv_start::TaylorVector{5,ComplexF64} |
| 62 | + dv_target::TaylorVector{5,ComplexF64} |
| 63 | +end |
| 64 | +``` |
| 65 | +Here, `start` and `target` are start and target system, respectively. We also save $\pi/24 in `half_pi` so that we don't need to compute it all the time. The remaining entries are for caching. |
| 66 | + |
| 67 | + |
| 68 | +The `evaluate!` function comes next: |
| 69 | +```julia |
| 70 | +function ModelKit.evaluate!( |
| 71 | + u, |
| 72 | + H::TrigonometricHomotopy, |
| 73 | + x::Vector{ComplexF64}, |
| 74 | + t, |
| 75 | + p = nothing, |
| 76 | +) |
| 77 | + evaluate!(H.v̄, H.start, x) |
| 78 | + evaluate!(H.ū, H.target, x) |
| 79 | + |
| 80 | + t0 = H.half_pi * t |
| 81 | + ts, tt = H.γ .* sin(t0), cos(t0) |
| 82 | + for i = 1:size(H, 1) |
| 83 | + @inbounds u[i] = ts * H.v̄[i] + tt * H.ū[i] |
| 84 | + end |
| 85 | +end |
| 86 | +``` |
| 87 | +Here, `ts` and `tt` are the coefficients at $t$ of $G$ and $F$, respectively. Notice that the coefficient of $G$ is indeed $\gamma \cdot \sin(t \cdot \pi/2)$ and the coefficient of $F$ is $\cos(t \cdot \pi/2)$. |
| 88 | + |
| 89 | +The `evaluate_and_jacobian!` is similar, but includes the Jacobian: |
| 90 | +```julia |
| 91 | +function ModelKit.evaluate_and_jacobian!( |
| 92 | + u, |
| 93 | + U, |
| 94 | + H::TrigonometricHomotopy, |
| 95 | + x::AbstractVector{T}, |
| 96 | + t, |
| 97 | + p = nothing, |
| 98 | +) where {T} |
| 99 | + evaluate_and_jacobian!(u, U, H.start, x) |
| 100 | + evaluate_and_jacobian!(H.u, H.U, H.target, x) |
| 101 | + |
| 102 | + t0 = H.half_pi * t |
| 103 | + ts, tt = H.γ .* sin(t0), cos(t0) |
| 104 | + for i = 1:size(H, 1) |
| 105 | + @inbounds u[i] = ts * u[i] + tt * H.u[i] |
| 106 | + end |
| 107 | + for j = 1:size(H, 2), i = 1:size(H, 1) |
| 108 | + @inbounds U[i, j] = ts * U[i, j] + tt * H.U[i, j] |
| 109 | + end |
| 110 | + nothing |
| 111 | +end |
| 112 | +``` |
| 113 | + |
| 114 | +Finally, we implement `ModelKit.taylor!`: |
| 115 | +```julia |
| 116 | +function ModelKit.taylor!(u, ::Val{1}, H::TrigonometricHomotopy, x, t) |
| 117 | + evaluate!(u, H.start, x) |
| 118 | + evaluate!(H.u, H.target, x) |
| 119 | + |
| 120 | + t0 = H.half_pi * t |
| 121 | + ts, tt = H.γ * cos(t0) * H.half_pi, -sin(t0) * H.half_pi |
| 122 | + for i = 1:size(H, 1) |
| 123 | + @inbounds u[i] = ts * u[i] + tt * H.u[i] |
| 124 | + end |
| 125 | + u |
| 126 | +end |
| 127 | +``` |
| 128 | +Here, the coefficients are given by differentiating $\gamma \sin(t\cdot \pi/2)$ and $\cos(t\cdot \pi/2)$ at $t$. The input `::Val{1}` indicated that this function returns the first derivative with respect to $t$. We also have to implement higher order derivatives (see the complete code at the end of this guide). |
| 129 | + |
| 130 | +<h3 class="section-head" id="homotopies"><a href="#homotopies">Running our custom homotopy</a></h3> |
| 131 | + |
| 132 | +Let us use our homotopy on the following data: |
| 133 | + |
| 134 | +$$G(x) = \begin{pmatrix} x^2 - 1\\\ y^2 - 1\end{pmatrix}, \\quad F(x) = \begin{pmatrix} y^2 + xy + 1\\\ -x^2 + x + 2y - 2\end{pmatrix}$$ |
| 135 | + |
| 136 | +choosing $\gamma\in\mathbb C$ at random. |
| 137 | + |
| 138 | +```julia |
| 139 | +@var x y |
| 140 | +G = System([x^2 - 1; y^2 - 1]) |
| 141 | +F = System([y^2 + x * y + 1; -x^2 + x + 2y - 2]) |
| 142 | + |
| 143 | +γ = exp(rand() * 2 * pi * im) |
| 144 | +H = TrigonometricHomotopy(G, F; γ = γ) |
| 145 | +``` |
| 146 | + |
| 147 | +Now, `H` is a `TrigonometricHomotopy` and we can track the zeros of $G$ from $t=1$ to $t=0$ along this homotopy. |
| 148 | + |
| 149 | +```julia |
| 150 | +julia> starts = [[1,1], [1,-1], [-1,1], [-1,-1]] |
| 151 | +julia> S = solve(H, starts) |
| 152 | + Result with 4 solutions |
| 153 | +======================= |
| 154 | +• 4 paths tracked |
| 155 | +• 4 non-singular solutions (0 real) |
| 156 | +• random_seed: nothing |
| 157 | +``` |
| 158 | + |
| 159 | +The computation was successful and all $4$ zeros of $F$ were computed! |
| 160 | + |
| 161 | + |
| 162 | +<h3 class="section-head" id="homotopies"><a href="#homotopies">Comparing with a straight line homotopy</a></h3> |
| 163 | + |
| 164 | +Let us compare our newly implemented homotopy with a `StraightLineHomotopy`. |
| 165 | +``` |
| 166 | +H_SL = StraightLineHomotopy(G, F; γ = γ) |
| 167 | +``` |
| 168 | + |
| 169 | +We wish to plot every single point tracked along the way. For this, we can use the following code: |
| 170 | + |
| 171 | +``` |
| 172 | +T = Tracker(H) |
| 173 | +T_SL = Tracker(H_SL) |
| 174 | +
|
| 175 | +pts = Vector{ComplexF64}() |
| 176 | +for (x, t) in iterator(T, starts[1], 1.0, 0.0) |
| 177 | + push!(pts, x[1]) |
| 178 | +end |
| 179 | +
|
| 180 | +pts_SL = Vector{ComplexF64}() |
| 181 | +for (x, t) in iterator(T_SL, starts[1], 1.0, 0.0) |
| 182 | + push!(pts_SL, x[1]) |
| 183 | +end |
| 184 | +``` |
| 185 | + |
| 186 | +Now, `pts` contains the list of points tracked along our `TrigonometricHomotopy` when starting at `starts[1] = [1,1]`. Similarly, `pts_SL` contains the points tracked along a `StraightLineHomotopy`. |
| 187 | + |
| 188 | +We plot the result: |
| 189 | + |
| 190 | +``` |
| 191 | +using Plots |
| 192 | +
|
| 193 | +scatter(real(pts), imag(pts), markercolor = :steelblue, markersize = 10, legend = false) |
| 194 | +scatter!(real(pts_SL), imag(pts_SL), markercolor = :indianred, markersize = 10) |
| 195 | +for i in 2:length(pts) |
| 196 | + v = pts[i] - pts[i-1] |
| 197 | + a = pts[i-1:i] + [0.25 .* v; -0.25 .* v] |
| 198 | + plot!(real(a) , imag(a) , linecolor = :black, linewidth = 2, arrow=true, opacity = 0.75) |
| 199 | +end |
| 200 | +plot!() |
| 201 | +``` |
| 202 | + |
| 203 | +<p style="text-align:center;"><img src="/images/tracking.png" width="500px"/></p> |
| 204 | + |
| 205 | +The blue points have been tracked by our `TrigonometricHomotopy` while the red points have been tracked by the `StraightLineHomotopy`. |
| 206 | +As we can see, both homotopies start and arrive at the same point, but the intermediate points are different. |
| 207 | + |
| 208 | + |
| 209 | + |
| 210 | +<h3 class="section-head" id="homotopies"><a href="#homotopies">The complete code for our example</a></h3> |
| 211 | + |
| 212 | + |
| 213 | +```julia |
| 214 | +struct TrigonometricHomotopy{S<:AbstractSystem, T<:AbstractSystem} <: AbstractHomotopy |
| 215 | + start::S |
| 216 | + target::T |
| 217 | + γ::ComplexF64 |
| 218 | + half_pi::Float64 |
| 219 | + |
| 220 | + u::Vector{ComplexF64} |
| 221 | + ū::Vector{ComplexF64} |
| 222 | + v̄::Vector{ComplexF64} |
| 223 | + U::Matrix{ComplexF64} |
| 224 | + |
| 225 | + dv_start::TaylorVector{5,ComplexF64} |
| 226 | + dv_target::TaylorVector{5,ComplexF64} |
| 227 | +end |
| 228 | + |
| 229 | +function TrigonometricHomotopy( |
| 230 | + start::System, |
| 231 | + target::System; |
| 232 | + kwargs..., |
| 233 | +) |
| 234 | + TrigonometricHomotopy( |
| 235 | + fixed(start), |
| 236 | + fixed(target); |
| 237 | + kwargs..., |
| 238 | + ) |
| 239 | +end |
| 240 | +function TrigonometricHomotopy( |
| 241 | + start::AbstractSystem, |
| 242 | + target::AbstractSystem; |
| 243 | + γ = 1.0, |
| 244 | + gamma = γ, |
| 245 | +) |
| 246 | + size(start) == size(target) || throw( |
| 247 | + ArgumentError( |
| 248 | + "Start and target do not have the same size, got $(size(start)) and $(size(target))", |
| 249 | + ), |
| 250 | + ) |
| 251 | + |
| 252 | + m, n = size(start) |
| 253 | + u = zeros(ComplexF64, m) |
| 254 | + ū = zeros(ComplexF64, m) |
| 255 | + v̄ = zeros(ComplexF64, m) |
| 256 | + U = zeros(ComplexF64, m, n) |
| 257 | + half_pi = π / 2 |
| 258 | + |
| 259 | + dv_start = TaylorVector{5}(ComplexF64, m) |
| 260 | + dv_target = TaylorVector{5}(ComplexF64, m) |
| 261 | + |
| 262 | + TrigonometricHomotopy( |
| 263 | + start, |
| 264 | + target, |
| 265 | + ComplexF64(gamma), |
| 266 | + half_pi, |
| 267 | + u, |
| 268 | + ū, |
| 269 | + v̄, |
| 270 | + U, |
| 271 | + dv_start, |
| 272 | + dv_target, |
| 273 | + ) |
| 274 | +end |
| 275 | + |
| 276 | +Base.size(H::TrigonometricHomotopy) = size(H.start) |
| 277 | + |
| 278 | +function Base.show(io::IO, mime::MIME"text/plain", H::TrigonometricHomotopy) |
| 279 | + println(io, typeof(H), ":") |
| 280 | + println(io, "γ: ", H.γ) |
| 281 | + println(io, "\nG: ") |
| 282 | + show(io, mime, H.start) |
| 283 | + println(io, "\n\nF:") |
| 284 | + show(io, mime, H.target) |
| 285 | +end |
| 286 | + |
| 287 | +function ModelKit.evaluate!( |
| 288 | + u, |
| 289 | + H::TrigonometricHomotopy, |
| 290 | + x::Vector{ComplexF64}, |
| 291 | + t, |
| 292 | + p = nothing, |
| 293 | +) |
| 294 | + evaluate!(H.v̄, H.start, x) |
| 295 | + evaluate!(H.ū, H.target, x) |
| 296 | + |
| 297 | + t0 = H.half_pi * t |
| 298 | + ts, tt = H.γ .* sin(t0), cos(t0) |
| 299 | + for i = 1:size(H, 1) |
| 300 | + @inbounds u[i] = ts * H.v̄[i] + tt * H.ū[i] |
| 301 | + end |
| 302 | +end |
| 303 | + |
| 304 | +function ModelKit.evaluate!(u, H::TrigonometricHomotopy, x, t, p = nothing) |
| 305 | + evaluate!(u, H.start, x) |
| 306 | + evaluate!(H.u, H.target, x) |
| 307 | + |
| 308 | + t0 = H.half_pi * t |
| 309 | + ts, tt = H.γ .* sin(t0), cos(t0) |
| 310 | + for i = 1:size(H, 1) |
| 311 | + @inbounds u[i] = ts * u[i] + tt * H.u[i] |
| 312 | + end |
| 313 | + u |
| 314 | +end |
| 315 | + |
| 316 | +function ModelKit.evaluate_and_jacobian!( |
| 317 | + u, |
| 318 | + U, |
| 319 | + H::TrigonometricHomotopy, |
| 320 | + x::AbstractVector{T}, |
| 321 | + t, |
| 322 | + p = nothing, |
| 323 | +) where {T} |
| 324 | + evaluate_and_jacobian!(u, U, H.start, x) |
| 325 | + evaluate_and_jacobian!(H.u, H.U, H.target, x) |
| 326 | + |
| 327 | + t0 = H.half_pi * t |
| 328 | + ts, tt = H.γ .* sin(t0), cos(t0) |
| 329 | + for i = 1:size(H, 1) |
| 330 | + @inbounds u[i] = ts * u[i] + tt * H.u[i] |
| 331 | + end |
| 332 | + for j = 1:size(H, 2), i = 1:size(H, 1) |
| 333 | + @inbounds U[i, j] = ts * U[i, j] + tt * H.U[i, j] |
| 334 | + end |
| 335 | + nothing |
| 336 | +end |
| 337 | + |
| 338 | +function ModelKit.taylor!(u, ::Val{1}, H::TrigonometricHomotopy, x, t) |
| 339 | + evaluate!(u, H.start, x) |
| 340 | + evaluate!(H.u, H.target, x) |
| 341 | + |
| 342 | + t0 = H.half_pi * t |
| 343 | + ts, tt = H.γ * cos(t0) * H.half_pi, -sin(t0) * H.half_pi |
| 344 | + for i = 1:size(H, 1) |
| 345 | + @inbounds u[i] = ts * u[i] + tt * H.u[i] |
| 346 | + end |
| 347 | + u |
| 348 | +end |
| 349 | + |
| 350 | +function ModelKit.taylor!( |
| 351 | + u, |
| 352 | + v::Val{K}, |
| 353 | + H::TrigonometricHomotopy, |
| 354 | + tx::TaylorVector, |
| 355 | + t, |
| 356 | +) where {K} |
| 357 | + taylor!(H.dv_start, v, H.start, tx) |
| 358 | + taylor!(H.dv_target, v, H.target, tx) |
| 359 | + |
| 360 | + t0 = H.half_pi * t |
| 361 | + for i = 1:size(H, 1) |
| 362 | + start = H.γ * (cos(t0) * H.half_pi * H.dv_start[i, K] + sin(t0) * H.dv_start[i, K+1]) |
| 363 | + target = cos(t0) * H.dv_target[i, K+1] - sin(t0) * H.half_pi * H.dv_target[i, K] |
| 364 | + u[i] = start + target |
| 365 | + end |
| 366 | + u |
| 367 | +end |
| 368 | +``` |
0 commit comments