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

RimuQMC / Rimu.jl / 16170041477

09 Jul 2025 01:01PM UTC coverage: 94.539% (-0.03%) from 94.571%
16170041477

Pull #330

github

web-flow
Merge a3604f84c into 358376721
Pull Request #330: Unified ModeMap Type

92 of 97 new or added lines in 16 files covered. (94.85%)

1 existing line in 1 file now uncovered.

7046 of 7453 relevant lines covered (94.54%)

11618885.32 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,259,957✔
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,131,636✔
30
num_modes(::Type{<:AbstractFockAddress{<:Any,M}}) where {M} = M
485,438,822✔
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,923,778✔
116
    mode_iterator = occupied_modes(b)
206,855,911✔
117
    T = eltype(mode_iterator)
63,035,387✔
118
    for (occnum, mode, offset) in mode_iterator
125,630,568✔
119
        index -= occnum ≥ n
144,845,448✔
120
        if index == 0
144,729,043✔
121
            return T(occnum, mode, offset)
62,982,880✔
122
        end
123
    end
164,486,459✔
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), [`occupied_modes`](@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 be constructed directly. Use [`occupied_mode_map`](@ref) or 
242
[`unoccupied_mode_map`](@ref) to obtain an instance.
243

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

251
Base.eltype(::ModeMap{N,T}) where {N,T} = T
109,867✔
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 Rimu.BitStringAddresses.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 Rimu.BitStringAddresses.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), [`unoccupied_mode_map`](@ref),
301
[`SingleComponentFockAddress`](@ref).
302
"""
303
function occupied_mode_map(addr::SingleComponentFockAddress{N,M}) where {N,M}
97,427,756✔
304
    modes = occupied_modes(addr)
97,427,754✔
305
    T = eltype(modes)
97,427,758✔
306
    # There are at most N occupied modes. This could be also @generated for cases where N ≫ M
307
    L = ismissing(N) ? M : min(N, M)
97,427,738✔
308
    indices = MVector{L,T}(undef)
97,427,751✔
309
    i = 0
97,427,592✔
310
    for index in modes
194,854,787✔
311
        i += 1
407,165,605✔
312
        @inbounds indices[i] = index
407,165,566✔
313
    end
716,902,135✔
314
    return ModeMap(SVector(indices), i)
97,427,806✔
315
end
316

317
Base.size(om::ModeMap) = (om.length,)
440,588,066✔
318
function Base.getindex(om::ModeMap, i)
265,976,260✔
319
    @boundscheck 1 ≤ i ≤ om.length || throw(BoundsError(om, i))
266,087,628✔
320
    return om.indices[i]
266,148,873✔
321
end
322

323
"""
324
    abstract type ModeIterator
325

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

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

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

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

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

351
julia> mb⋅(1:7) == onr(b)⋅(1:7)
352
true
353
```
354
See also [`SingleComponentFockAddress`](@ref).
355
"""
356
function LinearAlgebra.dot(map::ModeMap, vec::AbstractVector)
24,946,790✔
357
    value = zero(eltype(vec))
24,946,804✔
358
    for index in map
49,893,521✔
359
        value += vec[index.mode] * index.occnum
127,402,522✔
360
    end
229,857,772✔
361
    return value
24,946,851✔
362
end
363
LinearAlgebra.dot(vec::AbstractVector, map::ModeMap) = dot(map, vec)
127,402,423✔
364

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

386
"""
387
    parse_address(str)
388

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

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

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

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

476
# Example
477

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

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

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

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

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

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

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

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

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

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

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

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

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

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

546
## Fields:
547

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

554
"""
555
Base.@kwdef struct BoseFSIndex<:FieldVector{3,Int}
2✔
556
    occnum::Int
1,451,390,357✔
557
    mode::Int
558
    offset::Int
559
end
560

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

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

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

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

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

583
# Apply destruction operator to BoseFSIndex.
584
@inline _destroy(d, index::BoseFSIndex) = @set index.occnum -= (d.mode == index.mode)
1,265,313,803✔
585
@inline _destroy(d) = Base.Fix1(_destroy, d)
1,086,519,315✔
586
# Apply creation operator to BoseFSIndex.
587
@inline _create(c, index::BoseFSIndex) = @set index.occnum += (c.mode == index.mode)
244,316,455✔
588
@inline _create(c) = Base.Fix1(_create, c)
550,307,280✔
589

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

662
## Fields:
663

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

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

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

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

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

691
Base.length(::FermiOccupiedModes{N}) where {N} = N
4✔
692
Base.eltype(::FermiOccupiedModes) = FermiFSIndex
×
693

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

793
# Example
794

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

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

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

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

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

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

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