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

RimuQMC / Rimu.jl / 16436702757

22 Jul 2025 06:37AM UTC coverage: 94.472% (-0.1%) from 94.569%
16436702757

push

github

web-flow
Operator traits for iterable offdiagonals and random offdiagonal (#329)

This PR consolidates the changes to the `AbstractHamiltonian` interface
and updates related documentation.

# New features
- `has_random_offdiagonal(operator)` function acts on the type of an
operator and returns `true` if `random_offdiagonal(column)` is
implemented
- `has_iterable_offdiagonals(operator)` function acts on the type of an
operator and returns `true` if `offdiagonal(column)` is implemented
- `operator * address` calls `operator_column(operator, address)`
- `AbstractOperatorColumn`s iterate over Pairs
- `dot(dvec, column)` is defined and returns what is expected
- `column[l_address]` returns the matrix element
``⟨l_address|operator|address⟩``

# Bug fixes
- sort small dvecs before printing to make doctests reproducible; fixes
issue #322

62 of 69 new or added lines in 9 files covered. (89.86%)

4 existing lines in 2 files now uncovered.

7075 of 7489 relevant lines covered (94.47%)

11535176.79 hits per line

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

95.85
/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
* [`occupied_mode_map`](@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

NEW
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), [`occupied_mode_map`](@ref),
77
[`SingleComponentFockAddress`](@ref).
78
"""
79
function find_occupied_mode(b::SingleComponentFockAddress, index::Integer, n=1)
125,852,657✔
80
    mode_iterator = occupied_modes(b)
206,643,510✔
81
    T = eltype(mode_iterator)
63,026,175✔
82
    for (occnum, mode, offset) in mode_iterator
125,707,937✔
83
        index -= occnum ≥ n
145,217,231✔
84
        if index == 0
145,212,128✔
85
            return T(occnum, mode, offset)
63,095,093✔
86
        end
87
    end
164,805,414✔
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 [`occupied_mode_map`](@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
    unoccupied_modes(::FermiFS)
145

146
Return a lazy iterator over all unoccupied modes in an Fermi type address. Iterates over
147
over [`FermiFSIndex`](@ref)s for [`FermiFS`](@ref). 
148
See [`unoccupied_mode_map`](@ref) for an eager version.
149

150
# Example
151

152
```jldoctest
153
julia> f = FermiFS((1,1,0,1,0,0,1));
154

155
julia> foreach(println, unoccupied_modes(f))
156
FermiFSIndex(occnum=0, mode=3, offset=2)
157
FermiFSIndex(occnum=0, mode=5, offset=4)
158
FermiFSIndex(occnum=0, mode=6, offset=5)
159
```
160
See also [`find_occupied_mode`](@ref), [`occupied_modes`](@ref).
161
"""
162
unoccupied_modes
163

164
"""
165
    excitation(addr::SingleComponentFockAddress, creations::NTuple, destructions::NTuple)
166

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

171
```math
172
a^†_{c_1} a^†_{c_2} \\ldots a_{d_1} a_{d_2} \\ldots |\\mathrm{addr}\\rangle \\to
173
α|\\mathrm{naddr}\\rangle
174
```
175

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

180
# Example
181

182
```jldoctest
183
julia> f = FermiFS(1,1,0,0,1,1,1,1)
184
FermiFS{6,8}(1, 1, 0, 0, 1, 1, 1, 1)
185

186
julia> i, j, k, l = find_mode(f, (3,4,2,5))
187
(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))
188

189
julia> excitation(f, (i,j), (k,l))
190
(FermiFS{6,8}(1, 0, 1, 1, 0, 1, 1, 1), -1.0)
191
```
192

193
See [`SingleComponentFockAddress`](@ref).
194
"""
195
excitation
196

197

198
"""
199
    ModeMap <: AbstractVector
200

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

205
This struct is not intended to be constructed directly. Use [`occupied_mode_map`](@ref) or 
206
[`unoccupied_mode_map`](@ref) to obtain an instance.
207

208
See also [`SingleComponentFockAddress`](@ref).
209
"""
210
struct ModeMap{N,T} <: AbstractVector{T}
211
    indices::SVector{N,T} # N = min(N, M)
97,427,853✔
212
    length::Int
213
end
214

215
Base.eltype(::ModeMap{N,T}) where {N,T} = T
109,696✔
216

217
Base.@deprecate OccupiedModeMap(addr) occupied_mode_map(addr)
218

219

220
"""
221
    occupied_mode_map(addr) <: AbstractVector
222
    
223
Get a map of occupied modes in address as an `AbstractVector` of indices compatible with
224
[`excitation`](@ref) - [`BoseFSIndex`](@ref) or [`FermiFSIndex`](@ref).
225

226
`occupied_mode_map(addr)[i]` contains the index for the `i`-th occupied mode.
227
This is useful because repeatedly looking for occupied modes with
228
[`find_occupied_mode`](@ref) can be time-consuming.
229
`occupied_mode_map(addr)` is an eager version of the iterator returned by
230
[`occupied_modes`](@ref). It is similar to [`onr`](@ref) but contains more information.
231

232
# Example
233

234
```jldoctest
235
julia> b = BoseFS(10, 0, 0, 0, 2, 0, 1)
236
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
237

238
julia> mb = occupied_mode_map(b)
239
3-element Rimu.BitStringAddresses.ModeMap{7, BoseFSIndex}:
240
 BoseFSIndex(occnum=10, mode=1, offset=0)
241
 BoseFSIndex(occnum=2, mode=5, offset=14)
242
 BoseFSIndex(occnum=1, mode=7, offset=18)
243

244
julia> f = FermiFS(1,1,1,1,0,0,1,0,0)
245
FermiFS{5,9}(1, 1, 1, 1, 0, 0, 1, 0, 0)
246

247
julia> mf = occupied_mode_map(f)
248
5-element Rimu.BitStringAddresses.ModeMap{5, FermiFSIndex}:
249
 FermiFSIndex(occnum=1, mode=1, offset=0)
250
 FermiFSIndex(occnum=1, mode=2, offset=1)
251
 FermiFSIndex(occnum=1, mode=3, offset=2)
252
 FermiFSIndex(occnum=1, mode=4, offset=3)
253
 FermiFSIndex(occnum=1, mode=7, offset=6)
254

255
julia> mf == collect(occupied_modes(f))
256
true
257

258
julia> dot(mf, mb)
259
11
260

261
julia> dot(mf, 1:20)
262
17
263
```
264
See also [`dot`](@ref Main.Hamiltonians.dot), [`unoccupied_mode_map`](@ref),
265
[`SingleComponentFockAddress`](@ref).
266
"""
267
function occupied_mode_map(addr::SingleComponentFockAddress{N,M}) where {N,M}
97,427,555✔
268
    modes = occupied_modes(addr)
97,427,568✔
269
    T = eltype(modes)
97,427,636✔
270
    # There are at most N occupied modes. This could be also @generated for cases where N ≫ M
271
    L = ismissing(N) ? M : min(N, M)
97,427,653✔
272
    indices = MVector{L,T}(undef)
97,427,651✔
273
    i = 0
97,427,560✔
274
    for index in modes
194,854,061✔
275
        i += 1
407,165,600✔
276
        @inbounds indices[i] = index
407,165,503✔
277
    end
716,902,952✔
278
    return ModeMap(SVector(indices), i)
97,427,775✔
279
end
280

281
Base.size(om::ModeMap) = (om.length,)
441,372,818✔
282
function Base.getindex(om::ModeMap, i)
266,057,587✔
283
    @boundscheck 1 ≤ i ≤ om.length || throw(BoundsError(om, i))
266,041,727✔
284
    return om.indices[i]
266,165,649✔
285
end
286

287
"""
288
    abstract type ModeIterator
289

290
Iterator over modes with `eltype` [`BoseFSIndex`](@ref) or
291
[`FermiFSIndex`](@ref). A subtype of this should be returned when calling
292
[`occupied_modes`](@ref) on a Fock state.
293
"""
294
abstract type ModeIterator end
295

296
"""
297
    dot(map::ModeMap, vec::AbstractVector)
298
    dot(map1::ModeMap, map2::ModeMap)
299
Dot product extracting mode occupation numbers from an [`ModeMap`](@ref) similar
300
to [`onr`](@ref).
301

302
```jldoctest
303
julia> b = BoseFS(10, 0, 0, 0, 2, 0, 1)
304
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
305

306
julia> mb = occupied_mode_map(b)
307
3-element Rimu.BitStringAddresses.ModeMap{7, BoseFSIndex}:
308
 BoseFSIndex(occnum=10, mode=1, offset=0)
309
 BoseFSIndex(occnum=2, mode=5, offset=14)
310
 BoseFSIndex(occnum=1, mode=7, offset=18)
311

312
julia> dot(mb, 1:7)
313
27
314

315
julia> mb⋅(1:7) == onr(b)⋅(1:7)
316
true
317
```
318
See also [`SingleComponentFockAddress`](@ref).
319
"""
320
function LinearAlgebra.dot(map::ModeMap, vec::AbstractVector)
24,946,825✔
321
    value = zero(eltype(vec))
24,946,821✔
322
    for index in map
49,893,609✔
323
        value += vec[index.mode] * index.occnum
127,402,936✔
324
    end
229,858,700✔
325
    return value
24,946,861✔
326
end
327
LinearAlgebra.dot(vec::AbstractVector, map::ModeMap) = dot(map, vec)
127,402,655✔
328

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

350
"""
351
    parse_address(str)
352

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

420
    # BoseFS
421
    m = match(r"\|([ 0-9]+)⟩", str)
220✔
422
    if !isnothing(m)
324✔
423
        return BoseFS(parse.(Int, split(m.captures[1], r" +")))
104✔
424
    end
425
    # Single FermiFS
426
    m = match(r"\|([ ⋅↑]+)⟩", str)
116✔
427
    if !isnothing(m)
232✔
428
        chars = filter(!=(' '), Vector{Char}(m.captures[1]))
232✔
429
        return FermiFS(chars .== '↑')
116✔
430
    end
431
    throw(ArgumentError("invalid Fock state format \"$str\""))
×
432
end
433

434
"""
435
    fs"\$(string)"
436

437
Parse the compact representation of a Fock state.
438
Useful for copying the printout from a vector to the REPL.
439

440
# Example
441

442
```jldoctest
443
julia> DVec(BoseFS{3,4}(0, 1, 2, 0) => 1)
444
DVec{BoseFS{3, 4, BitString{6, 1, UInt8}},Int64} with 1 entry, style = IsStochasticInteger{Int64}()
445
  fs"|0 1 2 0⟩" => 1
446

447
julia> fs"|0 1 2 0⟩" => 1 # Copied from above printout
448
BoseFS{3,4}(0, 1, 2, 0) => 1
449

450
julia> fs"|1 2 3⟩⊗|0 1 0⟩" # composite bosonic Fock state
451
CompositeFS(
452
  BoseFS{6,3}(1, 2, 3),
453
  BoseFS{1,3}(0, 1, 0),
454
)
455

456
julia> fs"|↑↓↑⟩" # construct a fermionic Fock state
457
CompositeFS(
458
  FermiFS{2,3}(1, 0, 1),
459
  FermiFS{1,3}(0, 1, 0),
460
)
461

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

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

470
See also [`FermiFS`](@ref), [`BoseFS`](@ref), [`CompositeFS`](@ref), [`FermiFS2C`](@ref),
471
[`OccupationNumberFS`](@ref).
472
"""
473
macro fs_str(str)
91✔
474
    return parse_address(str)
91✔
475
end
476

477
"""
478
    print_address(io::IO, address)
479

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

483
This function is used to implement `Base.show` for [`AbstractFockAddress`](@ref).
484
"""
485
print_address
486

487
function Base.show(io::IO, add::AbstractFockAddress)
1,284✔
488
    if get(io, :typeinfo, nothing) == typeof(add) || get(io, :compact, false)
2,505✔
489
        print(io, "fs\"")
663✔
490
        print_address(io, add; compact=true)
663✔
491
        print(io, "\"")
663✔
492
    else
493
        print_address(io, add; compact=false)
621✔
494
    end
495
end
496

497
function onr_sparse_string(o)
90✔
498
    ps = map(p -> p[1] => p[2], Iterators.filter(p -> !iszero(p[2]), enumerate(o)))
19,358✔
499
    return join(ps, ", ")
90✔
500
end
501

502
###
503
### Boson stuff
504
###
505
"""
506
    BoseFSIndex
507

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

510
## Fields:
511

512
* `occnum`: the occupation number.
513
* `mode`: the index of the mode.
514
* `offset`: the position of the mode in the address. This is the bit offset of the mode when
515
 the address is represented by a bitstring, and the position in the list when it is
516
 represented by `SortedParticleList`.
517

518
"""
519
Base.@kwdef struct BoseFSIndex<:FieldVector{3,Int}
2✔
520
    occnum::Int
1,449,045,996✔
521
    mode::Int
522
    offset::Int
523
end
524

525
function Base.show(io::IO, i::BoseFSIndex)
54✔
526
    @unpack occnum, mode, offset = i
54✔
527
    print(io, "BoseFSIndex(occnum=$occnum, mode=$mode, offset=$offset)")
54✔
528
end
529
Base.show(io::IO, ::MIME"text/plain", i::BoseFSIndex) = show(io, i)
9✔
530

531
"""
532
    BoseOccupiedModes{C,S<:BoseFS}
533

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

537
See [`occupied_modes`](@ref).
538

539
Defining `Base.length` and `Base.iterate` for this struct is a part of the interface for an
540
underlying storage format used by [`BoseFS`](@ref).
541
"""
542
struct BoseOccupiedModes{N,M,S} <: ModeIterator
543
    storage::S
494,818,284✔
544
end
545
Base.eltype(::BoseOccupiedModes) = BoseFSIndex
×
546

547
# Apply destruction operator to BoseFSIndex.
548
@inline _destroy(d, index::BoseFSIndex) = @set index.occnum -= (d.mode == index.mode)
1,261,598,255✔
549
@inline _destroy(d) = Base.Fix1(_destroy, d)
1,084,186,704✔
550
# Apply creation operator to BoseFSIndex.
551
@inline _create(c, index::BoseFSIndex) = @set index.occnum += (c.mode == index.mode)
244,332,199✔
552
@inline _create(c) = Base.Fix1(_create, c)
549,690,528✔
553

554
"""
555
    bose_excitation_value(
556
        creations::NTuple{_,BoseFSIndex}, destructions::NTuple{_,::BoseFSIndex}
557
    ) -> Int
558

559
Compute the squared value of an excitation from indices. Starts by applying all destruction
560
operators, and then applying all creation operators. The operators must be given in reverse
561
order. Will return 0 if move is illegal.
562
"""
563
@inline bose_excitation_value(::Tuple{}, ::Tuple{}) = 1
×
564
@inline function bose_excitation_value((c, cs...)::NTuple{<:Any,BoseFSIndex}, ::Tuple{})
548,804,474✔
565
    return bose_excitation_value(map(_create(c), cs), ()) * (c.occnum + 1)
549,084,872✔
566
end
567
@inline function bose_excitation_value(
545,852,946✔
568
    creations::NTuple{<:Any,BoseFSIndex}, (d, ds...)::NTuple{<:Any,BoseFSIndex}
569
)
570
    return bose_excitation_value(map(_destroy(d), creations), map(_destroy(d), ds)) * d.occnum
546,042,262✔
571
end
572

573
"""
574
    from_bose_onr(::Type{B}, onr::AbstractArray) -> B
575

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

580
This function is a part of the interface for an underlying storage format used by
581
[`BoseFS`](@ref).
582
"""
583
from_bose_onr
584

585
"""
586
    to_bose_onr(bs::B) -> SVector
587

588
Convert `bs` to a static vector in the occupation number representation format.
589

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

595
"""
596
    bose_excitation(
597
        bs::B, creations::NTuple{N,BoseFSIndex}, destructions::NTuple{N,BoseFSIndex}
598
    ) -> Tuple{B,Float64}
599

600
Perform excitation as if `bs` was a bosonic address. See also
601
[`bose_excitation_value`](@ref).
602

603
This function is a part of the interface for an underlying storage format used by
604
[`BoseFS`](@ref).
605
"""
606
bose_excitation
607

608
"""
609
    bose_num_occupied_modes(bs::B)
610

611
Return the number of occupied modes, if `bs` represents a bosonic address.
612

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

618
###
619
### Fermion stuff
620
###
621
"""
622
    FermiFSIndex
623

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

626
## Fields:
627

628
* `occnum`: the occupation number.
629
* `mode`: the index of the mode.
630
* `offset`: the position of the mode in the address. This is `mode - 1` when the address is
631
  represented by a bitstring, and the position in the list when using `SortedParticleList`.
632

633
"""
634
Base.@kwdef struct FermiFSIndex<:FieldVector{3,Int}
635
    occnum::Int
71,850,387✔
636
    mode::Int
637
    offset::Int
638
end
639

640
function Base.show(io::IO, i::FermiFSIndex)
35✔
641
    @unpack occnum, mode, offset = i
35✔
642
    print(io, "FermiFSIndex(occnum=$occnum, mode=$mode, offset=$offset)")
35✔
643
end
644
Base.show(io::IO, ::MIME"text/plain", i::FermiFSIndex) = show(io, i)
8✔
645

646
"""
647
    FermiOccupiedModes{N,S<:BitString}
648

649
Iterator over occupied modes in address. `N` is the number of fermions. See [`occupied_modes`](@ref).
650
"""
651
struct FermiOccupiedModes{N,S} <: ModeIterator
652
    storage::S
10,981,051✔
653
end
654

655
Base.length(::FermiOccupiedModes{N}) where {N} = N
3✔
656
Base.eltype(::FermiOccupiedModes) = FermiFSIndex
×
657

658
"""
659
    FermiUnoccupiedModes{N}
660

661
Iterator over unoccupied modes in address. `N` is the number of unoccupied orbitals. See [`unoccupied_modes`](@ref).
662
"""
663
struct FermiUnoccupiedModes{N,S} <: ModeIterator
664
    storage::S
9✔
665
end
666
Base.length(::FermiUnoccupiedModes{N}) where {N} = N
4✔
667
Base.eltype(::FermiUnoccupiedModes) = FermiFSIndex
×
668

669
"""
670
    from_fermi_onr(::Type{B}, onr) -> B
671

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

676
This function is a part of the interface for an underlying storage format used by
677
[`FermiFS`](@ref).
678
"""
679
from_fermi_onr
680

681
"""
682
    fermi_find_mode(bs::B, i::Integer) -> FermiFSIndex
683

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

687
This function is a part of the interface for an underlying storage format used by
688
[`FermiFS`](@ref).
689
"""
690
fermi_find_mode
691

692
"""
693
    fermi_excitation(
694
        bs::B, creations::NTuple{N,FermiFSIndex}, destructions::NTuple{N,FermiFSIndex}
695
    ) -> Tuple{B,Float64}
696

697
Perform excitation as if `bs` was a fermionic address.
698

699
This function is a part of the interface for an underlying storage format used by
700
[`FermiFS`](@ref).
701
"""
702
fermi_excitation
703

704
###
705
### General
706
###
707
function LinearAlgebra.dot(occ_a::ModeIterator, occ_b::ModeIterator)
149,758✔
708
    (n_a, i_a, _), st_a = iterate(occ_a)
299,516✔
709
    (n_b, i_b, _), st_b = iterate(occ_b)
299,516✔
710

711
    acc = 0
149,758✔
712
    while true
258,919✔
713
        if i_a > i_b
258,919✔
714
            # b is behind and needs to do a step
715
            iter = iterate(occ_b, st_b)
155,081✔
716
            isnothing(iter) && return acc
155,081✔
717
            (n_b, i_b, _), st_b = iter
54,258✔
718
        elseif i_a < i_b
158,096✔
719
            # a is behind and needs to do a step
720
            iter = iterate(occ_a, st_a)
155,793✔
721
            isnothing(iter) && return acc
155,793✔
722
            (n_a, i_a, _), st_a = iter
54,644✔
723
        else
724
            # a and b are at the same position
725
            acc += n_a * n_b
56,947✔
726
            # now both need to do a step
727
            iter = iterate(occ_a, st_a)
72,954✔
728
            isnothing(iter) && return acc
72,954✔
729
            (n_a, i_a, _), st_a = iter
16,007✔
730
            iter = iterate(occ_b, st_b)
16,266✔
731
            isnothing(iter) && return acc
16,266✔
732
            (n_b, i_b, _), st_b = iter
259✔
733
        end
734
    end
109,161✔
735
end
736

737
function sparse_to_onr(M, pairs)
643✔
738
    onr = spzeros(Int, M)
643✔
739
    for (k, v) in pairs
681✔
740
        v ≥ 0 || throw(ArgumentError("Invalid pair `$k=>$v`: particle number negative"))
2,037✔
741
        0 < k ≤ M || throw(ArgumentError("Invalid pair `$k => $v`: key of of range `1:$M`"))
2,035✔
742
        onr[k] += v
2,031✔
743
    end
2,465✔
744
    return onr
638✔
745
end
746

747
"""
748
    OccupiedPairsMap(addr::SingleComponentFockAddress) <: AbstractVector
749

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

757
# Example
758

759
```jldoctest
760
julia> addr = BoseFS(10, 0, 0, 0, 2, 0, 1)
761
BoseFS{13,7}(10, 0, 0, 0, 2, 0, 1)
762

763
julia> pairs = OccupiedPairsMap(addr)
764
5-element OccupiedPairsMap{78, Tuple{BoseFSIndex, BoseFSIndex}}:
765
 (BoseFSIndex(occnum=10, mode=1, offset=0), BoseFSIndex(occnum=10, mode=1, offset=0))
766
 (BoseFSIndex(occnum=2, mode=5, offset=14), BoseFSIndex(occnum=2, mode=5, offset=14))
767
 (BoseFSIndex(occnum=2, mode=5, offset=14), BoseFSIndex(occnum=10, mode=1, offset=0))
768
 (BoseFSIndex(occnum=1, mode=7, offset=18), BoseFSIndex(occnum=10, mode=1, offset=0))
769
 (BoseFSIndex(occnum=1, mode=7, offset=18), BoseFSIndex(occnum=2, mode=5, offset=14))
770

771
julia> excitation(addr, pairs[2], pairs[4])
772
(BoseFS{13,7}(9, 0, 0, 0, 4, 0, 0), 10.954451150103322)
773
```
774

775
See also [`occupied_mode_map`](@ref).
776
"""
777
struct OccupiedPairsMap{N,T} <: AbstractVector{T}
778
    pairs::SVector{N,T}
66✔
779
    length::Int
780
end
781

782
function OccupiedPairsMap(addr::SingleComponentFockAddress{N}) where {N}
66✔
783
    omm = occupied_mode_map(addr)
137✔
784
    T = eltype(omm)
66✔
785
    P = N * (N - 1) ÷ 2
66✔
786
    pairs = MVector{P,Tuple{T,T}}(undef)
66✔
787
    a = 0
66✔
788
    for i in eachindex(omm)
66✔
789
        p_i = omm[i]
137✔
790
        if p_i.occnum > 1
137✔
791
            a += 1
46✔
792
            @inbounds pairs[a] = (p_i, p_i)
46✔
793
        end
794
        for j in 1:i-1
137✔
795
            p_j = omm[j]
93✔
796
            a += 1
93✔
797
            @inbounds pairs[a] = (p_i, p_j)
93✔
798
        end
115✔
799
    end
208✔
800

801
    return OccupiedPairsMap(SVector(pairs), a)
66✔
802
end
803

804
Base.size(opm::OccupiedPairsMap) = (opm.length,)
1,348✔
805
function Base.getindex(opm::OccupiedPairsMap, i)
2,251✔
806
    @boundscheck 1 ≤ i ≤ opm.length || throw(BoundsError(opm, i))
2,251✔
807
    return opm.pairs[i]
2,251✔
808
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