Skip to content

Commit 1ea343e

Browse files
authored
Harmonic variable overhaul (#26)
* HarmonicVariable overhauled to hold a single variable (not u,v) * Jacobian linear response docs * Symbolics 4.4.1 capped
1 parent a4bd550 commit 1ea343e

File tree

14 files changed

+229
-106
lines changed

14 files changed

+229
-106
lines changed

Project.toml

Lines changed: 2 additions & 2 deletions
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 <[email protected]>", "Javier del Pino <[email protected]>"]
4-
version = "0.4.4"
4+
version = "0.5.0"
55

66
[deps]
77
BijectiveHilbert = "91e7fc40-53cd-4118-bd19-d7fcd1de2a54"
@@ -42,7 +42,7 @@ ProgressMeter = "1.7.2"
4242
PyCall = "1.93.0"
4343
PyPlot = "2.10.0"
4444
SymbolicUtils = "0.19.7"
45-
Symbolics = "4.4.1"
45+
Symbolics = "= 4.4.1"
4646
julia = "1.7.0"
4747

4848
[extras]

docs/src/background/stability_response.md

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22

33
The core of the harmonic balance method is expressing the system's behaviour in terms of Fourier components or _harmonics_. For an $N$-coordinate system, we _choose_ a set of $M_i$ harmonics to describe each coordinate $x_i$ :
44
```math
5+
\begin{equation} \label{eq:ansatz}
56
x_i(t) = \sum_{j=1}^{M_i} u_{i,j} (T) \cos(\omega_{i,j} t)+ v_{i,j} (T) \sin(\omega_{i,j} t) \:,
7+
\end{equation}
68
```
7-
This means the system is now described using a discrete set of variables $u_{i,j}$ and $v_{i,j}$. Constructing a vector $\mathbf{u}(T) = (u_{1,1}(T), v_{1,1}(T), \ldots u_{N,M_N}(T), v_{N, M_N}(T))$, we may obtain the _harmonic equations_ (see [an example of this procedure](@ref Duffing_harmeq))
9+
This means the system is now described using a discrete set of variables $u_{i,j}$ and $v_{i,j}$. Constructing the vector
10+
```math
11+
\begin{equation} \label{eq:harmvar}
12+
\mathbf{u}(T) = (u_{1,1}(T), v_{1,1}(T), \ldots u_{N,M_N}(T), v_{N, M_N}(T))\,,
13+
\end{equation}
14+
```
15+
we may obtain the _harmonic equations_ (see [an example of this procedure](@ref Duffing_harmeq))
816
```math
917
\begin{equation} \label{eq:harmeq}
1018
\frac{d\mathbf{u}(T)}{dT} = \bar{\mathbf{F}} (\mathbf{u})
@@ -26,17 +34,84 @@ Eq. \eqref{eq:Jaceq} is exactly solvable for $\delta \mathbf{u}(T)$ given an ini
2634

2735
```math
2836
\begin{equation} \label{eq:fluct_evo}
29-
\delta \mathbf{u}(T) = \sum_{r}(\mathbf{v}_r\cdot \delta\mathbf{u}(T_0))\hspace{1mm}\mathbf{v}_r e^{\lambda_r T}.
37+
\delta \mathbf{u}(T) = \sum_{r} c_r \hspace{1mm}\mathbf{v}_r e^{\lambda_r T}.
3038
\end{equation}
3139
```
3240

3341
The dynamical behaviour near the steady states is thus governed by $e^{ \lambda_r T}$: if $\mathrm{Re}(\lambda_r)<0$ for all $\lambda_r$, the state $\mathbf{u}_0$ is stable. Conversely, if $\mathrm{Re}(\lambda_r)>0$ for at least one $\lambda_r$, the state is unstable - perturbations such as noise or a small applied drive will force the system away from $\mathbf{u}_0$.
3442

3543

36-
### Linear response (WIP)
44+
### Linear response
45+
46+
The response of a stable steady state to an additional oscillatory force, caused by weak probes or noise, is often of interest. It can be calculated by solving for the perturbation $\delta \mathbf{u}(T)$ in the presence of an additional drive term.
47+
48+
```math
49+
\begin{equation} \label{eq:Jacforced}
50+
\frac{d}{dT} \left[\delta \mathbf{u}(T)\right] = J(\mathbf{u}_0) \delta \mathbf{u}(T) + \boldsymbol{\xi} \,e^{i \Omega T}\,,
51+
\end{equation}
52+
```
53+
54+
Suppose we have found an eigenvector of $J(\mathbf{u}_0)$ such that $J(\mathbf{u}) \mathbf{v} = \lambda \mathbf{v}$. To solve Eq. \eqref{eq:Jacforced}, we insert $\delta \mathbf{u}(T) = A(\Omega)\, \mathbf{v} e^{i \Omega T}$. Projecting each side of Eq. \eqref{eq:Jacforced} onto $\mathbf{v}$ gives
55+
56+
```math
57+
A(\Omega) \left( i \Omega - \lambda \right) = \boldsymbol{\xi} \cdot \mathbf{v} \quad \implies \quad A(\Omega) = \frac{\boldsymbol{\xi} \cdot \mathbf{v} }{-\text{Re}[\lambda] + i \left( \Omega - \text{Im}[\lambda] \right)}
58+
```
59+
60+
We see that each eigenvalue $\lambda$ results in a linear response that is a Lorentzian centered at $\Omega = \text{Im}[\lambda]$. Effectively, the linear response matches that of a harmonic oscillator with resonance frequency $\text{Im}[\lambda]$ and damping $\text{Re}[\lambda]$.
61+
62+
Knowing the response of the harmonic variables $\mathbf{u}(T)$, what is the corresponding behaviour of the "natural" variables $x_i(t)$ ? To find this out, we insert the perturbation back into the harmonic ansatz \eqref{eq:ansatz}. Since we require real variables, let us use $\delta \mathbf{u}(T) = A(\Omega) \left( \mathbf{v} \, e^{i \Omega T} + \mathbf{v}^* \, e^{-i \Omega T} \right)$. Plugging this into
63+
```math
64+
\begin{equation}
65+
\delta x_{i}(t) = \sum_{j=1}^{M_i} \delta{u}_{i,j}(t) \cos(\omega_{i,j} \,t) + \delta v_{i,j} (t) \sin(\omega_{i,j} \,t)
66+
\end{equation}
67+
```
68+
and multiplying out the sines and cosines gives
69+
```math
70+
\begin{align} \label{eq:deltax}
71+
\delta x_i(t) = \sum_{j=1}^{M_i} \bigg\{ \left( \text{Re}[\delta u_{i,j}] - \text{Im}[\delta v_{i,j}] \right) \,\cos[(\omega_{i,j} - \Omega) t] \\
72+
+ \left( \text{Im}[\delta u_{i,j}] + \text{Re}[\delta v_{i,j}] \right) \,\sin[(\omega_{i,j} - \Omega) t] \nonumber \\
73+
+ \left( \text{Re}[\delta u_{i,j}] + \text{Im}[\delta v_{i,j}] \right) \,\cos[(\omega_{i,j} + \Omega) t] \nonumber \\
74+
+ \left( -\text{Im}[\delta u_{i,j}] + \text{Re}[\delta v_{i,j}] \right) \,\sin[(\omega_{i,j} + \Omega) t] \bigg\} \nonumber
75+
\end{align}
76+
```
77+
where $\delta u_{i,j}$ and $\delta v_{i,j}$ are the components of $\delta \mathbf{u}$ corresponding to the respective harmonics $\omega_{i,j}$.
78+
79+
We see that a motion of the harmonic variables at frequency $\Omega$ appears as motion of $\delta x_i(t)$ at frequencies $\omega_{i,j}\pm \Omega$.
80+
81+
To make sense of this, we normalize the vector $\delta \mathbf{u}$ and use normalised components $\delta \hat{u}_{i,j}$ and $\delta \hat{v}_{i,j}$. We also define the Lorentzian distribution
82+
```math
83+
\begin{equation}
84+
L(x)_{x_0, \gamma} = \frac{1}{\left( x - x_0 \right)^2 + \gamma^2 }
85+
\end{equation}
86+
```
87+
We see that all components of $\delta x_i(t)$ [Eq. \eqref{eq:deltax}] are proportional to $L(\Omega)_{\text{Im}[\lambda], \text{Re}[\lambda]}$. The first and last two summands are Lorentzians centered at $\pm \Omega$ which oscillate at $\omega_{i,j} \pm \Omega$, respectively. From this, we can extract the linear response function in Fourier space, $\chi (\tilde{\omega})$
88+
89+
```math
90+
\begin{multline}
91+
| \chi [\delta x _i](\tilde{\omega}) |^2 = \sum_{j=1}^{M_i} \bigg\{ \left[ \left( \text{Re}[\delta \hat{u}_{i,j}] - \text{Im}[\delta \hat{v}_{i,j}] \right)^2 + \left( \text{Im}[\delta \hat{u}_{i,j}] + \text{Re}[\delta \hat{v}_{i,j}] \right)^2 \right] L(\omega_{i,j} - \tilde{\omega})_{\text{Im}[\lambda], \text{Re}[\lambda]} \\
92+
+ \left[ \left( \text{Re}[\delta \hat{u}_{i,j}] + \text{Im}[\delta \hat{v}_{i,j}] \right)^2 + \left( \text{Re}[\delta \hat{v}_{i,j}] - \text{Im}[\delta \hat{u}_{i,j}] \right)^2 \right] L(\tilde{\omega} - \omega_{i,j})_{\text{Im}[\lambda], \text{Re}[\lambda]} \bigg\}
93+
\end{multline}
94+
```
95+
Keeping in mind that $L(x)_{x_0, \gamma} = L(x + \Delta)_{x_0 + \Delta, \gamma}$ and the normalization $\delta \hat{u}_{i,j}^2 + \delta \hat{v}_{i,j}^2 = 1$, we can rewrite this as
96+
```math
97+
\begin{equation}
98+
|\chi [\delta x _i](\tilde{\omega})|^2 = \sum_{j=1}^{M_i} \left( 1 + \alpha_{i,j} \right) L(\tilde{\omega})_{\omega_{i,j} - \text{Im}[\lambda], \text{Re}[\lambda]}
99+
+ \left( 1 - \alpha_{i,j} \right) L(\tilde{\omega})_{\omega_{i,j} + \text{Im}[\lambda], \text{Re}[\lambda]}
100+
\end{equation}
101+
```
102+
103+
where
104+
```math
105+
\alpha_{i,j} = 2\left( \text{Im}[\delta \hat{u}_{i,j}] \text{Re}[\delta \hat{v}_{i,j}] - \text{Re}[\delta \hat{u}_{i,j}] \text{Im}[\delta \hat{v}_{i,j}] \right)
106+
```
107+
The above solution applies to every eigenvalue $\lambda$ of the Jacobian. It is now clear that the linear response function $\chi [\delta x _i](\tilde{\omega})$ contains for each eigenvalue $\lambda_r$ and harmonic $\omega_{i,j}$ :
108+
109+
* A Lorentzian centered at $\omega_{i,j}-\text{Im}[\lambda_r]$ with amplitude $1 + \alpha_{i,j}^{(r)}$
110+
* A Lorentzian centered at $\omega_{i,j}+\text{Im}[\lambda_r]$ with amplitude $1 - \alpha_{i,j}^{(r)}$
111+
112+
_Sidenote:_ As $J$ a real matrix, there is an eigenvalue $\lambda_r^*$ for each $\lambda_r$. The maximum number of peaks in the linear response is thus equal to the dimensionality of $\mathbf{u}(T)$.
37113

38-
The response of a stable steady state to an additional oscillatory force, caused by weak probes or noise, is often of interest. It can be calculated by solving for the perturbation $\delta \mathbf{u}$ in the presence of an additional drive term.
114+
The linear response of the system in the state $\mathbf{u}_0$ is thus fully specified by the complex eigenvalues and eigenvectors of $J(\mathbf{u}_0)$. In HarmonicBalance.jl, the module [LinearResponse](@ref linresp_man) creates a set of plottable [`Lorentzian`](@ref HarmonicBalance.LinearResponse.Lorentzian) objects to represent this.
39115

40-
The linear response of the system in the state $\mathbf{u}_0$ is thus encoded in the complex eigenvalues and eigenvectors of $J(\mathbf{u}_0)$.
41116

42117
[Check out this example](@ref linresp_ex) of the linear response module of HarmonicBalance.jl

src/HarmonicEquation.jl

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,36 @@ show(eom::HarmonicEquation) = show_fields(eom)
1111
harmonic_ansatz(eom::DifferentialEquation, time::Num; coordinates="Cartesian")
1212
1313
Expand each variable of `diff_eom` using the harmonics assigned to it with `time` as the time variable.
14-
For each harmonic of each variable, an instance of `HarmonicVariable` (describing a pair of variables (u,v)) is automatically created and named.
14+
For each harmonic of each variable, instance(s) of `HarmonicVariable` are automatically created and named.
1515
16-
`coordinates` allows for using different coordinate systems (e.g. 'polars') - CURRENTLY INACTIVE
1716
"""
18-
harmonic_ansatz(eom::DifferentialEquation, time::Num; coordinates="Cartesian") = harmonic_ansatz(eom, time, Dict([[var, coordinates] for var in get_variables(eom)]))
19-
20-
21-
function harmonic_ansatz(diff_eom::DifferentialEquation, time::Num, trafo_types::Dict{Num,String})
17+
function harmonic_ansatz(diff_eom::DifferentialEquation, time::Num)
2218
!is_harmonic(diff_eom, time) && error("The differential equation is not harmonic in ", time, " !")
23-
old_eqs = [diff_eom.equations[var] for var in get_variables(diff_eom)]
24-
eqs, vars = deepcopy(old_eqs), []
25-
rules = Dict()
26-
27-
idx_counter = 1 # keep count to label new variables
28-
for var in get_variables(diff_eom)
29-
to_substitute = 0 # holds all the subtitution rules
30-
for ω in diff_eom.harmonics[var] # 2 new variables are created for each ω of each variable
31-
these_names = coordinate_names[trafo_types[var]] # stores two strings denoting the variables as either "a", "ϕ", "u", "v"
32-
unique_symbols = [ these_names[1] * string(idx_counter) , these_names[2] * string(idx_counter) ]
33-
repl_rule, new_vars = _rotate_variable(var, ω, time, trafo_types[var], new_symbols=unique_symbols) # create the new variables
34-
to_substitute += repl_rule
35-
append!(vars, [new_vars])
36-
idx_counter += 1
19+
eqs = collect(values(diff_eom.equations))
20+
rules, vars = Dict(), []
21+
22+
# keep count to label new variables
23+
uv_idx = 1
24+
a_idx = 1
25+
26+
for nvar in get_variables(diff_eom) # sum over natural variables
27+
to_substitute = 0 # combine all the subtitution rules for var
28+
for ω in diff_eom.harmonics[nvar]
29+
if !isequal(ω, 0) # nonzero harmonic - create u,v
30+
rule_u, hvar_u = _create_harmonic_variable(nvar, ω, time, "u", new_symbol="u"*string(uv_idx))
31+
rule_v, hvar_v = _create_harmonic_variable(nvar, ω, time, "v", new_symbol="v"*string(uv_idx))
32+
rule = rule_u + rule_v
33+
uv_idx += 1
34+
append!(vars, [hvar_u, hvar_v])
35+
else # zero harmonic - create a
36+
rule, hvar = _create_harmonic_variable(nvar, ω, time, "a", new_symbol="a"*string(a_idx))
37+
a_idx += 1
38+
append!(vars, [hvar])
39+
end
40+
to_substitute += rule
3741
end
38-
rules[var] = to_substitute
42+
43+
rules[nvar] = to_substitute # total sub rule for nvar
3944
end
4045
eqs = substitute_all(eqs, rules)
4146
HarmonicEquation(eqs, Vector{HarmonicVariable}(vars), diff_eom)
@@ -155,15 +160,16 @@ $(TYPEDSIGNATURES)
155160
Return the independent variables (typically time) of `eom`.
156161
"""
157162
function get_independent_variables(eom::HarmonicEquation)::Vector{Num}
158-
dynamic_vars = flatten(getfield.(eom.variables, Symbol("symbols")))
163+
dynamic_vars = flatten(getfield.(eom.variables, Symbol("symbol")))
159164
return flatten(unique([SymbolicUtils.arguments(var.val) for var in dynamic_vars]))
160165
end
161166

162167

163168
"""
164169
$(TYPEDSIGNATURES)
165170
Extract the Fourier components of `eom` corresponding to the harmonics specified in `eom.variables`.
166-
For each harmonic of each variable, 2 equations are generated (cos and sin Fourier coefficients).
171+
For each non-zero harmonic of each variable, 2 equations are generated (cos and sin Fourier coefficients).
172+
For each zero (constant) harmonic, 1 equation is generated
167173
`time` does not appear in the resulting equations anymore.
168174
169175
Underlying assumption: all time-dependences are harmonic.
@@ -176,20 +182,20 @@ end
176182

177183

178184
function fourier_transform!(eom::HarmonicEquation, time::Num)
179-
avg_eqs = Vector{Equation}(undef, 2*length(eom.variables))
180-
181-
natural_variables = flatten([[x,x] for x in getfield.(eom.variables, :natural_variable)])
182-
equation_assignments = flatten([findall(x -> isequal(x, var), collect(keys(eom.natural_equation.equations))) for var in natural_variables])
183-
184-
for i in 1:length(avg_eqs)
185-
j = Int(ceil(i/2))
186-
ω = eom.variables[j].ω
187-
eq = eom.equations[equation_assignments[i]]
188-
189-
if isodd(i)
190-
avg_eqs[i] = fourier_cos_term(eq, ω, time)
191-
else
192-
avg_eqs[i] = fourier_sin_term(eq, ω, time)
185+
avg_eqs = Vector{Equation}(undef, length(eom.variables))
186+
187+
# loop over the HarmonicVariables, each generates one equation
188+
for (i, hvar) in enumerate(eom.variables)
189+
# find the equation belonging to this variable
190+
eq_idx = findall(x -> isequal(x, hvar.natural_variable), collect(keys(eom.natural_equation.equations)))
191+
eq = eom.equations[eq_idx...]
192+
# "type" is usually "u" or "v" (harmonic) or ["a"] (zero-harmonic)
193+
if hvar.type == "u"
194+
avg_eqs[i] = fourier_cos_term(eq, hvar.ω, time)
195+
elseif hvar.type == "v"
196+
avg_eqs[i] = fourier_sin_term(eq, hvar.ω, time)
197+
elseif hvar.type == "a"
198+
avg_eqs[i] = fourier_cos_term(eq, 0, time) # pick out the constants
193199
end
194200
end
195201

src/HarmonicVariable.jl

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,35 @@
11
import Symbolics.get_variables; export get_variables
22

33
# pretty-printing
4-
display(vars::HarmonicVariable) = display(vars.names)
5-
display(vars::Vector{HarmonicVariable}) = display.(getfield.(vars, Symbol("names")))
4+
display(var::HarmonicVariable) = display(var.name)
5+
display(var::Vector{HarmonicVariable}) = display.(getfield.(var, Symbol("name")))
66

77

8-
# Each keyword gives two names for the new variables
9-
coordinate_names = Dict([["Cartesian", ["u", "v"]], ["polar", ["a", "ϕ"]]])
10-
11-
12-
function _coordinate_transform(new_vars, ω, t)
13-
Dict([["Cartesian", new_vars[1] * cos*t) + new_vars[2] * sin*t)],
14-
["polar", new_vars[1] * cos*t + new_vars[2])]])
8+
function _coordinate_transform(new_var, ω, t, type)
9+
coords = Dict([
10+
"u" => new_var * cos*t),
11+
"v" => new_var * sin*t),
12+
"a" => new_var])
13+
return coords[type]
1514
end
1615

1716

18-
"Generates two variables describing the harmonic ω of a natural variable."
19-
function _rotate_variable(nat_var::Num, ω::Num, time::Num, transform::String; new_symbols::Vector{String})
20-
21-
types = coordinate_names[transform] # is the transformation "Cartesian" or "polar"
22-
new_vars = [declare_variable(s, time) for s in new_symbols] # this holds the internal symbols
23-
24-
var_names_strings = [VDP_name * "_{" * var_name(nat_var) * "," * Base.replace(string(ω),"*"=>"") * "}" for VDP_name in types]
25-
var_names = Dict(zip([Num(s) for s in new_symbols], var_names_strings))
26-
repl_rule = _coordinate_transform(new_vars, ω, time)[transform] # a dictionary mapping the coordinate type to an expression
27-
VDP = HarmonicVariable(new_vars, var_names, types, ω, nat_var)
28-
29-
repl_rule, VDP
17+
function _create_harmonic_variable(nat_var::Num, ω::Num, t::Num, type::String; new_symbol::String)
18+
new_var = declare_variable(new_symbol, t) # this holds the internal symbol
19+
name = type * "_{" * var_name(nat_var) * "," * Base.replace(string(ω),"*"=>"") * "}"
20+
rule = _coordinate_transform(new_var, ω, t, type) # contribution of this harmonic variable to the natural variable
21+
hvar = HarmonicVariable(new_var, name, type, ω, nat_var)
22+
rule, hvar
3023
end
3124

3225

3326
###
3427
# Functions for variable substutions and manipulation of HarmonicVariable
3528
###
3629

37-
function substitute_all(vars::HarmonicVariable, rules)
38-
sym, freq = vars.symbols, vars.ω
39-
HarmonicVariable(substitute_all(sym, rules), vars.names, vars.types, substitute_all(freq, rules), vars.natural_variable)
30+
function substitute_all(var::HarmonicVariable, rules)
31+
sym, freq = var.symbol, var.ω
32+
HarmonicVariable(substitute_all(sym, rules), var.name, var.type, substitute_all(freq, rules), var.natural_variable)
4033
end
4134

4235

@@ -46,7 +39,7 @@ substitute_all(vars::Vector{HarmonicVariable}, rules) = [substitute_all(var, rul
4639
"Returns the symbols of a `HarmonicVariable`."
4740
get_variables(vars::Vector{Num}) = unique(flatten([Num.(get_variables(x)) for x in vars]))
4841

49-
get_variables(vars::HarmonicVariable) = unique(get_variables(vars.symbols))
42+
get_variables(var::HarmonicVariable) = Num.(get_variables(var.symbol))
5043

5144

5245

src/Symbolics_utils.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ function _fourier_term(x, ω, t, f)
241241
term = x * f* t)
242242
term = trig_reduce(term)
243243
indep = get_independent(term, t)
244-
2*Num(simplify_complex(Symbolics.expand(indep)))
244+
ft = Num(simplify_complex(Symbolics.expand(indep)))
245+
!isequal(ω, 0) ? 2*ft : ft # extra factor in case ω = 0 !
245246
end
246247

247248

src/modules/LinearResponse.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module LinearResponse
1414
import Base: *, show; export *, show
1515

1616
include("LinearResponse/types.jl")
17+
include("LinearResponse/utils.jl")
1718
include("LinearResponse/perturbations.jl")
1819
include("LinearResponse/jacobian_spectrum.jl")
1920
include("LinearResponse/linear_response.jl")

0 commit comments

Comments
 (0)