Skip to content

Commit 47174b8

Browse files
authored
Hopf support (#29)
* implicit Jacobian removes gauge DoF * substitute for HarmonicVariable fixed * duplicites in Jacobians removed * Symbolics overload fixes * Hopf support completed * Jacobian keywords unified * save/load J fixed
1 parent 08389b1 commit 47174b8

File tree

14 files changed

+126
-84
lines changed

14 files changed

+126
-84
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "HarmonicBalance"
22
uuid = "e13b9ff6-59c3-11ec-14b1-f3d2cc6c135e"
33
authors = ["Jan Kosata <kosataj@phys.ethz.ch>", "Javier del Pino <jdelpino@phys.ethz.ch>"]
4-
version = "0.5.0"
4+
version = "0.5.1"
55

66
[deps]
77
BijectiveHilbert = "91e7fc40-53cd-4118-bd19-d7fcd1de2a54"

src/HarmonicEquation.jl

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export get_independent_variables
22
export get_harmonic_equations
33
export is_rearranged
44
export slow_flow, slow_flow!
5-
export _equations_without_brackets
5+
export _remove_brackets
66

77
show(eom::HarmonicEquation) = show_fields(eom)
88

@@ -105,7 +105,9 @@ Check if `eom` is rearranged to the standard form, such that the derivatives of
105105
"""
106106
function is_rearranged(eom::HarmonicEquation)
107107
tvar = get_independent_variables(eom)[1]
108-
isequal(getfield.(eom.equations, :rhs), d(get_variables(eom), tvar))
108+
109+
# Hopf-containing equations are arranged by construstion (impossible to check)
110+
isequal(getfield.(eom.equations, :rhs), d(get_variables(eom), tvar)) || in("Hopf", getfield.(eom.variables, :type))
109111
end
110112

111113

@@ -142,10 +144,11 @@ end
142144
###
143145

144146
"Apply `rules` to both `equations` and `variables` field of `eom`"
145-
function substitute_all!(eom::HarmonicEquation, rules::Dict)
146-
eom.equations = replace(eom.equations, rules)
147-
eom.variables = substitute_all(eom.variables, rules)
148-
end
147+
function substitute_all(eom::HarmonicEquation, rules::Union{Dict, Pair})
148+
new_eom = deepcopy(eom)
149+
new_eom.equations = expand_derivatives.(substitute_all(eom.equations, rules))
150+
new_eom
151+
end
149152

150153

151154
"Simplify the equations in HarmonicEquation."
@@ -264,9 +267,14 @@ _set_zero_rhs(eq::Equation) = eq.lhs - eq.rhs ~ 0
264267
_set_zero_rhs(eqs::Vector{Equation}) = [_set_zero_rhs(eq) for eq in eqs]
265268

266269

270+
_remove_brackets(var::Num) = declare_variable(var_name(var))
271+
_remove_brackets(vars::Vector{Num}) = _remove_brackets.(vars)
272+
_remove_brackets(hv::HarmonicVariable) = _remove_brackets(hv.symbol)
273+
274+
267275
"Returns the equation system in `eom`, dropping all argument brackets (i.e., u(T) becomes u)."
268-
function _equations_without_brackets(eom::HarmonicEquation)
269-
variable_rules = [var => declare_variable(var_name(var)) for var in get_variables(eom)]
276+
function _remove_brackets(eom::HarmonicEquation)
277+
variable_rules = [var => _remove_brackets(var) for var in get_variables(eom)]
270278
equations_lhs = Num.(getfield.(eom.equations, :lhs) - getfield.(eom.equations, :rhs))
271279
substitute_all(equations_lhs, variable_rules)
272280
end

src/HarmonicVariable.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import Symbolics.get_variables; export get_variables
1+
import Symbolics: get_variables; export get_variables
2+
import Base.isequal; export isequal
23

34
# pretty-printing
45
display(var::HarmonicVariable) = display(var.name)
@@ -27,6 +28,10 @@ end
2728
# Functions for variable substutions and manipulation of HarmonicVariable
2829
###
2930

31+
32+
# when HV is used for substitute, substitute its symbol
33+
substitute_all(eq::Union{Num, Equation}, rules::Dict{HarmonicVariable}) = substitute(eq, Dict(zip(getfield.(keys(rules), :symbol), values(rules))))
34+
3035
function substitute_all(var::HarmonicVariable, rules)
3136
sym, freq = var.symbol, var.ω
3237
HarmonicVariable(substitute_all(sym, rules), var.name, var.type, substitute_all(freq, rules), var.natural_variable)
@@ -41,6 +46,8 @@ get_variables(vars::Vector{Num}) = unique(flatten([Num.(get_variables(x)) for x
4146

4247
get_variables(var::HarmonicVariable) = Num.(get_variables(var.symbol))
4348

49+
isequal(v1::HarmonicVariable, v2::HarmonicVariable) = isequal(v1.symbol, v2.symbol)
50+
4451

4552

4653

src/modules/HC_wrapper/homotopy_interface.jl

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using LinearAlgebra
12
import HomotopyContinuation: Variable
23
import HarmonicBalance: Problem; export Problem
34
export Num_to_Variable
@@ -36,19 +37,21 @@ declare_variable(x::Num) = declare_variable(string(x))
3637

3738
"Constructor for the type `Problem` (to be solved by HomotopyContinuation)
3839
from a `HarmonicEquation`."
39-
function Problem(eom::HarmonicEquation; explicit_Jacobian=true)
40+
function Problem(eom::HarmonicEquation; Jacobian=true)
4041

4142
S = System(eom)
4243
# use the rearranged system for the proper definition of the Jacobian
4344
# this possibly has variables in the denominator and cannot be used for solving
44-
if explicit_Jacobian == true
45+
if Jacobian == true || Jacobian == "explicit"
4546
J = HarmonicBalance.get_Jacobian(eom)
46-
elseif explicit_Jacobian isa Matrix
47-
size(explicit_Jacobian)[1] == size(explicit_Jacobian)[2] == length(get_variables(eom)) || error("The Jacobian shape must match the number of variables!")
48-
J = explicit_Jacobian
47+
elseif Jacobian == "implicit"
48+
# compute the Jacobian implicitly
49+
J = HarmonicBalance.LinearResponse.get_implicit_Jacobian(eom)
50+
elseif Jacobian == "false"
51+
dummy_J(arg) = I(1)
52+
J = dummy_J
4953
else
50-
# ignore the jacobian, is later computed implicitly
51-
J = false
54+
J = Jacobian
5255
end
5356
vars_orig = get_variables(eom)
5457
vars_new = declare_variable.(HarmonicBalance.var_name.(vars_orig))
@@ -69,7 +72,7 @@ end
6972

7073

7174
function System(eom::HarmonicEquation)
72-
eqs = expand_derivatives.(_equations_without_brackets(eom))
75+
eqs = expand_derivatives.(_remove_brackets(eom))
7376
conv_vars = Num_to_Variable.(get_variables(eom))
7477
conv_para = Num_to_Variable.(eom.parameters)
7578
S = HomotopyContinuation.System(parse_equations(eqs),variables=conv_vars,parameters=conv_para)

src/modules/Hopf/Hopf.jl

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ export add_Hopf!
22
export Hopf_variables
33
export replace_Hopf_variable!
44

5+
using HarmonicBalance: is_rearranged, rearrange_standard
6+
using HarmonicBalance: LinearResponse.get_implicit_Jacobian
7+
58

69
"Add a Hopf bifurcation to the system."
710
function add_Hopf!(eom::DifferentialEquation, Δω::Num)
@@ -16,39 +19,56 @@ end
1619
add_Hopf!(eom::DifferentialEquation; frequency::Num, multiplicity::Int) = [add_Hopf!(eom, frequency) for k in 1:multiplicity]
1720

1821

19-
2022
"""
2123
Hopf_variables(eom, freq)
2224
"""
2325
Hopf_variables(eom::HarmonicEquation, freq::Num) = filter(x -> any(isequal.(freq, get_all_terms(x.ω))), eom.variables)
2426

2527

26-
function replace_Hopf_variable!(eom::HarmonicEquation, freq::Num)
27-
pair_to_change = Hopf_variables(eom, freq)[1] # eliminate of the Cartesian variables, it does not matter from which VDP pair
28-
to_eliminate = pair_to_change.symbols[2]
29-
rules = Dict(freq => HarmonicBalance.declare_variable(string(freq), first(get_independent_variables(eom))), to_eliminate => Num(0))
30-
eom.equations = expand_derivatives.(substitute_all(eom.equations, rules))
31-
eom.parameters = setdiff(eom.parameters, [freq]) # freq is now NOT a parameter
32-
eom.variables = setdiff(eom.variables, [pair_to_change])
33-
new_variable = HarmonicBalance.declare_variable(string(freq))
34-
eom.variables = cat(eom.variables, _VDP_Hopf(pair_to_change, to_eliminate, new_variable), dims=1)
35-
end
28+
"""
29+
Obtain the Jacobian of `eom` with a free (Hopf) variable `free_var`.
30+
`free_var` marks the variable which is free due to U(1) symmetry. Its entry in the Jacobian matrix must be discarded
31+
(the discarded eigenvalue is 0, corresponding to a free phase.)
32+
"""
33+
function _Hopf_Jacobian(eom::HarmonicEquation, free_var::HarmonicVariable)
34+
eom_Jac = rearrange_standard(eom)
35+
free_idx = findall(x -> isequal(x, free_var), eom_Jac.variables)
36+
deleteat!(eom_Jac.equations, free_idx)
37+
deleteat!(eom_Jac.variables, free_idx)
3638

37-
function replace_Hopf_variable(eom::HarmonicEquation, freq::Num)
38-
new_eom = deepcopy(eom)
39-
replace_Hopf_variable!(new_eom, freq)
40-
new_eom
39+
# the free variable can be fixed to zero, this is also done in the corresponding HarmonicEquation later
40+
eom_Jac = substitute_all(eom_Jac, free_var => 0)
41+
J = get_implicit_Jacobian(eom_Jac)
4142
end
4243

4344

44-
function _VDP_Hopf(old::HarmonicVariable, eliminated::Num, new_sym::Num)
45-
new_VDP = deepcopy(old)
46-
idx = findall(x-> isequal(x, eliminated), old.symbols)[1]
47-
indep = Num(first(arguments(eliminated.val)))
48-
new_VDP.symbols[idx] = HarmonicBalance.declare_variable(string(new_sym), indep)
49-
new_VDP.types[idx] = "Hopf"
50-
new_VDP.names[new_sym] = string(new_sym)
51-
delete!(new_VDP.names, Num(var_name(eliminated)))
52-
return new_VDP
45+
"""
46+
Construct a `Problem` in the case where U(1) symmetry is present
47+
due to having added a Hopf variable with frequency `Hopf_ω`.
48+
"""
49+
function _Hopf_Problem(eom::HarmonicEquation, Hopf_ω::Num)
50+
51+
isempty(Hopf_variables(eom, Hopf_ω)) ? error("No Hopf variables found!") : nothing
52+
!any(isequal.(eom.parameters, Hopf_ω)) ? error(Hopf_ω, " is not a parameter of the harmonic equation!") : nothing
53+
54+
free_var = Hopf_variables(eom, Hopf_ω)[end] # eliminate one of the Cartesian variables, it does not matter which
55+
56+
# get the Hopf Jacobian before altering anything - this is the usual Jacobian but the entries corresponding
57+
# to the free variable are removed
58+
J = _Hopf_Jacobian(eom, free_var)
59+
60+
new_symbol = HarmonicBalance.declare_variable(string(Hopf_ω), first(get_independent_variables(eom)))
61+
rules = Dict(Hopf_ω => new_symbol, free_var.symbol => Num(0))
62+
eom.equations = expand_derivatives.(substitute_all(eom.equations, rules))
63+
eom.parameters = setdiff(eom.parameters, [Hopf_ω]) # Hopf_ω is now NOT a parameter anymore
64+
65+
free_var.type = "Hopf"
66+
free_var.ω = Num(0)
67+
free_var.symbol = new_symbol
68+
free_var.name = var_name(new_symbol)
69+
70+
# define Problem as usual but with the Hopf Jacobian (always computed implicitly)
71+
p = Problem(eom; Jacobian=J)
72+
return p
5373
end
5474

src/modules/LinearResponse/jacobian_spectrum.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ function JacobianSpectrum(res::Result; index::Int, branch::Int)
7676
hvars = res.problem.eom.variables # fetch the vector of HarmonicVariable
7777
λs, vs = eigen(res.jacobian(solution_dict))
7878

79-
solution_dict = get_single_solution(res, index=index, branch=branch)
79+
# convert OrderedDict to Dict - see Symbolics issue #601
80+
solution_dict = Dict(get_single_solution(res, index=index, branch=branch))
8081

8182
# blank JacobianSpectrum for each variable
8283
all_spectra = Dict{Num, JacobianSpectrum}([[nvar, JacobianSpectrum([])] for nvar in getfield.(hvars, :natural_variable)])

src/modules/LinearResponse/perturbations.jl

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,30 @@ export get_Jacobian
44
$(SIGNATURES)
55
66
Obtain the symbolic Jacobian matrix of `eom` (either a `HarmonicEquation` or a `DifferentialEquation`).
7+
This is the linearised left-hand side of F(u) = du/dT.
78
89
"""
910
function get_Jacobian(eom::HarmonicEquation)
1011

11-
lhs = Num.(getfield.(HarmonicBalance.rearrange_standard(eom).equations, :lhs))
12-
13-
old_vars = get_variables(eom)
14-
bracket_rules = [var => HarmonicBalance.declare_variable(var_name(var)) for var in old_vars]
15-
16-
lhs = substitute_all(lhs,bracket_rules)
17-
vars = getindex.(bracket_rules, 2)
18-
J = Matrix{Num}(undef, length(vars), length(vars))
19-
for idx in CartesianIndices(J)
20-
J[idx] = expand_derivatives(d(lhs[idx[1]], vars[idx[2]]))
21-
end
22-
J
12+
rearr = !HarmonicBalance.is_rearranged(eom) ? HarmonicBalance.rearrange_standard(eom) : eom
13+
lhs = _remove_brackets(rearr)
14+
vars = _remove_brackets.(eom.variables)
2315

16+
get_Jacobian(lhs, vars)
17+
2418
end
2519

26-
# Obtain a Jacobian from a `DifferentialEquation` by first converting it into a `HarmonicEquation`.
20+
" Obtain a Jacobian from a `DifferentialEquation` by first converting it into a `HarmonicEquation`. "
2721
function get_Jacobian(diff_eom::DifferentialEquation)
2822
@variables T
2923
harmonic_eq = get_harmonic_equations(diff_eom, slow_time=T, fast_time=first(get_independent_variables(diff_eom)))
3024
get_Jacobian(harmonic_eq)
3125
end
3226

3327

34-
"""Convert a set of equations in variables `vars` into matrix form.
35-
If the equations are linear, this matrix does not depends on the variables."""
36-
function equations_to_matrix(eqs::Vector{Num}, vars::Vector{Num})
37-
length(eqs) != length(vars) ? error("System under- or over-constrained!") : nothing
38-
M = Matrix{Num}(undef, length(eqs), length(eqs))
39-
for idx in CartesianIndices(M)
40-
M[idx] = expand_derivatives(d(eqs[idx[1]], vars[idx[2]]))
41-
end
42-
M
43-
end
44-
45-
# get the Jacobian of a set of equations `eqs` with respect to the variables `vars`
28+
" Get the Jacobian of a set of equations `eqs` with respect to the variables `vars`. "
4629
function get_Jacobian(eqs::Vector{Num}, vars::Vector{Num})
47-
length(eqs) == length(vars) || error("Jacobians are only defined for square matrices!")
30+
length(eqs) == length(vars) || error("Jacobians are only defined for square systems!")
4831
M = Matrix{Num}(undef, length(vars), length(vars))
4932

5033
for idx in CartesianIndices(M)
@@ -55,17 +38,25 @@ end
5538

5639
get_Jacobian(eqs::Vector{Equation}, vars::Vector{Num}) = get_Jacobian(Num.(getfield.(eqs, :lhs) .- getfield.(eqs, :rhs)), vars)
5740

41+
42+
# the Jacobian of some equations again
5843
function get_Jacobian_steady(eom::HarmonicEquation; differential_order=0)
44+
Hopf_vars = first.(getfield.(filter(x -> x.type == "Hopf", eom.variables), :symbol))
45+
Hopf_idx = findall(x -> any(isequal.(x, Hopf_vars)) , get_variables(eom))
46+
nonsingular = filter( x -> x Hopf_idx, 1:length(get_variables(eom)))
47+
5948
vars_simp = Dict([var => HarmonicBalance.declare_variable(var_name(var)) for var in get_variables(eom)])
6049
T = get_independent_variables(eom)[1]
61-
J = get_Jacobian(eom.equations, d(get_variables(eom), T, differential_order))
50+
J = get_Jacobian(eom.equations, d(get_variables(eom), T, differential_order))[nonsingular, nonsingular]
51+
6252
expand_derivatives.(HarmonicBalance.substitute_all(J, vars_simp))
6353
end
6454

6555
# COMPILE THIS!
66-
function get_implicit_Jacobian(eom::HarmonicEquation)
56+
function get_implicit_Jacobian(eom::HarmonicEquation)::Function
6757
M = get_Jacobian_steady(eom, differential_order=0)
6858
Mp = get_Jacobian_steady(eom, differential_order=1)
59+
6960
function J(soln::OrderedDict)
7061
-inv(ComplexF64.(substitute_all(Mp, soln))) * ComplexF64.(substitute_all(M, soln))
7162
end

src/saving.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ end
1616

1717
function save(filename, x::Result)
1818
x_nofunc = deepcopy(x)
19+
1920
# compiled functions cause problems in saving: ignore J now, compile when loading
20-
x_nofunc.jacobian = false
21+
null_J() = 0
22+
x_nofunc.jacobian = null_J
2123
JLD2.save(_jld2_name(filename), Dict("object" => x_nofunc))
2224
end
2325

src/solve_homotopy.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ function get_steady_states(prob::Problem, swept_parameters::ParameterRange, fixe
9898
# extract all the information we need from results
9999
#rounded_solutions = unique_points.(HomotopyContinuation.solutions.(getindex.(raw, 1)); metric = EuclideanNorm(), atol=1E-14, rtol=1E-8)
100100
rounded_solutions = HomotopyContinuation.solutions.(getindex.(raw, 1));
101+
all(isempty.(rounded_solutions)) ? error("No solutions found!") : nothing
101102
solutions = pad_solutions(rounded_solutions)
102103

103104
compiled_J = _compile_Jacobian(prob, swept_parameters, unique_fixed)
@@ -125,10 +126,10 @@ get_steady_states(eom::HarmonicEquation, swept, fixed; kwargs...) = get_steady_s
125126
function _compile_Jacobian(prob::Problem, swept_parameters::ParameterRange, fixed_parameters::ParameterList)
126127
J_variables = cat(prob.variables, collect(keys(swept_parameters)), dims=1)
127128

128-
if prob.jacobian != false
129+
if prob.jacobian isa Matrix
129130
compiled_J = compile_matrix(prob.jacobian, J_variables, fixed_parameters)
130131
else
131-
compiled_J = LinearResponse.get_implicit_Jacobian(prob.eom)
132+
compiled_J = prob.jacobian # leave implicit Jacobian as is
132133
end
133134
compiled_J
134135
end

src/types.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ end
136136

137137
"""Gives the relation between `var` and the underlying natural variable."""
138138
function _show_ansatz(var::HarmonicVariable)
139-
terms = Dict("u" => "cos", "v" => "sin", "a" => "")
139+
terms = Dict("u" => "cos", "v" => "sin", "a" => "", Hopf => "")
140140
t = var.natural_variable.val.arguments
141141
t = length(t) == 1 ? string(t[1]) : error("more than 1 independent variable")
142142
ω = string(var.ω)
@@ -168,8 +168,9 @@ $(TYPEDFIELDS)
168168
# Constructors
169169
```julia
170170
Problem(eom::HarmonicEquation; Jacobian=true) # find and store the symbolic Jacobian
171-
Problem(eom::HarmonicEquation; Jacobian=false) # ignore the Jacobian for now
172-
Problem(eom::HarmonicEquation; Jacobian::Matrix) # use the given matrix as the Jacobian
171+
Problem(eom::HarmonicEquation; Jacobian="implicit") # ignore the Jacobian for now, compute implicitly later
172+
Problem(eom::HarmonicEquation; Jacobian=J) # use J as the Jacobian (a function that takes a Dict)
173+
Problem(eom::HarmonicEquation; Jacobian=false) # ignore the Jacobian
173174
```
174175
"""
175176
mutable struct Problem
@@ -222,7 +223,7 @@ mutable struct Result
222223
If problem.jacobian is a symbolic matrix, this holds a compiled function.
223224
If problem.jacobian was `false`, this holds a function that rearranges the equations to find J
224225
only after numerical values are inserted (preferable in cases where the symbolic J would be very large)."
225-
jacobian
226+
jacobian::Function
226227

227228
Result(sol,swept, fixed, problem, classes, J) = new(sol, swept, fixed, problem, classes, J)
228229
Result(sol,swept, fixed, problem, classes) = new(sol, swept, fixed, problem, classes)

0 commit comments

Comments
 (0)