├── .gitignore ├── test └── runtests.jl ├── .github └── workflows │ └── TagBot.yml ├── examples ├── README.md ├── tbGrapheneDOS.jl ├── pseudopotAl.jl └── tbGraphene.jl ├── src ├── SimpleQuantum.jl ├── nfe.jl ├── tb.jl ├── misc.jl ├── interface.jl └── crystal.jl ├── Project.toml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /Manifest.toml 2 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | using SimpleQuantum 2 | using Test 3 | 4 | @testset "SimpleQuantum.jl" begin 5 | # Write your tests here. 6 | end 7 | -------------------------------------------------------------------------------- /.github/workflows/TagBot.yml: -------------------------------------------------------------------------------- 1 | name: TagBot 2 | on: 3 | issue_comment: 4 | types: 5 | - created 6 | workflow_dispatch: 7 | jobs: 8 | TagBot: 9 | if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: JuliaRegistries/TagBot@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | ssh: ${{ secrets.DOCUMENTER_KEY }} 16 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Full example scripts showing the usage of this library: 4 | 5 | - `pseudopotAl.jl` 6 | - Pseudopotential method calculation of aluminum band structure with a pseudopotential defined as a function of momentum. 7 | - `tbGraphene.jl` 8 | - Band structure of all 2s and 2p electrons of graphene using the tight binding method. 9 | - `tbGrapheneDOS.jl` 10 | - Calculation of density of states of a 2-band tight binding graphene model. 11 | -------------------------------------------------------------------------------- /examples/tbGrapheneDOS.jl: -------------------------------------------------------------------------------- 1 | using SimpleQuantum 2 | 3 | # Define the graphene crystal. 4 | graphene = Crystal( 5 | Lattice(2.468Å, 2.468Å, 120), 6 | UnitCell(:C, [2/3, 1/3], [1/3, 2/3]) 7 | ) 8 | 9 | # Create an empty hopping list based on graphene. 10 | grhops = Hoppings(graphene) 11 | 12 | # Iterate through unique nearest neighbor pairs and add them to the hopping list. 13 | for hop ∈ SimpleQuantum.unique_neighbors(graphene) 14 | addhop!(grhops, -2.8eV, hop.i, hop.j, hop.δ) 15 | end 16 | 17 | # Define the tight binding Hamiltonian 18 | grH = TightBindingHamiltonian(grhops) 19 | 20 | # Spec up the DOS calculation: Energy in range (-0.5 Ha, 0.5 Ha) with step of 10 mHa and 151x151 grid points. 21 | grdos = DOS([i * Ha for i ∈ -0.5:0.01:0.5], 151) 22 | 23 | # Solve the problem and plot the DOS. 24 | grH |> grdos |> solve |> plotSolution 25 | -------------------------------------------------------------------------------- /src/SimpleQuantum.jl: -------------------------------------------------------------------------------- 1 | module SimpleQuantum 2 | 3 | using LinearAlgebra 4 | using Unitful 5 | import Unitful: Å, eV 6 | using RangeHelpers: range, around 7 | using StaticArrays 8 | using SplitApplyCombine 9 | using Colors 10 | using GLMakie 11 | using Accessors 12 | using Combinatorics 13 | using MinkowskiReduction 14 | 15 | include("crystal.jl") 16 | include("interface.jl") 17 | include("tb.jl") 18 | include("nfe.jl") 19 | include("misc.jl") 20 | 21 | # Unit definitons 22 | 23 | a₀ = 0.529177249Å 24 | Ha = 27.2eV 25 | 26 | export a₀, Ha, Å, eV 27 | export Lattice, UnitCell, Crystal 28 | export shiftenergy, fermilevel 29 | export plotcrystal! 30 | export evals, evecs, kvecs 31 | export solve, plotSolution 32 | export ReciprocalPath, DOS 33 | export Hoppings, addhop!, addonsite!, addoverlap! 34 | export TightBindingHamiltonian, PseudoPotentialHamiltonian 35 | end 36 | -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "SimpleQuantum" 2 | uuid = "7c07e4e5-e402-48af-840a-a11b8ea3e28c" 3 | authors = ["Tomas Polakovic and contributors"] 4 | version = "0.2.5" 5 | 6 | [deps] 7 | Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" 8 | Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" 9 | Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" 10 | GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" 11 | LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" 12 | Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" 13 | Match = "7eb4fadd-790c-5f42-8a69-bfa0b872bfbf" 14 | MinkowskiReduction = "d617d09d-b3c0-43f1-8886-30d89b900f25" 15 | RangeHelpers = "3a07dd3d-1c52-4395-8858-40c6328157db" 16 | SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" 17 | StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" 18 | Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" 19 | 20 | [compat] 21 | julia = "1" 22 | 23 | [extras] 24 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 25 | 26 | [targets] 27 | test = ["Test"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tomas Polakovic and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/pseudopotAl.jl: -------------------------------------------------------------------------------- 1 | using SimpleQuantum 2 | using LinearAlgebra 3 | using GLMakie 4 | 5 | # Define the aluminum crystal structure. 6 | Al = Crystal( 7 | Lattice(2.856Å, 2.856Å, 2.856Å, 60, 60, 60), 8 | UnitCell(:Al, [0.0, 0.0, 0.0]) 9 | ) 10 | 11 | # Momentum representation of a Thomas-Fermi potential with charge of Q = 3 and screening length |q| = 3. 12 | V(k) = ifelse(norm(k) ≈ 0, 0, 4π * 3/(norm(k)^2 .+ 10^2)) 13 | 14 | # Define the Hamiltonian using the pseudopotential above with reciprocal lattice vectors from up to the 2nd shell 15 | alH = PseudoPotentialHamiltonian(2, V, Al) 16 | 17 | # Define the momentum path. 18 | kpath = ReciprocalPath([ 19 | :Γ => [0,0,0], 20 | :X => [1/2,0,1/2], 21 | :W => [1/2,1/4,3/4], 22 | :K => [3/8,3/8,3/4], 23 | :Γ => [0,0,0], 24 | :L => [1/2,1/2,1/2], 25 | :U => [5/8,1/4,5/8], 26 | :W => [1/2,1/4,3/4], 27 | :L => [1/2,1/2,1/2], 28 | :K => [3/8,3/8,3/4] 29 | ], 0.01) 30 | 31 | # Solve the problem 32 | sol = alH(kpath) |> solve 33 | 34 | # Find the Fermi energy (3 free electrons in the unit cell of Al). 35 | ef = sol |> fermilevel(3) 36 | println("Ef = $ef") 37 | 38 | # Plot the band diagram with energy shifted such that E_Fermi = 0. 39 | sol |> shiftenergy(ef) |> plotSolution 40 | 41 | # Scale the energy axis to focus only on reasonable states. 42 | ylims!(current_axis(), (-0.2,0.4)) 43 | -------------------------------------------------------------------------------- /src/nfe.jl: -------------------------------------------------------------------------------- 1 | function nfH(k, n::Integer, V::Function, crystal::Crystal) 2 | G = crystal.lattice.G 3 | e = sort(Iterators.product(fill(-n:n,length(k))...) |> collect |> vec, by=norm) |> collect 4 | k = G' * k 5 | Gs = (G' * collect(g) for g ∈ e) 6 | H = [V(j - i) for i ∈ Gs, j ∈ Gs] 7 | H .+= diagm((1/2*norm(k .+ g)^2 for g ∈ Gs) |> collect) 8 | end 9 | 10 | function nfH(k, n::Integer, V::Matrix, crystal::Crystal) 11 | G = crystal.lattice.G 12 | e = sort(Iterators.product(fill(-n:n,length(k))...) |> collect |> vec, by=norm) |> collect 13 | k = G' * k 14 | Gs = (G' * g for g ∈ e) 15 | V .+ diagm((1/2*norm(k .+ g)^2 for g ∈ Gs) |> collect) 16 | end 17 | 18 | struct PseudoPotentialHamiltonian <: ReciprocalHamiltonian 19 | pot 20 | n::Integer 21 | c::Crystal 22 | end 23 | 24 | # interface 25 | 26 | """ 27 | PsuedoPotentialHamiltonian(n::Integer, V::Function, c::Crystal) 28 | 29 | Assembles the pseudopotential Hamiltonian. 30 | 31 | The Hamiltonian is determined from the potential as a function of momentum `V` := V(k) with reciprocal lattice vectors from up to `n`-th shell Brillouin zone. 32 | 33 | Can be callend on a vector in k-space to output the Hamiltonian. 34 | """ 35 | function PseudoPotentialHamiltonian(n::Integer, V::F, c::Crystal) where F <: Function 36 | PseudoPotentialHamiltonian(V, n, c) 37 | end 38 | 39 | """ 40 | PsuedoPotentialHamiltonian(V::Matrix, c::Crystal) 41 | 42 | Assembles the pseudopotential Hamiltonian. 43 | 44 | The potential terms of the Hamiltoniain are stored in the matrix `V`. 45 | 46 | Can be callend on a vector in k-space to output the Hamiltonian. 47 | """ 48 | function PseudoPotentialHamiltonian(V::Matrix, c::Crystal) 49 | k = kpath(kpositions, kstep) 50 | (n1, n2) = size(V) 51 | if n1 != n2 52 | throw(ArgumentError("Pontential matrix is not square.")) 53 | end 54 | n = n1 55 | PseudoPotentialHamiltonian(V, n, c) 56 | end 57 | 58 | # function (h::PseudoPotentialHamiltonian)(k::Vector) 59 | # nfH(k, h.n, h.pot, h.c) 60 | # end 61 | 62 | crystal(h::PseudoPotentialHamiltonian) = h.c 63 | 64 | function getH(h::PseudoPotentialHamiltonian) 65 | k -> nfH(k, h.n, h.pot, h.c) 66 | end 67 | 68 | function Base.ndims(t::PseudoPotentialHamiltonian) 69 | t.c |> ndims 70 | end 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleQuantum 2 | 3 | [![DOI](https://zenodo.org/badge/554441370.svg)](https://zenodo.org/badge/latestdoi/554441370) 4 | 5 | A Julia package for calculating properties of crystalline solids. Currently supports tight binding and nearly free electron model calculations on up to 3-dimensional problems. 6 | 7 | Read the posts on the [Computational Physics for the Masses series](https://tpolakovic.github.io) for more detailed description of code and algorithms. 8 | 9 | ## Installation 10 | 11 | In Julia REPL, press `]` to enter pkg mode and enter: 12 | 13 | ``` 14 | pkg> add https://github.com/tpolakovic/SimpleQuantum 15 | ``` 16 | 17 | ## Sample 18 | 19 | ### Tight binding model of two-band graphene: 20 | 21 | ``` julia 22 | using SimpleQuantum 23 | 24 | # Define the graphene crystal. 25 | graphene = Crystal( 26 | Lattice(2.468Å, 2.468Å, 120), 27 | UnitCell(:C, [2/3, 1/3], [1/3, 2/3]) 28 | ) 29 | 30 | # Create an empty hopping list based on graphene. 31 | grhops = Hoppings(graphene) 32 | 33 | # Iterate through unique nearest neighbor pairs and add them to the hopping list. 34 | for hop ∈ SimpleQuantum.unique_neighbors(graphene) 35 | addhop!(grhops, -2.8eV, hop.i, hop.j, hop.δ) 36 | end 37 | 38 | # Define the tight binding Hamiltonian 39 | grH = TightBindingHamiltonian(grhops) 40 | 41 | # Define the momentum path. 42 | kpath = ReciprocalPath([ 43 | :K => [1/3,1/3], 44 | :Γ => [0,0], 45 | :M => [1/2,0], 46 | :K => [1/3,1/3] 47 | ], 0.005) 48 | 49 | # Solve and plot the problem. 50 | grH(kpath) |> solve |> plotSolution 51 | ``` 52 | Output: 53 | 54 | 55 | 56 | ### Nearly free electron model of aluminum: 57 | 58 | ``` julia 59 | using SimpleQuantum 60 | using LinearAlgebra 61 | using GLMakie 62 | 63 | # Define the aluminum crystal structure. 64 | Al = Crystal( 65 | Lattice(2.856Å, 2.856Å, 2.856Å, 60, 60, 60), 66 | UnitCell(:Al, [0.0, 0.0, 0.0]) 67 | ) 68 | 69 | # Momentum representation of a Thomas-Fermi potential with charge of Q = 3 and screening length |q| = 10. 70 | V(k) = ifelse(norm(k) ≈ 0, 0, 4π * 3/(norm(k)^2 .+ 10^2)) 71 | 72 | # Define the Hamiltonian using the pseudopotential above with reciprocal lattice vectors from up to the 2nd shell 73 | alH = PseudoPotentialHamiltonian(2, V, Al) 74 | 75 | # Define the momentum path. 76 | kpath = ReciprocalPath([ 77 | :Γ => [0,0,0], 78 | :X => [1/2,0,1/2], 79 | :W => [1/2,1/4,3/4], 80 | :K => [3/8,3/8,3/4], 81 | :Γ => [0,0,0], 82 | :L => [1/2,1/2,1/2], 83 | :U => [5/8,1/4,5/8], 84 | :W => [1/2,1/4,3/4], 85 | :L => [1/2,1/2,1/2], 86 | :K => [3/8,3/8,3/4] 87 | ], 0.01) 88 | 89 | # Solve the problem 90 | sol = alH(kpath) |> solve 91 | 92 | # Find the Fermi energy (3 free electrons in the unit cell of Al). 93 | ef = sol |> fermilevel(3) 94 | println("Ef = $ef") 95 | 96 | # Plot the band diagram with energy shifted such that E_Fermi = 0. 97 | sol |> shiftenergy(ef) |> plotSolution 98 | 99 | ylims!(current_axis(), (-0.2,0.4)) 100 | ``` 101 | 102 | output: 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/tb.jl: -------------------------------------------------------------------------------- 1 | struct Hop 2 | γ::Number 3 | i::Int 4 | j::Int 5 | offset::Union{<:Real, AbstractArray{<:Real}} 6 | end 7 | 8 | struct Onsite 9 | μ::Number 10 | ii::Int 11 | end 12 | 13 | struct Overlap 14 | S::Number 15 | i::Int 16 | j::Int 17 | offset::Union{<:Real, AbstractArray{<:Real}} 18 | end 19 | 20 | """ 21 | Hoppings(c::Crystal) 22 | 23 | Create an empty hopping list based on crystal structure `c`. 24 | """ 25 | mutable struct Hoppings 26 | c::Crystal 27 | γs::AbstractArray{Hop} 28 | μs::AbstractArray{Onsite} 29 | Ss::AbstractArray{Overlap} 30 | maxij::Int 31 | 32 | function Hoppings(c::Crystal) 33 | new(c, [], [], [], 0) 34 | end 35 | end 36 | 37 | """ 38 | addhop!(hops::Hoppings, γ, i, j, δ) 39 | 40 | Appends a hopping into `hops` with amplitude γ between sites `i` and `j` separated by `δ`. 41 | 42 | The separation `δ` is in reduced (fractional) coordinates. 43 | 44 | # Warning 45 | - Hoppings are conjugated automatically. This means that you need to only provide hopping i->j and j->i will be generated for you. 46 | """ 47 | function addhop!(hops, γ, i, j, δ) 48 | γ = Unitful.NoUnits(γ / Ha) 49 | offset = δ 50 | push!(hops.γs, Hop(γ, i, j, offset)) 51 | hops.maxij = max(i, j, hops.maxij) 52 | hops 53 | end 54 | 55 | """ 56 | addonsite!(hops::Hoppings, μ, ii) 57 | 58 | Adds onsite energy `μ` to site `ii`. 59 | """ 60 | function addonsite!(hops, μ, ii) 61 | μ = Unitful.NoUnits(μ / Ha) 62 | push!(hops.μs, Onsite(μ, ii)) 63 | hops.maxij = max(hops.maxij, ii) 64 | hops 65 | end 66 | 67 | """ 68 | addhop!(hops::Hoppings, γ, i, j, δ) 69 | 70 | Appends an overlap into `hops` with value S between sites `i` and `j` separated by `δ`. 71 | 72 | The separation `δ` is in reduced (fractional) coordinates. 73 | 74 | """ 75 | function addoverlap!(hops, S, i, j, δ) 76 | offset = δ 77 | push!(hops.Ss, Overlap(S, i, j, offset)) 78 | hops.maxij = max(i, j, hops.maxij) 79 | hops 80 | end 81 | 82 | function tbH(k, hops::Hoppings) 83 | n = hops.maxij 84 | ham = zeros(ComplexF32,n,n) 85 | Smat = zeros(ComplexF32,n,n) 86 | 87 | for hop ∈ hops.γs 88 | i, j = hop.i, hop.j 89 | r = hop.offset 90 | γ = hop.γ * exp(2im * π * (k ⋅ r)) 91 | ham[i,j] += γ 92 | ham[j,i] += conj(γ) 93 | end 94 | 95 | for onsite ∈ hops.μs 96 | i = onsite.ii 97 | ham[i,i] = onsite.μ 98 | end 99 | 100 | for overlap ∈ hops.Ss 101 | i, j = overlap.i, overlap.j 102 | r = overlap.offset 103 | S = overlap.S * exp(2im * π * (k ⋅ r)) 104 | Smat[i,j] += S 105 | Smat[j,i] += conj(S) 106 | end 107 | Smat += diagm(ones(n)) 108 | ham = Hermitian(ham) 109 | Smat = Hermitian(Smat) 110 | (ham, Smat) 111 | end 112 | 113 | # interface 114 | """ 115 | TightBindingProblem(hops::Hoppings) 116 | 117 | Tight binding problem definition. 118 | 119 | Can be called on a vector in k-space to output the Hamiltonian. 120 | """ 121 | struct TightBindingHamiltonian <: ReciprocalHamiltonian 122 | hops::Hoppings 123 | end 124 | 125 | crystal(h::TightBindingHamiltonian) = h.hops.c 126 | 127 | # function (p::TightBindingHamiltonian)(k) 128 | # tbH(k, p.hops) 129 | # end 130 | 131 | function getH(h::TightBindingHamiltonian) 132 | k -> tbH(k, h.hops) 133 | end 134 | 135 | function Base.ndims(t::TightBindingHamiltonian) 136 | t.hops.c |> ndims 137 | end 138 | -------------------------------------------------------------------------------- /src/misc.jl: -------------------------------------------------------------------------------- 1 | """ 2 | kpath(kpoints, dk) 3 | 4 | Constructs a k-space trajectory between multiple points. 5 | 6 | Mostly used for finding branches between critical points in the Brillouin zone. 7 | 8 | # Arguments 9 | - `kpoints::Vector{Pair(:Symbol, Vector)}`: A pair of type => . 10 | - `dk`: Spacing along the trajectory. 11 | """ 12 | function kpath(kpoints, dk) 13 | vertices = reverse([point.second for point ∈ kpoints]) 14 | labels = [point.first for point ∈ kpoints] 15 | path = [last(vertices)] 16 | plength = zeros(typeof(dk),1) 17 | idxs = [1] 18 | 19 | while length(vertices) >= 2 20 | pop!(path) 21 | v1 = pop!(vertices) 22 | v2 = last(vertices) 23 | dir = v2 .- v1 24 | dirm = norm(dir) 25 | segment = [v1 .+ dir .* dd 26 | for dd ∈ range(start = 0, stop = 1, step = around(dk/dirm))] 27 | path = append!(path,segment) 28 | idxs = push!(idxs, last(idxs) + length(segment) - 1) 29 | end 30 | 31 | plength = append!(plength, 32 | Iterators.accumulate( 33 | +,[norm(v2 .- v1) for (v1,v2) ∈ zip(path[1:end-1], path[2:end])] 34 | )) 35 | points = [lab => plength[i] for (lab,i) ∈ zip(labels, idxs)] 36 | (path=path, plength=plength, ppoints=points) 37 | end 38 | 39 | function dedup_floats(itr) 40 | out = Vector{eltype(itr)}() 41 | push!(out, itr[1]) 42 | for itrel ∈ itr 43 | if map(x -> !(itrel ≈ x), out) |> all 44 | push!(out, itrel) 45 | end 46 | end 47 | out 48 | end 49 | 50 | function approxunique(itr; kwargs...) 51 | out = Vector{eltype(itr)}() 52 | push!(out, itr[1]) 53 | for itrel ∈ itr 54 | if map(x -> !isapprox(itrel, x; kwargs...), out) |> all 55 | push!(out, itrel) 56 | end 57 | end 58 | out 59 | end 60 | 61 | """ 62 | isapproxin(el, itr; kwargs) 63 | 64 | Determines whether `el` is in iterable collection `itr` as determined by `isapprox`. 65 | """ 66 | function isapproxin(el, itr; kwargs...) 67 | for i ∈ itr 68 | isapprox(el, i; kwargs...) && return true 69 | end 70 | false 71 | end 72 | 73 | """ 74 | unique_neighbors(c::Crystal) 75 | 76 | Finds unique nearest neighbor pairs in the crystal `c`. 77 | 78 | This function *does not* return all nearest neighbor. If pair are conjugate (i.e. site1->site2 and site2->site1), only one will be returned. 79 | """ 80 | function unique_neighbors(c::Crystal) 81 | R = c.lattice.R 82 | positions = c.cell.positions 83 | supercellpositions = eltype(c.cell.positions)[] 84 | possible_hops = [] 85 | out = [] 86 | 87 | for (i, ipos) ∈ enumerate(positions) 88 | for offset ∈ Iterators.product(fill((-1,0,1), ndims(c.lattice))...) 89 | for (j, jpos) ∈ enumerate(positions) 90 | δ = (jpos .+ offset) - ipos 91 | push!(possible_hops, ( 92 | i = i, j = j, δ = δ, 93 | )) 94 | end 95 | end 96 | end 97 | filter!(v -> norm(v.δ) > 0, possible_hops) 98 | unique_dists = map(v -> (R * v.δ) |> norm, possible_hops) |> dedup_floats |> sort 99 | filter!(v -> norm(R * v.δ) <= unique_dists[1]+eps(), possible_hops) 100 | is_conjugate(u, v) = u.i == v.j && (u.δ .≈ -1 .* v.δ) |> all 101 | for hop ∈ possible_hops 102 | if map(v -> !is_conjugate(v, hop), out) |> all 103 | push!(out, hop) 104 | end 105 | end 106 | out 107 | end 108 | -------------------------------------------------------------------------------- /examples/tbGraphene.jl: -------------------------------------------------------------------------------- 1 | using SimpleQuantum 2 | using LinearAlgebra 3 | using Match 4 | 5 | # Define the orbitals at each site. 6 | orbs = [ 7 | (:s, [2/3, 1/3]), #1 8 | (:s, [1/3, 2/3]), #2 9 | ### 10 | (:px, [2/3, 1/3]), #3 11 | (:px, [1/3, 2/3]), #4 12 | ### 13 | (:py, [2/3, 1/3]), #5 14 | (:py, [1/3, 2/3]), #6 15 | ### 16 | (:pz, [2/3, 1/3]), #7 17 | (:pz, [1/3, 2/3]), #8 18 | ] 19 | 20 | # Construct an all-electron graphene crystal. 21 | grorbs = Crystal( 22 | Lattice(2.468Å, 2.468Å, 120), 23 | UnitCell([first(orb) for orb ∈ orbs], (last(orb) for orb ∈ orbs)...) 24 | ) 25 | 26 | # Define the hopping/onsite energies and ovelap integrals. 27 | hopping_ints = Dict( 28 | :ssσ => -5.729eV, 29 | :spσ => 5.618eV, 30 | :ppσ => 6.050eV, 31 | :ppπ => -3.070eV 32 | ) 33 | onsite_es = Dict( 34 | :s => -8.37eV, 35 | :p => 0eV 36 | ) 37 | overlap_vals = Dict( 38 | :ssσ => 0.102, 39 | :spσ => -0.171, 40 | :ppσ => -0.377, 41 | :ppπ => 0.070 42 | ) 43 | 44 | # Initialize an empty hopping list based on the crystal structure. 45 | grhops = Hoppings(grorbs) 46 | 47 | # Fill the hopping list 48 | dir_cos(r) = r ./ norm(r) 49 | for hop ∈ SimpleQuantum.unique_neighbors(grorbs) 50 | orb_types = (orbs[hop.i][1], orbs[hop.j][1]) 51 | l, m = dir_cos(grorbs.lattice.R * hop.δ) 52 | γ = @match orb_types begin 53 | (:s, :s) => hopping_ints[:ssσ] 54 | (:s, :px) => l * hopping_ints[:spσ] 55 | (:s, :py) => m * hopping_ints[:spσ] 56 | #(:s, :pz) => 0. 57 | (:px, :s) => l * hopping_ints[:spσ] 58 | (:px, :px) => l^2 * hopping_ints[:ppσ] + (1-l^2) * hopping_ints[:ppπ] 59 | (:px, :py) => l*m * (hopping_ints[:ppσ] - hopping_ints[:ppπ]) 60 | #(:px, :pz) => 0. 61 | (:py, :s) => m * hopping_ints[:spσ] 62 | (:py, :px) => l*m * (hopping_ints[:ppσ] - hopping_ints[:ppπ]) 63 | (:py, :py) => m^2 * hopping_ints[:ppσ] + (1-m^2) * hopping_ints[:ppπ] 64 | #(:py, :pz) => 0. 65 | #(:pz, :s) => 0. 66 | #(:pz, :px) => 0. 67 | #(:pz, :py) => 0. 68 | (:pz, :pz) => hopping_ints[:ppπ] 69 | _ => 0eV 70 | end 71 | addhop!(grhops, γ, hop.i, hop.j, hop.δ) 72 | 73 | s = @match orb_types begin 74 | (:s, :s) => overlap_vals[:ssσ] 75 | (:s, :px) => l * overlap_vals[:spσ] 76 | (:s, :py) => m * overlap_vals[:spσ] 77 | #(:s, :pz) => 0. 78 | (:px, :s) => l * overlap_vals[:spσ] 79 | (:px, :px) => l^2 * overlap_vals[:ppσ] + (1-l^2) * overlap_vals[:ppπ] 80 | (:px, :py) => l*m * (overlap_vals[:ppσ] - overlap_vals[:ppπ]) 81 | #(:px, :pz) => 0. 82 | (:py, :s) => m * overlap_vals[:spσ] 83 | (:py, :px) => l*m * (overlap_vals[:ppσ] - overlap_vals[:ppπ]) 84 | (:py, :py) => m^2 * overlap_vals[:ppσ] + (1-m^2) * overlap_vals[:ppπ] 85 | #(:py, :pz) => 0. 86 | #(:pz, :s) => 0. 87 | #(:pz, :px) => 0. 88 | #(:pz, :py) => 0. 89 | (:pz, :pz) => overlap_vals[:ppπ] 90 | _ => 0 91 | end 92 | addoverlap!(grhops, s, hop.i, hop.j, hop.δ) 93 | end 94 | 95 | for (i, orb) ∈ enumerate(orbs) 96 | orb_type = orb[1] 97 | μ = @match orb_type begin 98 | :s => onsite_es[:s] 99 | _ => onsite_es[:p] 100 | end 101 | addonsite!(grhops, μ , i) 102 | end 103 | 104 | # Define the tight binding Hamiltonian 105 | grH = TightBindingHamiltonian(grhops) 106 | 107 | # Define the momentum path. 108 | kpath = ReciprocalPath([ 109 | :K => [1/3,1/3], 110 | :Γ => [0,0], 111 | :M => [1/2,0], 112 | :K => [1/3,1/3] 113 | ], 0.005) 114 | 115 | # Solve and plot the problem. 116 | grH(kpath) |> solve |> plotSolution 117 | -------------------------------------------------------------------------------- /src/interface.jl: -------------------------------------------------------------------------------- 1 | ###################################################################################### 2 | abstract type GenericHamiltonian end 3 | 4 | # function Base.ndims(t::T) where {T <: GenericHamiltonian} 5 | # error(typeof(t), " needs to have `ndims` defined.") 6 | # end 7 | 8 | function getH(t::T) where {T <: GenericHamiltonian} 9 | error(typeof(t), " needs to have `getH` defined.") 10 | end 11 | 12 | abstract type ReciprocalHamiltonian <: GenericHamiltonian end 13 | 14 | function (h::ReciprocalHamiltonian)(k) 15 | getH(h)(k) 16 | end 17 | 18 | struct ReciprocalPath 19 | ks::Vector{Union{<:Real, Vector{<:Real}}} 20 | kp::Vector{<:Real} 21 | kl::Vector{Pair{Symbol, <:Real}} 22 | end 23 | 24 | function ReciprocalPath(kpositions::Vector, kstep::Real) 25 | k = kpath(kpositions, kstep) 26 | ReciprocalPath(k.path, k.plength, k.ppoints) 27 | end 28 | 29 | struct ReciprocalBandProblem 30 | h::ReciprocalHamiltonian 31 | kp::ReciprocalPath 32 | end 33 | 34 | function (h::ReciprocalHamiltonian)(rp::ReciprocalPath) 35 | ReciprocalBandProblem(h, rp) 36 | end 37 | 38 | function (rp::ReciprocalPath)(h::ReciprocalHamiltonian) 39 | ReciprocalBandProblem(h, rp) 40 | end 41 | 42 | """ 43 | BandSolution 44 | 45 | Type containing data of the electronic band structure calculation. 46 | """ 47 | struct BandSolution #<: Solution 48 | ks::Vector{Union{<:Real, Vector{<:Real}}} 49 | kp::Vector{<:Real} 50 | kl::Vector{Pair{Symbol, <:Real}} 51 | evals::Vector{Vector{Real}} 52 | evecs::Vector{Matrix{Complex}} 53 | end 54 | 55 | """ 56 | evals(s::BandSolution) 57 | 58 | Returns an array of eigenvalues of solution `s`. 59 | """ 60 | evals(s::BandSolution) = s.evals 61 | 62 | """ 63 | evecs(s::BandSolution) 64 | 65 | Returns an array of eigenvectors of solution `s`. 66 | """ 67 | evecs(s::BandSolution) = s.evecs 68 | 69 | """ 70 | kvecs(s::BandSolution) 71 | 72 | Returns the points along the k-space trajectory along which the solution was calculated. 73 | """ 74 | kvecs(s::BandSolution) = s.ks 75 | 76 | """ 77 | solve(rbp::ReciprocalBandProblem) 78 | 79 | Calculates the electronic bands of `rbp`. 80 | """ 81 | function solve(rbp::ReciprocalBandProblem) 82 | kbranch = rbp.kp 83 | hs = rbp.h.(kbranch.ks) 84 | if typeof(first(hs)) <: Tuple 85 | sols = hs .|> x -> eigen(x...) 86 | else 87 | sols = hs .|> eigen 88 | end 89 | BandSolution( 90 | kbranch.ks, 91 | kbranch.kp, 92 | kbranch.kl, 93 | [eig.values for eig ∈ sols], 94 | [mapslices(v -> v ./ norm(v), eig.vectors, dims = 1) for eig ∈ sols] 95 | ) 96 | end 97 | 98 | """ 99 | plotSolution(s::BandSolution) 100 | 101 | Plots the band diagram of `s`. 102 | 103 | The returned object is a figure that can be further modified if needed. 104 | """ 105 | function plotSolution(s::BandSolution) 106 | fig = Figure() 107 | ax = Axis(fig) 108 | ax.xticks = ([p.second for p ∈ s.kl], 109 | [string(p.first) for p ∈ s.kl]) 110 | ax.ylabel = "E [Ha]" 111 | 112 | xlims!(ax, (0, s.kp[end])) 113 | hideydecorations!(ax, ticks=false, ticklabels=false, label=false) 114 | for n ∈ 1:length(evals(s)[1]) 115 | lines!(s.kp, [e[n] for e ∈ evals(s)]) 116 | end 117 | fig[1,1] = ax 118 | fig 119 | end 120 | 121 | struct EFermi 122 | esincell 123 | end 124 | 125 | function(efs::EFermi)(s) 126 | es = evals(s) 127 | nks = es |> length 128 | esflat = es |> Iterators.flatten |> collect |> sort 129 | n = efs.esincell / 2 130 | n1 = floor(Int, n * nks) 131 | n2 = ceil(Int, n * nks) 132 | e = (esflat[n1] + esflat[n2]) / 2 133 | e * Ha 134 | end 135 | 136 | """ 137 | fermilevel(n) 138 | 139 | Finds the Fermi level with `n` electrons in the unit cell. 140 | """ 141 | function fermilevel(n) 142 | EFermi(n) 143 | end 144 | 145 | """ 146 | shiftenergy(shift) 147 | 148 | Rigidly shifts the energy axis by `shift`. 149 | """ 150 | function shiftenergy(shift) 151 | s -> @set s.evals = [e .- shift for e ∈ evals(s)] 152 | end 153 | 154 | function shiftenergy(shift::T) where T <: Quantity 155 | shift = Unitful.NoUnits(shift ./ Ha) 156 | shiftenergy(shift) 157 | end 158 | 159 | struct ReciprocalDOSProblem 160 | h 161 | dos 162 | end 163 | 164 | """ 165 | DOS(es, broadening, nq) 166 | 167 | Sets up the density of states calculation on energies `es` with Lorentzian energy broadening `broadening` by solving the D-dimensional problem on an evenly spaced grid with `nq`^D points. 168 | """ 169 | struct DOS 170 | es 171 | broadening 172 | nq 173 | 174 | function DOS(es::Vector{<:Real}, broadening::Real, nq::Integer) 175 | new(es, broadening, nq) 176 | end 177 | end 178 | 179 | function DOS(es::Vector{<:Real}, nq::Integer) 180 | broadening = √(minimum(diff(es)))/50 181 | DOS(es, broadening, nq) 182 | end 183 | 184 | function DOS(es::Vector{<:Quantity}, broadening::T, nq::Integer) where T <: Quantity 185 | es = Unitful.NoUnits.(es ./ Ha) 186 | broadening = Unitful.NoUnits(broadening / Ha) 187 | DOS(es, broadening, nq) 188 | end 189 | 190 | function DOS(es::Vector{<:Quantity}, nq::Integer) 191 | es = Unitful.NoUnits.(es ./ Ha) 192 | DOS(es, nq) 193 | end 194 | 195 | function (d::DOS)(h::ReciprocalHamiltonian) 196 | ReciprocalDOSProblem(h, d) 197 | end 198 | 199 | struct DOSSolution 200 | es 201 | evals 202 | ks 203 | ωs 204 | nks 205 | broadening 206 | end 207 | 208 | function (d::DOSSolution)(E) 209 | ϵ = d.broadening 210 | out = 0.0 211 | 212 | for (ω, ens) ∈ zip(d.ωs, d.evals) 213 | for En ∈ ens 214 | out += ω * ϵ / ((E - En)^2 + ϵ^2) 215 | end 216 | end 217 | (1/(π * d.nks)) * out 218 | end 219 | 220 | evals(s::DOSSolution) = s.evals 221 | 222 | """ 223 | solve(dp::ReciprocalDOSProblem) 224 | 225 | Calculates the density of states as defined in `dp`. 226 | """ 227 | function solve(dp::ReciprocalDOSProblem) 228 | h = dp.h 229 | c = crystal(h) 230 | dos = dp.dos 231 | q = dos.nq 232 | ϵ = dp.dos.broadening 233 | n = ndims(h) 234 | ωs, ks = ibzωk(c, q) 235 | nks = length(ωs) 236 | hs = h.(ks) 237 | 238 | if typeof(first(hs)) <: Tuple 239 | es = map(h -> eigen(h...).values, hs) 240 | else 241 | es = map(h -> eigen(h).values, hs) 242 | end 243 | 244 | DOSSolution(dos.es, es, ks, ωs, nks, ϵ) 245 | end 246 | 247 | """ 248 | plotSolution(s::DOSSolution) 249 | 250 | Plots the density of states of `s`. 251 | 252 | The returned object is a figure that can be further modified if needed. 253 | """ 254 | function plotSolution(s::DOSSolution) 255 | fig = Figure() 256 | ax = Axis(fig) 257 | ax.xlabel = "E [Ha]" 258 | ax.ylabel = "DOS [a.u.]" 259 | 260 | lines!(s.es, s.(s.es)) 261 | hidexdecorations!(ax, ticks=false, ticklabels=false, label=false) 262 | hideydecorations!(ax, label=false) 263 | 264 | fig[1,1] = ax 265 | fig 266 | end 267 | -------------------------------------------------------------------------------- /src/crystal.jl: -------------------------------------------------------------------------------- 1 | # lattice 2 | struct Lattice{T<:Real, N} 3 | R::Union{T, AbstractArray{T}} 4 | G::Union{T, AbstractArray{T}} 5 | V::T 6 | 7 | function Lattice(R::Union{<:Real, AbstractMatrix{<:Real}}) 8 | G = 2π * inv(R) 9 | R,G = promote(R,G) 10 | V = det(R) 11 | dim = isempty(size(R)) ? 1 : first(size(R)) 12 | new{eltype(R), dim}(R, G, V) 13 | end 14 | end 15 | 16 | function Lattice(R::Union{<:Quantity, AbstractMatrix{<:Quantity}}) 17 | Unitful.NoUnits.(R ./ a₀) |> Lattice 18 | end 19 | 20 | """ 21 | Lattice(a, b, γ) 22 | 23 | Construct a 2D Bravais lattice with lattice constants `a` and `b` and angle between them in degrees. 24 | 25 | Lattice constants can be dimensionless or dimensionfull using `Unitful` units. 26 | """ 27 | function Lattice(a::T, b::T, γ::Real) where T <: Union{Real, Quantity} 28 | γ = deg2rad(γ) 29 | R = @SMatrix [a*sin(γ) zero(T); 30 | a*cos(γ) b] 31 | 32 | Lattice(R) 33 | end 34 | 35 | """ 36 | Lattice(a, b, c, α, β, γ) 37 | 38 | Construct a 3D Bravais lattice with lattice constants `a`, `b` and `c` and angles `α`, `β` and `γ` between them in degrees. 39 | 40 | Lattice constants can be dimensionless or dimensionfull using `Unitful` units. 41 | """ 42 | function Lattice(a::T, b::T, c::T, α::Real, β::Real, γ::Real) where T <: Union{Real, Quantity} 43 | α, β, γ = deg2rad.((α, β, γ)) 44 | γ = (cos(α) * cos(β) - cos(γ)) / (sin(α) * sin(β)) 45 | γ = clamp(γ, -1, 1) |> acos 46 | R = @SMatrix [a*sin(β) -b*sin(α)*cos(γ) zero(T); 47 | zero(T) b*sin(α)*sin(γ) zero(T); 48 | a*cos(β) b*cos(α) c] 49 | 50 | Lattice(R) 51 | end 52 | 53 | Base.ndims(l::Lattice{T,N}) where {T,N} = N 54 | 55 | # unit cell 56 | struct UnitCell{T, N} 57 | positions::AbstractVector{T} 58 | species::AbstractVector{Symbol} 59 | 60 | """ 61 | UnitCell(species::AbstractVector{Symbol}, rs...) 62 | 63 | Construct an crystal unit cell. 64 | 65 | # Arguments 66 | - `species::AbstractVector{Symbol}`: Vector containing site identifiers as symbols. Must be the same length as number of site positions 67 | - `rs::Union{AbstractVector{T}, T}`: Site positions. Can be vectors or single numbers in 1D cells. 68 | 69 | # Examples 70 | ``` 71 | UnitCell([:B, :N], [2//3, 1//3], [1//3, 2//3]) 72 | UnitCell{Vector{Rational{Int64}}, 2}(Vector{Rational{Int64}}[[2//3, 1//3], [1//3, 2//3]], [:B, :N]) 73 | ``` 74 | """ 75 | function UnitCell(species::AbstractVector{Symbol}, rs::T...) where T 76 | positions = collect(rs) 77 | if length(species) == length(rs) 78 | new{T, length(first(rs))}(positions, species) 79 | else 80 | throw(DimensionMismatch("Number of species and positions does not match")) 81 | end 82 | end 83 | 84 | end 85 | 86 | """ 87 | UnitCell(species::AbstractVector{Symbol}, rs...) 88 | 89 | Construct an crystal unit cell. 90 | 91 | # Arguments 92 | - `species::Symbol`: Site identifier as symbol. All sites will have this identifier. 93 | - `rs::Union{AbstractVector{T}, T}`: Site positions. Can be vectors or single numbers in 1D cells. 94 | 95 | # Examples 96 | ``` 97 | UnitCell(:C, [2//3, 1//3], [1//3, 2//3]) 98 | UnitCell{Vector{Rational{Int64}}, 2}(Vector{Rational{Int64}}[[2//3, 1//3], [1//3, 2//3]], [:C, :C]) 99 | ``` 100 | """ 101 | function UnitCell(species::Symbol, rs...) 102 | species = fill(species, length(rs)) 103 | UnitCell(species, rs...) 104 | end 105 | 106 | """ 107 | UnitCell(rs...) 108 | 109 | Construct an crystal unit cell. Sites will have a generic label `:none` 110 | 111 | # Arguments 112 | - `rs::Union{AbstractVector{T}, T}`: Site positions. Can be vectors or single numbers in 1D cells. 113 | 114 | # Examples 115 | ``` 116 | UnitCell([2//3, 1//3], [1//3, 2//3]) 117 | UnitCell{Vector{Rational{Int64}}, 2}(Vector{Rational{Int64}}[[2//3, 1//3], [1//3, 2//3]], [:none, :none]) 118 | ``` 119 | """ 120 | function UnitCell(rs...) 121 | UnitCell(:none, rs...) 122 | end 123 | 124 | Base.ndims(c::UnitCell{T,N}) where {T,N} = N 125 | Base.length(c::UnitCell) = c.positions |> length 126 | 127 | # crystal 128 | """ 129 | Crystal(L::Lattice, c::UnitCell) 130 | 131 | Creates a crystal structure out of unit cell `c` on a Bravais lattice `L`. 132 | """ 133 | struct Crystal{N} 134 | lattice::Lattice 135 | cell::UnitCell 136 | 137 | function Crystal(l::Lattice{T,N}, c::UnitCell) where {T,N} 138 | new{N}(l, c) 139 | end 140 | end 141 | 142 | function plotcrystal!(ax, c::Crystal, vertpos; ncells, showcell, showbonds, cmap) 143 | R = c.lattice.R 144 | 145 | if showbonds 146 | for offset ∈ Iterators.product(fill(0:(ncells-1), ndims(c.lattice))...) 147 | supercellpositions = eltype(c.cell.positions)[] 148 | for pos ∈ c.cell.positions 149 | for tn ∈ Iterators.product(fill((-1,0,1), ndims(c.lattice))...) 150 | push!(supercellpositions, R * (pos .+ tn)) 151 | end 152 | end 153 | 154 | for pos ∈ c.cell.positions 155 | pos = R * pos 156 | rs = sort(map(v -> v .- pos, supercellpositions), by=norm) 157 | filter!(v -> norm(v) > 0, rs) 158 | nns = filter(v -> norm(v) ≈ norm(first(rs)), rs) 159 | 160 | for nn ∈ nns 161 | pos1 = pos .+ R * collect(offset) 162 | pos2 = pos .+ nn .+ R * collect(offset) 163 | lines!(ax, [tuple(pos1...), tuple(pos2...)], color=:black, linewidth=2) 164 | end 165 | end 166 | end 167 | end 168 | 169 | for offset ∈ Iterators.product(fill(0:(ncells-1), ndims(c.lattice))...) 170 | sps = unique(c.cell.species) 171 | nsps = length(sps) 172 | cmap = Makie.categorical_colors(cmap, nsps > 1 ? nsps : 2) 173 | if showcell 174 | cellvertices = map(v -> tuple((R * (v .+ offset))...), eachcol(vertpos)) 175 | lines!(ax, cellvertices, color=:grey, linewidth=0.5) 176 | end 177 | for (i,sp) ∈ enumerate(sps) 178 | idxs = findall(x -> x == sp, c.cell.species) 179 | pos = [tuple((R * (c.cell.positions[i] .+ offset))...) for i ∈ idxs] 180 | meshscatter!(ax, pos, markersize=0.2, color=cmap[i], shading=true, label=string(sp)) 181 | end 182 | end 183 | 184 | nothing 185 | end 186 | 187 | Base.ndims(::Crystal{N}) where {N} = N 188 | 189 | plotcrystal!(ax, c::Crystal{2}; ncells=1, showcell=true, showbonds=true, cmap=:PuOr_5) = 190 | plotcrystal!(ax, c, [0 0 1 1 0; 0 1 1 0 0]; ncells, showcell, showbonds, cmap=cmap) 191 | 192 | plotcrystal!(ax, c::Crystal{3}; ncells=1, showcell=true, showbonds=true, cmap=:PuOr_5) = plotcrystal!(ax, c, 193 | [0 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 1; 194 | 0 0 1 1 0 0 1 1 0 0 0 1 1 0 0 1 1 0 0; 195 | 0 0 0 0 0 1 1 0 0 0 1 1 0 0 1 1 1 1 1]; ncells, showcell, showbonds, cmap=cmap) 196 | 197 | function mpspace(q) 198 | [(2r - q - 1)/2q for r ∈ 1:q] 199 | end 200 | 201 | function reduced_basis(c::Crystal{2}) 202 | vs = GaussReduce((eachcol(c.lattice.R))...) 203 | hcat(vs...) 204 | end 205 | 206 | function reduced_basis(c::Crystal{3}) 207 | vs = minkReduce((eachcol(c.lattice.R))...) 208 | length(vs) > 3 && return hcat(vs[1:3]...) 209 | hcat(vs...) 210 | end 211 | 212 | """ 213 | getPG(c::Crystal) 214 | 215 | Calculates the point symmetry group operations of `c`. 216 | """ 217 | function getPG(c::Crystal) 218 | R = reduced_basis(c) |> Matrix 219 | D = ndims(c) 220 | iR = inv(R) 221 | norms = mapslices(norm, R; dims=1) 222 | vol = c.lattice.V 223 | ls = round.(norms) 224 | verts = (Iterators.product(ntuple(i -> -ls[i]:ls[i], D)...) 225 | .|> collect 226 | |> x -> reshape(x, :, 1) 227 | |> combinedims)[:,:,1] 228 | out = SMatrix{D,D}[] 229 | for perm ∈ permutations(1:size(verts, 2), D) 230 | vs = R * verts[:, perm] 231 | _norms = mapslices(norm, vs; dims=1) 232 | _vol = abs(det(vs)) 233 | if all(norms ≈ _norms) & all(vol ≈ _vol) 234 | op = SMatrix{D,D}(vs * iR) 235 | if all(op' * op ≈ I) 236 | append!(out, [op]) 237 | end 238 | end 239 | end 240 | out 241 | end 242 | 243 | """ 244 | _maptoibz(ks, c::Crystal[, pg]; kwargs) 245 | 246 | Finds irreducible momentum vectors of collection `ks`. 247 | """ 248 | function _maptoibz end 249 | 250 | function _maptoibz(ks, c::Crystal, pg; kwargs...) 251 | D = ndims(c) 252 | out = SVector{D}[] 253 | for k ∈ ks 254 | k = SVector{D}(k) 255 | mks = map(m -> m * k, pg) 256 | isin = false 257 | for mk ∈ mks 258 | if isapproxin(mk, out) 259 | isin = true 260 | break 261 | end 262 | end 263 | if !isin 264 | push!(out, k) 265 | end 266 | end 267 | out 268 | end 269 | 270 | function _maptoibz(ks, c::Crystal; kwargs...) 271 | _maptoibz(ks, c, getPG(c)) 272 | end 273 | 274 | """ 275 | ibzωk(c::Crystal, q::Integer[, pg]) 276 | 277 | Momentum vectors in irreducible Brillouin zone of `c`. 278 | 279 | A regular grid with `q` points along each axis is reduced to IBZ by application of 280 | point symmetry operations `pg` and multiplicieties of irreducible vector set are calculated. 281 | 282 | # Returns 283 | - `Tuple(Array{Int}, Array{SVector})`: Multiplicities and positions of irreducible grid points. 284 | """ 285 | function ibzωk end 286 | 287 | function ibzωk(c::Crystal, q::Integer, pg) 288 | n = ndims(c) 289 | ks = Iterators.product(ntuple(_ -> mpspace(q), n)...) 290 | ks = _maptoibz(ks, c) 291 | ωs = map(ks) do k 292 | map(p -> p * k, pg) |> approxunique |> length 293 | end 294 | (ωs, ks) 295 | end 296 | 297 | function ibzωk(c::Crystal, q::Integer) 298 | ibzωk(c::Crystal, q::Int, getPG(c)) 299 | end 300 | 301 | """ 302 | ∫bz(f::Function, c::Crystal, q::Int[, ωks]) 303 | 304 | Evaluates integral of `f` within Brillouin zone on a grid of `q` points along each axis. 305 | """ 306 | function ∫bz end 307 | 308 | function ∫bz(f::Function, c::Crystal, q::Int, ωks) 309 | ωs, ks = ωks 310 | mapreduce(x -> x[2] * f(x[1]), +, zip(eachcol(ks), ωs)) / length(ks) 311 | end 312 | 313 | function ∫bz(f::Function, c::Crystal, q::Int) 314 | ∫bz(f::Function, c::Crystal, q::Int, ibzωk(c, q)) 315 | end 316 | --------------------------------------------------------------------------------