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

RimuQMC / Rimu.jl / 15992867340

01 Jul 2025 07:33AM UTC coverage: 94.311% (-0.3%) from 94.571%
15992867340

Pull #330

github

lch
Unified ModeMap Type (#330)

1. Renamed the type OccupiedModeMap to ModeMap and replaced all occurrences.
2. Added the occupied_mode_map function as a constructor for ModeMap.
3. Added the unoccupied_mode_map function as a constructor for FermiFS
and corresponding iterator type FermiUnoccupiedModes.
Pull Request #330: Unified ModeMap Type

74 of 97 new or added lines in 16 files covered. (76.29%)

7 existing lines in 3 files now uncovered.

7029 of 7453 relevant lines covered (94.31%)

11608138.38 hits per line

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

95.48
/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.
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))
30,902✔
21
num_particles(::Type{<:AbstractFockAddress{N}}) where {N} = N
1,260,259✔
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))
485,401,985✔
30
num_modes(::Type{<:AbstractFockAddress{<:Any,M}}) where {M} = M
485,417,313✔
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,568✔
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
* [`occupied_mode_map`](@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

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), [`occupied_mode_map`](@ref),
113
[`SingleComponentFockAddress`](@ref).
114
"""
115
function find_occupied_mode(b::SingleComponentFockAddress, index::Integer, n=1)
125,895,329✔
116
    mode_iterator = occupied_modes(b)
206,503,720✔
117
    T = eltype(mode_iterator)
63,033,118✔
118
    for (occnum, mode, offset) in mode_iterator
125,596,474✔
119
        index -= occnum ≥ n
144,787,220✔
120
        if index == 0
144,822,289✔
121
            return T(occnum, mode, offset)
63,003,714✔
122
        end
123
    end
164,364,020✔
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 [`occupied_mode_map`](@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
    unoccupied_modes(::FermiFS)
181

182
Return a lazy iterator over all unoccupied modes in an Fermi type address. Iterates over
183
over [`FermiFSIndex`](@ref)s for [`FermiFS`](@ref). 
184
See [`unoccupied_mode_map`](@ref) for an eager version.
185

186
# Example
187

188
```jldoctest
189
julia> f = FermiFS((1,1,0,1,0,0,1));
190

191
julia> foreach(println, unoccupied_modes(f))
192
FermiFSIndex(occnum=0, mode=3, offset=2)
193
FermiFSIndex(occnum=0, mode=5, offset=4)
194
FermiFSIndex(occnum=0, mode=6, offset=5)
195
```
196
See also [`find_occupied_mode`](@ref), [`SingleComponentFockAddress`](@ref).
197
"""
198
unoccupied_modes
199

200
"""
201
    excitation(addr::SingleComponentFockAddress, creations::NTuple, destructions::NTuple)
202

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

207
```math
208
a^†_{c_1} a^†_{c_2} \\ldots a_{d_1} a_{d_2} \\ldots |\\mathrm{addr}\\rangle \\to
209
α|\\mathrm{naddr}\\rangle
210
```
211

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

216
# Example
217

218
```jldoctest
219
julia> f = FermiFS(1,1,0,0,1,1,1,1)
220
FermiFS{6,8}(1, 1, 0, 0, 1, 1, 1, 1)
221

222
julia> i, j, k, l = find_mode(f, (3,4,2,5))
223
(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))
224

225
julia> excitation(f, (i,j), (k,l))
226
(FermiFS{6,8}(1, 0, 1, 1, 0, 1, 1, 1), -1.0)
227
```
228

229
See [`SingleComponentFockAddress`](@ref).
230
"""
231
excitation
232

233

234
"""
235
    ModeMap <: AbstractVector
236

237
A unified storage structure for indices of `SingleComponentFockAddress`. 
238
It stores the FSIndex of corresponding address as an `AbstractVector` compatible with
239
[`excitation`](@ref) - [`BoseFSIndex`](@ref) or [`FermiFSIndex`](@ref).
240

241
This struct is not intended to construct directly. Use [`occupied_mode_map`](@ref) or 
242
[`unoccupied_mode_map`](@ref) to obtain an instance.
243

244
See also [`dot`](@ref Main.Hamiltonians.dot), [`SingleComponentFockAddress`](@ref).
245
"""
246
struct ModeMap{N,T} <: AbstractVector{T}
247
    indices::SVector{N,T} # N = min(N, M)
97,427,812✔
248
    length::Int
249
end
250

251
Base.eltype(::ModeMap{N,T}) where {N,T} = T
109,631✔
252

253
Base.@deprecate OccupiedModeMap(addr) occupied_mode_map(addr)
254

255

256
"""
257
    occupied_mode_map(addr) <: AbstractVector
258
    
259
Get a map of occupied modes in address as an `AbstractVector` of indices compatible with
260
[`excitation`](@ref) - [`BoseFSIndex`](@ref) or [`FermiFSIndex`](@ref).
261

262
`occupied_mode_map(addr)[i]` contains the index for the `i`-th occupied mode.
263
This is useful because repeatedly looking for occupied modes with
264
[`find_occupied_mode`](@ref) can be time-consuming.
265
`occupied_mode_map(addr)` is an eager version of the iterator returned by
266
[`occupied_modes`](@ref). It is similar to [`onr`](@ref) but contains more information.
267

268
# Example
269

270
```jldoctest
271
julia> b = BoseFS(10, 0, 0, 0, 2, 0, 1)
272
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
273

274
julia> mb = occupied_mode_map(b)
275
3-element ModeMap{7, BoseFSIndex}:
276
 BoseFSIndex(occnum=10, mode=1, offset=0)
277
 BoseFSIndex(occnum=2, mode=5, offset=14)
278
 BoseFSIndex(occnum=1, mode=7, offset=18)
279

280
julia> f = FermiFS(1,1,1,1,0,0,1,0,0)
281
FermiFS{5,9}(1, 1, 1, 1, 0, 0, 1, 0, 0)
282

283
julia> mf = occupied_mode_map(f)
284
5-element ModeMap{5, FermiFSIndex}:
285
 FermiFSIndex(occnum=1, mode=1, offset=0)
286
 FermiFSIndex(occnum=1, mode=2, offset=1)
287
 FermiFSIndex(occnum=1, mode=3, offset=2)
288
 FermiFSIndex(occnum=1, mode=4, offset=3)
289
 FermiFSIndex(occnum=1, mode=7, offset=6)
290

291
julia> mf == collect(occupied_modes(f))
292
true
293

294
julia> dot(mf, mb)
295
11
296

297
julia> dot(mf, 1:20)
298
17
299
```
300
See also [`dot`](@ref Main.Hamiltonians.dot), [`SingleComponentFockAddress`](@ref).
301
"""
302
function occupied_mode_map(addr::SingleComponentFockAddress{N,M}) where {N,M}
97,427,637✔
303
    modes = occupied_modes(addr)
97,427,659✔
304
    T = eltype(modes)
97,427,651✔
305
    # There are at most N occupied modes. This could be also @generated for cases where N ≫ M
306
    L = ismissing(N) ? M : min(N, M)
97,427,654✔
307
    indices = MVector{L,T}(undef)
97,427,657✔
308
    i = 0
97,427,599✔
309
    for index in modes
194,854,841✔
310
        i += 1
407,164,731✔
311
        @inbounds indices[i] = index
407,164,726✔
312
    end
716,901,696✔
313
    return ModeMap(SVector(indices), i)
97,427,634✔
314
end
315

316
Base.size(om::ModeMap) = (om.length,)
441,075,623✔
317
function Base.getindex(om::ModeMap, i)
266,267,484✔
318
    @boundscheck 1 ≤ i ≤ om.length || throw(BoundsError(om, i))
266,231,379✔
319
    return om.indices[i]
266,366,975✔
320
end
321

322
"""
323
    abstract type ModeIterator
324

325
Iterator over modes with `eltype` [`BoseFSIndex`](@ref) or
326
[`FermiFSIndex`](@ref). A subtype of this should be returned when calling
327
[`occupied_modes`](@ref) on a Fock state.
328
"""
329
abstract type ModeIterator end
330

331
"""
332
    dot(map::ModeMap, vec::AbstractVector)
333
    dot(map1::ModeMap, map2::ModeMap)
334
Dot product extracting mode occupation numbers from an [`ModeMap`](@ref) similar
335
to [`onr`](@ref).
336

337
```jldoctest
338
julia> b = BoseFS(10, 0, 0, 0, 2, 0, 1)
339
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
340

341
julia> mb = occupied_mode_map(b)
342
3-element ModeMap{7, BoseFSIndex}:
343
 BoseFSIndex(occnum=10, mode=1, offset=0)
344
 BoseFSIndex(occnum=2, mode=5, offset=14)
345
 BoseFSIndex(occnum=1, mode=7, offset=18)
346

347
julia> dot(mb, 1:7)
348
27
349

350
julia> mb⋅(1:7) == onr(b)⋅(1:7)
351
true
352
```
353
See also [`SingleComponentFockAddress`](@ref).
354
"""
355
function LinearAlgebra.dot(map::ModeMap, vec::AbstractVector)
24,946,826✔
356
    value = zero(eltype(vec))
24,946,826✔
357
    for index in map
49,893,613✔
358
        value += vec[index.mode] * index.occnum
127,402,741✔
359
    end
229,858,748✔
360
    return value
24,946,843✔
361
end
362
LinearAlgebra.dot(vec::AbstractVector, map::ModeMap) = dot(map, vec)
127,402,587✔
363

364
# Defined for consistency. Could also be used to compute cross-component interactions in
365
# real space.
366
function LinearAlgebra.dot(map1::ModeMap, map2::ModeMap)
1✔
367
    i = j = 1
1✔
368
    value = 0
1✔
369
    while i ≤ length(map1) && j ≤ length(map2)
7✔
370
        index1 = map1[i]
6✔
371
        index2 = map2[j]
6✔
372
        if index1.mode == index2.mode
6✔
373
            value += index1.occnum * index2.occnum
2✔
374
            i += 1
2✔
375
            j += 1
2✔
376
        elseif index1.mode < index2.mode
4✔
377
            i += 1
3✔
378
        else
379
            j += 1
1✔
380
        end
381
    end
6✔
382
    return value
1✔
383
end
384

385
"""
386
    parse_address(str)
387

388
Parse the compact representation of a Fock state address.
389
"""
390
function parse_address(str)
293✔
391
    # CompositeFS
392
    m = match(r"⊗", str)
293✔
393
    if !isnothing(m)
300✔
394
        if !isnothing(match(r"[↓⇅]", str))
9✔
395
            throw(ArgumentError("invalid fock state format \"$str\""))
2✔
396
        else
397
            return CompositeFS(map(parse_address, split(str, r" *⊗ *"))...)
5✔
398
        end
399
    end
400
    # FermiFS2C
401
    m = match(r"[↓⇅]", str)
286✔
402
    if !isnothing(m)
307✔
403
        m = match(r"\|([↑↓⇅⋅ ]+)⟩", str)
21✔
404
        if isnothing(m)
42✔
UNCOV
405
            throw(ArgumentError("invalid fock state format \"$str\""))
×
406
        else
407
            chars = filter(!=(' '), Vector{Char}(m.captures[1]))
42✔
408
            f1 = FermiFS((chars .== '↑') .| (chars .== '⇅'))
21✔
409
            f2 = FermiFS((chars .== '↓') .| (chars .== '⇅'))
21✔
410
            return CompositeFS(f1, f2)
21✔
411
        end
412
    end
413
    # Sparse BoseFS
414
    m = match(r"\|b *([0-9]+): *([ 0-9]+)⟩", str)
265✔
415
    if !isnothing(m)
277✔
416
        particles = parse.(Int, filter(!isempty, split(m.captures[2], r" +")))
12✔
417
        return BoseFS(parse(Int, m.captures[1]), zip(particles, fill(1, length(particles))))
12✔
418
    end
419
    # Sparse FermiFS
420
    m = match(r"\|f *([0-9]+): *([ 0-9]+)⟩", str)
253✔
421
    if !isnothing(m)
275✔
422
        particles = parse.(Int, filter(!isempty, split(m.captures[2], r" +")))
22✔
423
        return FermiFS(parse(Int, m.captures[1]), zip(particles, fill(1, length(particles))))
22✔
424
    end
425
    # OccupationNumberFS
426
    m = match(r"\|([ 0-9]+)⟩{[0-9]*}", str)
231✔
427
    if !isnothing(m)
244✔
428
        m2 = match(r"{([0-9]+)}", str)
13✔
429
        if isnothing(m2) # empty braces defaults to UInt8
24✔
430
            BITS = 8
2✔
431
        else
432
            BITS = parse(Int, m2.captures[1])
11✔
433
        end
434
        T = if BITS ≤ 8
13✔
435
            UInt8
8✔
436
        elseif BITS ≤ 16
5✔
437
            UInt16
1✔
438
        elseif BITS ≤ 32
4✔
439
            UInt32
1✔
440
        elseif BITS ≤ 64
3✔
441
            UInt64
1✔
442
        elseif BITS ≤ 128
2✔
443
            UInt128
1✔
444
        else
445
            throw(ArgumentError("invalid Fock state format \"$str\""))
22✔
446
        end
447
        t = Tuple(parse.(T, split(m.captures[1], r" +")))
12✔
448
        return OccupationNumberFS(SVector(t))
12✔
449
    end
450
    m = match(r"\|([ 0-9]+)⟩{", str) # anything else that has a curly brace
218✔
451
    if !isnothing(m)
219✔
452
        throw(ArgumentError("invalid Fock state format \"$str\""))
1✔
453
    end
454

455
    # BoseFS
456
    m = match(r"\|([ 0-9]+)⟩", str)
217✔
457
    if !isnothing(m)
318✔
458
        return BoseFS(parse.(Int, split(m.captures[1], r" +")))
101✔
459
    end
460
    # Single FermiFS
461
    m = match(r"\|([ ⋅↑]+)⟩", str)
116✔
462
    if !isnothing(m)
232✔
463
        chars = filter(!=(' '), Vector{Char}(m.captures[1]))
232✔
464
        return FermiFS(chars .== '↑')
116✔
465
    end
UNCOV
466
    throw(ArgumentError("invalid Fock state format \"$str\""))
×
467
end
468

469
"""
470
    fs"\$(string)"
471

472
Parse the compact representation of a Fock state.
473
Useful for copying the printout from a vector to the REPL.
474

475
# Example
476

477
```jldoctest
478
julia> DVec(BoseFS{3,4}(0, 1, 2, 0) => 1)
479
DVec{BoseFS{3, 4, BitString{6, 1, UInt8}},Int64} with 1 entry, style = IsStochasticInteger{Int64}()
480
  fs"|0 1 2 0⟩" => 1
481

482
julia> fs"|0 1 2 0⟩" => 1 # Copied from above printout
483
BoseFS{3,4}(0, 1, 2, 0) => 1
484

485
julia> fs"|1 2 3⟩⊗|0 1 0⟩" # composite bosonic Fock state
486
CompositeFS(
487
  BoseFS{6,3}(1, 2, 3),
488
  BoseFS{1,3}(0, 1, 0),
489
)
490

491
julia> fs"|↑↓↑⟩" # construct a fermionic Fock state
492
CompositeFS(
493
  FermiFS{2,3}(1, 0, 1),
494
  FermiFS{1,3}(0, 1, 0),
495
)
496

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

500
julia> [s] # prints out with the signifcant number of bits specified in braces
501
1-element Vector{OccupationNumberFS{4, UInt8}}:
502
 fs"|0 1 2 0⟩{8}"
503
```
504

505
See also [`FermiFS`](@ref), [`BoseFS`](@ref), [`CompositeFS`](@ref), [`FermiFS2C`](@ref),
506
[`OccupationNumberFS`](@ref).
507
"""
508
macro fs_str(str)
88✔
509
    return parse_address(str)
88✔
510
end
511

512
"""
513
    print_address(io::IO, address)
514

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

518
This function is used to implement `Base.show` for [`AbstractFockAddress`](@ref).
519
"""
520
print_address
521

522
function Base.show(io::IO, add::AbstractFockAddress)
1,274✔
523
    if get(io, :typeinfo, nothing) == typeof(add) || get(io, :compact, false)
2,485✔
524
        print(io, "fs\"")
653✔
525
        print_address(io, add; compact=true)
653✔
526
        print(io, "\"")
653✔
527
    else
528
        print_address(io, add; compact=false)
621✔
529
    end
530
end
531

532
function onr_sparse_string(o)
90✔
533
    ps = map(p -> p[1] => p[2], Iterators.filter(p -> !iszero(p[2]), enumerate(o)))
19,366✔
534
    return join(ps, ", ")
90✔
535
end
536

537
###
538
### Boson stuff
539
###
540
"""
541
    BoseFSIndex
542

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

545
## Fields:
546

547
* `occnum`: the occupation number.
548
* `mode`: the index of the mode.
549
* `offset`: the position of the mode in the address. This is the bit offset of the mode when
550
 the address is represented by a bitstring, and the position in the list when it is
551
 represented by `SortedParticleList`.
552

553
"""
554
Base.@kwdef struct BoseFSIndex<:FieldVector{3,Int}
2✔
555
    occnum::Int
1,448,873,974✔
556
    mode::Int
557
    offset::Int
558
end
559

560
function Base.show(io::IO, i::BoseFSIndex)
54✔
561
    @unpack occnum, mode, offset = i
54✔
562
    print(io, "BoseFSIndex(occnum=$occnum, mode=$mode, offset=$offset)")
54✔
563
end
564
Base.show(io::IO, ::MIME"text/plain", i::BoseFSIndex) = show(io, i)
9✔
565

566
"""
567
    BoseOccupiedModes{C,S<:BoseFS}
568

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

572
See [`occupied_modes`](@ref).
573

574
Defining `Base.length` and `Base.iterate` for this struct is a part of the interface for an
575
underlying storage format used by [`BoseFS`](@ref).
576
"""
577
struct BoseOccupiedModes{N,M,S} <: ModeIterator
578
    storage::S
496,594,445✔
579
end
UNCOV
580
Base.eltype(::BoseOccupiedModes) = BoseFSIndex
×
581

582
# Apply destruction operator to BoseFSIndex.
583
@inline _destroy(d, index::BoseFSIndex) = @set index.occnum -= (d.mode == index.mode)
1,269,062,538✔
584
@inline _destroy(d) = Base.Fix1(_destroy, d)
1,089,091,992✔
585
# Apply creation operator to BoseFSIndex.
586
@inline _create(c, index::BoseFSIndex) = @set index.occnum += (c.mode == index.mode)
244,641,282✔
587
@inline _create(c) = Base.Fix1(_create, c)
551,072,599✔
588

589
"""
590
    bose_excitation_value(
591
        creations::NTuple{_,BoseFSIndex}, destructions::NTuple{_,::BoseFSIndex}
592
    ) -> Int
593

594
Compute the squared value of an excitation from indices. Starts by applying all destruction
595
operators, and then applying all creation operators. The operators must be given in reverse
596
order. Will return 0 if move is illegal.
597
"""
UNCOV
598
@inline bose_excitation_value(::Tuple{}, ::Tuple{}) = 1
×
599
@inline function bose_excitation_value((c, cs...)::NTuple{<:Any,BoseFSIndex}, ::Tuple{})
550,733,989✔
600
    return bose_excitation_value(map(_create(c), cs), ()) * (c.occnum + 1)
551,027,648✔
601
end
602
@inline function bose_excitation_value(
547,249,125✔
603
    creations::NTuple{<:Any,BoseFSIndex}, (d, ds...)::NTuple{<:Any,BoseFSIndex}
604
)
605
    return bose_excitation_value(map(_destroy(d), creations), map(_destroy(d), ds)) * d.occnum
547,343,135✔
606
end
607

608
"""
609
    from_bose_onr(::Type{B}, onr::AbstractArray) -> B
610

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

615
This function is a part of the interface for an underlying storage format used by
616
[`BoseFS`](@ref).
617
"""
618
from_bose_onr
619

620
"""
621
    to_bose_onr(bs::B) -> SVector
622

623
Convert `bs` to a static vector in the occupation number representation format.
624

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

630
"""
631
    bose_excitation(
632
        bs::B, creations::NTuple{N,BoseFSIndex}, destructions::NTuple{N,BoseFSIndex}
633
    ) -> Tuple{B,Float64}
634

635
Perform excitation as if `bs` was a bosonic address. See also
636
[`bose_excitation_value`](@ref).
637

638
This function is a part of the interface for an underlying storage format used by
639
[`BoseFS`](@ref).
640
"""
641
bose_excitation
642

643
"""
644
    bose_num_occupied_modes(bs::B)
645

646
Return the number of occupied modes, if `bs` represents a bosonic address.
647

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

653
###
654
### Fermion stuff
655
###
656
"""
657
    FermiFSIndex
658

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

661
## Fields:
662

663
* `occnum`: the occupation number.
664
* `mode`: the index of the mode.
665
* `offset`: the position of the mode in the address. This is `mode - 1` when the address is
666
  represented by a bitstring, and the position in the list when using `SortedParticleList`.
667

668
"""
669
Base.@kwdef struct FermiFSIndex<:FieldVector{3,Int}
670
    occnum::Int
71,852,623✔
671
    mode::Int
672
    offset::Int
673
end
674

675
function Base.show(io::IO, i::FermiFSIndex)
35✔
676
    @unpack occnum, mode, offset = i
35✔
677
    print(io, "FermiFSIndex(occnum=$occnum, mode=$mode, offset=$offset)")
35✔
678
end
679
Base.show(io::IO, ::MIME"text/plain", i::FermiFSIndex) = show(io, i)
8✔
680

681
"""
682
    FermiOccupiedModes{N,S<:BitString}
683

684
Iterator over occupied modes in address. `N` is the number of fermions. See [`occupied_modes`](@ref).
685
"""
686
struct FermiOccupiedModes{N,S} <: ModeIterator
687
    storage::S
10,982,144✔
688
end
689

690
Base.length(::FermiOccupiedModes{N}) where {N} = N
3✔
UNCOV
691
Base.eltype(::FermiOccupiedModes) = FermiFSIndex
×
692

693
"""
694
    FermiUnoccupiedModes{N,S<:BitString}
695

696
Iterator over unoccupied modes in address. `N` is the number of fermions. See [`unoccupied_modes`](@ref).
697
"""
698
struct FermiUnoccupiedModes{N,S} <: ModeIterator
699
    storage::S
3✔
700
end
701
Base.length(::FermiUnoccupiedModes{N}) where {N} = N
1✔
NEW
702
Base.eltype(::FermiUnoccupiedModes) = FermiFSIndex
×
703

704
"""
705
    from_fermi_onr(::Type{B}, onr) -> B
706

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

711
This function is a part of the interface for an underlying storage format used by
712
[`FermiFS`](@ref).
713
"""
714
from_fermi_onr
715

716
"""
717
    fermi_find_mode(bs::B, i::Integer) -> FermiFSIndex
718

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

722
This function is a part of the interface for an underlying storage format used by
723
[`FermiFS`](@ref).
724
"""
725
fermi_find_mode
726

727
"""
728
    fermi_excitation(
729
        bs::B, creations::NTuple{N,FermiFSIndex}, destructions::NTuple{N,FermiFSIndex}
730
    ) -> Tuple{B,Float64}
731

732
Perform excitation as if `bs` was a fermionic address.
733

734
This function is a part of the interface for an underlying storage format used by
735
[`FermiFS`](@ref).
736
"""
737
fermi_excitation
738

739
###
740
### General
741
###
742
function LinearAlgebra.dot(occ_a::ModeIterator, occ_b::ModeIterator)
149,758✔
743
    (n_a, i_a, _), st_a = iterate(occ_a)
299,516✔
744
    (n_b, i_b, _), st_b = iterate(occ_b)
299,516✔
745

746
    acc = 0
149,758✔
747
    while true
258,919✔
748
        if i_a > i_b
258,919✔
749
            # b is behind and needs to do a step
750
            iter = iterate(occ_b, st_b)
155,081✔
751
            isnothing(iter) && return acc
155,081✔
752
            (n_b, i_b, _), st_b = iter
54,258✔
753
        elseif i_a < i_b
158,096✔
754
            # a is behind and needs to do a step
755
            iter = iterate(occ_a, st_a)
155,793✔
756
            isnothing(iter) && return acc
155,793✔
757
            (n_a, i_a, _), st_a = iter
54,644✔
758
        else
759
            # a and b are at the same position
760
            acc += n_a * n_b
56,947✔
761
            # now both need to do a step
762
            iter = iterate(occ_a, st_a)
72,954✔
763
            isnothing(iter) && return acc
72,954✔
764
            (n_a, i_a, _), st_a = iter
16,007✔
765
            iter = iterate(occ_b, st_b)
16,266✔
766
            isnothing(iter) && return acc
16,266✔
767
            (n_b, i_b, _), st_b = iter
259✔
768
        end
769
    end
109,161✔
770
end
771

772
function sparse_to_onr(M, pairs)
643✔
773
    onr = spzeros(Int, M)
643✔
774
    for (k, v) in pairs
681✔
775
        v ≥ 0 || throw(ArgumentError("Invalid pair `$k=>$v`: particle number negative"))
2,039✔
776
        0 < k ≤ M || throw(ArgumentError("Invalid pair `$k => $v`: key of of range `1:$M`"))
2,037✔
777
        onr[k] += v
2,033✔
778
    end
2,467✔
779
    return onr
638✔
780
end
781

782
"""
783
    OccupiedPairsMap(addr::SingleComponentFockAddress) <: AbstractVector
784

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

792
# Example
793

794
```jldoctest
795
julia> addr = BoseFS(10, 0, 0, 0, 2, 0, 1)
796
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
797

798
julia> pairs = OccupiedPairsMap(addr)
799
5-element OccupiedPairsMap{78, Tuple{BoseFSIndex, BoseFSIndex}}:
800
 (BoseFSIndex(occnum=10, mode=1, offset=0), BoseFSIndex(occnum=10, mode=1, offset=0))
801
 (BoseFSIndex(occnum=2, mode=5, offset=14), BoseFSIndex(occnum=2, mode=5, offset=14))
802
 (BoseFSIndex(occnum=2, mode=5, offset=14), BoseFSIndex(occnum=10, mode=1, offset=0))
803
 (BoseFSIndex(occnum=1, mode=7, offset=18), BoseFSIndex(occnum=10, mode=1, offset=0))
804
 (BoseFSIndex(occnum=1, mode=7, offset=18), BoseFSIndex(occnum=2, mode=5, offset=14))
805

806
julia> excitation(addr, pairs[2], pairs[4])
807
(BoseFS{13,7}(9, 0, 0, 0, 4, 0, 0), 10.954451150103322)
808
```
809

810
See also [`occupied_mode_map`](@ref).
811
"""
812
struct OccupiedPairsMap{N,T} <: AbstractVector{T}
813
    pairs::SVector{N,T}
66✔
814
    length::Int
815
end
816

817
function OccupiedPairsMap(addr::SingleComponentFockAddress{N}) where {N}
66✔
818
    omm = occupied_mode_map(addr)
137✔
819
    T = eltype(omm)
66✔
820
    P = N * (N - 1) ÷ 2
66✔
821
    pairs = MVector{P,Tuple{T,T}}(undef)
66✔
822
    a = 0
66✔
823
    for i in eachindex(omm)
66✔
824
        p_i = omm[i]
137✔
825
        if p_i.occnum > 1
137✔
826
            a += 1
46✔
827
            @inbounds pairs[a] = (p_i, p_i)
46✔
828
        end
829
        for j in 1:i-1
137✔
830
            p_j = omm[j]
93✔
831
            a += 1
93✔
832
            @inbounds pairs[a] = (p_i, p_j)
93✔
833
        end
115✔
834
    end
208✔
835

836
    return OccupiedPairsMap(SVector(pairs), a)
66✔
837
end
838

839
Base.size(opm::OccupiedPairsMap) = (opm.length,)
1,348✔
840
function Base.getindex(opm::OccupiedPairsMap, i)
2,251✔
841
    @boundscheck 1 ≤ i ≤ opm.length || throw(BoundsError(opm, i))
2,251✔
842
    return opm.pairs[i]
2,251✔
843
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