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

RimuQMC / Rimu.jl / 16178081864

09 Jul 2025 07:07PM UTC coverage: 94.55% (-0.02%) from 94.571%
16178081864

Pull #330

github

lch
Unified ModeMap Type (#330)

1. Resolve unoccupied_modes method test failed when FermiFS is
   constructed with SortedParticleList
Pull Request #330: Unified ModeMap Type

106 of 112 new or added lines in 17 files covered. (94.64%)

1 existing line in 1 file now uncovered.

7061 of 7468 relevant lines covered (94.55%)

11619041.62 hits per line

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

96.73
/src/BitStringAddresses/bitstring.jl
1
"""
2
    num_chunks(::Val{B})
3

4
Determine the number and type of chunks needed to store `B` bits.
5
"""
6
function num_chunks(::Val{B}) where {B}
29,576,832✔
7
    if B ≤ 0
29,576,826✔
8
        throw(ArgumentError("`B` must be positive!"))
2✔
9
    elseif B ≤ 8
29,576,830✔
10
        return 1, UInt8
72,001✔
11
    elseif B ≤ 16
29,504,832✔
12
        return 1, UInt16
25,243,353✔
13
    elseif B ≤ 32
4,261,479✔
14
        return 1, UInt32
1,379,257✔
15
    else
16
        return (B - 1) ÷ 64 + 1, UInt64
2,882,223✔
17
    end
18
end
19

20
"""
21
    check_bitstring_typeparams(::Val{B}, ::Val{N})
22

23
Check if number of bits `B` is consistent with number of chunks `N`. Throw an error if not.
24
"""
25
function check_bitstring_typeparams(::Val{B}, ::Val{N}, ::Type{UInt64}) where {B,N}
1,609,495✔
26
    if B > N * 64
1,609,495✔
27
        error("$B bits do not fit into $N 64-bit chunks")
×
28
    elseif B ≤ (N - 1) * 64
1,609,495✔
29
        error("$B bits fit into $(N - 1) 64-bit chunks, but $N chunks were provided")
×
30
    end
31
end
32
function check_bitstring_typeparams(::Val{B}, ::Val{1}, ::Type{T}) where {B,T}
637,272,025✔
33
    if B > sizeof(T) * 8
637,311,445✔
34
        error("$B bits do not fit into a $(sizeof(T) * 8)-bit chunk")
×
35
    end
36
end
37
function check_bitstring_typeparams(::Val{B}, ::Val{1}, ::Type{UInt64}) where {B}
5,594,999✔
38
    if B > 64
5,594,999✔
39
        error("$B bits do not fit into a 64-bit chunk")
×
40
    end
41
end
42
function check_bitstring_typeparams(::Val{B}, ::Val{N}, ::Type{T}) where {B,N,T}
×
43
    error("Only `UInt64` is supported for multi-bit chunks")
×
44
end
45

46
"""
47
    BitString{B,N,T<:Unsigned}
48

49
Type for storing bitstrings of static size. Holds `B` bits in `N` chunks, where each chunk is
50
of type `T`.
51

52
`N` is chosen automatically to accommodate `B` bits as efficiently as possible.
53

54
# Constructors
55

56
* `BitString{B,N,T}(::SVector{N,T})`: unsafe constructor. Does not check for ghost bits.
57

58
* `BitString{B,N,T}(i::T)`: as above, but sets `i` as the rightmost chunk.
59

60
* `BitString{B}(::Integer)`: Convert integer to `BitString`. Integer is truncated to the
61
  correct number of bits.
62

63
"""
64
struct BitString{B,N,T<:Unsigned}
65
    chunks::SVector{N,T}
66

67
    # This constructor is only to be used internally. It doesn't check for ghost bits.
68
    function BitString{B,N,T}(s::SVector{N,T}) where {B,N,T}
60,914,376✔
69
        check_bitstring_typeparams(Val(B), Val(N), T)
60,914,373✔
70
        return new{B,N,T}(s)
60,914,386✔
71
    end
72
    function BitString{B,N,T}(i::T) where {B,N,T<:Unsigned}
583,167,947✔
73
        check_bitstring_typeparams(Val(B), Val(N), T)
583,461,941✔
74
        return new{B,N,T}(setindex(zero(SVector{N,UInt64}), i, N))
583,648,923✔
75
    end
76
end
77

78
###
79
### Basic properties.
80
###
81
"""
82
    num_chunks(::Type{<:BitString})
83
    num_chunks(s::BitString)
84

85
Number of chunks in bitstring. Equivalent to `length(chunks(s))`.
86
"""
87
num_chunks(::Type{<:BitString{<:Any,N}}) where {N} = N
119,609,257✔
88

89
"""
90
    chunk_type(::Type{<:BitString})
91
    chunk_type(s::BitString)
92

93
Type of unsigned integer used to store the chunks.
94
"""
95
chunk_type(::Type{<:BitString{<:Any,<:Any,T}}) where {T} = T
523,070✔
96

97
"""
98
    num_bits(::Type{<:BitString})
99
    num_bits(s::BitString)
100

101
Total number of bits stored in bitstring.
102
"""
103
num_bits(::Type{<:BitString{B}}) where {B} = B
316,385✔
104

105
"""
106
    top_chunk_bits(::Type{<:BitString})
107
    top_chunk_bits(s::BitString)
108

109
Number of bits stored in top chunk. Equivalent to `chunk_bits(s, 1)`.
110
"""
111
function top_chunk_bits(::Type{<:BitString{B}}) where B
50,281,271✔
112
    return B % 64 == 0 ? 64 : B % 64
50,281,271✔
113
end
114

115
for f in (:num_chunks, :chunk_type, :num_bits, :top_chunk_bits)
116
    @eval $f(s::BitString) = $f(typeof(s))
75,688,767✔
117
end
118

119
"""
120
    chunks(s::BitString)
121

122
`SVector` that stores the chunks of `s`.
123
"""
124
chunks(s::BitString) = s.chunks
49,748,556✔
125

126
"""
127
    chunks_bits(::Type{<:BitString}, i)
128
    chunks_bits(s, i)
129

130
Number of bits in the `i`-th chunk of `s`.
131
"""
132
chunk_bits(s, i) = chunk_bits(typeof(s), i)
32,073,403✔
133
chunk_bits(::Type{<:BitString{B,1}}, _) where {B} = B
29,319,088✔
134
function chunk_bits(::Type{S}, i) where {S<:BitString}
49,761,761✔
135
    return ifelse(i == 1, top_chunk_bits(S), 64)
49,761,761✔
136
end
137

138
function ghost_bit_mask(::Type{S}) where S<:BitString
519,235✔
139
    T = chunk_type(S)
519,235✔
140
    unused_bits = sizeof(T) * 8 - top_chunk_bits(S)
519,235✔
141
    return ~zero(T) >>> unused_bits
519,235✔
142
end
143

144
"""
145
    remove_ghost_bits(s::BitString)
146

147
Remove set bits outside data field if any are present.
148

149
See also: [`has_ghost_bits`](@ref).
150
"""
151
function remove_ghost_bits(s::S) where {S<:BitString}
419,202✔
152
    mask = ghost_bit_mask(S)
419,202✔
153
    return S(setindex(s.chunks, s.chunks[1] & mask, 1))
419,202✔
154
end
155

156
@inline function remove_ghost_bits(s::S) where {S<:BitString{<:Any,1}}
96,499✔
157
    mask = ghost_bit_mask(S)
96,499✔
158
    return S(chunks(s) .& mask)
96,499✔
159
end
160

161
"""
162
    has_ghost_bits(s::BitString)
163

164
Check for bits outside data field.
165

166
See also: [`remove_ghost_bits`](@ref).
167
"""
168
function has_ghost_bits(s::S) where {S<:BitString}
40✔
169
    top = first(chunks(s))
40✔
170
    mask = ~zero(UInt64) << top_chunk_bits(S)
40✔
171
    return top & mask > 0
40✔
172
end
173

174
###
175
### Alternative/useful constructors. These are not super efficient, but they are safe.
176
###
177
function BitString{B}(i::Union{Int128,Int64,Int32,Int16,Int8}) where {B}
257,638✔
178
    return remove_ghost_bits(BitString{B}(unsigned(i)))
257,638✔
179
end
180
function BitString{B}(i::Union{UInt64,UInt32,UInt16,UInt8}) where {B}
257,653✔
181
    N, T = num_chunks(Val(B))
257,653✔
182
    s = setindex(zero(SVector{N,T}), T(i), N)
257,653✔
183
    return remove_ghost_bits(BitString{B,N,T}(s))
257,653✔
184
end
185
function BitString{B}(i::UInt128) where {B}
19✔
186
    N, T = num_chunks(Val(B))
19✔
187
    left = i >>> 0x40 % T # left will only be used if T == UInt64 and N > 1
19✔
188
    right = i  % T
19✔
189
    s = ntuple(Val(N)) do i
19✔
190
        i == N ? right : i == N - 1 ? left : zero(T)
39✔
191
    end
192
    return remove_ghost_bits(BitString{B,N,T}(SVector{N,T}(s)))
19✔
193
end
194
function BitString{B}(i::BigInt) where {B}
12✔
195
    N, T = num_chunks(Val(B))
12✔
196
    s = zero(SVector{N,T})
12✔
197
    j = N
12✔
198
    while i ≠ 0
34✔
199
        chunk = i & typemax(T) % T
44✔
200
        i >>>= 64 # Can use 64 here, as only 1-chunk addresses can be smaller
22✔
201
        s = setindex(s, chunk, j)
22✔
202
        j -= 1
22✔
203
    end
22✔
204
    return remove_ghost_bits(BitString{B,N,T}(s))
12✔
205
end
206

207
function Base.zero(S::Type{<:BitString{B}}) where {B}
29,319,117✔
208
    N, T = num_chunks(Val(B))
29,319,121✔
209
    BitString{B,N,T}(zero(SVector{N,T}))
29,319,126✔
210
end
211
Base.zero(s::BitString) = zero(typeof(s))
29,319,104✔
212

213
function Base.show(io::IO, s::BitString{B,N}) where {B,N}
6✔
214
    str = join(map(i -> repr(i)[3:end], s.chunks), '_')
20✔
215

216
    print(io, "BitString{$B}(big\"0x", str, "\")")
6✔
217
end
218
Base.bitstring(s::BitString{B}) where {B} = join(bitstring.(s.chunks))[(end - B + 1):end]
×
219

220
###
221
### Operations on BitStrings
222
###
223
for op in (:⊻, :&, :|)
224
    @eval (Base.$op)(l::S, r::S) where S<:BitString = S($op.(l.chunks, r.chunks))
465,919✔
225
end
226
Base.:~(s::S) where S<:BitString = remove_ghost_bits(S(.~(s.chunks)))
49✔
227

228
Base.count_ones(s::BitString) = sum(count_ones, s.chunks)
462,150✔
229
Base.count_zeros(s::BitString) = num_bits(s) - count_ones(s)
23✔
230

231
function _trailing(f, s::BitString)
29,319,160✔
232
    result = 0
29,319,161✔
233
    i = 0
29,319,165✔
234
    # Idea: if all whole chunk is the same digit, you have to look at the next one.
235
    # This gets compiled away if N=1
236
    for i in num_chunks(s):-1:1
29,319,164✔
237
        r = f(s.chunks[i])
29,319,162✔
238
        result += r
29,319,156✔
239
        r == chunk_bits(s, i) || break
29,319,159✔
240
    end
×
241
    # If top chunk occupies the whole integer, result will always be smaller or equal to B.
242
    if f ≢ trailing_ones && top_chunk_bits(s) ≠ 64
29,319,167✔
243
        return min(num_bits(s), result)
23✔
244
    else
245
        return result
29,319,142✔
246
    end
247
end
248

249
function _leading(f, s::BitString)
79✔
250
    N = sizeof(chunk_type(s)) * 8
79✔
251
    # First chunk is a special case - we have to ignore the empty space before the string.
252
    result = min(f(s.chunks[1] << (N - top_chunk_bits(s))), top_chunk_bits(s))
79✔
253

254
    # This gets compiled away if N=1
255
    if num_chunks(s) > 1 && result == top_chunk_bits(s)
79✔
256
        for i in 2:num_chunks(s)
14✔
257
            r = f(s.chunks[i])
14✔
258
            result += r
14✔
259
            r == 64 || break
14✔
260
        end
×
261
    end
262
    return result
79✔
263
end
264

265
Base.trailing_ones(s::BitString) = _trailing(trailing_ones, s)
29,319,132✔
266
Base.trailing_zeros(s::BitString) = _trailing(trailing_zeros, s)
23✔
267
Base.leading_ones(s::BitString) = _leading(leading_ones, s)
56✔
268
Base.leading_zeros(s::BitString) = _leading(leading_zeros, s)
23✔
269

270
@generated function _right_shift(s::S, k) where {S<:BitString}
146✔
271
    N = num_chunks(S)
20✔
272
    quote
20✔
273
        $(Expr(:meta, :inline))
138✔
274
        # equivalent to d, r = divrem(k, 64)
275
        d = k >>> 0x6
138✔
276
        r = k & 63
138✔
277
        ri = 64 - r
138✔
278
        mask = ~zero(UInt64) >>> ri # 2^r-1 # 0b0...01...1 with `r` 1s
138✔
279
        c = chunks(s)
138✔
280

281
        @nif $(N + 1) l -> (d < l) l -> (
146✔
282
            S(SVector((@ntuple l - 1 k -> zero(UInt64))... ,c[1] >>> r,
283
                      (@ntuple $N-l q -> (c[q + 1] >>> r | ((c[q] & mask) << ri)))...
284
                      ))
285
        ) l -> (
286
            return zero(S)
287
        )
288
    end
289
end
290

291
function _left_shift(s::S, k) where {S<:BitString}
3,534✔
292
    result = zeros(MVector{num_chunks(S),UInt64})
3,534✔
293
    # d, r = divrem(k, 64)
294
    d = k >>> 0x6
3,534✔
295
    r = k & 63
3,534✔
296

297
    shift = s.chunks .<< (r % UInt64)
3,534✔
298
    carry = s.chunks .>>> ((64 - r) % UInt64)
3,534✔
299

300
    for i in d + 1:length(result)
3,535✔
301
        @inbounds result[i - d] = shift[i] | get(carry, i + 1, zero(UInt64))
13,195✔
302
    end
22,857✔
303
    # This bit removes ghost bits.
304
    result[1] &= ghost_bit_mask(S)
3,534✔
305
    return S(SVector(result))
3,534✔
306
end
307

308
Base.:>>(s::BitString, k) = k ≥ 0 ? _right_shift(s, k) : _left_shift(s, -k)
126✔
309
Base.:<<(s::BitString, k) = k > 0 ? _left_shift(s, k) : _right_shift(s, -k)
3,554✔
310
Base.:>>>(s::BitString, k) = s >> k
29,319,095✔
311

312
# remove ghost bits must be applied to both because k might be negative.
313
Base.:>>(s::S, k) where S<:BitString{<:Any,1} = remove_ghost_bits(S(s.chunks .>> k))
28✔
314
Base.:>>(s::S, k::Unsigned) where S<:BitString{<:Any,1} = S(s.chunks .>> k)
29,319,064✔
315
Base.:<<(s::S, k) where S<:BitString{<:Any,1} = remove_ghost_bits(S(s.chunks .<< k))
290✔
316

317
function Base.isless(s1::B, s2::B) where {B<:BitString}
208,801✔
318
    for i in 1:num_chunks(B)
208,801✔
319
        if chunks(s1)[i] ≠ chunks(s2)[i]
208,801✔
320
            return chunks(s1)[i] < chunks(s2)[i]
203,380✔
321
        end
322
    end
5,421✔
323
    return false
5,421✔
324
end
325
Base.isodd(s::BitString) = isodd(chunks(s)[end])
50✔
326
Base.iseven(s::BitString) = iseven(chunks(s)[end])
×
327

328
# For compatibility. Changing any of the hashes will slightly change results and make the
329
# tests fail.
330
Base.hash(b::BitString{<:Any,1}, h::UInt) = hash(b.chunks[1], h)
876,055,200✔
331
Base.hash(b::BitString, h::UInt) = hash(b.chunks.data, h)
×
332

333
"""
334
    partial_left_shift(bs::BitString, i, j)
335

336
Shift a part of the bitstring left by one place with boundaries `i < j`.
337
In a `BoseFS` bitstring, it moves a particle at offset `i` to the position at
338
offset `j`.
339

340
See also: [`excitation`](@ref), [`partial_right_shift`](@ref).
341
"""
342
function partial_left_shift(chunk::T, i, j) where {T<:Unsigned}
274,925,749✔
343
    # Mask of one spanning from i to j
344
    mask = (T(1) << T(j - i + 1) - T(1)) << T(i)
274,933,058✔
345
    # Shift the part of the string that needs to be shifted, ensure a one is added at the end
346
    # swap shift to move in other direction
347
    #println(bitstring(mask))
348
    shifted_part = ((chunk & mask) << 0x1) & mask
274,776,020✔
349
    # Leave the rest intact
350
    intact_part = chunk & ~mask
274,695,043✔
351

352
    return shifted_part | intact_part | T(1) << T(i)
274,782,990✔
353
end
354

355
"""
356
    partial_right_shift(bs::BitString, i, j)
357

358
Shift a part of the bitstring right by one place with boundaries `i < j`.
359
In a `BoseFS` bitstring, it moves a particle at offset `j` to the position at
360
offset `i`.
361

362
See also: [`partial_left_shift`](@ref), [`excitation`](@ref).
363
"""
364
function partial_right_shift(chunk::T, i, j) where {T<:Unsigned}
277,214,319✔
365
    # Mask of one spanning from i to j
366
    mask = (T(1) << T(j - i + 1) - T(1)) << T(i)
277,199,051✔
367
    # Shift the part of the string that needs to be shifted, ensure a one is added at the end
368
    # swap shift to move in other direction
369
    shifted_part = ((chunk & mask) >> 0x1) & mask
277,319,383✔
370
    # Leave the rest intact
371
    intact_part = chunk & ~mask
277,354,633✔
372
    #println(lpad("↑" * " "^j, length(bitstring(chunk))))
373

374
    return shifted_part | intact_part | T(1) << T(j)
277,341,698✔
375
end
376

377
function partial_left_shift(bs::S, i, j) where {S<:BitString{<:Any,1}}
274,474,736✔
378
    return S(partial_left_shift(bs.chunks[1], i, j))
274,319,539✔
379
end
380

381
function partial_right_shift(bs::S, i, j) where {S<:BitString{<:Any,1}}
276,729,453✔
382
    return S(partial_right_shift(bs.chunks[1], i, j))
276,752,090✔
383
end
384

385
function partial_left_shift(bs::S, i, j) where {N,S<:BitString{<:Any,N}}
211,395✔
386
    result = MVector(bs.chunks)
211,395✔
387
    lo_idx = N - (i >>> 0x6)
211,395✔
388
    hi_idx = N - (j >>> 0x6)
211,395✔
389
    lo_off = i & 63
211,395✔
390
    hi_off = j & 63
211,395✔
391
    @inbounds if hi_idx == lo_idx
211,395✔
392
        result[hi_idx] = partial_left_shift(result[hi_idx], lo_off, hi_off)
68,922✔
393
    else
394
        # Top part first.
395
        chunk = result[hi_idx]
142,473✔
396
        chunk = partial_left_shift(chunk, 0, hi_off)
142,473✔
397
        # Carry bit.
398
        chunk &= -UInt(1) << 0x1
142,473✔
399
        chunk |= result[hi_idx + 1] >> 63
142,473✔
400
        result[hi_idx] = chunk
142,473✔
401

402
        idx = hi_idx + 1
142,473✔
403
        while idx < lo_idx
224,070✔
404
            chunk = result[idx]
81,597✔
405
            chunk <<= 0x1
81,597✔
406
            chunk |= result[idx + 1] >> 63
81,597✔
407
            result[idx] = chunk
81,597✔
408
            idx += 1
81,597✔
409
        end
81,597✔
410

411
        # Bottom part.
412
        chunk = result[lo_idx]
142,473✔
413
        chunk = partial_left_shift(chunk, lo_off, 64)
142,473✔
414
        result[lo_idx] = chunk
142,473✔
415
    end
416
    return S(SVector(result))
211,395✔
417
end
418

419
function partial_right_shift(bs::S, i, j) where {N,S<:BitString{<:Any,N}}
211,699✔
420
    result = MVector(bs.chunks)
211,699✔
421
    lo_idx = N - (i >>> 0x6)
211,699✔
422
    hi_idx = N - (j >>> 0x6)
211,699✔
423
    lo_off = i & 63
211,699✔
424
    hi_off = j & 63
211,699✔
425
    @inbounds if hi_idx == lo_idx
211,699✔
426
        result[hi_idx] = partial_right_shift(result[hi_idx], lo_off, hi_off)
72,572✔
427
    else
428
        # Bottom first
429
        chunk = result[lo_idx]
139,127✔
430
        chunk = partial_right_shift(chunk, lo_off, 64)
139,127✔
431
        # Carry bit.
432
        chunk &= -UInt(1) >> 0x1
139,127✔
433
        chunk |= result[lo_idx - 1] << 63
139,127✔
434
        result[lo_idx] = chunk
139,127✔
435

436
        idx = lo_idx - 1
139,127✔
437
        while idx > hi_idx
234,581✔
438
            chunk = result[idx]
95,454✔
439
            chunk >>= 0x1
95,454✔
440
            chunk |= result[idx - 1] << 63
95,454✔
441
            result[idx] = chunk
95,454✔
442
            idx -= 1
95,454✔
443
        end
95,454✔
444

445
        # Top part.
446
        chunk = result[hi_idx]
139,127✔
447
        chunk = partial_right_shift(chunk, 0, hi_off)
139,127✔
448
        result[hi_idx] = chunk
139,127✔
449
    end
450
    return S(SVector(result))
211,699✔
451
end
452

453
function Base.bitreverse(bs::BitString{B,1,T}) where {B,T}
3,162✔
454
    return typeof(bs)(SVector(bitreverse(bs.chunks[1]) >> T(sizeof(T) * 8 - B)))
3,162✔
455
end
456
function Base.bitreverse(bs::BitString{B,N}) where {B,N}
40✔
457
    return typeof(bs)(bitreverse.(reverse(bs.chunks))) >> (64 * N - B)
40✔
458
end
459
Base.reverse(bs::BitString) = bitreverse(bs)
3,202✔
460

461
###
462
### Bose interface
463
###
464
function from_bose_onr(::Type{S}, onr) where {T,S<:BitString{<:Any,1,T}}
40,172✔
465
    result = zero(T)
40,172✔
466
    for i in length(onr):-1:1
40,173✔
467
        curr_occnum = T(onr[i])
562,908✔
468
        result <<= curr_occnum + T(1)
562,908✔
469
        result |= one(T) << curr_occnum - T(1)
562,908✔
470
    end
1,085,644✔
471
    return S(SVector(result))
40,172✔
472
end
473
function from_bose_onr(::Type{S}, onr) where {K,S<:BitString{<:Any,K}}
205,034✔
474
    result = zeros(MVector{K,UInt64})
205,034✔
475
    offset = 0
205,034✔
476
    bits_left = chunk_bits(S, K)
205,034✔
477
    i = 1
205,034✔
478
    j = K
205,034✔
479
    while true
14,786,162✔
480
        # Write number to result
481
        curr_occnum = onr[i]
14,786,162✔
482
        while curr_occnum > 0
36,335,642✔
483
            x = min(curr_occnum, bits_left)
11,082,395✔
484
            mask = (one(UInt64) << x - 1) << offset
10,777,610✔
485
            @inbounds result[j] |= mask
10,777,610✔
486
            bits_left -= x
10,777,610✔
487
            offset += x
10,777,610✔
488
            curr_occnum -= x
11,082,395✔
489

490
            if bits_left == 0
10,777,610✔
491
                j -= 1
614,159✔
492
                offset = 0
614,159✔
493
                bits_left = chunk_bits(S, j)
614,159✔
494
            end
495
        end
10,777,610✔
496
        offset += 1
14,786,162✔
497
        bits_left -= 1
14,786,162✔
498

499
        if bits_left == 0
14,786,162✔
500
            j -= 1
244,246✔
501
            offset = 0
244,246✔
502
            bits_left = chunk_bits(S, j)
244,246✔
503
        end
504
        i += 1
14,786,162✔
505
        i > length(onr) && break
14,786,162✔
506
    end
14,581,128✔
507
    return S(SVector(result))
205,034✔
508
end
509

510
# Version specialized for single-chunk addresses.
511
@inline function to_bose_onr(bs::BitString{<:Any,1}, ::Val{M}) where {M}
6,048,922✔
512
    result = zeros(MVector{M,Int32})
6,048,922✔
513
    for mode in 1:M
6,048,921✔
514
        bosons = Int32(trailing_ones(bs))
29,319,042✔
515
        @inbounds result[mode] = bosons
29,319,055✔
516
        bs >>>= (bosons + 1) % UInt
29,319,055✔
517
        iszero(bs) && break
29,319,056✔
518
    end
23,270,144✔
519
    return SVector(result)
6,048,922✔
520
end
521
# Version specialized for multi-chunk addresses. This is quite a bit faster for large
522
# addresses.
523
@inline function to_bose_onr(bs::BitString{<:Any,K}, ::Val{M}) where {K,M}
304,700✔
524
    B = num_bits(bs)
304,700✔
525
    result = zeros(MVector{M,Int32})
304,700✔
526
    mode = 1
304,700✔
527
    i = K
304,700✔
528
    while true
1,409,753✔
529
        chunk = chunks(bs)[i]
1,409,753✔
530
        bits_left = chunk_bits(bs, i)
1,409,753✔
531
        while !iszero(chunk)
22,538,839✔
532
            bosons = trailing_ones(chunk)
21,129,086✔
533
            @inbounds result[mode] += unsafe_trunc(Int32, bosons)
21,129,086✔
534
            chunk >>>= bosons % UInt
21,129,086✔
535
            empty_modes = trailing_zeros(chunk)
21,129,086✔
536
            mode += empty_modes
21,129,086✔
537
            chunk >>>= empty_modes % UInt
21,129,086✔
538
            bits_left -= bosons + empty_modes
21,129,086✔
539
        end
21,129,086✔
540
        i == 1 && break
1,409,753✔
541
        i -= 1
1,105,053✔
542
        mode += bits_left
1,105,053✔
543
    end
1,105,053✔
544
    return SVector(result)
304,700✔
545
end
546

547
# Fix offsets that changed after performing a move.
548
@inline function _fix_offset(pair, index::BoseFSIndex)
487,151,079✔
549
    fst, snd = pair[1], pair[2]
487,551,291✔
550
    if fst.offset < snd.offset
487,866,042✔
551
        return @set index.offset += fst.offset < index.offset ≤ snd.offset
179,002,003✔
552
    else
553
        return @set index.offset -= fst.offset > index.offset > snd.offset
309,974,466✔
554
    end
555
end
556
_fix_offset(pair) = Base.Fix1(_fix_offset, pair)
244,150,818✔
557

558
# Move a single particle
559
function bose_move_particle(bs::BitString, from, to)
549,525,054✔
560
    if to == from
549,449,007✔
561
        return bs
20,555✔
562
    elseif to < from
549,877,225✔
563
        return partial_left_shift(bs, to, from)
274,670,679✔
564
    else
565
        return partial_right_shift(bs, from, to - 1)
276,962,000✔
566
    end
567
end
568

569
# Move multiple particles. This does not care about values, so it performs moves in an
570
# arbitrary order (from left to right in pairs).
571
@inline function bose_move_particles(bs::BitString, (c,)::NTuple{1}, (d,)::NTuple{1})
307,529,310✔
572
    return bose_move_particle(bs, d.offset, c.offset)
613,946,062✔
573
end
574
@inline function bose_move_particles(bs::BitString, (c, cs...), (d, ds...))
244,375,959✔
575
    bs = bose_move_particle(bs, d.offset, c.offset)
487,099,900✔
576
    fix = _fix_offset(c => d)
244,144,404✔
577
    bs = bose_move_particles(bs, map(fix, cs), map(fix, ds))
486,497,126✔
578
    return bs
244,257,159✔
579
end
580

581
function bose_excitation(
307,030,590✔
582
    bs::BitString, creations::NTuple{N}, destructions::NTuple{N}
583
) where N
584
    # We start by computing the value. This is where the check if the move is even legal
585
    # is done.
586
    creations_rev = reverse(creations)
307,066,737✔
587
    value = bose_excitation_value(creations_rev, reverse(destructions))
307,018,312✔
588
    if iszero(value)
307,620,718✔
589
        return bs, 0.0
249,529✔
590
    else
591
        # Now that we know the value and that the move is legal, we can apply the moves
592
        # without worrying about doing something weird.
593
        return bose_move_particles(bs, creations_rev, destructions), √value
307,478,782✔
594
    end
595
end
596

597
function bose_num_occupied_modes(bs::BitString{<:Any,1})
17,910,287✔
598
    chunk = bs.chunks[1]
17,912,852✔
599
    result = 0
17,916,043✔
600
    while true
65,608,509✔
601
        chunk >>= (trailing_zeros(chunk) % UInt)
65,618,938✔
602
        chunk >>= (trailing_ones(chunk) % UInt)
65,636,944✔
603
        result += 1
65,649,871✔
604
        iszero(chunk) && break
65,658,651✔
605
    end
47,773,763✔
606
    return result
17,927,440✔
607
end
608
function bose_num_occupied_modes(bs::BitString)
97✔
609
    # This version is faster than using the occupied_mode iterator
610
    result = 0
97✔
611
    K = num_chunks(bs)
97✔
612
    last_mask = UInt64(1) << 63 # = 0b100000...
97✔
613
    prev_top_bit = false
97✔
614
    for i in K:-1:1
97✔
615
        chunk = chunks(bs)[i]
415✔
616
        # This part handles modes that span across chunk boundaries.
617
        # If the previous top bit and the current bottom bit are both 1, we have to subtract
618
        # 1 from the result or the mode will be counted twice.
619
        result -= (chunk & prev_top_bit) % Int
415✔
620
        prev_top_bit = (chunk & last_mask) > 0
415✔
621
        while !iszero(chunk)
5,802✔
622
            chunk >>>= trailing_zeros(chunk)
5,387✔
623
            chunk >>>= trailing_ones(chunk)
5,387✔
624
            result += 1
5,387✔
625
        end
5,387✔
626
    end
733✔
627
    return result
97✔
628
end
629

630
# Iterator stuff. Alias for type added here to make the following code less verbose.
631
const DenseBoseOccupiedModes{K} = BoseOccupiedModes{N,M,BitString{B,K,T}} where {N,M,B,T}
632

633
Base.length(bom::DenseBoseOccupiedModes) = bose_num_occupied_modes(bom.storage)
48✔
634

635
# Single chunk versions are simpler.
636
@inline function Base.iterate(bom::DenseBoseOccupiedModes{1})
494,464,720✔
637
    chunk = bom.storage.chunks[1]
494,548,197✔
638
    empty_modes = trailing_zeros(chunk)
494,602,716✔
639
    return iterate(
987,597,934✔
640
        bom, (chunk >> (empty_modes % UInt), empty_modes, 1 + empty_modes)
641
    )
642
end
643
@inline function Base.iterate(bom::DenseBoseOccupiedModes{1}, (chunk, bit, mode))
583,334,855✔
644
    if iszero(chunk)
583,596,419✔
645
        return nothing
182,222,199✔
646
    else
647
        bosons = trailing_ones(chunk)
500,396,472✔
648
        chunk >>>= (bosons % UInt)
500,397,172✔
649
        empty_modes = trailing_zeros(chunk)
500,759,496✔
650
        chunk >>>= (empty_modes % UInt)
500,709,253✔
651
        next_bit = bit + bosons + empty_modes
501,034,308✔
652
        next_mode = mode + empty_modes
500,977,143✔
653
        return BoseFSIndex(bosons, mode, bit), (chunk, next_bit, next_mode)
501,095,313✔
654
    end
655
end
656

657
# Multi-chunk version
658
@inline function Base.iterate(bom::DenseBoseOccupiedModes)
1,344,486✔
659
    bitstring = bom.storage
1,344,486✔
660
    i = num_chunks(bitstring)
1,344,486✔
661
    chunk = chunks(bitstring)[i]
1,344,486✔
662
    bits_left = chunk_bits(bitstring, i)
1,344,486✔
663
    mode = 1
1,344,486✔
664
    return iterate(bom, (i, chunk, bits_left, mode))
1,344,486✔
665
end
666
@inline function Base.iterate(bom::DenseBoseOccupiedModes, (i, chunk, bits_left, mode))
43,889,354✔
667
    i < 1 && return nothing
43,889,354✔
668
    bitstring = bom.storage
43,889,285✔
669
    S = typeof(bitstring)
43,889,285✔
670
    bit_position = 0
43,889,285✔
671

672
    # Remove and count trailing zeros.
673
    empty_modes = min(trailing_zeros(chunk), bits_left)
43,889,285✔
674
    chunk >>>= empty_modes % UInt
43,889,285✔
675
    bits_left -= empty_modes
43,889,285✔
676
    mode += empty_modes
43,889,285✔
677
    while bits_left < 1
44,684,437✔
678
        i -= 1
797,516✔
679
        i < 1 && return nothing
797,516✔
680
        @inbounds chunk = chunks(bitstring)[i]
795,152✔
681
        bits_left = chunk_bits(S, i)
795,152✔
682
        empty_modes = min(bits_left, trailing_zeros(chunk))
795,152✔
683
        mode += empty_modes
795,152✔
684
        bits_left -= empty_modes
795,152✔
685
        chunk >>>= empty_modes % UInt
795,152✔
686
    end
795,152✔
687

688
    bit_position = chunk_bits(S, i) - bits_left + 64 * (num_chunks(bitstring) - i)
43,886,921✔
689

690
    # Remove and count trailing ones.
691
    result = 0
43,886,921✔
692
    bosons = trailing_ones(chunk)
43,886,921✔
693
    bits_left -= bosons
43,886,921✔
694
    chunk >>>= bosons % UInt
43,886,921✔
695
    result += bosons
43,886,921✔
696
    while bits_left < 1
45,148,854✔
697
        i -= 1
1,287,192✔
698
        i < 1 && break
1,287,192✔
699
        @inbounds chunk = chunks(bitstring)[i]
1,261,933✔
700
        bits_left = chunk_bits(S, i)
1,261,933✔
701

702
        bosons = trailing_ones(chunk)
1,261,933✔
703
        bits_left -= bosons
1,261,933✔
704
        result += bosons
1,261,933✔
705
        chunk >>>= bosons % UInt
1,261,933✔
706
    end
1,261,933✔
707
    return BoseFSIndex(result, mode, bit_position), (i, chunk, bits_left, mode)
43,886,921✔
708
end
709

710
###
711
### FermiFS interface
712
###
713
function from_fermi_onr(::Type{S}, onr) where {M,C,T,S<:BitString{M,C,T}}
24,066✔
714
    result = zero(SVector{C,T})
24,066✔
715
    for mode in 1:M
24,066✔
716
        iszero(onr[mode]) && continue
1,850,431✔
717
        minus_j, offset = fldmod(mode - 1, 64)
1,297,579✔
718
        j = C - minus_j
1,297,579✔
719
        new = result[j] | T(1) << T(offset)
1,297,579✔
720
        result = setindex(result, new, j)
1,297,579✔
721
    end
3,676,796✔
722
    return S(result)
24,066✔
723
end
724

725
function _is_occupied(bs::BitString{M,1,T}, mode) where {M,T}
18,344,310✔
726
    @boundscheck 1 ≤ mode ≤ M || throw(BoundsError(bs, mode))
18,344,486✔
727
    return bs.chunks[1] & (T(1) << (mode - 1) % T) > 0
18,344,899✔
728
end
729
function _is_occupied(bs::BitString{M}, mode) where {M}
491,520✔
730
    @boundscheck 1 ≤ mode ≤ M || throw(BoundsError(bs, mode))
491,520✔
731
    j, i = fldmod1(mode, 64)
491,520✔
732
    return bs.chunks[end + 1 - j] & (UInt(1) << UInt(i - 1)) > 0
491,520✔
733
end
734

735
fermi_find_mode(bs::BitString, i) = FermiFSIndex(Int(_is_occupied(bs, i)), i, i-1)
18,835,699✔
736
function fermi_find_mode(bs::BitString, is::Tuple)
1,875,681✔
737
    return map(i -> FermiFSIndex(fermi_find_mode(bs, i)), is)
5,626,955✔
738
end
739

740
"""
741
    _flip_and_count(bs::BitString, k)
742

743
Count the number of ones before the `k`-th mode, flip the `k`th bit. Return the new
744
bitstring, the count, and the value of the bit after the flip.
745
"""
746
@inline function _flip_and_count(bs::BitString{<:Any,1,T}, k::Unsigned) where {T}
33,618,539✔
747
    chunk = bs.chunks[1]
33,619,629✔
748
    # highlights the k-th bit
749
    kmask = one(T) << k
33,620,428✔
750

751
    count = count_ones((kmask - 0x1) & chunk)
33,620,878✔
752
    chunk = chunk ⊻ kmask
33,622,147✔
753
    val = chunk & kmask > 0
33,622,663✔
754
    return typeof(bs)(chunk), count, val
33,623,015✔
755
end
756
@inline function _flip_and_count(bs::BitString, k::Unsigned)
337,216✔
757
    j, i = fldmod(k % Int, UInt(64))
337,216✔
758
    j = length(bs.chunks) - j
337,216✔
759
    chunk = bs.chunks[j]
337,216✔
760

761
    kmask = one(UInt64) << i
337,216✔
762

763
    count = count_ones((kmask - 0x1) & chunk)
337,216✔
764
    chunk = chunk ⊻ kmask
337,216✔
765
    val = chunk & kmask > 0
337,216✔
766

767
    for k in j + 1:num_chunks(bs)
445,034✔
768
        count += count_ones(bs.chunks[k])
357,345✔
769
    end
485,292✔
770
    return typeof(bs)(setindex(bs.chunks, chunk, j)), count, val
337,216✔
771
end
772

773
function fermi_excitation(
15,106,125✔
774
    bs::BitString, creations::NTuple{N}, destructions::NTuple{N}
775
) where {N}
776
    orig_bs = bs
15,106,272✔
777
    count = 0
15,106,320✔
778
    for i in N:-1:1
15,106,735✔
779
        d = destructions[i].mode
17,290,756✔
780
        bs, x, val = _flip_and_count(bs, UInt(d - 0x1))
17,382,122✔
781
        val && return orig_bs, 0.0
17,291,044✔
782
        count += x
16,995,400✔
783
    end
19,179,017✔
784
    for i in N:-1:1
14,811,512✔
785
        c = creations[i].mode
16,671,532✔
786
        bs, x, val = _flip_and_count(bs, UInt(c - 0x1))
16,708,221✔
787
        !val && return orig_bs, 0.0
16,671,980✔
788
        count += x
13,431,309✔
789
    end
15,291,208✔
790

791
    return bs, ifelse(iseven(count), 1.0, -1.0)
11,571,426✔
792
end
793

794
function Base.iterate(o::FermiOccupiedModes{<:Any,<:BitString})
122,996✔
795
    c = 0
122,996✔
796
    chunk = o.storage.chunks[end]
122,996✔
797
    while iszero(chunk)
122,996✔
798
        c += 1
×
799
        chunk = o.storage.chunks[end - c]
×
800
    end
×
801
    zeros = trailing_zeros(chunk % Int)
122,996✔
802
    return iterate(o, (chunk >> (zeros % UInt64), c * 64 + zeros, c))
122,996✔
803
end
804
function Base.iterate(o::FermiOccupiedModes{<:Any,<:BitString}, st)
22,261,412✔
805
    chunk, index, c = st
22,261,412✔
806
    while iszero(chunk)
22,630,388✔
807
        c += 1
491,972✔
808
        c == num_chunks(o.storage) && return nothing
491,972✔
809
        chunk = o.storage.chunks[end - c]
368,976✔
810
        index = c * 64
368,976✔
811
    end
368,976✔
812
    zeros = trailing_zeros(chunk % Int)
22,138,416✔
813
    index += zeros
22,138,416✔
814
    chunk >>= zeros
22,138,416✔
815
    return FermiFSIndex(1, index + 1, index), (chunk >> 1, index + 1, c)
22,138,416✔
816
end
817

818
function Base.iterate(o::FermiOccupiedModes{<:Any,<:BitString{<:Any,1,T}}) where {T}
9,812,149✔
819
    chunk = o.storage.chunks[end]
9,812,012✔
820
    zeros = trailing_zeros(chunk % Int)
9,812,238✔
821
    return iterate(o, (chunk >> (zeros % T), zeros))
19,618,862✔
822
end
823
function Base.iterate(o::FermiOccupiedModes{<:Any,<:BitString{<:Any,1,T}}, st) where {T}
24,101,292✔
824
    chunk, index = st
24,101,801✔
825
    iszero(chunk) && return nothing
24,102,553✔
826
    chunk >>= 0x1
23,526,635✔
827
    index += 1
23,527,289✔
828
    zeros = trailing_zeros(chunk % Int)
23,528,363✔
829
    return FermiFSIndex(1, index, index - 1), (chunk >> (zeros % T), index + zeros)
23,528,931✔
830
end
831

832
# Default implementation uses iterating over occupied modes.
833
function LinearAlgebra.dot(
462,104✔
834
    occ_a::FermiOccupiedModes{<:Any,S}, occ_b::FermiOccupiedModes{<:Any,S}
835
) where {S<:BitString}
836
    return count_ones(occ_a.storage & occ_b.storage)
462,105✔
837
end
838

839

840
function Base.iterate(o::FermiUnoccupiedModes{<:Any,<:BitString})
2✔
841
    c = 0
2✔
842
    chunk = o.storage.chunks[end]
2✔
843
    while iszero(chunk)
2✔
NEW
844
        c += 1
×
NEW
845
        chunk = o.storage.chunks[end-c]
×
NEW
846
    end
×
847
    zeros = trailing_zeros(chunk % Int)
2✔
848
    return iterate(o, (chunk >> (zeros % UInt64), c * 64 + zeros, c))
2✔
849
end
850
function Base.iterate(o::FermiUnoccupiedModes{<:Any,<:BitString}, st)
70✔
851
    chunk, index, c = st
70✔
852
    while iszero(chunk)
72✔
853
        c += 1
4✔
854
        c == num_chunks(o.storage) && return nothing
4✔
855
        chunk = o.storage.chunks[end-c]
2✔
856
        index = c * 64
2✔
857
    end
2✔
858
    zeros = trailing_zeros(chunk % Int)
68✔
859
    index += zeros
68✔
860
    chunk >>= zeros
68✔
861
    return FermiFSIndex(0, index + 1, index), (chunk >> 1, index + 1, c)
68✔
862
end
863

864
function Base.iterate(o::FermiUnoccupiedModes{<:Any,<:BitString{<:Any,1,T}}) where {T}
5✔
865
    chunk = o.storage.chunks[end]
5✔
866
    zeros = trailing_zeros(chunk % Int)
5✔
867
    return iterate(o, (chunk >> (zeros % T), zeros))
10✔
868
end
869
function Base.iterate(o::FermiUnoccupiedModes{<:Any,<:BitString{<:Any,1,T}}, st) where {T}
24✔
870
    chunk, index = st
24✔
871
    iszero(chunk) && return nothing
24✔
872
    chunk >>= 0x1
19✔
873
    index += 1
19✔
874
    zeros = trailing_zeros(chunk % Int)
19✔
875
    return FermiFSIndex(0, index, index - 1), (chunk >> (zeros % T), index + zeros)
19✔
876
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