Skip to content

Commit 5149714

Browse files
committed
Generalize to non-Hermitian models
1 parent 77702d5 commit 5149714

18 files changed

Lines changed: 433 additions & 233 deletions

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ hopping ranges, the package:
2222
## Key types and API
2323

2424
### Core pipeline
25-
- `tb_hamiltonian(cbr, Rs; antihermitian)` — top-level entry point; returns `TightBindingModel`
25+
- `tb_hamiltonian(cbr, Rs, Val{hermiticity})` — top-level entry point; returns `TightBindingModel`
2626
- `obtain_symmetry_related_hoppings(Rs, br_a, br_b)` — enumerates hopping orbits
2727
- `sgrep_induced_by_siteir(br, op)` — site-symmetry induced space group representation
2828

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ makedocs(;
2525
"Band symmetry" => "band-symmetry.md",
2626
"Berry curvature & Chern numbers" => "berry.md",
2727
"Symmetry breaking" => "symmetry-breaking.md",
28+
"Nonhermitian models" => "nonhermitian.md",
2829
"API" => "api.md",
2930
"Internal API" => "internal-api.md",
3031
"Theory" => "theory.md",

docs/src/nonhermitian.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# [Non-Hermitian tight-binding models](@id nonhermitian)
2+
3+
By default, the models returned by `tb_hamiltonian` are Hermitian. In addition to Hermitian models, however, it is also possible to return anti-Hermitian or generically non-Hermitian (neither Hermitian or anti-Hermitian) models. Here, we showcase the latter.
4+
5+
## Hatano--Nelson model
6+
7+
The architypical non-Hermitian model is the 1D Hatano--Nelson model, consisting of a single site and nearest-neigbor hoppings, in a setting with only time-reversal symmetry.
8+
It is simple to build this model with SymmetricTightBinding.jl:
9+
10+
```@example hatano-nelson
11+
brs = calc_bandreps(1, 1) # EBRs in plane group 1, with time-reversal symmetry
12+
pin_free!(brs, [1=>[0]]) # the 1a Wyckoff position in plane group 1 has a free parameter: set to 0 for definiteness
13+
cbr = @composite brs[1] # single-site model
14+
tbm = tb_hamiltonian(cbr, [[1]], NONHERMITIAN) # nearest neigbor hoppings
15+
```
16+
17+
The model is very simple: two different hopping terms, corresponding to right- and left-directed hopping terms. The absence of hermiticity allows the hopping amplitudes in either direction to differ, contrasting the Hermitian case:
18+
19+
```@example hatano-nelson
20+
tb_hamiltonian(cbr, [[1]], HERMITIAN)
21+
```
22+
The non-Hermitian model reduces to the Hermitian model when the left- and right-directed hopping amplitudes are equal. When the two are _not_ equal, the Hatano-Nelson model features exceptional points and spontaneous symmetry breaking of the real spectrum, as we can verify by example (using Brillouin.jl and GLMakie.jl for dispersion plotting):
23+
24+
```@example hatano-nelson¨
25+
ptbm = tbm([0.9, 1.1]) # a model with 0.9 hopping amplitude to right, 1.1 to the left
26+
27+
using Brillouin, GLMakie
28+
kp = irrfbz_path(1, directbasis(1, 1)) # a k-path in plane group 1
29+
kpi = interpolate(kp, 500) # interpolated over 500 points
30+
Es = spectrum(ptbm, kpi)
31+
Es_re = sort(real.(Es); dims=2)
32+
Es_im = sort(imag.(Es); dims=2)
33+
34+
using GLMakie
35+
plot(kpi, Es_re, Es_im; color=[:blue, :red])
36+
```
37+
38+
We can also explore the consequences of breaking time-reversal symmetry:
39+
```
40+
brs_notr = calc_bandreps(1, 1; timereversal=false) # EBRs in plane group 1, without time-reversal symmetry
41+
pin_free!(brs_notr, [1=>[0]])
42+
tbm_notr = tb_hamiltonian((@composite brs_notr[1]), [[0], [1]], NONHERMITIAN) # on-site terms _and_ nearest-neighbor hoppings
43+
```
44+
45+
## A more complicated example: exceptional surfaces in p4
46+
47+
We can also construct more complicated examples where symmetry plays a role. Consider for example the following simple extension of the Hatano-Nelson model to a 2D lattice with p4 symmetry:
48+
```
49+
brs = calc_bandreps(10, Val(2))
50+
cbr = @composite brs[1]
51+
tbm_H = tb_hamiltonian(cbr, [[0,0], [1,0]], Val(HERMITIAN))
52+
tbm_NH = tb_hamiltonian(cbr, [[0,0], [1,0]], Val(NONHERMITIAN))
53+
```
54+
55+
It is instructive to visualize both the Hermitian and non-Hermitian models and compare the involved hopping terms:
56+
57+
```
58+
plot(tbm_H)
59+
plot(tbm_NH)
60+
```

docs/src/symmetry-breaking.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ This allows three additional terms. Conversely, we could have also tried to brea
4747
Δtbm_tr = subduced_complement(tbm, 11; timereversal = false) # break TR
4848
```
4949

50-
[^1]: For mirror symmetry-breaking, the absence of new terms is a result of looking only at a limited set of hopping orbits (in the original model `tb_hamiltonian(cbr, [[0,0], [1,0]])`): by including longer-range hopping orbits, we would eventually find new mirror-symmetry-broken terms. This is not so for time-reversal breaking, however: in *p*4mm, mirror symmetry and hermicity jointly impose an effective time-reversal symmetry.
50+
[^1]: For mirror symmetry-breaking, the absence of new terms is a result of looking only at a limited set of hopping orbits (in the original model `tb_hamiltonian(cbr, [[0,0], [1,0]])`): by including longer-range hopping orbits, we would eventually find new mirror-symmetry-broken terms. This is not so for time-reversal breaking, however: in *p*4mm, mirror symmetry and hermiticity jointly impose an effective time-reversal symmetry.
5151

5252
However, by breaking both mirror and time-reversal symmetry simultaneously, additional terms do appear:
5353

examples/sg213.jl

Lines changed: 0 additions & 35 deletions
This file was deleted.

ext/SymmetricTightBindingMakieExt.jl

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,22 @@ using Makie
1010
## --------------------------------------------------------------------------------------- #
1111

1212
const MaybeCoefficient = Union{Nothing, <:AbstractVector{<:Number}}
13-
14-
@recipe(HoppingOrbitPlot, h, Rs, t, offdiag) do Scene
15-
Attributes(;
16-
origins = Attributes(; color = :firebrick2, label = "Origins (a)"),
17-
destinations = Attributes(; color = :royalblue1, label = "Destinations (b+R)"),
18-
markersize = 0.05,
19-
bonds = Attributes(; color = :gray37, linewidth = 2.0, label = "Bonds"),
20-
unitcell = Attributes(; color = :gray65, linewidth = 2.0, label = "Unit cell",
21-
patchcolor = :gray97),
22-
context = Attributes(; include = true, color = :gray85,
23-
linecolor = :gray90, linewidth= 1.25,
24-
limits = nothing),
25-
)
13+
const default_context_attributes = Attributes(;
14+
include = true, color = :gray85, linecolor = :gray90, linewidth = 1.25, limits = nothing
15+
) # TODO: we define this so we can manually merge: remove this hack once Makie v0.25 is out
16+
17+
@recipe HoppingOrbitPlot (h, Rs, t, offdiag) begin
18+
origins = Attributes(; color = :firebrick2, label = "Origins (a)")
19+
destinations = Attributes(; color = :royalblue1, label = "Destinations (b+R)")
20+
markersize = 0.05
21+
bonds = Attributes(; color = :gray37, linewidth = 2.0, label = "Bonds")
22+
unitcell = Attributes(; color = :gray65, linewidth = 2.0, label = "Unit cell",
23+
patchcolor = :gray97)
24+
context = default_context_attributes
25+
# TODO: Broken as-is: caller must give _full_ attributes for any field that is itself
26+
# an `Attributes(...)`: no automatic merging of default attributes with partial
27+
# caller attributes.
28+
# Will apparently be fixed in Makie v0.25+ (cf. ffreyer, Slack).
2629
end
2730

2831
"""
@@ -50,7 +53,9 @@ function Makie.plot!(
5053
h = p.h[] # TODO: actually do the Observables updates; just so tedious...
5154
Rs = p.Rs[]
5255
t = p.t[]
53-
offdiag = p.offdiag[] # implies that (anti-)hermicity requires adding reversed hoppings
56+
offdiag = p.offdiag[] # implies that (anti-)hermiticity requires adding reversed hoppings
57+
context = p.context[]
58+
context = merge(context, default_context_attributes) # TODO: remove manual merging once Makie v0.25+ is out
5459

5560
P, V = Point{D, Float32}, Vec{D, Float32}
5661
Rm = stack(Rs)
@@ -135,7 +140,7 @@ function Makie.plot!(
135140
)
136141

137142
# set square axis limits, centered around unit cell center
138-
bbox = if isnothing(p.context[].limits[])
143+
bbox = if isnothing(context.limits[])
139144
bbox_coords = Makie.GeometryBasics.coordinates(data_limits(p))
140145
cntr = sum(Rs) ./ 2
141146
max_dist = maximum(abs,
@@ -145,11 +150,11 @@ function Makie.plot!(
145150
width = max_dist + pad*0.1
146151
Rect{D, Float32}(cntr-V(width), V(2*width))
147152
else
148-
p.context[].limits[] :: Rect{D, Float32}
153+
context.limits[] :: Rect{D, Float32}
149154
end
150155

151156
# include symmetry-related sites, that we didn't hop to
152-
if p.context[].include[]
157+
if context.include[]
153158
i_lower = Rm \ (bbox.origin)
154159
i_lower = ntuple(d->floor(Int, i_lower[d])-1, Val(D))
155160
i_upper = Rm \ (bbox.origin .+ bbox.widths)
@@ -169,14 +174,14 @@ function Makie.plot!(
169174
D == 3 && continue # doesn't look nice for 3D
170175
unitcell′ = unitcell .+ Ref(R)
171176
any(in(bbox), unitcell′) || continue
172-
lines!(unitcell′; color=p.context[].linecolor, linewidth=0.5, depth_shift=0)
177+
lines!(unitcell′; color=context.linecolor, linewidth=0.5, depth_shift=0)
173178
end
174179
scatter!(
175180
p,
176181
rs,
177182
marker = :circle,
178183
markersize = 11,
179-
color = p.context[].color,
184+
color = context.color,
180185
)
181186
end
182187
limits!(bbox)
@@ -321,7 +326,10 @@ _orbit(h::HoppingOrbit) = h
321326
_coefficients(tbt::TightBindingTerm) = _coefficients(tbt.block)
322327
_coefficients(tbb::TightBindingBlock) = tbb.t
323328
_offdiag(tbt::TightBindingTerm) = _offdiag(tbt.block)
324-
_offdiag(tbb::TightBindingBlock) = !tbb.diagonal_block
329+
function _offdiag(tbb::TightBindingBlock{D, S}) where {D, S}
330+
S === NONHERMITIAN && return false
331+
return !tbb.diagonal_block
332+
end
325333

326334
## --------------------------------------------------------------------------------------- #
327335
# Plotting entire tight-binding models by tiling their hopping terms
@@ -347,8 +355,12 @@ function Makie.convert_arguments(
347355
for idx in LinearIndices(axs)
348356
if idx length(tbm)
349357
tbt = tbm[idx]
350-
plots = [S.HoppingOrbitPlot(_orbit(tbt), Rs, _coefficients(tbt), _offdiag(tbt);
351-
context = (; limits = bbox))]
358+
plots = [
359+
S.HoppingOrbitPlot(
360+
_orbit(tbt), Rs, _coefficients(tbt), _offdiag(tbt);
361+
context = (; limits = bbox)
362+
)
363+
]
352364
else
353365
plots = PlotSpec[]
354366
end
@@ -417,7 +429,8 @@ function layout_limits(hs::AbstractVector{HoppingOrbit{D}}, Rs::DirectBasis{D})
417429

418430
# add points from unit cell
419431
rect = Rect{D, Float32}(P(0), V(1)) # unit cube at origin
420-
sites = P.(Ref(Rm) .* Makie.GeometryBasics.coordinates(rect))
432+
rect_coords = D == 1 ? [P(0), P(1)] : Makie.GeometryBasics.coordinates(rect)
433+
sites = P.(Ref(Rm) .* rect_coords)
421434
filter!(!isnan, sites)
422435
pts = @view sites[1:end] # for later referencing only the unit cell
423436

@@ -437,13 +450,14 @@ function layout_limits(hs::AbstractVector{HoppingOrbit{D}}, Rs::DirectBasis{D})
437450
lower_bound = ntuple(d -> minimum(v -> getindex(v, d), sites), Val(D))
438451
upper_bound = ntuple(d -> maximum(v -> getindex(v, d), sites), Val(D))
439452
data_bbox = Rect{D, Float32}(P(lower_bound), V(upper_bound .- lower_bound))
440-
bbox_coords = Makie.GeometryBasics.coordinates(data_bbox)
453+
bbox_coords = D == 1 ? [P(lower_bound), P(upper_bound)] : Makie.GeometryBasics.coordinates(data_bbox)
441454
cntr = sum(Rs) ./ 2
442455
max_dist = maximum(abs,
443456
ntuple(d->maximum(v->abs(cntr[d]-getindex(v, d)), bbox_coords), Val(D)))
444457
pad = maximum(abs,
445458
ntuple(d -> splat(-)(extrema(v -> getindex(v, d), filter(!isnan, pts))), Val(D)))
446459
width = max_dist + pad*0.1
460+
# TODO: the padding and added width should surely not be the same in all dimensions
447461
return Rect{D, Float32}(cntr-V(width), V(2*width))
448462
end
449463
function layout_limits(tbm::TightBindingModel{D}, Rs::DirectBasis{D}) where D

ext/SymmetricTightBindingOptimExt.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import SymmetricTightBinding: fit
1010
# Define loss as sum of absolute squared error (MSE, up to scaling)
1111

1212
function fg!(
13-
F, G, cs, tbm::TightBindingModel, Em_r, ks;
13+
F, G, cs, tbm::TightBindingModel{D, S}, Em_r, ks;
1414
lasso::Union{Nothing,Real} = nothing
15-
)
15+
) where {D, S}
16+
S HERMITIAN && error("loss function can currently only handle HERMITIAN models")
1617
ptbm = tbm(cs)
1718
if !isnothing(G)
1819
fill!(G, zero(eltype(G)))

src/SymmetricTightBinding.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const ZASSENHAUS_ATOL_DEFAULT = NULLSPACE_ATOL_DEFAULT
2323
include("types.jl")
2424
export HoppingOrbit
2525
export TightBindingBlock
26+
export Hermiticity, HERMITIAN, ANTIHERMITIAN, NONHERMITIAN
27+
export hermiticity
2628
export TightBindingModel
2729
export ParameterizedTightBindingModel
2830
include("show.jl")

src/gradients.jl

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,19 @@ The gradient is returned as column vectors, one for each band, with each column
4848
the gradient of the corresponding energy with respect to the hopping coefficients of `ptbm`.
4949
"""
5050
function energy_gradient_wrt_hopping(
51-
ptbm::ParameterizedTightBindingModel{D},
51+
ptbm::ParameterizedTightBindingModel{D, S},
5252
k::ReciprocalPointLike{D},
5353
(Es, us) = solve(ptbm, k; bloch_phase=Val(false)) # "unperturbed" energies & eigenstates
5454
;
5555
degen_rtol::Float64 = 1e-12,
5656
degen_atol::Float64 = 1e-12
57-
) where D
57+
) where {D, S}
58+
if S === NONHERMITIAN
59+
# TODO: requires left/right version of Feynman-Hellmann theorem + possibly handling
60+
# of defective case
61+
error("energy gradient with respect to hopping is not currently implemented for \
62+
NONHERMITIAN models")
63+
end
5864
Nᶜ = length(ptbm.tbm) # number of hopping terms
5965
Nᵇ = ptbm.tbm.N # number of bands
6066

@@ -82,19 +88,21 @@ function energy_gradient_wrt_hopping(
8288
end
8389

8490
# apply Feynman-Hellmann theorem, either in degenerate or non-degenerate variants
85-
∇ᶜEs = Matrix{Float64}(undef, Nᶜ, Nᵇ)
91+
ResultType = S == HERMITIAN ? Float64 : ComplexF64
92+
∇ᶜEs = Matrix{ResultType}(undef, Nᶜ, Nᵇ)
8693
for i in 1:Nᶜ
8794
∂ᵢH = tbmg(k, i)
8895
for ns in bands
8996
if length(ns) == 1 # non-degenerate band
9097
n = @inbounds ns[1]
9198
uₙ = @view us[:, n]
9299
∂ᵢEₙ = dot(uₙ, ∂ᵢH, uₙ)
93-
∇ᶜEs[i, n] = real(∂ᵢEₙ)
100+
∇ᶜEs[i, n] = S == HERMITIAN ? real(∂ᵢEₙ) : ∂ᵢEₙ
94101
else # degenerate bands
95102
us′ = @view us[:, ns]
96-
M = us′' * ∂ᵢH * us′ # Mₙₘ = ⟨uₙ|∂ᵢH|uₘ⟩ for n,m ∈ `ns`
97-
∂ᵢEs′ = eigvals!(Hermitian(M)) # ∂ᵢEₙ for n in `ns`
103+
_M = us′' * ∂ᵢH * us′ # Mₙₘ = ⟨uₙ|∂ᵢH|uₘ⟩ for n,m ∈ `ns`
104+
M = S == HERMITIAN ? Hermitian(_M) : _M
105+
∂ᵢEs′ = eigvals!(M) # ∂ᵢEₙ for n in `ns`
98106
∇ᶜEs[i, ns] = ∂ᵢEs′
99107
end
100108
end
@@ -215,12 +223,12 @@ The function is analogous to `evaluate_tight_binding_term!`, but computes moment
215223
gradient components rather than the Hamiltonian matrix itself.
216224
"""
217225
function evaluate_tight_binding_momentum_gradient_term!(
218-
tbt::TightBindingTerm{D},
226+
tbt::TightBindingTerm{D, S},
219227
k::ReciprocalPointLike{D},
220228
components::NTuple{C, Int},
221229
c::Union{Nothing, <:Number} = nothing,
222230
∇Hs::NTuple{C, Matrix{ComplexF64}} = ntuple(_ -> zeros(ComplexF64, size(tbt)), Val(C)),
223-
) where {D, C}
231+
) where {D, S, C}
224232
block = tbt.block
225233
block_i, block_j = tbt.block_ij
226234
is = tbt.axis[Block(block_i)] # global row indices
@@ -246,8 +254,9 @@ function evaluate_tight_binding_momentum_gradient_term!(
246254
∇Hᵢⱼ *= 2im * π # (-2πi) factor from ∂/∂kᵢ of e^{-2πik·δ}
247255
isnothing(c) || (∇Hᵢⱼ *= c) # multiply by coefficient if provided
248256
∇H[i, j] += ∇Hᵢⱼ
249-
i == j && continue # don't add diagonal elements twice
250-
∇H[j, i] += tbt.hermiticity == ANTIHERMITIAN ? -conj(∇Hᵢⱼ) : conj(∇Hᵢⱼ)
257+
if S !== NONHERMITIAN && i j # off-diagonal contribution (& don't double-add diagonal)
258+
∇H[j, i] += S == ANTIHERMITIAN ? -conj(∇Hᵢⱼ) : conj(∇Hᵢⱼ)
259+
end
251260
end
252261
end
253262
end

src/hermiticity.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
h_orbit::HoppingOrbit{D},
55
brₐ::NewBandRep{D},
66
brᵦ::NewBandRep{D},
7+
antihermitian::Bool,
78
orderingₐ::OrbitalOrdering{D} = OrbitalOrdering(brₐ),
89
orderingᵦ::OrbitalOrdering{D} = OrbitalOrdering(brᵦ),
9-
Mm::AbstractArray{Int, 4} = construct_M_matrix(h_orbit, brₐ, brᵦ, orderingₐ, orderingᵦ);
10-
antihermitian::Bool = false,
10+
Mm::AbstractArray{Int, 4} = construct_M_matrix(h_orbit, brₐ, brᵦ, orderingₐ, orderingᵦ)
1111
) where {D}
1212
1313
Constructs a basis for the coefficient vectors `t⁽ⁿ⁾` that span the space of Hermitian (or
@@ -27,10 +27,10 @@ function obtain_basis_free_parameters_hermiticity(
2727
h_orbit::HoppingOrbit{D},
2828
brₐ::NewBandRep{D},
2929
brᵦ::NewBandRep{D},
30+
antihermitian::Bool,
3031
orderingₐ::OrbitalOrdering{D} = OrbitalOrdering(brₐ),
3132
orderingᵦ::OrbitalOrdering{D} = OrbitalOrdering(brᵦ),
32-
Mm::AbstractArray{Int, 4} = construct_M_matrix(h_orbit, brₐ, brᵦ, orderingₐ, orderingᵦ);
33-
antihermitian::Bool = false,
33+
Mm::AbstractArray{Int, 4} = construct_M_matrix(h_orbit, brₐ, brᵦ, orderingₐ, orderingᵦ)
3434
) where {D}
3535
# Determine a basis for coefficient vectors t⁽ⁿ⁾ that span the space of Hermitian (or
3636
# `antihermitian` if `true`) TB Hamiltonians Hₛₜ(k) = vᵢ(k) Mᵢⱼₛₜ tⱼ = vᵀ(k) M⁽ˢᵗ⁾ t.
@@ -65,8 +65,8 @@ function constraint_hermiticity(
6565
) where {D}
6666
Q = Array{Int}(undef, size(Mm))
6767
opI = inversion(Val(D)) # inversion operation
68-
Pᵀ =
69-
transpose(_permute_symmetry_related_hoppings_under_symmetry_operation(h_orbit, opI))
68+
P = _permute_symmetry_related_hoppings_under_symmetry_operation(h_orbit, opI)
69+
Pᵀ = transpose(P)
7070
for s in axes(Mm, 3)
7171
for t in axes(Mm, 4)
7272
Q[:, :, s, t] .= Pᵀ * Mm[:, :, t, s] # Pᵀ M⁽ᵗˢ⁾

0 commit comments

Comments
 (0)