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

RimuQMC / Rimu.jl / 16221436519

11 Jul 2025 01:38PM UTC coverage: 94.473% (-0.08%) from 94.552%
16221436519

push

github

joachimbrand
remove more matrix tests

7025 of 7436 relevant lines covered (94.47%)

11631128.72 hits per line

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

96.3
/src/BitStringAddresses/fockaddress.jl
1
# AbstractFockAddress is defined in Interfaces/abstractfockaddress.jl, so we can use it here.
2
# So are num_particles, num_modes, num_components.
3

4
"""
5
    SingleComponentFockAddress{N,M} <: AbstractFockAddress{N,M}
6

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

9
Implemented subtypes: [`BoseFS`](@ref), [`FermiFS`](@ref).
10

11
# Supported functionality
12

13
* [`find_mode`](@ref)
14
* [`find_occupied_mode`](@ref)
15
* [`num_occupied_modes`](@ref)
16
* [`occupied_modes`](@ref): Lazy iterator.
17
* [`OccupiedModeMap`](@ref): `AbstractVector` with eager construction.
18
* [`excitation`](@ref): Create a new address.
19
* [`BoseFSIndex`](@ref) and [`FermiFSIndex`](@ref) for indexing.
20

21
See also [`CompositeFS`](@ref), [`AbstractFockAddress`](@ref).
22
"""
23
abstract type SingleComponentFockAddress{N,M} <: AbstractFockAddress{N,M} end
24

25
Interfaces.num_components(::Type{<:SingleComponentFockAddress}) = 1
×
26

27
"""
28
    occupation_number_representation(fs::SingleComponentFockAddress)
29
    onr(fs::SingleComponentFockAddress)
30

31
Compute and return the occupation number representation of the Fock state `fs` as an
32
`SVector{M}`, where `M` is the number of modes.
33
"""
34
onr
35

36
"""
37
    find_mode(::SingleComponentFockAddress, i)
38

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

43
```jldoctest
44
julia> find_mode(BoseFS(1, 0, 2), 2)
45
BoseFSIndex(occnum=0, mode=2, offset=2)
46

47
julia> find_mode(FermiFS(1, 1, 1, 0), (2,3))
48
(FermiFSIndex(occnum=1, mode=2, offset=1), FermiFSIndex(occnum=1, mode=3, offset=2))
49
```
50

51
See [`SingleComponentFockAddress`](@ref).
52
"""
53
find_mode
54

55
"""
56
    find_occupied_mode(::SingleComponentFockAddress, k)
57
    find_occupied_mode(::BoseFS, k, [n])
58

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

63
# Example
64

65
```jldoctest
66
julia> find_occupied_mode(FermiFS(1, 1, 1, 0), 2)
67
FermiFSIndex(occnum=1, mode=2, offset=1)
68

69
julia> find_occupied_mode(BoseFS(1, 0, 2), 1)
70
BoseFSIndex(occnum=1, mode=1, offset=0)
71

72
julia> find_occupied_mode(BoseFS(1, 0, 2), 1, 2)
73
BoseFSIndex(occnum=2, mode=3, offset=3)
74
```
75

76
See also [`occupied_modes`](@ref), [`OccupiedModeMap`](@ref),
77
[`SingleComponentFockAddress`](@ref).
78
"""
79
function find_occupied_mode(b::SingleComponentFockAddress, index::Integer, n=1)
125,907,526✔
80
    mode_iterator = occupied_modes(b)
206,617,247✔
81
    T = eltype(mode_iterator)
63,015,962✔
82
    for (occnum, mode, offset) in mode_iterator
125,728,381✔
83
        index -= occnum ≥ n
144,967,466✔
84
        if index == 0
144,941,573✔
85
            return T(occnum, mode, offset)
63,025,297✔
86
        end
87
    end
164,376,043✔
88
    return T(0, 0, 0)
×
89
end
90

91
"""
92
    num_occupied_modes(::SingleComponentFockAddress)
93

94
Get the number of occupied modes in address. Equivalent to
95
`length(`[`occupied_modes`](@ref)`(address))`, or the number of non-zeros in its ONR
96
representation.
97

98
# Example
99

100
```jldoctest
101
julia> num_occupied_modes(BoseFS((1, 0, 2)))
102
2
103
julia> num_occupied_modes(FermiFS((1, 1, 1, 0)))
104
3
105
```
106

107
See [`SingleComponentFockAddress`](@ref).
108
"""
109
num_occupied_modes
110

111
"""
112
    occupied_modes(::SingleComponentFockAddress)
113

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

118
# Example
119

120
```jldoctest
121
julia> b = BoseFS((1,5,0,4));
122

123
julia> foreach(println, occupied_modes(b))
124
BoseFSIndex(occnum=1, mode=1, offset=0)
125
BoseFSIndex(occnum=5, mode=2, offset=2)
126
BoseFSIndex(occnum=4, mode=4, offset=9)
127
```
128

129
```jldoctest
130
julia> f = FermiFS((1,1,0,1,0,0,1));
131

132
julia> foreach(println, occupied_modes(f))
133
FermiFSIndex(occnum=1, mode=1, offset=0)
134
FermiFSIndex(occnum=1, mode=2, offset=1)
135
FermiFSIndex(occnum=1, mode=4, offset=3)
136
FermiFSIndex(occnum=1, mode=7, offset=6)
137
```
138
See also [`find_occupied_mode`](@ref),
139
[`SingleComponentFockAddress`](@ref).
140
"""
141
occupied_modes
142

143
"""
144
    excitation(addr::SingleComponentFockAddress, creations::NTuple, destructions::NTuple)
145

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

150
```math
151
a^†_{c_1} a^†_{c_2} \\ldots a_{d_1} a_{d_2} \\ldots |\\mathrm{addr}\\rangle \\to
152
α|\\mathrm{naddr}\\rangle
153
```
154

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

159
# Example
160

161
```jldoctest
162
julia> f = FermiFS(1,1,0,0,1,1,1,1)
163
FermiFS{6,8}(1, 1, 0, 0, 1, 1, 1, 1)
164

165
julia> i, j, k, l = find_mode(f, (3,4,2,5))
166
(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))
167

168
julia> excitation(f, (i,j), (k,l))
169
(FermiFS{6,8}(1, 0, 1, 1, 0, 1, 1, 1), -1.0)
170
```
171

172
See [`SingleComponentFockAddress`](@ref).
173
"""
174
excitation
175

176
"""
177
    OccupiedModeMap(addr) <: AbstractVector
178

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

182
`OccupiedModeMap(addr)[i]` contains the index for the `i`-th occupied mode.
183
This is useful because repeatedly looking for occupied modes with
184
[`find_occupied_mode`](@ref) can be time-consuming.
185
`OccupiedModeMap(addr)` is an eager version of the iterator returned by
186
[`occupied_modes`](@ref). It is similar to [`onr`](@ref) but contains more information.
187

188
# Example
189

190
```jldoctest
191
julia> b = BoseFS(10, 0, 0, 0, 2, 0, 1)
192
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
193

194
julia> mb = OccupiedModeMap(b)
195
3-element OccupiedModeMap{7, BoseFSIndex}:
196
 BoseFSIndex(occnum=10, mode=1, offset=0)
197
 BoseFSIndex(occnum=2, mode=5, offset=14)
198
 BoseFSIndex(occnum=1, mode=7, offset=18)
199

200
julia> f = FermiFS(1,1,1,1,0,0,1,0,0)
201
FermiFS{5,9}(1, 1, 1, 1, 0, 0, 1, 0, 0)
202

203
julia> mf = OccupiedModeMap(f)
204
5-element OccupiedModeMap{5, FermiFSIndex}:
205
 FermiFSIndex(occnum=1, mode=1, offset=0)
206
 FermiFSIndex(occnum=1, mode=2, offset=1)
207
 FermiFSIndex(occnum=1, mode=3, offset=2)
208
 FermiFSIndex(occnum=1, mode=4, offset=3)
209
 FermiFSIndex(occnum=1, mode=7, offset=6)
210

211
julia> mf == collect(occupied_modes(f))
212
true
213

214
julia> dot(mf, mb)
215
11
216

217
julia> dot(mf, 1:20)
218
17
219
```
220
See also [`dot`](@ref Main.Hamiltonians.dot), [`SingleComponentFockAddress`](@ref).
221
"""
222
struct OccupiedModeMap{N,T} <: AbstractVector{T}
223
    indices::SVector{N,T} # N = min(N, M)
97,427,854✔
224
    length::Int
225
end
226

227
function OccupiedModeMap(addr::SingleComponentFockAddress{N,M}) where {N,M}
97,427,649✔
228
    modes = occupied_modes(addr)
97,427,651✔
229
    T = eltype(modes)
97,427,684✔
230
    # There are at most N occupied modes. This could be also @generated for cases where N ≫ M
231
    L = ismissing(N) ? M : min(N, M)
97,427,686✔
232
    indices = MVector{L,T}(undef)
97,427,701✔
233
    i = 0
97,427,548✔
234
    for index in modes
194,854,693✔
235
        i += 1
407,165,096✔
236
        @inbounds indices[i] = index
407,164,897✔
237
    end
716,901,529✔
238
    return OccupiedModeMap(SVector(indices), i)
97,427,716✔
239
end
240

241
Base.size(om::OccupiedModeMap) = (om.length,)
439,837,894✔
242
function Base.getindex(om::OccupiedModeMap, i)
265,700,326✔
243
    @boundscheck 1 ≤ i ≤ om.length || throw(BoundsError(om, i))
265,771,666✔
244
    return om.indices[i]
265,857,361✔
245
end
246

247
"""
248
    abstract type OccupiedModeIterator
249

250
Iterator over occupied modes with `eltype` [`BoseFSIndex`](@ref) or
251
[`FermiFSIndex`](@ref). A subtype of this should be returned when calling
252
[`occupied_modes`](@ref) on a Fock state.
253
"""
254
abstract type OccupiedModeIterator end
255

256
"""
257
    dot(map::OccupiedModeMap, vec::AbstractVector)
258
    dot(map1::OccupiedModeMap, map2::OccupiedModeMap)
259
Dot product extracting mode occupation numbers from an [`OccupiedModeMap`](@ref) similar
260
to [`onr`](@ref).
261

262
```jldoctest
263
julia> b = BoseFS(10, 0, 0, 0, 2, 0, 1)
264
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
265

266
julia> mb = OccupiedModeMap(b)
267
3-element OccupiedModeMap{7, BoseFSIndex}:
268
 BoseFSIndex(occnum=10, mode=1, offset=0)
269
 BoseFSIndex(occnum=2, mode=5, offset=14)
270
 BoseFSIndex(occnum=1, mode=7, offset=18)
271

272
julia> dot(mb, 1:7)
273
27
274

275
julia> mb⋅(1:7) == onr(b)⋅(1:7)
276
true
277
```
278
See also [`SingleComponentFockAddress`](@ref).
279
"""
280
function LinearAlgebra.dot(map::OccupiedModeMap, vec::AbstractVector)
24,946,806✔
281
    value = zero(eltype(vec))
24,946,805✔
282
    for index in map
49,893,526✔
283
        value += vec[index.mode] * index.occnum
127,402,598✔
284
    end
229,858,306✔
285
    return value
24,946,841✔
286
end
287
LinearAlgebra.dot(vec::AbstractVector, map::OccupiedModeMap) = dot(map, vec)
127,402,588✔
288

289
# Defined for consistency. Could also be used to compute cross-component interactions in
290
# real space.
291
function LinearAlgebra.dot(map1::OccupiedModeMap, map2::OccupiedModeMap)
1✔
292
    i = j = 1
1✔
293
    value = 0
1✔
294
    while i ≤ length(map1) && j ≤ length(map2)
7✔
295
        index1 = map1[i]
6✔
296
        index2 = map2[j]
6✔
297
        if index1.mode == index2.mode
6✔
298
            value += index1.occnum * index2.occnum
2✔
299
            i += 1
2✔
300
            j += 1
2✔
301
        elseif index1.mode < index2.mode
4✔
302
            i += 1
3✔
303
        else
304
            j += 1
1✔
305
        end
306
    end
6✔
307
    return value
1✔
308
end
309

310
"""
311
    parse_address(str)
312

313
Parse the compact representation of a Fock state address.
314
"""
315
function parse_address(str)
296✔
316
    # CompositeFS
317
    m = match(r"⊗", str)
296✔
318
    if !isnothing(m)
303✔
319
        if !isnothing(match(r"[↓⇅]", str))
9✔
320
            throw(ArgumentError("invalid fock state format \"$str\""))
2✔
321
        else
322
            return CompositeFS(map(parse_address, split(str, r" *⊗ *"))...)
5✔
323
        end
324
    end
325
    # FermiFS2C
326
    m = match(r"[↓⇅]", str)
289✔
327
    if !isnothing(m)
310✔
328
        m = match(r"\|([↑↓⇅⋅ ]+)⟩", str)
21✔
329
        if isnothing(m)
42✔
330
            throw(ArgumentError("invalid fock state format \"$str\""))
×
331
        else
332
            chars = filter(!=(' '), Vector{Char}(m.captures[1]))
42✔
333
            f1 = FermiFS((chars .== '↑') .| (chars .== '⇅'))
21✔
334
            f2 = FermiFS((chars .== '↓') .| (chars .== '⇅'))
21✔
335
            return CompositeFS(f1, f2)
21✔
336
        end
337
    end
338
    # Sparse BoseFS
339
    m = match(r"\|b *([0-9]+): *([ 0-9]+)⟩", str)
268✔
340
    if !isnothing(m)
280✔
341
        particles = parse.(Int, filter(!isempty, split(m.captures[2], r" +")))
12✔
342
        return BoseFS(parse(Int, m.captures[1]), zip(particles, fill(1, length(particles))))
12✔
343
    end
344
    # Sparse FermiFS
345
    m = match(r"\|f *([0-9]+): *([ 0-9]+)⟩", str)
256✔
346
    if !isnothing(m)
278✔
347
        particles = parse.(Int, filter(!isempty, split(m.captures[2], r" +")))
22✔
348
        return FermiFS(parse(Int, m.captures[1]), zip(particles, fill(1, length(particles))))
22✔
349
    end
350
    # OccupationNumberFS
351
    m = match(r"\|([ 0-9]+)⟩{[0-9]*}", str)
234✔
352
    if !isnothing(m)
247✔
353
        m2 = match(r"{([0-9]+)}", str)
13✔
354
        if isnothing(m2) # empty braces defaults to UInt8
24✔
355
            BITS = 8
2✔
356
        else
357
            BITS = parse(Int, m2.captures[1])
11✔
358
        end
359
        T = if BITS ≤ 8
13✔
360
            UInt8
8✔
361
        elseif BITS ≤ 16
5✔
362
            UInt16
1✔
363
        elseif BITS ≤ 32
4✔
364
            UInt32
1✔
365
        elseif BITS ≤ 64
3✔
366
            UInt64
1✔
367
        elseif BITS ≤ 128
2✔
368
            UInt128
1✔
369
        else
370
            throw(ArgumentError("invalid Fock state format \"$str\""))
22✔
371
        end
372
        t = Tuple(parse.(T, split(m.captures[1], r" +")))
12✔
373
        return OccupationNumberFS(SVector(t))
12✔
374
    end
375
    m = match(r"\|([ 0-9]+)⟩{", str) # anything else that has a curly brace
221✔
376
    if !isnothing(m)
222✔
377
        throw(ArgumentError("invalid Fock state format \"$str\""))
1✔
378
    end
379

380
    # BoseFS
381
    m = match(r"\|([ 0-9]+)⟩", str)
220✔
382
    if !isnothing(m)
324✔
383
        return BoseFS(parse.(Int, split(m.captures[1], r" +")))
104✔
384
    end
385
    # Single FermiFS
386
    m = match(r"\|([ ⋅↑]+)⟩", str)
116✔
387
    if !isnothing(m)
232✔
388
        chars = filter(!=(' '), Vector{Char}(m.captures[1]))
232✔
389
        return FermiFS(chars .== '↑')
116✔
390
    end
391
    throw(ArgumentError("invalid Fock state format \"$str\""))
×
392
end
393

394
"""
395
    fs"\$(string)"
396

397
Parse the compact representation of a Fock state.
398
Useful for copying the printout from a vector to the REPL.
399

400
# Example
401

402
```jldoctest
403
julia> DVec(BoseFS{3,4}(0, 1, 2, 0) => 1)
404
DVec{BoseFS{3, 4, BitString{6, 1, UInt8}},Int64} with 1 entry, style = IsStochasticInteger{Int64}()
405
  fs"|0 1 2 0⟩" => 1
406

407
julia> fs"|0 1 2 0⟩" => 1 # Copied from above printout
408
BoseFS{3,4}(0, 1, 2, 0) => 1
409

410
julia> fs"|1 2 3⟩⊗|0 1 0⟩" # composite bosonic Fock state
411
CompositeFS(
412
  BoseFS{6,3}(1, 2, 3),
413
  BoseFS{1,3}(0, 1, 0),
414
)
415

416
julia> fs"|↑↓↑⟩" # construct a fermionic Fock state
417
CompositeFS(
418
  FermiFS{2,3}(1, 0, 1),
419
  FermiFS{1,3}(0, 1, 0),
420
)
421

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

425
julia> [s] # prints out with the signifcant number of bits specified in braces
426
1-element Vector{OccupationNumberFS{4, UInt8}}:
427
 fs"|0 1 2 0⟩{8}"
428
```
429

430
See also [`FermiFS`](@ref), [`BoseFS`](@ref), [`CompositeFS`](@ref), [`FermiFS2C`](@ref),
431
[`OccupationNumberFS`](@ref).
432
"""
433
macro fs_str(str)
91✔
434
    return parse_address(str)
91✔
435
end
436

437
"""
438
    print_address(io::IO, address)
439

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

443
This function is used to implement `Base.show` for [`AbstractFockAddress`](@ref).
444
"""
445
print_address
446

447
function Base.show(io::IO, add::AbstractFockAddress)
1,283✔
448
    if get(io, :typeinfo, nothing) == typeof(add) || get(io, :compact, false)
2,501✔
449
        print(io, "fs\"")
663✔
450
        print_address(io, add; compact=true)
663✔
451
        print(io, "\"")
663✔
452
    else
453
        print_address(io, add; compact=false)
620✔
454
    end
455
end
456

457
function onr_sparse_string(o)
90✔
458
    ps = map(p -> p[1] => p[2], Iterators.filter(p -> !iszero(p[2]), enumerate(o)))
19,350✔
459
    return join(ps, ", ")
90✔
460
end
461

462
###
463
### Boson stuff
464
###
465
"""
466
    BoseFSIndex
467

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

470
## Fields:
471

472
* `occnum`: the occupation number.
473
* `mode`: the index of the mode.
474
* `offset`: the position of the mode in the address. This is the bit offset of the mode when
475
 the address is represented by a bitstring, and the position in the list when it is
476
 represented by `SortedParticleList`.
477

478
"""
479
Base.@kwdef struct BoseFSIndex<:FieldVector{3,Int}
2✔
480
    occnum::Int
1,450,932,338✔
481
    mode::Int
482
    offset::Int
483
end
484

485
function Base.show(io::IO, i::BoseFSIndex)
54✔
486
    @unpack occnum, mode, offset = i
54✔
487
    print(io, "BoseFSIndex(occnum=$occnum, mode=$mode, offset=$offset)")
54✔
488
end
489
Base.show(io::IO, ::MIME"text/plain", i::BoseFSIndex) = show(io, i)
9✔
490

491
"""
492
    BoseOccupiedModes{C,S<:BoseFS}
493

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

497
See [`occupied_modes`](@ref).
498

499
Defining `Base.length` and `Base.iterate` for this struct is a part of the interface for an
500
underlying storage format used by [`BoseFS`](@ref).
501
"""
502
struct BoseOccupiedModes{N,M,S}<:OccupiedModeIterator
503
    storage::S
495,412,068✔
504
end
505
Base.eltype(::BoseOccupiedModes) = BoseFSIndex
×
506

507
# Apply destruction operator to BoseFSIndex.
508
@inline _destroy(d, index::BoseFSIndex) = @set index.occnum -= (d.mode == index.mode)
1,263,601,062✔
509
@inline _destroy(d) = Base.Fix1(_destroy, d)
1,084,257,157✔
510
# Apply creation operator to BoseFSIndex.
511
@inline _create(c, index::BoseFSIndex) = @set index.occnum += (c.mode == index.mode)
244,639,844✔
512
@inline _create(c) = Base.Fix1(_create, c)
550,074,135✔
513

514
"""
515
    bose_excitation_value(
516
        creations::NTuple{_,BoseFSIndex}, destructions::NTuple{_,::BoseFSIndex}
517
    ) -> Int
518

519
Compute the squared value of an excitation from indices. Starts by applying all destruction
520
operators, and then applying all creation operators. The operators must be given in reverse
521
order. Will return 0 if move is illegal.
522
"""
523
@inline bose_excitation_value(::Tuple{}, ::Tuple{}) = 1
×
524
@inline function bose_excitation_value((c, cs...)::NTuple{<:Any,BoseFSIndex}, ::Tuple{})
548,845,054✔
525
    return bose_excitation_value(map(_create(c), cs), ()) * (c.occnum + 1)
548,908,010✔
526
end
527
@inline function bose_excitation_value(
545,570,238✔
528
    creations::NTuple{<:Any,BoseFSIndex}, (d, ds...)::NTuple{<:Any,BoseFSIndex}
529
)
530
    return bose_excitation_value(map(_destroy(d), creations), map(_destroy(d), ds)) * d.occnum
546,265,598✔
531
end
532

533
"""
534
    from_bose_onr(::Type{B}, onr::AbstractArray) -> B
535

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

540
This function is a part of the interface for an underlying storage format used by
541
[`BoseFS`](@ref).
542
"""
543
from_bose_onr
544

545
"""
546
    to_bose_onr(bs::B) -> SVector
547

548
Convert `bs` to a static vector in the occupation number representation format.
549

550
This function is a part of the interface for an underlying storage format used by
551
[`BoseFS`](@ref).
552
"""
553
to_bose_onr
554

555
"""
556
    bose_excitation(
557
        bs::B, creations::NTuple{N,BoseFSIndex}, destructions::NTuple{N,BoseFSIndex}
558
    ) -> Tuple{B,Float64}
559

560
Perform excitation as if `bs` was a bosonic address. See also
561
[`bose_excitation_value`](@ref).
562

563
This function is a part of the interface for an underlying storage format used by
564
[`BoseFS`](@ref).
565
"""
566
bose_excitation
567

568
"""
569
    bose_num_occupied_modes(bs::B)
570

571
Return the number of occupied modes, if `bs` represents a bosonic address.
572

573
This function is a part of the interface for an underlying storage format used by
574
[`BoseFS`](@ref).
575
"""
576
bose_num_occupied_modes
577

578
###
579
### Fermion stuff
580
###
581
"""
582
    FermiFSIndex
583

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

586
## Fields:
587

588
* `occnum`: the occupation number.
589
* `mode`: the index of the mode.
590
* `offset`: the position of the mode in the address. This is `mode - 1` when the address is
591
  represented by a bitstring, and the position in the list when using `SortedParticleList`.
592

593
"""
594
Base.@kwdef struct FermiFSIndex<:FieldVector{3,Int}
595
    occnum::Int
71,861,343✔
596
    mode::Int
597
    offset::Int
598
end
599

600
function Base.show(io::IO, i::FermiFSIndex)
26✔
601
    @unpack occnum, mode, offset = i
26✔
602
    print(io, "FermiFSIndex(occnum=$occnum, mode=$mode, offset=$offset)")
26✔
603
end
604
Base.show(io::IO, ::MIME"text/plain", i::FermiFSIndex) = show(io, i)
6✔
605

606
"""
607
    FermiOccupiedModes{N,S<:BitString}
608

609
Iterator over occupied modes in address. `N` is the number of fermions. See [`occupied_modes`](@ref).
610
"""
611
struct FermiOccupiedModes{N,S}<:OccupiedModeIterator
612
    storage::S
10,982,495✔
613
end
614

615
Base.length(::FermiOccupiedModes{N}) where {N} = N
3✔
616
Base.eltype(::FermiOccupiedModes) = FermiFSIndex
×
617

618
"""
619
    from_fermi_onr(::Type{B}, onr) -> B
620

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

625
This function is a part of the interface for an underlying storage format used by
626
[`FermiFS`](@ref).
627
"""
628
from_fermi_onr
629

630
"""
631
    fermi_find_mode(bs::B, i::Integer) -> FermiFSIndex
632

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

636
This function is a part of the interface for an underlying storage format used by
637
[`FermiFS`](@ref).
638
"""
639
fermi_find_mode
640

641
"""
642
    fermi_excitation(
643
        bs::B, creations::NTuple{N,FermiFSIndex}, destructions::NTuple{N,FermiFSIndex}
644
    ) -> Tuple{B,Float64}
645

646
Perform excitation as if `bs` was a fermionic address.
647

648
This function is a part of the interface for an underlying storage format used by
649
[`FermiFS`](@ref).
650
"""
651
fermi_excitation
652

653
###
654
### General
655
###
656
function LinearAlgebra.dot(occ_a::OccupiedModeIterator, occ_b::OccupiedModeIterator)
149,758✔
657
    (n_a, i_a, _), st_a = iterate(occ_a)
299,516✔
658
    (n_b, i_b, _), st_b = iterate(occ_b)
299,516✔
659

660
    acc = 0
149,758✔
661
    while true
258,919✔
662
        if i_a > i_b
258,919✔
663
            # b is behind and needs to do a step
664
            iter = iterate(occ_b, st_b)
155,081✔
665
            isnothing(iter) && return acc
155,081✔
666
            (n_b, i_b, _), st_b = iter
54,258✔
667
        elseif i_a < i_b
158,096✔
668
            # a is behind and needs to do a step
669
            iter = iterate(occ_a, st_a)
155,793✔
670
            isnothing(iter) && return acc
155,793✔
671
            (n_a, i_a, _), st_a = iter
54,644✔
672
        else
673
            # a and b are at the same position
674
            acc += n_a * n_b
56,947✔
675
            # now both need to do a step
676
            iter = iterate(occ_a, st_a)
72,954✔
677
            isnothing(iter) && return acc
72,954✔
678
            (n_a, i_a, _), st_a = iter
16,007✔
679
            iter = iterate(occ_b, st_b)
16,266✔
680
            isnothing(iter) && return acc
16,266✔
681
            (n_b, i_b, _), st_b = iter
259✔
682
        end
683
    end
109,161✔
684
end
685

686
function sparse_to_onr(M, pairs)
643✔
687
    onr = spzeros(Int, M)
643✔
688
    for (k, v) in pairs
681✔
689
        v ≥ 0 || throw(ArgumentError("Invalid pair `$k=>$v`: particle number negative"))
2,035✔
690
        0 < k ≤ M || throw(ArgumentError("Invalid pair `$k => $v`: key of of range `1:$M`"))
2,033✔
691
        onr[k] += v
2,029✔
692
    end
2,463✔
693
    return onr
638✔
694
end
695

696
"""
697
    OccupiedPairsMap(addr::SingleComponentFockAddress) <: AbstractVector
698

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

706
# Example
707

708
```jldoctest
709
julia> addr = BoseFS(10, 0, 0, 0, 2, 0, 1)
710
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
711

712
julia> pairs = OccupiedPairsMap(addr)
713
5-element OccupiedPairsMap{78, Tuple{BoseFSIndex, BoseFSIndex}}:
714
 (BoseFSIndex(occnum=10, mode=1, offset=0), BoseFSIndex(occnum=10, mode=1, offset=0))
715
 (BoseFSIndex(occnum=2, mode=5, offset=14), BoseFSIndex(occnum=2, mode=5, offset=14))
716
 (BoseFSIndex(occnum=2, mode=5, offset=14), BoseFSIndex(occnum=10, mode=1, offset=0))
717
 (BoseFSIndex(occnum=1, mode=7, offset=18), BoseFSIndex(occnum=10, mode=1, offset=0))
718
 (BoseFSIndex(occnum=1, mode=7, offset=18), BoseFSIndex(occnum=2, mode=5, offset=14))
719

720
julia> excitation(addr, pairs[2], pairs[4])
721
(BoseFS{13,7}(9, 0, 0, 0, 4, 0, 0), 10.954451150103322)
722
```
723

724
See also [`OccupiedModeMap`](@ref).
725
"""
726
struct OccupiedPairsMap{N,T} <: AbstractVector{T}
727
    pairs::SVector{N,T}
66✔
728
    length::Int
729
end
730

731
function OccupiedPairsMap(addr::SingleComponentFockAddress{N}) where {N}
66✔
732
    omm = OccupiedModeMap(addr)
137✔
733
    T = eltype(omm)
66✔
734
    P = N * (N - 1) ÷ 2
66✔
735
    pairs = MVector{P,Tuple{T,T}}(undef)
66✔
736
    a = 0
66✔
737
    for i in eachindex(omm)
66✔
738
        p_i = omm[i]
137✔
739
        if p_i.occnum > 1
137✔
740
            a += 1
46✔
741
            @inbounds pairs[a] = (p_i, p_i)
46✔
742
        end
743
        for j in 1:i-1
137✔
744
            p_j = omm[j]
93✔
745
            a += 1
93✔
746
            @inbounds pairs[a] = (p_i, p_j)
93✔
747
        end
115✔
748
    end
208✔
749

750
    return OccupiedPairsMap(SVector(pairs), a)
66✔
751
end
752

753
Base.size(opm::OccupiedPairsMap) = (opm.length,)
1,348✔
754
function Base.getindex(opm::OccupiedPairsMap, i)
2,251✔
755
    @boundscheck 1 ≤ i ≤ opm.length || throw(BoundsError(opm, i))
2,251✔
756
    return opm.pairs[i]
2,251✔
757
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