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

RimuQMC / Rimu.jl / 16167774480

09 Jul 2025 11:12AM UTC coverage: 94.552% (-0.02%) from 94.571%
16167774480

Pull #329

github

web-flow
Merge f429b5430 into 358376721
Pull Request #329: Operator traits for iterable offdiagonals and random offdiagonal

64 of 69 new or added lines in 5 files covered. (92.75%)

11 existing lines in 2 files now uncovered.

7046 of 7452 relevant lines covered (94.55%)

11627013.83 hits per line

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

96.39
/src/BitStringAddresses/fockaddress.jl
1
# AbstractFockAddress is defined in Interfaces.jl, so we can use it here.
2

3
"""
4
    num_particles(::Type{<:AbstractFockAddress})
5
    num_particles(::AbstractFockAddress)
6

7
Number of particles represented by address.
8
"""
9
num_particles(a::AbstractFockAddress) = num_particles(typeof(a))
30,902✔
10
num_particles(::Type{<:AbstractFockAddress{N}}) where {N} = N
1,259,920✔
11

12
"""
13
    num_modes(::Type{<:AbstractFockAddress})
14
    num_modes(::AbstractFockAddress)
15

16
Number of modes represented by address.
17
"""
18
num_modes(a::AbstractFockAddress) = num_modes(typeof(a))
485,978,649✔
19
num_modes(::Type{<:AbstractFockAddress{<:Any,M}}) where {M} = M
485,689,883✔
20

21
"""
22
    num_components(::Type{<:AbstractFockAddress})
23
    num_components(::AbstractFockAddress)
24

25
Number of components in address.
26
"""
27
num_components(b::AbstractFockAddress) = num_components(typeof(b))
65,568✔
28

29
"""
30
    SingleComponentFockAddress{N,M} <: AbstractFockAddress{N,M}
31

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

34
Implemented subtypes: [`BoseFS`](@ref), [`FermiFS`](@ref).
35

36
# Supported functionality
37

38
* [`find_mode`](@ref)
39
* [`find_occupied_mode`](@ref)
40
* [`num_occupied_modes`](@ref)
41
* [`occupied_modes`](@ref): Lazy iterator.
42
* [`OccupiedModeMap`](@ref): `AbstractVector` with eager construction.
43
* [`excitation`](@ref): Create a new address.
44
* [`BoseFSIndex`](@ref) and [`FermiFSIndex`](@ref) for indexing.
45

46
See also [`CompositeFS`](@ref), [`AbstractFockAddress`](@ref).
47
"""
48
abstract type SingleComponentFockAddress{N,M} <: AbstractFockAddress{N,M} end
49

UNCOV
50
num_components(::Type{<:SingleComponentFockAddress}) = 1
×
51

52
"""
53
    occupation_number_representation(fs::SingleComponentFockAddress)
54
    onr(fs::SingleComponentFockAddress)
55

56
Compute and return the occupation number representation of the Fock state `fs` as an
57
`SVector{M}`, where `M` is the number of modes.
58
"""
59
onr
60

61
"""
62
    find_mode(::SingleComponentFockAddress, i)
63

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

68
```jldoctest
69
julia> find_mode(BoseFS(1, 0, 2), 2)
70
BoseFSIndex(occnum=0, mode=2, offset=2)
71

72
julia> find_mode(FermiFS(1, 1, 1, 0), (2,3))
73
(FermiFSIndex(occnum=1, mode=2, offset=1), FermiFSIndex(occnum=1, mode=3, offset=2))
74
```
75

76
See [`SingleComponentFockAddress`](@ref).
77
"""
78
find_mode
79

80
"""
81
    find_occupied_mode(::SingleComponentFockAddress, k)
82
    find_occupied_mode(::BoseFS, k, [n])
83

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

88
# Example
89

90
```jldoctest
91
julia> find_occupied_mode(FermiFS(1, 1, 1, 0), 2)
92
FermiFSIndex(occnum=1, mode=2, offset=1)
93

94
julia> find_occupied_mode(BoseFS(1, 0, 2), 1)
95
BoseFSIndex(occnum=1, mode=1, offset=0)
96

97
julia> find_occupied_mode(BoseFS(1, 0, 2), 1, 2)
98
BoseFSIndex(occnum=2, mode=3, offset=3)
99
```
100

101
See also [`occupied_modes`](@ref), [`OccupiedModeMap`](@ref),
102
[`SingleComponentFockAddress`](@ref).
103
"""
104
function find_occupied_mode(b::SingleComponentFockAddress, index::Integer, n=1)
125,998,479✔
105
    mode_iterator = occupied_modes(b)
206,868,906✔
106
    T = eltype(mode_iterator)
63,067,898✔
107
    for (occnum, mode, offset) in mode_iterator
125,677,328✔
108
        index -= occnum ≥ n
145,060,103✔
109
        if index == 0
145,018,813✔
110
            return T(occnum, mode, offset)
63,040,791✔
111
        end
112
    end
164,573,620✔
UNCOV
113
    return T(0, 0, 0)
×
114
end
115

116
"""
117
    num_occupied_modes(::SingleComponentFockAddress)
118

119
Get the number of occupied modes in address. Equivalent to
120
`length(`[`occupied_modes`](@ref)`(address))`, or the number of non-zeros in its ONR
121
representation.
122

123
# Example
124

125
```jldoctest
126
julia> num_occupied_modes(BoseFS((1, 0, 2)))
127
2
128
julia> num_occupied_modes(FermiFS((1, 1, 1, 0)))
129
3
130
```
131

132
See [`SingleComponentFockAddress`](@ref).
133
"""
134
num_occupied_modes
135

136
"""
137
    occupied_modes(::SingleComponentFockAddress)
138

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

143
# Example
144

145
```jldoctest
146
julia> b = BoseFS((1,5,0,4));
147

148
julia> foreach(println, occupied_modes(b))
149
BoseFSIndex(occnum=1, mode=1, offset=0)
150
BoseFSIndex(occnum=5, mode=2, offset=2)
151
BoseFSIndex(occnum=4, mode=4, offset=9)
152
```
153

154
```jldoctest
155
julia> f = FermiFS((1,1,0,1,0,0,1));
156

157
julia> foreach(println, occupied_modes(f))
158
FermiFSIndex(occnum=1, mode=1, offset=0)
159
FermiFSIndex(occnum=1, mode=2, offset=1)
160
FermiFSIndex(occnum=1, mode=4, offset=3)
161
FermiFSIndex(occnum=1, mode=7, offset=6)
162
```
163
See also [`find_occupied_mode`](@ref),
164
[`SingleComponentFockAddress`](@ref).
165
"""
166
occupied_modes
167

168
"""
169
    excitation(addr::SingleComponentFockAddress, creations::NTuple, destructions::NTuple)
170

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

175
```math
176
a^†_{c_1} a^†_{c_2} \\ldots a_{d_1} a_{d_2} \\ldots |\\mathrm{addr}\\rangle \\to
177
α|\\mathrm{naddr}\\rangle
178
```
179

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

184
# Example
185

186
```jldoctest
187
julia> f = FermiFS(1,1,0,0,1,1,1,1)
188
FermiFS{6,8}(1, 1, 0, 0, 1, 1, 1, 1)
189

190
julia> i, j, k, l = find_mode(f, (3,4,2,5))
191
(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))
192

193
julia> excitation(f, (i,j), (k,l))
194
(FermiFS{6,8}(1, 0, 1, 1, 0, 1, 1, 1), -1.0)
195
```
196

197
See [`SingleComponentFockAddress`](@ref).
198
"""
199
excitation
200

201
"""
202
    OccupiedModeMap(addr) <: AbstractVector
203

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

207
`OccupiedModeMap(addr)[i]` contains the index for the `i`-th occupied mode.
208
This is useful because repeatedly looking for occupied modes with
209
[`find_occupied_mode`](@ref) can be time-consuming.
210
`OccupiedModeMap(addr)` is an eager version of the iterator returned by
211
[`occupied_modes`](@ref). It is similar to [`onr`](@ref) but contains more information.
212

213
# Example
214

215
```jldoctest
216
julia> b = BoseFS(10, 0, 0, 0, 2, 0, 1)
217
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
218

219
julia> mb = OccupiedModeMap(b)
220
3-element OccupiedModeMap{7, BoseFSIndex}:
221
 BoseFSIndex(occnum=10, mode=1, offset=0)
222
 BoseFSIndex(occnum=2, mode=5, offset=14)
223
 BoseFSIndex(occnum=1, mode=7, offset=18)
224

225
julia> f = FermiFS(1,1,1,1,0,0,1,0,0)
226
FermiFS{5,9}(1, 1, 1, 1, 0, 0, 1, 0, 0)
227

228
julia> mf = OccupiedModeMap(f)
229
5-element OccupiedModeMap{5, FermiFSIndex}:
230
 FermiFSIndex(occnum=1, mode=1, offset=0)
231
 FermiFSIndex(occnum=1, mode=2, offset=1)
232
 FermiFSIndex(occnum=1, mode=3, offset=2)
233
 FermiFSIndex(occnum=1, mode=4, offset=3)
234
 FermiFSIndex(occnum=1, mode=7, offset=6)
235

236
julia> mf == collect(occupied_modes(f))
237
true
238

239
julia> dot(mf, mb)
240
11
241

242
julia> dot(mf, 1:20)
243
17
244
```
245
See also [`dot`](@ref Main.Hamiltonians.dot), [`SingleComponentFockAddress`](@ref).
246
"""
247
struct OccupiedModeMap{N,T} <: AbstractVector{T}
248
    indices::SVector{N,T} # N = min(N, M)
97,427,885✔
249
    length::Int
250
end
251

252
function OccupiedModeMap(addr::SingleComponentFockAddress{N,M}) where {N,M}
97,427,608✔
253
    modes = occupied_modes(addr)
97,427,660✔
254
    T = eltype(modes)
97,427,655✔
255
    # There are at most N occupied modes. This could be also @generated for cases where N ≫ M
256
    L = ismissing(N) ? M : min(N, M)
97,427,674✔
257
    indices = MVector{L,T}(undef)
97,427,700✔
258
    i = 0
97,427,592✔
259
    for index in modes
194,854,243✔
260
        i += 1
407,166,388✔
261
        @inbounds indices[i] = index
407,165,824✔
262
    end
716,903,460✔
263
    return OccupiedModeMap(SVector(indices), i)
97,427,832✔
264
end
265

266
Base.size(om::OccupiedModeMap) = (om.length,)
441,282,740✔
267
function Base.getindex(om::OccupiedModeMap, i)
266,361,349✔
268
    @boundscheck 1 ≤ i ≤ om.length || throw(BoundsError(om, i))
266,372,950✔
269
    return om.indices[i]
266,469,529✔
270
end
271

272
"""
273
    abstract type OccupiedModeIterator
274

275
Iterator over occupied modes with `eltype` [`BoseFSIndex`](@ref) or
276
[`FermiFSIndex`](@ref). A subtype of this should be returned when calling
277
[`occupied_modes`](@ref) on a Fock state.
278
"""
279
abstract type OccupiedModeIterator end
280

281
"""
282
    dot(map::OccupiedModeMap, vec::AbstractVector)
283
    dot(map1::OccupiedModeMap, map2::OccupiedModeMap)
284
Dot product extracting mode occupation numbers from an [`OccupiedModeMap`](@ref) similar
285
to [`onr`](@ref).
286

287
```jldoctest
288
julia> b = BoseFS(10, 0, 0, 0, 2, 0, 1)
289
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
290

291
julia> mb = OccupiedModeMap(b)
292
3-element OccupiedModeMap{7, BoseFSIndex}:
293
 BoseFSIndex(occnum=10, mode=1, offset=0)
294
 BoseFSIndex(occnum=2, mode=5, offset=14)
295
 BoseFSIndex(occnum=1, mode=7, offset=18)
296

297
julia> dot(mb, 1:7)
298
27
299

300
julia> mb⋅(1:7) == onr(b)⋅(1:7)
301
true
302
```
303
See also [`SingleComponentFockAddress`](@ref).
304
"""
305
function LinearAlgebra.dot(map::OccupiedModeMap, vec::AbstractVector)
24,946,833✔
306
    value = zero(eltype(vec))
24,946,827✔
307
    for index in map
49,893,637✔
308
        value += vec[index.mode] * index.occnum
127,403,135✔
309
    end
229,859,331✔
310
    return value
24,946,860✔
311
end
312
LinearAlgebra.dot(vec::AbstractVector, map::OccupiedModeMap) = dot(map, vec)
127,402,978✔
313

314
# Defined for consistency. Could also be used to compute cross-component interactions in
315
# real space.
316
function LinearAlgebra.dot(map1::OccupiedModeMap, map2::OccupiedModeMap)
1✔
317
    i = j = 1
1✔
318
    value = 0
1✔
319
    while i ≤ length(map1) && j ≤ length(map2)
7✔
320
        index1 = map1[i]
6✔
321
        index2 = map2[j]
6✔
322
        if index1.mode == index2.mode
6✔
323
            value += index1.occnum * index2.occnum
2✔
324
            i += 1
2✔
325
            j += 1
2✔
326
        elseif index1.mode < index2.mode
4✔
327
            i += 1
3✔
328
        else
329
            j += 1
1✔
330
        end
331
    end
6✔
332
    return value
1✔
333
end
334

335
"""
336
    parse_address(str)
337

338
Parse the compact representation of a Fock state address.
339
"""
340
function parse_address(str)
296✔
341
    # CompositeFS
342
    m = match(r"⊗", str)
296✔
343
    if !isnothing(m)
303✔
344
        if !isnothing(match(r"[↓⇅]", str))
9✔
345
            throw(ArgumentError("invalid fock state format \"$str\""))
2✔
346
        else
347
            return CompositeFS(map(parse_address, split(str, r" *⊗ *"))...)
5✔
348
        end
349
    end
350
    # FermiFS2C
351
    m = match(r"[↓⇅]", str)
289✔
352
    if !isnothing(m)
310✔
353
        m = match(r"\|([↑↓⇅⋅ ]+)⟩", str)
21✔
354
        if isnothing(m)
42✔
UNCOV
355
            throw(ArgumentError("invalid fock state format \"$str\""))
×
356
        else
357
            chars = filter(!=(' '), Vector{Char}(m.captures[1]))
42✔
358
            f1 = FermiFS((chars .== '↑') .| (chars .== '⇅'))
21✔
359
            f2 = FermiFS((chars .== '↓') .| (chars .== '⇅'))
21✔
360
            return CompositeFS(f1, f2)
21✔
361
        end
362
    end
363
    # Sparse BoseFS
364
    m = match(r"\|b *([0-9]+): *([ 0-9]+)⟩", str)
268✔
365
    if !isnothing(m)
280✔
366
        particles = parse.(Int, filter(!isempty, split(m.captures[2], r" +")))
12✔
367
        return BoseFS(parse(Int, m.captures[1]), zip(particles, fill(1, length(particles))))
12✔
368
    end
369
    # Sparse FermiFS
370
    m = match(r"\|f *([0-9]+): *([ 0-9]+)⟩", str)
256✔
371
    if !isnothing(m)
278✔
372
        particles = parse.(Int, filter(!isempty, split(m.captures[2], r" +")))
22✔
373
        return FermiFS(parse(Int, m.captures[1]), zip(particles, fill(1, length(particles))))
22✔
374
    end
375
    # OccupationNumberFS
376
    m = match(r"\|([ 0-9]+)⟩{[0-9]*}", str)
234✔
377
    if !isnothing(m)
247✔
378
        m2 = match(r"{([0-9]+)}", str)
13✔
379
        if isnothing(m2) # empty braces defaults to UInt8
24✔
380
            BITS = 8
2✔
381
        else
382
            BITS = parse(Int, m2.captures[1])
11✔
383
        end
384
        T = if BITS ≤ 8
13✔
385
            UInt8
8✔
386
        elseif BITS ≤ 16
5✔
387
            UInt16
1✔
388
        elseif BITS ≤ 32
4✔
389
            UInt32
1✔
390
        elseif BITS ≤ 64
3✔
391
            UInt64
1✔
392
        elseif BITS ≤ 128
2✔
393
            UInt128
1✔
394
        else
395
            throw(ArgumentError("invalid Fock state format \"$str\""))
22✔
396
        end
397
        t = Tuple(parse.(T, split(m.captures[1], r" +")))
12✔
398
        return OccupationNumberFS(SVector(t))
12✔
399
    end
400
    m = match(r"\|([ 0-9]+)⟩{", str) # anything else that has a curly brace
221✔
401
    if !isnothing(m)
222✔
402
        throw(ArgumentError("invalid Fock state format \"$str\""))
1✔
403
    end
404

405
    # BoseFS
406
    m = match(r"\|([ 0-9]+)⟩", str)
220✔
407
    if !isnothing(m)
324✔
408
        return BoseFS(parse.(Int, split(m.captures[1], r" +")))
104✔
409
    end
410
    # Single FermiFS
411
    m = match(r"\|([ ⋅↑]+)⟩", str)
116✔
412
    if !isnothing(m)
232✔
413
        chars = filter(!=(' '), Vector{Char}(m.captures[1]))
232✔
414
        return FermiFS(chars .== '↑')
116✔
415
    end
UNCOV
416
    throw(ArgumentError("invalid Fock state format \"$str\""))
×
417
end
418

419
"""
420
    fs"\$(string)"
421

422
Parse the compact representation of a Fock state.
423
Useful for copying the printout from a vector to the REPL.
424

425
# Example
426

427
```jldoctest
428
julia> DVec(BoseFS{3,4}(0, 1, 2, 0) => 1)
429
DVec{BoseFS{3, 4, BitString{6, 1, UInt8}},Int64} with 1 entry, style = IsStochasticInteger{Int64}()
430
  fs"|0 1 2 0⟩" => 1
431

432
julia> fs"|0 1 2 0⟩" => 1 # Copied from above printout
433
BoseFS{3,4}(0, 1, 2, 0) => 1
434

435
julia> fs"|1 2 3⟩⊗|0 1 0⟩" # composite bosonic Fock state
436
CompositeFS(
437
  BoseFS{6,3}(1, 2, 3),
438
  BoseFS{1,3}(0, 1, 0),
439
)
440

441
julia> fs"|↑↓↑⟩" # construct a fermionic Fock state
442
CompositeFS(
443
  FermiFS{2,3}(1, 0, 1),
444
  FermiFS{1,3}(0, 1, 0),
445
)
446

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

450
julia> [s] # prints out with the signifcant number of bits specified in braces
451
1-element Vector{OccupationNumberFS{4, UInt8}}:
452
 fs"|0 1 2 0⟩{8}"
453
```
454

455
See also [`FermiFS`](@ref), [`BoseFS`](@ref), [`CompositeFS`](@ref), [`FermiFS2C`](@ref),
456
[`OccupationNumberFS`](@ref).
457
"""
458
macro fs_str(str)
91✔
459
    return parse_address(str)
91✔
460
end
461

462
"""
463
    print_address(io::IO, address)
464

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

468
This function is used to implement `Base.show` for [`AbstractFockAddress`](@ref).
469
"""
470
print_address
471

472
function Base.show(io::IO, add::AbstractFockAddress)
1,278✔
473
    if get(io, :typeinfo, nothing) == typeof(add) || get(io, :compact, false)
2,491✔
474
        print(io, "fs\"")
658✔
475
        print_address(io, add; compact=true)
658✔
476
        print(io, "\"")
658✔
477
    else
478
        print_address(io, add; compact=false)
620✔
479
    end
480
end
481

482
function onr_sparse_string(o)
90✔
483
    ps = map(p -> p[1] => p[2], Iterators.filter(p -> !iszero(p[2]), enumerate(o)))
19,350✔
484
    return join(ps, ", ")
90✔
485
end
486

487
###
488
### Boson stuff
489
###
490
"""
491
    BoseFSIndex
492

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

495
## Fields:
496

497
* `occnum`: the occupation number.
498
* `mode`: the index of the mode.
499
* `offset`: the position of the mode in the address. This is the bit offset of the mode when
500
 the address is represented by a bitstring, and the position in the list when it is
501
 represented by `SortedParticleList`.
502

503
"""
504
Base.@kwdef struct BoseFSIndex<:FieldVector{3,Int}
2✔
505
    occnum::Int
1,450,639,253✔
506
    mode::Int
507
    offset::Int
508
end
509

510
function Base.show(io::IO, i::BoseFSIndex)
54✔
511
    @unpack occnum, mode, offset = i
54✔
512
    print(io, "BoseFSIndex(occnum=$occnum, mode=$mode, offset=$offset)")
54✔
513
end
514
Base.show(io::IO, ::MIME"text/plain", i::BoseFSIndex) = show(io, i)
9✔
515

516
"""
517
    BoseOccupiedModes{C,S<:BoseFS}
518

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

522
See [`occupied_modes`](@ref).
523

524
Defining `Base.length` and `Base.iterate` for this struct is a part of the interface for an
525
underlying storage format used by [`BoseFS`](@ref).
526
"""
527
struct BoseOccupiedModes{N,M,S}<:OccupiedModeIterator
528
    storage::S
497,728,230✔
529
end
UNCOV
530
Base.eltype(::BoseOccupiedModes) = BoseFSIndex
×
531

532
# Apply destruction operator to BoseFSIndex.
533
@inline _destroy(d, index::BoseFSIndex) = @set index.occnum -= (d.mode == index.mode)
1,265,065,838✔
534
@inline _destroy(d) = Base.Fix1(_destroy, d)
1,089,973,561✔
535
# Apply creation operator to BoseFSIndex.
536
@inline _create(c, index::BoseFSIndex) = @set index.occnum += (c.mode == index.mode)
244,844,601✔
537
@inline _create(c) = Base.Fix1(_create, c)
551,137,205✔
538

539
"""
540
    bose_excitation_value(
541
        creations::NTuple{_,BoseFSIndex}, destructions::NTuple{_,::BoseFSIndex}
542
    ) -> Int
543

544
Compute the squared value of an excitation from indices. Starts by applying all destruction
545
operators, and then applying all creation operators. The operators must be given in reverse
546
order. Will return 0 if move is illegal.
547
"""
UNCOV
548
@inline bose_excitation_value(::Tuple{}, ::Tuple{}) = 1
×
549
@inline function bose_excitation_value((c, cs...)::NTuple{<:Any,BoseFSIndex}, ::Tuple{})
550,170,007✔
550
    return bose_excitation_value(map(_create(c), cs), ()) * (c.occnum + 1)
550,908,277✔
551
end
552
@inline function bose_excitation_value(
546,950,887✔
553
    creations::NTuple{<:Any,BoseFSIndex}, (d, ds...)::NTuple{<:Any,BoseFSIndex}
554
)
555
    return bose_excitation_value(map(_destroy(d), creations), map(_destroy(d), ds)) * d.occnum
548,424,681✔
556
end
557

558
"""
559
    from_bose_onr(::Type{B}, onr::AbstractArray) -> B
560

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

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

570
"""
571
    to_bose_onr(bs::B) -> SVector
572

573
Convert `bs` to a static vector in the occupation number representation format.
574

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

580
"""
581
    bose_excitation(
582
        bs::B, creations::NTuple{N,BoseFSIndex}, destructions::NTuple{N,BoseFSIndex}
583
    ) -> Tuple{B,Float64}
584

585
Perform excitation as if `bs` was a bosonic address. See also
586
[`bose_excitation_value`](@ref).
587

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

593
"""
594
    bose_num_occupied_modes(bs::B)
595

596
Return the number of occupied modes, if `bs` represents a bosonic address.
597

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

603
###
604
### Fermion stuff
605
###
606
"""
607
    FermiFSIndex
608

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

611
## Fields:
612

613
* `occnum`: the occupation number.
614
* `mode`: the index of the mode.
615
* `offset`: the position of the mode in the address. This is `mode - 1` when the address is
616
  represented by a bitstring, and the position in the list when using `SortedParticleList`.
617

618
"""
619
Base.@kwdef struct FermiFSIndex<:FieldVector{3,Int}
620
    occnum::Int
71,866,817✔
621
    mode::Int
622
    offset::Int
623
end
624

625
function Base.show(io::IO, i::FermiFSIndex)
26✔
626
    @unpack occnum, mode, offset = i
26✔
627
    print(io, "FermiFSIndex(occnum=$occnum, mode=$mode, offset=$offset)")
26✔
628
end
629
Base.show(io::IO, ::MIME"text/plain", i::FermiFSIndex) = show(io, i)
6✔
630

631
"""
632
    FermiOccupiedModes{N,S<:BitString}
633

634
Iterator over occupied modes in address. `N` is the number of fermions. See [`occupied_modes`](@ref).
635
"""
636
struct FermiOccupiedModes{N,S}<:OccupiedModeIterator
637
    storage::S
10,983,462✔
638
end
639

640
Base.length(::FermiOccupiedModes{N}) where {N} = N
3✔
UNCOV
641
Base.eltype(::FermiOccupiedModes) = FermiFSIndex
×
642

643
"""
644
    from_fermi_onr(::Type{B}, onr) -> B
645

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

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

655
"""
656
    fermi_find_mode(bs::B, i::Integer) -> FermiFSIndex
657

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

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

666
"""
667
    fermi_excitation(
668
        bs::B, creations::NTuple{N,FermiFSIndex}, destructions::NTuple{N,FermiFSIndex}
669
    ) -> Tuple{B,Float64}
670

671
Perform excitation as if `bs` was a fermionic address.
672

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

678
###
679
### General
680
###
681
function LinearAlgebra.dot(occ_a::OccupiedModeIterator, occ_b::OccupiedModeIterator)
149,758✔
682
    (n_a, i_a, _), st_a = iterate(occ_a)
299,516✔
683
    (n_b, i_b, _), st_b = iterate(occ_b)
299,516✔
684

685
    acc = 0
149,758✔
686
    while true
258,919✔
687
        if i_a > i_b
258,919✔
688
            # b is behind and needs to do a step
689
            iter = iterate(occ_b, st_b)
155,081✔
690
            isnothing(iter) && return acc
155,081✔
691
            (n_b, i_b, _), st_b = iter
54,258✔
692
        elseif i_a < i_b
158,096✔
693
            # a is behind and needs to do a step
694
            iter = iterate(occ_a, st_a)
155,793✔
695
            isnothing(iter) && return acc
155,793✔
696
            (n_a, i_a, _), st_a = iter
54,644✔
697
        else
698
            # a and b are at the same position
699
            acc += n_a * n_b
56,947✔
700
            # now both need to do a step
701
            iter = iterate(occ_a, st_a)
72,954✔
702
            isnothing(iter) && return acc
72,954✔
703
            (n_a, i_a, _), st_a = iter
16,007✔
704
            iter = iterate(occ_b, st_b)
16,266✔
705
            isnothing(iter) && return acc
16,266✔
706
            (n_b, i_b, _), st_b = iter
259✔
707
        end
708
    end
109,161✔
709
end
710

711
function sparse_to_onr(M, pairs)
643✔
712
    onr = spzeros(Int, M)
643✔
713
    for (k, v) in pairs
681✔
714
        v ≥ 0 || throw(ArgumentError("Invalid pair `$k=>$v`: particle number negative"))
2,035✔
715
        0 < k ≤ M || throw(ArgumentError("Invalid pair `$k => $v`: key of of range `1:$M`"))
2,033✔
716
        onr[k] += v
2,029✔
717
    end
2,463✔
718
    return onr
638✔
719
end
720

721
"""
722
    OccupiedPairsMap(addr::SingleComponentFockAddress) <: AbstractVector
723

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

731
# Example
732

733
```jldoctest
734
julia> addr = BoseFS(10, 0, 0, 0, 2, 0, 1)
735
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
736

737
julia> pairs = OccupiedPairsMap(addr)
738
5-element OccupiedPairsMap{78, Tuple{BoseFSIndex, BoseFSIndex}}:
739
 (BoseFSIndex(occnum=10, mode=1, offset=0), BoseFSIndex(occnum=10, mode=1, offset=0))
740
 (BoseFSIndex(occnum=2, mode=5, offset=14), BoseFSIndex(occnum=2, mode=5, offset=14))
741
 (BoseFSIndex(occnum=2, mode=5, offset=14), BoseFSIndex(occnum=10, mode=1, offset=0))
742
 (BoseFSIndex(occnum=1, mode=7, offset=18), BoseFSIndex(occnum=10, mode=1, offset=0))
743
 (BoseFSIndex(occnum=1, mode=7, offset=18), BoseFSIndex(occnum=2, mode=5, offset=14))
744

745
julia> excitation(addr, pairs[2], pairs[4])
746
(BoseFS{13,7}(9, 0, 0, 0, 4, 0, 0), 10.954451150103322)
747
```
748

749
See also [`OccupiedModeMap`](@ref).
750
"""
751
struct OccupiedPairsMap{N,T} <: AbstractVector{T}
752
    pairs::SVector{N,T}
66✔
753
    length::Int
754
end
755

756
function OccupiedPairsMap(addr::SingleComponentFockAddress{N}) where {N}
66✔
757
    omm = OccupiedModeMap(addr)
137✔
758
    T = eltype(omm)
66✔
759
    P = N * (N - 1) ÷ 2
66✔
760
    pairs = MVector{P,Tuple{T,T}}(undef)
66✔
761
    a = 0
66✔
762
    for i in eachindex(omm)
66✔
763
        p_i = omm[i]
137✔
764
        if p_i.occnum > 1
137✔
765
            a += 1
46✔
766
            @inbounds pairs[a] = (p_i, p_i)
46✔
767
        end
768
        for j in 1:i-1
137✔
769
            p_j = omm[j]
93✔
770
            a += 1
93✔
771
            @inbounds pairs[a] = (p_i, p_j)
93✔
772
        end
115✔
773
    end
208✔
774

775
    return OccupiedPairsMap(SVector(pairs), a)
66✔
776
end
777

778
Base.size(opm::OccupiedPairsMap) = (opm.length,)
1,348✔
779
function Base.getindex(opm::OccupiedPairsMap, i)
2,251✔
780
    @boundscheck 1 ≤ i ≤ opm.length || throw(BoundsError(opm, i))
2,251✔
781
    return opm.pairs[i]
2,251✔
782
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