BELFEM 0.9.0
Berkeley Lab Finite Element Framework
Loading...
Searching...
No Matches
cl_DynamicBitset.hpp
Go to the documentation of this file.
1/*
2 * BELFEM -- The Berkeley Lab Finite Element Framework
3 * Copyright (c) 2026, The Regents of the University of California,
4 * through Lawrence Berkeley National Laboratory (subject to receipt of any required
5 * approvals from the U.S. Dept. of Energy). All rights reserved.
6 *
7 * Developers: Christian Messe, Gregory Giard
8 *
9 * See the top-level LICENSE file for the complete license and disclaimer.
10 */
11
12#ifndef CL_DYNAMICBITSET_HPP
13#define CL_DYNAMICBITSET_HPP
14
15#include <cstdint> // For uint64_t
16#include <cstring> // For std::memcpy
17
18#include "typedefs.hpp"
19#include "assert.hpp"
20#include "cl_Cell.hpp"
21
22namespace belfem
23{
24//------------------------------------------------------------------------------
25
33 {
35 index_t mNumberOfBits;
36
38 index_t mMemorySize;
39
41 uint64_t* mData;
42
43 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
44 // two-level summary bitmap
45 //
46 // mSummary1 holds one bit per data word, mSummary2 one bit per
47 // level-1 word. where() and reset() walk the summaries instead of
48 // scanning mData, which turns both from O( mNumberOfBits / 64 ) into
49 // O( mSummary2Size + number of touched words ). For a 2.1e6-bit
50 // workspace that is 9 + O( k ) words instead of 32813.
51 //
52 // Correctness invariant ( one-directional ): every nonzero data word
53 // has its level-1 bit set, and every nonzero level-1 word has its
54 // level-2 bit set. A summary bit standing over a zero word is skipped
55 // harmlessly by the extraction loops.
56 //
57 // Tightness invariant ( maintained by every mutator, asserted by
58 // summaries_are_tight() ): a summary bit is set if and only if the
59 // word below it is nonzero. Tightness is not needed for correctness,
60 // but losing it silently degrades where() back towards a full scan,
61 // so the tests pin it.
62 //
63 // Safety rule: no level-1 bit may stand for a word index >=
64 // mMemorySize, and no level-2 bit for a level-1 index >=
65 // mSummary1Size, or the walks read past the arrays.
66 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
67
69 uint64_t* mSummary1 = nullptr ;
70
72 index_t mSummary1Size = 0 ;
73
75 uint64_t* mSummary2 = nullptr ;
76
78 index_t mSummary2Size = 0 ;
79
81 index_t mIndex = gNoIndex ;
82
83 // if hash is 0, we make the bitset writable
84 // once the hash is computed, we lock the bitset to prevent errors
85 size_t mHash = 0 ;
86
87 // Function pointer for optimized to_integer conversion
88 index_t ( DynamicBitset::*mFunToInt )() const ;
89
90//------------------------------------------------------------------------------
91 public:
92//------------------------------------------------------------------------------
93
95 DynamicBitset(const index_t aNumberOfBits );
96
97//------------------------------------------------------------------------------
98
100 DynamicBitset( const DynamicBitset & aBitset );
101
102//------------------------------------------------------------------------------
103
105 DynamicBitset( DynamicBitset && aBitset ) noexcept ;
106
107//------------------------------------------------------------------------------
108
111
112//------------------------------------------------------------------------------
113
115 index_t
116 size() const;
117
118//------------------------------------------------------------------------------
119
121 index_t
122 memory() const;
123
124//------------------------------------------------------------------------------
125
127 void
128 reset(const index_t aPos);
129
130//------------------------------------------------------------------------------
131
133 void
134 reset() ;
135
136//------------------------------------------------------------------------------
138 void
139 set(const index_t aPos);
140
141//------------------------------------------------------------------------------
142
144 void
145 set(const index_t aPos, const bool aValue);
146
147//------------------------------------------------------------------------------
148
150 void
151 flip(const index_t aPos);
152
153//------------------------------------------------------------------------------
154
156 void
157 flip();
158
159//------------------------------------------------------------------------------
160
162 bool
163 test(const index_t aPos) const;
164
165//------------------------------------------------------------------------------
166
168 index_t
169 count() const;
170
171//------------------------------------------------------------------------------
172
179 const uint64_t *
180 data() const;
181
182//------------------------------------------------------------------------------
183
188 bool
189 summaries_are_tight() const ;
190
191//------------------------------------------------------------------------------
192
194 void
195 lock() ;
196
197//------------------------------------------------------------------------------
198
200 void
201 unlock() ;
202
203//------------------------------------------------------------------------------
204
206 bool
207 is_locked() const ;
208
209//------------------------------------------------------------------------------
210
212 size_t hash() const;
213
214//------------------------------------------------------------------------------
215
216 string
217 to_string() const ;
218
219//------------------------------------------------------------------------------
220
221 string
222 to_hex() const ;
223
224//------------------------------------------------------------------------------
225
226 index_t
227 to_int() const ;
228
229//------------------------------------------------------------------------------
230
231 string
232 to_raw_string() const ;
233
234//------------------------------------------------------------------------------
235
236 void
237 set_from_hex( const string & aString );
238
239//------------------------------------------------------------------------------
240
241 void
242 set_index( const index_t aIndex );
243
244//------------------------------------------------------------------------------
245
246 index_t
247 index() const ;
248
249//------------------------------------------------------------------------------
250
251 // Returns the indices of the set bits, strictly ascending and free of
252 // duplicates. The scan walks the summary bitmaps, so it never visits a
253 // zero data word: cost is O( level-2 words + touched words + set bits ),
254 // i.e. essentially the number of set bits plus a size/262144 term.
255 //
256 // aAssumeSparse is retained for source compatibility and no longer
257 // selects a different algorithm - there is no longer a dense variant
258 // worth having, because the walk never visits a zero word.
259 void
260 where( Cell< index_t > & aBits, const bool aAssumeSparse = true ) const ;
261
262//------------------------------------------------------------------------------
263
268 bool operator==(const DynamicBitset & aRhs) const
269 {
270 BELFEM_ERROR(this->size() == aRhs.size(),
271 "Bitsets don't have the same size (%lu vs. %lu)",
272 (long unsigned int)this->size(),
273 (long unsigned int) aRhs.size());
274
275 if ( this->hash() != aRhs.hash() )
276 {
277 return false; // Quick reject if hashes differ
278 }
279
280 // Full comparison to resolve collisions
281 for (index_t k = 0; k < mMemorySize; ++k)
282 {
283 if (mData[k] != aRhs.data()[k])
284 {
285 return false;
286 }
287 }
288 return true;
289 }
290
291//------------------------------------------------------------------------------
292
294 bool operator!=(const DynamicBitset& aRhs) const
295 {
296 return !(*this == aRhs);
297 }
298
299//------------------------------------------------------------------------------
300
302 DynamicBitset& operator=(const DynamicBitset& aRhs ) ;
303
305 DynamicBitset& operator=(DynamicBitset&& aRhs) noexcept ;
306
307//------------------------------------------------------------------------------
308 private:
309
310 // Note: there is no mutable data() overload. A write through one would
311 // bypass the summary bitmaps, after which where() and reset() silently
312 // miss the affected words. Members use mData directly.
313
317 void
318 recompute_summaries();
319
320 void
321 where_dense( Cell< index_t > & aBits ) const ;
322
323 void
324 where_sparse( Cell< index_t > & aBits ) const ;
325
326 void
327 select_to_int_function();
328
329 index_t
330 to_int_partial() const ;
331
332 index_t
333 to_int_full() const ;
334
337 index_t
338 to_int_zero() const ;
339
340 index_t
341 to_int_fail() const ;
342
343
344//------------------------------------------------------------------------------
345 public:
346//------------------------------------------------------------------------------
347
350 {
351 BELFEM_ERROR(this->size() == aRhs.size(),
352 "Bitsets don't have the same size (%lu vs. %lu)",
353 (long unsigned int)this->size(),
354 (long unsigned int)aRhs.size());
355
356 DynamicBitset aResult(mNumberOfBits);
357
358 const uint64_t* tSrcA = mData;
359 const uint64_t* tSrcB = aRhs.data();
360 uint64_t* tDst = aResult.mData;
361 const uint64_t* tEnd = tSrcA + mMemorySize;
362
363 while(tSrcA != tEnd)
364 {
365 *tDst++ = *tSrcA++ | *tSrcB++;
366 }
367
368 // a|b is nonzero exactly where a or b is, so OR-ing tight
369 // summaries yields a tight summary - no rebuild needed
370 for( index_t i = 0; i < mSummary1Size; ++i )
371 {
372 aResult.mSummary1[ i ] = mSummary1[ i ] | aRhs.mSummary1[ i ];
373 }
374 for( index_t i = 0; i < mSummary2Size; ++i )
375 {
376 aResult.mSummary2[ i ] = mSummary2[ i ] | aRhs.mSummary2[ i ];
377 }
378
379 return aResult;
380 }
381
384 {
385 BELFEM_ERROR(this->size() == aRhs.size(),
386 "Bitsets don't have the same size (%lu vs. %lu)",
387 (long unsigned int)this->size(),
388 (long unsigned int) aRhs.size());
389
390 BELFEM_ERROR( mHash == 0, "Can't modify a locked bitset" );
391
392 const uint64_t* tData = aRhs.data();
393
394 for(index_t i = 0; i < mMemorySize; ++i)
395 {
396 mData[i] |= tData[i];
397 }
398
399 // OR of two tight summaries is tight ( see operator| )
400 for( index_t i = 0; i < mSummary1Size; ++i )
401 {
402 mSummary1[ i ] |= aRhs.mSummary1[ i ];
403 }
404 for( index_t i = 0; i < mSummary2Size; ++i )
405 {
406 mSummary2[ i ] |= aRhs.mSummary2[ i ];
407 }
408
409 return *this;
410 }
411
414 {
415 BELFEM_ERROR(this->size() == aRhs.size(),
416 "Bitsets don't have the same size (%lu vs. %lu)",
417 (long unsigned int)this->size(),
418 (long unsigned int)aRhs.size());
419
420 DynamicBitset aResult(mNumberOfBits);
421
422 const uint64_t* tData = aRhs.data();
423
424 for(index_t i = 0; i < mMemorySize; ++i)
425 {
426 aResult.mData[i] = mData[i] ^ tData[i];
427 }
428
429 // XOR can zero a word that was nonzero in both operands, so the
430 // summaries cannot be derived from the inputs - rebuild them
431 aResult.recompute_summaries();
432
433 return aResult;
434 }
435
438 {
439 BELFEM_ERROR(this->size() == aRhs.size(),
440 "Bitsets don't have the same size (%lu vs. %lu)",
441 (long unsigned int)this->size(),
442 (long unsigned int)aRhs.size());
443
444 BELFEM_ERROR( mHash == 0, "Can't modify a locked bitset");
445
446 for(index_t i = 0; i < mMemorySize; ++i)
447 {
448 mData[i] ^= aRhs.mData[i];
449 }
450
451 // XOR can zero words ( see operator^ )
452 this->recompute_summaries();
453
454 return *this;
455 }
456
459 {
460 BELFEM_ERROR(this->size() == aRhs.size(),
461 "Bitsets don't have the same size (%lu vs. %lu)",
462 (long unsigned int)this->size(),
463 (long unsigned int)aRhs.size());
464
465 DynamicBitset tResult(mNumberOfBits);
466
467 const uint64_t* tData = aRhs.data();
468
469 for(index_t i = 0; i < mMemorySize; ++i)
470 {
471 tResult.mData[i] = mData[i] & tData[i];
472 }
473
474 // AND can zero a word that was nonzero in both operands
475 tResult.recompute_summaries();
476
477 return tResult;
478 }
479
482 {
483 BELFEM_ERROR(this->size() == aRhs.size(),
484 "Bitsets don't have the same size (%lu vs. %lu)",
485 (long unsigned int)this->size(),
486 (long unsigned int)aRhs.size());
487
488 const uint64_t* tData = aRhs.data();
489
490 BELFEM_ERROR( mHash == 0, "Can't modify a locked bitset");
491
492 for(index_t i = 0; i < mMemorySize; ++i)
493 {
494 mData[i] &= tData[i];
495 }
496
497 // AND can zero words ( see operator& )
498 this->recompute_summaries();
499
500 return *this;
501 }
502
503 };
504
505
506
507//------------------------------------------------------------------------------
508
509 // Inline implementations of member functions
511 {
512 return mNumberOfBits;
513 }
514
515//------------------------------------------------------------------------------
516
518 {
519 return mMemorySize;
520 }
521
522//------------------------------------------------------------------------------
523
524 inline void DynamicBitset::set(const index_t aPos)
525 {
526 BELFEM_ASSERT(aPos < mNumberOfBits,
527 "Index %lu out of range (expect < %lu)",
528 (long unsigned int)aPos,
529 (long unsigned int)mNumberOfBits);
530
531 BELFEM_ASSERT( mHash == 0,
532 "can't write on a locked bitset");
533
534 // setting a bit always makes the word nonzero, so both summary levels
535 // can be updated unconditionally - no branch on this hot path
536 const index_t tWord = aPos / 64 ;
537 const index_t tS1 = tWord / 64 ;
538
539 mData [ tWord ] |= uint64_t( 1 ) << ( aPos % 64 );
540 mSummary1[ tS1 ] |= uint64_t( 1 ) << ( tWord % 64 );
541 mSummary2[ tS1 / 64 ] |= uint64_t( 1 ) << ( tS1 % 64 );
542 }
543
544//------------------------------------------------------------------------------
545
546 inline void DynamicBitset::reset(const index_t aPos)
547 {
548 BELFEM_ASSERT(aPos < mNumberOfBits,
549 "Index %lu out of range (expect < %lu)",
550 (long unsigned int)aPos,
551 (long unsigned int)mNumberOfBits);
552
553 BELFEM_ASSERT( mHash == 0,
554 "can't reset an single digit of a locked bitset" );
555
556 // clearing the last bit of a word must clear the summary bits above
557 // it. Leaving them stale would still be correct, but a caller that
558 // clears bits one by one instead of calling reset() would saturate
559 // the summaries and lose the whole benefit of the bitmap - which is
560 // exactly what the sparsity-pattern builders used to do.
561 const index_t tWord = aPos / 64 ;
562
563 mData[ tWord ] &= ~( uint64_t( 1 ) << ( aPos % 64 ) );
564
565 if ( mData[ tWord ] == 0 )
566 {
567 const index_t tS1 = tWord / 64 ;
568
569 mSummary1[ tS1 ] &= ~( uint64_t( 1 ) << ( tWord % 64 ) );
570
571 if ( mSummary1[ tS1 ] == 0 )
572 {
573 mSummary2[ tS1 / 64 ] &= ~( uint64_t( 1 ) << ( tS1 % 64 ) );
574 }
575 }
576 }
577
578
579//------------------------------------------------------------------------------
580
581 // reset() is implemented in the .cpp: it walks the summary bitmaps rather
582 // than memsetting the whole array, and shares the ctz helper with the
583 // other walkers.
584
585//------------------------------------------------------------------------------
586
587 inline void DynamicBitset::set(const index_t aPos, const bool aValue)
588 {
589 if (aValue)
590 {
591 this->set(aPos);
592 }
593 else
594 {
595 this->reset(aPos);
596 }
597 }
598
599//------------------------------------------------------------------------------
600
601 inline void DynamicBitset::flip(const index_t aPos)
602 {
603 BELFEM_ASSERT(aPos < mNumberOfBits,
604 "Index %lu out of range (expect < %lu)",
605 (long unsigned int)aPos,
606 (long unsigned int)mNumberOfBits);
607
608 BELFEM_ASSERT( mHash == 0,
609 "Can't flip a bit on a locked bitset" );
610
611 // a flip can turn a bit ON in a word whose summary bit is clear, so
612 // the summaries must be maintained here for correctness, not merely
613 // for tightness: without the OR branch, where() would miss the bit
614 const index_t tWord = aPos / 64 ;
615 const index_t tS1 = tWord / 64 ;
616
617 mData[ tWord ] ^= uint64_t( 1 ) << ( aPos % 64 );
618
619 if ( mData[ tWord ] != 0 )
620 {
621 mSummary1[ tS1 ] |= uint64_t( 1 ) << ( tWord % 64 );
622 mSummary2[ tS1 / 64 ] |= uint64_t( 1 ) << ( tS1 % 64 );
623 }
624 else
625 {
626 mSummary1[ tS1 ] &= ~( uint64_t( 1 ) << ( tWord % 64 ) );
627
628 if ( mSummary1[ tS1 ] == 0 )
629 {
630 mSummary2[ tS1 / 64 ] &= ~( uint64_t( 1 ) << ( tS1 % 64 ) );
631 }
632 }
633 }
634
635//------------------------------------------------------------------------------
636
637 inline bool DynamicBitset::test(const index_t aPos) const
638 {
639 BELFEM_ASSERT(aPos < mNumberOfBits,
640 "Index %lu out of range (expect < %lu)",
641 (long unsigned int)aPos,
642 (long unsigned int)mNumberOfBits);
643
644 return ( mData[aPos / 64] & (uint64_t(1) << (aPos % 64))) != 0;
645 }
646
647//------------------------------------------------------------------------------
648
649 inline const uint64_t* DynamicBitset::data() const
650 {
651 return mData;
652 }
653
654//------------------------------------------------------------------------------
655
656 inline void
658 {
659 mHash = 14695981039346656037ULL; // FNV-1a 64-bit offset basis
660 mHash ^= std::hash<index_t>{}(mNumberOfBits);
661 mHash *= 1099511628211ULL; // FNV-64 multiplier
662 for (index_t i = 0; i < mMemorySize; ++i)
663 {
664 mHash ^= std::hash<uint64_t>{}(mData[i]);
665 mHash *= 1099511628211ULL;
666 }
667
668 // zero is the "unlocked" sentinel, so a hash that happens to land on
669 // it would make a locked bitset report itself writable
670 if ( mHash == 0 )
671 {
672 mHash = 1 ;
673 }
674 }
675
676//------------------------------------------------------------------------------
677
678 inline void
680 {
681 mHash = 0 ;
682 }
683
684//------------------------------------------------------------------------------
685
686 inline bool DynamicBitset::is_locked() const
687 {
688 return mHash != 0 ;
689 }
690
691//------------------------------------------------------------------------------
692
694 inline size_t
696 {
697 BELFEM_ASSERT( mHash != 0, "can't return the hash of a writable bitset" );
698 return mHash ;
699 }
700
701//------------------------------------------------------------------------------
702
703 inline void
704 DynamicBitset::where( Cell< index_t > & aBits, const bool aAssumeSparse ) const
705 {
706 if ( mNumberOfBits == 0 )
707 {
708 aBits.clear(); // Reset the container
709 return;
710 }
711 // aAssumeSparse no longer selects a different algorithm: since the
712 // scan walks the summary bitmaps it never visits a zero word, so the
713 // count-then-fill variant has nothing left to win. The parameter is
714 // kept so existing call sites still compile.
715 ( void ) aAssumeSparse ;
716
717 this->where_sparse( aBits ) ;
718 }
719
720//------------------------------------------------------------------------------
721
722 inline index_t
724 {
725 return ( this->*mFunToInt )();
726 }
727
728//------------------------------------------------------------------------------
729
730 inline index_t
731 DynamicBitset::to_int_partial() const
732 {
733 uint64_t mask = (uint64_t(1) << mNumberOfBits) - 1;
734 return static_cast< index_t >(mData[0] & mask);
735 }
736
737//------------------------------------------------------------------------------
738
739 inline index_t
740 DynamicBitset::to_int_full() const
741 {
742 return static_cast<index_t>(mData[0]);
743 }
744
745//------------------------------------------------------------------------------
746
747 inline index_t
748 DynamicBitset::to_int_zero() const
749 {
750 return 0;
751 }
752
753//------------------------------------------------------------------------------
754
755 inline void
757 {
758 mIndex = aIndex ;
759 }
760
761//------------------------------------------------------------------------------
762
763 inline index_t
765 {
766 return mIndex;
767 }
768
769//------------------------------------------------------------------------------
770} // namespace belfem
771
772#endif // CL_DYNAMICBITSET_HPP
#define BELFEM_ERROR(aCheck,...)
Definition assert.hpp:264
#define BELFEM_ASSERT(aCheck,...)
Definition assert.hpp:244
void clear()
clear the memory
Definition cl_Cell.hpp:240
Runtime-sized bitset; one bit per flag, packed into 64-bit words.
Definition cl_DynamicBitset.hpp:33
void lock()
makes the bitset non-writable and computes the hash
Definition cl_DynamicBitset.hpp:657
DynamicBitset operator&(const DynamicBitset &aRhs) const
Bitwise AND operator.
Definition cl_DynamicBitset.hpp:458
const uint64_t * data() const
Definition cl_DynamicBitset.hpp:649
DynamicBitset & operator&=(const DynamicBitset &aRhs)
Bitwise AND assignment operator.
Definition cl_DynamicBitset.hpp:481
index_t count() const
Counts the number of bits set to 1.
Definition cl_DynamicBitset.cpp:187
string to_string() const
Definition cl_DynamicBitset.cpp:231
string to_raw_string() const
Definition cl_DynamicBitset.cpp:316
string to_hex() const
Definition cl_DynamicBitset.cpp:252
size_t hash() const
Returns a hash function for fast comparison.
Definition cl_DynamicBitset.hpp:695
void set_index(const index_t aIndex)
Definition cl_DynamicBitset.hpp:756
void set_from_hex(const string &aString)
Definition cl_DynamicBitset.cpp:334
void where(Cell< index_t > &aBits, const bool aAssumeSparse=true) const
Definition cl_DynamicBitset.hpp:704
index_t index() const
Definition cl_DynamicBitset.hpp:764
DynamicBitset operator^(const DynamicBitset &aRhs) const
Bitwise XOR operator.
Definition cl_DynamicBitset.hpp:413
DynamicBitset operator|(const DynamicBitset &aRhs) const
Bitwise OR operator.
Definition cl_DynamicBitset.hpp:349
void reset(const index_t aPos)
Resets (clears) the bit at the given position.
Definition cl_DynamicBitset.hpp:546
void flip()
Flips all bits at once.
Definition cl_DynamicBitset.cpp:559
index_t to_int() const
Definition cl_DynamicBitset.hpp:723
void unlock()
makes the bitset writable and resets the hash
Definition cl_DynamicBitset.hpp:679
void set(const index_t aPos)
Sets the bit at the given position to 1.
Definition cl_DynamicBitset.hpp:524
index_t memory() const
Returns the number of 64-bit blocks used (memory size).
Definition cl_DynamicBitset.hpp:517
void flip(const index_t aPos)
Flips (toggles) the bit at the given position.
Definition cl_DynamicBitset.hpp:601
bool operator==(const DynamicBitset &aRhs) const
Definition cl_DynamicBitset.hpp:268
bool test(const index_t aPos) const
Tests whether the bit at the given position is set.
Definition cl_DynamicBitset.hpp:637
void reset()
Resets (clears) all bits.
Definition cl_DynamicBitset.cpp:384
bool operator!=(const DynamicBitset &aRhs) const
Comparison operator: Checks if two bitsets are not equal.
Definition cl_DynamicBitset.hpp:294
DynamicBitset & operator^=(const DynamicBitset &aRhs)
Bitwise XOR assignment operator.
Definition cl_DynamicBitset.hpp:437
DynamicBitset & operator=(const DynamicBitset &aRhs)
Assignment operator.
Definition cl_DynamicBitset.cpp:599
bool is_locked() const
checks if the bitset is writable
Definition cl_DynamicBitset.hpp:686
DynamicBitset & operator|=(const DynamicBitset &aRhs)
Assignment or operator.
Definition cl_DynamicBitset.hpp:383
bool summaries_are_tight() const
Definition cl_DynamicBitset.cpp:448
index_t size() const
Returns the number of bits in the bitset.
Definition cl_DynamicBitset.hpp:510
DynamicBitset(const index_t aNumberOfBits)
Constructor: Initializes the bitset with the given number of bits.
Definition cl_DynamicBitset.cpp:72
USER GUIDES:
Definition cl_Capacitor.cpp:16
constexpr index_t gNoIndex
Definition typedefs.hpp:57
uint32_t index_t
Definition typedefs.hpp:52