• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

joachimbrand / Rimu.jl / 11565770445

29 Oct 2024 01:46AM UTC coverage: 94.31% (-0.5%) from 94.852%
11565770445

push

github

web-flow
Feature/ExtendedHubbardMom1D (#286)

* Create ExtendedHubbardMom1D.jl

- New model (i.e. ``ExtendedHubbardMom1D`` is added to Rimu

* Update excitations.jl

- ``extended_momentum_transfer_diagonal`` is added for the nearest neighbour term in ``ExtendedHubbardMom1D``

* Update Hamiltonians.jl

- ``ExtendedHubbardMom1D`` is added to all the necessary test sets.

* Update Hamiltonians.jl

* Update hamiltonians.md

* Update Hamiltonians.jl

* Update src/Hamiltonians/ExtendedHubbardMom1D.jl

Co-authored-by: Joachim Brand <joachim.brand@gmail.com>

* Update src/Hamiltonians/ExtendedHubbardMom1D.jl

Co-authored-by: Joachim Brand <joachim.brand@gmail.com>

* Update src/Hamiltonians/excitations.jl

Co-authored-by: Joachim Brand <joachim.brand@gmail.com>

* Update src/Hamiltonians/ExtendedHubbardMom1D.jl

Co-authored-by: Joachim Brand <joachim.brand@gmail.com>

* Update HubbardMom1D.jl

* Update HubbardMom1D.jl

* Update HubbardMom1D.jl

* Update HubbardMom1D.jl

* Update HubbardMom1D.jl

* Update excitations.jl

* Update ExtendedHubbardMom1D.jl

* Update HubbardMom1D.jl

* Update Hamiltonians.jl

* Update Hamiltonians.jl

* Update HubbardMom1D.jl

* Update Hamiltonians.jl

* Update excitations.jl

* Update ExtendedHubbardMom1D.jl

* Update ExtendedHubbardMom1D.jl

* docstring fix for HubbardMom1DEP

* Update HubbardMom1DEP.jl

* Update excitations.jl

* Update ExtendedHubbardMom1D.jl

* Update ExtendedHubbardMom1D.jl

* Update HubbardMom1D.jl

* Update ExtendedHubbardMom1D.jl

* Update Hamiltonians.jl

- Added test for comparison between energies of ``ExtendedHubbardMom1D`` and ``ExtendedHubbardReal1D``

* Update src/Hamiltonians/ExtendedHubbardMom1D.jl

Co-authored-by: mtsch <matijacufar@gmail.com>

* Update src/Hamiltonians/ExtendedHubbardMom1D.jl

Co-authored-by: mtsch <matijacufar@gmail.com>

* Update Hamiltonians.jl

* Update Hamiltonians.jl

* Update Hamiltonian... (continued)

66 of 73 new or added lines in 3 files covered. (90.41%)

40 existing lines in 18 files now uncovered.

6978 of 7399 relevant lines covered (94.31%)

13438492.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.9
/src/BitStringAddresses/fockaddress.jl
1
"""
2
    AbstractFockAddress{N,M}
3

4
Abstract type representing a Fock state with `N` particles and `M` modes.
5

6
See also [`SingleComponentFockAddress`](@ref), [`CompositeFS`](@ref), [`BoseFS`](@ref),
7
[`FermiFS`](@ref).
8
"""
9
abstract type AbstractFockAddress{N,M} end
10

11
# `AbstractFockAddress`es can be reconstructed from their printout.
UNCOV
12
Base.typeinfo_implicit(::Type{<:AbstractFockAddress}) = true
×
13

14
"""
15
    num_particles(::Type{<:AbstractFockAddress})
16
    num_particles(::AbstractFockAddress)
17

18
Number of particles represented by address.
19
"""
20
num_particles(a::AbstractFockAddress) = num_particles(typeof(a))
1,207,058✔
21
num_particles(::Type{<:AbstractFockAddress{N}}) where {N} = N
1,207,125✔
22

23
"""
24
    num_modes(::Type{<:AbstractFockAddress})
25
    num_modes(::AbstractFockAddress)
26

27
Number of modes represented by address.
28
"""
29
num_modes(a::AbstractFockAddress) = num_modes(typeof(a))
434,563,757✔
30
num_modes(::Type{<:AbstractFockAddress{<:Any,M}}) where {M} = M
434,563,916✔
31

32
"""
33
    num_components(::Type{<:AbstractFockAddress})
34
    num_components(::AbstractFockAddress)
35

36
Number of components in address.
37
"""
38
num_components(b::AbstractFockAddress) = num_components(typeof(b))
65,525✔
39

40
"""
41
    SingleComponentFockAddress{N,M} <: AbstractFockAddress{N,M}
42

43
A type representing a single component Fock state with `N` particles and `M` modes.
44

45
Implemented subtypes: [`BoseFS`](@ref), [`FermiFS`](@ref).
46

47
# Supported functionality
48

49
* [`find_mode`](@ref)
50
* [`find_occupied_mode`](@ref)
51
* [`num_occupied_modes`](@ref)
52
* [`occupied_modes`](@ref): Lazy iterator.
53
* [`OccupiedModeMap`](@ref): `AbstractVector` with eager construction.
54
* [`excitation`](@ref): Create a new address.
55
* [`BoseFSIndex`](@ref) and [`FermiFSIndex`](@ref) for indexing.
56

57
See also [`CompositeFS`](@ref), [`AbstractFockAddress`](@ref).
58
"""
59
abstract type SingleComponentFockAddress{N,M} <: AbstractFockAddress{N,M} end
60

UNCOV
61
num_components(::Type{<:SingleComponentFockAddress}) = 1
×
62

63
"""
64
    occupation_number_representation(fs::SingleComponentFockAddress)
65
    onr(fs::SingleComponentFockAddress)
66

67
Compute and return the occupation number representation of the Fock state `fs` as an
68
`SVector{M}`, where `M` is the number of modes.
69
"""
70
onr
71

72
"""
73
    find_mode(::SingleComponentFockAddress, i)
74

75
Find the `i`-th mode in address. Returns [`BoseFSIndex`](@ref) for [`BoseFS`](@ref), and
76
[`FermiFSIndex`](@ref) for [`FermiFS`](@ref). Can work on a tuple of modes. Does not check
77
bounds.
78

79
```jldoctest
80
julia> find_mode(BoseFS(1, 0, 2), 2)
81
BoseFSIndex(occnum=0, mode=2, offset=2)
82

83
julia> find_mode(FermiFS(1, 1, 1, 0), (2,3))
84
(FermiFSIndex(occnum=1, mode=2, offset=1), FermiFSIndex(occnum=1, mode=3, offset=2))
85
```
86

87
See [`SingleComponentFockAddress`](@ref).
88
"""
89
find_mode
90

91
"""
92
    find_occupied_mode(::SingleComponentFockAddress, k)
93
    find_occupied_mode(::BoseFS, k, [n])
94

95
Find the `k`-th occupied mode in address (with at least `n` particles).
96
Returns [`BoseFSIndex`](@ref) for [`BoseFS`](@ref), and [`FermiFSIndex`](@ref) for
97
[`FermiFS`](@ref). When unsuccessful it returns a zero index.
98

99
# Example
100

101
```jldoctest
102
julia> find_occupied_mode(FermiFS(1, 1, 1, 0), 2)
103
FermiFSIndex(occnum=1, mode=2, offset=1)
104

105
julia> find_occupied_mode(BoseFS(1, 0, 2), 1)
106
BoseFSIndex(occnum=1, mode=1, offset=0)
107

108
julia> find_occupied_mode(BoseFS(1, 0, 2), 1, 2)
109
BoseFSIndex(occnum=2, mode=3, offset=3)
110
```
111

112
See also [`occupied_modes`](@ref), [`OccupiedModeMap`](@ref),
113
[`SingleComponentFockAddress`](@ref).
114
"""
115
function find_occupied_mode(b::SingleComponentFockAddress, index::Integer, n=1)
2,236,431✔
116
    mode_iterator = occupied_modes(b)
2,961,717✔
117
    T = eltype(mode_iterator)
1,118,216✔
118
    for (occnum, mode, offset) in mode_iterator
2,234,028✔
119
        index -= occnum ≥ n
1,934,284✔
120
        if index == 0
1,934,284✔
121
            return T(occnum, mode, offset)
1,118,216✔
122
        end
123
    end
1,632,188✔
124
    return T(0, 0, 0)
×
125
end
126

127
"""
128
    num_occupied_modes(::SingleComponentFockAddress)
129

130
Get the number of occupied modes in address. Equivalent to
131
`length(`[`occupied_modes`](@ref)`(address))`, or the number of non-zeros in its ONR
132
representation.
133

134
# Example
135

136
```jldoctest
137
julia> num_occupied_modes(BoseFS((1, 0, 2)))
138
2
139
julia> num_occupied_modes(FermiFS((1, 1, 1, 0)))
140
3
141
```
142

143
See [`SingleComponentFockAddress`](@ref).
144
"""
145
num_occupied_modes
146

147
"""
148
    occupied_modes(::SingleComponentFockAddress)
149

150
Return a lazy iterator over all occupied modes in an address. Iterates over
151
[`BoseFSIndex`](@ref)s for [`BoseFS`](@ref), and over [`FermiFSIndex`](@ref)s for
152
[`FermiFS`](@ref). See [`OccupiedModeMap`](@ref) for an eager version.
153

154
# Example
155

156
```jldoctest
157
julia> b = BoseFS((1,5,0,4));
158

159
julia> foreach(println, occupied_modes(b))
160
BoseFSIndex(occnum=1, mode=1, offset=0)
161
BoseFSIndex(occnum=5, mode=2, offset=2)
162
BoseFSIndex(occnum=4, mode=4, offset=9)
163
```
164

165
```jldoctest
166
julia> f = FermiFS((1,1,0,1,0,0,1));
167

168
julia> foreach(println, occupied_modes(f))
169
FermiFSIndex(occnum=1, mode=1, offset=0)
170
FermiFSIndex(occnum=1, mode=2, offset=1)
171
FermiFSIndex(occnum=1, mode=4, offset=3)
172
FermiFSIndex(occnum=1, mode=7, offset=6)
173
```
174
See also [`find_occupied_mode`](@ref),
175
[`SingleComponentFockAddress`](@ref).
176
"""
177
occupied_modes
178

179
"""
180
    excitation(addr::SingleComponentFockAddress, creations::NTuple, destructions::NTuple)
181

182
Generate an excitation on address `addr` by applying `creations` and `destructions`, which
183
are tuples of the appropriate address indices (i.e. [`BoseFSIndex`](@ref) for bosons, or
184
[`FermiFSIndex`](@ref) for fermions).
185

186
```math
187
a^†_{c_1} a^†_{c_2} \\ldots a_{d_1} a_{d_2} \\ldots |\\mathrm{addr}\\rangle \\to
188
α|\\mathrm{naddr}\\rangle
189
```
190

191
Returns the new address `naddr` and the factor `α`. The value of `α` is given by the square
192
root of the product of mode occupations before destruction and after creation. If the
193
excitation is illegal, returns an arbitrary address and the value `0.0`.
194

195
# Example
196

197
```jldoctest
198
julia> f = FermiFS(1,1,0,0,1,1,1,1)
199
FermiFS{6,8}(1, 1, 0, 0, 1, 1, 1, 1)
200

201
julia> i, j, k, l = find_mode(f, (3,4,2,5))
202
(FermiFSIndex(occnum=0, mode=3, offset=2), FermiFSIndex(occnum=0, mode=4, offset=3), FermiFSIndex(occnum=1, mode=2, offset=1), FermiFSIndex(occnum=1, mode=5, offset=4))
203

204
julia> excitation(f, (i,j), (k,l))
205
(FermiFS{6,8}(1, 0, 1, 1, 0, 1, 1, 1), -1.0)
206
```
207

208
See [`SingleComponentFockAddress`](@ref).
209
"""
210
excitation
211

212
"""
213
    OccupiedModeMap(addr) <: AbstractVector
214

215
Get a map of occupied modes in address as an `AbstractVector` of indices compatible with
216
[`excitation`](@ref) - [`BoseFSIndex`](@ref) or [`FermiFSIndex`](@ref).
217

218
`OccupiedModeMap(addr)[i]` contains the index for the `i`-th occupied mode.
219
This is useful because repeatedly looking for occupied modes with
220
[`find_occupied_mode`](@ref) can be time-consuming.
221
`OccupiedModeMap(addr)` is an eager version of the iterator returned by
222
[`occupied_modes`](@ref). It is similar to [`onr`](@ref) but contains more information.
223

224
# Example
225

226
```jldoctest
227
julia> b = BoseFS(10, 0, 0, 0, 2, 0, 1)
228
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
229

230
julia> mb = OccupiedModeMap(b)
231
3-element OccupiedModeMap{7, BoseFSIndex}:
232
 BoseFSIndex(occnum=10, mode=1, offset=0)
233
 BoseFSIndex(occnum=2, mode=5, offset=14)
234
 BoseFSIndex(occnum=1, mode=7, offset=18)
235

236
julia> f = FermiFS(1,1,1,1,0,0,1,0,0)
237
FermiFS{5,9}(1, 1, 1, 1, 0, 0, 1, 0, 0)
238

239
julia> mf = OccupiedModeMap(f)
240
5-element OccupiedModeMap{5, FermiFSIndex}:
241
 FermiFSIndex(occnum=1, mode=1, offset=0)
242
 FermiFSIndex(occnum=1, mode=2, offset=1)
243
 FermiFSIndex(occnum=1, mode=3, offset=2)
244
 FermiFSIndex(occnum=1, mode=4, offset=3)
245
 FermiFSIndex(occnum=1, mode=7, offset=6)
246

247
julia> mf == collect(occupied_modes(f))
248
true
249

250
julia> dot(mf, mb)
251
11
252

253
julia> dot(mf, 1:20)
254
17
255
```
256
See also [`dot`](@ref Main.Hamiltonians.dot), [`SingleComponentFockAddress`](@ref).
257
"""
258
struct OccupiedModeMap{N,T} <: AbstractVector{T}
259
    indices::SVector{N,T} # N = min(N, M)
201,492,969✔
260
    length::Int
261
end
262

263
function OccupiedModeMap(addr::SingleComponentFockAddress{N,M}) where {N,M}
201,492,921✔
264
    modes = occupied_modes(addr)
201,492,917✔
265
    T = eltype(modes)
201,492,925✔
266
    # There are at most N occupied modes. This could be also @generated for cases where N ≫ M
267
    L = ismissing(N) ? M : min(N, M)
201,492,925✔
268
    indices = MVector{L,T}(undef)
201,492,926✔
269
    i = 0
201,492,914✔
270
    for index in modes
402,985,790✔
271
        i += 1
737,347,045✔
272
        @inbounds indices[i] = index
737,347,037✔
273
    end
1,273,201,413✔
274
    return OccupiedModeMap(SVector(indices), i)
201,492,950✔
275
end
276

277
Base.size(om::OccupiedModeMap) = (om.length,)
473,456,104✔
278
function Base.getindex(om::OccupiedModeMap, i)
1,238,380,132✔
279
    @boundscheck 1 ≤ i ≤ om.length || throw(BoundsError(om, i))
1,238,380,086✔
280
    return om.indices[i]
1,238,380,326✔
281
end
282

283
"""
284
    abstract type OccupiedModeIterator
285

286
Iterator over occupied modes with `eltype` [`BoseFSIndex`](@ref) or
287
[`FermiFSIndex`](@ref). A subtype of this should be returned when calling
288
[`occupied_modes`](@ref) on a Fock state.
289
"""
290
abstract type OccupiedModeIterator end
291

292
"""
293
    dot(map::OccupiedModeMap, vec::AbstractVector)
294
    dot(map1::OccupiedModeMap, map2::OccupiedModeMap)
295
Dot product extracting mode occupation numbers from an [`OccupiedModeMap`](@ref) similar
296
to [`onr`](@ref).
297

298
```jldoctest
299
julia> b = BoseFS(10, 0, 0, 0, 2, 0, 1)
300
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
301

302
julia> mb = OccupiedModeMap(b)
303
3-element OccupiedModeMap{7, BoseFSIndex}:
304
 BoseFSIndex(occnum=10, mode=1, offset=0)
305
 BoseFSIndex(occnum=2, mode=5, offset=14)
306
 BoseFSIndex(occnum=1, mode=7, offset=18)
307

308
julia> dot(mb, 1:7)
309
27
310

311
julia> mb⋅(1:7) == onr(b)⋅(1:7)
312
true
313
```
314
See also [`SingleComponentFockAddress`](@ref).
315
"""
316
function LinearAlgebra.dot(map::OccupiedModeMap, vec::AbstractVector)
28,951,204✔
317
    value = zero(eltype(vec))
28,951,200✔
318
    for index in map
57,902,396✔
319
        value += vec[index.mode] * index.occnum
138,680,891✔
320
    end
248,410,563✔
321
    return value
28,951,212✔
322
end
323
LinearAlgebra.dot(vec::AbstractVector, map::OccupiedModeMap) = dot(map, vec)
138,680,828✔
324

325
# Defined for consistency. Could also be used to compute cross-component interactions in
326
# real space.
327
function LinearAlgebra.dot(map1::OccupiedModeMap, map2::OccupiedModeMap)
1✔
328
    i = j = 1
1✔
329
    value = 0
1✔
330
    while i ≤ length(map1) && j ≤ length(map2)
7✔
331
        index1 = map1[i]
6✔
332
        index2 = map2[j]
6✔
333
        if index1.mode == index2.mode
6✔
334
            value += index1.occnum * index2.occnum
2✔
335
            i += 1
2✔
336
            j += 1
2✔
337
        elseif index1.mode < index2.mode
4✔
338
            i += 1
3✔
339
        else
340
            j += 1
1✔
341
        end
342
    end
6✔
343
    return value
1✔
344
end
345

346
"""
347
    parse_address(str)
348

349
Parse the compact representation of a Fock state address.
350
"""
351
function parse_address(str)
282✔
352
    # CompositeFS
353
    m = match(r"⊗", str)
282✔
354
    if !isnothing(m)
290✔
355
        if !isnothing(match(r"[↓⇅]", str))
10✔
356
            throw(ArgumentError("invalid fock state format \"$str\""))
2✔
357
        else
358
            return CompositeFS(map(parse_address, split(str, r" *⊗ *"))...)
6✔
359
        end
360
    end
361
    # FermiFS2C
362
    m = match(r"[↓⇅]", str)
274✔
363
    if !isnothing(m)
288✔
364
        m = match(r"\|([↑↓⇅⋅ ]+)⟩", str)
14✔
365
        if isnothing(m)
28✔
366
            throw(ArgumentError("invalid fock state format \"$str\""))
×
367
        else
368
            chars = filter(!=(' '), Vector{Char}(m.captures[1]))
28✔
369
            f1 = FermiFS((chars .== '↑') .| (chars .== '⇅'))
14✔
370
            f2 = FermiFS((chars .== '↓') .| (chars .== '⇅'))
14✔
371
            return CompositeFS(f1, f2)
14✔
372
        end
373
    end
374
    # Sparse BoseFS
375
    m = match(r"\|b *([0-9]+): *([ 0-9]+)⟩", str)
260✔
376
    if !isnothing(m)
272✔
377
        particles = parse.(Int, filter(!isempty, split(m.captures[2], r" +")))
12✔
378
        return BoseFS(parse(Int, m.captures[1]), zip(particles, fill(1, length(particles))))
12✔
379
    end
380
    # Sparse FermiFS
381
    m = match(r"\|f *([0-9]+): *([ 0-9]+)⟩", str)
248✔
382
    if !isnothing(m)
270✔
383
        particles = parse.(Int, filter(!isempty, split(m.captures[2], r" +")))
22✔
384
        return FermiFS(parse(Int, m.captures[1]), zip(particles, fill(1, length(particles))))
22✔
385
    end
386
    # OccupationNumberFS
387
    m = match(r"\|([ 0-9]+)⟩{[0-9]*}", str)
226✔
388
    if !isnothing(m)
240✔
389
        m2 = match(r"{([0-9]+)}", str)
14✔
390
        if isnothing(m2) # empty braces defaults to UInt8
26✔
391
            BITS = 8
2✔
392
        else
393
            BITS = parse(Int, m2.captures[1])
12✔
394
        end
395
        T = if BITS ≤ 8
14✔
396
            UInt8
9✔
397
        elseif BITS ≤ 16
5✔
398
            UInt16
1✔
399
        elseif BITS ≤ 32
4✔
400
            UInt32
1✔
401
        elseif BITS ≤ 64
3✔
402
            UInt64
1✔
403
        elseif BITS ≤ 128
2✔
404
            UInt128
1✔
405
        else
406
            throw(ArgumentError("invalid Fock state format \"$str\""))
23✔
407
        end
408
        t = Tuple(parse.(T, split(m.captures[1], r" +")))
13✔
409
        return OccupationNumberFS(SVector(t))
13✔
410
    end
411
    m = match(r"\|([ 0-9]+)⟩{", str) # anything else that has a curly brace
212✔
412
    if !isnothing(m)
213✔
413
        throw(ArgumentError("invalid Fock state format \"$str\""))
1✔
414
    end
415

416
    # BoseFS
417
    m = match(r"\|([ 0-9]+)⟩", str)
211✔
418
    if !isnothing(m)
313✔
419
        return BoseFS(parse.(Int, split(m.captures[1], r" +")))
102✔
420
    end
421
    # Single FermiFS
422
    m = match(r"\|([ ⋅↑]+)⟩", str)
109✔
423
    if !isnothing(m)
218✔
424
        chars = filter(!=(' '), Vector{Char}(m.captures[1]))
218✔
425
        return FermiFS(chars .== '↑')
109✔
426
    end
427
    throw(ArgumentError("invalid Fock state format \"$str\""))
×
428
end
429

430
"""
431
    fs"\$(string)"
432

433
Parse the compact representation of a Fock state.
434
Useful for copying the printout from a vector to the REPL.
435

436
# Example
437

438
```jldoctest
439
julia> DVec(BoseFS{3,4}(0, 1, 2, 0) => 1)
440
DVec{BoseFS{3, 4, BitString{6, 1, UInt8}},Int64} with 1 entry, style = IsStochasticInteger{Int64}()
441
  fs"|0 1 2 0⟩" => 1
442

443
julia> fs"|0 1 2 0⟩" => 1 # Copied from above printout
444
BoseFS{3,4}(0, 1, 2, 0) => 1
445

446
julia> fs"|1 2 3⟩⊗|0 1 0⟩" # composite bosonic Fock state
447
CompositeFS(
448
  BoseFS{6,3}(1, 2, 3),
449
  BoseFS{1,3}(0, 1, 0),
450
)
451

452
julia> fs"|↑↓↑⟩" # construct a fermionic Fock state
453
CompositeFS(
454
  FermiFS{2,3}(1, 0, 1),
455
  FermiFS{1,3}(0, 1, 0),
456
)
457

458
julia> s = fs"|0 1 2 0⟩{}" # constructing OccupationNumberFS with default UInt8 container
459
OccupationNumberFS{4, UInt8}(0, 1, 2, 0)
460

461
julia> [s] # prints out with the signifcant number of bits specified in braces
462
1-element Vector{OccupationNumberFS{4, UInt8}}:
463
 fs"|0 1 2 0⟩{8}"
464
```
465

466
See also [`FermiFS`](@ref), [`BoseFS`](@ref), [`CompositeFS`](@ref), [`FermiFS2C`](@ref),
467
[`OccupationNumberFS`](@ref).
468
"""
469
macro fs_str(str)
74✔
470
    return parse_address(str)
74✔
471
end
472

473
"""
474
    print_address(io::IO, address)
475

476
Print the `address` to `io`. If `get(io, :compact, false) == true`, the printed form should
477
be parsable by [`parse_address`](@ref).
478

479
This function is used to implement `Base.show` for [`AbstractFockAddress`](@ref).
480
"""
481
print_address
482

483
function Base.show(io::IO, add::AbstractFockAddress)
1,407✔
484
    if get(io, :typeinfo, nothing) == typeof(add) || get(io, :compact, false)
2,764✔
485
        print(io, "fs\"")
694✔
486
        print_address(io, add; compact=true)
694✔
487
        print(io, "\"")
694✔
488
    else
489
        print_address(io, add; compact=false)
713✔
490
    end
491
end
492

493
function onr_sparse_string(o)
90✔
494
    ps = map(p -> p[1] => p[2], Iterators.filter(p -> !iszero(p[2]), enumerate(o)))
19,362✔
495
    return join(ps, ", ")
90✔
496
end
497

498
###
499
### Boson stuff
500
###
501
"""
502
    BoseFSIndex
503

504
Struct used for indexing and performing [`excitation`](@ref)s on a [`BoseFS`](@ref).
505

506
## Fields:
507

508
* `occnum`: the occupation number.
509
* `mode`: the index of the mode.
510
* `offset`: the position of the mode in the address. This is the bit offset of the mode when
511
 the address is represented by a bitstring, and the position in the list when it is
512
 represented by `SortedParticleList`.
513

514
"""
515
Base.@kwdef struct BoseFSIndex<:FieldVector{3,Int}
2✔
516
    occnum::Int
439,171,782✔
517
    mode::Int
518
    offset::Int
519
end
520

521
function Base.show(io::IO, i::BoseFSIndex)
54✔
522
    @unpack occnum, mode, offset = i
54✔
523
    print(io, "BoseFSIndex(occnum=$occnum, mode=$mode, offset=$offset)")
54✔
524
end
525
Base.show(io::IO, ::MIME"text/plain", i::BoseFSIndex) = show(io, i)
9✔
526

527
"""
528
    BoseOccupiedModes{C,S<:BoseFS}
529

530
Iterator for occupied modes in [`BoseFS`](@ref). The definition of `iterate` is dispatched
531
on the storage type.
532

533
See [`occupied_modes`](@ref).
534

535
Defining `Base.length` and `Base.iterate` for this struct is a part of the interface for an
536
underlying storage format used by [`BoseFS`](@ref).
537
"""
538
struct BoseOccupiedModes{N,M,S}<:OccupiedModeIterator
539
    storage::S
499,483,487✔
540
end
UNCOV
541
Base.eltype(::BoseOccupiedModes) = BoseFSIndex
×
542

543
# Apply destruction operator to BoseFSIndex.
544
@inline _destroy(d, index::BoseFSIndex) = @set index.occnum -= (d.mode == index.mode)
1,150,662,581✔
545
@inline _destroy(d) = Base.Fix1(_destroy, d)
922,572,652✔
546
# Apply creation operator to BoseFSIndex.
547
@inline _create(c, index::BoseFSIndex) = @set index.occnum += (c.mode == index.mode)
229,792,536✔
548
@inline _create(c) = Base.Fix1(_create, c)
461,287,186✔
549

550
"""
551
    bose_excitation_value(
552
        creations::NTuple{_,BoseFSIndex}, destructions::NTuple{_,::BoseFSIndex}
553
    ) -> Int
554

555
Compute the squared value of an excitation from indices. Starts by applying all destruction
556
operators, and then applying all creation operators. The operators must be given in reverse
557
order. Will return 0 if move is illegal.
558
"""
UNCOV
559
@inline bose_excitation_value(::Tuple{}, ::Tuple{}) = 1
×
560
@inline function bose_excitation_value((c, cs...)::NTuple{<:Any,BoseFSIndex}, ::Tuple{})
461,287,095✔
561
    return bose_excitation_value(map(_create(c), cs), ()) * (c.occnum + 1)
461,287,143✔
562
end
563
@inline function bose_excitation_value(
461,286,763✔
564
    creations::NTuple{<:Any,BoseFSIndex}, (d, ds...)::NTuple{<:Any,BoseFSIndex}
565
)
566
    return bose_excitation_value(map(_destroy(d), creations), map(_destroy(d), ds)) * d.occnum
461,286,790✔
567
end
568

569
"""
570
    from_bose_onr(::Type{B}, onr::AbstractArray) -> B
571

572
Convert array `onr` to type `B`. It is safe to assume `onr` contains a valid
573
occupation-number representation array. The checks are preformed in the [`BoseFS`](@ref)
574
constructor.
575

576
This function is a part of the interface for an underlying storage format used by
577
[`BoseFS`](@ref).
578
"""
579
from_bose_onr
580

581
"""
582
    to_bose_onr(bs::B) -> SVector
583

584
Convert `bs` to a static vector in the occupation number representation format.
585

586
This function is a part of the interface for an underlying storage format used by
587
[`BoseFS`](@ref).
588
"""
589
to_bose_onr
590

591
"""
592
    bose_excitation(
593
        bs::B, creations::NTuple{N,BoseFSIndex}, destructions::NTuple{N,BoseFSIndex}
594
    ) -> Tuple{B,Float64}
595

596
Perform excitation as if `bs` was a bosonic address. See also
597
[`bose_excitation_value`](@ref).
598

599
This function is a part of the interface for an underlying storage format used by
600
[`BoseFS`](@ref).
601
"""
602
bose_excitation
603

604
"""
605
    bose_num_occupied_modes(bs::B)
606

607
Return the number of occupied modes, if `bs` represents a bosonic address.
608

609
This function is a part of the interface for an underlying storage format used by
610
[`BoseFS`](@ref).
611
"""
612
bose_num_occupied_modes
613

614
###
615
### Fermion stuff
616
###
617
"""
618
    FermiFSIndex
619

620
Struct used for indexing and performing [`excitation`](@ref)s on a [`FermiFS`](@ref).
621

622
## Fields:
623

624
* `occnum`: the occupation number.
625
* `mode`: the index of the mode.
626
* `offset`: the position of the mode in the address. This is `mode - 1` when the address is
627
  represented by a bitstring, and the position in the list when using `SortedParticleList`.
628

629
"""
630
Base.@kwdef struct FermiFSIndex<:FieldVector{3,Int}
631
    occnum::Int
70,846,097✔
632
    mode::Int
633
    offset::Int
634
end
635

636
function Base.show(io::IO, i::FermiFSIndex)
26✔
637
    @unpack occnum, mode, offset = i
26✔
638
    print(io, "FermiFSIndex(occnum=$occnum, mode=$mode, offset=$offset)")
26✔
639
end
640
Base.show(io::IO, ::MIME"text/plain", i::FermiFSIndex) = show(io, i)
6✔
641

642
"""
643
    FermiOccupiedModes{N,S<:BitString}
644

645
Iterator over occupied modes in address. `N` is the number of fermions. See [`occupied_modes`](@ref).
646
"""
647
struct FermiOccupiedModes{N,S}<:OccupiedModeIterator
648
    storage::S
10,653,234✔
649
end
650

651
Base.length(::FermiOccupiedModes{N}) where {N} = N
4✔
UNCOV
652
Base.eltype(::FermiOccupiedModes) = FermiFSIndex
×
653

654
"""
655
    from_fermi_onr(::Type{B}, onr) -> B
656

657
Convert array `onr` to type `B`. It is safe to assume `onr` contains a valid
658
occupation-number representation array. The checks are preformed in the [`FermiFS`](@ref)
659
constructor.
660

661
This function is a part of the interface for an underlying storage format used by
662
[`FermiFS`](@ref).
663
"""
664
from_fermi_onr
665

666
"""
667
    fermi_find_mode(bs::B, i::Integer) -> FermiFSIndex
668

669
Find `i`-th mode in `bs` if `bs` is a fermionic address. Should return an appropriately
670
formatted [`FermiFSIndex`](@ref).
671

672
This function is a part of the interface for an underlying storage format used by
673
[`FermiFS`](@ref).
674
"""
675
fermi_find_mode
676

677
"""
678
    fermi_excitation(
679
        bs::B, creations::NTuple{N,FermiFSIndex}, destructions::NTuple{N,FermiFSIndex}
680
    ) -> Tuple{B,Float64}
681

682
Perform excitation as if `bs` was a fermionic address.
683

684
This function is a part of the interface for an underlying storage format used by
685
[`FermiFS`](@ref).
686
"""
687
fermi_excitation
688

689
###
690
### General
691
###
692
function LinearAlgebra.dot(occ_a::OccupiedModeIterator, occ_b::OccupiedModeIterator)
147,812✔
693
    (n_a, i_a, _), st_a = iterate(occ_a)
295,624✔
694
    (n_b, i_b, _), st_b = iterate(occ_b)
295,624✔
695

696
    acc = 0
147,812✔
697
    while true
255,846✔
698
        if i_a > i_b
255,846✔
699
            # b is behind and needs to do a step
700
            iter = iterate(occ_b, st_b)
154,136✔
701
            isnothing(iter) && return acc
154,136✔
702
            (n_b, i_b, _), st_b = iter
53,988✔
703
        elseif i_a < i_b
155,698✔
704
            # a is behind and needs to do a step
705
            iter = iterate(occ_a, st_a)
154,196✔
706
            isnothing(iter) && return acc
154,196✔
707
            (n_a, i_a, _), st_a = iter
54,012✔
708
        else
709
            # a and b are at the same position
710
            acc += n_a * n_b
55,514✔
711
            # now both need to do a step
712
            iter = iterate(occ_a, st_a)
70,954✔
713
            isnothing(iter) && return acc
70,954✔
714
            (n_a, i_a, _), st_a = iter
15,440✔
715
            iter = iterate(occ_b, st_b)
15,474✔
716
            isnothing(iter) && return acc
15,474✔
717
            (n_b, i_b, _), st_b = iter
34✔
718
        end
719
    end
108,034✔
720
end
721

722
function sparse_to_onr(M, pairs)
632✔
723
    onr = spzeros(Int, M)
632✔
724
    for (k, v) in pairs
668✔
725
        v ≥ 0 || throw(ArgumentError("Invalid pair `$k=>$v`: particle number negative"))
2,011✔
726
        0 < k ≤ M || throw(ArgumentError("Invalid pair `$k => $v`: key of of range `1:$M`"))
2,009✔
727
        onr[k] += v
2,005✔
728
    end
2,429✔
729
    return onr
627✔
730
end
731

732
"""
733
    OccupiedPairsMap(addr::SingleComponentFockAddress) <: AbstractVector
734

735
Get a map of all distinct pairs of indices in `addr`. Pairs involving
736
multiply-occupied modes are counted once, (including self-pairing).
737
This is useful for cases where identifying pairs of particles for eg.
738
interactions is not well-defined or efficient to do on the fly.
739
This is an eager iterator whose elements are a tuple of particle indices that
740
can be given to `excitation`
741

742
# Example
743

744
```jldoctest
745
julia> addr = BoseFS(10, 0, 0, 0, 2, 0, 1)
746
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
747

748
julia> pairs = OccupiedPairsMap(addr)
749
5-element OccupiedPairsMap{78, Tuple{BoseFSIndex, BoseFSIndex}}:
750
 (BoseFSIndex(occnum=10, mode=1, offset=0), BoseFSIndex(occnum=10, mode=1, offset=0))
751
 (BoseFSIndex(occnum=2, mode=5, offset=14), BoseFSIndex(occnum=2, mode=5, offset=14))
752
 (BoseFSIndex(occnum=2, mode=5, offset=14), BoseFSIndex(occnum=10, mode=1, offset=0))
753
 (BoseFSIndex(occnum=1, mode=7, offset=18), BoseFSIndex(occnum=10, mode=1, offset=0))
754
 (BoseFSIndex(occnum=1, mode=7, offset=18), BoseFSIndex(occnum=2, mode=5, offset=14))
755

756
julia> excitation(addr, pairs[2], pairs[4])
757
(BoseFS{13,7}(9, 0, 0, 0, 4, 0, 0), 10.954451150103322)
758
```
759

760
See also [`OccupiedModeMap`](@ref).
761
"""
762
struct OccupiedPairsMap{N,T} <: AbstractVector{T}
763
    pairs::SVector{N,T}
64✔
764
    length::Int
765
end
766

767
function OccupiedPairsMap(addr::SingleComponentFockAddress{N}) where {N}
64✔
768
    omm = OccupiedModeMap(addr)
135✔
769
    T = eltype(omm)
64✔
770
    P = N * (N - 1) ÷ 2
64✔
771
    pairs = MVector{P,Tuple{T,T}}(undef)
64✔
772
    a = 0
64✔
773
    for i in eachindex(omm)
64✔
774
        p_i = omm[i]
135✔
775
        if p_i.occnum > 1
135✔
776
            a += 1
44✔
777
            @inbounds pairs[a] = (p_i, p_i)
44✔
778
        end
779
        for j in 1:i-1
135✔
780
            p_j = omm[j]
93✔
781
            a += 1
93✔
782
            @inbounds pairs[a] = (p_i, p_j)
93✔
783
        end
115✔
784
    end
206✔
785

786
    return OccupiedPairsMap(SVector(pairs), a)
64✔
787
end
788

789
Base.size(opm::OccupiedPairsMap) = (opm.length,)
1,341✔
790
function Base.getindex(opm::OccupiedPairsMap, i)
2,246✔
791
    @boundscheck 1 ≤ i ≤ opm.length || throw(BoundsError(opm, i))
2,246✔
792
    return opm.pairs[i]
2,246✔
793
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc