From 25e3926a80c924bd0bebe4569f20eb4b07eebb33 Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Wed, 6 Jun 2018 05:24:13 -0400 Subject: [PATCH 001/122] First big commit for chronomatic comp. stats. --- Makefile | 10 +- include/cqf.h | 43 ++- include/cqf/gqf.h | 1 + include/monochromatic_component_iterator.h | 155 +++++++++++ src/cqf/gqf.c | 31 +++ src/monochromatic_component_iterator.cc | 310 +++++++++++++++++++++ 6 files changed, 545 insertions(+), 5 deletions(-) create mode 100644 include/monochromatic_component_iterator.h create mode 100644 src/monochromatic_component_iterator.cc diff --git a/Makefile b/Makefile index 3d8c450..622f9f0 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -TARGETS= mantis +TARGETS= mantis monochromatic_component_iterator ifdef D DEBUG=-g -DDEBUG @@ -33,8 +33,8 @@ CFLAGS += -Wall $(DEBUG) $(PROFILE) $(OPT) $(ARCH) -m64 -I. -I$(LOC_INCLUDE)\ -Wno-unused-result -Wno-strict-aliasing -Wno-unused-function -Wno-sign-compare \ -Wno-implicit-function-declaration -LDFLAGS += $(DEBUG) $(PROFILE) $(OPT) -lsdsl -lpthread -lboost_system \ --lboost_thread -lm -lz -lrt +LDFLAGS += $(DEBUG) $(PROFILE) $(OPT) -lpthread -lboost_system \ +-lboost_thread -lm -lz -lrt lib/libsdsl.a # # declaration of dependencies @@ -45,6 +45,8 @@ all: $(TARGETS) # dependencies between programs and .o files mantis: $(OBJDIR)/kmer.o $(OBJDIR)/mantis.o $(OBJDIR)/validatemantis.o $(OBJDIR)/gqf.o $(OBJDIR)/hashutil.o $(OBJDIR)/query.o $(OBJDIR)/coloreddbg.o $(OBJDIR)/bitvector.o $(OBJDIR)/util.o $(OBJDIR)/MantisFS.o +monochromatic_component_iterator: $(OBJDIR)/kmer.o $(OBJDIR)/gqf.o $(OBJDIR)/hashutil.o $(OBJDIR)/monochromatic_component_iterator.o + # dependencies between .o files and .h files $(OBJDIR)/mantis.o: $(LOC_SRC)/mantis.cc $(OBJDIR)/MantisFs.o: $(LOC_SRC)/MantisFS.cc $(LOC_INCLUDE)/MantisFS.h @@ -59,7 +61,7 @@ $(OBJDIR)/hashutil.o: $(LOC_INCLUDE)/hashutil.h # dependencies between .o files and .cc (or .c) files $(OBJDIR)/gqf.o: $(LOC_SRC)/cqf/gqf.c $(LOC_INCLUDE)/cqf/gqf.h - +$(OBJDIR)/monochromatic_component_iterator.o: $(LOC_INCLUDE)/cqf.h $(LOC_INCLUDE)/monochromatic_component_iterator.h $(LOC_SRC)/monochromatic_component_iterator.cc # # generic build rules # diff --git a/include/cqf.h b/include/cqf.h index 6069909..6cee360 100644 --- a/include/cqf.h +++ b/include/cqf.h @@ -36,6 +36,13 @@ #define PAGE_DROP_GRANULARITY (1ULL << 21) #define PAGE_BUFFER_SIZE 4096 +/* +extern int is_runend(const QF *qf, uint64_t index); +extern uint64_t run_end(const QF *qf, uint64_t hash_bucket_index); +extern uint64_t decode_counter(const QF *qf, uint64_t index, uint64_t *remainder, uint64_t *count); +extern int is_occupied(const QF *qf, uint64_t index); +*/ + template class CQF { public: @@ -48,7 +55,7 @@ class CQF { /* Will return the count. */ uint64_t query(const key_obj& k); - + std::pair queryValAndIdx( key_obj& k) const; void serialize(std::string filename) { qf_serialize(&cqf, filename.c_str()); } @@ -57,6 +64,7 @@ class CQF { uint32_t seed(void) const { return cqf.metadata->seed; } uint32_t keybits(void) const { return cqf.metadata->key_bits; } uint64_t size(void) const { return cqf.metadata->ndistinct_elts; } + uint64_t slots(void) const {return cqf.metadata->nslots;} //uint64_t set_size(void) const { return set.size(); } void reset(void) { qf_reset(&cqf); } @@ -144,6 +152,39 @@ uint64_t CQF::query(const key_obj& k) { return qf_count_key_value(&cqf, k.key, k.value); } +template +std::pair CQF::queryValAndIdx(key_obj& k) const { + uint64_t eq, idx; + eq = qf_key_value_index(&cqf, k.key, k.value, &idx); + return std::make_pair(eq, idx); + /*uint64_t hash = (k.key << cqf.metadata->value_bits) | (k.value & + BITMASK(cqf.metadata->value_bits)); + uint64_t hash_remainder = hash & BITMASK(cqf.metadata->bits_per_slot); + int64_t hash_bucket_index = hash >> cqf.metadata->bits_per_slot; + + if (!is_occupied(&cqf, hash_bucket_index)) + return std::make_pair(0, 0); + + int64_t runstart_index = hash_bucket_index == 0 ? 0 : run_end(&cqf, + hash_bucket_index-1) + + 1; + if (runstart_index < hash_bucket_index) + runstart_index = hash_bucket_index; + + *//* printf("MC RUNSTART: %02lx RUNEND: %02lx\n", runstart_index, runend_index); *//* + + uint64_t current_remainder, current_count, current_end; + do { + current_end = decode_counter(&cqf, runstart_index, ¤t_remainder, + ¤t_count); + if (current_remainder == hash_remainder) + return std::make_pair(current_count, current_end); // FIXME not sure about this + runstart_index = current_end + 1; + } while (!is_runend(&cqf, current_end)); + + return std::make_pair(0, 0);*/ +} + template CQF::Iterator::Iterator(QFi it, uint32_t cutoff, uint64_t end_hash) : iter(it), last_prefetch_offset(LLONG_MIN), cutoff(cutoff), diff --git a/include/cqf/gqf.h b/include/cqf/gqf.h index cc954e1..266469d 100644 --- a/include/cqf/gqf.h +++ b/include/cqf/gqf.h @@ -141,6 +141,7 @@ extern "C" { value, into qf. */ uint64_t qf_count_key_value(const QF *qf, uint64_t key, uint64_t value); + uint64_t qf_key_value_index(const QF *qf, uint64_t key, uint64_t value, uint64_t* idx); /* Initialize an iterator */ bool qf_iterator(const QF *qf, QFi *qfi, uint64_t position); diff --git a/include/monochromatic_component_iterator.h b/include/monochromatic_component_iterator.h new file mode 100644 index 0000000..6eefce7 --- /dev/null +++ b/include/monochromatic_component_iterator.h @@ -0,0 +1,155 @@ +// +// Created by Fatemeh Almodaresi on 6/4/18. +// + +#ifndef MANTIS_MONOCHROME_ITERATOR_H +#define MANTIS_MONOCHROME_ITERATOR_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "sparsepp/spp.h" +#include "tsl/sparse_map.h" +#include "sdsl/bit_vectors.hpp" +//#include "bitvector.h" +#include "cqf.h" +#include "hashutil.h" +#include "common_types.h" + +namespace dna { + +/////////////// bases ///////////////// + enum base { + C = 0, A = 1, T = 2, G = 3 + }; + + base operator-(base b); // return the complementary base + extern const base bases[4]; + extern const std::map base_from_char; + extern const std::map base_to_char; + +///////////// kmers ///////////////////// + class kmer { + public: + int len; + uint64_t val; + + kmer(void); + + kmer(base b); + + kmer(int l, uint64_t v); + + kmer(std::string s); + + // Convert to string + operator std::string() const; + }; + + bool operator<(kmer a, kmer b); + + bool operator==(kmer a, kmer b); + + bool operator!=(kmer a, kmer b); + +// Return the reverse complement of k + kmer operator-(kmer k); + + kmer canonicalize(kmer k); + +// Return the kmer of length |a| that results from shifting b into a +// from the right + kmer operator<<(kmer a, kmer b); + +// Return the kmer of length |b| that results from shifting a into b +// from the left + kmer operator>>(kmer a, kmer b); + +// Append two kmers + kmer operator+(kmer a, kmer b); + + kmer suffix(kmer k, int len); + + kmer prefix(kmer k, int len); + +// The purpose of this class is to enable us to declare containers +// as holding canonical kmers, e.g. set. Then all +// inserts/queries/etc will automatically canonicalize their +// arguments. + class canonical_kmer : public kmer { + public: + canonical_kmer(void); + + canonical_kmer(base b); + + canonical_kmer(int l, uint64_t v); + + canonical_kmer(std::string s); + + canonical_kmer(kmer k); + }; +} + +struct Mc_stats { + uint64_t nodeCnt = 1; + uint64_t min_dist = -1; // infinity +}; + +typedef dna::canonical_kmer edge; // k-mer +typedef dna::canonical_kmer node; // (k-1)-mer + +class monochromatic_component_iterator { +public: + class work_item { + public: + node curr; + uint64_t idx; + uint64_t colorid; + + work_item(node currin, uint64_t idxin, uint64_t coloridin) : curr(currin), idx(idxin), colorid(coloridin) {} + + bool operator<(const work_item &item2) const { + return (*this).idx < item2.idx; + } + }; + + void operator++(void); + + Mc_stats operator*(void); + + monochromatic_component_iterator(const CQF *g); + +private: + + uint32_t k; + std::queue work; + + + const CQF *cqf; + CQF::Iterator it; + sdsl::bit_vector visited; + + bool exists(edge e, uint64_t &idx, uint64_t &eqid); + + std::set neighbors(monochromatic_component_iterator::work_item n); + + work_item front(std::queue &w); + + + uint64_t manhattanDist(uint64_t eq1, uint64_t eq2); + + Mc_stats getMC(); +}; + + +#endif //MANTIS_MONOCHROME_ITERATOR_H diff --git a/src/cqf/gqf.c b/src/cqf/gqf.c index 3715ed7..cb8f58e 100644 --- a/src/cqf/gqf.c +++ b/src/cqf/gqf.c @@ -1945,6 +1945,37 @@ uint64_t qf_count_key_value(const QF *qf, uint64_t key, uint64_t value) return 0; } + +uint64_t qf_key_value_index(const QF* qf, uint64_t key, uint64_t value, uint64_t* idx) { + uint64_t hash = (key << qf->metadata->value_bits) | (value & + BITMASK(qf->metadata->value_bits)); + uint64_t hash_remainder = hash & BITMASK(qf->metadata->bits_per_slot); + int64_t hash_bucket_index = hash >> qf->metadata->bits_per_slot; + + if (!is_occupied(qf, hash_bucket_index)) + return 0; + + int64_t runstart_index = hash_bucket_index == 0 ? 0 : run_end(qf, + hash_bucket_index-1) + + 1; + if (runstart_index < hash_bucket_index) + runstart_index = hash_bucket_index; + + /* printf("MC RUNSTART: %02lx RUNEND: %02lx\n", runstart_index, runend_index); */ + + uint64_t current_remainder, current_count, current_end; + do { + current_end = decode_counter(qf, runstart_index, ¤t_remainder, + ¤t_count); + if (current_remainder == hash_remainder) { + (*idx) = current_end; + return current_count; // FIXME not sure about this + } + runstart_index = current_end + 1; + } while (!is_runend(qf, current_end)); + + return 0; +} /* initialize the iterator at the run corresponding * to the position index */ diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc new file mode 100644 index 0000000..fe61b2b --- /dev/null +++ b/src/monochromatic_component_iterator.cc @@ -0,0 +1,310 @@ +// +// Created by Fatemeh Almodaresi on 6/4/18. +// + +#include "monochromatic_component_iterator.h" + +namespace dna { + + /////////////// bases ///////////////// + base operator-(base b) { + return (base)((~((uint64_t) b)) & 0x3ULL); + } + + const base bases[4] = {C, A, T, G}; + const std::map base_from_char = {{'A', A}, + {'C', C}, + {'G', G}, + {'T', T}, + {'N', A}}; + const std::map base_to_char = {{A, 'A'}, + {C, 'C'}, + {G, 'G'}, + {T, 'T'}}; + + ///////////// kmers ///////////////////// + kmer::kmer(void) : len(0), val(0) {} + + kmer::kmer(base b) : len(1), val((uint64_t) b) {} + + kmer::kmer(int l, uint64_t v) : len(l), val(v & BITMASK(2 * l)) { + assert(l <= 32); + } + + static uint64_t string_to_kmer_val(std::string s) { + uint64_t val = 0; + for (auto c : s) + val = (val << 2) | ((uint64_t) (base_from_char.at(c))); + return val; + } + + kmer::kmer(std::string s) : len(s.size()), val(string_to_kmer_val(s)) { + assert(s.size() <= 32); + } + + // Convert to string + kmer::operator std::string() const { + std::string s; + for (auto i = 1; i < len + 1; i++) + s = s + base_to_char.at((base)((val >> (2 * (len - i))) & BITMASK(2))); + return s; + } + + bool operator<(kmer a, kmer b) { + return a.len != b.len ? a.len < b.len : a.val < b.val; + } + + bool operator==(kmer a, kmer b) { + return a.len == b.len && a.val == b.val; + } + + bool operator!=(kmer a, kmer b) { + return !operator==(a, b); + } + + // Return the reverse complement of k + kmer operator-(kmer k) { + uint64_t val = k.val; + val = + (val >> 32) | + (val << 32); + val = + ((val >> 16) & 0x0000ffff0000ffff) | + ((val << 16) & 0xffff0000ffff0000); + val = + ((val >> 8) & 0x00ff00ff00ff00ff) | + ((val << 8) & 0xff00ff00ff00ff00); + val = + ((val >> 4) & 0x0f0f0f0f0f0f0f0f) | + ((val << 4) & 0xf0f0f0f0f0f0f0f0); + val = + ((val >> 2) & 0x3333333333333333) | + ((val << 2) & 0xcccccccccccccccc); + val = ~val; + val >>= 64 - 2 * k.len; + return kmer(k.len, val); + } + + // backwards from standard definition to match kmer.h definition + kmer canonicalize(kmer k) { + return -k < k ? k : -k; + } + + // Return the kmer of length |a| that results from shifting b into a + // from the right + kmer operator<<(kmer a, kmer b) { + uint64_t val = ((a.val << (2 * b.len)) | b.val) & BITMASK(2 * a.len); + return kmer(a.len, val); + } + + // Return the kmer of length |b| that results from shifting b into a + // from the left + kmer operator>>(kmer a, kmer b) { + uint64_t val + = ((b.val >> (2 * a.len)) | (a.val << (2 * (b.len - a.len)))) + & BITMASK(2 * b.len); + return kmer(b.len, val); + } + + // Append two kmers + kmer operator+(kmer a, kmer b) { + int len = a.len + b.len; + assert(len <= 32); + uint64_t val = (a.val << (2 * b.len)) | b.val; + return kmer(len, val); + } + + kmer prefix(kmer k, int len) { return kmer(len, k.val >> (2 * (k.len - len))); } + + kmer suffix(kmer k, int len) { return kmer(len, k.val & BITMASK(2 * len)); } + + bool period_divides(kmer k, uint64_t periodicity) { + static const uint64_t multipliers[33] = + { + 0, + 0x5555555555555555, // 1 + 0x1111111111111111, // 2 + 0x1041041041041041, // 3 + 0x0101010101010101, // 4 + 0x1004010040100401, // 5 + 0x1001001001001001, // 6 + 0x0100040010004001, // 7 + 0x0001000100010001, // 8 + 0x0040001000040001, // 9 + 0x1000010000100001, // 10 + 0x0000100000400001, // 11 + 0x0001000001000001, // 12 + 0x0010000004000001, // 13 + 0x0100000010000001, // 14 + 0x1000000040000001, // 15 + 0x0000000100000001, // 16 + 0x0000000400000001, // 17 + 0x0000001000000001, // 18 + 0x0000004000000001, // 19 + 0x0000010000000001, // 20 + 0x0000040000000001, // 21 + 0x0000100000000001, // 22 + 0x0000400000000001, // 23 + 0x0001000000000001, // 24 + 0x0004000000000001, // 25 + 0x0010000000000001, // 26 + 0x0040000000000001, // 27 + 0x0100000000000001, // 28 + 0x0400000000000001, // 29 + 0x1000000000000001, // 30 + 0x4000000000000001, // 31 + 0x0000000000000001, // 32 + }; + uint64_t piece = k.val & BITMASK(2 * periodicity); + piece = piece * multipliers[periodicity]; + piece = piece & BITMASK(2 * k.len); + return piece == k.val; + } + + uint64_t period(kmer k) { + for (int i = 1; i <= k.len; i++) { + if (period_divides(k, i)) + return i; + } + abort(); + } + + canonical_kmer::canonical_kmer(void) : kmer() {} + + canonical_kmer::canonical_kmer(base b) : kmer(canonicalize(kmer(b))) {} + + canonical_kmer::canonical_kmer(int l, uint64_t v) + : kmer(canonicalize(kmer(l, v))) {} + + canonical_kmer::canonical_kmer(std::string s) : kmer(canonicalize(kmer(s))) {} + + canonical_kmer::canonical_kmer(kmer k) : kmer(canonicalize(k)) {} + +} + + + +monochromatic_component_iterator::monochromatic_component_iterator(const CQF *g) + : cqf(g), it(g->begin(0)) { + cqf = g; + // initialize cqf iterator + k = cqf->keybits(); + //it = &(cqf->begin(0)); + sdsl::util::assign(visited, sdsl::bit_vector(cqf->slots(), 0)); + +} + +monochromatic_component_iterator::work_item +monochromatic_component_iterator::front(std::queue &w) { + return w.front(); +} + +Mc_stats monochromatic_component_iterator::operator*(void) { + if (work.empty()) { + std::cerr << "Throw Exception. We're out of bound for CQF.\n"; + std::exit(1); + } + //return getMC(); + Mc_stats res; + while (!work.empty()) { + work_item w = front(work); + work.pop(); + + for (auto &neighbor : neighbors(w)) { + if (neighbor.curr != w.curr) { + /*if (neighbor.colorid != w.colorid) { + res.min_dist = std::min(res.min_dist, manhattanDist(neighbor.colorid, w.colorid)); + }*/ + if (visited[neighbor.idx] == 0) { + if (neighbor.colorid == w.colorid) { + res.nodeCnt++; + work.push(neighbor); + } + } + } + } + visited[w.idx] = 1; // set the corresponding bit + } + return res; +} + +void monochromatic_component_iterator::operator++(void) { + while (!it.done() and (bool)(visited[it.iter.current])) { + ++it; + } + if (it.done()) return; + + KeyObject keyobj = *it; + uint64_t tmp = keyobj.key; + node root(k, HashUtil::hash_64i(tmp, BITMASK(k))); + monochromatic_component_iterator::work_item neww = {root,0, 0}; + work.push(neww); +} + +std::set + monochromatic_component_iterator::neighbors(monochromatic_component_iterator::work_item n) { + std::set result; + for (const auto b : dna::bases) { + uint64_t eqid, idx; + if (exists(b + n.curr, idx, eqid)) + result.insert(work_item(b >> n.curr, idx, eqid)); + if (exists(n.curr + b, idx, eqid)) + result.insert(work_item(n.curr << b, idx, eqid)); + } + return result; +} + +bool monochromatic_component_iterator::exists(edge e, uint64_t& idx, uint64_t& eqid) { + // no correction for exact CQF. right? + uint64_t tmp = e.val; + KeyObject key(HashUtil::hash_64(tmp, BITMASK(k)), 0, 0); + auto eq_idx = cqf->queryValAndIdx(key); + if (eq_idx.first) { + eqid = eq_idx.first; + idx = eq_idx.second; + return true; + } + return false; +} + + +Mc_stats monochromatic_component_iterator::getMC() { + Mc_stats res; + while (!work.empty()) { + monochromatic_component_iterator::work_item w = front(work); + work.pop(); + + for (const auto &neighbor : neighbors(w)) { + if (neighbor.curr != w.curr) { + /*if (neighbor.colorid != w.colorid) { + res.min_dist = std::min(res.min_dist, manhattanDist(neighbor.colorid, w.colorid)); + }*/ + if (visited[neighbor.idx] == 0) { + if (neighbor.colorid == w.colorid) { + res.nodeCnt++; + work.push(neighbor); + } + } + } + } + visited[w.idx] = 1; // set the corresponding bit + } + return res; +} + +/* + * === FUNCTION ============================================================ + * Name: main + * Description: + * =========================================================================== + */ + +int main ( int argc, char *argv[] ) { + std::cout << argc << " ....... \n"; + + std::string cqf_file = argv[1]; + std::cout << cqf_file << "\n"; + std::string eq_file = argv[2]; + std::cout << eq_file << "\n"; + CQF cqf(cqf_file, false); +} \ No newline at end of file From 587d13d6cee627710c1bd5463760db139343bcf9 Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 6 Jun 2018 09:21:54 -0400 Subject: [PATCH 002/122] let CMake take CXXFLAGS on the command line --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f172752..fbc6c6b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,8 @@ else() endif() set(CMAKE_C_FLAGS "-std=gnu11 -Wno-unused-result -Wno-strict-aliasing -Wno-unused-function -Wno-sign-compare -Wno-implicit-function-declaration") -set(CMAKE_CXX_FLAGS "-std=c++11 ${ARCH} -Wno-unused-result -Wno-strict-aliasing -Wno-unused-function -Wno-sign-compare") +set(CMAKE_CXX_FLAGS "-std=c++11 ${ARCH} -Wno-unused-result -Wno-strict-aliasing -Wno-unused-function -Wno-sign-compare ${CMAKE_CXX_FLAGS}") +message("CMAKE_CXX_FLAGS = ${CMAKE_CXX_FLAGS}") #set(CMAKE_EXE_LINKER_FLAGS "-L/home/rob/cosmo/3rd_party_inst/lib/") include(GNUInstallDirs) From dfb1aa109f3723c3c7f52efd3537da684c322a07 Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Wed, 6 Jun 2018 09:58:35 -0400 Subject: [PATCH 003/122] Added mci to cmake file --- src/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 87431e9..8c642d5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,9 +27,17 @@ target_link_libraries(mantis_core # with the relevant sub-commands add_executable(mantis mantis.cc) +add_executable(monochromatic_component_iterator + monochromatic_component_iterator.cc) target_include_directories(mantis PUBLIC $) +target_include_directories(monochromatic_component_iterator PUBLIC + $) + + target_link_libraries(mantis mantis_core) +target_link_libraries(monochromatic_component_iterator + mantis_core) From e2f561a17aeda637e9993086fd0524138319cf23 Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 6 Jun 2018 10:01:46 -0400 Subject: [PATCH 004/122] fixed it --- src/monochromatic_component_iterator.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index fe61b2b..a9feaf5 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -4,6 +4,8 @@ #include "monochromatic_component_iterator.h" +uint64_t start_time; + namespace dna { /////////////// bases ///////////////// @@ -307,4 +309,4 @@ int main ( int argc, char *argv[] ) { std::string eq_file = argv[2]; std::cout << eq_file << "\n"; CQF cqf(cqf_file, false); -} \ No newline at end of file +} From b758c34ccc32b9ddcf0e2a6f1c59c1e3eedb2fce Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Wed, 6 Jun 2018 11:04:08 -0400 Subject: [PATCH 005/122] resolved a few bugs --- include/monochromatic_component_iterator.h | 2 +- src/monochromatic_component_iterator.cc | 83 +++++++++++----------- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/include/monochromatic_component_iterator.h b/include/monochromatic_component_iterator.h index 6eefce7..5f277ef 100644 --- a/include/monochromatic_component_iterator.h +++ b/include/monochromatic_component_iterator.h @@ -123,6 +123,7 @@ class monochromatic_component_iterator { } }; + bool done(); void operator++(void); Mc_stats operator*(void); @@ -148,7 +149,6 @@ class monochromatic_component_iterator { uint64_t manhattanDist(uint64_t eq1, uint64_t eq2); - Mc_stats getMC(); }; diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index a9feaf5..b473ed0 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -188,11 +188,14 @@ namespace dna { monochromatic_component_iterator::monochromatic_component_iterator(const CQF *g) : cqf(g), it(g->begin(0)) { - cqf = g; // initialize cqf iterator - k = cqf->keybits(); + k = cqf->keybits()/2; + std::cerr << " k : " << k << "\n"; //it = &(cqf->begin(0)); sdsl::util::assign(visited, sdsl::bit_vector(cqf->slots(), 0)); + std::cerr << "slots: " << cqf->slots() << "\n"; + // initialize the first item and get to done if it's empty + ++(*this); } @@ -201,6 +204,29 @@ monochromatic_component_iterator::front(std::queue keybits()))); + monochromatic_component_iterator::work_item neww = {root,0, 0}; + work.push(neww); +} + Mc_stats monochromatic_component_iterator::operator*(void) { if (work.empty()) { std::cerr << "Throw Exception. We're out of bound for CQF.\n"; @@ -212,7 +238,9 @@ Mc_stats monochromatic_component_iterator::operator*(void) { work_item w = front(work); work.pop(); + std::cerr << "for w " << w.idx << " : "; for (auto &neighbor : neighbors(w)) { + std::cerr << "n " << neighbor.idx << " "; if (neighbor.curr != w.curr) { /*if (neighbor.colorid != w.colorid) { res.min_dist = std::min(res.min_dist, manhattanDist(neighbor.colorid, w.colorid)); @@ -224,23 +252,15 @@ Mc_stats monochromatic_component_iterator::operator*(void) { } } } + else { + std::cerr << " same || "; + } } visited[w.idx] = 1; // set the corresponding bit - } - return res; -} + std::cerr << " idx " << w.idx << " visited " << visited[w.idx] << "\n"; -void monochromatic_component_iterator::operator++(void) { - while (!it.done() and (bool)(visited[it.iter.current])) { - ++it; } - if (it.done()) return; - - KeyObject keyobj = *it; - uint64_t tmp = keyobj.key; - node root(k, HashUtil::hash_64i(tmp, BITMASK(k))); - monochromatic_component_iterator::work_item neww = {root,0, 0}; - work.push(neww); + return res; } std::set @@ -259,7 +279,7 @@ std::set bool monochromatic_component_iterator::exists(edge e, uint64_t& idx, uint64_t& eqid) { // no correction for exact CQF. right? uint64_t tmp = e.val; - KeyObject key(HashUtil::hash_64(tmp, BITMASK(k)), 0, 0); + KeyObject key(HashUtil::hash_64(tmp, BITMASK(cqf->keybits())), 0, 0); auto eq_idx = cqf->queryValAndIdx(key); if (eq_idx.first) { eqid = eq_idx.first; @@ -270,30 +290,6 @@ bool monochromatic_component_iterator::exists(edge e, uint64_t& idx, uint64_t& e } -Mc_stats monochromatic_component_iterator::getMC() { - Mc_stats res; - while (!work.empty()) { - monochromatic_component_iterator::work_item w = front(work); - work.pop(); - - for (const auto &neighbor : neighbors(w)) { - if (neighbor.curr != w.curr) { - /*if (neighbor.colorid != w.colorid) { - res.min_dist = std::min(res.min_dist, manhattanDist(neighbor.colorid, w.colorid)); - }*/ - if (visited[neighbor.idx] == 0) { - if (neighbor.colorid == w.colorid) { - res.nodeCnt++; - work.push(neighbor); - } - } - } - } - visited[w.idx] = 1; // set the corresponding bit - } - return res; -} - /* * === FUNCTION ============================================================ * Name: main @@ -302,11 +298,16 @@ Mc_stats monochromatic_component_iterator::getMC() { */ int main ( int argc, char *argv[] ) { - std::cout << argc << " ....... \n"; std::string cqf_file = argv[1]; std::cout << cqf_file << "\n"; std::string eq_file = argv[2]; std::cout << eq_file << "\n"; CQF cqf(cqf_file, false); + monochromatic_component_iterator mci(&cqf); + //while(!mci.done()) { + for (auto i = 0; i < 10; i++) { + std::cout << (*mci).nodeCnt << "\n"; + ++mci; + } } From dc466a8fe9e47b7ba2c21ffbbda7f4f3c20bce6d Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Wed, 6 Jun 2018 11:10:39 -0400 Subject: [PATCH 006/122] included kmer.h --- include/monochromatic_component_iterator.h | 75 +-------- src/monochromatic_component_iterator.cc | 180 --------------------- 2 files changed, 1 insertion(+), 254 deletions(-) diff --git a/include/monochromatic_component_iterator.h b/include/monochromatic_component_iterator.h index 5f277ef..949f159 100644 --- a/include/monochromatic_component_iterator.h +++ b/include/monochromatic_component_iterator.h @@ -22,84 +22,11 @@ #include "tsl/sparse_map.h" #include "sdsl/bit_vectors.hpp" //#include "bitvector.h" +#include "kmer.h" #include "cqf.h" #include "hashutil.h" #include "common_types.h" -namespace dna { - -/////////////// bases ///////////////// - enum base { - C = 0, A = 1, T = 2, G = 3 - }; - - base operator-(base b); // return the complementary base - extern const base bases[4]; - extern const std::map base_from_char; - extern const std::map base_to_char; - -///////////// kmers ///////////////////// - class kmer { - public: - int len; - uint64_t val; - - kmer(void); - - kmer(base b); - - kmer(int l, uint64_t v); - - kmer(std::string s); - - // Convert to string - operator std::string() const; - }; - - bool operator<(kmer a, kmer b); - - bool operator==(kmer a, kmer b); - - bool operator!=(kmer a, kmer b); - -// Return the reverse complement of k - kmer operator-(kmer k); - - kmer canonicalize(kmer k); - -// Return the kmer of length |a| that results from shifting b into a -// from the right - kmer operator<<(kmer a, kmer b); - -// Return the kmer of length |b| that results from shifting a into b -// from the left - kmer operator>>(kmer a, kmer b); - -// Append two kmers - kmer operator+(kmer a, kmer b); - - kmer suffix(kmer k, int len); - - kmer prefix(kmer k, int len); - -// The purpose of this class is to enable us to declare containers -// as holding canonical kmers, e.g. set. Then all -// inserts/queries/etc will automatically canonicalize their -// arguments. - class canonical_kmer : public kmer { - public: - canonical_kmer(void); - - canonical_kmer(base b); - - canonical_kmer(int l, uint64_t v); - - canonical_kmer(std::string s); - - canonical_kmer(kmer k); - }; -} - struct Mc_stats { uint64_t nodeCnt = 1; uint64_t min_dist = -1; // infinity diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index b473ed0..3bc8d79 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -6,186 +6,6 @@ uint64_t start_time; -namespace dna { - - /////////////// bases ///////////////// - base operator-(base b) { - return (base)((~((uint64_t) b)) & 0x3ULL); - } - - const base bases[4] = {C, A, T, G}; - const std::map base_from_char = {{'A', A}, - {'C', C}, - {'G', G}, - {'T', T}, - {'N', A}}; - const std::map base_to_char = {{A, 'A'}, - {C, 'C'}, - {G, 'G'}, - {T, 'T'}}; - - ///////////// kmers ///////////////////// - kmer::kmer(void) : len(0), val(0) {} - - kmer::kmer(base b) : len(1), val((uint64_t) b) {} - - kmer::kmer(int l, uint64_t v) : len(l), val(v & BITMASK(2 * l)) { - assert(l <= 32); - } - - static uint64_t string_to_kmer_val(std::string s) { - uint64_t val = 0; - for (auto c : s) - val = (val << 2) | ((uint64_t) (base_from_char.at(c))); - return val; - } - - kmer::kmer(std::string s) : len(s.size()), val(string_to_kmer_val(s)) { - assert(s.size() <= 32); - } - - // Convert to string - kmer::operator std::string() const { - std::string s; - for (auto i = 1; i < len + 1; i++) - s = s + base_to_char.at((base)((val >> (2 * (len - i))) & BITMASK(2))); - return s; - } - - bool operator<(kmer a, kmer b) { - return a.len != b.len ? a.len < b.len : a.val < b.val; - } - - bool operator==(kmer a, kmer b) { - return a.len == b.len && a.val == b.val; - } - - bool operator!=(kmer a, kmer b) { - return !operator==(a, b); - } - - // Return the reverse complement of k - kmer operator-(kmer k) { - uint64_t val = k.val; - val = - (val >> 32) | - (val << 32); - val = - ((val >> 16) & 0x0000ffff0000ffff) | - ((val << 16) & 0xffff0000ffff0000); - val = - ((val >> 8) & 0x00ff00ff00ff00ff) | - ((val << 8) & 0xff00ff00ff00ff00); - val = - ((val >> 4) & 0x0f0f0f0f0f0f0f0f) | - ((val << 4) & 0xf0f0f0f0f0f0f0f0); - val = - ((val >> 2) & 0x3333333333333333) | - ((val << 2) & 0xcccccccccccccccc); - val = ~val; - val >>= 64 - 2 * k.len; - return kmer(k.len, val); - } - - // backwards from standard definition to match kmer.h definition - kmer canonicalize(kmer k) { - return -k < k ? k : -k; - } - - // Return the kmer of length |a| that results from shifting b into a - // from the right - kmer operator<<(kmer a, kmer b) { - uint64_t val = ((a.val << (2 * b.len)) | b.val) & BITMASK(2 * a.len); - return kmer(a.len, val); - } - - // Return the kmer of length |b| that results from shifting b into a - // from the left - kmer operator>>(kmer a, kmer b) { - uint64_t val - = ((b.val >> (2 * a.len)) | (a.val << (2 * (b.len - a.len)))) - & BITMASK(2 * b.len); - return kmer(b.len, val); - } - - // Append two kmers - kmer operator+(kmer a, kmer b) { - int len = a.len + b.len; - assert(len <= 32); - uint64_t val = (a.val << (2 * b.len)) | b.val; - return kmer(len, val); - } - - kmer prefix(kmer k, int len) { return kmer(len, k.val >> (2 * (k.len - len))); } - - kmer suffix(kmer k, int len) { return kmer(len, k.val & BITMASK(2 * len)); } - - bool period_divides(kmer k, uint64_t periodicity) { - static const uint64_t multipliers[33] = - { - 0, - 0x5555555555555555, // 1 - 0x1111111111111111, // 2 - 0x1041041041041041, // 3 - 0x0101010101010101, // 4 - 0x1004010040100401, // 5 - 0x1001001001001001, // 6 - 0x0100040010004001, // 7 - 0x0001000100010001, // 8 - 0x0040001000040001, // 9 - 0x1000010000100001, // 10 - 0x0000100000400001, // 11 - 0x0001000001000001, // 12 - 0x0010000004000001, // 13 - 0x0100000010000001, // 14 - 0x1000000040000001, // 15 - 0x0000000100000001, // 16 - 0x0000000400000001, // 17 - 0x0000001000000001, // 18 - 0x0000004000000001, // 19 - 0x0000010000000001, // 20 - 0x0000040000000001, // 21 - 0x0000100000000001, // 22 - 0x0000400000000001, // 23 - 0x0001000000000001, // 24 - 0x0004000000000001, // 25 - 0x0010000000000001, // 26 - 0x0040000000000001, // 27 - 0x0100000000000001, // 28 - 0x0400000000000001, // 29 - 0x1000000000000001, // 30 - 0x4000000000000001, // 31 - 0x0000000000000001, // 32 - }; - uint64_t piece = k.val & BITMASK(2 * periodicity); - piece = piece * multipliers[periodicity]; - piece = piece & BITMASK(2 * k.len); - return piece == k.val; - } - - uint64_t period(kmer k) { - for (int i = 1; i <= k.len; i++) { - if (period_divides(k, i)) - return i; - } - abort(); - } - - canonical_kmer::canonical_kmer(void) : kmer() {} - - canonical_kmer::canonical_kmer(base b) : kmer(canonicalize(kmer(b))) {} - - canonical_kmer::canonical_kmer(int l, uint64_t v) - : kmer(canonicalize(kmer(l, v))) {} - - canonical_kmer::canonical_kmer(std::string s) : kmer(canonicalize(kmer(s))) {} - - canonical_kmer::canonical_kmer(kmer k) : kmer(canonicalize(k)) {} - -} - - - monochromatic_component_iterator::monochromatic_component_iterator(const CQF *g) : cqf(g), it(g->begin(0)) { // initialize cqf iterator From 87cb8e03b5ee699ba2face2b5456f5ec5b3e3397 Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Wed, 6 Jun 2018 16:27:13 -0400 Subject: [PATCH 007/122] fixed the bugs in the bfs algorithm and logic of operators --- include/monochromatic_component_iterator.h | 8 +- src/monochromatic_component_iterator.cc | 111 +++++++++++---------- 2 files changed, 62 insertions(+), 57 deletions(-) diff --git a/include/monochromatic_component_iterator.h b/include/monochromatic_component_iterator.h index 5f277ef..c55b470 100644 --- a/include/monochromatic_component_iterator.h +++ b/include/monochromatic_component_iterator.h @@ -101,7 +101,7 @@ namespace dna { } struct Mc_stats { - uint64_t nodeCnt = 1; + uint64_t nodeCnt = 0; uint64_t min_dist = -1; // infinity }; @@ -119,7 +119,7 @@ class monochromatic_component_iterator { work_item(node currin, uint64_t idxin, uint64_t coloridin) : curr(currin), idx(idxin), colorid(coloridin) {} bool operator<(const work_item &item2) const { - return (*this).idx < item2.idx; + return (*this).curr < item2.curr; } }; @@ -129,13 +129,13 @@ class monochromatic_component_iterator { Mc_stats operator*(void); monochromatic_component_iterator(const CQF *g); + uint64_t cntr = 0; private: uint32_t k; std::queue work; - - + std::unordered_set visitedKeys; const CQF *cqf; CQF::Iterator it; sdsl::bit_vector visited; diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index b473ed0..771ef13 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -10,19 +10,19 @@ namespace dna { /////////////// bases ///////////////// base operator-(base b) { - return (base)((~((uint64_t) b)) & 0x3ULL); + return (base) ((~((uint64_t) b)) & 0x3ULL); } const base bases[4] = {C, A, T, G}; const std::map base_from_char = {{'A', A}, - {'C', C}, - {'G', G}, - {'T', T}, - {'N', A}}; + {'C', C}, + {'G', G}, + {'T', T}, + {'N', A}}; const std::map base_to_char = {{A, 'A'}, - {C, 'C'}, - {G, 'G'}, - {T, 'T'}}; + {C, 'C'}, + {G, 'G'}, + {T, 'T'}}; ///////////// kmers ///////////////////// kmer::kmer(void) : len(0), val(0) {} @@ -48,7 +48,7 @@ namespace dna { kmer::operator std::string() const { std::string s; for (auto i = 1; i < len + 1; i++) - s = s + base_to_char.at((base)((val >> (2 * (len - i))) & BITMASK(2))); + s = s + base_to_char.at((base) ((val >> (2 * (len - i))) & BITMASK(2))); return s; } @@ -185,12 +185,11 @@ namespace dna { } - monochromatic_component_iterator::monochromatic_component_iterator(const CQF *g) : cqf(g), it(g->begin(0)) { // initialize cqf iterator - k = cqf->keybits()/2; - std::cerr << " k : " << k << "\n"; + k = cqf->keybits() / 2; + std::cerr << "k : " << k << "\n"; //it = &(cqf->begin(0)); sdsl::util::assign(visited, sdsl::bit_vector(cqf->slots(), 0)); std::cerr << "slots: " << cqf->slots() << "\n"; @@ -200,84 +199,89 @@ monochromatic_component_iterator::monochromatic_component_iterator(const CQF &w) { +monochromatic_component_iterator::front(std::queue &w) { return w.front(); } -bool monochromatic_component_iterator::done() {return it.done();} +bool monochromatic_component_iterator::done() { return it.done(); } + void monochromatic_component_iterator::operator++(void) { - std::cerr << "++ "; + + if (it.done()) return; // don't cross the bound (undefined behaviour) + ++it; + auto keyFromKmer = [this](KeyObject keyobj) { + return HashUtil::hash_64i(keyobj.key, BITMASK(this->cqf->keybits())); + }; while (!it.done()) { - std::cerr << " , " << it.iter.current << " " << (bool)(visited[it.iter.current]) << " "; - if ((bool)(visited[it.iter.current])) { + if (visitedKeys.find(keyFromKmer(*it)) != visitedKeys.end()) { //(bool)(visited[it.iter.current])) { ++it; - //std::cerr << " ++it "; - } - else { + } else { break; - std::cerr << " break\n"; } } - std::cerr << "\n"; - if (it.done()) return; - - KeyObject keyobj = *it; - node root(k, HashUtil::hash_64i(keyobj.key, BITMASK(cqf->keybits()))); - monochromatic_component_iterator::work_item neww = {root,0, 0}; - work.push(neww); } Mc_stats monochromatic_component_iterator::operator*(void) { - if (work.empty()) { - std::cerr << "Throw Exception. We're out of bound for CQF.\n"; + if (!work.empty()) { + std::cerr << "Throw Exception. The work queue should be empty at this point.\n"; std::exit(1); } - //return getMC(); Mc_stats res; + if (it.done()) return res; + + KeyObject keyobj = *it; + node root(k, HashUtil::hash_64i(keyobj.key, BITMASK(cqf->keybits()))); + monochromatic_component_iterator::work_item neww = {root, it.iter.current, keyobj.count}; + work.push(neww); + while (!work.empty()) { work_item w = front(work); work.pop(); - - std::cerr << "for w " << w.idx << " : "; + // pass over those that have been already visited + if (visitedKeys.find(w.curr.val) != visitedKeys.end()) { + continue; + } + //std::cerr << "for w " << std::string(w.curr) << " : "; for (auto &neighbor : neighbors(w)) { - std::cerr << "n " << neighbor.idx << " "; + //std::cerr << "n " << std::string(neighbor.curr) << " "; if (neighbor.curr != w.curr) { /*if (neighbor.colorid != w.colorid) { res.min_dist = std::min(res.min_dist, manhattanDist(neighbor.colorid, w.colorid)); }*/ - if (visited[neighbor.idx] == 0) { + if (visitedKeys.find(neighbor.curr.val) == visitedKeys.end()) { //visited[neighbor.idx] == 0) { if (neighbor.colorid == w.colorid) { - res.nodeCnt++; work.push(neighbor); } } } - else { - std::cerr << " same || "; - } } + //std::cerr << "\n"; + visitedKeys.insert(w.curr.val); + res.nodeCnt++; visited[w.idx] = 1; // set the corresponding bit - std::cerr << " idx " << w.idx << " visited " << visited[w.idx] << "\n"; + cntr++; + if (visitedKeys.size() % 1000000 == 0) + std::cerr << "visited " << cntr << " kmers\n"; + //std::cerr << " idx " << w.idx << " visited " << visited[w.idx] << "\n"; } return res; } -std::set - monochromatic_component_iterator::neighbors(monochromatic_component_iterator::work_item n) { - std::set result; +std::set +monochromatic_component_iterator::neighbors(monochromatic_component_iterator::work_item n) { + std::set result; for (const auto b : dna::bases) { uint64_t eqid, idx; - if (exists(b + n.curr, idx, eqid)) + if (exists(b >> n.curr/*b + n.curr*/, idx, eqid)) result.insert(work_item(b >> n.curr, idx, eqid)); - if (exists(n.curr + b, idx, eqid)) + if (exists(n.curr << b/*n.curr + b*/, idx, eqid)) result.insert(work_item(n.curr << b, idx, eqid)); } return result; } -bool monochromatic_component_iterator::exists(edge e, uint64_t& idx, uint64_t& eqid) { - // no correction for exact CQF. right? +bool monochromatic_component_iterator::exists(edge e, uint64_t &idx, uint64_t &eqid) { uint64_t tmp = e.val; KeyObject key(HashUtil::hash_64(tmp, BITMASK(cqf->keybits())), 0, 0); auto eq_idx = cqf->queryValAndIdx(key); @@ -297,17 +301,18 @@ bool monochromatic_component_iterator::exists(edge e, uint64_t& idx, uint64_t& e * =========================================================================== */ -int main ( int argc, char *argv[] ) { +int main(int argc, char *argv[]) { std::string cqf_file = argv[1]; - std::cout << cqf_file << "\n"; std::string eq_file = argv[2]; - std::cout << eq_file << "\n"; CQF cqf(cqf_file, false); monochromatic_component_iterator mci(&cqf); - //while(!mci.done()) { - for (auto i = 0; i < 10; i++) { - std::cout << (*mci).nodeCnt << "\n"; + uint64_t cntrr = 0; + while (!mci.done()) { + cntrr++; + //for (auto i = 0; i < 3; i++) { + //std::cout << (*mci).nodeCnt << "\n"; ++mci; } + std::cerr << "cntr: " << cntrr << "\n"; } From a69a7f4af3c4d87628621eec68bc1eb9548bc7ee Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Wed, 6 Jun 2018 16:30:26 -0400 Subject: [PATCH 008/122] reformating --- src/monochromatic_component_iterator.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index 771ef13..a1d5784 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -208,7 +208,7 @@ bool monochromatic_component_iterator::done() { return it.done(); } void monochromatic_component_iterator::operator++(void) { if (it.done()) return; // don't cross the bound (undefined behaviour) - ++it; + ++it; auto keyFromKmer = [this](KeyObject keyobj) { return HashUtil::hash_64i(keyobj.key, BITMASK(this->cqf->keybits())); }; From 9a11b78f1f3a773cf0cb67d0044a04c06f05bb7a Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Wed, 6 Jun 2018 16:39:00 -0400 Subject: [PATCH 009/122] correct version (not scalable yet) --- src/monochromatic_component_iterator.cc | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index a1d5784..3e2ac7c 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -188,13 +188,10 @@ namespace dna { monochromatic_component_iterator::monochromatic_component_iterator(const CQF *g) : cqf(g), it(g->begin(0)) { // initialize cqf iterator - k = cqf->keybits() / 2; + k = cqf->keybits() / 2; // 2-bit encoded std::cerr << "k : " << k << "\n"; - //it = &(cqf->begin(0)); sdsl::util::assign(visited, sdsl::bit_vector(cqf->slots(), 0)); std::cerr << "slots: " << cqf->slots() << "\n"; - // initialize the first item and get to done if it's empty - ++(*this); } @@ -307,12 +304,8 @@ int main(int argc, char *argv[]) { std::string eq_file = argv[2]; CQF cqf(cqf_file, false); monochromatic_component_iterator mci(&cqf); - uint64_t cntrr = 0; while (!mci.done()) { - cntrr++; - //for (auto i = 0; i < 3; i++) { - //std::cout << (*mci).nodeCnt << "\n"; + std::cout << (*mci).nodeCnt << "\n"; ++mci; } - std::cerr << "cntr: " << cntrr << "\n"; } From fc37687314652dab1feb5f6599a74ad030fbc9c2 Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Mon, 11 Jun 2018 01:28:36 -0400 Subject: [PATCH 010/122] Added the distance calculator --- include/monochromatic_component_iterator.h | 11 ++- src/monochromatic_component_iterator.cc | 87 +++++++++++++++++++--- 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/include/monochromatic_component_iterator.h b/include/monochromatic_component_iterator.h index c55b470..3aceacc 100644 --- a/include/monochromatic_component_iterator.h +++ b/include/monochromatic_component_iterator.h @@ -15,13 +15,14 @@ #include #include #include +#include #include #include "sparsepp/spp.h" #include "tsl/sparse_map.h" #include "sdsl/bit_vectors.hpp" -//#include "bitvector.h" +#include "bitvector.h" #include "cqf.h" #include "hashutil.h" #include "common_types.h" @@ -128,7 +129,11 @@ class monochromatic_component_iterator { Mc_stats operator*(void); - monochromatic_component_iterator(const CQF *g); +//monochromatic_component_iterator(const CQF *g); + monochromatic_component_iterator(const CQF *g, + BitVectorRRR& bvin, + uint64_t num_samplesin=2586); + void neighborDist(); uint64_t cntr = 0; private: @@ -138,6 +143,8 @@ class monochromatic_component_iterator { std::unordered_set visitedKeys; const CQF *cqf; CQF::Iterator it; + BitVectorRRR& bv; + uint64_t num_samples; sdsl::bit_vector visited; bool exists(edge e, uint64_t &idx, uint64_t &eqid); diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index 3e2ac7c..deece47 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -185,14 +185,18 @@ namespace dna { } -monochromatic_component_iterator::monochromatic_component_iterator(const CQF *g) - : cqf(g), it(g->begin(0)) { +//////////////////////////////// monochromatic_component_iterator ////////////////////////// + +monochromatic_component_iterator::monochromatic_component_iterator(const CQF *g, + BitVectorRRR& bvin, + uint64_t num_samplesin) + : cqf(g), it(g->begin(0)), bv(bvin), num_samples(num_samplesin) { // initialize cqf iterator k = cqf->keybits() / 2; // 2-bit encoded std::cerr << "k : " << k << "\n"; sdsl::util::assign(visited, sdsl::bit_vector(cqf->slots(), 0)); + std::cerr << "kmers: " << cqf->size() << "\n"; std::cerr << "slots: " << cqf->slots() << "\n"; - } monochromatic_component_iterator::work_item @@ -210,7 +214,8 @@ void monochromatic_component_iterator::operator++(void) { return HashUtil::hash_64i(keyobj.key, BITMASK(this->cqf->keybits())); }; while (!it.done()) { - if (visitedKeys.find(keyFromKmer(*it)) != visitedKeys.end()) { //(bool)(visited[it.iter.current])) { + if (visitedKeys.find(keyFromKmer(*it)) != visitedKeys.end()) { + //if ((bool)(visited[it.iter.current])) { ++it; } else { break; @@ -236,6 +241,7 @@ Mc_stats monochromatic_component_iterator::operator*(void) { work.pop(); // pass over those that have been already visited if (visitedKeys.find(w.curr.val) != visitedKeys.end()) { + //if (visited[w.idx]) { continue; } //std::cerr << "for w " << std::string(w.curr) << " : "; @@ -245,7 +251,8 @@ Mc_stats monochromatic_component_iterator::operator*(void) { /*if (neighbor.colorid != w.colorid) { res.min_dist = std::min(res.min_dist, manhattanDist(neighbor.colorid, w.colorid)); }*/ - if (visitedKeys.find(neighbor.curr.val) == visitedKeys.end()) { //visited[neighbor.idx] == 0) { + if (visitedKeys.find(neighbor.curr.val) == visitedKeys.end()) { + //if (visited[neighbor.idx] == 0) { if (neighbor.colorid == w.colorid) { work.push(neighbor); } @@ -258,6 +265,7 @@ Mc_stats monochromatic_component_iterator::operator*(void) { visited[w.idx] = 1; // set the corresponding bit cntr++; if (visitedKeys.size() % 1000000 == 0) + //if (cntr % 1000000 == 0) std::cerr << "visited " << cntr << " kmers\n"; //std::cerr << " idx " << w.idx << " visited " << visited[w.idx] << "\n"; @@ -290,6 +298,50 @@ bool monochromatic_component_iterator::exists(edge e, uint64_t &idx, uint64_t &e return false; } +uint64_t monochromatic_component_iterator::manhattanDist(uint64_t eqid1, uint64_t eqid2) { + uint64_t dist{0}; + std::vector eq1(( (num_samples-1) / 64) + 1), eq2(( (num_samples-1) / 64) + 1); + auto colorbuilder = [this](std::vector &eq, uint64_t eqid) { + uint64_t i{0}, bitcnt{0}, wrdcnt{0}; + while (i < this->num_samples) { + bitcnt = std::min(this->num_samples - i, (uint64_t)64); + uint64_t wrd = (this->bv).get_int(this->num_samples * eqid + i, bitcnt); + eq[wrdcnt++] = wrd; + i += bitcnt; + } + }; + colorbuilder(eq1, eqid1); + colorbuilder(eq2, eqid2); + + for(uint64_t i = 0; i < eq1.size(); i++) { + if (eq1[i] != eq2[i]) + dist += sdsl::bits::cnt(eq1[i] ^ eq2[i]); + } + return dist; + +} + +void monochromatic_component_iterator::neighborDist() { + KeyObject keyobj = *it; + //std::cout << "keyobj cnt: " << keyobj.count << "\n"; + node curn(k, HashUtil::hash_64i(keyobj.key, BITMASK(cqf->keybits()))); + work_item cur = {curn, it.iter.current, keyobj.count}; + uint64_t mind{UINTMAX_MAX}, meand{0}, maxd{0}, neighborCnt{0}; + for (auto& nei : neighbors(cur)) { + neighborCnt++; + if (nei.colorid != cur.colorid) { + //std::cerr << cur.colorid << " other: " << nei.colorid << "\n"; + auto d = manhattanDist(nei.colorid, cur.colorid); + mind = std::min(mind, d); + maxd = std::max(maxd, d); + meand += d; + } + else + mind = 0; + } + std::cout << neighborCnt << "\t" << (neighborCnt?(mind):0) << "\t" + << (neighborCnt?(meand/neighborCnt):0) << "\t" << maxd << "\n"; +} /* * === FUNCTION ============================================================ @@ -300,12 +352,25 @@ bool monochromatic_component_iterator::exists(edge e, uint64_t &idx, uint64_t &e int main(int argc, char *argv[]) { - std::string cqf_file = argv[1]; - std::string eq_file = argv[2]; + std::string command = argv[1]; + std::string cqf_file = argv[2]; + std::string eq_file = argv[3]; + uint64_t num_samples = 2586; + if (argc > 4) + num_samples = std::stoull(argv[4]); + std::cerr << "num samples: " << num_samples << "\n"; CQF cqf(cqf_file, false); - monochromatic_component_iterator mci(&cqf); - while (!mci.done()) { - std::cout << (*mci).nodeCnt << "\n"; - ++mci; + BitVectorRRR bv(eq_file); + monochromatic_component_iterator mci(&cqf, bv, num_samples); + if (command == "monocomp") { + while (!mci.done()) { + std::cout << (*mci).nodeCnt << "\n"; + ++mci; + } + } else if (command == "neighborDist") { + while (!mci.done()) { + mci.neighborDist(); + ++mci; + } } } From 28a0aab6ac67fd9216e06a777d86d2d81bc8ba5e Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Wed, 13 Jun 2018 12:16:40 -0400 Subject: [PATCH 011/122] walkEqcls provides different statistics/capabilities/tests that require traversing eq. cls. table --- src/walkEqcls.cc | 830 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 830 insertions(+) create mode 100644 src/walkEqcls.cc diff --git a/src/walkEqcls.cc b/src/walkEqcls.cc new file mode 100644 index 0000000..743906f --- /dev/null +++ b/src/walkEqcls.cc @@ -0,0 +1,830 @@ + +#include "compressedSetBit.h" +#include "hashutil.h" +#include "bitvector.h" +#include "sdsl/bits.hpp" +#include "sparsepp/spp.h" + +#include +#include +#include +#include + + +void print_time_elapsed(string desc, struct timeval *start, struct timeval *end) { + struct timeval elapsed; + if (start->tv_usec > end->tv_usec) { + end->tv_usec += 1000000; + end->tv_sec--; + } + elapsed.tv_usec = end->tv_usec - start->tv_usec; + elapsed.tv_sec = end->tv_sec - start->tv_sec; + float time_elapsed = (elapsed.tv_sec * 1000000 + elapsed.tv_usec) / 1000000.f; + std::cout << desc << "Total Time Elapsed: " << to_string(time_elapsed) << " seconds" << std::endl; +} + +// @input +// cnt: setbit cnt +// num_samples: total number of bits +// @return true if the output of delta_compression/decompression is the same as bitvector_rrr +//template +bool validate(uint16_t cnt, uint16_t num_samples = 2586) { + std::cout << "\n[Validate]\n"; + + std::cout << "validate for " << cnt << " set bits out of " << num_samples << "\n"; + std::set randIdx; + + BitVector bv(num_samples); + std::vector idxList(cnt); + size_t i = 0; + while (i < cnt) { + //std::cout << "i:"< bvr_idxList; + uint16_t wrdCnt = 64; + for (uint16_t i = 0; i < num_samples; i += wrdCnt) { + wrdCnt = std::min((uint16_t) 64, (uint16_t) (num_samples - i)); + uint64_t wrd = bvr.get_int(i, wrdCnt); + for (uint16_t j = 0, idx = i; j < wrdCnt; j++, idx++) { + if (wrd >> j & 0x01) { + //std::cout << i << " " << j << "\n"; + bvr_idxList.push_back(idx); + } + } + } + //std::cout <<"\nidx size: " << idxList.size() << "\n"; + CompressedSetBit setBitList(idxList); + + vector output; + setBitList.uncompress(output); + //std::cout << "\nAfter compress&decompress size is " << output.size() << "\n"; + if (output.size() != bvr_idxList.size()) { + std::cout << "rrr idx list size: " << bvr_idxList.size() << " deltac size: " << output.size() << "\n"; + return false; + } + for (size_t i = 0; i < output.size(); i++) + if (output[i] != bvr_idxList[i]) { + std::cout << i << " rrr idx: " << bvr_idxList[i] << " deltac: " << output[i] << "\n"; + return false; + } + return true; +} + +/* + * validates if the rrr bvs in the input directory all together have unique rows + */ +void validate_uniqueness(std::string &directory, size_t num_samples) { + spp::sparse_hash_map> eqMap; + uint64_t cntr = 0; + + for (auto f = 0; f < 12; f++) { + uint64_t curCntr = 0; + std::string filename = directory + std::to_string(f) + "_eqclass_rrr.cls"; + BitVectorRRR eqcls(filename); + //subPattern.calcHash(); + uint64_t bitcntr = 0; + while (bitcntr < eqcls.bit_size()) { + std::cerr << "\n" << curCntr << "\n"; + + BitVector eq(num_samples); + size_t i = 0; + while (i < num_samples) { + size_t bitCnt = std::min(num_samples - i, (size_t) 64); + size_t wrd = eqcls.get_int(curCntr * num_samples + i, bitCnt); + for (size_t j = 0, curIdx = i; j < bitCnt; j++, curIdx++) { + if ((wrd >> j) & 0x01) { + eq.set(curIdx); + + } else { + std::cerr << curIdx << " "; + } + } + i += bitCnt; + bitcntr += bitCnt; + } + // Find if the eqclass of the kmer is already there. + // If it is there then increment the abundance. + // Else create a new eq class. + if (eqMap.find(eq) == eqMap.end()) { + eqMap.emplace(std::piecewise_construct, std::forward_as_tuple(eq), std::forward_as_tuple(cntr)); + } else { // eq class is seen before. Throw exception + std::cerr << "\nFound an already existing equivalence class pattern. eq. number " + << cntr << " file " << f << " cur cntr: " << curCntr + << " before: " << eqMap[eq] << "\n"; + std::exit(1); + } + cntr++; + curCntr++; + } + } + + +} + +void compareCompressions(std::string &filename, size_t num_samples) { + std::cout << "\n[CompareCompressions]\n"; + size_t gtBV = 0; + size_t gtBVR = 0; + size_t bvrGTbv = 0; + size_t bvEQbvr = 0; + size_t compressedSum = 0; + size_t bvSum = 0; + size_t bvrSum = 0; + + size_t roundIdxCnt = 0; + size_t totalIdxCnt = 0; + BitVectorRRR eqcls(filename); + size_t totalEqClsCnt = eqcls.bit_size() / num_samples; //222584822; + std::cout << "Total bit size: " << eqcls.bit_size() << "\ntotal # of equivalence classes: " << totalEqClsCnt + << "\n"; + for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { + BitVector bv(num_samples); + std::vector idxList; + idxList.reserve(num_samples); + size_t i = 0; + while (i < num_samples) { + size_t bitCnt = std::min(num_samples - i, (size_t) 64); + size_t wrd = eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); + for (size_t j = 0, curIdx = i; j < bitCnt; j++, curIdx++) { + if ((wrd >> j) & 0x01) { + bv.set(curIdx); + idxList.push_back(curIdx); + } + } + i += bitCnt; + } + //if (idxList.size() == 0) std::cerr << "Error!! " << eqclsCntr << " Shouldn't ever happen\n"; + totalIdxCnt += idxList.size(); + roundIdxCnt += idxList.size(); + if (eqclsCntr != 0 && (eqclsCntr) % 1000000 == 0) { + std::cout << "\n\nTotal number of experiments are : " << eqclsCntr << + "\nTotal average set bits: " << totalIdxCnt / eqclsCntr << + "\nThis round average set bits: " << roundIdxCnt / 1000000 << + "\nbv average size : " << bvSum / eqclsCntr << + "\nbvr average size : " << bvrSum / eqclsCntr << + "\ncompressed average size : " << compressedSum / eqclsCntr << + "\ncompressed > bv : " << gtBV << " or " << (gtBV * 100) / eqclsCntr << "% of the time" << + "\ncompressed > bvr : " << gtBVR << " or " << (gtBVR * 100) / eqclsCntr << "% of the time" << + "\nbvr > bv : " << bvrGTbv << " or " << (bvrGTbv * 100) / eqclsCntr << "% of the time" << + "\n"; + roundIdxCnt = 0; + } + + BitVectorRRR bvr(bv); + CompressedSetBit setBitList(idxList); + + size_t bvSize = bv.size_in_bytes(); + size_t bvrSize = bvr.size_in_bytes(); + size_t compressedSize = setBitList.size_in_bytes(); + bvSum += bvSize; + bvrSum += bvrSize; + compressedSum += compressedSize; + if (compressedSize > bvSize) gtBV++; + if (compressedSize > bvrSize) gtBVR++; + if (bvrSize > bvSize) bvrGTbv++; + if (bvrSize == bvSize) bvEQbvr++; + //vector output; + //setBitList.uncompress(output); + } + std::cout << "\n\n\nFinalResults:\n" << + "Total number of experiments are : " << totalEqClsCnt << "\n" << + "\nbv average size : " << bvSum / totalEqClsCnt << + "\nbvr average size : " << bvrSum / totalEqClsCnt << + "\ncompressed average size : " << compressedSum / totalEqClsCnt << + "\nHow many times compressed > bv : " << gtBV << " or " << (gtBV * 100) / totalEqClsCnt << "% of the time" + << + "\nHow many times compressed > bvr : " << gtBVR << " or " << (gtBVR * 100) / totalEqClsCnt + << "% of the time" << + "\nHow many times bvr > bv : " << bvrGTbv << " or " << (bvrGTbv * 100) / totalEqClsCnt << "% of the time" + << + "\nHow many times bvr == bv : " << bvEQbvr << " or " << (bvEQbvr * 100) / totalEqClsCnt << "% of the time" + << + "\n"; +} + +void compareCopies(size_t num_samples) { + std::cout << "\n[CompareCopies]\n"; + + struct timeval start, end; + struct timezone tzp; + + size_t numOfCopies = 10; + + size_t bit_cnt = 1000000000; + size_t i; + sdsl::bit_vector bits(bit_cnt); + srand(time(NULL)); + + gettimeofday(&start, &tzp); + for (auto i = 0; i < 1000000; i++) { + bits[rand() % bit_cnt] = 1; + } + gettimeofday(&end, &tzp); + print_time_elapsed("Set random bits done: ", &start, &end); + + sdsl::bit_vector bits2(bit_cnt); + sdsl::bit_vector bits3(bit_cnt); + sdsl::bit_vector bits4(bit_cnt + 1); + + gettimeofday(&start, &tzp); + for (auto c = 0; c < numOfCopies; c++) { + bits2 = bits; + } + gettimeofday(&end, &tzp); + print_time_elapsed("MemCpy: ", &start, &end); + + gettimeofday(&start, &tzp); + for (auto c = 0; c < numOfCopies; c++) { + i = 0; + while (i < bit_cnt) { + if (bits[i]) + bits3[i] = 1; + i++; + } + } + + gettimeofday(&end, &tzp); + print_time_elapsed("Index by index!: ", &start, &end); + + gettimeofday(&start, &tzp); + for (auto c = 0; c < numOfCopies; c++) { + i = 0; + size_t j = 1; + while (i < bit_cnt) { + size_t bitCnt = std::min(bit_cnt - i, (size_t) 64); + size_t wrd = bits.get_int(i, bitCnt); + bits4.set_int(j, wrd, bitCnt); + i += bitCnt; + j += bitCnt; + } + } + gettimeofday(&end, &tzp); + print_time_elapsed("Read int and write int: ", &start, &end); + +} + +void splitRows(std::string filename, + std::string outdir, + uint64_t num_samples = 2586) { + //std::cerr << "1\n"; + std::string eqfile; + std::ifstream eqlist(filename); + if (eqlist.is_open()) { + while (getline(myfile, eqfile)) { + sdsl::rrr_vector<63> bvr; + sdsl::load_from_file(bvr, eqfile); + /*sdsl::rank_support_rrr<1, 63> ranks(&bvr); + size_t prev = 0; + size_t cur = 0; + //std::cerr << "1\n"; + for (uint64_t i = 1; i < 21; i++) { + cur = ranks(2586 * i); + std::cout << "rank: " << cur << " " << cur - prev << "\n"; + prev = cur; + } + std::exit(1);*/ + std::cerr << "loaded " << bvr.size() << "\n"; + totalEqCls = bvr.size()/num_samples; + std::cerr << "" + sdsl::bit_vector eqcls(totalEqCls *num_samples, 0); + std::cerr << "created\n"; + for (uint64_t i = 0; i < totalEqCls * num_samples; i += 64) { + uint64_t bitCnt = std::min(totalEqCls * num_samples - i, (uint64_t) 64); + uint64_t wrd = bvr.get_int(i, bitCnt); + //std::cerr << wrd << " "; + eqcls.set_int(i, wrd, bitCnt); + } + sdsl::store_to_file(eqcls, outfilename); + + } + eqlist.close(); + } + +} + +void decompress(std::string filename, + std::string outfilename, + uint64_t totalEqCls, + uint64_t num_samples = 2586) { + //std::cerr << "1\n"; + sdsl::rrr_vector<63> bvr; + sdsl::load_from_file(bvr, filename); + sdsl::rank_support_rrr<1, 63> ranks(&bvr); + //std::cerr << "1\n"; + totalEqCls = 1; + size_t prev = 0; + size_t cur = 0; + //std::cerr << "1\n"; + for (uint64_t i = 1; i < 21; i++) { + cur = ranks(2586 * i); + std::cout << "rank: " << cur << " " << cur - prev << "\n"; + prev = cur; + } + std::exit(1); + std::cerr << "loaded " << bvr.size() << "\n"; + sdsl::bit_vector eqcls(totalEqCls * num_samples, 0); + std::cerr << "created\n"; + for (uint64_t i = 0; i < totalEqCls * num_samples; i += 64) { + uint64_t bitCnt = std::min(totalEqCls * num_samples - i, (uint64_t) 64); + uint64_t wrd = bvr.get_int(i, bitCnt); + //std::cerr << wrd << " "; + eqcls.set_int(i, wrd, bitCnt); + } + sdsl::store_to_file(eqcls, outfilename); +} + +void compare_reordered_original(std::string filename1, + std::string filename2, + std::string order_filename, + uint64_t num_samples) { + std::cout << "\n[compare_reordered_original]\n"; + + struct timeval start, end; + struct timezone tzp; + + uint64_t setBitCnt1{0}, setBitCnt2{0}; + std::ifstream orderFile(order_filename); + std::vector newOrder; + while (!orderFile.eof()) { + size_t val; + orderFile >> val; + //std::cout << val << " "; + newOrder.push_back(val); + } + std::cout << "\n"; + sdsl::bit_vector eqcls; + sdsl::bit_vector reordered_eqcls; + sdsl::load_from_file(eqcls, filename1); + sdsl::load_from_file(reordered_eqcls, filename2); + size_t totalEqClsCnt = reordered_eqcls.size() / num_samples; //222584822; + std::cout << "Total number of Eq. Clss: " << totalEqClsCnt << "\n"; + //totalEqClsCnt = 1000001; + gettimeofday(&start, &tzp); + + for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { + + std::vector eqcls_setbits; + std::vector reordered_eqcls_setbits; + eqcls_setbits.reserve(num_samples); + reordered_eqcls_setbits.reserve(num_samples); + + // transpose the matrix of eq. classes + size_t i = 0; + while (i < num_samples) { + size_t bitCnt = std::min(num_samples - i, (size_t) 64); + size_t wrd1 = eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); + size_t wrd2 = reordered_eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); + for (size_t j = 0, curIdx = i; j < bitCnt; j++, curIdx++) { + if ((wrd1 >> j) & 0x01) { + setBitCnt1++; + eqcls_setbits.push_back(newOrder[curIdx]); + } + if ((wrd2 >> j) & 0x01) { + setBitCnt2++; + reordered_eqcls_setbits.push_back(curIdx); + } + + } + i += bitCnt; + } + std::sort(eqcls_setbits.begin(), eqcls_setbits.end()); + std::sort(reordered_eqcls_setbits.begin(), reordered_eqcls_setbits.end()); + + if (eqcls_setbits != reordered_eqcls_setbits) { + std::cout << "ERROR! EQ CLSES in ROW: " << eqclsCntr << " NOT THE SAME.\n"; + std::exit(1); + } + if (eqclsCntr != 0 && (eqclsCntr) % 1000000 == 0) { + gettimeofday(&end, &tzp); + std::stringstream ss; + ss << eqclsCntr << " eqclses processed, "; + print_time_elapsed(ss.str(), &start, &end); + gettimeofday(&start, &tzp); + } + + } + if (setBitCnt1 != setBitCnt2) { + std::cout << "ERROR! EQ CLSES SET BITS NOT THE SAME: " << setBitCnt1 << " VS " << setBitCnt2 << "\n"; + std::exit(1); + } + std::cout << "\nValidation Passed!\n\n"; +} + +void reorder(std::string filename, + std::string out_filename, + std::vector &newOrder, + uint64_t num_samples) { + std::cout << "\n[Reorder]\n"; + struct timeval start, end; + struct timezone tzp; + + //std::cout << "\n"; + sdsl::bit_vector eqcls; + sdsl::load_from_file(eqcls, filename); + size_t totalEqClsCnt = eqcls.size() / num_samples; //222584822; + std::cout << "Total number of Eq. Clss: " << totalEqClsCnt << "\n"; + //totalEqClsCnt = 1280000; + sdsl::bit_vector reordered_eqcls(eqcls.size(), 0); + gettimeofday(&start, &tzp); + + for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { + + // transpose the matrix of eq. classes + size_t i = 0; + while (i < num_samples) { + size_t bitCnt = std::min(num_samples - i, (size_t) 64); + size_t wrd = eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); + for (size_t j = 0, curIdx = i; j < bitCnt; j++, curIdx++) { + if ((wrd >> j) & 0x01) { + //std::cout << curIdx << ":" << newOrder[curIdx] << " "; + reordered_eqcls[eqclsCntr * num_samples + newOrder[curIdx]] = 1; + } + } + i += bitCnt; + } + + //std::cout << "\n"; + //std::exit(1); + if (eqclsCntr != 0 && (eqclsCntr) % 1000000 == 0) { + gettimeofday(&end, &tzp); + std::stringstream ss; + ss << eqclsCntr << " eqclses processed, "; + print_time_elapsed(ss.str(), &start, &end); + gettimeofday(&start, &tzp); + } + } + std::cout << "\noutfilename: " << out_filename << "\n"; + for (auto idx = 0; idx < num_samples; idx++) { + std::cout << reordered_eqcls[idx]; + } + std::cout << "\n"; + for (size_t eqclsCntr = 0; eqclsCntr < 2; eqclsCntr++) { + for (size_t i = 0; i < num_samples; i++) { + std::cout << reordered_eqcls[num_samples * eqclsCntr + i]; + } + std::cout << "\n"; + } + sdsl::store_to_file(reordered_eqcls, out_filename); + +} + +void reorder(std::string filename, + std::string out_filename, + std::string order_filename, + uint64_t num_samples) { + + std::ifstream orderFile(order_filename); + std::vector newOrder; + newOrder.resize(num_samples); + uint64_t cntr = 0; + while (!orderFile.eof()) { + size_t val; + orderFile >> val; + //std::cout << val << " "; + newOrder[val] = cntr++; + //newOrder.push_back(val); + } + + reorder(filename, out_filename, newOrder, num_samples); +} + +void build_fakeEq_And_randReord(std::string outfile, + uint64_t totalEqClsCnt = 1280000, + uint64_t num_samples = 2586, + uint64_t wordSize = 8) { + std::cout << "\n[Build_fakeEq_And_randReord]\n"; + uint64_t bvSize = num_samples * totalEqClsCnt; + sdsl::bit_vector eqcls(bvSize, 0); + + for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { + size_t i = 0; + while (i < num_samples) { + uint64_t wrd = rand() % 2 ? 0 : -1; + size_t bitCnt = std::min(num_samples - i, (size_t) wordSize); + eqcls.set_int(eqclsCntr * num_samples + i, wrd, bitCnt); + //wrd = wrd?0:-1; + i += bitCnt; + } + } + + for (size_t eqclsCntr = 0; eqclsCntr < 2; eqclsCntr++) { + for (size_t i = 0; i < num_samples; i++) { + std::cout << eqcls[num_samples * eqclsCntr + i]; + } + std::cout << "\n"; + } + sdsl::store_to_file(eqcls, outfile + ".eq"); + + std::vector randomizedIndex(num_samples); + for (uint64_t i = 0; i < num_samples; i++) + randomizedIndex[i] = i; + std::random_shuffle(randomizedIndex.begin(), randomizedIndex.end()); + for (size_t i = 0; i < num_samples; i++) { + std::cout << randomizedIndex[i] << " "; + } + std::cout << "\n"; + reorder(outfile + ".eq", outfile + "_randOrder.eq", randomizedIndex, num_samples); +} + +void build_distMat(std::string filename, + std::string out_filename, + uint64_t num_samples) { + std::cout << "\n[Build_distMat]\n"; + struct timeval start, end, row2colStart, colPairCompStart; + struct timezone tzp; + + std::vector> hamDistMat(num_samples); + for (auto &v : hamDistMat) { + v.resize(num_samples); + for (auto &d : v) d = 0; + } + + std::vector> mutInfoMat(num_samples); + std::vector> jointMat(num_samples); + for (auto &v : mutInfoMat) { + v.resize(num_samples); + for (auto &d : v) d = 0; + } + for (auto &v : jointMat) { + v.resize(num_samples); + for (auto &d : v) d = 0; + } + + //BitVectorRRR eqcls(filename); + sdsl::bit_vector eqcls; + sdsl::load_from_file(eqcls, filename); + + size_t totalEqClsCnt = eqcls.bit_size() / num_samples; //222584822; + //totalEqClsCnt = 1280000; + //sdsl::bit_vector uncompressed(totalEqClsCnt*num_samples, 0); + std::cout << "Total bit size: " << eqcls.bit_size() + << "\ntotal # of equivalence classes: " << totalEqClsCnt << "\n"; + + std::vector cols(num_samples); + for (auto &bv : cols) bv = sdsl::bit_vector(totalEqClsCnt, 0); + + gettimeofday(&start, &tzp); + gettimeofday(&row2colStart, &tzp); + + for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { + + // transpose the matrix of eq. classes + size_t i = 0; + while (i < num_samples) { + size_t bitCnt = std::min(num_samples - i, (size_t) 64); + size_t wrd = eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); + //uncompressed.set_int(eqclsCntr*num_samples+i, wrd, bitCnt); + for (size_t j = 0, curIdx = i; j < bitCnt; j++, curIdx++) { + if ((wrd >> j) & 0x01) { + cols[curIdx][eqclsCntr] = 1; + } + } + i += bitCnt; + } + + if (eqclsCntr != 0 && (eqclsCntr) % 1000000 == 0) { + gettimeofday(&end, &tzp); + std::stringstream ss; + ss << eqclsCntr << " eqclses processed, "; + print_time_elapsed(ss.str(), &start, &end); + gettimeofday(&start, &tzp); + } + } + //sdsl::store_to_file(uncompressed, "test.eq"); + gettimeofday(&end, &tzp); + print_time_elapsed("DONE!, ", &row2colStart, &end); + gettimeofday(&start, &tzp); + + gettimeofday(&colPairCompStart, &tzp); + + // calc ham distance + size_t bitCnt = 64; + for (size_t k = 0; k < num_samples; k++) { + for (size_t j = k + 1; j < num_samples; j++) { + uint64_t dist = 0; + uint64_t mutprob[4]; + uint64_t denum = totalEqClsCnt; + for (int b = 0; b < 4; b++) mutprob[b] = 0; + uint64_t indivprob[2]; + for (int b = 0; b < 2; b++) indivprob[b] = 0; + //calculate distance between column i and j + size_t i = 0; + while (i < totalEqClsCnt) { + bitCnt = std::min(totalEqClsCnt - i, (size_t) 64); + uint64_t wrd1 = cols[k].get_int(i, bitCnt); + uint64_t wrd2 = cols[j].get_int(i, bitCnt); + + // compare words + // xor & pop count + dist += sdsl::bits::cnt(wrd1 ^ wrd2); + + indivprob[0] += sdsl::bits::cnt(wrd1); + indivprob[1] += sdsl::bits::cnt(wrd2); + mutprob[0] += sdsl::bits::cnt((~wrd1) & (~wrd2)) - (64 - bitCnt); + mutprob[1] += sdsl::bits::cnt((~wrd1) & wrd2); + mutprob[2] += sdsl::bits::cnt(wrd1 & (~wrd2)); + mutprob[3] += sdsl::bits::cnt(wrd1 & wrd2); + i += bitCnt; + } + hamDistMat[k][j] = dist; + hamDistMat[j][k] = dist; + + + /* mutInfoMat[k][j] = + (mutprob[0]?mutprob[0]*(log2(denum)+log2(mutprob[0])- log2(denum - indivprob[0])-log2(denum - indivprob[1])):0) + + (mutprob[1]?mutprob[1]*(log2(denum)+log2(mutprob[1])- log2(denum - indivprob[0])-log2(indivprob[1])):0) + + (mutprob[2]?mutprob[2]*(log2(denum)+log2(mutprob[2])- log2(indivprob[0])-log2(denum - indivprob[1])):0) + + (mutprob[3]?mutprob[3]*(log2(denum)+log2(mutprob[3])- log2(indivprob[0])-log2(indivprob[1])):0); + mutInfoMat[k][j] /= (double)(denum); + mutInfoMat[j][k] = mutInfoMat[k][j]; */ + + mutInfoMat[k][j] = + (mutprob[0] ? mutprob[0] * log2((double) (denum * mutprob[0]) / + (double) ((denum - indivprob[0]) * (denum - indivprob[1]))) : 0) + + (mutprob[1] ? mutprob[1] * + log2((double) (denum * mutprob[1]) / (double) ((denum - indivprob[0]) * indivprob[1])) + : 0) + + (mutprob[2] ? mutprob[2] * log2((double) (denum * mutprob[2]) / + (double) ((indivprob[0]) * (denum - indivprob[1]))) : 0) + + (mutprob[3] ? mutprob[3] * + log2((double) (denum * mutprob[3]) / (double) (indivprob[0] * (indivprob[1]))) : 0); + mutInfoMat[k][j] /= (double) (denum); + mutInfoMat[j][k] = mutInfoMat[k][j]; + + jointMat[k][j] = + (mutprob[0] ? mutprob[0] * log2((double) (mutprob[0]) / (double) denum) : 0) + + (mutprob[1] ? mutprob[1] * log2((double) (mutprob[1]) / (double) denum) : 0) + + (mutprob[2] ? mutprob[2] * log2((double) (mutprob[2]) / (double) denum) : 0) + + (mutprob[3] ? mutprob[3] * log2((double) (mutprob[3]) / (double) denum) : 0); + jointMat[k][j] /= (-1.0 * (double) (denum)); + jointMat[j][k] = jointMat[k][j]; + /* std::cout << mutInfoMat[k][j] << " : " + << mutprob[0] << " " + << mutprob[1] << " " + << mutprob[2] << " " + << mutprob[3] << " " + << indivprob[0] << " " + << indivprob[1] << " " + << denum << "\n"; */ + + //if (mutInfoMat[k][j]) std::cout << mutInfoMat[k][j] << "\n"; + } + if (k % 100 == 0) { + gettimeofday(&end, &tzp); + std::stringstream ss; + ss << k << ",";//<< j << ", "; + print_time_elapsed(ss.str(), &start, &end); + gettimeofday(&start, &tzp); + } + } + gettimeofday(&end, &tzp); + + print_time_elapsed(" DONE!, ", &start, &end); + gettimeofday(&colPairCompStart, &tzp); + + std::ofstream out(out_filename + ".ham"); + std::ofstream mutout(out_filename + ".mutinf"/* ".mutinfo" */); + //std::ofstream jout(out_filename+".joint"); + //out << hamDistMat.size() << "\n"; + double mutInfoInv = 0; + + for (size_t i = 0; i < hamDistMat.size(); i++) { + out << hamDistMat[i][0]; + mutInfoInv = (jointMat[i][0] - mutInfoMat[i][0]) < 0 ? 0 : (jointMat[i][0] - mutInfoMat[i][0]); + mutout << mutInfoInv; + for (size_t j = 1; j < hamDistMat[i].size(); j++) { + out << "\t" << hamDistMat[i][j]; + mutInfoInv = (jointMat[i][j] - mutInfoMat[i][j]) < 0 ? 0 : (jointMat[i][j] - mutInfoMat[i][j]); + mutout << "\t" << mutInfoInv; + //jout << jointMat[i][j] << "\t"; + //if (hamDistMat[i][j] != 0) { + + //std::cout << j << " "; + // out << i << "\t" << j << "\t" << hamDistMat[i][j] << "\n"; + //} + //if (mutInfoMat[i][j] != 0) { + // mutout << i << "\t" << j << "\t" << mutInfoMat[i][j] << "\t" << jointMat[i][j] << "\n"; + //} + } + out << "\n"; + mutout << "\n"; + //jout << "\n"; + } +} + +void writeEq(std::string filename, + std::string out_filename, + uint64_t num_samples, + uint64_t totalEqCls) { + sdsl::bit_vector eqcls; + sdsl::load_from_file(eqcls, filename); + std::ofstream out(out_filename + ".matrix"); + size_t totalEqClsCnt = eqcls.bit_size() / num_samples; //222584822; + if (totalEqCls > 0) { + totalEqClsCnt = totalEqCls; + } + for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { + size_t i = 0; + while (i < num_samples) { + size_t bitCnt = std::min(num_samples - i, (size_t) 64); + size_t wrd = eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); + for (size_t j = 0; j < bitCnt; j++) { + out << ((wrd >> j) & 0x01) << "\t"; + } + i += bitCnt; + } + out << "\n"; + } + out.close(); +} + +int main(int argc, char *argv[]) { + + uint64_t num_samples = 2586; + std::string command = argv[1]; + + if (command == "validate") { + std::cout << "validate for different number of set bits\n"; + for (uint16_t i = 1; i <= num_samples; i++) { + if (!validate(i, num_samples)) { + std::cerr << "ERROR: NOT VALIDATED\n"; + std::exit(1); + } + } + std::cout + << "SUCCESSFULLY VALIDATED THE COMPRESSION/DECOMPRESSION PROCESS.\n\n#####NEXT STEP#####\nCompare Sizes:\n"; + } else if (command == "decompress") { + std::string filename = argv[2]; + std::string outfilename = argv[3]; + uint64_t totalEqCls = std::stoull(argv[4]); + decompress(filename, outfilename, totalEqCls, num_samples); + } else if (command == "compareCompressions") { + std::string filename = argv[2]; + compareCompressions(filename, num_samples); + } else if (command == "compareCopies") + compareCopies(num_samples); + else if (command == "distMat") { + if (argc < 4) { + std::cerr << "ERROR: MISSING LAST ARGUMENT\n"; + std::exit(1); + } + std::string filename = argv[2]; + std::string output_filename = argv[3]; + build_distMat(filename, output_filename, num_samples); + } else if (command == "writeEq") { + if (argc < 4) { + std::cerr << "ERROR: MISSING LAST ARGUMENT\n"; + std::exit(1); + } + std::string filename = argv[2]; + std::string output_filename = argv[3]; + uint64_t totalEqCls = 0; + if (argc == 5) + totalEqCls = std::stoull(argv[4]); + + writeEq(filename, output_filename, num_samples, totalEqCls); + } else if (command == "reorder") { + if (argc < 5) { + std::cerr << "ERROR: MISSING ARGUMENT. 4 needed.\n"; + std::exit(1); + } + std::string filename = argv[2]; + std::string output_filename = argv[3]; + std::string order_filename = argv[4]; + reorder(filename, output_filename, order_filename, num_samples); + } else if (command == "compare_reordered_original") { + if (argc < 5) { + std::cerr << "ERROR: MISSING ARGUMENT. 4 needed.\n"; + std::exit(1); + } + std::string filename1 = argv[2]; + std::string filename2 = argv[3]; + std::string order_filename = argv[4]; + compare_reordered_original(filename1, filename2, order_filename, num_samples); + } else if (command == "fakeEq_randOrder") { + std::string outfile = argv[2]; + uint64_t rows = 1048576;//2^20; + build_fakeEq_And_randReord(outfile, + rows, + num_samples, + 8); + } else if (command == "validate_uniqueness") { + std::string dir = argv[2]; + validate_uniqueness(dir, num_samples); + } else { + std::cerr << "ERROR: NO COMMANDS PROVIDED\n" + << "OPTIONS ARE: validate, compareCompressions, compareCopies, reorder\n"; + std::exit(1); + } + +} + From 480e32b7937355dcd845d1c77d4a79076f242f20 Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Wed, 13 Jun 2018 12:43:52 -0400 Subject: [PATCH 012/122] required headers for walkEqcls --- .../VarIntG8IU.h | 237 +++ .../binarypacking.h | 187 +++ .../bitpacking.h | 174 +++ .../bitpackinghelpers.h | 691 +++++++++ .../boolarray.h | 181 +++ .../codecfactory.h | 185 +++ .../SIMDCompressionAndIntersection/codecs.h | 131 ++ .../SIMDCompressionAndIntersection/common.h | 60 + .../compositecodec.h | 69 + .../SIMDCompressionAndIntersection/delta.h | 86 ++ .../deltatemplates.h | 164 ++ .../SIMDCompressionAndIntersection/fastpfor.h | 367 +++++ include/SIMDCompressionAndIntersection/for.h | 285 ++++ .../SIMDCompressionAndIntersection/forcodec.h | 92 ++ .../frameofreference.h | 134 ++ .../SIMDCompressionAndIntersection/hybm2.h | 642 ++++++++ .../integratedbitpacking.h | 216 +++ .../intersection.h | 108 ++ .../SIMDCompressionAndIntersection/mersenne.h | 93 ++ .../SIMDCompressionAndIntersection/platform.h | 44 + .../simdbinarypacking.h | 403 +++++ .../simdbitpacking.h | 117 ++ .../simdbitpackinghelpers.h | 1156 ++++++++++++++ .../simdfastpfor.h | 279 ++++ .../simdintegratedbitpacking.h | 895 +++++++++++ .../simdvariablebyte.h | 493 ++++++ .../SIMDCompressionAndIntersection/skipping.h | 292 ++++ .../sortedbitpacking.h | 200 +++ .../streamvariablebyte.h | 247 +++ .../synthetic.h | 389 +++++ .../SIMDCompressionAndIntersection/timer.h | 85 ++ .../usimdbitpacking.h | 150 ++ include/SIMDCompressionAndIntersection/util.h | 126 ++ .../variablebyte.h | 1328 +++++++++++++++++ .../SIMDCompressionAndIntersection/varintgb.h | 929 ++++++++++++ include/compressedSetBit.h | 90 ++ 36 files changed, 11325 insertions(+) create mode 100644 include/SIMDCompressionAndIntersection/VarIntG8IU.h create mode 100644 include/SIMDCompressionAndIntersection/binarypacking.h create mode 100644 include/SIMDCompressionAndIntersection/bitpacking.h create mode 100644 include/SIMDCompressionAndIntersection/bitpackinghelpers.h create mode 100644 include/SIMDCompressionAndIntersection/boolarray.h create mode 100644 include/SIMDCompressionAndIntersection/codecfactory.h create mode 100644 include/SIMDCompressionAndIntersection/codecs.h create mode 100644 include/SIMDCompressionAndIntersection/common.h create mode 100644 include/SIMDCompressionAndIntersection/compositecodec.h create mode 100644 include/SIMDCompressionAndIntersection/delta.h create mode 100644 include/SIMDCompressionAndIntersection/deltatemplates.h create mode 100644 include/SIMDCompressionAndIntersection/fastpfor.h create mode 100644 include/SIMDCompressionAndIntersection/for.h create mode 100644 include/SIMDCompressionAndIntersection/forcodec.h create mode 100644 include/SIMDCompressionAndIntersection/frameofreference.h create mode 100644 include/SIMDCompressionAndIntersection/hybm2.h create mode 100644 include/SIMDCompressionAndIntersection/integratedbitpacking.h create mode 100644 include/SIMDCompressionAndIntersection/intersection.h create mode 100644 include/SIMDCompressionAndIntersection/mersenne.h create mode 100644 include/SIMDCompressionAndIntersection/platform.h create mode 100644 include/SIMDCompressionAndIntersection/simdbinarypacking.h create mode 100644 include/SIMDCompressionAndIntersection/simdbitpacking.h create mode 100644 include/SIMDCompressionAndIntersection/simdbitpackinghelpers.h create mode 100644 include/SIMDCompressionAndIntersection/simdfastpfor.h create mode 100644 include/SIMDCompressionAndIntersection/simdintegratedbitpacking.h create mode 100644 include/SIMDCompressionAndIntersection/simdvariablebyte.h create mode 100644 include/SIMDCompressionAndIntersection/skipping.h create mode 100644 include/SIMDCompressionAndIntersection/sortedbitpacking.h create mode 100644 include/SIMDCompressionAndIntersection/streamvariablebyte.h create mode 100644 include/SIMDCompressionAndIntersection/synthetic.h create mode 100644 include/SIMDCompressionAndIntersection/timer.h create mode 100644 include/SIMDCompressionAndIntersection/usimdbitpacking.h create mode 100644 include/SIMDCompressionAndIntersection/util.h create mode 100644 include/SIMDCompressionAndIntersection/variablebyte.h create mode 100644 include/SIMDCompressionAndIntersection/varintgb.h create mode 100644 include/compressedSetBit.h diff --git a/include/SIMDCompressionAndIntersection/VarIntG8IU.h b/include/SIMDCompressionAndIntersection/VarIntG8IU.h new file mode 100644 index 0000000..b187f9a --- /dev/null +++ b/include/SIMDCompressionAndIntersection/VarIntG8IU.h @@ -0,0 +1,237 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + */ +#ifndef __SSSE3__ +#pragma message \ + "Disabling varintg8iu due to lack of SSSE3 support, try adding -mssse3 or the equivalent on your compiler" +#else +#ifndef VARINTG8IU_H__ +#define VARINTG8IU_H__ +#include +#include "codecs.h" +#include "delta.h" +#ifdef __GNUC__ +#define PREDICT_FALSE(x) (__builtin_expect(x, 0)) +#else +#define PREDICT_FALSE(x) x +#endif + +namespace SIMDCompressionLib { +/** + * + * Implementation of varint-G8IU taken from + * Stepanov et al., SIMD-Based Decoding of Posting Lists, CIKM 2011 + * + * Update: D. Lemire believes that this scheme was patented by Rose, Stepanov et + * al. (patent 20120221539). + * We wrote this code before the patent was published (August 2012). + * + * By Maxime Caron and Daniel Lemire + * + * This code was originally written by M. Caron and then + * optimized by D. Lemire. + * + * + * + */ + +template class VarIntG8IU : public IntegerCODEC { + +public: + // For all possible values of the + // descriptor we build a table of any shuffle sequence + // that might be needed at decode time. + VarIntG8IU() { + char mask[256][32]; + for (int desc = 0; desc <= 255; desc++) { + memset(mask[desc], -1, 32); + int bitmask = 0x00000001; + int bitindex = 0; + // count number of 0 in the char + int complete = 0; + int ithSize[8]; + int lastpos = -1; + while (bitindex < 8) { + if ((desc & bitmask) == 0) { + ithSize[complete] = bitindex - lastpos; + lastpos = bitindex; + complete++; + } + bitindex++; + bitmask = bitmask << 1; + } + maskOutputSize[desc] = complete; + + int j = 0; + int k = 0; + for (int i = 0; i < complete; i++) { + for (int n = 0; n < 4; n++) { + if (n < ithSize[i]) { + mask[desc][k] = static_cast(j); + j = j + 1; + } else { + mask[desc][k] = -1; + } + k = k + 1; + } + } + } + for (int desc = 0; desc <= 255; desc++) { + vecmask[desc][0] = + _mm_lddqu_si128(reinterpret_cast<__m128i const *>(mask[desc])); + vecmask[desc][1] = + _mm_lddqu_si128(reinterpret_cast<__m128i const *>(mask[desc] + 16)); + } + } + + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + uint32_t prev = 0; // for deltas + const uint32_t *src = in; + size_t srclength = length * 4; // number of input bytes + + unsigned char *dst = reinterpret_cast(out); + nvalue = nvalue * 4; // output bytes + + size_t compressed_size = 0; + while (srclength > 0 && nvalue >= 9) { + compressed_size += encodeBlock(src, srclength, dst, nvalue, prev); + } + // Ouput might not be a multiple of 4 so we make it so + nvalue = ((compressed_size + 3) / 4); + } + const uint32_t *decodeArray(const uint32_t *in, const size_t length, + uint32_t *out, size_t &nvalue) { + __m128i mprev = _mm_setzero_si128(); // for deltas + const unsigned char *src = reinterpret_cast(in); + const uint32_t *const initdst = out; + + uint32_t *dst = out; + size_t srclength = length * 4; + for (; srclength >= 22; srclength -= 8, src += 8) { + unsigned char desc = *src; + src += 1; + srclength -= 1; + const __m128i data = + _mm_lddqu_si128(reinterpret_cast<__m128i const *>(src)); + const __m128i result = _mm_shuffle_epi8(data, vecmask[desc][0]); + if (delta) { + mprev = RegularDeltaSIMD::PrefixSum(result, mprev); + _mm_storeu_si128(reinterpret_cast<__m128i *>(dst), mprev); + } else { + _mm_storeu_si128(reinterpret_cast<__m128i *>(dst), result); + } + int readSize = maskOutputSize[desc]; + if (readSize > 4) { + const __m128i result2 = _mm_shuffle_epi8(data, vecmask[desc][1]); + if (delta) { + mprev = RegularDeltaSIMD::PrefixSum(result2, mprev); + _mm_storeu_si128(reinterpret_cast<__m128i *>(dst + 4), mprev); + } else { + _mm_storeu_si128(reinterpret_cast<__m128i *>(dst + 4), result2); + } + } + dst += readSize; + } + while (srclength >= 9) { + unsigned char desc = *src; + src += 1; + srclength -= 1; + char buff[32]; + memcpy(buff, src, 8); + const __m128i data = + _mm_lddqu_si128(reinterpret_cast<__m128i const *>(buff)); + const __m128i result = _mm_shuffle_epi8(data, vecmask[desc][0]); + if (delta) { + mprev = RegularDeltaSIMD::PrefixSum(result, mprev); + _mm_storeu_si128(reinterpret_cast<__m128i *>(buff), mprev); + } else { + _mm_storeu_si128(reinterpret_cast<__m128i *>(buff), result); + } + int readSize = maskOutputSize[desc]; + if (readSize > 4) { + const __m128i result2 = _mm_shuffle_epi8(data, vecmask[desc][1]); + if (delta) { + mprev = RegularDeltaSIMD::PrefixSum(result2, mprev); + _mm_storeu_si128(reinterpret_cast<__m128i *>(buff + 16), mprev); + } else { + _mm_storeu_si128(reinterpret_cast<__m128i *>(buff + 16), result2); + } + } + memcpy(dst, buff, 4 * readSize); + dst += readSize; + srclength -= 8; + src += 8; + } + + nvalue = (dst - initdst); + return reinterpret_cast((reinterpret_cast(src) + 3) & + ~3); + } + + virtual std::string name() const { + return std::string("VarIntG8IU") + + ((delta == 1) ? "scalardelta" : ((delta == 2) ? "delta" : "")); + } + + int encodeBlock(const uint32_t *&src, size_t &srclength, unsigned char *&dest, + size_t &dstlength, uint32_t &prev) { + unsigned char desc = 0xFF; + unsigned char bitmask = 0x01; + uint32_t buffer[8]; + int ithSize[8]; + int length = 0; + int numInt = 0; + + while (srclength > 0) { + const uint32_t *temp = src; + + int byteNeeded = + delta ? getNumByteNeeded(*temp - prev) : getNumByteNeeded(*temp); + + if (PREDICT_FALSE(length + byteNeeded > 8)) { + break; + } + + // flip the correct bit in desc + bitmask = static_cast(bitmask << (byteNeeded - 1)); + desc = desc ^ bitmask; + bitmask = static_cast(bitmask << 1); + + ithSize[numInt] = byteNeeded; + length += byteNeeded; + buffer[numInt] = delta ? *temp - prev : *temp; + if (delta) + prev = *temp; + src = src + 1; + srclength -= 4; + numInt++; + } + + dest[0] = desc; + int written = 1; + for (int i = 0; i < numInt; i++) { + int size = ithSize[i]; + uint32_t value = buffer[i]; + for (int j = 0; j < size; j++) { + dest[written] = static_cast(value >> (j * 8)); + written++; + } + } + dest += 9; + dstlength -= 9; + return 9; + } + +private: + int maskOutputSize[256]; + __m128i vecmask[256][2]; + + int getNumByteNeeded(const uint32_t val) { + return ((__builtin_clz(val | 255) ^ 31) >> 3) + 1; + } +}; +} +#endif // VARINTG8IU_H__ +#endif //__SSE3__ diff --git a/include/SIMDCompressionAndIntersection/binarypacking.h b/include/SIMDCompressionAndIntersection/binarypacking.h new file mode 100644 index 0000000..43955fb --- /dev/null +++ b/include/SIMDCompressionAndIntersection/binarypacking.h @@ -0,0 +1,187 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire, http://lemire.me/en/ + */ + +#ifndef SIMDCompressionAndIntersection_BINARYPACKING_H_ +#define SIMDCompressionAndIntersection_BINARYPACKING_H_ + +#include "codecs.h" +#include "bitpackinghelpers.h" +#include "util.h" + +namespace SIMDCompressionLib { + +struct BasicBlockPacker { + static void inline unpackblock(const uint32_t *in, uint32_t *out, + const uint32_t bit, uint32_t &initoffset) { + BitPackingHelpers::fastunpack(in, out, bit); + if (bit < 32) + inverseDelta(initoffset, out); + initoffset = *(out + BitPackingHelpers::BlockSize - 1); + } + + static uint32_t maxbits(const uint32_t *in, uint32_t &initoffset) { + uint32_t accumulator = in[0] - initoffset; + for (uint32_t k = 1; k < BitPackingHelpers::BlockSize; ++k) { + accumulator |= in[k] - in[k - 1]; + } + initoffset = in[BitPackingHelpers::BlockSize - 1]; + return gccbits(accumulator); + } + + static void inline packblockwithoutmask(uint32_t *in, uint32_t *out, + const uint32_t bit, + uint32_t &initoffset) { + const uint32_t nextoffset = *(in + BitPackingHelpers::BlockSize - 1); + if (bit < 32) + delta(initoffset, in); + BitPackingHelpers::fastpackwithoutmask(in, out, bit); + initoffset = nextoffset; + } + static string name() { return "BasicBlockPacker"; } +}; + +struct NoDeltaBlockPacker { + static void inline unpackblock(const uint32_t *in, uint32_t *out, + const uint32_t bit, uint32_t &) { + BitPackingHelpers::fastunpack(in, out, bit); + } + static void inline packblockwithoutmask(uint32_t *in, uint32_t *out, + const uint32_t bit, uint32_t &) { + BitPackingHelpers::fastpackwithoutmask(in, out, bit); + } + + static uint32_t maxbits(const uint32_t *in, uint32_t &) { + uint32_t accumulator = 0; + for (uint32_t k = 0; k < BitPackingHelpers::BlockSize; ++k) { + accumulator |= in[k]; + } + return gccbits(accumulator); + } + + static string name() { return "NoDeltaBlockPacker"; } +}; + +struct IntegratedBlockPacker { + PURE_FUNCTION + static uint32_t maxbits(const uint32_t *in, uint32_t &initoffset) { + uint32_t accumulator = in[0] - initoffset; + for (uint32_t k = 1; k < BitPackingHelpers::BlockSize; ++k) { + accumulator |= in[k] - in[k - 1]; + } + initoffset = in[BitPackingHelpers::BlockSize - 1]; + return gccbits(accumulator); + } + + static void inline packblockwithoutmask(const uint32_t *in, uint32_t *out, + const uint32_t bit, + uint32_t &initoffset) { + BitPackingHelpers::integratedfastpackwithoutmask(initoffset, in, out, bit); + initoffset = *(in + BitPackingHelpers::BlockSize - 1); + } + static void inline unpackblock(const uint32_t *in, uint32_t *out, + const uint32_t bit, uint32_t &initoffset) { + BitPackingHelpers::integratedfastunpack(initoffset, in, out, bit); + initoffset = *(out + BitPackingHelpers::BlockSize - 1); + } + static string name() { return "IntegratedBlockPacker"; } +}; + +template class BinaryPacking : public IntegerCODEC { +public: + static const uint32_t MiniBlockSize = 32; + static const uint32_t HowManyMiniBlocks = 4; + static const uint32_t BlockSize = + MiniBlockSize; // HowManyMiniBlocks * MiniBlockSize; + static const uint32_t bits32 = 8; + + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + checkifdivisibleby(length, BlockSize); + const uint32_t *const initout(out); + *out++ = static_cast(length); + uint32_t Bs[HowManyMiniBlocks]; + uint32_t init = 0; + const uint32_t *const final = in + length; + for (; in + HowManyMiniBlocks * MiniBlockSize <= final; + in += HowManyMiniBlocks * MiniBlockSize) { + uint32_t tmpinit = init; + for (uint32_t i = 0; i < HowManyMiniBlocks; ++i) { + Bs[i] = BlockPacker::maxbits(in + i * MiniBlockSize, tmpinit); + } + *out++ = (Bs[0] << 24) | (Bs[1] << 16) | (Bs[2] << 8) | Bs[3]; + for (uint32_t i = 0; i < HowManyMiniBlocks; ++i) { + BlockPacker::packblockwithoutmask(in + i * MiniBlockSize, out, Bs[i], + init); + out += Bs[i]; + } + } + if (in < final) { + size_t howmany = (final - in) / MiniBlockSize; + uint32_t tmpinit = init; + memset(&Bs[0], 0, HowManyMiniBlocks * sizeof(uint32_t)); + for (uint32_t i = 0; i < howmany; ++i) { + Bs[i] = BlockPacker::maxbits(in + i * MiniBlockSize, tmpinit); + } + *out++ = (Bs[0] << 24) | (Bs[1] << 16) | (Bs[2] << 8) | Bs[3]; + for (uint32_t i = 0; i < howmany; ++i) { + BlockPacker::packblockwithoutmask(in + i * MiniBlockSize, out, Bs[i], + init); + out += Bs[i]; + } + } + nvalue = out - initout; + } + + const uint32_t *decodeArray(const uint32_t *in, const size_t /*length*/, + uint32_t *out, size_t &nvalue) { + const uint32_t actuallength = *in++; + checkifdivisibleby(actuallength, BlockSize); + const uint32_t *const initout(out); + uint32_t Bs[HowManyMiniBlocks]; + uint32_t init = 0; + for (; out < initout + + actuallength / (HowManyMiniBlocks * MiniBlockSize) * + HowManyMiniBlocks * MiniBlockSize; + out += HowManyMiniBlocks * MiniBlockSize) { + Bs[0] = static_cast(in[0] >> 24); + Bs[1] = static_cast(in[0] >> 16); + Bs[2] = static_cast(in[0] >> 8); + Bs[3] = static_cast(in[0]); + ++in; + for (uint32_t i = 0; i < HowManyMiniBlocks; ++i) { + BlockPacker::unpackblock(in, out + i * MiniBlockSize, Bs[i], init); + in += Bs[i]; + } + } + if (out < initout + actuallength) { + size_t howmany = (initout + actuallength - out) / MiniBlockSize; + Bs[0] = static_cast(in[0] >> 24); + Bs[1] = static_cast(in[0] >> 16); + Bs[2] = static_cast(in[0] >> 8); + Bs[3] = static_cast(in[0]); + ++in; + for (uint32_t i = 0; i < howmany; ++i) { + BlockPacker::unpackblock(in, out + i * MiniBlockSize, Bs[i], init); + in += Bs[i]; + } + out += howmany * MiniBlockSize; + } + nvalue = out - initout; + return in; + } + + string name() const { + ostringstream convert; + convert << "BinaryPacking" + << "With" << BlockPacker::name() << MiniBlockSize; + return convert.str(); + } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_BINARYPACKING_H_ */ diff --git a/include/SIMDCompressionAndIntersection/bitpacking.h b/include/SIMDCompressionAndIntersection/bitpacking.h new file mode 100644 index 0000000..6f67e96 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/bitpacking.h @@ -0,0 +1,174 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire, http://lemire.me/en/ + */ +#ifndef SIMDCompressionAndIntersection_BITPACKING +#define SIMDCompressionAndIntersection_BITPACKING +#include +#include "platform.h" + +namespace SIMDCompressionLib { + +void __fastunpack0(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastunpack1(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastunpack2(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastunpack3(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastunpack4(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastunpack5(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastunpack6(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastunpack7(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastunpack8(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastunpack9(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastunpack10(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack11(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack12(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack13(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack14(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack15(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack16(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack17(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack18(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack19(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack20(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack21(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack22(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack23(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack24(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack25(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack26(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack27(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack28(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack29(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack30(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack31(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastunpack32(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); + +void __fastpack0(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack1(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack2(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack3(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack4(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack5(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack6(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack7(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack8(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack9(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack10(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack11(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack12(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack13(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack14(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack15(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack16(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack17(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack18(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack19(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack20(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack21(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack22(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack23(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack24(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack25(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack26(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack27(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack28(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack29(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack30(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack31(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); +void __fastpack32(const uint32_t *__restrict__ in, uint32_t *__restrict__ out); + +void __fastpackwithoutmask0(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask1(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask2(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask3(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask4(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask5(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask6(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask7(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask8(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask9(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask10(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask11(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask12(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask13(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask14(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask15(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask16(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask17(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask18(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask19(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask20(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask21(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask22(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask23(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask24(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask25(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask26(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask27(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask28(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask29(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask30(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask31(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __fastpackwithoutmask32(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); + +} // namespace SIMDCompressionLib + +#endif // SIMDCompressionAndIntersection_BITPACKING diff --git a/include/SIMDCompressionAndIntersection/bitpackinghelpers.h b/include/SIMDCompressionAndIntersection/bitpackinghelpers.h new file mode 100644 index 0000000..a8ef3e8 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/bitpackinghelpers.h @@ -0,0 +1,691 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Leonid Boytsov, Nathan Kurz and Daniel Lemire + */ + +#ifndef SIMDCompressionAndIntersection_BITPACKINGHELPERS_H_ +#define SIMDCompressionAndIntersection_BITPACKINGHELPERS_H_ + +#include "bitpacking.h" +#include "integratedbitpacking.h" +#include "delta.h" +#include "util.h" + +namespace SIMDCompressionLib { + +struct BitPackingHelpers { + const static unsigned BlockSize = 32; + + static void inline fastunpack(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out, + const uint32_t bit) { + // Could have used function pointers instead of switch. + // Switch calls do offer the compiler more opportunities for optimization in + // theory. In this case, it makes no difference with a good compiler. + switch (bit) { + case 0: + __fastunpack0(in, out); + break; + case 1: + __fastunpack1(in, out); + break; + case 2: + __fastunpack2(in, out); + break; + case 3: + __fastunpack3(in, out); + break; + case 4: + __fastunpack4(in, out); + break; + case 5: + __fastunpack5(in, out); + break; + case 6: + __fastunpack6(in, out); + break; + case 7: + __fastunpack7(in, out); + break; + case 8: + __fastunpack8(in, out); + break; + case 9: + __fastunpack9(in, out); + break; + case 10: + __fastunpack10(in, out); + break; + case 11: + __fastunpack11(in, out); + break; + case 12: + __fastunpack12(in, out); + break; + case 13: + __fastunpack13(in, out); + break; + case 14: + __fastunpack14(in, out); + break; + case 15: + __fastunpack15(in, out); + break; + case 16: + __fastunpack16(in, out); + break; + case 17: + __fastunpack17(in, out); + break; + case 18: + __fastunpack18(in, out); + break; + case 19: + __fastunpack19(in, out); + break; + case 20: + __fastunpack20(in, out); + break; + case 21: + __fastunpack21(in, out); + break; + case 22: + __fastunpack22(in, out); + break; + case 23: + __fastunpack23(in, out); + break; + case 24: + __fastunpack24(in, out); + break; + case 25: + __fastunpack25(in, out); + break; + case 26: + __fastunpack26(in, out); + break; + case 27: + __fastunpack27(in, out); + break; + case 28: + __fastunpack28(in, out); + break; + case 29: + __fastunpack29(in, out); + break; + case 30: + __fastunpack30(in, out); + break; + case 31: + __fastunpack31(in, out); + break; + case 32: + __fastunpack32(in, out); + break; + default: + break; + } + } + + static void inline fastpack(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out, const uint32_t bit) { + // Could have used function pointers instead of switch. + // Switch calls do offer the compiler more opportunities for optimization in + // theory. In this case, it makes no difference with a good compiler. + switch (bit) { + case 0: + __fastpack0(in, out); + break; + case 1: + __fastpack1(in, out); + break; + case 2: + __fastpack2(in, out); + break; + case 3: + __fastpack3(in, out); + break; + case 4: + __fastpack4(in, out); + break; + case 5: + __fastpack5(in, out); + break; + case 6: + __fastpack6(in, out); + break; + case 7: + __fastpack7(in, out); + break; + case 8: + __fastpack8(in, out); + break; + case 9: + __fastpack9(in, out); + break; + case 10: + __fastpack10(in, out); + break; + case 11: + __fastpack11(in, out); + break; + case 12: + __fastpack12(in, out); + break; + case 13: + __fastpack13(in, out); + break; + case 14: + __fastpack14(in, out); + break; + case 15: + __fastpack15(in, out); + break; + case 16: + __fastpack16(in, out); + break; + case 17: + __fastpack17(in, out); + break; + case 18: + __fastpack18(in, out); + break; + case 19: + __fastpack19(in, out); + break; + case 20: + __fastpack20(in, out); + break; + case 21: + __fastpack21(in, out); + break; + case 22: + __fastpack22(in, out); + break; + case 23: + __fastpack23(in, out); + break; + case 24: + __fastpack24(in, out); + break; + case 25: + __fastpack25(in, out); + break; + case 26: + __fastpack26(in, out); + break; + case 27: + __fastpack27(in, out); + break; + case 28: + __fastpack28(in, out); + break; + case 29: + __fastpack29(in, out); + break; + case 30: + __fastpack30(in, out); + break; + case 31: + __fastpack31(in, out); + break; + case 32: + __fastpack32(in, out); + break; + default: + break; + } + } + + /*assumes that integers fit in the prescribed number of bits*/ + static void inline fastpackwithoutmask(const uint32_t *__restrict__ in, + uint32_t *__restrict__ out, + const uint32_t bit) { + // Could have used function pointers instead of switch. + // Switch calls do offer the compiler more opportunities for optimization in + // theory. In this case, it makes no difference with a good compiler. + switch (bit) { + case 0: + __fastpackwithoutmask0(in, out); + break; + case 1: + __fastpackwithoutmask1(in, out); + break; + case 2: + __fastpackwithoutmask2(in, out); + break; + case 3: + __fastpackwithoutmask3(in, out); + break; + case 4: + __fastpackwithoutmask4(in, out); + break; + case 5: + __fastpackwithoutmask5(in, out); + break; + case 6: + __fastpackwithoutmask6(in, out); + break; + case 7: + __fastpackwithoutmask7(in, out); + break; + case 8: + __fastpackwithoutmask8(in, out); + break; + case 9: + __fastpackwithoutmask9(in, out); + break; + case 10: + __fastpackwithoutmask10(in, out); + break; + case 11: + __fastpackwithoutmask11(in, out); + break; + case 12: + __fastpackwithoutmask12(in, out); + break; + case 13: + __fastpackwithoutmask13(in, out); + break; + case 14: + __fastpackwithoutmask14(in, out); + break; + case 15: + __fastpackwithoutmask15(in, out); + break; + case 16: + __fastpackwithoutmask16(in, out); + break; + case 17: + __fastpackwithoutmask17(in, out); + break; + case 18: + __fastpackwithoutmask18(in, out); + break; + case 19: + __fastpackwithoutmask19(in, out); + break; + case 20: + __fastpackwithoutmask20(in, out); + break; + case 21: + __fastpackwithoutmask21(in, out); + break; + case 22: + __fastpackwithoutmask22(in, out); + break; + case 23: + __fastpackwithoutmask23(in, out); + break; + case 24: + __fastpackwithoutmask24(in, out); + break; + case 25: + __fastpackwithoutmask25(in, out); + break; + case 26: + __fastpackwithoutmask26(in, out); + break; + case 27: + __fastpackwithoutmask27(in, out); + break; + case 28: + __fastpackwithoutmask28(in, out); + break; + case 29: + __fastpackwithoutmask29(in, out); + break; + case 30: + __fastpackwithoutmask30(in, out); + break; + case 31: + __fastpackwithoutmask31(in, out); + break; + case 32: + __fastpackwithoutmask32(in, out); + break; + default: + break; + } + } + + static void inline integratedfastunpack(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out, + const uint32_t bit) { + // Could have used function pointers instead of switch. + // Switch calls do offer the compiler more opportunities for optimization in + // theory. In this case, it makes no difference with a good compiler. + switch (bit) { + case 0: + __integratedfastunpack0(initoffset, in, out); + break; + case 1: + __integratedfastunpack1(initoffset, in, out); + break; + case 2: + __integratedfastunpack2(initoffset, in, out); + break; + case 3: + __integratedfastunpack3(initoffset, in, out); + break; + case 4: + __integratedfastunpack4(initoffset, in, out); + break; + case 5: + __integratedfastunpack5(initoffset, in, out); + break; + case 6: + __integratedfastunpack6(initoffset, in, out); + break; + case 7: + __integratedfastunpack7(initoffset, in, out); + break; + case 8: + __integratedfastunpack8(initoffset, in, out); + break; + case 9: + __integratedfastunpack9(initoffset, in, out); + break; + case 10: + __integratedfastunpack10(initoffset, in, out); + break; + case 11: + __integratedfastunpack11(initoffset, in, out); + break; + case 12: + __integratedfastunpack12(initoffset, in, out); + break; + case 13: + __integratedfastunpack13(initoffset, in, out); + break; + case 14: + __integratedfastunpack14(initoffset, in, out); + break; + case 15: + __integratedfastunpack15(initoffset, in, out); + break; + case 16: + __integratedfastunpack16(initoffset, in, out); + break; + case 17: + __integratedfastunpack17(initoffset, in, out); + break; + case 18: + __integratedfastunpack18(initoffset, in, out); + break; + case 19: + __integratedfastunpack19(initoffset, in, out); + break; + case 20: + __integratedfastunpack20(initoffset, in, out); + break; + case 21: + __integratedfastunpack21(initoffset, in, out); + break; + case 22: + __integratedfastunpack22(initoffset, in, out); + break; + case 23: + __integratedfastunpack23(initoffset, in, out); + break; + case 24: + __integratedfastunpack24(initoffset, in, out); + break; + case 25: + __integratedfastunpack25(initoffset, in, out); + break; + case 26: + __integratedfastunpack26(initoffset, in, out); + break; + case 27: + __integratedfastunpack27(initoffset, in, out); + break; + case 28: + __integratedfastunpack28(initoffset, in, out); + break; + case 29: + __integratedfastunpack29(initoffset, in, out); + break; + case 30: + __integratedfastunpack30(initoffset, in, out); + break; + case 31: + __integratedfastunpack31(initoffset, in, out); + break; + case 32: + __integratedfastunpack32(initoffset, in, out); + break; + default: + break; + } + } + + /*assumes that integers fit in the prescribed number of bits*/ + static void inline integratedfastpackwithoutmask( + const uint32_t initoffset, const uint32_t *__restrict__ in, + uint32_t *__restrict__ out, const uint32_t bit) { + // Could have used function pointers instead of switch. + // Switch calls do offer the compiler more opportunities for optimization in + // theory. In this case, it makes no difference with a good compiler. + switch (bit) { + case 0: + __integratedfastpack0(initoffset, in, out); + break; + case 1: + __integratedfastpack1(initoffset, in, out); + break; + case 2: + __integratedfastpack2(initoffset, in, out); + break; + case 3: + __integratedfastpack3(initoffset, in, out); + break; + case 4: + __integratedfastpack4(initoffset, in, out); + break; + case 5: + __integratedfastpack5(initoffset, in, out); + break; + case 6: + __integratedfastpack6(initoffset, in, out); + break; + case 7: + __integratedfastpack7(initoffset, in, out); + break; + case 8: + __integratedfastpack8(initoffset, in, out); + break; + case 9: + __integratedfastpack9(initoffset, in, out); + break; + case 10: + __integratedfastpack10(initoffset, in, out); + break; + case 11: + __integratedfastpack11(initoffset, in, out); + break; + case 12: + __integratedfastpack12(initoffset, in, out); + break; + case 13: + __integratedfastpack13(initoffset, in, out); + break; + case 14: + __integratedfastpack14(initoffset, in, out); + break; + case 15: + __integratedfastpack15(initoffset, in, out); + break; + case 16: + __integratedfastpack16(initoffset, in, out); + break; + case 17: + __integratedfastpack17(initoffset, in, out); + break; + case 18: + __integratedfastpack18(initoffset, in, out); + break; + case 19: + __integratedfastpack19(initoffset, in, out); + break; + case 20: + __integratedfastpack20(initoffset, in, out); + break; + case 21: + __integratedfastpack21(initoffset, in, out); + break; + case 22: + __integratedfastpack22(initoffset, in, out); + break; + case 23: + __integratedfastpack23(initoffset, in, out); + break; + case 24: + __integratedfastpack24(initoffset, in, out); + break; + case 25: + __integratedfastpack25(initoffset, in, out); + break; + case 26: + __integratedfastpack26(initoffset, in, out); + break; + case 27: + __integratedfastpack27(initoffset, in, out); + break; + case 28: + __integratedfastpack28(initoffset, in, out); + break; + case 29: + __integratedfastpack29(initoffset, in, out); + break; + case 30: + __integratedfastpack30(initoffset, in, out); + break; + case 31: + __integratedfastpack31(initoffset, in, out); + break; + case 32: + __integratedfastpack32(initoffset, in, out); + break; + default: + break; + } + } + + static void inline ipackwithoutmask(const uint32_t *in, const size_t Qty, + uint32_t *out, const uint32_t bit) { + if (Qty % BlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + uint32_t initoffset = 0; + + for (size_t k = 0; k < Qty / BlockSize; ++k) { + integratedfastpackwithoutmask(initoffset, in + k * BlockSize, + out + k * bit, bit); + initoffset = *(in + k * BlockSize + BlockSize - 1); + } + } + + static void inline pack(uint32_t *in, const size_t Qty, uint32_t *out, + const uint32_t bit) { + if (Qty % BlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + uint32_t initoffset = 0; + + for (size_t k = 0; k < Qty / BlockSize; ++k) { + const uint32_t nextoffset = *(in + k * BlockSize + BlockSize - 1); + if (bit < 32) + delta(initoffset, in + k * BlockSize); + fastpack(in + k * BlockSize, out + k * bit, bit); + initoffset = nextoffset; + } + } + + static void inline packWithoutDelta(uint32_t *in, const size_t Qty, + uint32_t *out, const uint32_t bit) { + for (size_t k = 0; k < Qty / BlockSize; ++k) { + fastpack(in + k * BlockSize, out + k * bit, bit); + } + } + + static void inline unpack(const uint32_t *in, const size_t Qty, uint32_t *out, + const uint32_t bit) { + if (Qty % BlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + uint32_t initoffset = 0; + + for (size_t k = 0; k < Qty / BlockSize; ++k) { + fastunpack(in + k * bit, out + k * BlockSize, bit); + if (bit < 32) + inverseDelta(initoffset, out + k * BlockSize); + initoffset = *(out + k * BlockSize + BlockSize - 1); + } + } + + static void inline unpackWithoutDelta(const uint32_t *in, const size_t Qty, + uint32_t *out, const uint32_t bit) { + for (size_t k = 0; k < Qty / BlockSize; ++k) { + fastunpack(in + k * bit, out + k * BlockSize, bit); + } + } + + static void inline packwithoutmask(uint32_t *in, const size_t Qty, + uint32_t *out, const uint32_t bit) { + if (Qty % BlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + uint32_t initoffset = 0; + + for (size_t k = 0; k < Qty / BlockSize; ++k) { + const uint32_t nextoffset = *(in + k * BlockSize + BlockSize - 1); + if (bit < 32) + delta(initoffset, in + k * BlockSize); + fastpackwithoutmask(in + k * BlockSize, out + k * bit, bit); + initoffset = nextoffset; + } + } + + static void inline packwithoutmaskWithoutDelta(uint32_t *in, const size_t Qty, + uint32_t *out, + const uint32_t bit) { + for (size_t k = 0; k < Qty / BlockSize; ++k) { + fastpackwithoutmask(in + k * BlockSize, out + k * bit, bit); + } + } + + static void inline iunpack(const uint32_t *in, const size_t Qty, + uint32_t *out, const uint32_t bit) { + if (Qty % BlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + + uint32_t initoffset = 0; + for (size_t k = 0; k < Qty / BlockSize; ++k) { + integratedfastunpack(initoffset, in + k * bit, out + k * BlockSize, bit); + initoffset = *(out + k * BlockSize + BlockSize - 1); + } + } + + /*static void GenRandom(std::vector& data, int b) { + data[0] = random(b); + + for(size_t i = 1 ; i < data.size() ; ++i ) + data[i] = random(b) + data[i-1]; + }*/ + + static void CheckMaxDiff(const std::vector &refdata, unsigned bit) { + for (size_t i = 1; i < refdata.size(); ++i) { + if (gccbits(refdata[i] - refdata[i - 1]) > bit) + throw std::runtime_error("bug"); + } + } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_BITPACKINGHELPERS_H_ */ diff --git a/include/SIMDCompressionAndIntersection/boolarray.h b/include/SIMDCompressionAndIntersection/boolarray.h new file mode 100644 index 0000000..6b0f9d0 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/boolarray.h @@ -0,0 +1,181 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + */ + +#ifndef SIMDCompressionAndIntersection_BOOLARRAY_H_ +#define SIMDCompressionAndIntersection_BOOLARRAY_H_ + +#include "common.h" + +namespace SIMDCompressionLib { + +using namespace std; + +static inline int numberOfTrailingZeros(uint64_t x) { + if (x == 0) + return 64; + return __builtin_ctzl(x); +} + +class BoolArray { +public: + vector buffer; + size_t sizeinbits; + BoolArray(const size_t n, const uint64_t initval = 0) + : buffer(n / 64 + (n % 64 == 0 ? 0 : 1), initval), sizeinbits(n) {} + + BoolArray() : buffer(), sizeinbits(0) {} + + BoolArray(const BoolArray &ba) + : buffer(ba.buffer), sizeinbits(ba.sizeinbits) {} + + void inplaceIntersect(const BoolArray &other) { + assert(other.buffer.size() == buffer.size()); + for (size_t i = 0; i < buffer.size(); ++i) + buffer[i] &= other.buffer[i]; + } + + // this is no faster because the compiler will vectorize + // inplaceIntersect automagically? + void SIMDinplaceIntersect(const BoolArray &other) { + assert(other.buffer.size() == buffer.size()); + __m128i *bin = reinterpret_cast<__m128i *>(buffer.data()); + const __m128i *bo = reinterpret_cast(other.buffer.data()); + for (size_t i = 0; i < buffer.size() / 2; ++i) { + __m128i p1 = MM_LOAD_SI_128(bin + i); + __m128i p2 = MM_LOAD_SI_128(bo + i); + __m128i andp1p2 = _mm_and_si128(p1, p2); + _mm_storeu_si128(bin + i, andp1p2); + } + for (size_t i = buffer.size() / 2 * 2; i < buffer.size(); ++i) + buffer[i] &= other.buffer[i]; + } + + void intersect(const BoolArray &other, BoolArray &output) { + assert(other.buffer.size() == buffer.size()); + output.buffer.resize(buffer.size()); + for (size_t i = 0; i < buffer.size(); ++i) + output.buffer[i] = buffer[i] & other.buffer[i]; + } + + // this is no faster because the compiler will vectorize + // intersect automagically? + void SIMDintersect(const BoolArray &other, BoolArray &output) { + assert(other.buffer.size() == buffer.size()); + output.buffer.resize(buffer.size()); + const __m128i *bin = reinterpret_cast(buffer.data()); + const __m128i *bo = reinterpret_cast(other.buffer.data()); + __m128i *bout = reinterpret_cast<__m128i *>(output.buffer.data()); + + for (size_t i = 0; i < buffer.size() / 2; ++i) { + __m128i p1 = MM_LOAD_SI_128(bin + i); + __m128i p2 = MM_LOAD_SI_128(bo + i); + __m128i andp1p2 = _mm_and_si128(p1, p2); + _mm_storeu_si128(bout + i, andp1p2); + } + for (size_t i = buffer.size() / 2 * 2; i < buffer.size(); ++i) + output.buffer[i] = buffer[i] & other.buffer[i]; + } + + void setSizeInBits(const size_t sizeib) { sizeinbits = sizeib; } + + /** + * Write out this bitmap to a vector as a list of integers corresponding + * to set bits. The caller should have allocated enough memory. + */ + void toArray(vector &ans) { + uint32_t pos = 0; + for (uint32_t k = 0; k < buffer.size(); ++k) { + uint64_t myword = buffer[k]; + while (myword != 0) { + int ntz = __builtin_ctzl(myword); + ans[pos++] = k * 64 + ntz; + myword ^= (1ll << ntz); + } + } + ans.resize(pos); + } + + /** + * This is a version of toArray where we write to a pointer. + * Returns the number of written ints. + */ + size_t toInts(uint32_t *out) { + size_t pos = 0; + for (uint32_t k = 0; k < buffer.size(); ++k) { + const uint64_t myword = buffer[k]; + for (int offset = 0; offset < 64; ++offset) { + if ((myword >> offset) == 0) + break; + offset += numberOfTrailingZeros((myword >> offset)); + out[pos++] = 64 * k + offset; + } + } + return pos; + } + BoolArray &operator=(const BoolArray &x) { + this->buffer = x.buffer; + this->sizeinbits = x.sizeinbits; + return *this; + } + + /** + * set to true (whether it was already set to true or not) + * + * This is an expensive (random access) API, you really ought to + * prepare a new word and then append it. + */ + ALWAYS_INLINE + void set(const size_t pos) { + buffer[pos / 64] |= (static_cast(1) << (pos % 64)); + } + + /** + * set to false (whether it was already set to false or not) + * + * This is an expensive (random access) API, you really ought to + * prepare a new word and then append it. + */ + ALWAYS_INLINE + void unset(const size_t pos) { + buffer[pos / 64] |= ~(static_cast(1) << (pos % 64)); + } + + /** + * true of false? (set or unset) + */ + ALWAYS_INLINE + bool get(const size_t pos) const { + return (buffer[pos / 64] & (static_cast(1) << (pos % 64))) != 0; + } + + /** + * set all bits to 0 + */ + void reset() { + memset(buffer.data(), 0, + sizeof(uint64_t) * + buffer.size()); // memset can be slow, does it matter? + sizeinbits = 0; + } + + size_t sizeInBits() const { return sizeinbits; } + + size_t sizeInBytes() const { return buffer.size() * sizeof(uint64_t); } + + /** + * Return memory usage of a bitmap spanning n bits + */ + static size_t sizeInBytes(size_t n) { + size_t buffersize = n / 64 + (n % 64 == 0 ? 0 : 1); + return buffersize * sizeof(uint64_t); + } + + ~BoolArray() {} +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_BOOLARRAY_H_ */ diff --git a/include/SIMDCompressionAndIntersection/codecfactory.h b/include/SIMDCompressionAndIntersection/codecfactory.h new file mode 100644 index 0000000..ddaf7d8 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/codecfactory.h @@ -0,0 +1,185 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire, http://lemire.me/en/ + */ + +#ifndef SIMDCompressionAndIntersection_CODECFACTORY_H_ +#define SIMDCompressionAndIntersection_CODECFACTORY_H_ + +#include "common.h" +#include "codecs.h" +#include "common.h" +#include "compositecodec.h" +#include "bitpackinghelpers.h" +#include "simdbitpackinghelpers.h" +#include "delta.h" +#include "util.h" +#include "synthetic.h" +#include "binarypacking.h" +#include "simdbinarypacking.h" +#include "simdvariablebyte.h" +#include "fastpfor.h" +#include "simdfastpfor.h" +#include "variablebyte.h" +#include "varintgb.h" +#include "streamvariablebyte.h" +#include "VarIntG8IU.h" // warning: patented scheme +#include "frameofreference.h" +#include "forcodec.h" + +namespace SIMDCompressionLib { + +using namespace std; + +typedef VariableByte leftovercodec; + +static std::map> initializefactory() { + std::map> schemes; +#ifdef __SSSE3__ + schemes["varintg8iu"] = shared_ptr(new VarIntG8IU()); +#endif /* __SSSE3__ */ + schemes["fastpfor"] = shared_ptr( + new CompositeCodec, leftovercodec>()); + + schemes["copy"] = shared_ptr(new JustCopy()); + schemes["varint"] = shared_ptr(new VariableByte()); + schemes["vbyte"] = shared_ptr(new VByte()); + schemes["maskedvbyte"] = shared_ptr(new MaskedVByte()); + schemes["streamvbyte"] = shared_ptr(new StreamVByteD1()); + schemes["frameofreference"] = + shared_ptr(new FrameOfReference()); + + schemes["simdframeofreference"] = + shared_ptr(new SIMDFrameOfReference()); + + schemes["varintgb"] = std::shared_ptr(new VarIntGB()); + + schemes["s4-fastpfor-d4"] = shared_ptr( + new CompositeCodec, leftovercodec>()); + schemes["s4-fastpfor-dm"] = shared_ptr( + new CompositeCodec, VariableByte>()); + schemes["s4-fastpfor-d1"] = shared_ptr( + new CompositeCodec, leftovercodec>()); + schemes["s4-fastpfor-d2"] = shared_ptr( + new CompositeCodec, leftovercodec>()); + + schemes["bp32"] = shared_ptr( + new CompositeCodec, + VariableByte>()); + schemes["ibp32"] = shared_ptr( + new CompositeCodec, + leftovercodec>()); + + schemes["s4-bp128-d1-ni"] = shared_ptr( + new CompositeCodec< + SIMDBinaryPacking>, + leftovercodec>()); + schemes["s4-bp128-d2-ni"] = shared_ptr( + new CompositeCodec< + SIMDBinaryPacking>, + leftovercodec>()); + schemes["s4-bp128-d4-ni"] = shared_ptr( + new CompositeCodec< + SIMDBinaryPacking>, + leftovercodec>()); + schemes["s4-bp128-dm-ni"] = shared_ptr( + new CompositeCodec< + SIMDBinaryPacking>, + leftovercodec>()); + + schemes["s4-bp128-d1"] = shared_ptr( + new CompositeCodec< + SIMDBinaryPacking>, + leftovercodec>()); + schemes["s4-bp128-d2"] = shared_ptr( + new CompositeCodec< + SIMDBinaryPacking>, + leftovercodec>()); + schemes["s4-bp128-d4"] = shared_ptr( + new CompositeCodec< + SIMDBinaryPacking>, + leftovercodec>()); + schemes["s4-bp128-dm"] = shared_ptr( + new CompositeCodec< + SIMDBinaryPacking>, + leftovercodec>()); + schemes["for"] = shared_ptr(new ForCODEC()); + return schemes; +} + +class CODECFactory { +public: + static map> scodecmap; + static shared_ptr defaultptr; + + // hacked for convenience + static vector> allSchemes() { + vector> ans; + for (auto i = scodecmap.begin(); i != scodecmap.end(); ++i) { + ans.push_back(i->second); + } + return ans; + } + + static vector allNames() { + vector ans; + for (auto i = scodecmap.begin(); i != scodecmap.end(); ++i) { + ans.push_back(i->first); + } + return ans; + } + + /** + * This function tries to determine whether the + * input is modified during compression. + */ + static bool modifiesInputDuringCompression(IntegerCODEC &v) { + vector test; + const uint32_t N = 2049; + for (uint32_t k = 0; k < N; ++k) + test.emplace_back(k); + vector out(N + 1024); + size_t outsize = out.size(); + v.encodeArray(test.data(), N, out.data(), outsize); + for (uint32_t k = 0; k < N; ++k) + if (test[k] != k) + return true; + return false; // granted this is not full-proof, but is ok in our context + } + + static string getName(IntegerCODEC &v) { + for (auto i = scodecmap.begin(); i != scodecmap.end(); ++i) { + if (i->second.get() == &v) + return i->first; + } + return "UNKNOWN"; + } + + static bool valid(string name) { + return (scodecmap.find(name) != scodecmap.end()); + } + + static shared_ptr &getFromName(string name) { + if (scodecmap.find(name) == scodecmap.end()) { + cerr << "name " << name << " does not refer to a CODEC." << endl; + cerr << "possible choices:" << endl; + for (auto i = scodecmap.begin(); i != scodecmap.end(); ++i) { + cerr << static_cast(i->first) + << endl; // useless cast, but just to be clear + } + return defaultptr; + } + return scodecmap[name]; + } +}; + +map> CODECFactory::scodecmap = + initializefactory(); + +shared_ptr CODECFactory::defaultptr = + shared_ptr(nullptr); +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_CODECFACTORY_H_ */ diff --git a/include/SIMDCompressionAndIntersection/codecs.h b/include/SIMDCompressionAndIntersection/codecs.h new file mode 100644 index 0000000..f90dddc --- /dev/null +++ b/include/SIMDCompressionAndIntersection/codecs.h @@ -0,0 +1,131 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire, http://lemire.me/en/ + */ + +#ifndef SIMDCompressionAndIntersection_CODECS_H_ +#define SIMDCompressionAndIntersection_CODECS_H_ + +#include "common.h" +#include "util.h" +#include "bitpackinghelpers.h" + +namespace SIMDCompressionLib { + +using namespace std; + +class NotEnoughStorage : public std::runtime_error { +public: + size_t required; // number of 32-bit symbols required + NotEnoughStorage(const size_t req) : runtime_error(""), required(req) {} +}; + +class IntegerCODEC { +public: + /** + * You specify input and input length, as well as + * output and output length. nvalue gets modified to + * reflect how much was used. If the new value of + * nvalue is more than the original value, we can + * consider this a buffer overrun. + * + * You are responsible for allocating the memory (length + * for *in and nvalue for *out). + */ + virtual void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) = 0; + + /** + * Usage is similar to encodeArray except that it returns a pointer + * incremented from in. In theory it should be in+length. If the + * returned pointer is less than in+length, then this generally means + * that the decompression is not finished (some scheme compress + * the bulk of the data one way, and they then they compress remaining + * integers using another scheme). + * + * As with encodeArray, you need to have length elements allocated + * for *in and at least nvalue elements allocated for out. The value + * of the variable nvalue gets updated with the number actually used + * (if nvalue exceeds the original value, there might be a buffer + * overrun). + */ + virtual const uint32_t *decodeArray(const uint32_t *in, const size_t length, + uint32_t *out, size_t &nvalue) = 0; + virtual ~IntegerCODEC() {} + + /** + * Will compress the content of a vector into + * another vector. + * + * This is offered for convenience. It might be slow. + */ + virtual vector compress(vector &data) { + vector compresseddata(data.size() * 2 + + 1024); // allocate plenty of memory + size_t memavailable = compresseddata.size(); + encodeArray(data.data(), data.size(), compresseddata.data(), memavailable); + compresseddata.resize(memavailable); + return compresseddata; + } + + /** + * Will uncompress the content of a vector into + * another vector. Some CODECs know exactly how much data to uncompress, + * others need to uncompress it all to know how data there is to uncompress... + * So it useful to have a hint (expected_uncompressed_size) that tells how + * much data there will be to uncompress. Otherwise, the code will + * try to guess, but the result is uncertain and inefficient. You really + * ought to keep track of how many symbols you had compressed. + * + * For convenience. Might be slow. + */ + virtual vector uncompress(vector &compresseddata, + size_t expected_uncompressed_size = 0) { + vector data( + expected_uncompressed_size); // allocate plenty of memory + size_t memavailable = data.size(); + try { + decodeArray(compresseddata.data(), compresseddata.size(), data.data(), + memavailable); + } catch (NotEnoughStorage &nes) { + data.resize(nes.required + 1024); + decodeArray(compresseddata.data(), compresseddata.size(), data.data(), + memavailable); + } + data.resize(memavailable); + return data; + } + + virtual string name() const = 0; +}; + +/****************** + * This just copies the data, no compression. + */ +class JustCopy : public IntegerCODEC { +public: + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + memcpy(out, in, sizeof(uint32_t) * length); + nvalue = length; + } + // like encodeArray, but we don't actually copy + void fakeencodeArray(const uint32_t * /*in*/, const size_t length, + size_t &nvalue) { + nvalue = length; + } + + const uint32_t *decodeArray(const uint32_t *in, const size_t length, + uint32_t *out, size_t &nvalue) { + memcpy(out, in, sizeof(uint32_t) * length); + nvalue = length; + return in + length; + } + string name() const { return "JustCopy"; } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_CODECS_H_ */ diff --git a/include/SIMDCompressionAndIntersection/common.h b/include/SIMDCompressionAndIntersection/common.h new file mode 100644 index 0000000..ffb22d3 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/common.h @@ -0,0 +1,60 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire, http://lemire.me/en/ + */ +#ifndef SIMDCompressionAndIntersection_COMMON_H_ +#define SIMDCompressionAndIntersection_COMMON_H_ + +#include +#include +#include +#include +#include +#ifndef _MSC_VER +#include +#endif +#include +#include +#include +#include +#ifndef _MSC_VER +#include +#include +#endif +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "platform.h" + +#ifdef USE_ALIGNED +#define MM_LOAD_SI_128 _mm_load_si128 +#define MM_STORE_SI_128 _mm_store_si128 +#else +#define MM_LOAD_SI_128 _mm_loadu_si128 +#define MM_STORE_SI_128 _mm_storeu_si128 +#endif + +namespace SIMDCompressionLib {} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_COMMON_H_ */ diff --git a/include/SIMDCompressionAndIntersection/compositecodec.h b/include/SIMDCompressionAndIntersection/compositecodec.h new file mode 100644 index 0000000..c1e1577 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/compositecodec.h @@ -0,0 +1,69 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire, http://lemire.me/en/ + */ +#ifndef SIMDCompressionAndIntersection_COMPOSITECODEC_H_ +#define SIMDCompressionAndIntersection_COMPOSITECODEC_H_ + +#include "common.h" +#include "util.h" +#include "codecs.h" + +namespace SIMDCompressionLib { + +/** + * This is a useful class for CODEC that only compress + * data having length a multiple of some unit length. + */ +template +class CompositeCodec : public IntegerCODEC { +public: + CompositeCodec() : codec1(), codec2() {} + Codec1 codec1; + Codec2 codec2; + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + const size_t roundedlength = length / Codec1::BlockSize * Codec1::BlockSize; + size_t nvalue1 = nvalue; + codec1.encodeArray(in, roundedlength, out, nvalue1); + + if (roundedlength < length) { + ASSERT(nvalue >= nvalue1, nvalue << " " << nvalue1); + size_t nvalue2 = nvalue - nvalue1; + codec2.encodeArray(in + roundedlength, length - roundedlength, + out + nvalue1, nvalue2); + nvalue = nvalue1 + nvalue2; + } else { + nvalue = nvalue1; + } + } + const uint32_t *decodeArray(const uint32_t *in, const size_t length, + uint32_t *out, size_t &nvalue) { + const uint32_t *const initin(in); + size_t mynvalue1 = nvalue; + const uint32_t *in2 = codec1.decodeArray(in, length, out, mynvalue1); + if (length + in > in2) { + assert(nvalue > mynvalue1); + size_t nvalue2 = nvalue - mynvalue1; + const uint32_t *in3 = codec2.decodeArray(in2, length - (in2 - in), + out + mynvalue1, nvalue2); + nvalue = mynvalue1 + nvalue2; + assert(initin + length >= in3); + return in3; + } + nvalue = mynvalue1; + assert(initin + length >= in2); + return in2; + } + string name() const { + ostringstream convert; + convert << codec1.name() << "+" << codec2.name(); + return convert.str(); + } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_COMPOSITECODEC_H_ */ diff --git a/include/SIMDCompressionAndIntersection/delta.h b/include/SIMDCompressionAndIntersection/delta.h new file mode 100644 index 0000000..4ca74c4 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/delta.h @@ -0,0 +1,86 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Leonid Boytsov, Nathan Kurz and Daniel Lemire + */ + +#ifndef SIMDCompressionAndIntersection_DELTA_H_ +#define SIMDCompressionAndIntersection_DELTA_H_ + +#include "common.h" + +namespace SIMDCompressionLib { + +/** + * To avoid crazy dependencies, this header should not + * include any other header file. + */ + +template void delta(const T initoffset, T *data, const size_t size) { + if (size == 0) + return; // nothing to do + if (size > 1) + for (size_t i = size - 1; i > 0; --i) { + data[i] -= data[i - 1]; + } + data[0] -= initoffset; +} + +template void delta(const T initoffset, T *data) { + if (size == 0) + return; // nothing to do + if (size > 1) + for (size_t i = size - 1; i > 0; --i) { + data[i] -= data[i - 1]; + } + data[0] -= initoffset; +} + +template +void inverseDelta(const T initoffset, T *data, const size_t size) { + if (size == 0) + return; // nothing to do + data[0] += initoffset; + const size_t UnrollQty = 4; + const size_t sz0 = + (size / UnrollQty) * UnrollQty; // equal to 0, if size < UnrollQty + size_t i = 1; + if (sz0 >= UnrollQty) { + T a = data[0]; + for (; i < sz0 - UnrollQty; i += UnrollQty) { + a = data[i] += a; + a = data[i + 1] += a; + a = data[i + 2] += a; + a = data[i + 3] += a; + } + } + for (; i != size; ++i) { + data[i] += data[i - 1]; + } +} +template void inverseDelta(const T initoffset, T *data) { + if (size == 0) + return; // nothing to do + data[0] += initoffset; + const size_t UnrollQty = 4; + const size_t sz0 = + (size / UnrollQty) * UnrollQty; // equal to 0, if size < UnrollQty + size_t i = 1; + if (sz0 >= UnrollQty) { + T a = data[0]; + for (; i < sz0 - UnrollQty; i += UnrollQty) { + a = data[i] += a; + a = data[i + 1] += a; + a = data[i + 2] += a; + a = data[i + 3] += a; + } + } + for (; i != size; ++i) { + data[i] += data[i - 1]; + } +} + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_DELTA_H_ */ diff --git a/include/SIMDCompressionAndIntersection/deltatemplates.h b/include/SIMDCompressionAndIntersection/deltatemplates.h new file mode 100644 index 0000000..160c36d --- /dev/null +++ b/include/SIMDCompressionAndIntersection/deltatemplates.h @@ -0,0 +1,164 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Leonid Boytsov, Nathan Kurz and Daniel Lemire + */ + +#ifndef SIMDCompressionAndIntersection_DELTATEMPLATES_H_ +#define SIMDCompressionAndIntersection_DELTATEMPLATES_H_ + +#include "common.h" + +namespace SIMDCompressionLib { + +/** + * To avoid crazy dependencies, this header should not + * include any other header file. + */ + +/** + * The structs RegularDeltaSIMD, NoDelta, CoarseDelta4SIMD, CoarseDelta2SIMD, + * Max4DeltaSIMD + * are used in templates to specify which type of differential encoding to use + * (if any). + * + * See SIMDDeltaProcessor + */ + +struct RegularDeltaSIMD { + // Folklore code, unknown origin of this idea + ALWAYS_INLINE + static __m128i PrefixSum(__m128i curr, __m128i prev) { + const __m128i _tmp1 = _mm_add_epi32(_mm_slli_si128(curr, 8), curr); + const __m128i _tmp2 = _mm_add_epi32(_mm_slli_si128(_tmp1, 4), _tmp1); + return _mm_add_epi32(_tmp2, _mm_shuffle_epi32(prev, 0xff)); + } + + ALWAYS_INLINE + static __m128i Delta(__m128i curr, __m128i prev) { + return _mm_sub_epi32( + curr, _mm_or_si128(_mm_slli_si128(curr, 4), _mm_srli_si128(prev, 12))); + } + + static bool usesDifferentialEncoding() { return true; } + + static std::string name() { return "Delta1"; } +}; + +struct NoDelta { + ALWAYS_INLINE + static __m128i PrefixSum(__m128i curr, __m128i) { return curr; } + ALWAYS_INLINE + static __m128i Delta(__m128i curr, __m128i) { return curr; } + + static bool usesDifferentialEncoding() { return false; } + static std::string name() { return "NoDelta"; } +}; + +struct CoarseDelta4SIMD { + ALWAYS_INLINE + // Proposed and implemented by L. Boytosv + static __m128i PrefixSum(__m128i curr, __m128i prev) { + return _mm_add_epi32(curr, prev); + } + ALWAYS_INLINE + static __m128i Delta(__m128i curr, __m128i prev) { + return _mm_sub_epi32(curr, prev); + } + + static bool usesDifferentialEncoding() { return true; } + + static std::string name() { return "Delta4"; } +}; + +struct CoarseDelta2SIMD { + ALWAYS_INLINE + // Proposed and implemented by L. Boytosv + static __m128i PrefixSum(__m128i curr, __m128i prev) { + const __m128i _tmp1 = _mm_add_epi32(_mm_slli_si128(curr, 8), curr); + return _mm_add_epi32(_tmp1, + _mm_shuffle_epi32(prev, _MM_SHUFFLE(3, 2, 3, 2))); + } + ALWAYS_INLINE + static __m128i Delta(__m128i curr, __m128i prev) { + return _mm_sub_epi32( + curr, _mm_or_si128(_mm_slli_si128(curr, 8), _mm_srli_si128(prev, 8))); + } + static bool usesDifferentialEncoding() { return true; } + + static std::string name() { return "Delta2"; } +}; + +struct Max4DeltaSIMD { + ALWAYS_INLINE + // The idea is due to N. Kurz + static __m128i PrefixSum(__m128i curr, __m128i prev) { + return _mm_add_epi32(curr, _mm_shuffle_epi32(prev, 0xff)); + } + ALWAYS_INLINE + static __m128i Delta(__m128i curr, __m128i prev) { + return _mm_sub_epi32(curr, _mm_shuffle_epi32(prev, 0xff)); + } + static std::string name() { return "DeltaM4"; } + + static bool usesDifferentialEncoding() { return true; } +}; + +/** + * Wrapper around the structs RegularDeltaSIMD, NoDelta, CoarseDelta4SIMD, + * CoarseDelta2SIMD, Max4DeltaSIMD + * to compute differential encoding and prefix sums. + */ +template struct SIMDDeltaProcessor { + static __m128i runPrefixSum(__m128i initOffset, uint32_t *pData) { + const size_t QtyDivBy4 = TotalQty / 4; + // The block should contain 8N 32-bit integers, where N is some integer + assert(QtyDivBy4 % 2 == 0); + + __m128i *pCurr = reinterpret_cast<__m128i *>(pData); + const __m128i *pEnd = pCurr + QtyDivBy4; + + // Leonid Boytsov: manual loop unrolling may be crucial here. + while (pCurr < pEnd) { + initOffset = DeltaHelper::PrefixSum(MM_LOAD_SI_128(pCurr), initOffset); + MM_STORE_SI_128(pCurr++, initOffset); + + initOffset = DeltaHelper::PrefixSum(MM_LOAD_SI_128(pCurr), initOffset); + MM_STORE_SI_128(pCurr++, initOffset); + } + + return initOffset; + } + + static void runDelta(__m128i initOffset, uint32_t *pData) { + const size_t QtyDivBy4 = TotalQty / 4; + // The block should contain 8N 32-bit integers, where N is some integer + assert(QtyDivBy4 && QtyDivBy4 % 2 == 0); + __m128i *pCurr = reinterpret_cast<__m128i *>(pData) + QtyDivBy4 - 1; + __m128i *pStart = reinterpret_cast<__m128i *>(pData); + __m128i a = MM_LOAD_SI_128(pCurr); + // Leonid Boytsov: manual loop unrolling may be crucial here. + while (pCurr > pStart + 1) { + __m128i b = MM_LOAD_SI_128(pCurr - 1); + MM_STORE_SI_128(pCurr, DeltaHelper::Delta(a, b)); + a = b; + --pCurr; + + b = MM_LOAD_SI_128(pCurr - 1); + MM_STORE_SI_128(pCurr, DeltaHelper::Delta(a, b)); + a = b; + --pCurr; + } + + __m128i b = MM_LOAD_SI_128(pStart); + MM_STORE_SI_128(pStart + 1, DeltaHelper::Delta(a, b)); + a = b; + + MM_STORE_SI_128(pStart, DeltaHelper::Delta(a, initOffset)); + } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_DELTATEMPLATES_H_ */ diff --git a/include/SIMDCompressionAndIntersection/fastpfor.h b/include/SIMDCompressionAndIntersection/fastpfor.h new file mode 100644 index 0000000..217a061 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/fastpfor.h @@ -0,0 +1,367 @@ +/* + * This is the non-SIMD version of FastPFOR. + * It is not recommended per se, only provided for + * comparison purposes. + */ + +#ifndef SIMDCompressionAndIntersection_FASTPFOR_H_ +#define SIMDCompressionAndIntersection_FASTPFOR_H_ + +#include "common.h" +#include "codecs.h" +#include "bitpackinghelpers.h" +#include "util.h" +#include "delta.h" + +namespace SIMDCompressionLib { + +class ScalarSortedBitPacker { +public: + enum { DEFAULTSIZE = 128 }; + uint32_t buffer[32]; + + ScalarSortedBitPacker() { + for (uint32_t i = 0; i < 32; ++i) { + data[i] = new uint32_t[DEFAULTSIZE]; + memset(data[i], 0, DEFAULTSIZE * sizeof(uint32_t)); + actualsizes[i] = DEFAULTSIZE; + } + clear(); + } + + void reset() { + for (uint32_t i = 0; i < 32; ++i) { + delete[] data[i]; + data[i] = new uint32_t[DEFAULTSIZE]; + memset(data[i], 0, DEFAULTSIZE * sizeof(uint32_t)); + actualsizes[i] = DEFAULTSIZE; + } + clear(); + } + + ~ScalarSortedBitPacker() { free(); } + void free() { + clear(); + for (uint32_t i = 0; i < 32; ++i) + if (data[i] != NULL) { + delete[] data[i]; + data[i] = NULL; + actualsizes[i] = 0; + } + } + void directAppend(uint32_t i, uint32_t val) { data[i][sizes[i]++] = val; } + + const uint32_t *get(int i) { return data[i]; } + + void ensureCapacity(int i, uint32_t datatoadd) { + if (sizes[i] + datatoadd > actualsizes[i]) { + actualsizes[i] = (sizes[i] + datatoadd + 127) / 128 * 128 * 2; + uint32_t *tmp = new uint32_t[actualsizes[i]]; + for (uint32_t j = 0; j < sizes[i]; ++j) + tmp[j] = data[i][j]; + delete[] data[i]; + data[i] = tmp; + } + } + + void clear() { + for (uint32_t i = 0; i < 32; ++i) + sizes[i] = 0; // memset "might" be faster. + } + + uint32_t *write(uint32_t *out) { + uint32_t bitmap = 0; + for (uint32_t k = 1; k < 32; ++k) { + if (sizes[k] != 0) + bitmap |= (1U << k); + } + *(out++) = bitmap; + + for (uint32_t k = 1; k < 32; ++k) { + if (sizes[k] != 0) { + *out = sizes[k]; + out++; + uint32_t j = 0; + for (; j < sizes[k]; j += 32) { + BitPackingHelpers::fastpackwithoutmask(&data[k][j], out, k + 1); + out += k + 1; + } + out -= (j - sizes[k]) * (k + 1) / 32; + } + } + return out; + } + + const uint32_t *read(const uint32_t *in) { + clear(); + const uint32_t bitmap = *(in++); + + for (uint32_t k = 1; k < 32; ++k) { + if ((bitmap & (1U << k)) != 0) { + sizes[k] = *in++; + if (actualsizes[k] < sizes[k]) { + delete[] data[k]; + actualsizes[k] = (sizes[k] + 31) / 32 * 32; + data[k] = new uint32_t[actualsizes[k]]; + } + uint32_t j = 0; + for (; j + 31 < sizes[k]; j += 32) { + BitPackingHelpers::fastunpack(in, &data[k][j], k + 1); + in += k + 1; + } + uint32_t remaining = sizes[k] - j; + memcpy(buffer, in, (remaining * (k + 1) + 31) / 32 * sizeof(uint32_t)); + uint32_t *bpointer = buffer; + in += ((sizes[k] + 31) / 32 * 32 - j) / 32 * (k + 1); + for (; j < sizes[k]; j += 32) { + BitPackingHelpers::fastunpack(bpointer, &data[k][j], k + 1); + bpointer += k + 1; + } + in -= (j - sizes[k]) * (k + 1) / 32; + } + } + return in; + } + +private: + uint32_t *data[32]; + uint32_t sizes[32]; + uint32_t actualsizes[32]; + + // we don't want anyone to start copying this class + ScalarSortedBitPacker(const ScalarSortedBitPacker &); + ScalarSortedBitPacker &operator=(const ScalarSortedBitPacker &); +}; + +/** + * FastPFor + * + * Reference and documentation: + * + * Daniel Lemire and Leonid Boytsov, Decoding billions of integers per second + * through vectorization + * http://arxiv.org/abs/1209.2137 + * + * Designed by D. Lemire with ideas from Leonid Boytsov. This scheme is NOT + * patented. + * + */ +template // BlockSizeInUnitsOfPackSize should be 4 or 8 +class FastPFor : public IntegerCODEC { +public: + /** + * ps (page size) should be a multiple of BlockSize, any "large" + * value should do. + */ + FastPFor(uint32_t ps = 65536) + : PageSize(ps), bitsPageSize(gccbits(PageSize)), bpacker(), + bytescontainer(PageSize + 3 * PageSize / BlockSize) { + assert(ps / BlockSize * BlockSize == ps); + assert(gccbits(BlockSizeInUnitsOfPackSize * PACKSIZE - 1) <= 8); + } + enum { + PACKSIZE = 32, + overheadofeachexcept = 8, + overheadduetobits = 8, + overheadduetonmbrexcept = 8, + BlockSize = BlockSizeInUnitsOfPackSize * PACKSIZE + }; + + const uint32_t PageSize; + const uint32_t bitsPageSize; + ScalarSortedBitPacker bpacker; + vector bytescontainer; + + const uint32_t *decodeArray(const uint32_t *in, const size_t length, + uint32_t *out, size_t &nvalue) { + const uint32_t *const initin(in); + const size_t mynvalue = *in; + ++in; + if (mynvalue > nvalue) + throw NotEnoughStorage(mynvalue); + nvalue = mynvalue; + const uint32_t *const finalout(out + nvalue); + uint32_t prev = 0; + while (out != finalout) { + size_t thisnvalue(0); + size_t thissize = static_cast( + finalout > PageSize + out ? PageSize : (finalout - out)); + + __decodeArray(in, thisnvalue, out, thissize, prev); + in += thisnvalue; + out += thissize; + } + assert(initin + length >= in); + bpacker.reset(); // if you don't do this, the codec has a "memory". + return in; + } + + /** + * If you save the output and recover it in memory, you are + * responsible to ensure that the alignment is preserved. + * + * The input size (length) should be a multiple of + * BlockSizeInUnitsOfPackSize * PACKSIZE. (This was done + * to simplify slightly the implementation.) + */ + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + checkifdivisibleby(length, BlockSize); + const uint32_t *const initout(out); + const uint32_t *const finalin(in + length); + + *out++ = static_cast(length); + const size_t oldnvalue = nvalue; + nvalue = 1; + uint32_t prev = 0; + while (in != finalin) { + size_t thissize = static_cast( + finalin > PageSize + in ? PageSize : (finalin - in)); + size_t thisnvalue(0); + __encodeArray(in, thissize, out, thisnvalue, prev); + nvalue += thisnvalue; + out += thisnvalue; + in += thissize; + } + assert(out == nvalue + initout); + if (oldnvalue < nvalue) + std::cerr + << "It is possible we have a buffer overrun. You reported having allocated " + << oldnvalue * sizeof(uint32_t) + << " bytes for the compressed data but we needed " + << nvalue * sizeof(uint32_t) + << " bytes. Please increase the available memory" + " for compressed data or check the value of the last parameter provided " + " to the encodeArray method." << std::endl; + bpacker.reset(); // if you don't do this, the buffer has a memory + } + + void getBestBFromData(const uint32_t *in, uint8_t &bestb, + uint8_t &bestcexcept, uint8_t &maxb) { + uint32_t freqs[33]; + for (uint32_t k = 0; k <= 32; ++k) + freqs[k] = 0; + for (uint32_t k = 0; k < BlockSize; ++k) { + freqs[asmbits(in[k])]++; + } + bestb = 32; + while (freqs[bestb] == 0) + bestb--; + maxb = bestb; + uint32_t bestcost = bestb * BlockSize; + uint32_t cexcept = 0; + bestcexcept = static_cast(cexcept); + for (uint32_t b = bestb - 1; b < 32; --b) { + cexcept += freqs[b + 1]; + uint32_t thiscost = cexcept * overheadofeachexcept + + cexcept * (maxb - b) + b * BlockSize + + 8; // the extra 8 is the cost of storing maxbits + if (bestb - b == 1) + thiscost -= cexcept; + if (thiscost < bestcost) { + bestcost = thiscost; + bestb = static_cast(b); + bestcexcept = static_cast(cexcept); + } + } + } + + void __encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue, uint32_t &prev) { + uint32_t *const initout = out; // keep track of this + checkifdivisibleby(length, BlockSize); + uint32_t *const headerout = out++; // keep track of this + bpacker.clear(); + uint8_t *bc = bytescontainer.data(); + for (const uint32_t *const final = in + length; (in + BlockSize <= final); + in += BlockSize) { + uint8_t bestb, bestcexcept, maxb; + if (useDelta) { + uint32_t nextprev = in[BlockSize - 1]; + delta(prev, in, BlockSize); + prev = nextprev; + } + getBestBFromData(in, bestb, bestcexcept, maxb); + *bc++ = bestb; + *bc++ = bestcexcept; + if (bestcexcept > 0) { + *bc++ = maxb; + bpacker.ensureCapacity(maxb - bestb - 1, bestcexcept); + const uint32_t maxval = 1U << bestb; + for (uint32_t k = 0; k < BlockSize; ++k) { + if (in[k] >= maxval) { + bpacker.directAppend(maxb - bestb - 1, in[k] >> bestb); + *bc++ = static_cast(k); + } + } + } + for (size_t k = 0; k < BlockSizeInUnitsOfPackSize; ++k) { + BitPackingHelpers::fastpack(in + k * 32, out + k * bestb, bestb); + } + out += BlockSizeInUnitsOfPackSize * bestb; + } + headerout[0] = static_cast(out - headerout); + const uint32_t bytescontainersize = + static_cast(bc - bytescontainer.data()); + *(out++) = bytescontainersize; + memcpy(out, bytescontainer.data(), bytescontainersize); + out += (bytescontainersize + sizeof(uint32_t) - 1) / sizeof(uint32_t); + const uint32_t *const lastout = bpacker.write(out); + nvalue = lastout - initout; + } + + void __decodeArray(const uint32_t *in, size_t &length, uint32_t *out, + const size_t nvalue, uint32_t &prev) { + const uint32_t *const initin = in; + const uint32_t *const headerin = in++; + const uint32_t wheremeta = headerin[0]; + const uint32_t *inexcept = headerin + wheremeta; + const uint32_t bytesize = *inexcept++; + const uint8_t *bytep = reinterpret_cast(inexcept); + + inexcept += (bytesize + sizeof(uint32_t) - 1) / sizeof(uint32_t); + inexcept = bpacker.read(inexcept); + length = inexcept - initin; + const uint32_t *unpackpointers[32 + 1]; + for (uint32_t k = 1; k <= 32; ++k) { + unpackpointers[k] = bpacker.get(k - 1); + } + for (uint32_t run = 0; run < nvalue / BlockSize; ++run, out += BlockSize) { + const uint8_t b = *bytep++; + const uint8_t cexcept = *bytep++; + for (size_t k = 0; k < BlockSizeInUnitsOfPackSize; ++k) { + BitPackingHelpers::fastunpack(in + k * b, out + k * 32, b); + } + in += BlockSizeInUnitsOfPackSize * b; + if (cexcept > 0) { + const uint8_t maxbits = *bytep++; + if (maxbits - b == 1) { + for (uint32_t k = 0; k < cexcept; ++k) { + const uint8_t pos = *(bytep++); + out[pos] |= static_cast(1) << b; + } + } else { + const uint32_t *vals = unpackpointers[maxbits - b]; + unpackpointers[maxbits - b] += cexcept; + for (uint32_t k = 0; k < cexcept; ++k) { + const uint8_t pos = *(bytep++); + out[pos] |= vals[k] << b; + } + } + } + if (useDelta) { + inverseDelta(prev, out, BlockSize); + prev = out[BlockSize - 1]; + } + } + + assert(in == headerin + wheremeta); + } + + string name() const { return string("FastPFor") + (useDelta ? "Delta" : ""); } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_FASTPFOR_H_ */ diff --git a/include/SIMDCompressionAndIntersection/for.h b/include/SIMDCompressionAndIntersection/for.h new file mode 100644 index 0000000..cc989f6 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/for.h @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2005-2015 Christoph Rupp (chris@crupp.de). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A fast implementation for Frame of Reference encoding. + * + * See the README.md file for more information, example code and references. + * + * Feel free to send comments/questions to chris@crupp.de. I am available + * for consulting. + */ + +#ifndef FOR_H_5580af15_4570_41f9_ba2b_8afb1400e81e +#define FOR_H_5580af15_4570_41f9_ba2b_8afb1400e81e + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Returns the size required to compress a sequence of |length| ints, + * each compressed with |bits| bits + * + * This function will NOT include any overhead required by + * for_compress_sorted() and for_compress_unsorted(). + * + * Invariant: bits <= 32 + */ +extern uint32_t for_compressed_size_bits(uint32_t length, uint32_t bits); + +/** + * Returns the size required to compress an unsorted sequence of |length| ints. + * + * This routine scans |in| for the min/max values and then calls + * for_compressed_size_bits(). + * + * The returned size will include the overhead required for + * for_compress_sorted() and for_compressed_unsorted(). + */ +extern uint32_t for_compressed_size_unsorted(const uint32_t *in, + uint32_t length); + +/** + * Returns the size required to compress a sorted sequence of |length| ints. + * + * This routine extracts min/max values at the beginning and end of + * the sequence, then calls for_compressed_size_bits(). It is therefore + * slightly faster than for_compressed_size_unsorted(). + * + * The returned size will include the overhead required for + * for_compress_sorted() and for_compressed_unsorted(). + */ +extern uint32_t for_compressed_size_sorted(const uint32_t *in, uint32_t length); + +/** + * Compresses a sequence of |length| ints at |in| and stores the result + * in |out|. + * + * |base| is the "offset" (or common delta value) of all ints. It is usually + * set to the minimum value of the uncompressed sequence. + * + * |bits| are the bits required to store a single integer. + * + * Returns the number of bytes used for compression. + * + * This is for advanced users who opt for storing |base| and |bits| on their + * own. This function is called by for_compress_sorted() and + * for_compress_unsorted(). + * + * Invariant: bits <= 32 + */ +extern uint32_t for_compress_bits(const uint32_t *in, uint8_t *out, + uint32_t length, uint32_t base, + uint32_t bits); + +/** + * Compresses an unsorted sequence of |length| ints at |in| and stores the + * result in |out|. + * + * This routine scans |in| for the min/max values and then calls + * for_compress_bits(). + * + * The minimun value and the bits are stored as metadata in |out|. + */ +extern uint32_t for_compress_unsorted(const uint32_t *in, uint8_t *out, + uint32_t length); + +/** + * Compresses a sorted sequence of |length| ints at |in| and stores the + * result in |out|. + * + * This routine extracts min/max values at the beginning and end of + * the sequence, then calls for_compress_bits(). + * + * The minimun value and the bits are stored as metadata in |out|. + */ +extern uint32_t for_compress_sorted(const uint32_t *in, uint8_t *out, + uint32_t length); + +/** + * Uncompresses a sequence of |length| ints at |in| and stores the + * result in |out|. + * + * |base| is the "offset" (or common delta value) of all ints. It is usually + * set to the minimum value of the uncompressed sequence. + * + * |bits| are the bits required to store a single integer. + * + * Returns the number of compressed bytes processed. + * + * This function is for advanced users. It is the counterpart of + * for_compress_bits(). + * + * Invariant: bits <= 32 + */ +extern uint32_t for_uncompress_bits(const uint8_t *in, uint32_t *out, + uint32_t length, uint32_t base, + uint32_t bits); + +/** + * Uncompresses a sequence of |length| ints at |in| and stores the + * result in |out|. + * + * This function is a convenience wrapper for for_uncompress_bits(). It + * expects metadata at the beginning of |in|. Use in combination with + * for_compress_sorted() and for_compress_unsorted(). + * + * Returns the number of compressed bytes processed. + */ +extern uint32_t for_uncompress(const uint8_t *in, uint32_t *out, + uint32_t length); + +/** + * Appends a |value| to a compressed sequence of unsorted integers. + * + * This function is optimized for appending new values at the end of an + * encoded sequence. This is only possible if the new value (more precisely: + * the delta of the new value) can be stored in the same amount of bits that + * were used to encode the other integers. + * + * If this is not the case then memory is allocated, the whole sequence is + * decoded and re-encoded using more bits. This requires a heap allocation + * with malloc(). + * + * Returns the size (in bytes) of the compressed data, or 0 if malloc() fails. + */ +extern uint32_t for_append_unsorted(uint8_t *in, uint32_t length, + uint32_t value); + +/** + * Appends a |value| to a compressed sequence of sorted integers. + * + * This function is optimized for appending new values at the end of an + * encoded sequence. This is only possible if the new value (more precisely: + * the delta of the new value) can be stored in the same amount of bits that + * were used to encode the other integers. + * + * If this is not the case then memory is allocated, the whole sequence is + * decoded and re-encoded using more bits. This requires a heap allocation + * with malloc(). + * + * Returns the size (in bytes) of the compressed data, or 0 if malloc() fails. + */ +extern uint32_t for_append_sorted(uint8_t *in, uint32_t length, uint32_t value); + +/** + * Appends a |value| to a compressed integer sequence. + * + * |base| is the "offset" (or common delta value) of all ints. It is usually + * set to the minimum value of the uncompressed sequence. + * + * |bits| are the bits required to store a single integer. + * + * Returns the size (in bytes) of the compressed data. + * + * Invariant: bits <= 32 + * Invariant: the new |value| (more precisely: |value - base|) can be stored + * in |bits| bits. Details can be found in the implementation of + * for_append() in for.c. + */ +extern uint32_t for_append_bits(uint8_t *in, uint32_t length, uint32_t base, + uint32_t bits, uint32_t value); + +/** + * Returns the value at the given |index| from a compressed sequence. + * + * Make sure that |index| does not exceed the length of the sequence. + * + * |base| is the "offset" (or common delta value) of all ints. It is usually + * set to the minimum value of the uncompressed sequence. + * + * Invariant: bits <= 32 + */ +extern uint32_t for_select_bits(const uint8_t *in, uint32_t base, uint32_t bits, + uint32_t index); + +/** + * Returns the value at the given |index| from a compressed sequence. + * + * Make sure that |index| does not exceed the length of the sequence. + * + * This function is a convenience wrapper for for_select_bits(). It + * expects metadata at the beginning of |in|. Use in combination with + * for_compress_sorted() and for_compress_unsorted(). + */ +extern uint32_t for_select(const uint8_t *in, uint32_t index); + +/** + * Performs a linear search for |value|. + * + * Returns the index of the found element, or |length| if the key was not + * found. + * + * This function is a convenience wrapper for for_linear_search_bits(). It + * expects metadata at the beginning of |in|. Use in combination with + * for_compress_sorted() and for_compress_unsorted(). + */ +extern uint32_t for_linear_search(const uint8_t *in, uint32_t length, + uint32_t value); + +/** + * Performs a linear search for |value|. + * + * Returns the index of the found element, or |length| if the key was not + * found. + * + * |base| is the "offset" (or common delta value) of all ints. It is usually + * set to the minimum value of the uncompressed sequence. + * + * Invariant: bits <= 32 + */ +extern uint32_t for_linear_search_bits(const uint8_t *in, uint32_t length, + uint32_t base, uint32_t bits, + uint32_t value); + +/** + * Performs lower bound binary search search for |value|. + * + * A lower bound search returns the first element in the sequence which does + * not compare less than |value|. + * The actual result is stored in |*actual|. + * + * This function is a convenience wrapper for for_lower_bound_search_bits(). It + * expects metadata at the beginning of |in|. Use in combination with + * for_compress_sorted() and for_compress_unsorted(). + */ +extern uint32_t for_lower_bound_search(const uint8_t *in, uint32_t length, + uint32_t value, uint32_t *actual); + +/** + * Performs lower bound binary search search for |value|. + * + * A lower bound search returns the first element in the sequence which does + * not compare less than |value|. + * The actual result is stored in |*actual|. + * + * |base| is the "offset" (or common delta value) of all ints. It is usually + * set to the minimum value of the uncompressed sequence. + * + * Invariant: bits <= 32 + */ +extern uint32_t for_lower_bound_search_bits(const uint8_t *in, uint32_t length, + uint32_t base, uint32_t bits, + uint32_t value, uint32_t *actual); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* FOR_H_5580af15_4570_41f9_ba2b_8afb1400e81e */ diff --git a/include/SIMDCompressionAndIntersection/forcodec.h b/include/SIMDCompressionAndIntersection/forcodec.h new file mode 100644 index 0000000..6c53aeb --- /dev/null +++ b/include/SIMDCompressionAndIntersection/forcodec.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2005-2015 Christoph Rupp (chris@crupp.de). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Frame of Reference encoding + * + * based on code from http://github.com/cruppstahl/for + */ + +#ifndef INCLUDE_FOR_H +#define INCLUDE_FOR_H + +#include "common.h" +#include "codecs.h" +#include "util.h" +#include "for.h" + +namespace SIMDCompressionLib { + +/* + * Optimized scalar implementation of FOR (frame-of-reference) compression + */ +class ForCODEC : public IntegerCODEC { +public: + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + uint32_t cappedLength = static_cast( + std::min(length, std::numeric_limits::max())); + *(uint32_t *)out = cappedLength; + out++; + // for_compress_sorted() would be a bit faster, but requires + // sorted input + nvalue = (4 + for_compress_unsorted((const uint32_t *)in, (uint8_t *)out, + cappedLength) + + 3) / + 4; + } + + const uint32_t *decodeArray(const uint32_t *in, const size_t, uint32_t *out, + size_t &nvalue) { + nvalue = *in; + in++; + return in + for_uncompress((const uint8_t *)in, out, + static_cast(nvalue)); + } + + // append a key. + // Returns the new size of the compressed array *in bytes* + size_t appendToByteArray(uint8_t *in, const size_t bytesize, + uint32_t /*previous_key*/, uint32_t key) { + return append(in, bytesize, key); + } + + size_t append(uint8_t *in, const size_t /* unused */, uint32_t value) { + uint32_t length = *(uint32_t *)in; + size_t s = for_append_unsorted(in + 4, length, value); + *(uint32_t *)in = length + 1; + return s + 4; + } + + size_t findLowerBound(const uint32_t *in, const size_t, uint32_t key, + uint32_t *presult) { + uint32_t length = *in; + in++; + return (size_t)for_lower_bound_search((const uint8_t *)in, length, key, + presult); + } + + uint32_t select(const uint32_t *in, size_t index) { + in++; // Skip length + return for_select((const uint8_t *)in, static_cast(index)); + } + + string name() const { return "For"; } +}; + +} /* namespace */ + +#endif /* INCLUDE_FOR_H */ diff --git a/include/SIMDCompressionAndIntersection/frameofreference.h b/include/SIMDCompressionAndIntersection/frameofreference.h new file mode 100644 index 0000000..8bf35c3 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/frameofreference.h @@ -0,0 +1,134 @@ + + +#ifndef INCLUDE_FRAMEOFREFERENCE_H_ +#define INCLUDE_FRAMEOFREFERENCE_H_ + +#include "common.h" +#include "codecs.h" +#include "util.h" + +namespace SIMDCompressionLib { + +/** + * Simple implementation of FOR compression using blocks of + * 32 integers. + * + * This implementation is inferior to ForCODEC. Please use + * ForCODEC instead. + * + * FOR does not compress particularly well but it supports + * fast random access. + */ +class FrameOfReference : public IntegerCODEC { +public: + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + *out = static_cast( + std::min(length, std::numeric_limits::max())); + uint32_t *finalout = compress_length(in, *out, out + 1); + nvalue = finalout - out; + } + + const uint32_t *uncompress_length(const uint32_t *in, uint32_t *out, + uint32_t nvalue); + uint32_t *compress_length(const uint32_t *in, uint32_t length, uint32_t *out); + + const uint32_t *decodeArray(const uint32_t *in, const size_t, uint32_t *out, + size_t &nvalue) { + nvalue = *in; + in++; + return uncompress_length(in, out, static_cast(nvalue)); + } + + // appends the value "value" at the end of the compressed stream. Assumes that + // we have + // the space to do so. + // returns the next (total) size of the compressed output in bytes + // the "currentcompressedsizeinbytes" should be zero when no data has been + // compressed yet + size_t append(uint8_t *inbyte, const size_t currentcompressedsizeinbytes, + uint32_t value); + + // append a key. + // Returns the new size of the compressed array *in bytes* + size_t appendToByteArray(uint8_t *in, const size_t bytesize, + uint32_t /*previous_key*/, uint32_t key) { + return append(in, bytesize, key); + } + + // Performs a lower bound find in the encoded array. + // Returns the index + size_t findLowerBound(const uint32_t *in, const size_t length, uint32_t key, + uint32_t *presult); + + // Returns a decompressed value in an encoded array + uint32_t select(const uint32_t *in, size_t index); + + string name() const { return "FrameOfReference"; } + +private: +}; + +/** + * Accelerated implementation of FOR compression using blocks of + * 128 integers. . + * + * + * FOR does not compress particularly well but it supports + * fast random access. + */ +class SIMDFrameOfReference : public IntegerCODEC { +public: + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + *out = static_cast( + std::min(length, std::numeric_limits::max())); + uint32_t *finalout = simd_compress_length(in, *out, out + 1); + nvalue = finalout - out; + } + + const uint32_t *simd_uncompress_length(const uint32_t *in, uint32_t *out, + uint32_t nvalue); + uint32_t *simd_compress_length(const uint32_t *in, uint32_t length, + uint32_t *out); + uint32_t *simd_compress_length_sorted(const uint32_t *in, uint32_t length, + uint32_t *out); + + const uint32_t *decodeArray(const uint32_t *in, const size_t, uint32_t *out, + size_t &nvalue) { + nvalue = *in; + in++; + return simd_uncompress_length(in, out, static_cast(nvalue)); + } + + // appends the value "value" at the end of the compressed stream. Assumes that + // we have + // the space to do so. + // returns the next (total) size of the compressed output in bytes + // the "currentcompressedsizeinbytes" should be zero when no data has been + // compressed yet + size_t append(uint8_t *inbyte, const size_t currentcompressedsizeinbytes, + uint32_t value); + + // append a key. + // Returns the new size of the compressed array *in bytes* + size_t appendToByteArray(uint8_t *in, const size_t bytesize, + uint32_t /* previous_key*/, uint32_t key) { + return append(in, bytesize, key); + } + + // Performs a lower bound find in the encoded array. + // Returns the index + size_t findLowerBound(const uint32_t *in, const size_t length, uint32_t key, + uint32_t *presult); + + // Returns a decompressed value in an encoded array + uint32_t select(const uint32_t *in, size_t index); + + string name() const { return "SIMDFrameOfReference"; } + +private: +}; +} + +#endif /* INCLUDE_FRAMEOFREFERENCE_H_ */ diff --git a/include/SIMDCompressionAndIntersection/hybm2.h b/include/SIMDCompressionAndIntersection/hybm2.h new file mode 100644 index 0000000..c01fb78 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/hybm2.h @@ -0,0 +1,642 @@ +/* + * This is an implementation of the hyb+m2 method proposed in: + * + * J. S. Culpepper and A. Moffat. Efficient set intersection for + * inverted indexing. ACM Trans. Inf. Syst., 29(1):1:1Ð1:25, Dec. 2010. + * + * Implemented by Daniel Lemire + */ + +#ifndef SIMDCompressionAndIntersection_HYBM2_H_ +#define SIMDCompressionAndIntersection_HYBM2_H_ + +#include "common.h" +#include "codecs.h" +#include "codecfactory.h" +#include "boolarray.h" +#include "intersection.h" +#include "skipping.h" + +namespace SIMDCompressionLib { + +class HybM2 { +public: + // th = 0 means that we select bitmaps as needed + HybM2(IntegerCODEC &c, intersectionfunction inter, uint32_t MaxId, + vector &recovbuffer, uint32_t th = 32) + : bitmapmap(), shortlistmap(), mapuncompsizes(), mMaxId(MaxId), + threshold(th), recovbuffer(recovbuffer), codec(c), Inter(inter) {} + + /** + * Returns the *uncompressed* size of a given posting list (specified + * by ID). + */ + size_t getSizeInInts(uint32_t postId) { return mapuncompsizes[postId]; } + + /** + * Load an array (data) of length "length" as the posting list corresponding + * to id postid + * into the data structure. The data will be either converted to a bitmap or + * compressed. + */ + size_t load(const uint32_t postid, const uint32_t *data, + const uint32_t length) { + if (threshold == 0) + return loadOptimized(postid, data, length); + else if (length * threshold >= mMaxId) + return loadAsBitmap(postid, data, length); + else + return loadAsShortArray(postid, data, length); + } + + /** + * Check whether we have a posting list corresponding to postid + */ + bool hasBeenLoaded(const uint32_t postid) { + return ((shortlistmap.find(postid) != shortlistmap.end()) || + (bitmapmap.find(postid) != bitmapmap.end())); + } + + /** + * Compute the total of the volume of of posting lists + * corresponding to a query. + */ + size_t computeUnpackVolume(const vector &ids) { + size_t answer = 0; + for (uint32_t id : ids) { + answer += mapuncompsizes[id]; + } + return answer; + } + + /** + * Compute the total of the intersection of of posting lists + * corresponding to a query, and output how many integers are + * in + * + * ids: the query as a set of posting ids + * out: write to result + * sizeout: will indicate how many integers were written to out. + * + * return unpack volume defined as the total volume of the posting + * lists that were needed to compute the intersection (which can + * be less than the total volume possible due to early abandoning). + */ + size_t intersect(const vector &ids, uint32_t *out, + size_t &sizeout) { + if (ids.empty()) { + sizeout = 0; + return 0; + } + vector>>> shortlists; + vector>> bitmaps; + // vector bitmapscard; + + for (uint32_t id : ids) { + if (shortlistmap.find(id) != shortlistmap.end()) + shortlists.push_back(make_pair(mapuncompsizes[id], shortlistmap[id])); + else { + assert(bitmapmap.find(id) != bitmapmap.end()); + bitmaps.push_back(make_pair(mapuncompsizes[id], bitmapmap[id])); + } + } + size_t unpackVolume = 0; + if (shortlists.empty()) { + if (bitmaps.size() == 1) { + sizeout = bitmaps.front().second->toInts(out); + unpackVolume += sizeout; + return unpackVolume; + } + + BoolArray answer(mMaxId); + bitmaps[0].second->intersect(*bitmaps[1].second, answer); + unpackVolume += bitmaps[0].first + bitmaps[1].first; + for (uint32_t i = 2; i < bitmaps.size(); ++i) { + answer.inplaceIntersect(*bitmaps[i].second); + unpackVolume += bitmaps[i].first; + } + sizeout = answer.toInts(out); + return unpackVolume; + } else { + sort(shortlists.begin(), shortlists.end()); + sort(bitmaps.begin(), bitmaps.end()); + codec.decodeArray(shortlists[0].second->data(), + shortlists[0].second->size(), out, sizeout); + assert(shortlists[0].first == sizeout); + unpackVolume += sizeout; + assert(sizeout == shortlists[0].first); + for (uint32_t i = 1; (sizeout > 0) && (i < shortlists.size()); ++i) { + size_t thissize = recovbuffer.size(); + codec.decodeArray(shortlists[i].second->data(), + shortlists[i].second->size(), recovbuffer.data(), + thissize); + unpackVolume += thissize; + assert(shortlists[i].first == thissize); + sizeout = Inter(out, sizeout, recovbuffer.data(), thissize, out); + } + size_t pos = 0; + for (uint32_t i = 0; (sizeout > 0) && (i < bitmaps.size()); ++i) { + unpackVolume += bitmaps[i].first; + shared_ptr &ba = bitmaps[i].second; + pos = 0; + for (uint32_t i = 0; i < sizeout; ++i) { + if (!ba->get(out[i])) + continue; + else + out[pos++] = out[i]; + } + sizeout = pos; + } + return unpackVolume; + } + } + + ~HybM2() {} + + /** + * Estimate of the volume of data used by this object. + */ + size_t storageInBytes() const { + size_t answer = 0; + for (auto i : bitmapmap) + answer += i.second->sizeInBytes(); + for (auto i : shortlistmap) + answer += i.second->size() * sizeof(uint32_t); + return answer; + } + + size_t sizeOfRecoveryBufferInWords() const { return recovbuffer.size(); } + +private: + // load as either a bitmap or a compressed short list + size_t loadOptimized(const uint32_t postid, const uint32_t *data, + const uint32_t length) { + if (mapuncompsizes.find(postid) != mapuncompsizes.end()) + return 0; + vector *compressedbuffer = new vector(length + 1024); + size_t outlength = compressedbuffer->size(); + vector tmp( + data, + data + length); // use the buffer because some codecs modify the input + codec.encodeArray(tmp.data(), length, compressedbuffer->data(), outlength); + if (outlength * sizeof(uint32_t) < + BoolArray::sizeInBytes(mMaxId)) { // we are good + if (recovbuffer.size() < length) + recovbuffer.resize(length); + compressedbuffer->resize(outlength); + compressedbuffer->shrink_to_fit(); + shortlistmap[postid] = shared_ptr>(compressedbuffer); + mapuncompsizes[postid] = length; + return compressedbuffer->size(); + } else { + delete compressedbuffer; + return loadAsBitmap(postid, data, length); + } + } + /** + * Load an array (data) of length "length" as the posting list corresponding + * to id postid + * as a bitmap. + * + * Do not call this directly, call load() instead. + */ + size_t loadAsBitmap(const uint32_t postid, const uint32_t *data, + const uint32_t length) { + if (bitmapmap.find(postid) != bitmapmap.end()) + return 0; + BoolArray *ba = new BoolArray(mMaxId); + for (uint32_t k = 0; k < length; ++k) + ba->set(data[k]); + bitmapmap[postid] = shared_ptr(ba); + mapuncompsizes[postid] = length; + return ba->sizeInBytes() / sizeof(uint32_t); + } + + /** + * Load an array (data) of length "length" as the posting list corresponding + * to id postid + * as a short array. + * + * Do not call this directly, call load() instead. + */ + size_t loadAsShortArray(const uint32_t postid, const uint32_t *data, + const uint32_t length) { + if (shortlistmap.find(postid) != shortlistmap.end()) + return 0; + if (recovbuffer.size() < length) + recovbuffer.resize(length); + vector *compressedbuffer = new vector(length + 1024); + size_t outlength = compressedbuffer->size(); + for (size_t i = 0; i < length; + ++i) // use the buffer because some codecs modify the input + recovbuffer[i] = data[i]; + codec.encodeArray(recovbuffer.data(), length, compressedbuffer->data(), + outlength); + compressedbuffer->resize(outlength); + compressedbuffer->shrink_to_fit(); + shortlistmap[postid] = shared_ptr>(compressedbuffer); + mapuncompsizes[postid] = length; + return compressedbuffer->size(); + } + + map> bitmapmap; + map>> shortlistmap; + map mapuncompsizes; + + const size_t mMaxId; // max value that can be stored in a list + const size_t threshold; //// 32 seems to be the recommended setting, no need + ///to change it? + + vector &recovbuffer; + + IntegerCODEC &codec; // how we compress the short lists + intersectionfunction Inter; +}; + +/** + * This is a version of HybM2 without compression (other than the bitmaps). + */ +class UncompressedHybM2 { +public: + UncompressedHybM2(intersectionfunction inter, uint32_t MaxId, + uint32_t th = 32) + : bitmapmap(), shortlistmap(), mapuncompsizes(), mMaxId(MaxId), + threshold(th), Inter(inter) {} + + /** + * Returns the *uncompressed* size of a given posting list (specified + * by ID). + */ + size_t getSizeInInts(uint32_t postId) { return mapuncompsizes[postId]; } + + /** + * Load an array (data) of length "length" as the posting list corresponding + * to id postid + * into the data structure. The data will be either converted to a bitmap or + * compressed. + */ + size_t load(const uint32_t postid, const uint32_t *data, + const uint32_t length) { + if (length * threshold >= mMaxId) + return loadAsBitmap(postid, data, length); + else + return loadAsShortArray(postid, data, length); + } + + /** + * Check whether we have a posting list corresponding to postid + */ + bool hasBeenLoaded(const uint32_t postid) { + return ((shortlistmap.find(postid) != shortlistmap.end()) || + (bitmapmap.find(postid) != bitmapmap.end())); + } + + /** + * Compute the total of the volume of of posting lists + * corresponding to a query. + */ + size_t computeUnpackVolume(const vector &ids) { + size_t answer = 0; + for (uint32_t id : ids) { + answer += mapuncompsizes[id]; + } + return answer; + } + + /** + * Compute the total of the intersection of of posting lists + * corresponding to a query, and output how many integers are + * in + * + * ids: the query as a set of posting ids + * out: write to result + * sizeout: will indicate how many integers were written to out. + * + * return unpack volume defined as the total volume of the posting + * lists that were needed to compute the intersection (which can + * be less than the total volume possible due to early abandoning). + */ + size_t intersect(const vector &ids, uint32_t *out, + size_t &sizeout) { + if (ids.empty()) { + sizeout = 0; + return 0; + } + vector>>> shortlists; + vector>> bitmaps; + // vector bitmapscard; + + for (uint32_t id : ids) { + if (shortlistmap.find(id) != shortlistmap.end()) + shortlists.push_back(make_pair(mapuncompsizes[id], shortlistmap[id])); + else { + assert(bitmapmap.find(id) != bitmapmap.end()); + bitmaps.push_back(make_pair(mapuncompsizes[id], bitmapmap[id])); + } + } + size_t unpackVolume = 0; + if (shortlists.empty()) { + if (bitmaps.size() == 1) { + sizeout = bitmaps.front().second->toInts(out); + unpackVolume += sizeout; + return unpackVolume; + } + + BoolArray answer(mMaxId); + bitmaps[0].second->intersect(*bitmaps[1].second, answer); + unpackVolume += bitmaps[0].first + bitmaps[1].first; + for (uint32_t i = 2; i < bitmaps.size(); ++i) { + answer.inplaceIntersect(*bitmaps[i].second); + unpackVolume += bitmaps[i].first; + } + sizeout = answer.toInts(out); + return unpackVolume; + } else { + sort(shortlists.begin(), shortlists.end()); + sort(bitmaps.begin(), bitmaps.end()); + assert(sizeout >= shortlists[0].second->size()); + sizeout = shortlists[0].second->size(); + unpackVolume += shortlists[0].second->size(); + assert(sizeout == shortlists[0].first); + // we have to make a copy because by convention the output is not directly + // from the index + const vector &firstvector = *shortlists[0].second; + for (uint32_t i = 0; i < firstvector.size(); ++i) + out[i] = firstvector[i]; + for (uint32_t i = 1; (sizeout > 0) && (i < shortlists.size()); ++i) { + unpackVolume += shortlists[i].first; + sizeout = Inter(out, sizeout, shortlists[i].second->data(), + shortlists[i].second->size(), out); + } + size_t pos = 0; + for (uint32_t i = 0; (sizeout > 0) && (i < bitmaps.size()); ++i) { + unpackVolume += bitmaps[i].first; + shared_ptr &ba = bitmaps[i].second; + pos = 0; + for (uint32_t i = 0; i < sizeout; ++i) { + if (!ba->get(out[i])) + continue; + else + out[pos++] = out[i]; + } + sizeout = pos; + } + return unpackVolume; + } + } + + ~UncompressedHybM2() {} + + /** + * Estimate of the volume of data used by this object. + */ + size_t storageInBytes() const { + size_t answer = 0; + for (auto i : bitmapmap) + answer += i.second->sizeInBytes(); + for (auto i : shortlistmap) + answer += i.second->size() * sizeof(uint32_t); + return answer; + } + +private: + /** + * Load an array (data) of length "length" as the posting list corresponding + * to id postid + * as a bitmap. + * + * Do not call this directly, call load() instead. + */ + size_t loadAsBitmap(const uint32_t postid, const uint32_t *data, + const uint32_t length) { + if (bitmapmap.find(postid) != bitmapmap.end()) + return 0; + BoolArray *ba = new BoolArray(mMaxId); + for (uint32_t k = 0; k < length; ++k) + ba->set(data[k]); + bitmapmap[postid] = shared_ptr(ba); + mapuncompsizes[postid] = length; + return ba->sizeInBytes() / sizeof(uint32_t); + } + + /** + * Load an array (data) of length "length" as the posting list corresponding + * to id postid + * as a short array. + * + * Do not call this directly, call load() instead. + */ + size_t loadAsShortArray(const uint32_t postid, const uint32_t *data, + const uint32_t length) { + if (shortlistmap.find(postid) != shortlistmap.end()) + return 0; + mapuncompsizes[postid] = length; + vector *compressedbuffer = + new vector(data, data + length); + shortlistmap[postid] = shared_ptr>(compressedbuffer); + return compressedbuffer->size(); + } + + map> bitmapmap; + map>> shortlistmap; + map mapuncompsizes; + + const size_t mMaxId; // max value that can be stored in a list + const size_t threshold; //// 32 seems to be the recommended setting, no need + ///to change it? + + intersectionfunction Inter; +}; + +/** + * This is a version of HybM2 with a skipping data structure akin to what is + * described + * by Culpepper and Moffat. We seem to get no gain from this approach. + */ +class SkippingHybM2 { +public: + SkippingHybM2(uint32_t MaxId, uint32_t th = 32, uint32_t BS = 8) + : bitmapmap(), shortlistmap(), mapuncompsizes(), mMaxId(MaxId), + threshold(th), BlockSizeLog(BS) {} + + /** + * Returns the *uncompressed* size of a given posting list (specified + * by ID). + */ + size_t getSizeInInts(uint32_t postId) { return mapuncompsizes[postId]; } + + /** + * Load an array (data) of length "length" as the posting list corresponding + * to id postid + * into the data structure. The data will be either converted to a bitmap or + * compressed. + */ + size_t load(const uint32_t postid, const uint32_t *data, + const uint32_t length) { + if (length * threshold >= mMaxId) + return loadAsBitmap(postid, data, length); + else + return loadAsShortArray(postid, data, length); + } + + /** + * Check whether we have a posting list corresponding to postid + */ + bool hasBeenLoaded(const uint32_t postid) { + return ((shortlistmap.find(postid) != shortlistmap.end()) || + (bitmapmap.find(postid) != bitmapmap.end())); + } + + /** + * Compute the total of the volume of of posting lists + * corresponding to a query. + */ + size_t computeUnpackVolume(const vector &ids) { + size_t answer = 0; + for (uint32_t id : ids) { + answer += mapuncompsizes[id]; + } + return answer; + } + + /** + * Compute the total of the intersection of of posting lists + * corresponding to a query, and output how many integers are + * in + * + * ids: the query as a set of posting ids + * out: write to result + * sizeout: will indicate how many integers were written to out. + * + * return unpack volume defined as the total volume of the posting + * lists that were needed to compute the intersection (which can + * be less than the total volume possible due to early abandoning). + */ + size_t intersect(const vector &ids, uint32_t *out, + size_t &sizeout) { + if (ids.empty()) { + sizeout = 0; + return 0; + } + vector>> shortlists; + vector>> bitmaps; + for (uint32_t id : ids) { + if (shortlistmap.find(id) != shortlistmap.end()) + shortlists.push_back(make_pair(mapuncompsizes[id], shortlistmap[id])); + else { + assert(bitmapmap.find(id) != bitmapmap.end()); + bitmaps.push_back(make_pair(mapuncompsizes[id], bitmapmap[id])); + } + } + size_t unpackVolume = 0; + if (shortlists.empty()) { + if (bitmaps.size() == 1) { + sizeout = bitmaps.front().second->toInts(out); + unpackVolume += sizeout; + return unpackVolume; + } + + BoolArray answer(mMaxId); + bitmaps[0].second->intersect(*bitmaps[1].second, answer); + unpackVolume += bitmaps[0].first + bitmaps[1].first; + for (uint32_t i = 2; i < bitmaps.size(); ++i) { + answer.inplaceIntersect(*bitmaps[i].second); + unpackVolume += bitmaps[i].first; + } + sizeout = answer.toInts(out); + return unpackVolume; + } else { + sort(shortlists.begin(), shortlists.end()); + sort(bitmaps.begin(), bitmaps.end()); + if (shortlists.size() == 1) { + sizeout = shortlists[0].second->decompress(out); + unpackVolume += shortlists[0].second->Length; + } else { + unpackVolume += shortlists[0].second->Length; + unpackVolume += shortlists[1].second->Length; + sizeout = shortlists[0].second->intersect(*shortlists[1].second, out); + for (uint32_t i = 2; (sizeout > 0) && (i < shortlists.size()); ++i) { + unpackVolume += shortlists[i].first; + sizeout = shortlists[i].second->intersect(out, sizeout, out); + } + } + size_t pos = 0; + for (uint32_t i = 0; (sizeout > 0) && (i < bitmaps.size()); ++i) { + unpackVolume += bitmaps[i].first; + shared_ptr &ba = bitmaps[i].second; + pos = 0; + for (uint32_t i = 0; i < sizeout; ++i) { + if (!ba->get(out[i])) + continue; + else + out[pos++] = out[i]; + } + sizeout = pos; + } + return unpackVolume; + } + } + + ~SkippingHybM2() {} + + /** + * Estimate of the volume of data used by this object. + */ + size_t storageInBytes() const { + size_t answer = 0; + for (auto i : bitmapmap) + answer += i.second->sizeInBytes(); + for (auto i : shortlistmap) + answer += i.second->storageInBytes(); + return answer; + } + +private: + /** + * Load an array (data) of length "length" as the posting list corresponding + * to id postid + * as a bitmap. + * + * Do not call this directly, call load() instead. + */ + size_t loadAsBitmap(const uint32_t postid, const uint32_t *data, + const uint32_t length) { + if (bitmapmap.find(postid) != bitmapmap.end()) + return 0; + BoolArray *ba = new BoolArray(mMaxId); + for (uint32_t k = 0; k < length; ++k) + ba->set(data[k]); + bitmapmap[postid] = shared_ptr(ba); + mapuncompsizes[postid] = length; + return ba->sizeInBytes() / sizeof(uint32_t); + } + + /** + * Load an array (data) of length "length" as the posting list corresponding + * to id postid + * as a short array. + * + * Do not call this directly, call load() instead. + */ + size_t loadAsShortArray(const uint32_t postid, const uint32_t *data, + const uint32_t length) { + if (shortlistmap.find(postid) != shortlistmap.end()) + return 0; + + Skipping *compressedbuffer = new Skipping(BlockSizeLog, data, length); + shortlistmap[postid] = shared_ptr(compressedbuffer); + return compressedbuffer->storageInBytes() / sizeof(uint32_t); + } + + map> bitmapmap; + map> shortlistmap; + map mapuncompsizes; + + const size_t mMaxId; // max value that can be stored in a list + const size_t threshold; //// 32 seems to be the recommended setting, no need + ///to change it? + uint32_t BlockSizeLog; +}; +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_HYBM2_H_ */ diff --git a/include/SIMDCompressionAndIntersection/integratedbitpacking.h b/include/SIMDCompressionAndIntersection/integratedbitpacking.h new file mode 100644 index 0000000..bf18c3b --- /dev/null +++ b/include/SIMDCompressionAndIntersection/integratedbitpacking.h @@ -0,0 +1,216 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire, http://lemire.me/en/ + */ +#ifndef SIMDCompressionAndIntersection_INTEGRATEDBITPACKING +#define SIMDCompressionAndIntersection_INTEGRATEDBITPACKING +#include +#include "platform.h" + +namespace SIMDCompressionLib { + +void __integratedfastunpack0(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack1(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack2(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack3(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack4(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack5(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack6(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack7(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack8(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack9(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack10(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack11(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack12(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack13(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack14(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack15(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack16(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack17(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack18(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack19(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack20(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack21(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack22(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack23(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack24(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack25(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack26(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack27(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack28(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack29(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack30(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack31(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastunpack32(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); + +void __integratedfastpack0(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack1(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack2(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack3(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack4(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack5(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack6(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack7(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack8(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack9(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack10(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack11(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack12(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack13(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack14(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack15(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack16(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack17(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack18(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack19(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack20(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack21(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack22(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack23(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack24(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack25(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack26(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack27(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack28(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack29(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack30(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack31(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); +void __integratedfastpack32(const uint32_t initoffset, + const uint32_t *__restrict__ in, + uint32_t *__restrict__ out); + +} // namespace SIMDCompressionLib + +#endif // SIMDCompressionAndIntersection_INTEGRATEDBITPACKING diff --git a/include/SIMDCompressionAndIntersection/intersection.h b/include/SIMDCompressionAndIntersection/intersection.h new file mode 100644 index 0000000..fb89d7b --- /dev/null +++ b/include/SIMDCompressionAndIntersection/intersection.h @@ -0,0 +1,108 @@ + + +#ifndef SIMDCompressionAndIntersection_INTERSECTION_H_ +#define SIMDCompressionAndIntersection_INTERSECTION_H_ + +#include + +namespace SIMDCompressionLib { + +using namespace std; +/* + * Given two arrays, this writes the intersection to out. Returns the + * cardinality of the intersection. + */ +typedef size_t (*intersectionfunction)(const uint32_t *set1, + const size_t length1, + const uint32_t *set2, + const size_t length2, uint32_t *out); + +/* + * Given two arrays, this writes the intersection to out. Returns the + * cardinality of the intersection. + * + * This is a mix of very fast vectorized intersection algorithms, several + * designed by N. Kurz, with adaptations by D. Lemire. + */ +size_t SIMDintersection(const uint32_t *set1, const size_t length1, + const uint32_t *set2, const size_t length2, + uint32_t *out); + +#ifdef __AVX2__ +#include + +/* + * Straight port of SIMDintersection to AVX2. + */ +size_t SIMDintersection_avx2(const uint32_t *set1, const size_t length1, + const uint32_t *set2, const size_t length2, + uint32_t *out); + +#endif +/* + * Given two arrays, this writes the intersection to out. Returns the + * cardinality of the intersection. + * + * This is a well-written, but otherwise unsophisticated function. + * Written by N. Kurz. + */ +size_t nate_scalar(const uint32_t *set1, const size_t length1, + const uint32_t *set2, const size_t length2, uint32_t *out); + +/* + * Given two arrays, this writes the intersection to out. Returns the + * cardinality of the intersection. + * + * This applies a state-of-the-art algorithm. First coded by O. Kaser, adapted + * by D. Lemire. + */ +size_t onesidedgallopingintersection(const uint32_t *smallset, + const size_t smalllength, + const uint32_t *largeset, + const size_t largelength, uint32_t *out); + +class IntersectionFactory { +public: + static std::map intersection_schemes; + + static vector allNames() { + vector ans; + for (auto i = intersection_schemes.begin(); i != intersection_schemes.end(); + ++i) { + ans.push_back(i->first); + } + return ans; + } + + static string getName(intersectionfunction v) { + for (auto i = intersection_schemes.begin(); i != intersection_schemes.end(); + ++i) { + if (i->second == v) + return i->first; + } + return "UNKNOWN"; + } + + static bool valid(string name) { + return (intersection_schemes.find(name) != intersection_schemes.end()); + } + + static intersectionfunction getFromName(string name) { + if (intersection_schemes.find(name) == intersection_schemes.end()) { + cerr << "name " << name << " does not refer to an intersection procedure." + << endl; + cerr << "possible choices:" << endl; + for (auto i = intersection_schemes.begin(); + i != intersection_schemes.end(); ++i) { + cerr << static_cast(i->first) + << endl; // useless cast, but just to be clear + } + return NULL; + } + return intersection_schemes[name]; + } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_INTERSECTION_H_ */ diff --git a/include/SIMDCompressionAndIntersection/mersenne.h b/include/SIMDCompressionAndIntersection/mersenne.h new file mode 100644 index 0000000..f00dcb1 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/mersenne.h @@ -0,0 +1,93 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + */ + +#ifndef SIMDCompressionAndIntersection_MERSENNE_H_ +#define SIMDCompressionAndIntersection_MERSENNE_H_ + +#include "common.h" +#include "util.h" + +namespace SIMDCompressionLib { + +/** + * Mersenne twister - random number generator. + * Generate uniform distribution of 32 bit integers with the MT19937 algorithm. + * source: http://bannister.us/weblog/?s=Mersenne + */ +class ZRandom { + +public: + enum { N = 624, M = 397 }; + unsigned int MT[N + 1]; + unsigned int *map[N]; + int nValues; + + ZRandom(unsigned int iSeed = 20070102); + void seed(unsigned iSeed); + unsigned int getValue(); + unsigned int getValue(const uint32_t MaxValue); + double getDouble(); + bool test(const double p); +}; + +ZRandom::ZRandom(unsigned iSeed) : nValues(0) { seed(iSeed); } + +void ZRandom::seed(unsigned iSeed) { + nValues = 0; + // Seed the array used in random number generation. + MT[0] = iSeed; + for (int i = 1; i < N; ++i) { + MT[i] = 1 + (69069 * MT[i - 1]); + } + // Compute map once to avoid % in inner loop. + for (int i = 0; i < N; ++i) { + map[i] = MT + ((i + M) % N); + } +} + +inline bool ZRandom::test(const double p) { return getDouble() <= p; } +inline double ZRandom::getDouble() { + return double(getValue()) * (1.0 / 4294967296.0); +} + +unsigned int ZRandom::getValue(const uint32_t MaxValue) { + unsigned int used = MaxValue; + used |= used >> 1; + used |= used >> 2; + used |= used >> 4; + used |= used >> 8; + used |= used >> 16; + + // Draw numbers until one is found in [0,n] + unsigned int i; + do + i = getValue() & used; // toss unused bits to shorten search + while (i > MaxValue); + return i; +} + +unsigned int ZRandom::getValue() { + if (0 == nValues) { + MT[N] = MT[0]; + for (int i = 0; i < N; ++i) { + unsigned y = (0x80000000 & MT[i]) | (0x7FFFFFFF & MT[i + 1]); + unsigned v = *(map[i]) ^ (y >> 1); + if (1 & y) + v ^= 2567483615; + MT[i] = v; + } + nValues = N; + } + unsigned y = MT[N - nValues--]; + y ^= y >> 11; + y ^= static_cast((y << 7) & 2636928640); + y ^= static_cast((y << 15) & 4022730752); + y ^= y >> 18; + return y; +} + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_MERSENNE_H_ */ diff --git a/include/SIMDCompressionAndIntersection/platform.h b/include/SIMDCompressionAndIntersection/platform.h new file mode 100644 index 0000000..309aef0 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/platform.h @@ -0,0 +1,44 @@ +/* platform.h: Cross-platform macros and compatibility shims. */ + +#pragma once + +#include + +#if defined(_MSC_VER) +#define ALWAYS_INLINE __forceinline +#define CONST_FUNCTION +#define PURE_FUNCTION +#define __restrict__ __restrict +#define SIMDCOMP_ALIGNED(x) __declspec(align(x)) +#else +#if defined(__GNUC__) +#define ALWAYS_INLINE __attribute__((always_inline)) inline +#define CONST_FUNCTION __attribute__((const)) +#define PURE_FUNCTION __attribute__((pure)) +#define SIMDCOMP_ALIGNED(x) __attribute__((aligned(x))) +#endif +#endif + +#ifdef _MSC_VER +#include + +uint32_t __inline __builtin_clz(uint32_t value) { + unsigned long leading_zero = 0; + return _BitScanReverse(&leading_zero, value) == 0 ? 0 : (31 - leading_zero); +} + +uint32_t __inline __builtin_ctz(uint32_t value) { + unsigned long trailing_zero = 0; + return _BitScanForward(&trailing_zero, value) == 0 ? 32 : trailing_zero; +} + +uint32_t __inline __builtin_ctzl(uint64_t value) { +#ifdef _M_X64 + unsigned long trailing_zero = 0; + return _BitScanForward64(&trailing_zero, value) == 0 ? 64 : trailing_zero; +#else + return ((value & 0xFFFFFFFF) == 0) ? (__builtin_ctz(value >> 32) + 32) + : __builtin_ctz(value & 0xFFFFFFFF); +#endif +} +#endif diff --git a/include/SIMDCompressionAndIntersection/simdbinarypacking.h b/include/SIMDCompressionAndIntersection/simdbinarypacking.h new file mode 100644 index 0000000..879064b --- /dev/null +++ b/include/SIMDCompressionAndIntersection/simdbinarypacking.h @@ -0,0 +1,403 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire, http://lemire.me/en/ + */ + +#ifndef SIMDCompressionAndIntersection_SIMDBINARYPACKING_H_ +#define SIMDCompressionAndIntersection_SIMDBINARYPACKING_H_ + +#include "codecs.h" +#include "simdbitpackinghelpers.h" +#include "util.h" + +namespace SIMDCompressionLib { + +extern "C" { + +/* searches "bit" 128-bit vectors from "in" (= 128 encoded integers) for the +* first encoded uint32 value + * which is >= |key|, and returns its position. It is assumed that the values + * stored are in sorted order. + * The encoded key is stored in "*presult". If no value is larger or equal to +* the key, +* 128 is returned */ +int simdsearchd1(__m128i *initOffset, const __m128i *in, uint32_t bit, + uint32_t key, uint32_t *presult); + +/** + * Simply scan, updating initOffset as it proceeds. + */ +void simdscand1(__m128i *initOffset, const __m128i *in, uint32_t bit); + +/* returns the value stored at the specified "slot". */ +uint32_t simdselectd1(__m128i *initOffset, const __m128i *in, uint32_t bit, + int slot); +} + +template struct SIMDBlockPacker { + typedef SIMDDeltaProcessor DeltaProcessor; + static void unpackblock(const uint32_t *in, uint32_t *out, const uint32_t bit, + __m128i &initoffset) { + if (ArrayDispatch) + ArrayDispatch::SIMDunpack(reinterpret_cast(in), out, + bit); + else + simdunpack(reinterpret_cast(in), out, bit); + if (bit < 32) { + initoffset = DeltaProcessor::runPrefixSum(initoffset, out); + } else { + initoffset = + MM_LOAD_SI_128(reinterpret_cast<__m128i *>(out + SIMDBlockSize - 4)); + } + } + + static uint32_t maxbits(const uint32_t *in, __m128i &initoffset) { + const __m128i *pin = reinterpret_cast(in); + __m128i newvec = MM_LOAD_SI_128(pin); + __m128i accumulator = DeltaHelper::Delta(newvec, initoffset); + __m128i oldvec = newvec; + for (uint32_t k = 1; 4 * k < SIMDBlockSize; ++k) { + newvec = MM_LOAD_SI_128(pin + k); + accumulator = + _mm_or_si128(accumulator, DeltaHelper::Delta(newvec, oldvec)); + oldvec = newvec; + } + initoffset = oldvec; + return maxbitas32int(accumulator); + } + + static void packblockwithoutmask(uint32_t *in, uint32_t *out, + const uint32_t bit, __m128i &initoffset) { + __m128i nextoffset = + MM_LOAD_SI_128(reinterpret_cast<__m128i *>(in + SIMDBlockSize - 4)); + if (bit < 32) + DeltaProcessor::runDelta(initoffset, in); + if (ArrayDispatch) + ArrayDispatch::SIMDpackwithoutmask(in, reinterpret_cast<__m128i *>(out), + bit); + else + simdpackwithoutmask(in, reinterpret_cast<__m128i *>(out), bit); + initoffset = nextoffset; + } + + static string name() { + if (ArrayDispatch) + return string("SIMDBlockPackerAD+") + DeltaHelper::name(); + else + return string("SIMDBlockPacker+") + DeltaHelper::name(); + } +}; + +template +struct SIMDIntegratedBlockPacker { + + static void unpackblock(const uint32_t *in, uint32_t *out, const uint32_t bit, + __m128i &initoffset) { + if (ArrayDispatch) + initoffset = IntegratedArrayDispatch::SIMDiunpack( + initoffset, reinterpret_cast(in), out, bit); + else + initoffset = SIMDiunpack( + initoffset, reinterpret_cast(in), out, bit); + } + + static uint32_t maxbits(const uint32_t *in, __m128i &initoffset) { + const __m128i *pin = reinterpret_cast(in); + __m128i newvec = MM_LOAD_SI_128(pin); + __m128i accumulator = DeltaHelper::Delta(newvec, initoffset); + __m128i oldvec = newvec; + for (uint32_t k = 1; 4 * k < SIMDBlockSize; ++k) { + newvec = MM_LOAD_SI_128(pin + k); + accumulator = + _mm_or_si128(accumulator, DeltaHelper::Delta(newvec, oldvec)); + oldvec = newvec; + } + initoffset = oldvec; + return maxbitas32int(accumulator); + } + + static void packblockwithoutmask(uint32_t *in, uint32_t *out, + const uint32_t bit, __m128i &initoffset) { + __m128i nextoffset = + MM_LOAD_SI_128(reinterpret_cast<__m128i *>(in + SIMDBlockSize - 4)); + if (ArrayDispatch) + IntegratedArrayDispatch::SIMDipackwithoutmask( + initoffset, in, reinterpret_cast<__m128i *>(out), bit); + else + SIMDipackwithoutmask(initoffset, in, + reinterpret_cast<__m128i *>(out), bit); + initoffset = nextoffset; + } + + static string name() { + if (ArrayDispatch) + return string("SIMDIntegratedBlockPackerAD+") + DeltaHelper::name(); + else + return string("SIMDIntegratedBlockPacker+") + DeltaHelper::name(); + } +}; + +/** + * + * + * Code data in miniblocks of 128 integers. + * To preserve alignment, we regroup + * 8 such miniblocks into a block of 8 * 128 = 1024 + * integers. + * + */ +template class SIMDBinaryPacking : public IntegerCODEC { +public: +#ifdef USE_ALIGNED + static const uint32_t CookiePadder = 123456; // just some made up number +#endif + static const uint32_t MiniBlockSize = 128; + static const uint32_t HowManyMiniBlocks = 16; + static const uint32_t BlockSize = + MiniBlockSize; // HowManyMiniBlocks * MiniBlockSize; + + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + checkifdivisibleby(length, BlockSize); + const uint32_t *const initout(out); +#ifdef USE_ALIGNED + if (needPaddingTo128Bits(out) or needPaddingTo128Bits(in)) + throw std::runtime_error( + "alignment issue: pointers should be aligned on 128-bit boundaries"); +#endif + *out++ = static_cast(length); +#ifdef USE_ALIGNED + while (needPaddingTo128Bits(out)) + *out++ = CookiePadder; +#endif + uint32_t Bs[HowManyMiniBlocks]; + __m128i init = _mm_set1_epi32(0); + const uint32_t *const final = in + length; + for (; in + HowManyMiniBlocks * MiniBlockSize <= final; + in += HowManyMiniBlocks * MiniBlockSize) { + __m128i tmpinit = init; + for (uint32_t i = 0; i < HowManyMiniBlocks; ++i) { + Bs[i] = BlockPacker::maxbits(in + i * MiniBlockSize, tmpinit); + } + *out++ = (Bs[0] << 24) | (Bs[1] << 16) | (Bs[2] << 8) | Bs[3]; + *out++ = (Bs[4] << 24) | (Bs[5] << 16) | (Bs[6] << 8) | Bs[7]; + *out++ = (Bs[8] << 24) | (Bs[9] << 16) | (Bs[10] << 8) | Bs[11]; + *out++ = (Bs[12] << 24) | (Bs[13] << 16) | (Bs[14] << 8) | Bs[15]; + for (uint32_t i = 0; i < HowManyMiniBlocks; ++i) { + BlockPacker::packblockwithoutmask(in + i * MiniBlockSize, out, Bs[i], + init); + out += MiniBlockSize / 32 * Bs[i]; + } + } + if (in < final) { + const size_t howmany = (final - in) / MiniBlockSize; + __m128i tmpinit = init; + memset(&Bs[0], 0, HowManyMiniBlocks * sizeof(uint32_t)); + for (uint32_t i = 0; i < howmany; ++i) { + Bs[i] = BlockPacker::maxbits(in + i * MiniBlockSize, tmpinit); + } + *out++ = (Bs[0] << 24) | (Bs[1] << 16) | (Bs[2] << 8) | Bs[3]; + *out++ = (Bs[4] << 24) | (Bs[5] << 16) | (Bs[6] << 8) | Bs[7]; + *out++ = (Bs[8] << 24) | (Bs[9] << 16) | (Bs[10] << 8) | Bs[11]; + *out++ = (Bs[12] << 24) | (Bs[13] << 16) | (Bs[14] << 8) | Bs[15]; + for (uint32_t i = 0; i < howmany; ++i) { + BlockPacker::packblockwithoutmask(in + i * MiniBlockSize, out, Bs[i], + init); + out += MiniBlockSize / 32 * Bs[i]; + } + in += howmany * MiniBlockSize; + assert(in == final); + } + nvalue = out - initout; + } + + const uint32_t *decodeArray(const uint32_t *in, const size_t /*length*/, + uint32_t *out, size_t &nvalue) { +#ifdef USE_ALIGNED + if (needPaddingTo128Bits(out) or needPaddingTo128Bits(in)) + throw std::runtime_error( + "alignment issue: pointers should be aligned on 128-bit boundaries"); +#endif + const uint32_t actuallength = *in++; +#ifdef USE_ALIGNED + while (needPaddingTo128Bits(in)) { + if (in[0] != CookiePadder) + throw logic_error("SIMDBinaryPacking alignment issue."); + ++in; + } +#endif + const uint32_t *const initout(out); + uint32_t Bs[HowManyMiniBlocks]; + __m128i init = _mm_set1_epi32(0); + for (; out < initout + + actuallength / (HowManyMiniBlocks * MiniBlockSize) * + HowManyMiniBlocks * MiniBlockSize; + out += HowManyMiniBlocks * MiniBlockSize) { + for (uint32_t i = 0; i < 4; ++i, ++in) { + Bs[0 + 4 * i] = static_cast(in[0] >> 24); + Bs[1 + 4 * i] = static_cast(in[0] >> 16); + Bs[2 + 4 * i] = static_cast(in[0] >> 8); + Bs[3 + 4 * i] = static_cast(in[0]); + } + for (uint32_t i = 0; i < HowManyMiniBlocks; ++i) { + BlockPacker::unpackblock(in, out + i * MiniBlockSize, Bs[i], init); + in += MiniBlockSize / 32 * Bs[i]; + } + } + + if (out < initout + actuallength) { + const size_t howmany = (initout + actuallength - out) / MiniBlockSize; + for (uint32_t i = 0; i < 4; ++i, ++in) { + Bs[0 + 4 * i] = static_cast(in[0] >> 24); + Bs[1 + 4 * i] = static_cast(in[0] >> 16); + Bs[2 + 4 * i] = static_cast(in[0] >> 8); + Bs[3 + 4 * i] = static_cast(in[0]); + } + for (uint32_t i = 0; i < howmany; ++i) { + BlockPacker::unpackblock(in, out + i * MiniBlockSize, Bs[i], init); + in += MiniBlockSize / 32 * Bs[i]; + } + out += howmany * MiniBlockSize; + assert(out == initout + actuallength); + } + nvalue = out - initout; + return in; + } + + // Returns a decompressed value in an encoded array + // could be greatly optimized in the non-differential coding case: currently + // just for delta coding + // WARNING: THIS IMPLEMENTATION WILL ONLY PROVIDE THE CORRECT RESULT + // WHEN USING REGULAR (D1) DIFFERENTIAL CODING. TODO: Generalize the + // support. TODO: Should check the type. + uint32_t select(uint32_t *in, size_t index) { +#ifdef USE_ALIGNED + if (needPaddingTo128Bits(in)) + throw std::runtime_error( + "alignment issue: pointers should be aligned on 128-bit boundaries"); +#endif + const uint32_t actuallength = *in++; +#ifdef USE_ALIGNED + while (needPaddingTo128Bits(in)) { + if (in[0] != CookiePadder) + throw logic_error("SIMDBinaryPacking alignment issue."); + ++in; + } +#endif + uint32_t Bs[HowManyMiniBlocks]; + __m128i init = _mm_set1_epi32(0); + size_t runningindex = 0; + for (; runningindex < actuallength / (HowManyMiniBlocks * MiniBlockSize) * + HowManyMiniBlocks * MiniBlockSize;) { + for (uint32_t i = 0; i < 4; ++i, ++in) { + Bs[0 + 4 * i] = static_cast(in[0] >> 24); + Bs[1 + 4 * i] = static_cast(in[0] >> 16); + Bs[2 + 4 * i] = static_cast(in[0] >> 8); + Bs[3 + 4 * i] = static_cast(in[0]); + } + for (uint32_t i = 0; i < HowManyMiniBlocks; ++i) { + if (runningindex + 128 > index) { + return simdselectd1(&init, (const __m128i *)in, Bs[i], + static_cast(index - runningindex)); + } + simdscand1(&init, (const __m128i *)in, Bs[i]); + runningindex += MiniBlockSize; + in += MiniBlockSize / 32 * Bs[i]; + } + } + + if (runningindex < actuallength) { + const size_t howmany = (actuallength - runningindex) / MiniBlockSize; + for (uint32_t i = 0; i < 4; ++i, ++in) { + Bs[0 + 4 * i] = static_cast(in[0] >> 24); + Bs[1 + 4 * i] = static_cast(in[0] >> 16); + Bs[2 + 4 * i] = static_cast(in[0] >> 8); + Bs[3 + 4 * i] = static_cast(in[0]); + } + for (uint32_t i = 0; i < howmany; ++i) { + if (runningindex + 128 > index) { + return simdselectd1(&init, (const __m128i *)in, Bs[i], + static_cast(index - runningindex)); + } + simdscand1(&init, (const __m128i *)in, Bs[i]); + runningindex += MiniBlockSize; + in += MiniBlockSize / 32 * Bs[i]; + } + } + return static_cast(runningindex); + } + + // Performs a lower bound find in the encoded array. + // Returns the index + // WARNING: THIS IMPLEMENTATION WILL ONLY PROVIDE THE CORRECT RESULT + // WHEN USING REGULAR (D1) DIFFERENTIAL CODING. TODO: Generalize the + // support. TODO: Should check the type. + size_t findLowerBound(const uint32_t *in, const size_t /*length*/, + uint32_t key, uint32_t *presult) { +#ifdef USE_ALIGNED + if (needPaddingTo128Bits(in)) + throw std::runtime_error( + "alignment issue: pointers should be aligned on 128-bit boundaries"); +#endif + const uint32_t actuallength = *in++; +#ifdef USE_ALIGNED + while (needPaddingTo128Bits(in)) { + if (in[0] != CookiePadder) + throw logic_error("SIMDBinaryPacking alignment issue."); + ++in; + } +#endif + uint32_t Bs[HowManyMiniBlocks]; + __m128i init = _mm_set1_epi32(0); + size_t runningindex = 0; + for (; runningindex < actuallength / (HowManyMiniBlocks * MiniBlockSize) * + HowManyMiniBlocks * MiniBlockSize;) { + for (uint32_t i = 0; i < 4; ++i, ++in) { + Bs[0 + 4 * i] = static_cast(in[0] >> 24); + Bs[1 + 4 * i] = static_cast(in[0] >> 16); + Bs[2 + 4 * i] = static_cast(in[0] >> 8); + Bs[3 + 4 * i] = static_cast(in[0]); + } + for (uint32_t i = 0; i < HowManyMiniBlocks; ++i) { + size_t index = + simdsearchd1(&init, (const __m128i *)in, Bs[i], key, presult); + runningindex += index; + if (index < MiniBlockSize) + return runningindex; + in += MiniBlockSize / 32 * Bs[i]; + } + } + + if (runningindex < actuallength) { + const size_t howmany = (actuallength - runningindex) / MiniBlockSize; + for (uint32_t i = 0; i < 4; ++i, ++in) { + Bs[0 + 4 * i] = static_cast(in[0] >> 24); + Bs[1 + 4 * i] = static_cast(in[0] >> 16); + Bs[2 + 4 * i] = static_cast(in[0] >> 8); + Bs[3 + 4 * i] = static_cast(in[0]); + } + for (uint32_t i = 0; i < howmany; ++i) { + size_t index = + simdsearchd1(&init, (const __m128i *)in, Bs[i], key, presult); + runningindex += index; + if (index < MiniBlockSize) + return runningindex; + in += MiniBlockSize / 32 * Bs[i]; + } + } + return runningindex; + } + + string name() const { + ostringstream convert; + convert << "SIMDBinaryPacking" + << "With" << BlockPacker::name() << MiniBlockSize; + return convert.str(); + } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_SIMDBINARYPACKING_H_ */ diff --git a/include/SIMDCompressionAndIntersection/simdbitpacking.h b/include/SIMDCompressionAndIntersection/simdbitpacking.h new file mode 100644 index 0000000..f3a4e3d --- /dev/null +++ b/include/SIMDCompressionAndIntersection/simdbitpacking.h @@ -0,0 +1,117 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire + */ +#ifndef SIMDCompressionAndIntersection_SIMDBITPACKING_H_ +#define SIMDCompressionAndIntersection_SIMDBITPACKING_H_ + +#include "common.h" + +namespace SIMDCompressionLib { + +void __SIMD_fastunpack1(const __m128i *, uint32_t *); +void __SIMD_fastunpack2(const __m128i *, uint32_t *); +void __SIMD_fastunpack3(const __m128i *, uint32_t *); +void __SIMD_fastunpack4(const __m128i *, uint32_t *); +void __SIMD_fastunpack5(const __m128i *, uint32_t *); +void __SIMD_fastunpack6(const __m128i *, uint32_t *); +void __SIMD_fastunpack7(const __m128i *, uint32_t *); +void __SIMD_fastunpack8(const __m128i *, uint32_t *); +void __SIMD_fastunpack9(const __m128i *, uint32_t *); +void __SIMD_fastunpack10(const __m128i *, uint32_t *); +void __SIMD_fastunpack11(const __m128i *, uint32_t *); +void __SIMD_fastunpack12(const __m128i *, uint32_t *); +void __SIMD_fastunpack13(const __m128i *, uint32_t *); +void __SIMD_fastunpack14(const __m128i *, uint32_t *); +void __SIMD_fastunpack15(const __m128i *, uint32_t *); +void __SIMD_fastunpack16(const __m128i *, uint32_t *); +void __SIMD_fastunpack17(const __m128i *, uint32_t *); +void __SIMD_fastunpack18(const __m128i *, uint32_t *); +void __SIMD_fastunpack19(const __m128i *, uint32_t *); +void __SIMD_fastunpack20(const __m128i *, uint32_t *); +void __SIMD_fastunpack21(const __m128i *, uint32_t *); +void __SIMD_fastunpack22(const __m128i *, uint32_t *); +void __SIMD_fastunpack23(const __m128i *, uint32_t *); +void __SIMD_fastunpack24(const __m128i *, uint32_t *); +void __SIMD_fastunpack25(const __m128i *, uint32_t *); +void __SIMD_fastunpack26(const __m128i *, uint32_t *); +void __SIMD_fastunpack27(const __m128i *, uint32_t *); +void __SIMD_fastunpack28(const __m128i *, uint32_t *); +void __SIMD_fastunpack29(const __m128i *, uint32_t *); +void __SIMD_fastunpack30(const __m128i *, uint32_t *); +void __SIMD_fastunpack31(const __m128i *, uint32_t *); +void __SIMD_fastunpack32(const __m128i *, uint32_t *); + +void __SIMD_fastpackwithoutmask0(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask1(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask2(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask3(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask4(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask5(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask6(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask7(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask8(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask9(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask10(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask11(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask12(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask13(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask14(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask15(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask16(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask17(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask18(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask19(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask20(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask21(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask22(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask23(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask24(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask25(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask26(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask27(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask28(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask29(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask30(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask31(const uint32_t *, __m128i *); +void __SIMD_fastpackwithoutmask32(const uint32_t *, __m128i *); + +void __SIMD_fastpack0(const uint32_t *, __m128i *); +void __SIMD_fastpack1(const uint32_t *, __m128i *); +void __SIMD_fastpack2(const uint32_t *, __m128i *); +void __SIMD_fastpack3(const uint32_t *, __m128i *); +void __SIMD_fastpack4(const uint32_t *, __m128i *); +void __SIMD_fastpack5(const uint32_t *, __m128i *); +void __SIMD_fastpack6(const uint32_t *, __m128i *); +void __SIMD_fastpack7(const uint32_t *, __m128i *); +void __SIMD_fastpack8(const uint32_t *, __m128i *); +void __SIMD_fastpack9(const uint32_t *, __m128i *); +void __SIMD_fastpack10(const uint32_t *, __m128i *); +void __SIMD_fastpack11(const uint32_t *, __m128i *); +void __SIMD_fastpack12(const uint32_t *, __m128i *); +void __SIMD_fastpack13(const uint32_t *, __m128i *); +void __SIMD_fastpack14(const uint32_t *, __m128i *); +void __SIMD_fastpack15(const uint32_t *, __m128i *); +void __SIMD_fastpack16(const uint32_t *, __m128i *); +void __SIMD_fastpack17(const uint32_t *, __m128i *); +void __SIMD_fastpack18(const uint32_t *, __m128i *); +void __SIMD_fastpack19(const uint32_t *, __m128i *); +void __SIMD_fastpack20(const uint32_t *, __m128i *); +void __SIMD_fastpack21(const uint32_t *, __m128i *); +void __SIMD_fastpack22(const uint32_t *, __m128i *); +void __SIMD_fastpack23(const uint32_t *, __m128i *); +void __SIMD_fastpack24(const uint32_t *, __m128i *); +void __SIMD_fastpack25(const uint32_t *, __m128i *); +void __SIMD_fastpack26(const uint32_t *, __m128i *); +void __SIMD_fastpack27(const uint32_t *, __m128i *); +void __SIMD_fastpack28(const uint32_t *, __m128i *); +void __SIMD_fastpack29(const uint32_t *, __m128i *); +void __SIMD_fastpack30(const uint32_t *, __m128i *); +void __SIMD_fastpack31(const uint32_t *, __m128i *); +void __SIMD_fastpack32(const uint32_t *, __m128i *); + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_SIMDBITPACKING_H_ */ diff --git a/include/SIMDCompressionAndIntersection/simdbitpackinghelpers.h b/include/SIMDCompressionAndIntersection/simdbitpackinghelpers.h new file mode 100644 index 0000000..27c66fe --- /dev/null +++ b/include/SIMDCompressionAndIntersection/simdbitpackinghelpers.h @@ -0,0 +1,1156 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Leonid Boytsov, Nathan Kurz and Daniel Lemire + */ + +#ifndef SIMDCompressionAndIntersection_SIMD_BITPACKING_HELPERS_H_ +#define SIMDCompressionAndIntersection_SIMD_BITPACKING_HELPERS_H_ + +#include "common.h" +#include "simdbitpacking.h" +#include "usimdbitpacking.h" +#include "simdintegratedbitpacking.h" +#include "delta.h" +#include "util.h" + +namespace SIMDCompressionLib { + +const size_t SIMDBlockSize = 128; + +void SIMD_nullunpacker32(const __m128i *__restrict__, + uint32_t *__restrict__ out) { + memset(out, 0, 32 * 4 * 4); +} +void uSIMD_nullunpacker32(const __m128i *__restrict__, + uint32_t *__restrict__ out) { + memset(out, 0, 32 * 4 * 4); +} + +void simdunpack(const __m128i *__restrict__ in, uint32_t *__restrict__ out, + const uint32_t bit) { + switch (bit) { + case 0: + SIMD_nullunpacker32(in, out); + return; + + case 1: + __SIMD_fastunpack1(in, out); + return; + + case 2: + __SIMD_fastunpack2(in, out); + return; + + case 3: + __SIMD_fastunpack3(in, out); + return; + + case 4: + __SIMD_fastunpack4(in, out); + return; + + case 5: + __SIMD_fastunpack5(in, out); + return; + + case 6: + __SIMD_fastunpack6(in, out); + return; + + case 7: + __SIMD_fastunpack7(in, out); + return; + + case 8: + __SIMD_fastunpack8(in, out); + return; + + case 9: + __SIMD_fastunpack9(in, out); + return; + + case 10: + __SIMD_fastunpack10(in, out); + return; + + case 11: + __SIMD_fastunpack11(in, out); + return; + + case 12: + __SIMD_fastunpack12(in, out); + return; + + case 13: + __SIMD_fastunpack13(in, out); + return; + + case 14: + __SIMD_fastunpack14(in, out); + return; + + case 15: + __SIMD_fastunpack15(in, out); + return; + + case 16: + __SIMD_fastunpack16(in, out); + return; + + case 17: + __SIMD_fastunpack17(in, out); + return; + + case 18: + __SIMD_fastunpack18(in, out); + return; + + case 19: + __SIMD_fastunpack19(in, out); + return; + + case 20: + __SIMD_fastunpack20(in, out); + return; + + case 21: + __SIMD_fastunpack21(in, out); + return; + + case 22: + __SIMD_fastunpack22(in, out); + return; + + case 23: + __SIMD_fastunpack23(in, out); + return; + + case 24: + __SIMD_fastunpack24(in, out); + return; + + case 25: + __SIMD_fastunpack25(in, out); + return; + + case 26: + __SIMD_fastunpack26(in, out); + return; + + case 27: + __SIMD_fastunpack27(in, out); + return; + + case 28: + __SIMD_fastunpack28(in, out); + return; + + case 29: + __SIMD_fastunpack29(in, out); + return; + + case 30: + __SIMD_fastunpack30(in, out); + return; + + case 31: + __SIMD_fastunpack31(in, out); + return; + + case 32: + __SIMD_fastunpack32(in, out); + return; + + default: + break; + } + throw std::logic_error("number of bits is unsupported"); +} + +/*assumes that integers fit in the prescribed number of bits*/ +void simdpackwithoutmask(const uint32_t *__restrict__ in, + __m128i *__restrict__ out, const uint32_t bit) { + switch (bit) { + case 0: + return; + + case 1: + __SIMD_fastpackwithoutmask1(in, out); + return; + + case 2: + __SIMD_fastpackwithoutmask2(in, out); + return; + + case 3: + __SIMD_fastpackwithoutmask3(in, out); + return; + + case 4: + __SIMD_fastpackwithoutmask4(in, out); + return; + + case 5: + __SIMD_fastpackwithoutmask5(in, out); + return; + + case 6: + __SIMD_fastpackwithoutmask6(in, out); + return; + + case 7: + __SIMD_fastpackwithoutmask7(in, out); + return; + + case 8: + __SIMD_fastpackwithoutmask8(in, out); + return; + + case 9: + __SIMD_fastpackwithoutmask9(in, out); + return; + + case 10: + __SIMD_fastpackwithoutmask10(in, out); + return; + + case 11: + __SIMD_fastpackwithoutmask11(in, out); + return; + + case 12: + __SIMD_fastpackwithoutmask12(in, out); + return; + + case 13: + __SIMD_fastpackwithoutmask13(in, out); + return; + + case 14: + __SIMD_fastpackwithoutmask14(in, out); + return; + + case 15: + __SIMD_fastpackwithoutmask15(in, out); + return; + + case 16: + __SIMD_fastpackwithoutmask16(in, out); + return; + + case 17: + __SIMD_fastpackwithoutmask17(in, out); + return; + + case 18: + __SIMD_fastpackwithoutmask18(in, out); + return; + + case 19: + __SIMD_fastpackwithoutmask19(in, out); + return; + + case 20: + __SIMD_fastpackwithoutmask20(in, out); + return; + + case 21: + __SIMD_fastpackwithoutmask21(in, out); + return; + + case 22: + __SIMD_fastpackwithoutmask22(in, out); + return; + + case 23: + __SIMD_fastpackwithoutmask23(in, out); + return; + + case 24: + __SIMD_fastpackwithoutmask24(in, out); + return; + + case 25: + __SIMD_fastpackwithoutmask25(in, out); + return; + + case 26: + __SIMD_fastpackwithoutmask26(in, out); + return; + + case 27: + __SIMD_fastpackwithoutmask27(in, out); + return; + + case 28: + __SIMD_fastpackwithoutmask28(in, out); + return; + + case 29: + __SIMD_fastpackwithoutmask29(in, out); + return; + + case 30: + __SIMD_fastpackwithoutmask30(in, out); + return; + + case 31: + __SIMD_fastpackwithoutmask31(in, out); + return; + + case 32: + __SIMD_fastpackwithoutmask32(in, out); + return; + + default: + break; + } + throw std::logic_error("number of bits is unsupported"); +} + +/*assumes that integers fit in the prescribed number of bits*/ +void simdpack(const uint32_t *__restrict__ in, __m128i *__restrict__ out, + const uint32_t bit) { + switch (bit) { + case 0: + return; + + case 1: + __SIMD_fastpack1(in, out); + return; + + case 2: + __SIMD_fastpack2(in, out); + return; + + case 3: + __SIMD_fastpack3(in, out); + return; + + case 4: + __SIMD_fastpack4(in, out); + return; + + case 5: + __SIMD_fastpack5(in, out); + return; + + case 6: + __SIMD_fastpack6(in, out); + return; + + case 7: + __SIMD_fastpack7(in, out); + return; + + case 8: + __SIMD_fastpack8(in, out); + return; + + case 9: + __SIMD_fastpack9(in, out); + return; + + case 10: + __SIMD_fastpack10(in, out); + return; + + case 11: + __SIMD_fastpack11(in, out); + return; + + case 12: + __SIMD_fastpack12(in, out); + return; + + case 13: + __SIMD_fastpack13(in, out); + return; + + case 14: + __SIMD_fastpack14(in, out); + return; + + case 15: + __SIMD_fastpack15(in, out); + return; + + case 16: + __SIMD_fastpack16(in, out); + return; + + case 17: + __SIMD_fastpack17(in, out); + return; + + case 18: + __SIMD_fastpack18(in, out); + return; + + case 19: + __SIMD_fastpack19(in, out); + return; + + case 20: + __SIMD_fastpack20(in, out); + return; + + case 21: + __SIMD_fastpack21(in, out); + return; + + case 22: + __SIMD_fastpack22(in, out); + return; + + case 23: + __SIMD_fastpack23(in, out); + return; + + case 24: + __SIMD_fastpack24(in, out); + return; + + case 25: + __SIMD_fastpack25(in, out); + return; + + case 26: + __SIMD_fastpack26(in, out); + return; + + case 27: + __SIMD_fastpack27(in, out); + return; + + case 28: + __SIMD_fastpack28(in, out); + return; + + case 29: + __SIMD_fastpack29(in, out); + return; + + case 30: + __SIMD_fastpack30(in, out); + return; + + case 31: + __SIMD_fastpack31(in, out); + return; + + case 32: + __SIMD_fastpack32(in, out); + return; + + default: + break; + } + throw std::logic_error("number of bits is unsupported"); +} + +void usimdunpack(const __m128i *__restrict__ in, uint32_t *__restrict__ out, + const uint32_t bit) { + switch (bit) { + case 0: + uSIMD_nullunpacker32(in, out); + return; + + case 1: + __uSIMD_fastunpack1(in, out); + return; + + case 2: + __uSIMD_fastunpack2(in, out); + return; + + case 3: + __uSIMD_fastunpack3(in, out); + return; + + case 4: + __uSIMD_fastunpack4(in, out); + return; + + case 5: + __uSIMD_fastunpack5(in, out); + return; + + case 6: + __uSIMD_fastunpack6(in, out); + return; + + case 7: + __uSIMD_fastunpack7(in, out); + return; + + case 8: + __uSIMD_fastunpack8(in, out); + return; + + case 9: + __uSIMD_fastunpack9(in, out); + return; + + case 10: + __uSIMD_fastunpack10(in, out); + return; + + case 11: + __uSIMD_fastunpack11(in, out); + return; + + case 12: + __uSIMD_fastunpack12(in, out); + return; + + case 13: + __uSIMD_fastunpack13(in, out); + return; + + case 14: + __uSIMD_fastunpack14(in, out); + return; + + case 15: + __uSIMD_fastunpack15(in, out); + return; + + case 16: + __uSIMD_fastunpack16(in, out); + return; + + case 17: + __uSIMD_fastunpack17(in, out); + return; + + case 18: + __uSIMD_fastunpack18(in, out); + return; + + case 19: + __uSIMD_fastunpack19(in, out); + return; + + case 20: + __uSIMD_fastunpack20(in, out); + return; + + case 21: + __uSIMD_fastunpack21(in, out); + return; + + case 22: + __uSIMD_fastunpack22(in, out); + return; + + case 23: + __uSIMD_fastunpack23(in, out); + return; + + case 24: + __uSIMD_fastunpack24(in, out); + return; + + case 25: + __uSIMD_fastunpack25(in, out); + return; + + case 26: + __uSIMD_fastunpack26(in, out); + return; + + case 27: + __uSIMD_fastunpack27(in, out); + return; + + case 28: + __uSIMD_fastunpack28(in, out); + return; + + case 29: + __uSIMD_fastunpack29(in, out); + return; + + case 30: + __uSIMD_fastunpack30(in, out); + return; + + case 31: + __uSIMD_fastunpack31(in, out); + return; + + case 32: + __uSIMD_fastunpack32(in, out); + return; + + default: + break; + } + throw std::logic_error("number of bits is unsupported"); +} + +/*assumes that integers fit in the prescribed number of bits*/ +void usimdpackwithoutmask(const uint32_t *__restrict__ in, + __m128i *__restrict__ out, const uint32_t bit) { + switch (bit) { + case 0: + return; + + case 1: + __uSIMD_fastpackwithoutmask1(in, out); + return; + + case 2: + __uSIMD_fastpackwithoutmask2(in, out); + return; + + case 3: + __uSIMD_fastpackwithoutmask3(in, out); + return; + + case 4: + __uSIMD_fastpackwithoutmask4(in, out); + return; + + case 5: + __uSIMD_fastpackwithoutmask5(in, out); + return; + + case 6: + __uSIMD_fastpackwithoutmask6(in, out); + return; + + case 7: + __uSIMD_fastpackwithoutmask7(in, out); + return; + + case 8: + __uSIMD_fastpackwithoutmask8(in, out); + return; + + case 9: + __uSIMD_fastpackwithoutmask9(in, out); + return; + + case 10: + __uSIMD_fastpackwithoutmask10(in, out); + return; + + case 11: + __uSIMD_fastpackwithoutmask11(in, out); + return; + + case 12: + __uSIMD_fastpackwithoutmask12(in, out); + return; + + case 13: + __uSIMD_fastpackwithoutmask13(in, out); + return; + + case 14: + __uSIMD_fastpackwithoutmask14(in, out); + return; + + case 15: + __uSIMD_fastpackwithoutmask15(in, out); + return; + + case 16: + __uSIMD_fastpackwithoutmask16(in, out); + return; + + case 17: + __uSIMD_fastpackwithoutmask17(in, out); + return; + + case 18: + __uSIMD_fastpackwithoutmask18(in, out); + return; + + case 19: + __uSIMD_fastpackwithoutmask19(in, out); + return; + + case 20: + __uSIMD_fastpackwithoutmask20(in, out); + return; + + case 21: + __uSIMD_fastpackwithoutmask21(in, out); + return; + + case 22: + __uSIMD_fastpackwithoutmask22(in, out); + return; + + case 23: + __uSIMD_fastpackwithoutmask23(in, out); + return; + + case 24: + __uSIMD_fastpackwithoutmask24(in, out); + return; + + case 25: + __uSIMD_fastpackwithoutmask25(in, out); + return; + + case 26: + __uSIMD_fastpackwithoutmask26(in, out); + return; + + case 27: + __uSIMD_fastpackwithoutmask27(in, out); + return; + + case 28: + __uSIMD_fastpackwithoutmask28(in, out); + return; + + case 29: + __uSIMD_fastpackwithoutmask29(in, out); + return; + + case 30: + __uSIMD_fastpackwithoutmask30(in, out); + return; + + case 31: + __uSIMD_fastpackwithoutmask31(in, out); + return; + + case 32: + __uSIMD_fastpackwithoutmask32(in, out); + return; + + default: + break; + } + throw std::logic_error("number of bits is unsupported"); +} + +void usimdpack(const uint32_t *__restrict__ in, __m128i *__restrict__ out, + const uint32_t bit) { + switch (bit) { + case 0: + return; + + case 1: + __uSIMD_fastpack1(in, out); + return; + + case 2: + __uSIMD_fastpack2(in, out); + return; + + case 3: + __uSIMD_fastpack3(in, out); + return; + + case 4: + __uSIMD_fastpack4(in, out); + return; + + case 5: + __uSIMD_fastpack5(in, out); + return; + + case 6: + __uSIMD_fastpack6(in, out); + return; + + case 7: + __uSIMD_fastpack7(in, out); + return; + + case 8: + __uSIMD_fastpack8(in, out); + return; + + case 9: + __uSIMD_fastpack9(in, out); + return; + + case 10: + __uSIMD_fastpack10(in, out); + return; + + case 11: + __uSIMD_fastpack11(in, out); + return; + + case 12: + __uSIMD_fastpack12(in, out); + return; + + case 13: + __uSIMD_fastpack13(in, out); + return; + + case 14: + __uSIMD_fastpack14(in, out); + return; + + case 15: + __uSIMD_fastpack15(in, out); + return; + + case 16: + __uSIMD_fastpack16(in, out); + return; + + case 17: + __uSIMD_fastpack17(in, out); + return; + + case 18: + __uSIMD_fastpack18(in, out); + return; + + case 19: + __uSIMD_fastpack19(in, out); + return; + + case 20: + __uSIMD_fastpack20(in, out); + return; + + case 21: + __uSIMD_fastpack21(in, out); + return; + + case 22: + __uSIMD_fastpack22(in, out); + return; + + case 23: + __uSIMD_fastpack23(in, out); + return; + + case 24: + __uSIMD_fastpack24(in, out); + return; + + case 25: + __uSIMD_fastpack25(in, out); + return; + + case 26: + __uSIMD_fastpack26(in, out); + return; + + case 27: + __uSIMD_fastpack27(in, out); + return; + + case 28: + __uSIMD_fastpack28(in, out); + return; + + case 29: + __uSIMD_fastpack29(in, out); + return; + + case 30: + __uSIMD_fastpack30(in, out); + return; + + case 31: + __uSIMD_fastpack31(in, out); + return; + + case 32: + __uSIMD_fastpack32(in, out); + return; + + default: + break; + } + throw std::logic_error("number of bits is unsupported"); +} + +namespace ArrayDispatch { +typedef void (*unpackingfunction)(const __m128i *, uint32_t *); +typedef void (*packingfunction)(const uint32_t *, __m128i *); + +constexpr unpackingfunction unpack[33] = { + SIMD_nullunpacker32, __SIMD_fastunpack1, __SIMD_fastunpack2, + __SIMD_fastunpack3, __SIMD_fastunpack4, __SIMD_fastunpack5, + __SIMD_fastunpack6, __SIMD_fastunpack7, __SIMD_fastunpack8, + __SIMD_fastunpack9, __SIMD_fastunpack10, __SIMD_fastunpack11, + __SIMD_fastunpack12, __SIMD_fastunpack13, __SIMD_fastunpack14, + __SIMD_fastunpack15, __SIMD_fastunpack16, __SIMD_fastunpack17, + __SIMD_fastunpack18, __SIMD_fastunpack19, __SIMD_fastunpack20, + __SIMD_fastunpack21, __SIMD_fastunpack22, __SIMD_fastunpack23, + __SIMD_fastunpack24, __SIMD_fastunpack25, __SIMD_fastunpack26, + __SIMD_fastunpack27, __SIMD_fastunpack28, __SIMD_fastunpack29, + __SIMD_fastunpack30, __SIMD_fastunpack31, __SIMD_fastunpack32}; + +ALWAYS_INLINE +void SIMDunpack(const __m128i *__restrict__ in, uint32_t *__restrict__ out, + const uint32_t bit) { + return unpack[bit](in, out); +} + +constexpr packingfunction packwithoutmask[33] = { + __SIMD_fastpackwithoutmask0, __SIMD_fastpackwithoutmask1, + __SIMD_fastpackwithoutmask2, __SIMD_fastpackwithoutmask3, + __SIMD_fastpackwithoutmask4, __SIMD_fastpackwithoutmask5, + __SIMD_fastpackwithoutmask6, __SIMD_fastpackwithoutmask7, + __SIMD_fastpackwithoutmask8, __SIMD_fastpackwithoutmask9, + __SIMD_fastpackwithoutmask10, __SIMD_fastpackwithoutmask11, + __SIMD_fastpackwithoutmask12, __SIMD_fastpackwithoutmask13, + __SIMD_fastpackwithoutmask14, __SIMD_fastpackwithoutmask15, + __SIMD_fastpackwithoutmask16, __SIMD_fastpackwithoutmask17, + __SIMD_fastpackwithoutmask18, __SIMD_fastpackwithoutmask19, + __SIMD_fastpackwithoutmask20, __SIMD_fastpackwithoutmask21, + __SIMD_fastpackwithoutmask22, __SIMD_fastpackwithoutmask23, + __SIMD_fastpackwithoutmask24, __SIMD_fastpackwithoutmask25, + __SIMD_fastpackwithoutmask26, __SIMD_fastpackwithoutmask27, + __SIMD_fastpackwithoutmask28, __SIMD_fastpackwithoutmask29, + __SIMD_fastpackwithoutmask30, __SIMD_fastpackwithoutmask31, + __SIMD_fastpackwithoutmask32}; + +ALWAYS_INLINE +void SIMDpackwithoutmask(const uint32_t *__restrict__ in, + __m128i *__restrict__ out, const uint32_t bit) { + packwithoutmask[bit](in, out); +} +constexpr packingfunction pack[33] = { + __SIMD_fastpack0, __SIMD_fastpack1, __SIMD_fastpack2, __SIMD_fastpack3, + __SIMD_fastpack4, __SIMD_fastpack5, __SIMD_fastpack6, __SIMD_fastpack7, + __SIMD_fastpack8, __SIMD_fastpack9, __SIMD_fastpack10, __SIMD_fastpack11, + __SIMD_fastpack12, __SIMD_fastpack13, __SIMD_fastpack14, __SIMD_fastpack15, + __SIMD_fastpack16, __SIMD_fastpack17, __SIMD_fastpack18, __SIMD_fastpack19, + __SIMD_fastpack20, __SIMD_fastpack21, __SIMD_fastpack22, __SIMD_fastpack23, + __SIMD_fastpack24, __SIMD_fastpack25, __SIMD_fastpack26, __SIMD_fastpack27, + __SIMD_fastpack28, __SIMD_fastpack29, __SIMD_fastpack30, __SIMD_fastpack31, + __SIMD_fastpack32}; + +ALWAYS_INLINE +void SIMDpack(const uint32_t *__restrict__ in, __m128i *__restrict__ out, + const uint32_t bit) { + pack[bit](in, out); +} + +constexpr unpackingfunction Uunpack[33] = { + uSIMD_nullunpacker32, __uSIMD_fastunpack1, __uSIMD_fastunpack2, + __uSIMD_fastunpack3, __uSIMD_fastunpack4, __uSIMD_fastunpack5, + __uSIMD_fastunpack6, __uSIMD_fastunpack7, __uSIMD_fastunpack8, + __uSIMD_fastunpack9, __uSIMD_fastunpack10, __uSIMD_fastunpack11, + __uSIMD_fastunpack12, __uSIMD_fastunpack13, __uSIMD_fastunpack14, + __uSIMD_fastunpack15, __uSIMD_fastunpack16, __uSIMD_fastunpack17, + __uSIMD_fastunpack18, __uSIMD_fastunpack19, __uSIMD_fastunpack20, + __uSIMD_fastunpack21, __uSIMD_fastunpack22, __uSIMD_fastunpack23, + __uSIMD_fastunpack24, __uSIMD_fastunpack25, __uSIMD_fastunpack26, + __uSIMD_fastunpack27, __uSIMD_fastunpack28, __uSIMD_fastunpack29, + __uSIMD_fastunpack30, __uSIMD_fastunpack31, __uSIMD_fastunpack32}; + +ALWAYS_INLINE +void uSIMDunpack(const __m128i *__restrict__ in, uint32_t *__restrict__ out, + const uint32_t bit) { + return Uunpack[bit](in, out); +} + +constexpr packingfunction Upackwithoutmask[33] = { + __uSIMD_fastpackwithoutmask0, __uSIMD_fastpackwithoutmask1, + __uSIMD_fastpackwithoutmask2, __uSIMD_fastpackwithoutmask3, + __uSIMD_fastpackwithoutmask4, __uSIMD_fastpackwithoutmask5, + __uSIMD_fastpackwithoutmask6, __uSIMD_fastpackwithoutmask7, + __uSIMD_fastpackwithoutmask8, __uSIMD_fastpackwithoutmask9, + __uSIMD_fastpackwithoutmask10, __uSIMD_fastpackwithoutmask11, + __uSIMD_fastpackwithoutmask12, __uSIMD_fastpackwithoutmask13, + __uSIMD_fastpackwithoutmask14, __uSIMD_fastpackwithoutmask15, + __uSIMD_fastpackwithoutmask16, __uSIMD_fastpackwithoutmask17, + __uSIMD_fastpackwithoutmask18, __uSIMD_fastpackwithoutmask19, + __uSIMD_fastpackwithoutmask20, __uSIMD_fastpackwithoutmask21, + __uSIMD_fastpackwithoutmask22, __uSIMD_fastpackwithoutmask23, + __uSIMD_fastpackwithoutmask24, __uSIMD_fastpackwithoutmask25, + __uSIMD_fastpackwithoutmask26, __uSIMD_fastpackwithoutmask27, + __uSIMD_fastpackwithoutmask28, __uSIMD_fastpackwithoutmask29, + __uSIMD_fastpackwithoutmask30, __uSIMD_fastpackwithoutmask31, + __uSIMD_fastpackwithoutmask32}; + +ALWAYS_INLINE +void uSIMDpackwithoutmask(const uint32_t *__restrict__ in, + __m128i *__restrict__ out, const uint32_t bit) { + Upackwithoutmask[bit](in, out); +} +constexpr packingfunction Upack[33] = { + __uSIMD_fastpack0, __uSIMD_fastpack1, __uSIMD_fastpack2, + __uSIMD_fastpack3, __uSIMD_fastpack4, __uSIMD_fastpack5, + __uSIMD_fastpack6, __uSIMD_fastpack7, __uSIMD_fastpack8, + __uSIMD_fastpack9, __uSIMD_fastpack10, __uSIMD_fastpack11, + __uSIMD_fastpack12, __uSIMD_fastpack13, __uSIMD_fastpack14, + __uSIMD_fastpack15, __uSIMD_fastpack16, __uSIMD_fastpack17, + __uSIMD_fastpack18, __uSIMD_fastpack19, __uSIMD_fastpack20, + __uSIMD_fastpack21, __uSIMD_fastpack22, __uSIMD_fastpack23, + __uSIMD_fastpack24, __uSIMD_fastpack25, __uSIMD_fastpack26, + __uSIMD_fastpack27, __uSIMD_fastpack28, __uSIMD_fastpack29, + __uSIMD_fastpack30, __uSIMD_fastpack31, __uSIMD_fastpack32}; + +ALWAYS_INLINE +void uSIMDpack(const uint32_t *__restrict__ in, __m128i *__restrict__ out, + const uint32_t bit) { + Upack[bit](in, out); +} +} + +template struct SIMDBitPackingHelpers { + + static void pack(uint32_t *in, const size_t Qty, uint32_t *out, + const uint32_t bit) { + if (Qty % SIMDBlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + if (SIMDBlockSize % 32) { + throw std::logic_error("Incorrect SIMDBlockSize."); + } + __m128i initoffset = _mm_set1_epi32(0); + + for (size_t k = 0; k < Qty / SIMDBlockSize; ++k) { + __m128i nextoffset = MM_LOAD_SI_128(reinterpret_cast<__m128i *>( + (in + k * SIMDBlockSize + SIMDBlockSize - 4))); + + if (bit < 32) + SIMDDeltaProcessor::runDelta( + initoffset, in + k * SIMDBlockSize); + simdpack(in + k * SIMDBlockSize, + reinterpret_cast<__m128i *>(out + SIMDBlockSize * k * bit / 32), + bit); + initoffset = nextoffset; + } + } + + static void unpack(const uint32_t *in, size_t Qty, uint32_t *out, + const uint32_t bit) { + if (Qty % SIMDBlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + if (Qty % SIMDBlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + __m128i initoffset = _mm_set1_epi32(0); + + for (size_t k = 0; k < Qty / SIMDBlockSize; ++k) { + simdunpack( + reinterpret_cast(in + SIMDBlockSize * k * bit / 32), + out + k * SIMDBlockSize, bit); + if (bit < 32) { + initoffset = + SIMDDeltaProcessor::runPrefixSum( + initoffset, out + k * SIMDBlockSize); + } + } + } + + static void packwithoutmask(uint32_t *in, const size_t Qty, uint32_t *out, + const uint32_t bit) { + if (Qty % SIMDBlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + if (Qty % SIMDBlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + __m128i initoffset = _mm_set1_epi32(0); + + for (size_t k = 0; k < Qty / SIMDBlockSize; ++k) { + __m128i nextoffset = MM_LOAD_SI_128(reinterpret_cast<__m128i *>( + (in + k * SIMDBlockSize + SIMDBlockSize - 4))); + if (bit < 32) + SIMDDeltaProcessor::runDelta( + initoffset, in + k * SIMDBlockSize); + simdpackwithoutmask( + in + k * SIMDBlockSize, + reinterpret_cast<__m128i *>(out + SIMDBlockSize * k * bit / 32), bit); + initoffset = nextoffset; + } + } + + static void ipack(const uint32_t *in, const size_t Qty, uint32_t *_out, + const uint32_t bit) { + if (Qty % SIMDBlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + __m128i *out = reinterpret_cast<__m128i *>(_out); + __m128i initoffset = _mm_set1_epi32(0U); + ; + + for (size_t k = 0; k < Qty / SIMDBlockSize; ++k) { + SIMDipack(initoffset, in + k * SIMDBlockSize, out + k * bit, + bit); + initoffset = MM_LOAD_SI_128(reinterpret_cast( + in + k * SIMDBlockSize + SIMDBlockSize - 4)); + // memcpy(&initoffset, (in+k*SIMDBlockSize+SIMDBlockSize - 4), sizeof + // initoffset);// Daniel: memcpy looks like a hack + } + } + + static void ipackwithoutmask(const uint32_t *in, const size_t Qty, + uint32_t *_out, const uint32_t bit) { + if (Qty % SIMDBlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + __m128i *out = reinterpret_cast<__m128i *>(_out); + __m128i initoffset = _mm_set1_epi32(0U); + ; + + for (size_t k = 0; k < Qty / SIMDBlockSize; ++k) { + SIMDipackwithoutmask(initoffset, in + k * SIMDBlockSize, + out + k * bit, bit); + initoffset = MM_LOAD_SI_128(reinterpret_cast( + in + k * SIMDBlockSize + SIMDBlockSize - 4)); + // memcpy(&initoffset, (in+k*SIMDBlockSize+SIMDBlockSize - 4), sizeof + // initoffset);// Daniel: memcpy looks like a hack + } + } + + static void iunpack(const uint32_t *_in, size_t Qty, uint32_t *out, + const uint32_t bit) { + if (Qty % SIMDBlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + const __m128i *in = reinterpret_cast(_in); + + __m128i initoffset = _mm_set1_epi32(0U); + ; + + for (size_t k = 0; k < Qty / SIMDBlockSize; ++k) { + initoffset = SIMDiunpack(initoffset, in + k * bit, + out + k * SIMDBlockSize, bit); + } + } + + // this is not expected to be useful, only for benchmarking + static void ipatchedunpack(const uint32_t *_in, size_t Qty, uint32_t *out, + const uint32_t bit) { + if (Qty % SIMDBlockSize) { + throw std::logic_error("Incorrect # of entries."); + } + const __m128i *in = reinterpret_cast(_in); + + __m128i initoffset = _mm_set1_epi32(0U); + ; + + for (size_t k = 0; k < Qty / SIMDBlockSize; ++k) { + initoffset = SIMDipatchedunpack( + initoffset, in + k * bit, out + k * SIMDBlockSize, + reinterpret_cast(out + k * SIMDBlockSize), bit); + } + } + + static void CheckMaxDiff(const std::vector &refdata, unsigned bit) { + for (size_t i = 4; i < refdata.size(); ++i) { + if (gccbits(refdata[i] - refdata[i - 4]) > bit) + throw std::runtime_error("bug"); + } + } +}; + +} // namespace SIMDCompressionLib + +#endif diff --git a/include/SIMDCompressionAndIntersection/simdfastpfor.h b/include/SIMDCompressionAndIntersection/simdfastpfor.h new file mode 100644 index 0000000..a0d5488 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/simdfastpfor.h @@ -0,0 +1,279 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire, http://lemire.me/en/ + */ +#ifndef SIMDCompressionAndIntersection_SIMDFASTPFOR_H_ +#define SIMDCompressionAndIntersection_SIMDFASTPFOR_H_ + +#include "common.h" +#include "codecs.h" +#include "sortedbitpacking.h" +#include "simdbitpacking.h" +#include "util.h" +#include "delta.h" + +namespace SIMDCompressionLib { + +/** + * SIMDFastPFor + * + * Reference and documentation: + * + * Daniel Lemire and Leonid Boytsov, Decoding billions of integers per second + * through vectorization + * http://arxiv.org/abs/1209.2137 + * + * Note that this implementation is slightly improved compared to the version + * presented + * in the paper. + * + * Designed by D. Lemire with ideas from Leonid Boytsov. This scheme is NOT + * patented. + * + */ +template +class SIMDFastPFor : public IntegerCODEC { +public: + /** + * ps (page size) should be a multiple of BlockSize, any "large" + * value should do. + */ + SIMDFastPFor(uint32_t ps = 65536) + : PageSize(ps), bitsPageSize(gccbits(PageSize)), bpacker(), + bytescontainer(PageSize + 3 * PageSize / BlockSize) { + assert(ps / BlockSize * BlockSize == ps); + assert(gccbits(BlockSizeInUnitsOfPackSize * PACKSIZE - 1) <= 8); + } + enum { + PACKSIZE = 32, + overheadofeachexcept = 8, + overheadduetobits = 8, + overheadduetonmbrexcept = 8, + BlockSize = BlockSizeInUnitsOfPackSize * PACKSIZE + }; + + const uint32_t PageSize; + const uint32_t bitsPageSize; + SortedBitPacker bpacker; + vector bytescontainer; + + const uint32_t *decodeArray(const uint32_t *in, const size_t length, + uint32_t *out, size_t &nvalue) { +#ifdef USE_ALIGNED + if (needPaddingTo128Bits(out) or needPaddingTo128Bits(in)) + throw std::runtime_error( + "alignment issue: pointers should be aligned on 128-bit boundaries"); +#endif + const uint32_t *const initin(in); + const size_t mynvalue = *in; + ++in; + if (mynvalue > nvalue) + throw NotEnoughStorage(mynvalue); + nvalue = mynvalue; + const uint32_t *const finalout(out + nvalue); + __m128i prev = _mm_set1_epi32(0); + while (out != finalout) { + size_t thisnvalue(0); + size_t thissize = static_cast( + finalout > PageSize + out ? PageSize : (finalout - out)); + + __decodeArray(in, thisnvalue, out, thissize, prev); + in += thisnvalue; + out += thissize; + } + assert(initin + length >= in); + bpacker.reset(); // if you don't do this, the codec has a "memory". + return in; + } + + /** + * If you save the output and recover it in memory, you are + * responsible to ensure that the alignment is preserved. + * + * The input size (length) should be a multiple of + * BlockSizeInUnitsOfPackSize * PACKSIZE. (This was done + * to simplify slightly the implementation.) + */ + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { +#ifdef USE_ALIGNED + if (needPaddingTo128Bits(out) or needPaddingTo128Bits(in)) + throw std::runtime_error( + "alignment issue: pointers should be aligned on 128-bit boundaries"); +#endif + checkifdivisibleby(length, BlockSize); + const uint32_t *const initout(out); + const uint32_t *const finalin(in + length); + + *out++ = static_cast(length); + const size_t oldnvalue = nvalue; + nvalue = 1; + __m128i prev = _mm_set1_epi32(0); + while (in != finalin) { + size_t thissize = static_cast( + finalin > PageSize + in ? PageSize : (finalin - in)); + size_t thisnvalue(0); + __encodeArray(in, thissize, out, thisnvalue, prev); + nvalue += thisnvalue; + out += thisnvalue; + in += thissize; + } + assert(out == nvalue + initout); + if (oldnvalue < nvalue) + std::cerr + << "It is possible we have a buffer overrun. You reported having allocated " + << oldnvalue * sizeof(uint32_t) + << " bytes for the compressed data but we needed " + << nvalue * sizeof(uint32_t) + << " bytes. Please increase the available memory" + " for compressed data or check the value of the last parameter provided " + " to the encodeArray method." << std::endl; + bpacker.reset(); // if you don't do this, the buffer has a memory + } + + void getBestBFromData(const uint32_t *in, uint8_t &bestb, + uint8_t &bestcexcept, uint8_t &maxb) { + uint32_t freqs[33]; + for (uint32_t k = 0; k <= 32; ++k) + freqs[k] = 0; + for (uint32_t k = 0; k < BlockSize; ++k) { + freqs[asmbits(in[k])]++; + } + bestb = 32; + while (freqs[bestb] == 0) + bestb--; + maxb = bestb; + uint32_t bestcost = bestb * BlockSize; + uint32_t cexcept = 0; + bestcexcept = static_cast(cexcept); + for (uint32_t b = bestb - 1; b < 32; --b) { + cexcept += freqs[b + 1]; + uint32_t thiscost = cexcept * overheadofeachexcept + + cexcept * (maxb - b) + b * BlockSize + + 8; // the extra 8 is the cost of storing maxbits + if (bestb - b == 1) + thiscost -= cexcept; + if (thiscost < bestcost) { + bestcost = thiscost; + bestb = static_cast(b); + bestcexcept = static_cast(cexcept); + } + } + } + + void + __encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue, + __m128i &prev) { // = _mm_set1_epi32 (0);// for delta + uint32_t *const initout = out; // keep track of this + checkifdivisibleby(length, BlockSize); + uint32_t *const headerout = out++; // keep track of this + bpacker.clear(); + uint8_t *bc = bytescontainer.data(); +#ifdef USE_ALIGNED + out = padTo128bits(out); + if (needPaddingTo128Bits(in)) + throw std::runtime_error("alignment bug"); +#endif + for (const uint32_t *const final = in + length; (in + BlockSize <= final); + in += BlockSize) { + uint8_t bestb, bestcexcept, maxb; + + const __m128i nextprev = + MM_LOAD_SI_128(reinterpret_cast(in + BlockSize - 4)); + SIMDDeltaProcessor::runDelta(prev, in); + prev = nextprev; + + getBestBFromData(in, bestb, bestcexcept, maxb); + *bc++ = bestb; + *bc++ = bestcexcept; + if (bestcexcept > 0) { + *bc++ = maxb; + bpacker.ensureCapacity(maxb - bestb - 1, bestcexcept); + const uint32_t maxval = 1U << bestb; + for (uint32_t k = 0; k < BlockSize; ++k) { + if (in[k] >= maxval) { + bpacker.directAppend(maxb - bestb - 1, in[k] >> bestb); + *bc++ = static_cast(k); + } + } + } + for (int k = 0; k < BlockSize; k += 128) { + simdpack(in + k, reinterpret_cast<__m128i *>(out), bestb); + out += 4 * bestb; + } + } + headerout[0] = static_cast(out - headerout); + const uint32_t bytescontainersize = + static_cast(bc - bytescontainer.data()); + *(out++) = bytescontainersize; + memcpy(out, bytescontainer.data(), bytescontainersize); + out += (bytescontainersize + sizeof(uint32_t) - 1) / sizeof(uint32_t); + const uint32_t *const lastout = bpacker.write(out); + nvalue = lastout - initout; + } + + void __decodeArray(const uint32_t *in, size_t &length, uint32_t *out, + const size_t nvalue, __m128i &prev) { + const uint32_t *const initin = in; + const uint32_t *const headerin = in++; + const uint32_t wheremeta = headerin[0]; + const uint32_t *inexcept = headerin + wheremeta; + const uint32_t bytesize = *inexcept++; + const uint8_t *bytep = reinterpret_cast(inexcept); + + inexcept += (bytesize + sizeof(uint32_t) - 1) / sizeof(uint32_t); + inexcept = bpacker.read(inexcept); + length = inexcept - initin; + const uint32_t *unpackpointers[32 + 1]; + for (uint32_t k = 1; k <= 32; ++k) { + unpackpointers[k] = bpacker.get(k - 1); + } +#ifdef USE_ALIGNED + in = padTo128bits(in); + assert(!needPaddingTo128Bits(out)); +#endif + for (uint32_t run = 0; run < nvalue / BlockSize; ++run, out += BlockSize) { + const uint8_t b = *bytep++; + const uint8_t cexcept = *bytep++; + for (int k = 0; k < BlockSize; k += 128) { + if (arraydispatch) + simdunpack(reinterpret_cast(in), out + k, b); + else + ArrayDispatch::SIMDunpack(reinterpret_cast(in), + out + k, b); + in += 4 * b; + } + if (cexcept > 0) { + const uint8_t maxbits = *bytep++; + if (maxbits - b == 1) { + for (uint32_t k = 0; k < cexcept; ++k) { + const uint8_t pos = *(bytep++); + out[pos] |= static_cast(1) << b; + } + } else { + const uint32_t *vals = unpackpointers[maxbits - b]; + unpackpointers[maxbits - b] += cexcept; + for (uint32_t k = 0; k < cexcept; ++k) { + const uint8_t pos = *(bytep++); + out[pos] |= vals[k] << b; + } + } + } + prev = + SIMDDeltaProcessor::runPrefixSum(prev, out); + } + + assert(in == headerin + wheremeta); + } + + string name() const { return string("SIMDFastPFor") + DeltaHelper::name(); } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_SIMDFASTPFOR_H_ */ diff --git a/include/SIMDCompressionAndIntersection/simdintegratedbitpacking.h b/include/SIMDCompressionAndIntersection/simdintegratedbitpacking.h new file mode 100644 index 0000000..9493fae --- /dev/null +++ b/include/SIMDCompressionAndIntersection/simdintegratedbitpacking.h @@ -0,0 +1,895 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Leonid Boytsov, Nathan Kurz and Daniel Lemire + */ + +#ifndef SIMDCompressionAndIntersection_SIMD_INTEGRATED_BITPACKING_H +#define SIMDCompressionAndIntersection_SIMD_INTEGRATED_BITPACKING_H + +/** + * To avoid crazy dependencies, this header should not + * include any other header beside delta.h. + */ +#include "deltatemplates.h" + +namespace SIMDCompressionLib { + +template +__m128i iunpack0(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack0(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack0(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask0(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack1(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack1(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack1(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask1(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack2(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack2(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack2(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask2(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack3(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack3(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack3(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask3(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack4(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack4(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack4(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask4(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack5(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack5(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack5(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask5(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack6(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack6(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack6(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask6(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack7(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack7(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack7(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask7(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack8(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack8(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack8(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask8(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack9(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack9(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack9(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask9(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack10(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack10(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack10(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask10(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack11(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack11(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack11(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask11(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack12(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack12(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack12(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask12(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack13(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack13(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack13(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask13(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack14(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack14(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack14(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask14(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack15(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack15(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack15(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask15(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack16(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack16(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack16(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask16(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack17(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack17(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack17(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask17(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack18(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack18(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack18(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask18(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack19(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack19(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack19(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask19(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack20(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack20(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack20(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask20(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack21(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack21(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack21(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask21(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack22(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack22(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack22(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask22(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack23(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack23(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack23(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask23(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack24(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack24(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack24(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask24(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack25(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack25(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack25(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask25(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack26(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack26(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack26(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask26(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack27(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack27(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack27(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask27(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack28(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack28(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack28(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask28(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack29(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack29(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack29(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask29(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack30(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack30(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack30(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask30(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack31(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack31(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack31(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask31(__m128i, const uint32_t *, __m128i *); + +template +__m128i iunpack32(__m128i, const __m128i *, uint32_t *); +template +__m128i ipatchedunpack32(__m128i, const __m128i *, uint32_t *, const __m128i *); +template void ipack32(__m128i, const uint32_t *, __m128i *); +template +void ipackwithoutmask32(__m128i, const uint32_t *, __m128i *); + +typedef __m128i (*integratedunpackingfunction)(__m128i, const __m128i *, + uint32_t *); +typedef __m128i (*integratedpatchedunpackingfunction)(__m128i, const __m128i *, + uint32_t *, + const __m128i *); + +typedef void (*integratedpackingfunction)(__m128i, const uint32_t *, __m128i *); + +template struct IntegratedArrayDispatch { + static integratedunpackingfunction unpack[33]; + + static inline __m128i SIMDiunpack(__m128i initOffset, const __m128i *in, + uint32_t *out, const uint32_t bit) { + return unpack[bit](initOffset, in, out); + } + static integratedpatchedunpackingfunction patchedunpack[33]; + + static inline __m128i SIMDipatchedunpack(__m128i initOffset, + const __m128i *in, uint32_t *out, + const __m128i *patchedbuffer, + const uint32_t bit) { + return patchedunpack[bit](initOffset, in, out, patchedbuffer); + } + static integratedpackingfunction packwithoutmask[33]; + + static inline void SIMDipackwithoutmask(__m128i initOffset, + const uint32_t *in, __m128i *out, + const uint32_t bit) { + packwithoutmask[bit](initOffset, in, out); + } + static integratedpackingfunction pack[33]; + + static inline void SIMDipack(__m128i initOffset, const uint32_t *in, + __m128i *out, const uint32_t bit) { + pack[bit](initOffset, in, out); + } +}; + +template +integratedunpackingfunction IntegratedArrayDispatch::unpack[33] = { + iunpack0, iunpack1, iunpack2, + iunpack3, iunpack4, iunpack5, + iunpack6, iunpack7, iunpack8, + iunpack9, iunpack10, iunpack11, + iunpack12, iunpack13, iunpack14, + iunpack15, iunpack16, iunpack17, + iunpack18, iunpack19, iunpack20, + iunpack21, iunpack22, iunpack23, + iunpack24, iunpack25, iunpack26, + iunpack27, iunpack28, iunpack29, + iunpack30, iunpack31, iunpack32}; + +template +integratedpatchedunpackingfunction + IntegratedArrayDispatch::patchedunpack[33] = { + ipatchedunpack0, ipatchedunpack1, + ipatchedunpack2, ipatchedunpack3, + ipatchedunpack4, ipatchedunpack5, + ipatchedunpack6, ipatchedunpack7, + ipatchedunpack8, ipatchedunpack9, + ipatchedunpack10, ipatchedunpack11, + ipatchedunpack12, ipatchedunpack13, + ipatchedunpack14, ipatchedunpack15, + ipatchedunpack16, ipatchedunpack17, + ipatchedunpack18, ipatchedunpack19, + ipatchedunpack20, ipatchedunpack21, + ipatchedunpack22, ipatchedunpack23, + ipatchedunpack24, ipatchedunpack25, + ipatchedunpack26, ipatchedunpack27, + ipatchedunpack28, ipatchedunpack29, + ipatchedunpack30, ipatchedunpack31, + ipatchedunpack32}; + +template +integratedpackingfunction + IntegratedArrayDispatch::packwithoutmask[33] = { + ipackwithoutmask0, ipackwithoutmask1, + ipackwithoutmask2, ipackwithoutmask3, + ipackwithoutmask4, ipackwithoutmask5, + ipackwithoutmask6, ipackwithoutmask7, + ipackwithoutmask8, ipackwithoutmask9, + ipackwithoutmask10, ipackwithoutmask11, + ipackwithoutmask12, ipackwithoutmask13, + ipackwithoutmask14, ipackwithoutmask15, + ipackwithoutmask16, ipackwithoutmask17, + ipackwithoutmask18, ipackwithoutmask19, + ipackwithoutmask20, ipackwithoutmask21, + ipackwithoutmask22, ipackwithoutmask23, + ipackwithoutmask24, ipackwithoutmask25, + ipackwithoutmask26, ipackwithoutmask27, + ipackwithoutmask28, ipackwithoutmask29, + ipackwithoutmask30, ipackwithoutmask31, + ipackwithoutmask32}; + +template +integratedpackingfunction IntegratedArrayDispatch::pack[33] = { + ipack0, ipack1, ipack2, + ipack3, ipack4, ipack5, + ipack6, ipack7, ipack8, + ipack9, ipack10, ipack11, + ipack12, ipack13, ipack14, + ipack15, ipack16, ipack17, + ipack18, ipack19, ipack20, + ipack21, ipack22, ipack23, + ipack24, ipack25, ipack26, + ipack27, ipack28, ipack29, + ipack30, ipack31, ipack32}; + +template +inline __m128i SIMDiunpack(__m128i initOffset, const __m128i *in, uint32_t *out, + const uint32_t bit) { + switch (bit) { + case 0: + return iunpack0(initOffset, in, out); + + case 1: + return iunpack1(initOffset, in, out); + + case 2: + return iunpack2(initOffset, in, out); + + case 3: + return iunpack3(initOffset, in, out); + + case 4: + return iunpack4(initOffset, in, out); + + case 5: + return iunpack5(initOffset, in, out); + + case 6: + return iunpack6(initOffset, in, out); + + case 7: + return iunpack7(initOffset, in, out); + + case 8: + return iunpack8(initOffset, in, out); + + case 9: + return iunpack9(initOffset, in, out); + + case 10: + return iunpack10(initOffset, in, out); + + case 11: + return iunpack11(initOffset, in, out); + + case 12: + return iunpack12(initOffset, in, out); + + case 13: + return iunpack13(initOffset, in, out); + + case 14: + return iunpack14(initOffset, in, out); + + case 15: + return iunpack15(initOffset, in, out); + + case 16: + return iunpack16(initOffset, in, out); + + case 17: + return iunpack17(initOffset, in, out); + + case 18: + return iunpack18(initOffset, in, out); + + case 19: + return iunpack19(initOffset, in, out); + + case 20: + return iunpack20(initOffset, in, out); + + case 21: + return iunpack21(initOffset, in, out); + + case 22: + return iunpack22(initOffset, in, out); + + case 23: + return iunpack23(initOffset, in, out); + + case 24: + return iunpack24(initOffset, in, out); + + case 25: + return iunpack25(initOffset, in, out); + + case 26: + return iunpack26(initOffset, in, out); + + case 27: + return iunpack27(initOffset, in, out); + + case 28: + return iunpack28(initOffset, in, out); + + case 29: + return iunpack29(initOffset, in, out); + + case 30: + return iunpack30(initOffset, in, out); + + case 31: + return iunpack31(initOffset, in, out); + + case 32: + return iunpack32(initOffset, in, out); + + default: + break; + } + throw std::logic_error("number of bits is unsupported"); +} + +template +inline __m128i SIMDipatchedunpack(__m128i initOffset, const __m128i *in, + uint32_t *out, const __m128i *patchedbuffer, + const uint32_t bit) { + switch (bit) { + case 0: + return ipatchedunpack0(initOffset, in, out, patchedbuffer); + + case 1: + return ipatchedunpack1(initOffset, in, out, patchedbuffer); + + case 2: + return ipatchedunpack2(initOffset, in, out, patchedbuffer); + + case 3: + return ipatchedunpack3(initOffset, in, out, patchedbuffer); + + case 4: + return ipatchedunpack4(initOffset, in, out, patchedbuffer); + + case 5: + return ipatchedunpack5(initOffset, in, out, patchedbuffer); + + case 6: + return ipatchedunpack6(initOffset, in, out, patchedbuffer); + + case 7: + return ipatchedunpack7(initOffset, in, out, patchedbuffer); + + case 8: + return ipatchedunpack8(initOffset, in, out, patchedbuffer); + + case 9: + return ipatchedunpack9(initOffset, in, out, patchedbuffer); + + case 10: + return ipatchedunpack10(initOffset, in, out, patchedbuffer); + + case 11: + return ipatchedunpack11(initOffset, in, out, patchedbuffer); + + case 12: + return ipatchedunpack12(initOffset, in, out, patchedbuffer); + + case 13: + return ipatchedunpack13(initOffset, in, out, patchedbuffer); + + case 14: + return ipatchedunpack14(initOffset, in, out, patchedbuffer); + + case 15: + return ipatchedunpack15(initOffset, in, out, patchedbuffer); + + case 16: + return ipatchedunpack16(initOffset, in, out, patchedbuffer); + + case 17: + return ipatchedunpack17(initOffset, in, out, patchedbuffer); + + case 18: + return ipatchedunpack18(initOffset, in, out, patchedbuffer); + + case 19: + return ipatchedunpack19(initOffset, in, out, patchedbuffer); + + case 20: + return ipatchedunpack20(initOffset, in, out, patchedbuffer); + + case 21: + return ipatchedunpack21(initOffset, in, out, patchedbuffer); + + case 22: + return ipatchedunpack22(initOffset, in, out, patchedbuffer); + + case 23: + return ipatchedunpack23(initOffset, in, out, patchedbuffer); + + case 24: + return ipatchedunpack24(initOffset, in, out, patchedbuffer); + + case 25: + return ipatchedunpack25(initOffset, in, out, patchedbuffer); + + case 26: + return ipatchedunpack26(initOffset, in, out, patchedbuffer); + + case 27: + return ipatchedunpack27(initOffset, in, out, patchedbuffer); + + case 28: + return ipatchedunpack28(initOffset, in, out, patchedbuffer); + + case 29: + return ipatchedunpack29(initOffset, in, out, patchedbuffer); + + case 30: + return ipatchedunpack30(initOffset, in, out, patchedbuffer); + + case 31: + return ipatchedunpack31(initOffset, in, out, patchedbuffer); + + case 32: + return ipatchedunpack32(initOffset, in, out, patchedbuffer); + + default: + break; + } + throw std::logic_error("number of bits is unsupported"); +} + +/*assumes that integers fit in the prescribed number of bits*/ +template +void SIMDipackwithoutmask(__m128i initOffset, const uint32_t *in, __m128i *out, + const uint32_t bit) { + switch (bit) { + case 0: + return; + + case 1: + ipackwithoutmask1(initOffset, in, out); + return; + + case 2: + ipackwithoutmask2(initOffset, in, out); + return; + + case 3: + ipackwithoutmask3(initOffset, in, out); + return; + + case 4: + ipackwithoutmask4(initOffset, in, out); + return; + + case 5: + ipackwithoutmask5(initOffset, in, out); + return; + + case 6: + ipackwithoutmask6(initOffset, in, out); + return; + + case 7: + ipackwithoutmask7(initOffset, in, out); + return; + + case 8: + ipackwithoutmask8(initOffset, in, out); + return; + + case 9: + ipackwithoutmask9(initOffset, in, out); + return; + + case 10: + ipackwithoutmask10(initOffset, in, out); + return; + + case 11: + ipackwithoutmask11(initOffset, in, out); + return; + + case 12: + ipackwithoutmask12(initOffset, in, out); + return; + + case 13: + ipackwithoutmask13(initOffset, in, out); + return; + + case 14: + ipackwithoutmask14(initOffset, in, out); + return; + + case 15: + ipackwithoutmask15(initOffset, in, out); + return; + + case 16: + ipackwithoutmask16(initOffset, in, out); + return; + + case 17: + ipackwithoutmask17(initOffset, in, out); + return; + + case 18: + ipackwithoutmask18(initOffset, in, out); + return; + + case 19: + ipackwithoutmask19(initOffset, in, out); + return; + + case 20: + ipackwithoutmask20(initOffset, in, out); + return; + + case 21: + ipackwithoutmask21(initOffset, in, out); + return; + + case 22: + ipackwithoutmask22(initOffset, in, out); + return; + + case 23: + ipackwithoutmask23(initOffset, in, out); + return; + + case 24: + ipackwithoutmask24(initOffset, in, out); + return; + + case 25: + ipackwithoutmask25(initOffset, in, out); + return; + + case 26: + ipackwithoutmask26(initOffset, in, out); + return; + + case 27: + ipackwithoutmask27(initOffset, in, out); + return; + + case 28: + ipackwithoutmask28(initOffset, in, out); + return; + + case 29: + ipackwithoutmask29(initOffset, in, out); + return; + + case 30: + ipackwithoutmask30(initOffset, in, out); + return; + + case 31: + ipackwithoutmask31(initOffset, in, out); + return; + + case 32: + ipackwithoutmask32(initOffset, in, out); + return; + + default: + break; + } + throw std::logic_error("number of bits is unsupported"); +} + +template +void SIMDipack(__m128i initOffset, const uint32_t *in, __m128i *out, + const uint32_t bit) { + switch (bit) { + case 0: + return; + + case 1: + ipack1(initOffset, in, out); + return; + + case 2: + ipack2(initOffset, in, out); + return; + + case 3: + ipack3(initOffset, in, out); + return; + + case 4: + ipack4(initOffset, in, out); + return; + + case 5: + ipack5(initOffset, in, out); + return; + + case 6: + ipack6(initOffset, in, out); + return; + + case 7: + ipack7(initOffset, in, out); + return; + + case 8: + ipack8(initOffset, in, out); + return; + + case 9: + ipack9(initOffset, in, out); + return; + + case 10: + ipack10(initOffset, in, out); + return; + + case 11: + ipack11(initOffset, in, out); + return; + + case 12: + ipack12(initOffset, in, out); + return; + + case 13: + ipack13(initOffset, in, out); + return; + + case 14: + ipack14(initOffset, in, out); + return; + + case 15: + ipack15(initOffset, in, out); + return; + + case 16: + ipack16(initOffset, in, out); + return; + + case 17: + ipack17(initOffset, in, out); + return; + + case 18: + ipack18(initOffset, in, out); + return; + + case 19: + ipack19(initOffset, in, out); + return; + + case 20: + ipack20(initOffset, in, out); + return; + + case 21: + ipack21(initOffset, in, out); + return; + + case 22: + ipack22(initOffset, in, out); + return; + + case 23: + ipack23(initOffset, in, out); + return; + + case 24: + ipack24(initOffset, in, out); + return; + + case 25: + ipack25(initOffset, in, out); + return; + + case 26: + ipack26(initOffset, in, out); + return; + + case 27: + ipack27(initOffset, in, out); + return; + + case 28: + ipack28(initOffset, in, out); + return; + + case 29: + ipack29(initOffset, in, out); + return; + + case 30: + ipack30(initOffset, in, out); + return; + + case 31: + ipack31(initOffset, in, out); + return; + + case 32: + ipack32(initOffset, in, out); + return; + + default: + break; + } + throw std::logic_error("number of bits is unsupported"); +} + +} // namespace SIMDCompressionLib + +#endif diff --git a/include/SIMDCompressionAndIntersection/simdvariablebyte.h b/include/SIMDCompressionAndIntersection/simdvariablebyte.h new file mode 100644 index 0000000..b575c9e --- /dev/null +++ b/include/SIMDCompressionAndIntersection/simdvariablebyte.h @@ -0,0 +1,493 @@ +/** + * (c) Part of the copyright is to Indeed.com + * Licensed under the Apache License Version 2.0 + */ + +/* + * Based on an initial design by Jeff Plaisance and + * improved by Nathan Kurz. + */ + +#ifndef SIMDCompressionAndIntersection_SIMDVARIABLEBYTE_H_ +#define SIMDCompressionAndIntersection_SIMDVARIABLEBYTE_H_ + +#include "common.h" +#include "codecs.h" +#include "util.h" + +namespace SIMDCompressionLib { + +extern "C" { +size_t masked_vbyte_read_loop(const uint8_t *in, uint32_t *out, + uint64_t length); +size_t masked_vbyte_read_loop_delta(const uint8_t *in, uint32_t *out, + uint64_t length, uint32_t prev); +size_t masked_vbyte_read_loop_fromcompressedsize(const uint8_t *in, + uint32_t *out, + size_t inputsize); +size_t masked_vbyte_read_loop_fromcompressedsize_delta(const uint8_t *in, + uint32_t *out, + size_t inputsize, + uint32_t prev); +// size_t read_ints(const uint8_t* in, uint32_t* out, int length) ; +// size_t read_ints_delta(const uint8_t* in, uint32_t* out, int length, uint32_t +// prev) ; +uint32_t masked_vbyte_select_delta(const uint8_t *in, uint64_t length, + uint32_t prev, size_t slot); +int masked_vbyte_search_delta(const uint8_t *in, uint64_t length, uint32_t prev, + uint32_t key, uint32_t *presult); +} + +/** + * This is a SIMD-accelerated version that is byte-by-byte format compatible + * with + * the VByte codec (that is, standard vbyte). + */ +template class MaskedVByte : public IntegerCODEC { +public: + MaskedVByte() {} + + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + const uint8_t *const initbout = reinterpret_cast(out); + *out = static_cast(length); + out++; + uint8_t *bout = reinterpret_cast(out); + uint32_t prev = 0; + for (size_t k = 0; k < length; ++k) { + + const uint32_t val = delta ? (in[k] - prev) : in[k]; + if (delta) + prev = in[k]; + /** + * Code below could be shorter. Whether it could be faster + * depends on your compiler and machine. + */ + if (val < (1U << 7)) { + *bout = val & 0x7F; + ++bout; + } else if (val < (1U << 14)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 7); + ++bout; + } else if (val < (1U << 21)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 14); + ++bout; + } else if (val < (1U << 28)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 14) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 21); + ++bout; + } else { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 14) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 21) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 28); + ++bout; + } + } + while (needPaddingTo32Bits(bout)) { + *bout++ = 0xff; + } + const size_t storageinbytes = bout - initbout; + nvalue = storageinbytes / 4; + } + + const uint32_t *decodeArray(const uint32_t *in, const size_t length, + uint32_t *out, size_t &nvalue) { + nvalue = *in; + const uint8_t *inbyte = + decodeFromByteArray((const uint8_t *)in, length, out, nvalue); + inbyte = padTo32bits(inbyte); + return reinterpret_cast(inbyte); + } + + // Same as above, but operates on byte arrays (uint8_t *) and avoids + // the padding at the end + const uint8_t *decodeFromByteArray(const uint8_t *in, + const size_t /* length */, uint32_t *out, + size_t &nvalue) { + nvalue = *(uint32_t *)in; + in += sizeof(uint32_t); + if (nvalue == 0) { + return in; // abort + } + if (delta) { + uint32_t prev = 0; + in += masked_vbyte_read_loop_delta(in, out, nvalue, prev); + } else + in += masked_vbyte_read_loop(in, out, nvalue); + return in; + } + + // append a key. Keys must be in sorted order. We assume that there is + // enough room and that delta encoding was used. + // Returns the new size of the compressed array *in bytes* + size_t appendToByteArray(uint8_t *in, size_t bytesize, uint32_t previous_key, + uint32_t key) { + uint32_t num_ints = *(uint32_t *)in; + if (bytesize == 0) + bytesize = 4; + uint8_t *bytein = in + bytesize; + bytein += encodeOneIntegerToByteArray(key - previous_key, bytein); + *(uint32_t *)in = num_ints + 1; + return bytein - in; + } + + // Returns a decompressed value in a delta-encoded array + // only supported for delta encoded data (TODO) + uint32_t select(uint32_t *in, size_t index) { + assert(delta == true); + uint32_t num_ints = *in; + in++; + return (masked_vbyte_select_delta((uint8_t *)in, num_ints, 0, index)); + } + + // Performs a lower bound find in the delta-encoded array. + // Returns the index + // length is the size of the compressed input + // only supported for delta encoded data (TODO) + size_t findLowerBound(const uint32_t *in, const size_t /*length*/, + uint32_t key, uint32_t *presult) { + assert(delta == true); + uint32_t num_ints = *in; + in++; + return ( + masked_vbyte_search_delta((uint8_t *)in, num_ints, 0, key, presult)); + } + // insert the key in sorted order. We assume that there is enough room + // and that delta encoding was used. + size_t insert(uint32_t *in, const size_t length, uint32_t key) { + assert(delta); + size_t bytesize = length * 4; + bytesize -= paddingBytes(in, length); + uint8_t *bytein = (uint8_t *)in; + uint8_t *byteininit = bytein; + bytein += insert(bytein, bytesize, key); + + while (needPaddingTo32Bits(bytein)) { + *bytein++ = 0xFF; + } + size_t storageinbytes = bytein - byteininit; + assert((storageinbytes % 4) == 0); + return storageinbytes / 4; + } + + // insert the key in sorted order. We assume that there is enough room and + // that delta encoding was used. + // the new size (in *byte) is returned + size_t insertInByteArray(uint8_t *inbyte, const size_t length, uint32_t key) { + uint32_t prev = 0; + assert(delta); + const uint8_t *const endbyte = + reinterpret_cast(inbyte + length); + // this assumes that there is a value to be read + + while (endbyte > inbyte + 5) { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c < 128) { + inbyte += 1; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c < 128) { + inbyte += 2; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c < 128) { + inbyte += 3; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c < 128) { + inbyte += 4; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + prev = v + prev; + if (prev >= key) { + return length + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + } + while (endbyte > inbyte) { + unsigned int shift = 0; + for (uint32_t v = 0; endbyte > inbyte; shift += 7) { + uint8_t c = *inbyte++; + v += ((c & 127) << shift); + if ((c < 128)) { + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + break; + } + } + } + // if we make it here, then we need to append + assert(key >= prev); + return length + encodeOneIntegerToByteArray(key - prev, inbyte); + } + + std::string name() const { + if (delta) + return "MaskedVByteDelta"; + else + return "MaskedVByte"; + } + +private: + // convenience function used by insert, writes key and newvalue to compressed + // stream, and return + // extra storage used, pointer should be right after where nextvalue is right + // now + size_t __insert(uint8_t *in, uint32_t previous, uint32_t key, + uint32_t nextvalue, size_t followingbytes) { + assert(nextvalue >= key); + assert(key >= previous); + size_t oldstorage = storageCost(nextvalue - previous); + size_t newstorage = + storageCost(nextvalue - key) + storageCost(key - previous); + assert(newstorage >= oldstorage); + if (newstorage > oldstorage) + std::memmove(in + newstorage - oldstorage, in, followingbytes); + uint8_t *newin = in - oldstorage; + newin += encodeOneIntegerToByteArray(key - previous, newin); + newin += encodeOneIntegerToByteArray(nextvalue - key, newin); + assert(newin == in + newstorage - oldstorage); + return newstorage - oldstorage; + } + + // how many bytes are required to store this integer? + int storageCost(uint32_t val) { + if (val < (1U << 7)) { + return 1; + } else if (val < (1U << 14)) { + return 2; + } else if (val < (1U << 21)) { + return 3; + } else if (val < (1U << 28)) { + return 4; + } else { + return 5; + } + } + + template uint8_t extract7bits(const uint32_t val) { + return static_cast((val >> (7 * i)) & ((1U << 7) - 1)); + } + + template uint8_t extract7bitsmaskless(const uint32_t val) { + return static_cast((val >> (7 * i))); + } + + // determine how many padding bytes were used + int paddingBytes(const uint32_t *in, const size_t length) { + if (length == 0) + return 0; + uint32_t lastword = in[length - 1]; + if (lastword < (1U << 8)) { + return 3; + } else if (lastword < (1U << 16)) { + return 2; + } else if (lastword < (1U << 24)) { + return 1; + } + return 0; + } + + // write one compressed integer (without differential coding) + // returns the number of bytes written + size_t encodeOneIntegerToByteArray(uint32_t val, uint8_t *bout) { + const uint8_t *const initbout = bout; + if (val < (1U << 7)) { + *bout = val & 0x7F; + ++bout; + } else if (val < (1U << 14)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 7); + ++bout; + } else if (val < (1U << 21)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 14); + ++bout; + } else if (val < (1U << 28)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 14) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 21); + ++bout; + } else { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 14) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 21) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 28); + ++bout; + } + return bout - initbout; + } +}; + +// this version differs from MaskedVByte in that it does not write out the +// number of integers compressed as part of a header. +template class HeadlessMaskedVByte : public IntegerCODEC { +public: + HeadlessMaskedVByte() {} + + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + const uint8_t *const initbout = reinterpret_cast(out); + uint8_t *bout = reinterpret_cast(out); + uint32_t prev = 0; + for (size_t k = 0; k < length; ++k) { + const uint32_t val = delta ? (in[k] - prev) : in[k]; + if (delta) + prev = in[k]; + /** + * Code below could be shorter. Whether it could be faster + * depends on your compiler and machine. + */ + if (val < (1U << 7)) { + *bout = val & 0x7F; + ++bout; + } else if (val < (1U << 14)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 7); + ++bout; + } else if (val < (1U << 21)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 14); + ++bout; + } else if (val < (1U << 28)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 14) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 21); + ++bout; + } else { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 14) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 21) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 28); + ++bout; + } + } + while (needPaddingTo32Bits(bout)) { + *bout++ = 0xFFU; + ; + } + const size_t storageinbytes = bout - initbout; + nvalue = storageinbytes / 4; + } + + const uint32_t *decodeArray(const uint32_t *in, const size_t length, + uint32_t *out, size_t &nvalue) { + if (length == 0) { + nvalue = 0; + return in; // abort + } + const uint8_t *inbyte = reinterpret_cast(in); + if (delta) { + uint32_t prev = 0; + nvalue = masked_vbyte_read_loop_fromcompressedsize_delta( + inbyte, out, length * 4, prev); + } else { + nvalue = + masked_vbyte_read_loop_fromcompressedsize(inbyte, out, length * 4); + } + + return in + length; + } + + std::string name() const { + if (delta) + return "HeadlessMaskedVByteDelta"; + else + return "HeadlessMaskedVByte"; + } + +private: + template uint8_t extract7bits(const uint32_t val) { + return static_cast((val >> (7 * i)) & ((1U << 7) - 1)); + } + + template uint8_t extract7bitsmaskless(const uint32_t val) { + return static_cast((val >> (7 * i))); + } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_SIMDVARIABLEBYTE_H_ */ diff --git a/include/SIMDCompressionAndIntersection/skipping.h b/include/SIMDCompressionAndIntersection/skipping.h new file mode 100644 index 0000000..3df98a4 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/skipping.h @@ -0,0 +1,292 @@ +/* + * This is a simple implementation of a skipping data structure and algorithms similar to + * what is described in + * + * Sanders and Transier, Intersection in Integer Inverted Indices, ALENEX 2007, 2007. + * + * As suggested in their conclusion, we leave the higher-level structure uncompressed. We also + * use differential coding. + * + * To paraphrase Sanders and Transier... + * + * In addition to a delta-encoded compressed list, a top-level data structure stores + * every B-th element of N in t together with its position in the main list (B is a tuning + * parameter). We can now run any search algorithm on t and then scan only the pieces of + * the main list that might contain an element to be located. + * + * In our implementation, we assume that B is a power of two and use 1 << BlockSizeLog as + * the block size. + * + * Sanders and Transier's proposal is similar in spirit to the skipping + * structure proposed in + * + * Moffat, A., Zobel, J.: Self-indexing inverted files for fast text retrieval. + * ACM Transactions on Information Systems 14 (1996). + * + * + * Author: Daniel Lemire + */ + +#ifndef SIMDCompressionAndIntersection_SKIPPING_H_ +#define SIMDCompressionAndIntersection_SKIPPING_H_ + +#include "common.h" + +namespace SIMDCompressionLib { + +class Skipping { +public: + Skipping(uint32_t BS, const uint32_t *data, uint32_t length) + : BlockSizeLog(BS), mainbuffer(), highbuffer(), Length(0) { + if ((BlockSizeLog == 0) && (BlockSizeLog >= 32)) + throw runtime_error("please use a reasonable BlockSizeLog"); + load(data, length); // cheap constructor + } + + ~Skipping() {} + + size_t storageInBytes() const { + return mainbuffer.size() * sizeof(uint8_t) + + highbuffer.size() * sizeof(higharraypair) + + sizeof(Length); // rough estimates (good enough) + } + + uint32_t decompress(uint32_t *out) const { + const uint8_t *bout = mainbuffer.data(); + uint32_t pos = 0; + + uint32_t val = 0; + for (uint32_t k = 0; k < Length; ++k) { + bout = decode(bout, val); + out[pos++] = val; + } + return pos; + } + + /** + * Intersects the current Skipping structure with a (small) uncompressed array + * and + * writes the answer to out. + */ + uint32_t intersect(const uint32_t *smallarray, uint32_t length, + uint32_t *out) const { + uint32_t intersectsize = 0; + const uint8_t *largemainpointer = mainbuffer.data(); + uint32_t largemainval = 0; + largemainpointer = decode(largemainpointer, largemainval); + uint32_t x = 0; + for (uint32_t k = 0; k < length; ++k) { + uint32_t val = smallarray[k]; + // if the last value of the current block is too small, skip the block + // entirely + if (highbuffer[x >> BlockSizeLog].first < val) { + do { + x = ((x >> BlockSizeLog) + 1) << BlockSizeLog; + if (x >= Length) { + return intersectsize; + } + } while (highbuffer[x >> BlockSizeLog].first < val); + largemainpointer = + mainbuffer.data() + highbuffer[x >> BlockSizeLog].second; + largemainval = highbuffer[(x >> BlockSizeLog) - 1].first; + largemainpointer = decode(largemainpointer, largemainval); + } + // at this point, we have that the last value of the current block is >= + // val + // this means that we shall decode at most one block before giving up + while (largemainval < val) { + ++x; + if (x >= Length) { + return intersectsize; + } + largemainpointer = decode(largemainpointer, largemainval); + } + if (largemainval == val) { + out[intersectsize++] = val; + } + } + return intersectsize; + } + uint32_t intersect(const Skipping &otherlarger, uint32_t *out) const { + // we assume that "this" is the smallest of the two + if (otherlarger.Length < Length) + return otherlarger.intersect(*this, out); + if (Length == 0) + return 0; // special silly case + assert(otherlarger.Length >= Length); + assert(otherlarger.Length > 0); + uint32_t intersectsize = 0; + + const uint8_t *inbyte = mainbuffer.data(); + const uint8_t *const endbyte = mainbuffer.data() + mainbuffer.size(); + const uint8_t *largemainpointer = otherlarger.mainbuffer.data(); + uint32_t largemainval = 0; + largemainpointer = decode(largemainpointer, largemainval); + uint32_t val = 0; // where I put decoded values + uint32_t x = 0; + while (endbyte > inbyte) { + inbyte = decode(inbyte, val); + // if the last value of the current block is too small, skip the block + // entirely + if (otherlarger.highbuffer[x >> otherlarger.BlockSizeLog].first < val) { + do { + x = ((x >> otherlarger.BlockSizeLog) + 1) << otherlarger.BlockSizeLog; + if (x >= otherlarger.Length) { + return intersectsize; + } + } while (otherlarger.highbuffer[x >> otherlarger.BlockSizeLog].first < + val); + largemainpointer = + otherlarger.mainbuffer.data() + + otherlarger.highbuffer[x >> otherlarger.BlockSizeLog].second; + largemainval = + otherlarger.highbuffer[(x >> otherlarger.BlockSizeLog) - 1].first; + largemainpointer = decode(largemainpointer, largemainval); + } + // at this point, we have that the last value of the current block is >= + // val + // this means that we shall decode at most one block before giving up + while (largemainval < val) { + ++x; + if (x >= otherlarger.Length) { + return intersectsize; + } + largemainpointer = decode(largemainpointer, largemainval); + } + if (largemainval == val) { + out[intersectsize++] = val; + } + } + return intersectsize; + } + + uint32_t BlockSizeLog; + vector mainbuffer; + typedef pair higharraypair; + + typedef vector higharray; + higharray highbuffer; + uint32_t Length; + + // please don't use the default constructor... + + Skipping() : BlockSizeLog(0), mainbuffer(), highbuffer(), Length(0) {} + +private: + Skipping(const Skipping &); + + // making it private on purpose + Skipping &operator=(const Skipping &); + + void load(const uint32_t *data, uint32_t length); + + template uint8_t extract7bits(const uint32_t val) { + return static_cast((val >> (7 * i)) & ((1U << 7) - 1)); + } + + template uint8_t extract7bitsmaskless(const uint32_t val) { + return static_cast((val >> (7 * i))); + } + static inline const uint8_t *decode(const uint8_t *buffer, uint32_t &prev) { + // manually unrolled for performance + uint32_t v = 0; + uint8_t c = *buffer++; + v += (c & 127); + if ((c & 128)) { + prev += v; + return buffer; + } + c = *buffer++; + v += ((c & 127) << 7); + if ((c & 128)) { + prev += v; + return buffer; + } + c = *buffer++; + v += ((c & 127) << 14); + if ((c & 128)) { + prev += v; + return buffer; + } + c = *buffer++; + v += ((c & 127) << 21); + if ((c & 128)) { + prev += v; + return buffer; + } + c = *buffer++; + v += ((c & 127) << 30); + prev += v; + return buffer; + } +}; + +void Skipping::load(const uint32_t *data, uint32_t len) { + assert(numeric_limits::max() < + (numeric_limits::max() / 5)); // check for overflow + Length = len; + if (Length == 0) + return; // nothing to do + uint32_t BlockNumber = (Length + (1 << BlockSizeLog) - 1) / + (1 << BlockSizeLog); // count full blocks + assert(BlockNumber << BlockSizeLog >= Length); + highbuffer.resize(BlockNumber); + mainbuffer.resize(5 * Length); + uint8_t *bout = mainbuffer.data(); + uint8_t *const boutinit = bout; + uint32_t prev = 0; + for (uint32_t k = 0; k < BlockNumber; ++k) { + const uint32_t howmany = (((k + 1) << BlockSizeLog) > Length) + ? Length - (k << BlockSizeLog) + : 1 << BlockSizeLog; + highbuffer[k] = make_pair(data[(k << BlockSizeLog) + howmany - 1], + static_cast(bout - boutinit)); + for (uint32_t x = 0; x < howmany; ++x) { + const uint32_t v = data[x + (k << BlockSizeLog)]; + const uint32_t val = v - prev; + prev = v; + if (val < (1U << 7)) { + *bout = static_cast(val | (1U << 7)); + ++bout; + } else if (val < (1U << 14)) { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bitsmaskless<1>(val) | (1U << 7); + ++bout; + } else if (val < (1U << 21)) { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bits<1>(val); + ++bout; + *bout = extract7bitsmaskless<2>(val) | (1U << 7); + ++bout; + } else if (val < (1U << 28)) { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bits<1>(val); + ++bout; + *bout = extract7bits<2>(val); + ++bout; + *bout = extract7bitsmaskless<3>(val) | (1U << 7); + ++bout; + } else { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bits<1>(val); + ++bout; + *bout = extract7bits<2>(val); + ++bout; + *bout = extract7bits<3>(val); + ++bout; + *bout = extract7bitsmaskless<4>(val) | (1U << 7); + ++bout; + } + } + } + mainbuffer.resize(static_cast(bout - boutinit)); + mainbuffer.shrink_to_fit(); +} + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_SKIPPING_H_ */ diff --git a/include/SIMDCompressionAndIntersection/sortedbitpacking.h b/include/SIMDCompressionAndIntersection/sortedbitpacking.h new file mode 100644 index 0000000..2d00e8b --- /dev/null +++ b/include/SIMDCompressionAndIntersection/sortedbitpacking.h @@ -0,0 +1,200 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire, http://lemire.me/en/ + */ + +#ifndef SIMDCompressionAndIntersection_SORTEDBITPACKING_H_ +#define SIMDCompressionAndIntersection_SORTEDBITPACKING_H_ + +#include "common.h" +#include "simdbitpacking.h" +#include "bitpackinghelpers.h" + +namespace SIMDCompressionLib { + +template CONST_FUNCTION static T *padTo128bits(T *inbyte) { + return reinterpret_cast((reinterpret_cast(inbyte) + 15) & + ~15); +} + +/** + * This is a minimalist class that allows you to store data + * in one of 32 "stores". Each store is for + * integers having bit width 1, 2..., 32 respectively. + * + * Design by D. Lemire + */ +class BasicSortedBitPacker { +public: + enum { DEFAULTSIZE = 128 }; // should be a multiple of 128 + uint32_t buffer[32]; + + static string name() { return "uBSBP"; } + + BasicSortedBitPacker() { + for (uint32_t i = 0; i < 32; ++i) { + data[i] = new uint32_t[DEFAULTSIZE]; + memset(data[i], 0, DEFAULTSIZE * sizeof(uint32_t)); + actualsizes[i] = DEFAULTSIZE; + } + clear(); + } + + void reset() { + for (uint32_t i = 0; i < 32; ++i) { + delete[] data[i]; + data[i] = new uint32_t[DEFAULTSIZE]; + memset(data[i], 0, DEFAULTSIZE * sizeof(uint32_t)); + actualsizes[i] = DEFAULTSIZE; + } + clear(); + } + + ~BasicSortedBitPacker() { free(); } + void free() { + clear(); + for (uint32_t i = 0; i < 32; ++i) + if (data[i] != NULL) { + delete[] data[i]; + data[i] = NULL; + actualsizes[i] = 0; + } + } + void directAppend(uint32_t i, uint32_t val) { data[i][sizes[i]++] = val; } + + const uint32_t *get(int i) { return data[i]; } + + void ensureCapacity(int i, uint32_t datatoadd) { + if (sizes[i] + datatoadd > actualsizes[i]) { + actualsizes[i] = (sizes[i] + datatoadd + 127) / 128 * 128 * + 2; // so we always get a multiple of 128 + uint32_t *tmp = new uint32_t[actualsizes[i]]; + for (uint32_t j = 0; j < sizes[i]; ++j) + tmp[j] = data[i][j]; + delete[] data[i]; + data[i] = tmp; + } + } + + void clear() { + for (uint32_t i = 0; i < 32; ++i) + sizes[i] = 0; // memset "might" be faster. + } + + uint32_t *write(uint32_t *out) { + uint32_t bitmap = 0; + for (uint32_t k = 1; k < 32; ++k) { + if (sizes[k] != 0) + bitmap |= (1U << k); + } + *(out++) = bitmap; + + for (uint32_t k = 1; k < 32; ++k) { + if (sizes[k] != 0) { + *out = sizes[k]; + out++; + uint32_t j = 0; + for (; j + 128 <= sizes[k]; j += 128) { + usimdpackwithoutmask(&data[k][j], reinterpret_cast<__m128i *>(out), + k + 1); + out += 4 * (k + 1); + } + // falling back on scalar + for (; j < sizes[k]; j += 32) { + BitPackingHelpers::fastpackwithoutmask(&data[k][j], out, k + 1); + out += k + 1; + } + out -= (j - sizes[k]) * (k + 1) / 32; + } + } + return out; + } + const uint32_t *read(const uint32_t *in) { + clear(); + const uint32_t bitmap = *(in++); + + for (uint32_t k = 1; k < 32; ++k) { + if ((bitmap & (1U << k)) != 0) { + sizes[k] = *in++; + if (actualsizes[k] < sizes[k]) { + delete[] data[k]; + actualsizes[k] = (sizes[k] + 127) / 128 * 128; + data[k] = new uint32_t[actualsizes[k]]; + } + uint32_t j = 0; + for (; j + 128 <= sizes[k]; j += 128) { + + usimdunpack(reinterpret_cast(in), &data[k][j], + k + 1); + in += 4 * (k + 1); + } + for (; j + 31 < sizes[k]; j += 32) { + BitPackingHelpers::fastunpack(in, &data[k][j], k + 1); + in += k + 1; + } + uint32_t remaining = sizes[k] - j; + memcpy(buffer, in, (remaining * (k + 1) + 31) / 32 * sizeof(uint32_t)); + uint32_t *bpointer = buffer; + in += ((sizes[k] + 31) / 32 * 32 - j) / 32 * (k + 1); + for (; j < sizes[k]; j += 32) { + BitPackingHelpers::fastunpack(bpointer, &data[k][j], k + 1); + bpointer += k + 1; + } + in -= (j - sizes[k]) * (k + 1) / 32; + } + } + return in; + } + + // for debugging + void sanityCheck() { + for (uint32_t k = 0; k < 32; ++k) { + if (sizes[k] > actualsizes[k]) { + cerr << "overflow at " << k << endl; + throw runtime_error("bug"); + } + if (sizes[k] != 0) { + cout << "k=" << k << endl; + uint32_t mask = 0u; + for (uint32_t j = 0; j < sizes[k]; ++j) { + cout << data[k][j] << " "; + mask |= data[k][j]; + } + cout << endl; + + if (gccbits(mask) > k + 1) { + cerr << "At " << (k + 1) << " we have " << gccbits(mask) << endl; + throw runtime_error("bug"); + } + } + } + } + + bool equals(const BasicSortedBitPacker &o) { + for (uint32_t k = 0; k < 32; ++k) { + if (sizes[k] != o.sizes[k]) { + return false; + } + for (uint32_t j = 0; j < sizes[k]; ++j) + if (data[k][j] != o.data[k][j]) { + return false; + } + } + return true; + } + +private: + uint32_t *data[32]; + uint32_t sizes[32]; + uint32_t actualsizes[32]; + + // we don't want anyone to start copying this class + BasicSortedBitPacker(const BasicSortedBitPacker &); + BasicSortedBitPacker &operator=(const BasicSortedBitPacker &); +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_SORTEDBITPACKING_H_ */ diff --git a/include/SIMDCompressionAndIntersection/streamvariablebyte.h b/include/SIMDCompressionAndIntersection/streamvariablebyte.h new file mode 100644 index 0000000..d575688 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/streamvariablebyte.h @@ -0,0 +1,247 @@ +#ifndef SIMDCompressionAndIntersection_STREAMVARIABLEBYTE_ +#define SIMDCompressionAndIntersection_STREAMVARIABLEBYTE_ + +#include "common.h" +#include "codecs.h" + +namespace SIMDCompressionLib { + +/** + * StreamVByte is an integer CODEC invented by Nathan Kurz. + */ + +extern "C" { +uint64_t svb_encode(uint8_t *out, const uint32_t *in, uint32_t count, int delta, + int type); +uint8_t *svb_decode_avx_simple(uint32_t *out, uint8_t *keyPtr, uint8_t *dataPtr, + uint64_t count); +uint8_t *svb_decode_avx_d1_simple(uint32_t *out, uint8_t *keyPtr, + uint8_t *dataPtr, uint64_t count); +uint8_t *svb_decode_scalar_d1_init(uint32_t *outPtr, const uint8_t *keyPtr, + uint8_t *dataPtr, uint32_t count, + uint32_t prev); +uint32_t svb_select_avx_d1_init(uint8_t *keyPtr, uint8_t *dataPtr, + uint64_t count, uint32_t prev, int slot); +int svb_find_avx_d1_init(uint8_t *keyPtr, uint8_t *dataPtr, uint64_t count, + uint32_t prev, uint32_t key, uint32_t *presult); +uint8_t *svb_insert_scalar_d1_init(uint8_t *keyPtr, uint8_t *dataPtr, + size_t dataSize, uint32_t count, + uint32_t prev, uint32_t new_key, + uint32_t *position); +uint8_t *svb_append_scalar_d1(uint8_t *keyPtr, uint8_t *dataPtr, + size_t sizebytes, size_t count, uint32_t delta); +} + +/** + * Regular StreamVByte (no differential coding) + */ +class StreamVByte : public IntegerCODEC { +public: + void encodeArray(uint32_t *in, const size_t count, uint32_t *out, + size_t &nvalue) { + uint64_t bytesWritten = svb_encode( + (uint8_t *)out, in, static_cast(std::min( + count, std::numeric_limits::max())), + 0, 1); + nvalue = static_cast(bytesWritten + 3) / 4; + } + + const uint32_t *decodeArray(const uint32_t *in, const size_t /* count */, + uint32_t *out, size_t &nvalue) { + uint32_t count = *(uint32_t *)in; // first 4 bytes is number of ints + nvalue = count; + if (count == 0) + return 0; + + uint8_t *keyPtr = (uint8_t *)in + 4; // full list of keys is next + uint32_t keyLen = ((count + 3) / 4); // 2-bits per key (rounded up) + uint8_t *dataPtr = keyPtr + keyLen; // data starts at end of keys + nvalue = count; + return reinterpret_cast( + (reinterpret_cast( + svb_decode_avx_simple(out, keyPtr, dataPtr, count)) + + 3) & + ~3); + } + + std::string name() const { return "streamvbyte"; } +}; + +/** + * StreamVByte with integrated differential coding + */ +class StreamVByteD1 : public IntegerCODEC { +public: + void encodeArray(uint32_t *in, const size_t count, uint32_t *out, + size_t &nvalue) { + uint32_t bytesWritten = static_cast( + svb_encode((uint8_t *)(out + 1), in, + static_cast(std::min( + count, std::numeric_limits::max())), + 1, 1)); + *out = 4 + bytesWritten; + nvalue = 1 + (bytesWritten + 3) / 4; + } + + void encodeToByteArray(uint32_t *in, const size_t count, uint8_t *out, + size_t &nvalue) { + uint32_t bytesWritten = static_cast( + svb_encode((uint8_t *)(out + 1), in, + static_cast(std::min( + count, std::numeric_limits::max())), + 1, 1)); + *out = 4 + bytesWritten; + nvalue = 4 + bytesWritten; + } + + const uint32_t *decodeArray(const uint32_t *in, const size_t /* count */, + uint32_t *out, size_t &nvalue) { + ++in; // number of encoded bytes + uint32_t count = *(uint32_t *)in; // next 4 bytes is number of ints + nvalue = count; + if (count == 0) + return 0; + + uint8_t *keyPtr = (uint8_t *)in + 4; // full list of keys is next + uint32_t keyLen = ((count + 3) / 4); // 2-bits per key (rounded up) + uint8_t *dataPtr = keyPtr + keyLen; // data starts at end of keys + return reinterpret_cast( + (reinterpret_cast( + svb_decode_avx_d1_simple(out, keyPtr, dataPtr, count)) + + 3) & + ~3); + } + + const uint8_t *decodeFromByteArray(const uint8_t *in, + const size_t /* count */, uint32_t *out, + size_t &nvalue) { + in += 4; // number of encoded bytes + uint32_t count = *(uint32_t *)in; // next 4 bytes is number of ints + nvalue = count; + if (count == 0) + return 0; + + uint8_t *keyPtr = (uint8_t *)in + 4; // full list of keys is next + uint32_t keyLen = ((count + 3) / 4); // 2-bits per key (rounded up) + uint8_t *dataPtr = keyPtr + keyLen; // data starts at end of keys + return svb_decode_avx_d1_simple(out, keyPtr, dataPtr, count); + } + + uint32_t select(const uint32_t *in, int slot) { + ++in; // number of encoded bytes + uint32_t count = *in; // next 4 bytes is number of ints + assert(slot < (int)count); + uint8_t *keyPtr = (uint8_t *)in + 4; // full list of keys is next + uint32_t keyLen = ((count + 3) / 4); // 2-bits per key (rounded up) + uint8_t *dataPtr = keyPtr + keyLen; // data starts at end of keys + return svb_select_avx_d1_init(keyPtr, dataPtr, count, 0, slot); + } + + uint32_t findLowerBound(const uint32_t *in, uint32_t /* count */, + uint32_t key, uint32_t *presult) { + ++in; // skip number of encoded bytes + uint32_t count = *(uint32_t *)in; // next 4 bytes is number of ints + uint8_t *keyPtr = (uint8_t *)in + 4; // full list of keys is next + uint32_t keyLen = ((count + 3) / 4); // 2-bits per key (rounded up) + uint8_t *dataPtr = keyPtr + keyLen; // data starts at end of keys + return (uint32_t)svb_find_avx_d1_init(keyPtr, dataPtr, count, 0, key, + presult); + } + + // append a key. Keys must be in sorted order. We assume that there is + // enough room and that delta encoding was used. + // Returns the new size of the compressed array *in bytes* + size_t appendToByteArray(uint8_t *in, const size_t /* length */, + uint32_t previous_key, uint32_t key) { + uint8_t *initin = in; + size_t size = *(uint32_t *)in; + in += 4; + size_t count = *(uint32_t *)in; + in += 4; + + // if the buffer is not yet initialized: pretend that the first 8 + // bytes are already occupied + if (size == 0) + size = 8; + + uint8_t *keyPtr = (uint8_t *)in; // full list of keys is next + uint32_t keyLen = + static_cast((count + 3) / 4); // 2-bits per key (rounded up) + uint8_t *dataPtr = keyPtr + keyLen; // data starts after the keys + size = svb_append_scalar_d1(keyPtr, dataPtr, size - 8, count, + key - previous_key) - + initin; + + // update 'size' and 'count' at the beginning of the buffer + in = initin; + *(uint32_t *)in = static_cast(size); + in += 4; + *(uint32_t *)in = static_cast(count + 1); + return size; + } + + // Inserts |key| into an encoded sequence. |encodedSize| is the total + // allocated size for |in| (in bytes). + // Returns the number of values written. + uint32_t insert(uint32_t *in, uint32_t, uint32_t key) { + uint32_t bytesEncoded = *in; + uint32_t count = *(in + 1); // first 4 bytes is number of ints + uint8_t *keyPtr = (uint8_t *)(in + 2); // full list of keys is next + // keyLen: 2-bits per key (rounded up), but at least 1 byte + uint32_t keyLen = count == 0 ? 1 : ((count + 3) / 4); + uint8_t *dataPtr = keyPtr + keyLen; // data starts at end of keys + uint32_t dataSize = (bytesEncoded - 8) - keyLen; + + // make space for the new key? + if (count > 0 && count % 4 == 0 && keyPtr + keyLen + 1 > dataPtr) { + memmove(dataPtr + 1, dataPtr, dataSize); + dataPtr++; + } + + *(in + 1) = count + 1; + + uint32_t position; + uint32_t bytesWritten = static_cast( + svb_insert_scalar_d1_init(keyPtr, dataPtr, dataSize, count, 0, key, + &position) - + (uint8_t *)in); + *in = bytesWritten; + return (bytesWritten + 3) / 4; + } + + // Inserts |key| into an encoded sequence. |encodedSize| is the total + // allocated size for |in| (in bytes). + // Returns the number of *bytes* written. + size_t insertInByteArray(uint8_t *inbyte, uint32_t, uint32_t key) { + uint32_t *in = (uint32_t *)inbyte; + uint32_t bytesEncoded = *in; + uint32_t count = *(in + 1); // first 4 bytes is number of ints + uint8_t *keyPtr = (uint8_t *)(in + 2); // full list of keys is next + // keyLen: 2-bits per key (rounded up), but at least 1 byte + uint32_t keyLen = count == 0 ? 1 : ((count + 3) / 4); + uint8_t *dataPtr = keyPtr + keyLen; // data starts at end of keys + uint32_t dataSize = (bytesEncoded - 8) - keyLen; + + // make space for the new key? + if (count > 0 && count % 4 == 0 && keyPtr + keyLen + 1 > dataPtr) { + memmove(dataPtr + 1, dataPtr, dataSize); + dataPtr++; + } + + *(in + 1) = count + 1; + + uint32_t position; + uint32_t bytesWritten = static_cast( + svb_insert_scalar_d1_init(keyPtr, dataPtr, dataSize, count, 0, key, + &position) - + (uint8_t *)in); + *in = bytesWritten; + return bytesWritten; + } + + std::string name() const { return "streamvbyte_d1"; } +}; + +} // namespace SIMDCompressionLib + +#endif // STREAMVARIABLEBYTE_ diff --git a/include/SIMDCompressionAndIntersection/synthetic.h b/include/SIMDCompressionAndIntersection/synthetic.h new file mode 100644 index 0000000..2f0a7a5 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/synthetic.h @@ -0,0 +1,389 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire + */ + +#ifndef SIMDCompressionAndIntersection_SYNTHETIC_H_ +#define SIMDCompressionAndIntersection_SYNTHETIC_H_ + +#include "common.h" +#include "util.h" +#include "mersenne.h" +#include "intersection.h" +#include "boolarray.h" + +namespace SIMDCompressionLib { + +using namespace std; + +vector generateArray(uint32_t N, const uint32_t mask = 0xFFFFFFFFU) { + vector ans(N); + for (size_t k = 0; k < N; ++k) + ans[k] = rand() & mask; + return ans; +} + +vector generateArray32(uint32_t N, + const uint32_t mask = 0xFFFFFFFFU) { + vector ans(N); + for (size_t k = 0; k < N; ++k) + ans[k] = rand() & mask; + return ans; +} + +class UniformDataGenerator { +public: + UniformDataGenerator(uint32_t seed = static_cast(time(NULL))) + : rand(seed) {} + + void negate(vector &in, vector &out, uint32_t Max) { + out.resize(Max - in.size()); + in.push_back(Max); + uint32_t i = 0; + size_t c = 0; + for (size_t j = 0; j < in.size(); ++j) { + const uint32_t v = in[j]; + for (; i < v; ++i) + out[c++] = i; + ++i; + } + assert(c == out.size()); + } + + /** + * fill the vector with N numbers uniformly picked from from 0 to Max, not + * including Max + * if it is not possible, an exception is thrown + */ + vector generateUniformHash(uint32_t N, uint32_t Max, + vector &ans) { + if (Max < N) + throw runtime_error( + "can't generate enough distinct elements in small interval"); + ans.clear(); + if (N == 0) + return ans; // nothing to do + ans.reserve(N); + assert(Max >= 1); + unordered_set s; + while (s.size() < N) + s.insert(rand.getValue(Max - 1)); + ans.assign(s.begin(), s.end()); + sort(ans.begin(), ans.end()); + assert(N == ans.size()); + return ans; + } + + void generateUniformBitmap(uint32_t N, uint32_t Max, vector &ans) { + if (Max < N) + throw runtime_error( + "can't generate enough distinct elements in small interval"); + assert(Max >= 1); + BoolArray bs(Max); + uint32_t card = 0; + while (card < N) { + uint32_t v = rand.getValue(Max - 1); + if (!bs.get(v)) { + bs.set(v); + ++card; + } + } + ans.resize(N); + bs.toArray(ans); + } + + void fastgenerateUniform(uint32_t N, uint32_t Max, vector &ans) { + if (2 * N > Max) { + vector buf(N); + fastgenerateUniform(Max - N, Max, buf); + negate(buf, ans, Max); + return; + } + if (N * 1024 > Max) { + generateUniformBitmap(N, Max, ans); + } + generateUniformHash(N, Max, ans); + } + + // Max value is excluded from range + vector generate(uint32_t N, uint32_t Max) { + vector ans; + ans.reserve(N); + fastgenerateUniform(N, Max, ans); + return ans; + } + ZRandom rand; +}; + +/* + * Reference: Vo Ngoc Anh and Alistair Moffat. 2010. Index compression using + * 64-bit words. Softw. Pract. Exper.40, 2 (February 2010), 131-147. + */ +class ClusteredDataGenerator { +public: + vector buffer; + UniformDataGenerator unidg; + ClusteredDataGenerator(uint32_t seed = static_cast(time(NULL))) + : buffer(), unidg(seed) {} + + // Max value is excluded from range + template + void fillUniform(iterator begin, iterator end, uint32_t Min, uint32_t Max) { + unidg.fastgenerateUniform(static_cast(end - begin), Max - Min, + buffer); + for (size_t k = 0; k < buffer.size(); ++k) + *(begin + k) = Min + buffer[k]; + } + + // Max value is excluded from range + // throws exception if impossible + template + void fillClustered(iterator begin, iterator end, uint32_t Min, uint32_t Max) { + const uint32_t N = static_cast(end - begin); + const uint32_t range = Max - Min; + if (range < N) + throw runtime_error("can't generate that many in small interval."); + assert(range >= N); + if ((range == N) or (N < 10)) { + fillUniform(begin, end, Min, Max); + return; + } + const uint32_t cut = N / 2 + unidg.rand.getValue(range - N); + assert(cut >= N / 2); + assert(Max - Min - cut >= N - N / 2); + const double p = unidg.rand.getDouble(); + assert(p <= 1); + assert(p >= 0); + if (p <= 0.25) { + fillUniform(begin, begin + N / 2, Min, Min + cut); + fillClustered(begin + N / 2, end, Min + cut, Max); + } else if (p <= 0.5) { + fillClustered(begin, begin + N / 2, Min, Min + cut); + fillUniform(begin + N / 2, end, Min + cut, Max); + } else { + fillClustered(begin, begin + N / 2, Min, Min + cut); + fillClustered(begin + N / 2, end, Min + cut, Max); + } + } + + // Max value is excluded from range + vector generate(uint32_t N, uint32_t Max) { + return generateClustered(N, Max); + } + + // Max value is excluded from range + vector generateClustered(uint32_t N, uint32_t Max) { + vector ans(N); + fillClustered(ans.begin(), ans.end(), 0, Max); + return ans; + } +}; + +class ZipfianGenerator { +public: + uint32_t n; + double zetan, theta; + vector proba; + + ZRandom rand; + ZipfianGenerator(uint32_t seed = static_cast(time(NULL))) + : n(0), zetan(0), theta(0), proba(n), rand(seed) {} + + void init(int _items, double _zipfianconstant = 1.0) { + n = _items; + if (_items == 0) + throw runtime_error("no items?"); + theta = _zipfianconstant; + if (theta > 0) { + zetan = 1 / zeta(n, theta); + proba.clear(); + proba.resize(n, 0); + proba[0] = zetan; + for (uint32_t i = 1; i < n; ++i) + proba[i] = proba[i - 1] + zetan / pow(i + 1, theta); + } else { + proba.resize(n, 1.0 / n); + } + } + + void seed(uint32_t s) { rand.seed(s); } + + ZipfianGenerator(int _items, double _zipfianconstant, + uint32_t seed = static_cast(time(NULL))) + : n(_items), zetan(0), theta(_zipfianconstant), proba(n), rand(seed) { + init(_items, _zipfianconstant); + } + + double zeta(int n, double theta) { + double sum = 0; + for (long i = 0; i < n; i++) { + sum += 1 / (pow(i + 1, theta)); + } + return sum; + } + int nextInt() { + // Map z to the value + const double u = rand.getDouble(); + return static_cast(lower_bound(proba.begin(), proba.end(), u) - + proba.begin()); + } +}; + +vector generateZipfianArray32(uint32_t N, double power, + const uint32_t mask = 0xFFFFFFFFU) { + vector ans(N); + ZipfianGenerator zipf; + const uint32_t MAXVALUE = 1U << 22; + zipf.init(mask > MAXVALUE - 1 ? MAXVALUE : mask + 1, power); + for (size_t k = 0; k < N; ++k) + ans[k] = zipf.nextInt(); + return ans; +} + +size_t unite(const uint32_t *set1, const size_t length1, const uint32_t *set2, + const size_t length2, uint32_t *out) { + size_t pos = 0; + size_t k1 = 0, k2 = 0; + if (0 == length1) { + for (size_t k = 0; k < length2; ++k) + out[k] = set2[k]; + return length2; + } + if (0 == length2) { + for (size_t k = 0; k < length1; ++k) + out[k] = set1[k]; + return length1; + } + while (true) { + if (set1[k1] < set2[k2]) { + out[pos++] = set1[k1]; + ++k1; + if (k1 >= length1) { + for (; k2 < length2; ++k2) + out[pos++] = set2[k2]; + break; + } + } else if (set1[k1] == set2[k2]) { + out[pos++] = set1[k1]; + ++k1; + ++k2; + if (k1 >= length1) { + for (; k2 < length2; ++k2) + out[pos++] = set2[k2]; + break; + } + if (k2 >= length2) { + for (; k1 < length1; ++k1) + out[pos++] = set1[k1]; + break; + } + } else { // if (set1[k1]>set2[k2]) { + out[pos++] = set2[k2]; + ++k2; + if (k2 >= length2) { + for (; k1 < length1; ++k1) + out[pos++] = set1[k1]; + break; + } + } + } + return pos; +} + +vector unite(const vector &x, const vector &y) { + vector ans(x.size() + y.size()); + ans.resize(unite(x.data(), x.size(), y.data(), y.size(), ans.data())); + return ans; +} + +size_t classicalintersection(const uint32_t *set1, const size_t length1, + const uint32_t *set2, const size_t length2, + uint32_t *out) { + if ((0 == length1) or (0 == length2)) + return 0; + size_t answer = 0; + size_t k1 = 0, k2 = 0; + while (true) { + if (set1[k1] < set2[k2]) { + ++k1; + if (k1 == length1) + return answer; + } else if (set2[k2] < set1[k1]) { + ++k2; + if (k2 == length2) + return answer; + } else { + // (set2[k2] == set1[k1]) + out[answer++] = set1[k1]; + ++k1; + if (k1 == length1) + break; + ++k2; + if (k2 == length2) + break; + } + } + return answer; +} + +vector intersect(const vector &x, + const vector &y) { + vector ans(x.size() + y.size()); + ans.resize(classicalintersection(x.data(), x.size(), y.data(), y.size(), + ans.data())); + return ans; +} +/** + * Generate a pair of arrays. One small, one larger. + * + * minlength: length of the smallest of the two arrays + * Max is the largest possible value + * sizeratio * minlength : length of the largest of the two arrays + * intersectionratio * minlength : length of the intersection + */ +template +pair, vector> +getPair(generator gen, uint32_t minlength, uint32_t Max, float sizeratio, + float intersectionratio) { + if (sizeratio < 1) + throw runtime_error("sizeratio should be larger or equal to 1"); + if (intersectionratio < 0) + throw runtime_error("intersectionratio should be positive"); + if (intersectionratio > 1) + throw runtime_error("intersectionratio cannot be larger than 1"); + const uint32_t maxlenth = + static_cast(round(static_cast(minlength) * sizeratio)); + if (maxlenth > Max) + throw runtime_error( + "I can't generate an array so large in such a small range."); + if (maxlenth < minlength) + throw runtime_error("something went wrong, possibly an overflow."); + // we basically assume that, if we do nothing, intersections are very small + const uint32_t intersize = static_cast( + round(static_cast(minlength) * intersectionratio)); + + vector inter = gen.generate(intersize, Max); + vector smallest = + unite(gen.generate(static_cast(minlength - inter.size()), Max), + inter); + vector largest = unite( + gen.generate(static_cast(maxlenth - inter.size()), Max), inter); + vector intersection = intersect(smallest, largest); + + if (abs(static_cast(intersection.size()) / + static_cast(smallest.size()) - + intersectionratio) > 0.05) + throw runtime_error("Bad intersection ratio. Fix me."); + + if (abs(static_cast(largest.size()) / + static_cast(smallest.size()) - + sizeratio) > 0.05) + throw runtime_error("Bad size ratio. Fix me."); + return pair, vector>(smallest, largest); +} + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_SYNTHETIC_H_ */ diff --git a/include/SIMDCompressionAndIntersection/timer.h b/include/SIMDCompressionAndIntersection/timer.h new file mode 100644 index 0000000..9a46611 --- /dev/null +++ b/include/SIMDCompressionAndIntersection/timer.h @@ -0,0 +1,85 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + */ + +#ifndef SIMDCompressionAndIntersection_TIMER_H_ +#define SIMDCompressionAndIntersection_TIMER_H_ + +#include +#include +#include + +namespace SIMDCompressionLib { + +class WallClockTimer { +public: +#ifdef _WIN32 + typedef qpc_clock clock; +#else + typedef std::chrono::high_resolution_clock clock; +#endif + + std::chrono::time_point t1, t2; + WallClockTimer() : t1(), t2() { + t1 = clock::now(); + t2 = t1; + } + void reset() { + t1 = clock::now(); + t2 = t1; + } + uint64_t elapsed() { + std::chrono::microseconds delta = + std::chrono::duration_cast(t2 - t1); + return delta.count(); + } + uint64_t split() { + t2 = clock::now(); + return elapsed(); + } +}; + +#ifndef _WIN32 + +class CPUTimer { +public: + // clock_t t1, t2; + struct rusage t1, t2; + + CPUTimer() : t1(), t2() { + getrusage(RUSAGE_SELF, &t1); + // t1 = clock(); + t2 = t1; + } + void reset() { + getrusage(RUSAGE_SELF, &t1); + t2 = t1; + } + // proxy for userelapsed + uint64_t elapsed() { return totalelapsed(); } + + uint64_t totalelapsed() { return userelapsed() + systemelapsed(); } + // returns the *user* CPU time in micro seconds (mu s) + uint64_t userelapsed() { + return ((t2.ru_utime.tv_sec - t1.ru_utime.tv_sec) * 1000ULL * 1000ULL) + + ((t2.ru_utime.tv_usec - t1.ru_utime.tv_usec)); + } + + // returns the *system* CPU time in micro seconds (mu s) + uint64_t systemelapsed() { + return ((t2.ru_stime.tv_sec - t1.ru_stime.tv_sec) * 1000ULL * 1000ULL) + + ((t2.ru_stime.tv_usec - t1.ru_stime.tv_usec)); + } + + uint64_t split() { + getrusage(RUSAGE_SELF, &t2); + return elapsed(); + } +}; +#endif + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_TIMER_H_ */ diff --git a/include/SIMDCompressionAndIntersection/usimdbitpacking.h b/include/SIMDCompressionAndIntersection/usimdbitpacking.h new file mode 100644 index 0000000..8822bea --- /dev/null +++ b/include/SIMDCompressionAndIntersection/usimdbitpacking.h @@ -0,0 +1,150 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire + */ +#ifndef SIMDCompressionAndIntersection_USIMDBITPACKING_H_ +#define SIMDCompressionAndIntersection_USIMDBITPACKING_H_ + +#include "common.h" + +namespace SIMDCompressionLib { + +void __uSIMD_fastunpack1(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack2(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack3(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack4(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack5(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack6(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack7(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack8(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack9(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack10(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack11(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack12(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack13(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack14(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack15(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack16(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack17(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack18(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack19(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack20(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack21(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack22(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack23(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack24(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack25(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack26(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack27(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack28(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack29(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack30(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack31(const __m128i *__restrict__, uint32_t *__restrict__); +void __uSIMD_fastunpack32(const __m128i *__restrict__, uint32_t *__restrict__); + +void __uSIMD_fastpackwithoutmask0(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask1(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask2(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask3(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask4(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask5(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask6(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask7(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask8(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask9(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask10(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask11(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask12(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask13(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask14(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask15(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask16(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask17(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask18(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask19(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask20(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask21(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask22(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask23(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask24(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask25(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask26(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask27(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask28(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask29(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask30(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask31(const uint32_t *__restrict__, + __m128i *__restrict__); +void __uSIMD_fastpackwithoutmask32(const uint32_t *__restrict__, + __m128i *__restrict__); + +void __uSIMD_fastpack0(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack1(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack2(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack3(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack4(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack5(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack6(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack7(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack8(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack9(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack10(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack11(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack12(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack13(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack14(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack15(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack16(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack17(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack18(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack19(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack20(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack21(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack22(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack23(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack24(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack25(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack26(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack27(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack28(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack29(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack30(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack31(const uint32_t *__restrict__, __m128i *__restrict__); +void __uSIMD_fastpack32(const uint32_t *__restrict__, __m128i *__restrict__); + +} // namespace SIMDCompressionLib + +#endif /* SIMDBITPACKING_H_ */ diff --git a/include/SIMDCompressionAndIntersection/util.h b/include/SIMDCompressionAndIntersection/util.h new file mode 100644 index 0000000..f94b7dd --- /dev/null +++ b/include/SIMDCompressionAndIntersection/util.h @@ -0,0 +1,126 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire + */ + +#ifndef SIMDCompressionAndIntersection_UTIL_H_ +#define SIMDCompressionAndIntersection_UTIL_H_ + +#include "common.h" + +namespace SIMDCompressionLib { + +inline uint32_t random(int b) { + if (b == 32) + return rand(); + return rand() % (1U << b); +} + +// taken from stackoverflow +#ifndef NDEBUG +#define ASSERT(condition, message) \ + do { \ + if (!(condition)) { \ + std::cerr << "Assertion `" #condition "` failed in " << __FILE__ \ + << " line " << __LINE__ << ": " << message << std::endl; \ + std::exit(EXIT_FAILURE); \ + } \ + } while (false) +#else +#define ASSERT(condition, message) \ + do { \ + } while (false) +#endif + +CONST_FUNCTION +inline uint32_t gccbits(const uint32_t v) { + return v == 0 ? 0 : 32 - __builtin_clz(v); +} + +/** + * Treats __m128i as 4 x 32-bit integers and asks for the max + * number of bits used (integer logarithm). + */ +inline uint32_t maxbitas32int(const __m128i accumulator) { + SIMDCOMP_ALIGNED(16) uint32_t tmparray[4]; + MM_STORE_SI_128(reinterpret_cast<__m128i *>(tmparray), accumulator); + return gccbits(tmparray[0] | tmparray[1] | tmparray[2] | tmparray[3]); +} + +static CONST_FUNCTION bool divisibleby(size_t a, uint32_t x) { + return (a % x == 0); +} + +#ifdef __GNUC__ +__attribute__((unused)) +#endif +static void +checkifdivisibleby(size_t a, uint32_t x) { + if (!divisibleby(a, x)) { + std::ostringstream convert; + convert << a << " not divisible by " << x; + throw std::logic_error(convert.str()); + } +} + +template +PURE_FUNCTION uint32_t maxbits(const iterator &begin, const iterator &end) { + uint32_t accumulator = 0; + for (iterator k = begin; k != end; ++k) { + accumulator |= *k; + } + return gccbits(accumulator); +} + +template +CONST_FUNCTION inline bool needPaddingTo128Bits(const T *inbyte) { + return (reinterpret_cast(inbyte) & 15) != 0; +} + +template +CONST_FUNCTION inline bool needPaddingTo32Bits(const T *inbyte) { + return (reinterpret_cast(inbyte) & 3) != 0; +} + +template CONST_FUNCTION T *padTo32bits(T *inbyte) { + return reinterpret_cast((reinterpret_cast(inbyte) + 3) & ~3); +} + +template CONST_FUNCTION const T *padTo32bits(const T *inbyte) { + return reinterpret_cast((reinterpret_cast(inbyte) + 3) & + ~3); +} + +#ifndef _MSC_VER +CONST_FUNCTION +inline uint32_t asmbits(const uint32_t v) { + if (v == 0) + return 0; + uint32_t answer; + __asm__("bsr %1, %0;" : "=r"(answer) : "r"(v)); + return answer + 1; +} +#else +inline uint32_t asmbits(const uint32_t v) { + unsigned long index; + return (v == 0 || _BitScanReverse(&index, v) == 0) ? 0 : (index + 1); +} +#endif + +template +bool is_strictlysorted(iterator first, iterator last) { + iterator next = first; + ++next; + while (next < last) { + if (*first >= *next) + return false; + ++first; + ++next; + } + return true; +} +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_UTIL_H_ */ diff --git a/include/SIMDCompressionAndIntersection/variablebyte.h b/include/SIMDCompressionAndIntersection/variablebyte.h new file mode 100644 index 0000000..454000d --- /dev/null +++ b/include/SIMDCompressionAndIntersection/variablebyte.h @@ -0,0 +1,1328 @@ +/** + * This code is released under the + * Apache License Version 2.0 http://www.apache.org/licenses/. + * + * (c) Daniel Lemire, http://lemire.me/en/ + */ + +#ifndef SIMDCompressionAndIntersection_VARIABLEBYTE_H_ +#define SIMDCompressionAndIntersection_VARIABLEBYTE_H_ +#include "common.h" +#include "codecs.h" +#include "util.h" + +namespace SIMDCompressionLib { + +/*** + * VariableByte and VByte are basically identical, except that + * one uses 0..0..0..1 to indicate 4 whereas the other one uses 1..1..1..0. + * The latter is maybe more common. + */ + +template class VariableByte : public IntegerCODEC { +public: + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + uint8_t *bout = reinterpret_cast(out); + const uint8_t *const initbout = reinterpret_cast(out); + size_t bytenvalue = nvalue * sizeof(uint32_t); + encodeToByteArray(in, length, bout, bytenvalue); + bout += bytenvalue; + while (needPaddingTo32Bits(bout)) { + *bout++ = 0; + } + const size_t storageinbytes = bout - initbout; + assert((storageinbytes % 4) == 0); + nvalue = storageinbytes / 4; + } + + // write one compressed integer (without differential coding) + // returns the number of bytes written + size_t encodeOneIntegerToByteArray(uint32_t val, uint8_t *bout) { + const uint8_t *const initbout = bout; + if (val < (1U << 7)) { + *bout = static_cast(val | (1U << 7)); + ++bout; + } else if (val < (1U << 14)) { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bitsmaskless<1>(val) | (1U << 7); + ++bout; + } else if (val < (1U << 21)) { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bits<1>(val); + ++bout; + *bout = extract7bitsmaskless<2>(val) | (1U << 7); + ++bout; + } else if (val < (1U << 28)) { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bits<1>(val); + ++bout; + *bout = extract7bits<2>(val); + ++bout; + *bout = extract7bitsmaskless<3>(val) | (1U << 7); + ++bout; + } else { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bits<1>(val); + ++bout; + *bout = extract7bits<2>(val); + ++bout; + *bout = extract7bits<3>(val); + ++bout; + *bout = extract7bitsmaskless<4>(val) | (1U << 7); + ++bout; + } + return bout - initbout; + } + + void encodeToByteArray(uint32_t *in, const size_t length, uint8_t *bout, + size_t &nvalue) { + const uint8_t *const initbout = bout; + uint32_t prev = 0; + for (size_t k = 0; k < length; ++k) { + const uint32_t val = delta ? in[k] - prev : in[k]; + if (delta) + prev = in[k]; + /** + * Code below could be shorter. Whether it could be faster + * depends on your compiler and machine. + */ + if (val < (1U << 7)) { + *bout = static_cast(val | (1U << 7)); + ++bout; + } else if (val < (1U << 14)) { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bitsmaskless<1>(val) | (1U << 7); + ++bout; + } else if (val < (1U << 21)) { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bits<1>(val); + ++bout; + *bout = extract7bitsmaskless<2>(val) | (1U << 7); + ++bout; + } else if (val < (1U << 28)) { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bits<1>(val); + ++bout; + *bout = extract7bits<2>(val); + ++bout; + *bout = extract7bitsmaskless<3>(val) | (1U << 7); + ++bout; + } else { + *bout = extract7bits<0>(val); + ++bout; + *bout = extract7bits<1>(val); + ++bout; + *bout = extract7bits<2>(val); + ++bout; + *bout = extract7bits<3>(val); + ++bout; + *bout = extract7bitsmaskless<4>(val) | (1U << 7); + ++bout; + } + } + nvalue = bout - initbout; + } + + const uint32_t *decodeArray(const uint32_t *in, const size_t length, + uint32_t *out, size_t &nvalue) { + decodeFromByteArray((const uint8_t *)in, length * sizeof(uint32_t), out, + nvalue); + return in + length; + } + + // determine how many padding bytes were used + int paddingBytes(const uint32_t *in, const size_t length) { + if (length == 0) + return 0; + uint32_t lastword = in[length - 1]; + if (lastword < (1U << 8)) { + return 3; + } else if (lastword < (1U << 16)) { + return 2; + } else if (lastword < (1U << 24)) { + return 1; + } + return 0; + } + + // how many bytes are required to store this integer? + int storageCost(uint32_t val) { + if (val < (1U << 7)) { + return 1; + } else if (val < (1U << 14)) { + return 2; + } else if (val < (1U << 21)) { + return 3; + } else if (val < (1U << 28)) { + return 4; + } else { + return 5; + } + } + + const uint8_t *decodeFromByteArray(const uint8_t *inbyte, const size_t length, + uint32_t *out, size_t &nvalue) { + uint32_t prev = 0; + if (length == 0) { + nvalue = 0; + return inbyte; // abort + } + const uint8_t *const endbyte = inbyte + length; + const uint32_t *const initout(out); + // this assumes that there is a value to be read + + while (endbyte > inbyte + 5) { + if (delta) { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c >= 128) { + inbyte += 1; + *out++ = (prev = v + prev); + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c >= 128) { + inbyte += 2; + *out++ = (prev = v + prev); + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c >= 128) { + inbyte += 3; + *out++ = (prev = v + prev); + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c >= 128) { + inbyte += 4; + *out++ = (prev = v + prev); + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + *out++ = (prev = v + prev); + } else { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c >= 128) { + inbyte += 1; + *out++ = v; + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c >= 128) { + inbyte += 2; + *out++ = v; + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c >= 128) { + inbyte += 3; + *out++ = v; + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c >= 128) { + inbyte += 4; + *out++ = v; + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + *out++ = v; + } + } + while (endbyte > inbyte) { + unsigned int shift = 0; + for (uint32_t v = 0; endbyte > inbyte; shift += 7) { + uint8_t c = *inbyte++; + v += ((c & 127) << shift); + if ((c & 128)) { + *out++ = delta ? (prev = v + prev) : v; + break; + } + } + } + nvalue = out - initout; + return inbyte; + } + + // Performs a lower bound find in the encoded array. + // length is the size of the compressed input + // Returns the index + size_t findLowerBound(const uint32_t *in, const size_t length, uint32_t key, + uint32_t *presult) { + uint32_t prev = 0; + if (length == 0) { + return 0; // abort + } + const uint8_t *inbyte = reinterpret_cast(in); + const uint8_t *const endbyte = + reinterpret_cast(in + length); + size_t i = 0; + // this assumes that there is a value to be read + + while (endbyte > inbyte + 5) { + if (delta) { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c >= 128) { + inbyte += 1; + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + i++; + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c >= 128) { + inbyte += 2; + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + i++; + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c >= 128) { + inbyte += 3; + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + i++; + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c >= 128) { + inbyte += 4; + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + i++; + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + i++; + } else { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c >= 128) { + inbyte += 1; + if (v >= key) { + *presult = v; + return i; + } + i++; + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c >= 128) { + inbyte += 2; + if (v >= key) { + *presult = v; + return i; + } + i++; + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c >= 128) { + inbyte += 3; + if (v >= key) { + *presult = v; + return i; + } + i++; + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c >= 128) { + inbyte += 4; + if (v >= key) { + *presult = v; + return i; + } + i++; + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + if (v >= key) { + *presult = v; + return i; + } + i++; + } + } + while (endbyte > inbyte) { + unsigned int shift = 0; + for (uint32_t v = 0; endbyte > inbyte; shift += 7) { + uint8_t c = *inbyte++; + v += ((c & 127) << shift); + if ((c & 128)) { + if (delta) { + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + } else { + if (v >= key) { + *presult = v; + return i; + } + } + i++; + break; + } + } + } + return i; + } + + // append a key. Keys must be in sorted order. We assume that there is + // enough room and that delta encoding was used. + // Returns the new size of the compressed array *in bytes* + size_t appendToByteArray(uint8_t *in, const size_t bytesize, + uint32_t previous_key, uint32_t key) { + assert(delta); // no performance impact expected. + uint8_t *byteininit = (uint8_t *)in; + uint8_t *bytein = (uint8_t *)in + bytesize; + bytein += encodeOneIntegerToByteArray(key - previous_key, bytein); + return bytein - byteininit; + } + + // insert the key in sorted order. We assume that there is enough room and + // that delta encoding was used. + size_t insert(uint32_t *in, const size_t length, uint32_t key) { + size_t bytesize = length * 4; + bytesize -= paddingBytes(in, length); + uint8_t *bytein = (uint8_t *)in; + uint8_t *byteininit = bytein; + size_t bl = insertInByteArray(bytein, bytesize, key); + bytein += bl; + + while (needPaddingTo32Bits(bytein)) { + *bytein++ = 0; + } + size_t storageinbytes = bytein - byteininit; + assert((storageinbytes % 4) == 0); + return storageinbytes / 4; + } + + // insert the key in sorted order. We assume that there is enough room and + // that delta encoding was used. + // the new size is returned + size_t insertInByteArray(uint8_t *inbyte, const size_t length, uint32_t key) { + uint32_t prev = 0; + assert(delta); + const uint8_t *const endbyte = + reinterpret_cast(inbyte + length); + // this assumes that there is a value to be read + + while (endbyte > inbyte + 5) { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c >= 128) { + inbyte += 1; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c >= 128) { + inbyte += 2; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c >= 128) { + inbyte += 3; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c >= 128) { + inbyte += 4; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + prev = v + prev; + if (prev >= key) { + return length + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + } + while (endbyte > inbyte) { + unsigned int shift = 0; + for (uint32_t v = 0; endbyte > inbyte; shift += 7) { + uint8_t c = *inbyte++; + v += ((c & 127) << shift); + if ((c & 128)) { + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + break; + } + } + } + // if we make it here, then we need to append + assert(key >= prev); + return length + encodeOneIntegerToByteArray(key - prev, inbyte); + } + + // Returns a decompressed value in an encoded array + // could be greatly optimized in the non-differential coding case: currently + // just for delta coding + uint32_t select(uint32_t *in, size_t index) { + assert(delta); + uint32_t prev = 0; + size_t i = 0; + const uint8_t *inbyte = reinterpret_cast(in); + while (i <= index) { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c >= 128) { + inbyte += 1; + prev = v + prev; + i++; + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c >= 128) { + inbyte += 2; + prev = v + prev; + i++; + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c >= 128) { + inbyte += 3; + prev = v + prev; + i++; + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c >= 128) { + inbyte += 4; + prev = v + prev; + i++; + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + prev = v + prev; + i++; + } + assert(i == index + 1); + return prev; + } + + string name() const { + if (delta) + return "VariableByteDelta"; + else + return "VariableByte"; + } + +private: + // convenience function used by insert, writes key and newvalue to compressed + // stream, and return + // extra storage used, pointer should be right after where nextvalue is right + // now + size_t __insert(uint8_t *in, uint32_t previous, uint32_t key, + uint32_t nextvalue, size_t followingbytes) { + assert(nextvalue >= key); + assert(key >= previous); + size_t oldstorage = storageCost(nextvalue - previous); + size_t newstorage = + storageCost(nextvalue - key) + storageCost(key - previous); + assert(newstorage >= oldstorage); + if (newstorage > oldstorage) + std::memmove(in + newstorage - oldstorage, in, followingbytes); + uint8_t *newin = in - oldstorage; + newin += encodeOneIntegerToByteArray(key - previous, newin); + newin += encodeOneIntegerToByteArray(nextvalue - key, newin); + assert(newin == in + newstorage - oldstorage); + return newstorage - oldstorage; + } + + template uint8_t extract7bits(const uint32_t val) { + return static_cast((val >> (7 * i)) & ((1U << 7) - 1)); + } + + template uint8_t extract7bitsmaskless(const uint32_t val) { + return static_cast((val >> (7 * i))); + } +}; + +template class VByte : public IntegerCODEC { +public: + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + uint8_t *bout = reinterpret_cast(out); + const uint8_t *const initbout = reinterpret_cast(out); + size_t bytenvalue = nvalue * sizeof(uint32_t); + encodeToByteArray(in, length, bout, bytenvalue); + bout += bytenvalue; + while (needPaddingTo32Bits(bout)) { + *bout++ = 0xFF; + } + const size_t storageinbytes = bout - initbout; + assert((storageinbytes % 4) == 0); + nvalue = storageinbytes / 4; + } + + void encodeToByteArray(uint32_t *in, const size_t length, uint8_t *bout, + size_t &nvalue) { + uint32_t prev = 0; + const uint8_t *const initbout = bout; + for (size_t k = 0; k < length; ++k) { + const uint32_t val = delta ? (in[k] - prev) : in[k]; + if (delta) + prev = in[k]; + /** + * Code below could be shorter. Whether it could be faster + * depends on your compiler and machine. + */ + if (val < (1U << 7)) { + *bout = val & 0x7F; + ++bout; + } else if (val < (1U << 14)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 7); + ++bout; + } else if (val < (1U << 21)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 14); + ++bout; + } else if (val < (1U << 28)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 14) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 21); + ++bout; + } else { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 14) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 21) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 28); + ++bout; + } + } + nvalue = bout - initbout; + } + + // write one compressed integer (without differential coding) + // returns the number of bytes written + size_t encodeOneIntegerToByteArray(uint32_t val, uint8_t *bout) { + const uint8_t *const initbout = bout; + if (val < (1U << 7)) { + *bout = val & 0x7F; + ++bout; + } else if (val < (1U << 14)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 7); + ++bout; + } else if (val < (1U << 21)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 14); + ++bout; + } else if (val < (1U << 28)) { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 14) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 21); + ++bout; + } else { + *bout = static_cast((val & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 7) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 14) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(((val >> 21) & 0x7F) | (1U << 7)); + ++bout; + *bout = static_cast(val >> 28); + ++bout; + } + return bout - initbout; + } + + // determine how many padding bytes were used + int paddingBytes(const uint32_t *in, const size_t length) { + if (length == 0) + return 0; + uint32_t lastword = in[length - 1]; + lastword = ~lastword; + if (lastword < (1U << 8)) { + return 3; + } else if (lastword < (1U << 16)) { + return 2; + } else if (lastword < (1U << 24)) { + return 1; + } + return 0; + } + + const uint32_t *decodeArray(const uint32_t *in, const size_t length, + uint32_t *out, size_t &nvalue) { + decodeFromByteArray((const uint8_t *)in, length * sizeof(uint32_t), out, + nvalue); + return in + length; + } + + // how many bytes are required to store this integer? + int storageCost(uint32_t val) { + if (val < (1U << 7)) { + return 1; + } else if (val < (1U << 14)) { + return 2; + } else if (val < (1U << 21)) { + return 3; + } else if (val < (1U << 28)) { + return 4; + } else { + return 5; + } + } + + const uint8_t *decodeFromByteArray(const uint8_t *inbyte, const size_t length, + uint32_t *out, size_t &nvalue) { + uint32_t prev = 0; + if (length == 0) { + nvalue = 0; + return inbyte; // abort + } + const uint8_t *const endbyte = inbyte + length; + const uint32_t *const initout(out); + // this assumes that there is a value to be read + + while (endbyte > inbyte + 5) { + if (delta) { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c < 128) { + inbyte += 1; + *out++ = (prev = v + prev); + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c < 128) { + inbyte += 2; + *out++ = (prev = v + prev); + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c < 128) { + inbyte += 3; + *out++ = (prev = v + prev); + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c < 128) { + inbyte += 4; + *out++ = (prev = v + prev); + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + *out++ = (prev = v + prev); + } else { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c < 128) { + inbyte += 1; + *out++ = v; + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c < 128) { + inbyte += 2; + *out++ = v; + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c < 128) { + inbyte += 3; + *out++ = v; + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c < 128) { + inbyte += 4; + *out++ = v; + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + *out++ = v; + } + } + while (endbyte > inbyte) { + unsigned int shift = 0; + for (uint32_t v = 0; endbyte > inbyte; shift += 7) { + uint8_t c = *inbyte++; + v += ((c & 127) << shift); + if ((c < 128)) { + *out++ = delta ? (prev = v + prev) : v; + break; + } + } + } + nvalue = out - initout; + return inbyte; + } + + // Performs a lower bound find in the encoded array. + // length is the size of the compressed input + // Returns the index and the value found (presult) + size_t findLowerBound(const uint32_t *in, const size_t length, uint32_t key, + uint32_t *presult) { + uint32_t prev = 0; + if (length == 0) { + return 0; // abort + } + size_t i = 0; + const uint8_t *inbyte = reinterpret_cast(in); + const uint8_t *const endbyte = + reinterpret_cast(in + length); + // this assumes that there is a value to be read + + while (endbyte > inbyte + 5) { + if (delta) { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c < 128) { + inbyte += 1; + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + i++; + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c < 128) { + inbyte += 2; + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + i++; + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c < 128) { + inbyte += 3; + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + i++; + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c < 128) { + inbyte += 4; + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + i++; + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + i++; + } else { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c < 128) { + inbyte += 1; + if (v >= key) { + *presult = v; + return i; + } + i++; + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c < 128) { + inbyte += 2; + if (v >= key) { + *presult = v; + return i; + } + i++; + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c < 128) { + inbyte += 3; + if (v >= key) { + *presult = v; + return i; + } + i++; + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c < 128) { + inbyte += 4; + if (v >= key) { + *presult = v; + return i; + } + i++; + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + if (v >= key) { + *presult = v; + return i; + } + i++; + } + } + while (endbyte > inbyte) { + unsigned int shift = 0; + for (uint32_t v = 0; endbyte > inbyte; shift += 7) { + uint8_t c = *inbyte++; + v += ((c & 127) << shift); + if ((c < 128)) { + if (delta) { + prev = v + prev; + if (prev >= key) { + *presult = prev; + return i; + } + } else { + if (v >= key) { + *presult = v; + return i; + } + } + i++; + break; + } + } + } + return i; + } + + // append a key. Keys must be in sorted order. We assume that there is + // enough room and that delta encoding was used. + /*size_t append(uint32_t *in, const size_t length, uint32_t previous_key, + uint32_t key) { + size_t bytesize = (length * 4) - paddingBytes(in, length); + uint8_t *byteininit = (uint8_t *)in; + uint8_t *bytein = (uint8_t *)in + bytesize; + bytein += encodeOneIntegerToByteArray(key - previous_key, bytein); + while (needPaddingTo32Bits(bytein)) { + *bytein++ = 0xFF; + } + size_t storageinbytes = bytein - byteininit; + assert((storageinbytes % 4) == 0); + return storageinbytes / 4; + }*/ + + // append a key. Keys must be in sorted order. We assume that there is + // enough room and that delta encoding was used. + // Returns the new size of the compressed array *in bytes* + size_t appendToByteArray(uint8_t *in, const size_t bytesize, + uint32_t previous_key, uint32_t key) { + assert(delta); // no performance impact expected. + uint8_t *byteininit = (uint8_t *)in; + uint8_t *bytein = (uint8_t *)in + bytesize; + bytein += encodeOneIntegerToByteArray(key - previous_key, bytein); + return bytein - byteininit; + } + + // insert the key in sorted order. We assume that there is enough room + // and that delta encoding was used. + size_t insert(uint32_t *in, const size_t length, uint32_t key) { + assert(delta); + size_t bytesize = length * 4; + bytesize -= paddingBytes(in, length); + uint8_t *bytein = (uint8_t *)in; + uint8_t *byteininit = bytein; + bytein += insertInByteArray(bytein, bytesize, key); + + while (needPaddingTo32Bits(bytein)) { + *bytein++ = 0xFF; + } + size_t storageinbytes = bytein - byteininit; + assert((storageinbytes % 4) == 0); + return storageinbytes / 4; + } + + // insert the key in sorted order. We assume that there is enough room and + // that delta encoding was used. + // the new size (in *byte) is returned + size_t insertInByteArray(uint8_t *inbyte, const size_t length, uint32_t key) { + uint32_t prev = 0; + assert(delta); + const uint8_t *const endbyte = + reinterpret_cast(inbyte + length); + // this assumes that there is a value to be read + + while (endbyte > inbyte + 5) { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c < 128) { + inbyte += 1; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c < 128) { + inbyte += 2; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c < 128) { + inbyte += 3; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c < 128) { + inbyte += 4; + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + prev = v + prev; + if (prev >= key) { + return length + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + } + while (endbyte > inbyte) { + unsigned int shift = 0; + for (uint32_t v = 0; endbyte > inbyte; shift += 7) { + uint8_t c = *inbyte++; + v += ((c & 127) << shift); + if ((c < 128)) { + prev = v + prev; + if (prev >= key) { + return length + + __insert(inbyte, prev - v, key, prev, endbyte - inbyte); + } + break; + } + } + } + // if we make it here, then we need to append + assert(key >= prev); + return length + encodeOneIntegerToByteArray(key - prev, inbyte); + } + + // Returns a decompressed value in an encoded array + // could be greatly optimized in the non-differential coding case: currently + // just for delta coding + uint32_t select(uint32_t *in, size_t index) { + assert(delta); + uint32_t prev = 0; + size_t i = 0; + const uint8_t *inbyte = reinterpret_cast(in); + + while (i <= index) { + uint8_t c; + uint32_t v; + + c = inbyte[0]; + v = c & 0x7F; + if (c < 128) { + inbyte += 1; + prev = v + prev; + ++i; + continue; + } + + c = inbyte[1]; + v |= (c & 0x7F) << 7; + if (c < 128) { + inbyte += 2; + prev = v + prev; + ++i; + continue; + } + + c = inbyte[2]; + v |= (c & 0x7F) << 14; + if (c < 128) { + inbyte += 3; + prev = v + prev; + ++i; + continue; + } + + c = inbyte[3]; + v |= (c & 0x7F) << 21; + if (c < 128) { + inbyte += 4; + prev = v + prev; + ++i; + continue; + } + + c = inbyte[4]; + inbyte += 5; + v |= (c & 0x0F) << 28; + prev = v + prev; + ++i; + } + assert(i == index + 1); + return prev; + } + + std::string name() const { + if (delta) + return "VByteDelta"; + else + return "VByte"; + } + +private: + // convenience function used by insert, writes key and newvalue to compressed + // stream, and return + // extra storage used, pointer should be right after where nextvalue is right + // now + size_t __insert(uint8_t *in, uint32_t previous, uint32_t key, + uint32_t nextvalue, size_t followingbytes) { + assert(nextvalue >= key); + assert(key >= previous); + size_t oldstorage = storageCost(nextvalue - previous); + size_t newstorage = + storageCost(nextvalue - key) + storageCost(key - previous); + assert(newstorage >= oldstorage); + if (newstorage > oldstorage) + std::memmove(in + newstorage - oldstorage, in, followingbytes); + uint8_t *newin = in - oldstorage; + newin += encodeOneIntegerToByteArray(key - previous, newin); + newin += encodeOneIntegerToByteArray(nextvalue - key, newin); + assert(newin == in + newstorage - oldstorage); + return newstorage - oldstorage; + } + + template uint8_t extract7bits(const uint32_t val) { + return static_cast((val >> (7 * i)) & ((1U << 7) - 1)); + } + + template uint8_t extract7bitsmaskless(const uint32_t val) { + return static_cast((val >> (7 * i))); + } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_VARIABLEBYTE_H_ */ diff --git a/include/SIMDCompressionAndIntersection/varintgb.h b/include/SIMDCompressionAndIntersection/varintgb.h new file mode 100644 index 0000000..769b75b --- /dev/null +++ b/include/SIMDCompressionAndIntersection/varintgb.h @@ -0,0 +1,929 @@ +/* + * varintgb.h + * + * Created on: Jul 25, 2013 + * Author: lemire + */ + +#ifndef SIMDCompressionAndIntersection_VARINTGB_H_ +#define SIMDCompressionAndIntersection_VARINTGB_H_ + +#include "common.h" +#include "codecs.h" +#include "variablebyte.h" + +namespace SIMDCompressionLib { + +using namespace std; + +static uint8_t group_size[] = { + 4, 5, 6, 7, 5, 6, 7, 8, 6, 7, 8, 9, 7, 8, 9, 10, 5, 6, 7, + 8, 6, 7, 8, 9, 7, 8, 9, 10, 8, 9, 10, 11, 6, 7, 8, 9, 7, 8, + 9, 10, 8, 9, 10, 11, 9, 10, 11, 12, 7, 8, 9, 10, 8, 9, 10, 11, 9, + 10, 11, 12, 10, 11, 12, 13, 5, 6, 7, 8, 6, 7, 8, 9, 7, 8, 9, 10, + 8, 9, 10, 11, 6, 7, 8, 9, 7, 8, 9, 10, 8, 9, 10, 11, 9, 10, 11, + 12, 7, 8, 9, 10, 8, 9, 10, 11, 9, 10, 11, 12, 10, 11, 12, 13, 8, 9, + 10, 11, 9, 10, 11, 12, 10, 11, 12, 13, 11, 12, 13, 14, 6, 7, 8, 9, 7, + 8, 9, 10, 8, 9, 10, 11, 9, 10, 11, 12, 7, 8, 9, 10, 8, 9, 10, 11, + 9, 10, 11, 12, 10, 11, 12, 13, 8, 9, 10, 11, 9, 10, 11, 12, 10, 11, 12, + 13, 11, 12, 13, 14, 9, 10, 11, 12, 10, 11, 12, 13, 11, 12, 13, 14, 12, 13, + 14, 15, 7, 8, 9, 10, 8, 9, 10, 11, 9, 10, 11, 12, 10, 11, 12, 13, 8, + 9, 10, 11, 9, 10, 11, 12, 10, 11, 12, 13, 11, 12, 13, 14, 9, 10, 11, 12, + 10, 11, 12, 13, 11, 12, 13, 14, 12, 13, 14, 15, 10, 11, 12, 13, 11, 12, 13, + 14, 12, 13, 14, 15, 13, 14, 15, 16}; + +/** + * Group VarInt. + * + * Implemented and designed by D. Lemire based on a talk by Jeff Dean (Google), + * optimized by N. Kurz. + */ + +template class VarIntGB : public IntegerCODEC { +public: + void encodeArray(uint32_t *in, const size_t length, uint32_t *out, + size_t &nvalue) { + uint8_t *bout = reinterpret_cast(out); + const uint8_t *const initbout = reinterpret_cast(out); + *out = static_cast(length); + bout += 4; + bout = headlessEncode(in, length, 0, bout); + + while (needPaddingTo32Bits(bout)) { + *bout++ = 0; + } + const size_t storageinbytes = bout - initbout; + assert((storageinbytes % 4) == 0); + nvalue = storageinbytes / 4; + } + + void encodeToByteArray(uint32_t *in, const size_t length, uint8_t *bout, + size_t &nvalue) { + const uint8_t *const initbout = bout; + *(uint32_t *)bout = static_cast(length); + bout += 4; + bout = headlessEncode(in, length, 0, bout); + nvalue = bout - initbout; + } + + const uint32_t *decodeArray(const uint32_t *in, const size_t length, + uint32_t *out, size_t &nvalue) { + if (length == 0) { + nvalue = 0; + return in; + } + const uint8_t *inbyte = reinterpret_cast(in); + nvalue = *in; + inbyte += 4; + size_t decoded = + headlessDecode(inbyte, (length - 1) * sizeof(uint32_t), 0, out, nvalue); + assert(decoded == nvalue); + return in + length; + } + + const uint8_t *decodeFromByteArray(const uint8_t *inbyte, const size_t length, + uint32_t *out, size_t &nvalue) { + if (length == 0) { + nvalue = 0; + return inbyte; + } + nvalue = *(uint32_t *)inbyte; + inbyte += 4; + size_t decoded = headlessDecode(inbyte, length - 4, 0, out, nvalue); + assert(decoded == nvalue); + return inbyte + length; + } + + // appends a key + // return the new size in bytes + size_t appendToByteArray(uint8_t *in, const size_t length, uint32_t previous, + uint32_t value) { + uint32_t num_ints = *(uint32_t *)in; + uint8_t *bout = reinterpret_cast(in + 4); + uint8_t *bend = in + (length == 0 ? 4 : length); + + if (delta) + value -= previous; + + uint8_t *keyp; + int shift; + + // fast-forward to the last block + if (num_ints % 4 != 0) { + uint32_t size = 0; + do { + bout += size; + size = 1 + group_size[*bout]; + } while (bout + size < bend); + keyp = bout; + bout = bend; + shift = (num_ints % 4) * 2; + } else { + keyp = bend; + bout = keyp + 1; + *keyp = 0; + shift = 0; + } + + if (value < (1U << 8)) { + *bout++ = static_cast(value); + } else if (value < (1U << 16)) { + *bout++ = static_cast(value); + *bout++ = static_cast(value >> 8); + *keyp |= (1 << shift); + } else if (value < (1U << 24)) { + *bout++ = static_cast(value); + *bout++ = static_cast(value >> 8); + *bout++ = static_cast(value >> 16); + *keyp |= (2 << shift); + } else { + // the compiler will do the right thing + *reinterpret_cast(bout) = value; + bout += 4; + *keyp |= (3 << shift); + } + + *(uint32_t *)in = num_ints + 1; + return bout - in; + } + + // insert the key in sorted order. We assume that there is enough room and + // that delta encoding was used. + size_t insert(uint32_t *in, const size_t length, uint32_t key) { + size_t bytesize = length * 4; + uint8_t *bytein = (uint8_t *)in; + uint8_t *byteininit = bytein; + size_t bl = insertInByteArray(bytein, bytesize, key); + bytein += bl; + + while (needPaddingTo32Bits(bytein)) { + *bytein++ = 0; + } + size_t storageinbytes = bytein - byteininit; + assert((storageinbytes % 4) == 0); + return storageinbytes / 4; + } + + // insert the key in sorted order. We assume that there is enough room and + // that delta encoding was used. + // the new size is returned (in bytes) + size_t insertInByteArray(uint8_t *inbyte, size_t length, uint32_t key) { + if (length == 0) { + *((uint32_t *)inbyte) = 0; + length = 4; + } + uint8_t *finalinbyte = inbyte + length; + const uint8_t *const initinbyte = inbyte; + uint32_t nvalue = *((uint32_t *)inbyte); + *((uint32_t *)inbyte) = nvalue + 1; // incrementing + inbyte += 4; // skip nvalue + assert(delta); + uint32_t initial = 0; + size_t i = 0; + while (i + 3 < nvalue) { + uint32_t copyinitial = initial; + const uint8_t *const newinbyte = + scanGroupVarIntDelta(inbyte, ©initial); + if (copyinitial >= key) { + goto finish; + } + inbyte = (uint8_t *)newinbyte; + initial = copyinitial; + i += 4; + } + finish: + assert(finalinbyte >= inbyte); + assert(i <= nvalue); + static const int REASONABLEBUFFER = 256; + if (nvalue - i + 1 > REASONABLEBUFFER) { + if (nvalue == i) { // straight append + const uint8_t *const newfinalinbyte = + headlessEncode(&key, 1, initial, inbyte); + return newfinalinbyte - initinbyte; + } + if (nvalue - i <= 4) { + // easy case + uint32_t tmpbuffer[5]; + tmpbuffer[0] = key; + size_t decoded = headlessDecode(inbyte, finalinbyte - inbyte, initial, + tmpbuffer + 1, nvalue - i); + assert(decoded == nvalue - i); + sortinfirstvalue(tmpbuffer, nvalue - i); + const uint8_t *const newfinalinbyte = + headlessEncode(tmpbuffer, nvalue - i + 1, initial, inbyte); + return newfinalinbyte - initinbyte; + } + // harder case + // this part is a bit complicated since we need to merge in the key + uint32_t readinitial = initial; + uint32_t tmpbuffer[5]; + tmpbuffer[0] = key; + const uint8_t *readinginbyte = + decodeGroupVarIntDelta(inbyte, &readinitial, tmpbuffer + 1); + assert(tmpbuffer[4] >= key); + assert(readinginbyte > inbyte); + + sortinfirstvalue(tmpbuffer, nvalue - i); + i += 4; + + // initialize blocks + + Block b1, b2; + + Block *block1 = &b1; + Block *block2 = &b2; + Block *blocktmp; + + // load block1 + + uint8_t *fb = encodeGroupVarIntDelta(block1->data, initial, tmpbuffer); + + block1->length = static_cast(fb - block1->data); + uint32_t nextval = tmpbuffer[4] - tmpbuffer[3]; + uint32_t newsel = getByteLength(nextval) - 1; + // everything after that is just going to be shifting + while (nvalue - i >= 4) { + + // load block 2 + assert(readinginbyte >= inbyte); + readinginbyte = loadblock(block2, readinginbyte); + i += 4; + // shift in block 1 + shiftin(block2, &nextval, &newsel); + // write block1 + memcpy(inbyte, block1->data, block1->length); + inbyte += block1->length; + // block1 = block2 + blocktmp = block1; + block1 = block2; + block2 = blocktmp; + } + if (nvalue != i) { + readinginbyte = loadblockcarefully(block2, readinginbyte, nvalue - i); + finalshiftin(block2, nextval, newsel, + nvalue - i + 1); // nextval is useless here + memcpy(inbyte, block1->data, block1->length); + inbyte += block1->length; + memcpy(inbyte, block2->data, block2->length); + inbyte += block2->length; + return inbyte - initinbyte; + } else { + memcpy(inbyte, block1->data, block1->length); + inbyte += block1->length; + inbyte[0] = newsel; + inbyte++; + memcpy(inbyte, &nextval, newsel + 1); + inbyte += newsel + 1; + return inbyte - initinbyte; + } + // we are using brute force here, by decoding everything to a buffer and + // then reencoding. + } else { + uint32_t tmpbuffer[REASONABLEBUFFER]; + assert(tmpbuffer); + tmpbuffer[0] = key; + if (nvalue != i) { + size_t decoded = headlessDecode(inbyte, finalinbyte - inbyte, initial, + tmpbuffer + 1, nvalue - i); + assert(decoded == nvalue - i); + sortinfirstvalue(tmpbuffer, nvalue - i); + } + const uint8_t *const newfinalinbyte = + headlessEncode(tmpbuffer, nvalue - i + 1, initial, inbyte); + return newfinalinbyte - initinbyte; + } + } + + // Performs a lower bound find in the encoded array. + // Returns the index + // assumes delta coding was used + size_t findLowerBound(const uint32_t *in, const size_t length, uint32_t key, + uint32_t *presult) { + const uint8_t *inbyte = reinterpret_cast(in); + uint32_t out[4] = {0}; + assert(delta); + size_t i = 0; + uint32_t initial = 0; + uint32_t nvalue = *in; + + inbyte += 4; // skip nvalue + + const uint8_t *const endbyte = + reinterpret_cast(in + length); + while (i + 3 < nvalue) { + uint32_t gap1, gap2, gap3, gap4; + uint32_t gap12, gap34; + + const uint32_t sel = *inbyte++; + if (sel == 0) { + gap1 = static_cast(inbyte[0]); + gap2 = static_cast(inbyte[1]); + gap12 = gap1 + gap2; + gap3 = static_cast(inbyte[2]); + gap4 = static_cast(inbyte[3]); + gap34 = gap3 + gap4; + inbyte += 4; + } else { + const uint32_t sel1 = (sel & 3); + gap1 = *(reinterpret_cast(inbyte)) & mask[sel1]; + inbyte += sel1 + 1; + const uint32_t sel2 = ((sel >> 2) & 3); + gap2 = *(reinterpret_cast(inbyte)) & mask[sel2]; + gap12 = gap1 + gap2; + inbyte += sel2 + 1; + const uint32_t sel3 = ((sel >> 4) & 3); + gap3 = *(reinterpret_cast(inbyte)) & mask[sel3]; + inbyte += sel3 + 1; + const uint32_t sel4 = (sel >> 6); + gap4 = *(reinterpret_cast(inbyte)) & mask[sel4]; + gap34 = gap3 + gap4; + inbyte += sel4 + 1; + } + initial += gap12 + gap34; + if (key <= initial) { + if (key <= initial - gap34 - gap2) { + *presult = initial - gap34 - gap2; + return (i + 0); + } + if (key <= initial - gap34) { + *presult = initial - gap34; + return (i + 1); + } + if (key <= initial - gap4) { + *presult = initial - gap4; + return (i + 2); + } + *presult = initial; + return (i + 3); + } + i += 4; + } + if (endbyte > inbyte && nvalue > i) { + uint32_t tnvalue = static_cast(nvalue - 1 - i); + inbyte = decodeCarefully(inbyte, &initial, out, tnvalue); + assert(inbyte <= endbyte); + if (key <= out[0]) { + *presult = out[0]; + return (i + 0); + } + if (tnvalue > 0 && key <= out[1]) { + *presult = out[1]; + return (i + 1); + } + if (tnvalue > 1 && key <= out[2]) { + *presult = out[2]; + return (i + 2); + } + if (tnvalue > 2 && key <= out[3]) { + *presult = out[3]; + return (i + 3); + } + } + assert(false); + *presult = key + 1; + return (i); + } + + // Returns a decompressed value in an encoded array + // This code has been optimized for delta-encoded arrays (TODO: optimize for + // the regular case). + uint32_t select(uint32_t *in, size_t index) { + const uint8_t *inbyte = reinterpret_cast(in); + uint32_t out[4]; + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 0; + size_t i = 0; + uint32_t initial = 0; + uint32_t nvalue = *in; + inbyte += 4; // skip nvalue + if (index + 3 < + nvalue) { // this common case can be done with fewer branches + while (i + 4 <= index) { + inbyte = delta ? scanGroupVarIntDelta(inbyte, &initial) + : scanGroupVarInt(inbyte); // note: delta known at + // compile time: this is not a + // branch + i += 4; + } + inbyte = delta ? decodeGroupVarIntDelta(inbyte, &initial, out) + : decodeGroupVarInt(inbyte, out); // note: delta known at + // compile time: this is + // not a branch + return (out[index - i]); + } // else + // we finish with the uncommon case + while (i + 3 < index) { // a single branch will do for this case (bulk of + // the computation) + inbyte = delta ? scanGroupVarIntDelta(inbyte, &initial) + : scanGroupVarInt(inbyte); + i += 4; + } + // lots of branching ahead... + while (i + 3 < nvalue) { + inbyte = delta ? decodeGroupVarIntDelta(inbyte, &initial, out) + : decodeGroupVarInt(inbyte, out); + i += 4; + if (i > index) + return (out[index - (i - 4)]); + } + { + nvalue = static_cast(nvalue - i); + inbyte = decodeCarefully(inbyte, &initial, out, nvalue); + if (index == i) + return (out[0]); + if (nvalue > 1 && index == i + 1) + return (out[1]); + if (nvalue > 2 && index == i + 2) + return (out[2]); + if (nvalue > 3 && index == i + 3) + return (out[3]); + } + assert(false); // we should never get here + return (0); + } + + string name() const { + if (delta) + return "varintgbdelta"; + else + return "varintgb"; + } + + uint8_t *headlessEncode(uint32_t *in, const size_t length, uint32_t prev, + uint8_t *bout) { + size_t k = 0; + for (; k + 3 < length; k += 4) { + uint8_t *keyp = bout++; + *keyp = 0; + { + const uint32_t val = delta ? in[k] - prev : in[k]; + if (delta) + prev = in[k]; + if (val < (1U << 8)) { + *bout++ = static_cast(val); + } else if (val < (1U << 16)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *keyp = static_cast(1); + } else if (val < (1U << 24)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *bout++ = static_cast(val >> 16); + *keyp = static_cast(2); + } else { + // the compiler will do the right thing + *reinterpret_cast(bout) = val; + bout += 4; + *keyp = static_cast(3); + } + } + { + const uint32_t val = delta ? in[k + 1] - prev : in[k + 1]; + if (delta) + prev = in[k + 1]; + if (val < (1U << 8)) { + *bout++ = static_cast(val); + } else if (val < (1U << 16)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *keyp |= static_cast(1 << 2); + } else if (val < (1U << 24)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *bout++ = static_cast(val >> 16); + *keyp |= static_cast(2 << 2); + } else { + // the compiler will do the right thing + *reinterpret_cast(bout) = val; + bout += 4; + *keyp |= static_cast(3 << 2); + } + } + { + const uint32_t val = delta ? in[k + 2] - prev : in[k + 2]; + if (delta) + prev = in[k + 2]; + if (val < (1U << 8)) { + *bout++ = static_cast(val); + } else if (val < (1U << 16)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *keyp |= static_cast(1 << 4); + } else if (val < (1U << 24)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *bout++ = static_cast(val >> 16); + *keyp |= static_cast(2 << 4); + } else { + // the compiler will do the right thing + *reinterpret_cast(bout) = val; + bout += 4; + *keyp |= static_cast(3 << 4); + } + } + { + const uint32_t val = delta ? in[k + 3] - prev : in[k + 3]; + if (delta) + prev = in[k + 3]; + if (val < (1U << 8)) { + *bout++ = static_cast(val); + } else if (val < (1U << 16)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *keyp |= static_cast(1 << 6); + } else if (val < (1U << 24)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *bout++ = static_cast(val >> 16); + *keyp |= static_cast(2 << 6); + } else { + // the compiler will do the right thing + *reinterpret_cast(bout) = val; + bout += 4; + *keyp |= static_cast(3 << 6); + } + } + } + if (k < length) { + uint8_t *keyp = bout++; + *keyp = 0; + for (int j = 0; k < length && j < 8; j += 2, ++k) { + const uint32_t val = delta ? in[k] - prev : in[k]; + if (delta) + prev = in[k]; + if (val < (1U << 8)) { + *bout++ = static_cast(val); + } else if (val < (1U << 16)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *keyp |= static_cast(1 << j); + } else if (val < (1U << 24)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *bout++ = static_cast(val >> 16); + *keyp |= static_cast(2 << j); + } else { + // the compiler will do the right thing + *reinterpret_cast(bout) = val; + bout += 4; + *keyp |= static_cast(3 << j); + } + } + } + return bout; + } + + // returns how many values were decoded to out, will try to decode + // desirednumber + // if input allows. + size_t headlessDecode(const uint8_t *inbyte, const size_t length, + uint32_t prev, uint32_t *out, + const size_t desirednumber) { + + uint32_t *initout = out; + const uint32_t *const endout = out + desirednumber; + const uint8_t *const endbyte = inbyte + length; + uint32_t val; + while ((endbyte > inbyte + 4 * 4)) { //&& (endout > out + 3) + inbyte = delta ? decodeGroupVarIntDelta(inbyte, &prev, out) + : decodeGroupVarInt(inbyte, out); + out += 4; + } + while (endbyte > inbyte + 1) { + uint8_t key = *inbyte++; + // printf("last key is %u \n",key); + for (int k = 0; (k < 4) && (endout > out); k++) { + const uint32_t howmanybyte = key & 3; + // printf("last key is %u howmanybyte = %u \n",key, howmanybyte+1); + + key = static_cast(key >> 2); + val = static_cast(*inbyte++); + if (howmanybyte >= 1) { + val |= (static_cast(*inbyte++) << 8); + if (howmanybyte >= 2) { + val |= (static_cast(*inbyte++) << 16); + if (howmanybyte >= 3) { + val |= (static_cast(*inbyte++) << 24); + } + } + } + // printf("decoded %u\n",val); + prev = (delta ? prev : 0) + val; + // printf("writing %u\n",prev); + + *out++ = prev; + } + assert(inbyte <= endbyte); + } + return out - initout; + } + +private: + const uint32_t mask[4] = {0xFF, 0xFFFF, 0xFFFFFF, 0xFFFFFFFF}; + + void sortinfirstvalue(uint32_t *tmpbuffer, size_t length) { + size_t top = length < 4 ? length : 4; + + for (size_t j = 0; j < top; ++j) { + if (tmpbuffer[j] > tmpbuffer[j + 1]) { + uint32_t t = tmpbuffer[j + 1]; + tmpbuffer[j + 1] = tmpbuffer[j]; + tmpbuffer[j] = t; + } + } + } + + const uint8_t *decodeGroupVarInt(const uint8_t *in, uint32_t *out) { + const uint32_t sel = *in++; + if (sel == 0) { + out[0] = static_cast(in[0]); + out[1] = static_cast(in[1]); + out[2] = static_cast(in[2]); + out[3] = static_cast(in[3]); + return in + 4; + } + const uint32_t sel1 = (sel & 3); + *out++ = *((uint32_t *)(in)) & mask[sel1]; + in += sel1 + 1; + const uint32_t sel2 = ((sel >> 2) & 3); + *out++ = *((uint32_t *)(in)) & mask[sel2]; + in += sel2 + 1; + const uint32_t sel3 = ((sel >> 4) & 3); + *out++ = *((uint32_t *)(in)) & mask[sel3]; + in += sel3 + 1; + const uint32_t sel4 = (sel >> 6); + *out++ = *((uint32_t *)(in)) & mask[sel4]; + in += sel4 + 1; + return in; + } + + const uint8_t *decodeGroupVarIntDelta(const uint8_t *in, uint32_t *val, + uint32_t *out) { + const uint32_t sel = *in++; + if (sel == 0) { + out[0] = (*val += static_cast(in[0])); + out[1] = (*val += static_cast(in[1])); + out[2] = (*val += static_cast(in[2])); + out[3] = (*val += static_cast(in[3])); + return in + 4; + } + const uint32_t sel1 = (sel & 3); + *val += *((uint32_t *)(in)) & mask[sel1]; + *out++ = *val; + in += sel1 + 1; + const uint32_t sel2 = ((sel >> 2) & 3); + *val += *((uint32_t *)(in)) & mask[sel2]; + *out++ = *val; + in += sel2 + 1; + const uint32_t sel3 = ((sel >> 4) & 3); + *val += *((uint32_t *)(in)) & mask[sel3]; + *out++ = *val; + in += sel3 + 1; + const uint32_t sel4 = (sel >> 6); + *val += *((uint32_t *)(in)) & mask[sel4]; + *out++ = *val; + in += sel4 + 1; + return in; + } + + const uint8_t *decodeCarefully(const uint8_t *inbyte, uint32_t *initial, + uint32_t *out, uint32_t count) { + uint32_t val; + uint32_t k, key = *inbyte++; + for (k = 0; k < count && k < 4; k++) { + const uint32_t howmanybyte = key & 3; + key = static_cast(key >> 2); + val = static_cast(*inbyte++); + if (howmanybyte >= 1) { + val |= (static_cast(*inbyte++) << 8); + if (howmanybyte >= 2) { + val |= (static_cast(*inbyte++) << 16); + if (howmanybyte >= 3) { + val |= (static_cast(*inbyte++) << 24); + } + } + } + if (delta) { + *initial += val; + *out = *initial; + } else { + *out = val; + } + out++; + } + return (inbyte); + } + + const uint8_t *scanGroupVarIntDelta(const uint8_t *in, uint32_t *val) { + const uint32_t sel = *in++; + if (sel == 0) { + *val += static_cast(in[0]); + *val += static_cast(in[1]); + *val += static_cast(in[2]); + *val += static_cast(in[3]); + return in + 4; + } + const uint32_t sel1 = (sel & 3); + *val += *(reinterpret_cast(in)) & mask[sel1]; + in += sel1 + 1; + const uint32_t sel2 = ((sel >> 2) & 3); + *val += *(reinterpret_cast(in)) & mask[sel2]; + in += sel2 + 1; + const uint32_t sel3 = ((sel >> 4) & 3); + *val += *(reinterpret_cast(in)) & mask[sel3]; + in += sel3 + 1; + const uint32_t sel4 = (sel >> 6); + *val += *(reinterpret_cast(in)) & mask[sel4]; + in += sel4 + 1; + return in; + } + + const uint8_t *scanGroupVarInt(const uint8_t *in) { + const uint32_t sel = *in++; + if (sel == 0) { + return in + 4; + } + const uint32_t sel1 = (sel & 3); + in += sel1 + 1; + const uint32_t sel2 = ((sel >> 2) & 3); + in += sel2 + 1; + const uint32_t sel3 = ((sel >> 4) & 3); + in += sel3 + 1; + const uint32_t sel4 = (sel >> 6); + in += sel4 + 1; + return in; + } + + // encode 4 integers + uint8_t *encodeGroupVarIntDelta(uint8_t *bout, uint32_t prev, uint32_t *in) { + uint8_t *keyp = bout++; + *keyp = 0; + { + const uint32_t val = in[0] - prev; + prev = in[0]; + if (val < (1U << 8)) { + *bout++ = static_cast(val); + } else if (val < (1U << 16)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *keyp = static_cast(1); + } else if (val < (1U << 24)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *bout++ = static_cast(val >> 16); + *keyp = static_cast(2); + } else { + // the compiler will do the right thing + *reinterpret_cast(bout) = val; + bout += 4; + *keyp = static_cast(3); + } + } + { + const uint32_t val = in[1] - prev; + prev = in[1]; + if (val < (1U << 8)) { + *bout++ = static_cast(val); + } else if (val < (1U << 16)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *keyp |= static_cast(1 << 2); + } else if (val < (1U << 24)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *bout++ = static_cast(val >> 16); + *keyp |= static_cast(2 << 2); + } else { + // the compiler will do the right thing + *reinterpret_cast(bout) = val; + bout += 4; + *keyp |= static_cast(3 << 2); + } + } + { + const uint32_t val = in[2] - prev; + prev = in[2]; + if (val < (1U << 8)) { + *bout++ = static_cast(val); + } else if (val < (1U << 16)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *keyp |= static_cast(1 << 4); + } else if (val < (1U << 24)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *bout++ = static_cast(val >> 16); + *keyp |= static_cast(2 << 4); + } else { + // the compiler will do the right thing + *reinterpret_cast(bout) = val; + bout += 4; + *keyp |= static_cast(3 << 4); + } + } + { + const uint32_t val = in[3] - prev; + prev = in[3]; + if (val < (1U << 8)) { + *bout++ = static_cast(val); + } else if (val < (1U << 16)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *keyp |= static_cast(1 << 6); + } else if (val < (1U << 24)) { + *bout++ = static_cast(val); + *bout++ = static_cast(val >> 8); + *bout++ = static_cast(val >> 16); + *keyp |= static_cast(2 << 6); + } else { + // the compiler will do the right thing + *reinterpret_cast(bout) = val; + bout += 4; + *keyp |= static_cast(3 << 6); + } + } + return bout; + } + + uint32_t getByteLength(uint32_t val) { + if (val < (1U << 8)) { + return 1; + } else if (val < (1U << 16)) { + return 2; + } else if (val < (1U << 24)) { + return 3; + } else { + return 4; + } + } + + struct Block { // should fit in two cache lines. + uint8_t data[4 * 4 + 1 + 3]; // the final +3 is a safety buffer + uint32_t length; + }; + + uint8_t lengths[256] = { + 5, 6, 7, 8, 6, 7, 8, 9, 7, 8, 9, 10, 8, 9, 10, 11, 6, 7, + 8, 9, 7, 8, 9, 10, 8, 9, 10, 11, 9, 10, 11, 12, 7, 8, 9, 10, + 8, 9, 10, 11, 9, 10, 11, 12, 10, 11, 12, 13, 8, 9, 10, 11, 9, 10, + 11, 12, 10, 11, 12, 13, 11, 12, 13, 14, 6, 7, 8, 9, 7, 8, 9, 10, + 8, 9, 10, 11, 9, 10, 11, 12, 7, 8, 9, 10, 8, 9, 10, 11, 9, 10, + 11, 12, 10, 11, 12, 13, 8, 9, 10, 11, 9, 10, 11, 12, 10, 11, 12, 13, + 11, 12, 13, 14, 9, 10, 11, 12, 10, 11, 12, 13, 11, 12, 13, 14, 12, 13, + 14, 15, 7, 8, 9, 10, 8, 9, 10, 11, 9, 10, 11, 12, 10, 11, 12, 13, + 8, 9, 10, 11, 9, 10, 11, 12, 10, 11, 12, 13, 11, 12, 13, 14, 9, 10, + 11, 12, 10, 11, 12, 13, 11, 12, 13, 14, 12, 13, 14, 15, 10, 11, 12, 13, + 11, 12, 13, 14, 12, 13, 14, 15, 13, 14, 15, 16, 8, 9, 10, 11, 9, 10, + 11, 12, 10, 11, 12, 13, 11, 12, 13, 14, 9, 10, 11, 12, 10, 11, 12, 13, + 11, 12, 13, 14, 12, 13, 14, 15, 10, 11, 12, 13, 11, 12, 13, 14, 12, 13, + 14, 15, 13, 14, 15, 16, 11, 12, 13, 14, 12, 13, 14, 15, 13, 14, 15, 16, + 14, 15, 16, 17}; + + const uint8_t *loadblock(Block *b, const uint8_t *readinginbyte) { + b->length = lengths[readinginbyte[0]]; + memcpy(b->data, readinginbyte, b->length); + return readinginbyte + b->length; + } + + const uint8_t *loadblockcarefully(Block *b, const uint8_t *readinginbyte, + size_t howmanyvals) { + b->length = 1; + for (size_t k = 0; k < howmanyvals; ++k) + b->length += 1 + ((readinginbyte[0] >> (2 * k)) & 3); + memcpy(b->data, readinginbyte, b->length); + return readinginbyte + b->length; + } + + void shiftin(Block *b, uint32_t *nextval, uint32_t *newsel) { + uint32_t offsettolastval = lengths[b->data[0] & 63] - 1; + uint32_t newnextsel = b->data[0] >> 6; + uint32_t newnextval; + memcpy(&newnextval, b->data + offsettolastval, 4); + newnextval &= mask[newnextsel]; + // uint32_t newnextval = *(reinterpret_cast(b->data + + // offsettolastval)) & mask[newnextsel]; + b->data[0] = (b->data[0] << 2) | *newsel; + std::memmove(b->data + 2 + *newsel, b->data + 1, + b->length - 1 - 1 - newnextsel); + b->length = offsettolastval + 1 + *newsel; + std::memcpy(b->data + 1, nextval, *newsel + 1); + *nextval = newnextval; + *newsel = newnextsel; + } + + void finalshiftin(Block *b, uint32_t nextval, uint32_t newsel, + size_t howmany) { + b->data[0] = (b->data[0] << 2) | newsel; + std::memmove(b->data + 2 + newsel, b->data + 1, b->length - 1); + b->length = 1; + for (size_t k = 0; k < howmany; ++k) + b->length += 1 + ((b->data[0] >> (2 * k)) & 3); + std::memcpy(b->data + 1, &nextval, newsel + 1); + } +}; + +} // namespace SIMDCompressionLib + +#endif /* SIMDCompressionAndIntersection_VARINTGB_H_ */ diff --git a/include/compressedSetBit.h b/include/compressedSetBit.h new file mode 100644 index 0000000..0fc43d2 --- /dev/null +++ b/include/compressedSetBit.h @@ -0,0 +1,90 @@ +#ifndef __COMPRESSED_SET_BIT_H__ +#define __COMPRESSED_SET_BIT_H__ +#include "codecfactory.h" +#include "intersection.h" + +#include +#include /* for sort, random_shuffle */ + +using namespace SIMDCompressionLib; + +template +class CompressedSetBit { +public: + CompressedSetBit(std::vector idxList) { + nSetInts = idxList.size(); + + std::sort(idxList.begin(), idxList.end()); + //std::cout << "\nAfter sorting:\n"; + //for (size_t i = 0; i < idxList.size(); i++) std::cout << idxList[i] << " "; + //std::cout << "\n"; + // We pick a CODEC + //IntegerCODEC &codec = *CODECFactory::getFromName("s4-bp128-dm"); + IntegerCODEC &codec = *CODECFactory::getFromName("s4-fastpfor-d1"); + + std::vector dat(idxList.size() + 1024); + //deltaCompressedSetList.resize(idxList.size() + 1024); + size_t compressedsize = dat.size(); + codec.encodeArray(idxList.data(), idxList.size(), dat.data(), + compressedsize); + + dat.resize(compressedsize); + dat.shrink_to_fit(); + nCompressedInts = compressedsize; + data_.reset(new uint32_t[compressedsize]); + std::copy(dat.begin(), dat.end(), data_.get()); + } + + void uncompress(std::vector& idxList) { + //IntegerCODEC &codec = *CODECFactory::getFromName("s4-bp128-dm"); + IntegerCODEC &codec = *CODECFactory::getFromName("s4-fastpfor-d1"); + idxList.resize(nSetInts); + size_t compressedSize = nCompressedInts;//deltaCompressedSetList.size(); + size_t originalSize = nSetInts; + codec.decodeArray(&data_[0], compressedSize, + idxList.data(), originalSize); + idxList.resize(originalSize); //?? why do we need this? + } + + size_t size_in_bytes() { + //std::cout << " size: " << deltaCompressedSetList.size() << " "; + return sizeof(*this) + (sizeof(uint32_t) * nCompressedInts); + } + + bool serialize(std::ostream& output) { + output.write(reinterpret_cast(&nSetInts), sizeof(nSetInts)); + output.write(reinterpret_cast(&nCompressedInts), sizeof(nCompressedInts)); + output.write(reinterpret_cast(&data_[0]), sizeof(uint32_t) * nCompressedInts); + return true; + } + + bool deserialize(std::istream& input) { + input.read(reinterpret_cast(&nSetInts), sizeof(nSetInts)); + input.read(reinterpret_cast(&nCompressedInts), sizeof(nCompressedInts)); + data_.reset(new uint32_t[nCompressedInts]); + input.read(reinterpret_cast(&data_[0]), sizeof(uint32_t) * nCompressedInts); + return true; + } + +private: + template + friend inline bool operator==(const CompressedSetBit& lhs, const CompressedSetBit& rhs); + template + friend inline bool operator!=(const CompressedSetBit& lhs, const CompressedSetBit& rhs); + //std::vector deltaCompressedSetList; + std::unique_ptr data_; + IndexSizeT nSetInts; + IndexSizeT nCompressedInts; +}; + +template +inline bool operator==(const CompressedSetBit& lhs, const CompressedSetBit& rhs) { + return (lhs.nCompressedInts == rhs.nCompressedInts) ? (std::memcmp(&lhs.data_[0], &rhs.data_[0], lhs.nCompressedInts) == 0) : false; +} + +template +inline bool operator!=(const CompressedSetBit& lhs, const CompressedSetBit& rhs) { + return !(lhs == rhs); +} + +#endif From 8956213dd4d617c0d3fdd1d407fd53fcf3692371 Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Wed, 13 Jun 2018 12:44:32 -0400 Subject: [PATCH 013/122] added the new target walkEqcls --- src/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8c642d5..cf8a8ca 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,6 +29,7 @@ add_executable(mantis mantis.cc) add_executable(monochromatic_component_iterator monochromatic_component_iterator.cc) +add_executable(walkEqcls walkEqcls.cc) target_include_directories(mantis PUBLIC $) @@ -36,8 +37,12 @@ target_include_directories(mantis PUBLIC target_include_directories(monochromatic_component_iterator PUBLIC $) +target_include_directories(walkEqcls PUBLIC + $ $) target_link_libraries(mantis mantis_core) target_link_libraries(monochromatic_component_iterator mantis_core) +target_link_libraries(walkEqcls + mantis_core) From 4789ad1bc2c8bf523ac9d906b553632cd15374bb Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Sat, 23 Jun 2018 18:16:08 -0400 Subject: [PATCH 014/122] Adding two tests: ssbt on columns & unique delta bvs --- CMakeLists.txt | 2 +- include/monochromatic_component_iterator.h | 40 ++++- src/CMakeLists.txt | 2 +- src/monochromatic_component_iterator.cc | 167 ++++++++++++++++++--- src/walkEqcls.cc | 117 ++++++++++++--- 5 files changed, 283 insertions(+), 45 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fbc6c6b..1ad13c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ else() endif() set(CMAKE_C_FLAGS "-std=gnu11 -Wno-unused-result -Wno-strict-aliasing -Wno-unused-function -Wno-sign-compare -Wno-implicit-function-declaration") +#set(CMAKE_CXX_FLAGS "-std=c++11 ${ARCH} -Wno-unused-result -Wno-strict-aliasing -Wno-unused-function -Wno-sign-compare") set(CMAKE_CXX_FLAGS "-std=c++11 ${ARCH} -Wno-unused-result -Wno-strict-aliasing -Wno-unused-function -Wno-sign-compare ${CMAKE_CXX_FLAGS}") message("CMAKE_CXX_FLAGS = ${CMAKE_CXX_FLAGS}") #set(CMAKE_EXE_LINKER_FLAGS "-L/home/rob/cosmo/3rd_party_inst/lib/") @@ -21,4 +22,3 @@ message("CMAKE_CXX_FLAGS = ${CMAKE_CXX_FLAGS}") include(GNUInstallDirs) add_subdirectory(src) - diff --git a/include/monochromatic_component_iterator.h b/include/monochromatic_component_iterator.h index 3aceacc..14b26f0 100644 --- a/include/monochromatic_component_iterator.h +++ b/include/monochromatic_component_iterator.h @@ -109,6 +109,23 @@ struct Mc_stats { typedef dna::canonical_kmer edge; // k-mer typedef dna::canonical_kmer node; // (k-1)-mer +struct hash128 { + uint64_t operator()(const __uint128_t &val128) const { + __uint128_t val = val128; + // Using the same seed as we use in k-mer hashing. + return HashUtil::MurmurHash64A((void *) &val, sizeof(__uint128_t), + 2038074743); + } +}; + +struct bvHash128 { + __uint128_t operator()(const sdsl::bit_vector&bv) const { + return HashUtil::MurmurHash128A((void *) bv.data(), + bv.capacity()/8, 2038074743, + 2038074751); + } +}; + class monochromatic_component_iterator { public: class work_item { @@ -125,16 +142,24 @@ class monochromatic_component_iterator { }; bool done(); + void operator++(void); Mc_stats operator*(void); //monochromatic_component_iterator(const CQF *g); monochromatic_component_iterator(const CQF *g, - BitVectorRRR& bvin, - uint64_t num_samplesin=2586); - void neighborDist(); + BitVectorRRR &bvin, + uint64_t num_samplesin = 2586); + + void neighborDist(uint64_t cntrr); + void uniqNeighborDist(uint64_t num_samples); + uint64_t cntr = 0; + std::vector withMax0; + //spp::sparse_hash_map<__uint128_t, uint64_t, hash128> eqclass_map; + spp::sparse_hash_map eqclass_map; + private: @@ -143,7 +168,7 @@ class monochromatic_component_iterator { std::unordered_set visitedKeys; const CQF *cqf; CQF::Iterator it; - BitVectorRRR& bv; + BitVectorRRR &bv; uint64_t num_samples; sdsl::bit_vector visited; @@ -156,6 +181,13 @@ class monochromatic_component_iterator { uint64_t manhattanDist(uint64_t eq1, uint64_t eq2); + __uint128_t manhattanDistBvHash(uint64_t eq1, uint64_t eq2, + uint64_t num_samples); + void manhattanDistBvHash(uint64_t eq1, uint64_t eq2, + sdsl::bit_vector& dist, + uint64_t num_samples); + + }; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cf8a8ca..bccef8c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -45,4 +45,4 @@ target_link_libraries(mantis target_link_libraries(monochromatic_component_iterator mantis_core) target_link_libraries(walkEqcls - mantis_core) + mantis_core SIMDCompressionAndIntersection) diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index deece47..8c65530 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -188,7 +188,7 @@ namespace dna { //////////////////////////////// monochromatic_component_iterator ////////////////////////// monochromatic_component_iterator::monochromatic_component_iterator(const CQF *g, - BitVectorRRR& bvin, + BitVectorRRR &bvin, uint64_t num_samplesin) : cqf(g), it(g->begin(0)), bv(bvin), num_samples(num_samplesin) { // initialize cqf iterator @@ -197,6 +197,7 @@ monochromatic_component_iterator::monochromatic_component_iterator(const CQFslots(), 0)); std::cerr << "kmers: " << cqf->size() << "\n"; std::cerr << "slots: " << cqf->slots() << "\n"; + withMax0.resize(9); } monochromatic_component_iterator::work_item @@ -291,7 +292,7 @@ bool monochromatic_component_iterator::exists(edge e, uint64_t &idx, uint64_t &e KeyObject key(HashUtil::hash_64(tmp, BITMASK(cqf->keybits())), 0, 0); auto eq_idx = cqf->queryValAndIdx(key); if (eq_idx.first) { - eqid = eq_idx.first; + eqid = eq_idx.first - 1; idx = eq_idx.second; return true; } @@ -300,20 +301,20 @@ bool monochromatic_component_iterator::exists(edge e, uint64_t &idx, uint64_t &e uint64_t monochromatic_component_iterator::manhattanDist(uint64_t eqid1, uint64_t eqid2) { uint64_t dist{0}; - std::vector eq1(( (num_samples-1) / 64) + 1), eq2(( (num_samples-1) / 64) + 1); - auto colorbuilder = [this](std::vector &eq, uint64_t eqid) { + std::vector eq1(((num_samples - 1) / 64) + 1), eq2(((num_samples - 1) / 64) + 1); + auto colorBuilder = [this](std::vector &eq, uint64_t eqid) { uint64_t i{0}, bitcnt{0}, wrdcnt{0}; while (i < this->num_samples) { - bitcnt = std::min(this->num_samples - i, (uint64_t)64); + bitcnt = std::min(this->num_samples - i, (uint64_t) 64); uint64_t wrd = (this->bv).get_int(this->num_samples * eqid + i, bitcnt); eq[wrdcnt++] = wrd; i += bitcnt; } }; - colorbuilder(eq1, eqid1); - colorbuilder(eq2, eqid2); + colorBuilder(eq1, eqid1); + colorBuilder(eq2, eqid2); - for(uint64_t i = 0; i < eq1.size(); i++) { + for (uint64_t i = 0; i < eq1.size(); i++) { if (eq1[i] != eq2[i]) dist += sdsl::bits::cnt(eq1[i] ^ eq2[i]); } @@ -321,26 +322,112 @@ uint64_t monochromatic_component_iterator::manhattanDist(uint64_t eqid1, uint64_ } -void monochromatic_component_iterator::neighborDist() { +__uint128_t monochromatic_component_iterator::manhattanDistBvHash(uint64_t eqid1, + uint64_t eqid2, + uint64_t num_samples = 2586) { + sdsl::bit_vector dist(num_samples, 0); + std::vector eq1(((num_samples - 1) / 64) + 1), eq2(((num_samples - 1) / 64) + 1); + auto colorBuilder = [this](std::vector &eq, uint64_t eqid) { + uint64_t i{0}, bitcnt{0}, wrdcnt{0}; + while (i < this->num_samples) { + bitcnt = std::min(this->num_samples - i, (uint64_t) 64); + uint64_t wrd = (this->bv).get_int(this->num_samples * eqid + i, bitcnt); + eq[wrdcnt++] = wrd; + i += bitcnt; + } + }; + colorBuilder(eq1, eqid1); + colorBuilder(eq2, eqid2); + + for (uint64_t i = 0; i < eq1.size(); i++) { + uint64_t bitcnt = std::min(this->num_samples - (i * 64), (uint64_t) 64); + dist.set_int((i * 64), (eq1[i] ^ eq2[i]), bitcnt); + //std::cerr << i << " " << eq1[i] << " " << eq2[i] << " " << (eq1[i] ^ eq2[i]) << "\n"; + } + __uint128_t dist_hash = HashUtil::MurmurHash128A((void *) dist.data(), + dist.capacity() / 8, 2038074743, + 2038074751); + return dist_hash; +} + +void monochromatic_component_iterator::manhattanDistBvHash(uint64_t eqid1, + uint64_t eqid2, + sdsl::bit_vector &dist, + uint64_t num_samples = 2586) { + std::vector eq1(((num_samples - 1) / 64) + 1), eq2(((num_samples - 1) / 64) + 1); + auto colorBuilder = [this](std::vector &eq, uint64_t eqid) { + uint64_t i{0}, bitcnt{0}, wrdcnt{0}; + while (i < this->num_samples) { + bitcnt = std::min(this->num_samples - i, (uint64_t) 64); + uint64_t wrd = (this->bv).get_int(this->num_samples * eqid + i, bitcnt); + eq[wrdcnt++] = wrd; + i += bitcnt; + } + }; + colorBuilder(eq1, eqid1); + colorBuilder(eq2, eqid2); + + for (uint64_t i = 0; i < eq1.size(); i++) { + uint64_t bitcnt = std::min(this->num_samples - (i * 64), (uint64_t) 64); + dist.set_int((i * 64), (eq1[i] ^ eq2[i]), bitcnt); + //std::cerr << i << " " << eq1[i] << " " << eq2[i] << " " << (eq1[i] ^ eq2[i]) << "\n"; + } +} + + +void monochromatic_component_iterator::neighborDist(uint64_t cntrr) { KeyObject keyobj = *it; - //std::cout << "keyobj cnt: " << keyobj.count << "\n"; +/* + if (keyobj.count == 0) + std::cerr << "we have 0\n"; + if (keyobj.count >= 19216547) + std::cerr << cntrr << ", keyobj cnt: " << keyobj.count << "\n"; +*/ node curn(k, HashUtil::hash_64i(keyobj.key, BITMASK(cqf->keybits()))); - work_item cur = {curn, it.iter.current, keyobj.count}; + work_item cur = {curn, it.iter.current, keyobj.count - 1}; uint64_t mind{UINTMAX_MAX}, meand{0}, maxd{0}, neighborCnt{0}; - for (auto& nei : neighbors(cur)) { + for (auto &nei : neighbors(cur)) { neighborCnt++; if (nei.colorid != cur.colorid) { - //std::cerr << cur.colorid << " other: " << nei.colorid << "\n"; auto d = manhattanDist(nei.colorid, cur.colorid); mind = std::min(mind, d); maxd = std::max(maxd, d); meand += d; } - else + /*else { mind = 0; + }*/ + } + + if (!maxd) { + withMax0[neighborCnt]++; + return; + } + // when we get here, neighborCnt is > 0, and mind is not UINT_MAX + std::cout << neighborCnt << "\t" << mind << "\t" + << (meand / neighborCnt) << "\t" << maxd << "\n"; +} + +void monochromatic_component_iterator::uniqNeighborDist(uint64_t num_samples) { + KeyObject keyobj = *it; + node curn(k, HashUtil::hash_64i(keyobj.key, BITMASK(cqf->keybits()))); + work_item cur = {curn, it.iter.current, keyobj.count - 1}; + uint64_t mind{UINTMAX_MAX}, meand{0}, maxd{0}, neighborCnt{0}; + //std::cerr << " current : " << cur.colorid << "\n"; + for (auto &nei : neighbors(cur)) { + neighborCnt++; + if (nei.colorid != cur.colorid) { + //std::cerr << nei.colorid << "\n"; + //auto d = manhattanDistBvHash(nei.colorid, cur.colorid, num_samples); + sdsl::bit_vector d(num_samples, 0); + manhattanDistBvHash(nei.colorid, cur.colorid, d, num_samples); + if (eqclass_map.find(d) == eqclass_map.end()) { + eqclass_map[d] = 1; + } else { + eqclass_map[d]++; + } + } } - std::cout << neighborCnt << "\t" << (neighborCnt?(mind):0) << "\t" - << (neighborCnt?(meand/neighborCnt):0) << "\t" << maxd << "\n"; } /* @@ -355,12 +442,14 @@ int main(int argc, char *argv[]) { std::string command = argv[1]; std::string cqf_file = argv[2]; std::string eq_file = argv[3]; - uint64_t num_samples = 2586; + uint64_t num_samples = std::stoull(argv[4]); + //uint64_t num_samples = 2586; if (argc > 4) num_samples = std::stoull(argv[4]); std::cerr << "num samples: " << num_samples << "\n"; CQF cqf(cqf_file, false); BitVectorRRR bv(eq_file); + std::cerr << "num eq clss: " << bv.bit_size() / num_samples << "\n"; monochromatic_component_iterator mci(&cqf, bv, num_samples); if (command == "monocomp") { while (!mci.done()) { @@ -368,9 +457,51 @@ int main(int argc, char *argv[]) { ++mci; } } else if (command == "neighborDist") { + size_t cntrr = 0; while (!mci.done()) { - mci.neighborDist(); + mci.neighborDist(cntrr++); ++mci; + if (cntrr % 10000000 == 0) { + std::cerr << cntrr << " done\n"; + } } + std::cerr << "\n\nIn order of 0 to 8:\n"; + uint64_t total0s{0}; + for (auto val : mci.withMax0) { + std::cerr << val << ","; + total0s += val; + } + std::cerr << "\ntotal 0s: " << total0s - mci.withMax0[0] << "\n"; + } else if (command == "uniquDistanceDistribution") { + uint64_t cntr{0}; + while (!mci.done()) { + mci.uniqNeighborDist(num_samples); + if (++cntr % 1000000 == 0) { + std::cerr << cntr << " kmers & " << mci.eqclass_map.size() << " unique distances.\n"; + } + ++mci; + } + std::cerr << "writing the list of distinct distances:\n"; + sdsl::bit_vector outbv(mci.eqclass_map.size() * num_samples, 0); + std::cerr << "total cnt: " << mci.eqclass_map.size() << " " + << (mci.eqclass_map.size() * num_samples) << "\n"; + cntr = 0; + for (auto &eq_keyval : mci.eqclass_map) { + std::cout << eq_keyval.second << "\n"; + auto &bv = eq_keyval.first; + uint64_t b = 0; + while (b < num_samples) { + uint64_t bitcnt = std::min(num_samples - b, (uint64_t) 64); + uint64_t wrd = bv.get_int(b, bitcnt); + outbv.set_int(num_samples * cntr + b, wrd, bitcnt); + b += bitcnt; + } + cntr++; + //std::cerr << cntr << "\n"; + } + uint64_t till = cqf_file.find_last_of('/'); + sdsl::rrr_vector<> cbv(outbv); + sdsl::store_to_file(cbv, cqf_file.substr(0, till + 1) + "dist_bv.rrr"); + } } diff --git a/src/walkEqcls.cc b/src/walkEqcls.cc index 743906f..1b3cf0d 100644 --- a/src/walkEqcls.cc +++ b/src/walkEqcls.cc @@ -182,8 +182,8 @@ void compareCompressions(std::string &filename, size_t num_samples) { BitVectorRRR bvr(bv); CompressedSetBit setBitList(idxList); - size_t bvSize = bv.size_in_bytes(); - size_t bvrSize = bvr.size_in_bytes(); + size_t bvSize = bv.bit_size()/8;//size_in_bytes(); + size_t bvrSize = bvr.bit_size()/8;//size_in_bytes(); size_t compressedSize = setBitList.size_in_bytes(); bvSum += bvSize; bvrSum += bvrSize; @@ -278,35 +278,81 @@ void splitRows(std::string filename, //std::cerr << "1\n"; std::string eqfile; std::ifstream eqlist(filename); + std::ofstream bvlist(outdir+"/list.txt"); + uint64_t totalEqCls = 0; + uint64_t rowcntr = 0; if (eqlist.is_open()) { - while (getline(myfile, eqfile)) { + while (getline(eqlist, eqfile)) { sdsl::rrr_vector<63> bvr; sdsl::load_from_file(bvr, eqfile); - /*sdsl::rank_support_rrr<1, 63> ranks(&bvr); - size_t prev = 0; - size_t cur = 0; - //std::cerr << "1\n"; - for (uint64_t i = 1; i < 21; i++) { - cur = ranks(2586 * i); - std::cout << "rank: " << cur << " " << cur - prev << "\n"; - prev = cur; - } - std::exit(1);*/ std::cerr << "loaded " << bvr.size() << "\n"; totalEqCls = bvr.size()/num_samples; - std::cerr << "" - sdsl::bit_vector eqcls(totalEqCls *num_samples, 0); + std::cerr << "total eq in this set: " << totalEqCls << "\t"; std::cerr << "created\n"; - for (uint64_t i = 0; i < totalEqCls * num_samples; i += 64) { - uint64_t bitCnt = std::min(totalEqCls * num_samples - i, (uint64_t) 64); - uint64_t wrd = bvr.get_int(i, bitCnt); - //std::cerr << wrd << " "; - eqcls.set_int(i, wrd, bitCnt); + for (uint64_t eqcntr = 0; eqcntr < totalEqCls; eqcntr++) { + sdsl::bit_vector eqcls(num_samples, 0); + uint64_t i = 0; + while (i < num_samples) { + uint64_t bitCnt = std::min(num_samples - i, (uint64_t) 64); + uint64_t wrd = bvr.get_int(eqcntr*num_samples+i, bitCnt); + //std::cerr << wrd << " "; + eqcls.set_int(i, wrd, bitCnt); + i+=bitCnt; + } + std::string outbvfile = outdir + "/row" + std::to_string(rowcntr) + ".sim.bf.bv"; + sdsl::store_to_file(eqcls, outbvfile); + bvlist << outbvfile << "\n"; + rowcntr++; } - sdsl::store_to_file(eqcls, outfilename); + } + eqlist.close(); + bvlist.close(); + } + +} +void splitColumns(std::string filename, + std::string outdir, + uint64_t totalEqClsCnt = 222000000, + uint64_t num_samples = 2586) { + std::string eqfile; + std::ifstream eqlist(filename); + std::ofstream bvlist(outdir+"/collist.txt"); + std::vector cols(num_samples); + for (auto &bv : cols) bv = sdsl::bit_vector(totalEqClsCnt, 0); + if (eqlist.is_open()) { + uint64_t accumTotalEqCls = 0; + while (getline(eqlist, eqfile)) { + sdsl::rrr_vector<63> bvr; + sdsl::load_from_file(bvr, eqfile); + std::cerr << "loaded " << bvr.size() << "\n"; + //std::cerr << "total eq in this set: " << totalEqCls << "\t"; + //std::cerr << "created\n"; + uint64_t b = 0; + while (b < bvr.size()) { + for (uint64_t c = 0; c < num_samples; c++) { + if (bvr[(b + c)]) { + cols[c][accumTotalEqCls] = 1; + }/* else { + std::cerr << c << ":" << cols[c][accumTotalEqCls] << " "; + }*/ + } + //std::cerr << "\n"; + accumTotalEqCls++; + b += num_samples; + if (accumTotalEqCls == totalEqClsCnt) break; + } + } + uint64_t colcntr{0}; + for (auto& v : cols) { + std::string outbvfile = outdir + "/col" + std::to_string(colcntr) + ".sim.bf.bv"; + sdsl::store_to_file(v, outbvfile); + bvlist << outbvfile << "\n"; + colcntr++; } + eqlist.close(); + bvlist.close(); } } @@ -820,6 +866,35 @@ int main(int argc, char *argv[]) { } else if (command == "validate_uniqueness") { std::string dir = argv[2]; validate_uniqueness(dir, num_samples); + } else if (command == "splitRows") { + std::string filename = argv[2]; + std::string outdir = argv[3]; + num_samples = stoull(argv[4]); + splitRows(filename, outdir, num_samples); + } else if (command == "splitColumns") { + std::string filename = argv[2]; + std::string outdir = argv[3]; + num_samples = stoull(argv[4]); + uint64_t num_eqs = stoull(argv[5]); + splitColumns(filename, outdir, num_eqs, num_samples); + } else if (command == "quickvalidation") { + std::string filename = argv[2]; + sdsl::bit_vector eqcls; + sdsl::load_from_file(eqcls, filename); + std::ofstream out(filename + ".seq"); + uint64_t i = 0; + out << "start " << eqcls.bit_size() << "\n"; + while (i < eqcls.bit_size()) { + size_t wrd = eqcls.get_int(i, 64); + for (size_t j = 0; j < 64; j++) { + if (((wrd >> j) & 0x01)) { + std::cerr << (i+j) << " "; + } + } + i += 64; + } + out.close(); + } else { std::cerr << "ERROR: NO COMMANDS PROVIDED\n" << "OPTIONS ARE: validate, compareCompressions, compareCopies, reorder\n"; From e1523048f6395c888e32afa6f4526809b12024b7 Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Fri, 29 Jun 2018 01:12:32 -0400 Subject: [PATCH 015/122] updated and added a couple more tests and features --- include/monochromatic_component_iterator.h | 7 +- src/monochromatic_component_iterator.cc | 82 ++++++++++---------- src/walkEqcls.cc | 89 +++++++++++++++------- 3 files changed, 105 insertions(+), 73 deletions(-) diff --git a/include/monochromatic_component_iterator.h b/include/monochromatic_component_iterator.h index 14b26f0..2fe6cf1 100644 --- a/include/monochromatic_component_iterator.h +++ b/include/monochromatic_component_iterator.h @@ -149,7 +149,7 @@ class monochromatic_component_iterator { //monochromatic_component_iterator(const CQF *g); monochromatic_component_iterator(const CQF *g, - BitVectorRRR &bvin, + std::vector> &bvin, uint64_t num_samplesin = 2586); void neighborDist(uint64_t cntrr); @@ -168,7 +168,8 @@ class monochromatic_component_iterator { std::unordered_set visitedKeys; const CQF *cqf; CQF::Iterator it; - BitVectorRRR &bv; + //BitVectorRRR &bv; + std::vector> &bvs; uint64_t num_samples; sdsl::bit_vector visited; @@ -186,7 +187,7 @@ class monochromatic_component_iterator { void manhattanDistBvHash(uint64_t eq1, uint64_t eq2, sdsl::bit_vector& dist, uint64_t num_samples); - + void buildColor(std::vector &eq, uint64_t eqid); }; diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index 8c65530..5bb2a55 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -4,6 +4,8 @@ #include "monochromatic_component_iterator.h" +#define EQS_PER_SLOT 20000000 + uint64_t start_time; namespace dna { @@ -188,9 +190,9 @@ namespace dna { //////////////////////////////// monochromatic_component_iterator ////////////////////////// monochromatic_component_iterator::monochromatic_component_iterator(const CQF *g, - BitVectorRRR &bvin, + std::vector> &bvin, uint64_t num_samplesin) - : cqf(g), it(g->begin(0)), bv(bvin), num_samples(num_samplesin) { + : cqf(g), it(g->begin(0)), bvs(bvin), num_samples(num_samplesin) { // initialize cqf iterator k = cqf->keybits() / 2; // 2-bit encoded std::cerr << "k : " << k << "\n"; @@ -299,20 +301,24 @@ bool monochromatic_component_iterator::exists(edge e, uint64_t &idx, uint64_t &e return false; } +void monochromatic_component_iterator::buildColor(std::vector &eq, uint64_t eqid) { + uint64_t i{0}, bitcnt{0}, wrdcnt{0}; + uint64_t idx = eqid / EQS_PER_SLOT; + uint64_t offset = eqid % EQS_PER_SLOT; + //std::cerr << eqid << " " << num_samples << " " << idx << " " << offset << "\n"; + while (i < num_samples) { + bitcnt = std::min(num_samples - i, (uint64_t) 64); + uint64_t wrd = (bvs[idx]).get_int(offset*num_samples + i, bitcnt); + eq[wrdcnt++] = wrd; + i += bitcnt; + } +} + uint64_t monochromatic_component_iterator::manhattanDist(uint64_t eqid1, uint64_t eqid2) { uint64_t dist{0}; std::vector eq1(((num_samples - 1) / 64) + 1), eq2(((num_samples - 1) / 64) + 1); - auto colorBuilder = [this](std::vector &eq, uint64_t eqid) { - uint64_t i{0}, bitcnt{0}, wrdcnt{0}; - while (i < this->num_samples) { - bitcnt = std::min(this->num_samples - i, (uint64_t) 64); - uint64_t wrd = (this->bv).get_int(this->num_samples * eqid + i, bitcnt); - eq[wrdcnt++] = wrd; - i += bitcnt; - } - }; - colorBuilder(eq1, eqid1); - colorBuilder(eq2, eqid2); + buildColor(eq1, eqid1); + buildColor(eq2, eqid2); for (uint64_t i = 0; i < eq1.size(); i++) { if (eq1[i] != eq2[i]) @@ -327,17 +333,8 @@ __uint128_t monochromatic_component_iterator::manhattanDistBvHash(uint64_t eqid1 uint64_t num_samples = 2586) { sdsl::bit_vector dist(num_samples, 0); std::vector eq1(((num_samples - 1) / 64) + 1), eq2(((num_samples - 1) / 64) + 1); - auto colorBuilder = [this](std::vector &eq, uint64_t eqid) { - uint64_t i{0}, bitcnt{0}, wrdcnt{0}; - while (i < this->num_samples) { - bitcnt = std::min(this->num_samples - i, (uint64_t) 64); - uint64_t wrd = (this->bv).get_int(this->num_samples * eqid + i, bitcnt); - eq[wrdcnt++] = wrd; - i += bitcnt; - } - }; - colorBuilder(eq1, eqid1); - colorBuilder(eq2, eqid2); + buildColor(eq1, eqid1); + buildColor(eq2, eqid2); for (uint64_t i = 0; i < eq1.size(); i++) { uint64_t bitcnt = std::min(this->num_samples - (i * 64), (uint64_t) 64); @@ -355,17 +352,8 @@ void monochromatic_component_iterator::manhattanDistBvHash(uint64_t eqid1, sdsl::bit_vector &dist, uint64_t num_samples = 2586) { std::vector eq1(((num_samples - 1) / 64) + 1), eq2(((num_samples - 1) / 64) + 1); - auto colorBuilder = [this](std::vector &eq, uint64_t eqid) { - uint64_t i{0}, bitcnt{0}, wrdcnt{0}; - while (i < this->num_samples) { - bitcnt = std::min(this->num_samples - i, (uint64_t) 64); - uint64_t wrd = (this->bv).get_int(this->num_samples * eqid + i, bitcnt); - eq[wrdcnt++] = wrd; - i += bitcnt; - } - }; - colorBuilder(eq1, eqid1); - colorBuilder(eq2, eqid2); + buildColor(eq1, eqid1); + buildColor(eq2, eqid2); for (uint64_t i = 0; i < eq1.size(); i++) { uint64_t bitcnt = std::min(this->num_samples - (i * 64), (uint64_t) 64); @@ -417,7 +405,7 @@ void monochromatic_component_iterator::uniqNeighborDist(uint64_t num_samples) { for (auto &nei : neighbors(cur)) { neighborCnt++; if (nei.colorid != cur.colorid) { - //std::cerr << nei.colorid << "\n"; + //xstd::cerr << nei.colorid << "\n"; //auto d = manhattanDistBvHash(nei.colorid, cur.colorid, num_samples); sdsl::bit_vector d(num_samples, 0); manhattanDistBvHash(nei.colorid, cur.colorid, d, num_samples); @@ -441,16 +429,29 @@ int main(int argc, char *argv[]) { std::string command = argv[1]; std::string cqf_file = argv[2]; - std::string eq_file = argv[3]; + std::string eqlistfile = argv[3]; uint64_t num_samples = std::stoull(argv[4]); //uint64_t num_samples = 2586; if (argc > 4) num_samples = std::stoull(argv[4]); std::cerr << "num samples: " << num_samples << "\n"; CQF cqf(cqf_file, false); - BitVectorRRR bv(eq_file); - std::cerr << "num eq clss: " << bv.bit_size() / num_samples << "\n"; - monochromatic_component_iterator mci(&cqf, bv, num_samples); + std::cerr << "cqf loaded: " << cqf.size() << "\n"; + std::string eqfile; + std::ifstream eqlist(eqlistfile); + std::vector> bvs; + bvs.reserve(20); + if (eqlist.is_open()) { + uint64_t accumTotalEqCls = 0; + while (getline(eqlist, eqfile)) { + sdsl::rrr_vector<63> bv; + bvs.push_back(bv); + sdsl::load_from_file(bvs.back(), eqfile); + } + } + //BitVectorRRR bv(eqfile); + std::cerr << "num eq clss: " << ((bvs.size()-1)*EQS_PER_SLOT*num_samples + bvs.back().size()) / num_samples << "\n"; + monochromatic_component_iterator mci(&cqf, bvs, num_samples); if (command == "monocomp") { while (!mci.done()) { std::cout << (*mci).nodeCnt << "\n"; @@ -502,6 +503,5 @@ int main(int argc, char *argv[]) { uint64_t till = cqf_file.find_last_of('/'); sdsl::rrr_vector<> cbv(outbv); sdsl::store_to_file(cbv, cqf_file.substr(0, till + 1) + "dist_bv.rrr"); - } } diff --git a/src/walkEqcls.cc b/src/walkEqcls.cc index 1b3cf0d..edc648f 100644 --- a/src/walkEqcls.cc +++ b/src/walkEqcls.cc @@ -93,7 +93,7 @@ void validate_uniqueness(std::string &directory, size_t num_samples) { //subPattern.calcHash(); uint64_t bitcntr = 0; while (bitcntr < eqcls.bit_size()) { - std::cerr << "\n" << curCntr << "\n"; + //std::cerr << "\n" << curCntr << "\n"; BitVector eq(num_samples); size_t i = 0; @@ -104,9 +104,9 @@ void validate_uniqueness(std::string &directory, size_t num_samples) { if ((wrd >> j) & 0x01) { eq.set(curIdx); - } else { + } /*else { std::cerr << curIdx << " "; - } + }*/ } i += bitCnt; bitcntr += bitCnt; @@ -124,8 +124,12 @@ void validate_uniqueness(std::string &directory, size_t num_samples) { } cntr++; curCntr++; + if (curCntr % 1000000 == 0) { + std::cerr << "passed " << curCntr << "th eq.\n"; + } } } + std::cerr << "Validation passed!!! Found " << cntr << " unique eq classes.\n"; } @@ -319,30 +323,51 @@ void splitColumns(std::string filename, std::ifstream eqlist(filename); std::ofstream bvlist(outdir+"/collist.txt"); std::vector cols(num_samples); + //std::vector wrds(num_samples); for (auto &bv : cols) bv = sdsl::bit_vector(totalEqClsCnt, 0); if (eqlist.is_open()) { - uint64_t accumTotalEqCls = 0; + uint64_t accumTotalEqCls = 0, lineCntr{0}; while (getline(eqlist, eqfile)) { - sdsl::rrr_vector<63> bvr; - sdsl::load_from_file(bvr, eqfile); - std::cerr << "loaded " << bvr.size() << "\n"; - //std::cerr << "total eq in this set: " << totalEqCls << "\t"; - //std::cerr << "created\n"; - uint64_t b = 0; - while (b < bvr.size()) { - for (uint64_t c = 0; c < num_samples; c++) { - if (bvr[(b + c)]) { - cols[c][accumTotalEqCls] = 1; - }/* else { - std::cerr << c << ":" << cols[c][accumTotalEqCls] << " "; + //if (lineCntr >= 10) { + sdsl::rrr_vector<63> bvr; + std::cerr << "file: " << eqfile << "\n"; + sdsl::load_from_file(bvr, eqfile); + std::cerr << "loaded " << bvr.size() + << " accumulative Eq Cnt: " << accumTotalEqCls << "\n"; + //std::cerr << "total eq in this set: " << totalEqCls << "\t"; + //std::cerr << "created\n"; + uint64_t b = 0; + while (b < bvr.size() and accumTotalEqCls < totalEqClsCnt) { + uint64_t c{0}, bitcnt{0}, wrd{0}, sampleCntr{0}; + while (c < num_samples) { + bitcnt = std::min(num_samples - c, (uint64_t) 64); + wrd = bvr.get_int(b + c, bitcnt); + for (uint64_t j = 0; j < bitcnt; j++) { + if ((wrd >> j) & 0x01) { + cols[sampleCntr][accumTotalEqCls] = 1; + } + sampleCntr++; + } + c += bitcnt; + } + /*for (uint64_t c = 0; c < num_samples; c++) { + if (bvr[(b + c)]) { + cols[c][accumTotalEqCls] = 1; + } }*/ + //std::cerr << "\n"; + accumTotalEqCls++; + if (accumTotalEqCls % 1000000 == 0 || accumTotalEqCls > 222584820) { + std::cerr << accumTotalEqCls << " processed\n"; + } + b += num_samples; } - //std::cerr << "\n"; - accumTotalEqCls++; - b += num_samples; - if (accumTotalEqCls == totalEqClsCnt) break; - } + /*} else { + lineCntr++; + accumTotalEqCls += 20000000; + }*/ } + std::cerr << "\n\nColumn bvs ready to be stored to file .. \n"; uint64_t colcntr{0}; for (auto& v : cols) { std::string outbvfile = outdir + "/col" + std::to_string(colcntr) + ".sim.bf.bv"; @@ -357,6 +382,7 @@ void splitColumns(std::string filename, } + void decompress(std::string filename, std::string outfilename, uint64_t totalEqCls, @@ -364,18 +390,18 @@ void decompress(std::string filename, //std::cerr << "1\n"; sdsl::rrr_vector<63> bvr; sdsl::load_from_file(bvr, filename); - sdsl::rank_support_rrr<1, 63> ranks(&bvr); + //sdsl::rank_support_rrr<1, 63> ranks(&bvr); //std::cerr << "1\n"; - totalEqCls = 1; + //totalEqCls = 1; size_t prev = 0; size_t cur = 0; //std::cerr << "1\n"; - for (uint64_t i = 1; i < 21; i++) { + /*for (uint64_t i = 1; i < 21; i++) { cur = ranks(2586 * i); std::cout << "rank: " << cur << " " << cur - prev << "\n"; prev = cur; - } - std::exit(1); + }*/ + //std::exit(1); std::cerr << "loaded " << bvr.size() << "\n"; sdsl::bit_vector eqcls(totalEqCls * num_samples, 0); std::cerr << "created\n"; @@ -771,10 +797,10 @@ void writeEq(std::string filename, std::string out_filename, uint64_t num_samples, uint64_t totalEqCls) { - sdsl::bit_vector eqcls; + sdsl::rrr_vector<63> eqcls; sdsl::load_from_file(eqcls, filename); std::ofstream out(out_filename + ".matrix"); - size_t totalEqClsCnt = eqcls.bit_size() / num_samples; //222584822; + size_t totalEqClsCnt = eqcls.size() / num_samples; //222584822; if (totalEqCls > 0) { totalEqClsCnt = totalEqCls; } @@ -784,7 +810,11 @@ void writeEq(std::string filename, size_t bitCnt = std::min(num_samples - i, (size_t) 64); size_t wrd = eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); for (size_t j = 0; j < bitCnt; j++) { - out << ((wrd >> j) & 0x01) << "\t"; + if (((wrd >> j) & 0x01)) + out << "1"; + else + out <<" "; + //out << ((wrd >> j) & 0x01) << "\t"; } i += bitCnt; } @@ -865,6 +895,7 @@ int main(int argc, char *argv[]) { 8); } else if (command == "validate_uniqueness") { std::string dir = argv[2]; + num_samples = stoull(argv[3]); validate_uniqueness(dir, num_samples); } else if (command == "splitRows") { std::string filename = argv[2]; From f71d2b2121169e58ac93df470dfd795ac3030a78 Mon Sep 17 00:00:00 2001 From: Fatemeh Date: Fri, 29 Jun 2018 01:35:17 -0400 Subject: [PATCH 016/122] added num_samples as an argument to validate_uniqueness subcommand for walkEqcls --- src/walkEqcls.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/walkEqcls.cc b/src/walkEqcls.cc index edc648f..66310cd 100644 --- a/src/walkEqcls.cc +++ b/src/walkEqcls.cc @@ -863,9 +863,10 @@ int main(int argc, char *argv[]) { } std::string filename = argv[2]; std::string output_filename = argv[3]; + num_samples = stoull(argv[4]); uint64_t totalEqCls = 0; - if (argc == 5) - totalEqCls = std::stoull(argv[4]); + if (argc == 6) + totalEqCls = std::stoull(argv[5]); writeEq(filename, output_filename, num_samples, totalEqCls); } else if (command == "reorder") { From b767b582e640d0d57212f028941c3f080d265304 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 13 Jul 2018 11:45:00 -0400 Subject: [PATCH 017/122] 1) Ignore self-loops in neighbor calculations 2) Consider neighbors of distance 0 --- src/monochromatic_component_iterator.cc | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index 5bb2a55..854a3ec 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -200,6 +200,8 @@ monochromatic_component_iterator::monochromatic_component_iterator(const CQFsize() << "\n"; std::cerr << "slots: " << cqf->slots() << "\n"; withMax0.resize(9); + sdsl::bit_vector d(num_samples, 0); + eqclass_map[d] = 0; } monochromatic_component_iterator::work_item @@ -282,9 +284,11 @@ monochromatic_component_iterator::neighbors(monochromatic_component_iterator::wo for (const auto b : dna::bases) { uint64_t eqid, idx; if (exists(b >> n.curr/*b + n.curr*/, idx, eqid)) - result.insert(work_item(b >> n.curr, idx, eqid)); + if (eqid != n.colorid) // ignore the neighbor if it's a self-loop + result.insert(work_item(b >> n.curr, idx, eqid)); if (exists(n.curr << b/*n.curr + b*/, idx, eqid)) - result.insert(work_item(n.curr << b, idx, eqid)); + if (eqid != n.colorid) + result.insert(work_item(n.curr << b, idx, eqid)); } return result; } @@ -382,9 +386,13 @@ void monochromatic_component_iterator::neighborDist(uint64_t cntrr) { maxd = std::max(maxd, d); meand += d; } - /*else { + else { mind = 0; - }*/ + } + } + // if the node is isolated (or only has a self-loop) it should have maximum possible distance + if (neighborCnt == 0) { + mind = maxd = meand = num_samples; } if (!maxd) { @@ -404,17 +412,19 @@ void monochromatic_component_iterator::uniqNeighborDist(uint64_t num_samples) { //std::cerr << " current : " << cur.colorid << "\n"; for (auto &nei : neighbors(cur)) { neighborCnt++; + sdsl::bit_vector d(num_samples, 0); if (nei.colorid != cur.colorid) { //xstd::cerr << nei.colorid << "\n"; //auto d = manhattanDistBvHash(nei.colorid, cur.colorid, num_samples); - sdsl::bit_vector d(num_samples, 0); manhattanDistBvHash(nei.colorid, cur.colorid, d, num_samples); if (eqclass_map.find(d) == eqclass_map.end()) { eqclass_map[d] = 1; } else { eqclass_map[d]++; } - } + } /*else { + eqclass_map[d]++; + }*/ } } From 1339d5fc46196ba634d8633182d9353b3ce7c4fb Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Sat, 14 Jul 2018 20:23:27 -0400 Subject: [PATCH 018/122] Code to get list of edges between equivalence classes using kmers in dbg (no threshold) --- include/monochromatic_component_iterator.h | 26 ++++++++- src/monochromatic_component_iterator.cc | 64 ++++++++++++++++++---- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/include/monochromatic_component_iterator.h b/include/monochromatic_component_iterator.h index 2fe6cf1..139fa5d 100644 --- a/include/monochromatic_component_iterator.h +++ b/include/monochromatic_component_iterator.h @@ -126,6 +126,25 @@ struct bvHash128 { } }; + +struct Edge { + uint32_t n1; + uint32_t n2; + + Edge(uint32_t inN1, uint32_t inN2) : n1(inN1), n2(inN2) {} + + bool operator==(const Edge& e) const { + return n1 == e.n1 && n2 == e.n2; + } +}; + +struct edge_hash { + uint64_t operator() (const Edge& e) const { + uint64_t res = e.n1; + return (res << 32) | (uint64_t)e.n2; + } +}; + class monochromatic_component_iterator { public: class work_item { @@ -153,12 +172,15 @@ class monochromatic_component_iterator { uint64_t num_samplesin = 2586); void neighborDist(uint64_t cntrr); + void buildEqGraph(uint64_t cntrr); + void uniqNeighborDist(uint64_t num_samples); - uint64_t cntr = 0; + uint64_t cntr{0}, isolatedCnt{0}; std::vector withMax0; //spp::sparse_hash_map<__uint128_t, uint64_t, hash128> eqclass_map; spp::sparse_hash_map eqclass_map; + std::unordered_map edges; private: @@ -172,6 +194,8 @@ class monochromatic_component_iterator { std::vector> &bvs; uint64_t num_samples; sdsl::bit_vector visited; + uint16_t distThreshold = 5; + bool exists(edge e, uint64_t &idx, uint64_t &eqid); diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index 854a3ec..c44ad05 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -190,9 +190,14 @@ namespace dna { //////////////////////////////// monochromatic_component_iterator ////////////////////////// monochromatic_component_iterator::monochromatic_component_iterator(const CQF *g, - std::vector> &bvin, - uint64_t num_samplesin) - : cqf(g), it(g->begin(0)), bvs(bvin), num_samples(num_samplesin) { + std::vector> + +&bvin, +uint64_t num_samplesin +) +: + +cqf (g), it(g->begin(0)), bvs(bvin), num_samples(num_samplesin) { // initialize cqf iterator k = cqf->keybits() / 2; // 2-bit encoded std::cerr << "k : " << k << "\n"; @@ -312,7 +317,7 @@ void monochromatic_component_iterator::buildColor(std::vector &eq, uin //std::cerr << eqid << " " << num_samples << " " << idx << " " << offset << "\n"; while (i < num_samples) { bitcnt = std::min(num_samples - i, (uint64_t) 64); - uint64_t wrd = (bvs[idx]).get_int(offset*num_samples + i, bitcnt); + uint64_t wrd = (bvs[idx]).get_int(offset * num_samples + i, bitcnt); eq[wrdcnt++] = wrd; i += bitcnt; } @@ -385,14 +390,14 @@ void monochromatic_component_iterator::neighborDist(uint64_t cntrr) { mind = std::min(mind, d); maxd = std::max(maxd, d); meand += d; - } - else { + } else { mind = 0; } } // if the node is isolated (or only has a self-loop) it should have maximum possible distance if (neighborCnt == 0) { - mind = maxd = meand = num_samples; + isolatedCnt++; + return; } if (!maxd) { @@ -404,6 +409,28 @@ void monochromatic_component_iterator::neighborDist(uint64_t cntrr) { << (meand / neighborCnt) << "\t" << maxd << "\n"; } +void monochromatic_component_iterator::buildEqGraph(uint64_t cntrr) { + KeyObject keyobj = *it; + node curn(k, HashUtil::hash_64i(keyobj.key, BITMASK(cqf->keybits()))); + work_item cur = {curn, it.iter.current, keyobj.count - 1}; + uint64_t neighborCnt{0}; + for (auto &nei : neighbors(cur)) { + neighborCnt++; + if (nei.colorid < cur.colorid) { + uint16_t d = (uint16_t) manhattanDist(nei.colorid, cur.colorid); + //if (d <= distThreshold) { + Edge e(static_cast(nei.colorid), static_cast(cur.colorid)); + if (edges.find(e) == edges.end()) { + edges[e] = d; + } else if (edges[e] > d) { + edges[e] = d; + } + //} + } + } + +} + void monochromatic_component_iterator::uniqNeighborDist(uint64_t num_samples) { KeyObject keyobj = *it; node curn(k, HashUtil::hash_64i(keyobj.key, BITMASK(cqf->keybits()))); @@ -449,7 +476,8 @@ int main(int argc, char *argv[]) { std::cerr << "cqf loaded: " << cqf.size() << "\n"; std::string eqfile; std::ifstream eqlist(eqlistfile); - std::vector> bvs; + std::vector> + bvs; bvs.reserve(20); if (eqlist.is_open()) { uint64_t accumTotalEqCls = 0; @@ -460,7 +488,8 @@ int main(int argc, char *argv[]) { } } //BitVectorRRR bv(eqfile); - std::cerr << "num eq clss: " << ((bvs.size()-1)*EQS_PER_SLOT*num_samples + bvs.back().size()) / num_samples << "\n"; + std::cerr << "num eq clss: " << ((bvs.size() - 1) * EQS_PER_SLOT * num_samples + bvs.back().size()) / num_samples + << "\n"; monochromatic_component_iterator mci(&cqf, bvs, num_samples); if (command == "monocomp") { while (!mci.done()) { @@ -482,7 +511,22 @@ int main(int argc, char *argv[]) { std::cerr << val << ","; total0s += val; } - std::cerr << "\ntotal 0s: " << total0s - mci.withMax0[0] << "\n"; + std::cerr << "\ntotal 0s: " << total0s - mci.withMax0[0] + << "\ntota isolated kmers: " << mci.isolatedCnt + << "\n"; + } else if (command == "buildEqGraph") { + size_t cntrr = 0; + while (!mci.done()) { + mci.buildEqGraph(cntrr++); + ++mci; + if (cntrr % 10000000 == 0) { + std::cerr << cntrr << " kmers & " << mci.edges.size() << " edges\n"; + } + } + + for (auto &kv : mci.edges) { + std::cout << kv.first.n1 << "\t" << kv.first.n2 << "\t" << kv.second << "\n"; + } } else if (command == "uniquDistanceDistribution") { uint64_t cntr{0}; while (!mci.done()) { From 509a2b73d8a864afb07c20f2d9c8d6d14b1d4152 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 17 Jul 2018 23:37:39 -0400 Subject: [PATCH 019/122] Needed some cleaning first. 1) Used clipp for input arguments 2) Added two commands, one for just outputting some information about the CCs and the other for actually building the MSF datastructure 3) Added MSF as a target to cmake file --- include/cqf.h | 8 ++ src/CMakeLists.txt | 20 ++--- src/{MSF.cpp => MSF.cc} | 107 ++++++++++++++++++++---- src/monochromatic_component_iterator.cc | 4 +- 4 files changed, 111 insertions(+), 28 deletions(-) rename src/{MSF.cpp => MSF.cc} (62%) diff --git a/include/cqf.h b/include/cqf.h index 456375e..e2134c2 100644 --- a/include/cqf.h +++ b/include/cqf.h @@ -55,6 +55,7 @@ class CQF { qf_serialize(&cqf, filename.c_str()); } + std::pair queryValAndIdx( key_obj& k) const; uint64_t range(void) const { return cqf.metadata->range; } uint32_t seed(void) const { return cqf.metadata->seed; } uint32_t keybits(void) const { return cqf.metadata->key_bits; } @@ -147,6 +148,13 @@ uint64_t CQF::query(const key_obj& k) { return qf_count_key_value(&cqf, k.key, k.value); } +template +std::pair CQF::queryValAndIdx(key_obj& k) const { + uint64_t eq, idx; + eq = qf_key_value_index(&cqf, k.key, k.value, &idx); + return std::make_pair(eq, idx); +} + template uint64_t CQF::get_index(const key_obj& k) { return get_bucket_index(k.key); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 93bf2c0..a0a7aae 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -42,9 +42,6 @@ target_compile_options(mantis PUBLIC "$<$,$,$>:${MANTIS_RELEASE_CFLAGS}>") target_compile_options(mantis PUBLIC "$<$,$>:${MANTIS_RELEASE_CXXFLAGS}>") target_compile_definitions(mantis PUBLIC "${ARCH_DEFS}") -add_executable(monochromatic_component_iterator - monochromatic_component_iterator.cc) -add_executable(walkEqcls walkEqcls.cc) add_executable(build_eq_graph build_eq_graph.cc) target_include_directories(build_eq_graph PUBLIC $) @@ -63,15 +60,18 @@ endif() install(TARGETS mantis RUNTIME DESTINATION bin) + +add_executable(monochromatic_component_iterator + monochromatic_component_iterator.cc) target_include_directories(monochromatic_component_iterator PUBLIC $) - -target_include_directories(walkEqcls PUBLIC - $ $) - -target_link_libraries(mantis - mantis_core) target_link_libraries(monochromatic_component_iterator mantis_core) + +#link_directories($) +add_executable(walkEqcls walkEqcls.cc) +target_include_directories(walkEqcls PUBLIC + $ $) +find_library(compression_library NAMES SIMDCompressionAndIntersection HINTS "${CMAKE_SOURCE_DIR}/lib") target_link_libraries(walkEqcls - mantis_core SIMDCompressionAndIntersection) + mantis_core ${compression_library}) diff --git a/src/MSF.cpp b/src/MSF.cc similarity index 62% rename from src/MSF.cpp rename to src/MSF.cc index 27d4ea2..0952804 100644 --- a/src/MSF.cpp +++ b/src/MSF.cc @@ -4,9 +4,11 @@ // The algorithm's basic implementation taken from // https://www.geeksforgeeks.org/kruskals-minimum-spanning-tree-using-stl-in-c/ // + #include #include #include +#include "clipp.h" #include "bitvector.h" //#include "sdsl/bits.hpp" @@ -22,12 +24,15 @@ struct Edge { }; struct DisjointSetNode { - uint64_t parent{0}, rnk{0}, w{0}, edges{0}; + uint64_t parent{0}, realParent{0}, rnk{0}, w{0}, edges{0}; void setParent(uint64_t p) { parent = p; } - void mergeWith(DisjointSetNode &n, uint16_t edgeW) { + void setRealParent(uint64_t p) { parent = p; } + + void mergeWith(DisjointSetNode &n, uint16_t edgeW, uint64_t id) { n.setParent(parent); + n.setRealParent(id); w += (n.w + static_cast(edgeW)); edges += (n.edges + 1); n.edges = 0; @@ -53,6 +58,7 @@ struct DisjointSets { for (uint64_t i = 0; i <= n; i++) { //every element is parent of itself els[i].setParent(i); + els[i].setRealParent(i); } } @@ -73,10 +79,10 @@ struct DisjointSets { /* Make tree with smaller height a subtree of the other tree */ if (els[x].rnk > els[y].rnk) { - els[x].mergeWith(els[y], edgeW); + els[x].mergeWith(els[y], edgeW, x); } else {// If rnk[x] <= rnk[y] - els[y].mergeWith(els[x], edgeW); + els[y].mergeWith(els[x], edgeW, y); } } }; @@ -164,22 +170,88 @@ struct Graph { } }; +struct Opts { + std::string filename; + uint64_t numNodes; // = std::stoull(argv[2]); + uint64_t bucketCnt; // = std::stoull(argv[3]); + uint64_t numSamples; + std::string eqClsListFile; +}; + int main(int argc, char *argv[]) { /* Let us create above shown weighted and undirected graph */ - std::string filename = argv[1]; - uint64_t numNodes = std::stoull(argv[2]); - uint64_t bucketCnt = std::stoull(argv[3]); - ifstream file(filename); + using namespace clipp; + enum class mode { + build, ccInfo, help + }; + mode selected = mode::help; + Opts opt; + auto ccInfo_mode = ( + command("ccInfo").set(selected, mode::ccInfo), + required("-e", "--edge-filename") & + value("edge_filename", opt.filename) % "file containing list of eq. class edges.", + required("-n", "--eqCls-cnt") & + value("equivalenceClass_count", opt.numNodes) % "Total number of equivalence (color) classes.", + required("-b", "--bucket-cnt") & + value("bucket_count", opt.bucketCnt) % "Total number of valid distances." + ); + + auto build_mode = ( + command("build").set(selected, mode::build), + required("-e", "--edge-filename") & + value("edge_filename", opt.filename) % "File containing list of eq. class edges.", + required("-n", "--eqCls-cnt") & + value("equivalenceClass_count", opt.numNodes) % "Total number of equivalence (color) classes.", + required("-b", "--bucket-cnt") & + value("bucket_count", opt.bucketCnt) % "Total number of valid distances.", + required("-s", "--numSamples") & + value("numSamples", opt.numSamples) % "Total number of experiments (samples).", + required("-c", "--eqCls-lst") & + value("eqCls_list", opt.eqClsListFile) % "File containing list of equivalence (color) classes." + ); + + auto cli = ( + (build_mode | ccInfo_mode | command("help").set(selected, mode::help) + ) + ); + + decltype(parse(argc, argv, cli)) res; + try { + res = parse(argc, argv, cli); + } catch (std::exception &e) { + std::cout << "\n\nParsing command line failed with exception: " << e.what() << "\n"; + std::cout << "\n\n"; + std::cout << make_man_page(cli, "MSF"); + return 1; + } - Graph g(bucketCnt); + //explore_options_verbose(res); + + if (res) { + switch (selected) { + //case mode::ccInfo: query_main(qopt); break; + //case mode::build: validate_main(vopt); break; + case mode::help: + std::cerr << make_man_page(cli, "MSF"); + break; + } + } + + std::cerr << "here are the inputs: \n" + << opt.filename << "\n" + << opt.numNodes << "\n" + << opt.bucketCnt << "\n"; + ifstream file(opt.filename); + + Graph g(opt.bucketCnt); uint32_t w_; uint64_t n1, n2, edgeCntr{0}; std::string tmp; { //unordered_set nodes; - sdsl::bit_vector nodes(numNodes, 0); + sdsl::bit_vector nodes(opt.numNodes, 0); std::getline(file, tmp); while (file.good()) { file >> n1 >> n2 >> w_; @@ -195,7 +267,7 @@ int main(int argc, char *argv[]) { file.close(); uint64_t distinctNodes{0}; - for (uint64_t i = 0; i < numNodes; i += 64) { + for (uint64_t i = 0; i < nodes.size(); i += 64) { distinctNodes += sdsl::bits::cnt(nodes.get_int(i, 64)); } std::cerr << "# of nodes: " << distinctNodes//nodes.size() @@ -203,18 +275,21 @@ int main(int argc, char *argv[]) { << "\n"; // nodes.clear(); } - g.V = numNodes; + g.V = opt.numNodes; g.E = edgeCntr; //g.V = numNodes; //ifstream file(filename); - DisjointSets ds = g.kruskalMSF(bucketCnt, file); + DisjointSets ds = g.kruskalMSF(opt.bucketCnt, file); - for (auto &el : ds.els) { - if (el.w != 0) { - std::cout << el.edges << "\t" << el.w << "\t" << el.rnk << "\n"; + if (selected == mode::ccInfo) { + for (auto &el : ds.els) { + if (el.w != 0) { + std::cout << el.edges << "\t" << el.w << "\t" << el.rnk << "\n"; + } } } + return 0; } diff --git a/src/monochromatic_component_iterator.cc b/src/monochromatic_component_iterator.cc index c44ad05..6ba60d9 100644 --- a/src/monochromatic_component_iterator.cc +++ b/src/monochromatic_component_iterator.cc @@ -201,9 +201,9 @@ cqf (g), it(g->begin(0)), bvs(bvin), num_samples(num_samplesin) { // initialize cqf iterator k = cqf->keybits() / 2; // 2-bit encoded std::cerr << "k : " << k << "\n"; - sdsl::util::assign(visited, sdsl::bit_vector(cqf->slots(), 0)); + sdsl::util::assign(visited, sdsl::bit_vector(cqf->capacity(), 0)); std::cerr << "kmers: " << cqf->size() << "\n"; - std::cerr << "slots: " << cqf->slots() << "\n"; + std::cerr << "slots: " << cqf->capacity() << "\n"; withMax0.resize(9); sdsl::bit_vector d(num_samples, 0); eqclass_map[d] = 0; From c6b1008d4c4553114ae2f97e28470827fd6e0cc0 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Wed, 18 Jul 2018 13:08:57 -0400 Subject: [PATCH 020/122] Add the ability to build parent bv --- src/MSF.cc | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/MSF.cc b/src/MSF.cc index 0952804..2e9e7b2 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -12,6 +12,8 @@ #include "bitvector.h" //#include "sdsl/bits.hpp" +#define EQS_PER_SLOT 20000000 + using namespace std; struct Edge { @@ -28,7 +30,7 @@ struct DisjointSetNode { void setParent(uint64_t p) { parent = p; } - void setRealParent(uint64_t p) { parent = p; } + void setRealParent(uint64_t p) { realParent = p; } void mergeWith(DisjointSetNode &n, uint16_t edgeW, uint64_t id) { n.setParent(parent); @@ -178,6 +180,40 @@ struct Opts { std::string eqClsListFile; }; +void loadEqs(std::string filename, std::vector> &bvs) { + bvs.reserve(20); + std::string eqfile; + std::ifstream eqlist(filename); + if (eqlist.is_open()) { + uint64_t accumTotalEqCls = 0; + while (getline(eqlist, eqfile)) { + sdsl::rrr_vector<63> bv; + bvs.push_back(bv); + sdsl::load_from_file(bvs.back(), eqfile); + } + } + //BitVectorRRR bv(eqfile); + std::cerr << "loaded all the equivalence classes: " << ((bvs.size() - 1) * EQS_PER_SLOT + bvs.back().size()) + << "\n"; +} + +void buildColor(std::vector> &bvs, + std::vector &eq, + uint64_t eqid, + uint64_t num_samples) { + uint64_t i{0}, bitcnt{0}, wrdcnt{0}; + uint64_t idx = eqid / EQS_PER_SLOT; + uint64_t offset = eqid % EQS_PER_SLOT; +//std::cerr << eqid << " " << num_samples << " " << idx << " " << offset << "\n"; + while (i < num_samples) { + bitcnt = std::min(num_samples - i, (uint64_t) 64); + uint64_t wrd = (bvs[idx]).get_int(offset * num_samples + i, bitcnt); + eq[wrdcnt++] = wrd; + i += bitcnt; + } +} + + int main(int argc, char *argv[]) { /* Let us create above shown weighted and undirected graph */ @@ -242,6 +278,7 @@ int main(int argc, char *argv[]) { << opt.filename << "\n" << opt.numNodes << "\n" << opt.bucketCnt << "\n"; + ifstream file(opt.filename); Graph g(opt.bucketCnt); @@ -290,6 +327,39 @@ int main(int argc, char *argv[]) { } } + if (selected == mode::build) { + std::vector> bvs; + loadEqs(opt.eqClsListFile, bvs); + std::vector eq1(((opt.numSamples - 1) / 64) + 1), + eq2(((opt.numSamples - 1) / 64) + 1); + sdsl::int_vector<> parents(opt.numNodes, 0, (uint64_t) ceil(log2(opt.numNodes))); + uint64_t totalDeltas{0}, rootDeltas{0}, rootCnt{0}; + for (auto i = 0; i < opt.numNodes; i++) { + if (ds.els[i].realParent == i) { + parents[i] = 0; + buildColor(bvs, eq1, i+1, opt.numSamples); + for (auto& wrd : eq1) { + rootDeltas += sdsl::bits::cnt(wrd); + } + rootCnt ++; + } + else + parents[i] = ds.els[i].realParent; + totalDeltas += ds.els[i].w; + } + /*buildColor(eq1, eqid, opt.numSamples); + for (uint64_t i = 0; i < eq1.size(); i++) { + uint64_t bitcnt = std::min(this->num_samples - (i * 64), (uint64_t) 64); + dist.set_int((i * 64), (eq1[i] ^ eq2[i]), bitcnt); + //std::cerr << i << " " << eq1[i] << " " << eq2[i] << " " << (eq1[i] ^ eq2[i]) << "\n"; + }*/ + sdsl::store_to_file(parents, "parent.tst"); + std::cerr << "parent size: " << sdsl::size_in_mega_bytes(parents) << "MB\n" + << "delta size: " << totalDeltas * log2(opt.numSamples) / 8 << "B\n" + << "bbv size: " << totalDeltas/8 << "B\n" + << "root delta count: " << rootCnt << " " << rootDeltas << "\n"; + } + return 0; } From 7654c7308bbdf878d51b115b55407f34f9c0a182 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Thu, 19 Jul 2018 11:15:47 -0400 Subject: [PATCH 021/122] Start using boost for MST --- src/CMakeLists.txt | 11 ++++++++ src/MST_boost.cc | 67 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/MST_boost.cc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a0a7aae..d89e2ea 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -68,6 +68,17 @@ target_include_directories(monochromatic_component_iterator PUBLIC target_link_libraries(monochromatic_component_iterator mantis_core) + +add_executable(msf MSF.cc) +target_include_directories(msf PUBLIC + $) +target_link_libraries(msf + mantis_core) + +add_executable(mstBoost MST_boost.cc) + target_include_directories(mstBoost PUBLIC + $) +target_link_libraries(mstBoost mantis_core) #link_directories($) add_executable(walkEqcls walkEqcls.cc) target_include_directories(walkEqcls PUBLIC diff --git a/src/MST_boost.cc b/src/MST_boost.cc new file mode 100644 index 0000000..ee019e7 --- /dev/null +++ b/src/MST_boost.cc @@ -0,0 +1,67 @@ +// +// Created by Fatemeh Almodaresi on 7/19/18. +// + +#include +#include +#include +#include + +int main(int argc, char *argv[]) { + using namespace boost; + typedef adjacency_list > Graph; + typedef graph_traits::edge_descriptor Edge; + typedef graph_traits::vertex_descriptor Vertex; + typedef std::pair E; + + const int num_nodes = 5; + E edge_array[] = {E(0, 2), E(1, 3), E(1, 4), E(2, 1), E(2, 3), + E(3, 4), E(4, 0), E(4, 1) + }; + int weights[] = {1, 1, 2, 7, 3, 1, 1, 1}; + std::size_t num_edges = sizeof(edge_array) / sizeof(E); +#if defined(BOOST_MSVC) && BOOST_MSVC <= 1300 + Graph g(num_nodes); + property_map::type weightmap = get(edge_weight, g); + for (std::size_t j = 0; j < num_edges; ++j) { + Edge e; bool inserted; + tie(e, inserted) = add_edge(edge_array[j].first, edge_array[j].second, g); + weightmap[e] = weights[j]; + } +#else + Graph g(edge_array, edge_array + num_edges, weights, num_nodes); +#endif + property_map::type weight = get(edge_weight, g); + std::vector spanning_tree; + + kruskal_minimum_spanning_tree(g, std::back_inserter(spanning_tree)); + + std::cout << "Print the edges in the MST:" << std::endl; + for (std::vector::iterator ei = spanning_tree.begin(); + ei != spanning_tree.end(); ++ei) { + std::cout << source(*ei, g) << " <--> " << target(*ei, g) + << " with weight of " << weight[*ei] + << std::endl; + } + + std::ofstream fout("figs/kruskal-eg.dot"); + fout << "graph A {\n" + << " rankdir=LR\n" + << " size=\"3,3\"\n" + << " ratio=\"filled\"\n" + << " edge[style=\"bold\"]\n" << " node[shape=\"circle\"]\n"; + graph_traits::edge_iterator eiter, eiter_end; + for (tie(eiter, eiter_end) = edges(g); eiter != eiter_end; ++eiter) { + fout << source(*eiter, g) << " -- " << target(*eiter, g); + if (std::find(spanning_tree.begin(), spanning_tree.end(), *eiter) + != spanning_tree.end()) + fout << "[color=\"black\", label=\"" << get(edge_weight, g, *eiter) + << "\"];\n"; + else + fout << "[color=\"gray\", label=\"" << get(edge_weight, g, *eiter) + << "\"];\n"; + } + fout << "}\n"; + return EXIT_SUCCESS; +} \ No newline at end of file From ab09c65b0ac1a548fd55c8291fbd5d06016b7495 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Thu, 19 Jul 2018 16:38:02 -0400 Subject: [PATCH 022/122] A simple MST boost test! (also removed realParent from MSF) --- src/MSF.cc | 35 +--------- src/MST_boost.cc | 162 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 114 insertions(+), 83 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index 2e9e7b2..14fc625 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -30,11 +30,8 @@ struct DisjointSetNode { void setParent(uint64_t p) { parent = p; } - void setRealParent(uint64_t p) { realParent = p; } - void mergeWith(DisjointSetNode &n, uint16_t edgeW, uint64_t id) { n.setParent(parent); - n.setRealParent(id); w += (n.w + static_cast(edgeW)); edges += (n.edges + 1); n.edges = 0; @@ -60,7 +57,6 @@ struct DisjointSets { for (uint64_t i = 0; i <= n; i++) { //every element is parent of itself els[i].setParent(i); - els[i].setRealParent(i); } } @@ -328,36 +324,7 @@ int main(int argc, char *argv[]) { } if (selected == mode::build) { - std::vector> bvs; - loadEqs(opt.eqClsListFile, bvs); - std::vector eq1(((opt.numSamples - 1) / 64) + 1), - eq2(((opt.numSamples - 1) / 64) + 1); - sdsl::int_vector<> parents(opt.numNodes, 0, (uint64_t) ceil(log2(opt.numNodes))); - uint64_t totalDeltas{0}, rootDeltas{0}, rootCnt{0}; - for (auto i = 0; i < opt.numNodes; i++) { - if (ds.els[i].realParent == i) { - parents[i] = 0; - buildColor(bvs, eq1, i+1, opt.numSamples); - for (auto& wrd : eq1) { - rootDeltas += sdsl::bits::cnt(wrd); - } - rootCnt ++; - } - else - parents[i] = ds.els[i].realParent; - totalDeltas += ds.els[i].w; - } - /*buildColor(eq1, eqid, opt.numSamples); - for (uint64_t i = 0; i < eq1.size(); i++) { - uint64_t bitcnt = std::min(this->num_samples - (i * 64), (uint64_t) 64); - dist.set_int((i * 64), (eq1[i] ^ eq2[i]), bitcnt); - //std::cerr << i << " " << eq1[i] << " " << eq2[i] << " " << (eq1[i] ^ eq2[i]) << "\n"; - }*/ - sdsl::store_to_file(parents, "parent.tst"); - std::cerr << "parent size: " << sdsl::size_in_mega_bytes(parents) << "MB\n" - << "delta size: " << totalDeltas * log2(opt.numSamples) / 8 << "B\n" - << "bbv size: " << totalDeltas/8 << "B\n" - << "root delta count: " << rootCnt << " " << rootDeltas << "\n"; + } return 0; diff --git a/src/MST_boost.cc b/src/MST_boost.cc index ee019e7..5cff8b2 100644 --- a/src/MST_boost.cc +++ b/src/MST_boost.cc @@ -2,66 +2,130 @@ // Created by Fatemeh Almodaresi on 7/19/18. // + #include #include +#include #include #include +#include "clipp.h" + +struct Opts { + std::string filename; + uint64_t numNodes; // = std::stoull(argv[2]); + uint64_t bucketCnt; // = std::stoull(argv[3]); + uint64_t numSamples; + std::string eqClsListFile; +}; int main(int argc, char *argv[]) { + + /* Let us create above shown weighted + and undirected graph */ + using namespace clipp; + enum class mode { + build, ccInfo, help + }; + mode selected = mode::help; + Opts opt; + auto ccInfo_mode = ( + command("ccInfo").set(selected, mode::ccInfo), + required("-e", "--edge-filename") & + value("edge_filename", opt.filename) % "file containing list of eq. class edges.", + required("-n", "--eqCls-cnt") & + value("equivalenceClass_count", opt.numNodes) % "Total number of equivalence (color) classes.", + required("-b", "--bucket-cnt") & + value("bucket_count", opt.bucketCnt) % "Total number of valid distances." + ); + + auto build_mode = ( + command("build").set(selected, mode::build), + required("-e", "--edge-filename") & + value("edge_filename", opt.filename) % "File containing list of eq. class edges.", + required("-n", "--eqCls-cnt") & + value("equivalenceClass_count", opt.numNodes) % "Total number of equivalence (color) classes.", + required("-b", "--bucket-cnt") & + value("bucket_count", opt.bucketCnt) % "Total number of valid distances.", + required("-s", "--numSamples") & + value("numSamples", opt.numSamples) % "Total number of experiments (samples).", + required("-c", "--eqCls-lst") & + value("eqCls_list", opt.eqClsListFile) % "File containing list of equivalence (color) classes." + ); + + auto cli = ( + (build_mode | ccInfo_mode | command("help").set(selected, mode::help) + ) + ); + + decltype(parse(argc, argv, cli)) res; + try { + res = parse(argc, argv, cli); + } catch (std::exception &e) { + std::cout << "\n\nParsing command line failed with exception: " << e.what() << "\n"; + std::cout << "\n\n"; + std::cout << make_man_page(cli, "MSF"); + return 1; + } + + //explore_options_verbose(res); + + if (res) { + switch (selected) { + //case mode::ccInfo: query_main(qopt); break; + //case mode::build: validate_main(vopt); break; + case mode::help: + std::cerr << make_man_page(cli, "MSF"); + break; + } + } + using namespace boost; - typedef adjacency_list > Graph; + typedef adjacency_list > Graph; typedef graph_traits::edge_descriptor Edge; typedef graph_traits::vertex_descriptor Vertex; - typedef std::pair E; + typedef std::pair E; - const int num_nodes = 5; - E edge_array[] = {E(0, 2), E(1, 3), E(1, 4), E(2, 1), E(2, 3), - E(3, 4), E(4, 0), E(4, 1) - }; - int weights[] = {1, 1, 2, 7, 3, 1, 1, 1}; - std::size_t num_edges = sizeof(edge_array) / sizeof(E); -#if defined(BOOST_MSVC) && BOOST_MSVC <= 1300 - Graph g(num_nodes); - property_map::type weightmap = get(edge_weight, g); - for (std::size_t j = 0; j < num_edges; ++j) { - Edge e; bool inserted; - tie(e, inserted) = add_edge(edge_array[j].first, edge_array[j].second, g); - weightmap[e] = weights[j]; - } -#else - Graph g(edge_array, edge_array + num_edges, weights, num_nodes); -#endif - property_map::type weight = get(edge_weight, g); - std::vector spanning_tree; - - kruskal_minimum_spanning_tree(g, std::back_inserter(spanning_tree)); - - std::cout << "Print the edges in the MST:" << std::endl; - for (std::vector::iterator ei = spanning_tree.begin(); - ei != spanning_tree.end(); ++ei) { - std::cout << source(*ei, g) << " <--> " << target(*ei, g) - << " with weight of " << weight[*ei] - << std::endl; - } + if (selected == mode::build) { + + Graph g(opt.numNodes+1); + property_map::type weightmap = get(edge_weight, g); + std::ifstream file(opt.filename); + uint64_t n1,n2, zero{opt.numNodes+2}, edgeCntr{0}; + uint32_t w; + for (auto i : irange((uint64_t)0, opt.numNodes)) { + add_edge(0, i, opt.numSamples, g); + edgeCntr++; + if (edgeCntr % 1000000 == 0) { + + } + } + std::cerr << edgeCntr << " zero-end edges\n"; + edgeCntr = 0; + while (file.good()) { + file >> n1 >> n2 >> w; + /*Edge e; + bool inserted;*/ + /*tie(e, inserted) = */add_edge(n1, n2, w, g); + //weightmap[e] = w; + edgeCntr++; + if (edgeCntr % 10000000 == 0) { + std::cerr << edgeCntr << "\n"; + } + } + std::cerr << edgeCntr << " edges\n"; + + std::vector spanning_tree; + + kruskal_minimum_spanning_tree(g, std::back_inserter(spanning_tree)); - std::ofstream fout("figs/kruskal-eg.dot"); - fout << "graph A {\n" - << " rankdir=LR\n" - << " size=\"3,3\"\n" - << " ratio=\"filled\"\n" - << " edge[style=\"bold\"]\n" << " node[shape=\"circle\"]\n"; - graph_traits::edge_iterator eiter, eiter_end; - for (tie(eiter, eiter_end) = edges(g); eiter != eiter_end; ++eiter) { - fout << source(*eiter, g) << " -- " << target(*eiter, g); - if (std::find(spanning_tree.begin(), spanning_tree.end(), *eiter) - != spanning_tree.end()) - fout << "[color=\"black\", label=\"" << get(edge_weight, g, *eiter) - << "\"];\n"; - else - fout << "[color=\"gray\", label=\"" << get(edge_weight, g, *eiter) - << "\"];\n"; + std::cerr << "Done building MST\nTotal # of edges: " << spanning_tree.size() << "\n"; + /*std::cout << "Print the edges in the MST:" << std::endl; + for (std::vector::iterator ei = spanning_tree.begin(); + ei != spanning_tree.end(); ++ei) { + std::cout << source(*ei, g) << " <--> " << target(*ei, g) + << " with weight of " << weight[*ei] + << std::endl; + }*/ } - fout << "}\n"; return EXIT_SUCCESS; } \ No newline at end of file From a8f325e65e0ca8e031ce1bc65eb4ca1f7d090e14 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Thu, 19 Jul 2018 19:11:35 -0400 Subject: [PATCH 023/122] A very important commit containing the whole algorithm to find the root of the mst (including the dummy node) + outputting stats about it. --- src/MSF.cc | 178 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 151 insertions(+), 27 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index 14fc625..882665e 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -8,6 +8,7 @@ #include #include #include +#include #include "clipp.h" #include "bitvector.h" //#include "sdsl/bits.hpp" @@ -17,20 +18,45 @@ using namespace std; struct Edge { - uint64_t n1; - uint64_t n2; - uint32_t weight; + uint32_t n1; + uint32_t n2; + uint16_t weight; - Edge(uint64_t inN1, uint64_t inN2, uint32_t inWeight) + Edge(uint32_t inN1, uint32_t inN2, uint16_t inWeight) : n1(inN1), n2(inN2), weight(inWeight) {} }; +struct EdgePtr { + uint16_t bucket; + uint32_t idx; + + EdgePtr(uint16_t bucketIn, uint32_t idxIn) : bucket(bucketIn), idx(idxIn) {} +}; + +struct Child { + uint32_t id; + uint16_t weight; + + Child(uint32_t inN1, uint16_t inWeight) : id(inN1), weight(inWeight) {} +}; + +struct Path { + uint32_t id; + uint32_t steps; + uint64_t weight; + + Path(uint32_t idIn, + uint32_t stepsIn, + uint64_t weightIn) : id(idIn), steps(stepsIn), weight(weightIn) {} +}; + struct DisjointSetNode { - uint64_t parent{0}, realParent{0}, rnk{0}, w{0}, edges{0}; + uint32_t parent{0}; + uint64_t rnk{0}, w{0}, edges{0}; - void setParent(uint64_t p) { parent = p; } + void setParent(uint32_t p) { parent = p; } - void mergeWith(DisjointSetNode &n, uint16_t edgeW, uint64_t id) { + void mergeWith(DisjointSetNode &n, uint16_t edgeW, uint32_t id) { n.setParent(parent); w += (n.w + static_cast(edgeW)); edges += (n.edges + 1); @@ -62,7 +88,7 @@ struct DisjointSets { // Find the parent of a node 'u' // Path Compression - int find(uint64_t u) { + int find(uint32_t u) { /* Make the parent of the nodes in the path from u--> parent[u] point to parent[u] */ if (u != els[u].parent) @@ -71,7 +97,7 @@ struct DisjointSets { } // Union by rank - void merge(uint64_t x, uint64_t y, uint16_t edgeW) { + void merge(uint32_t x, uint32_t y, uint16_t edgeW) { x = find(x), y = find(y); /* Make tree with smaller height @@ -90,18 +116,19 @@ struct Graph { uint64_t V, E; //vector edges; vector> edges; + vector> mst; Graph(uint64_t bucketCnt) { edges.resize(bucketCnt); } // Utility function to add an edge - void addEdge(uint64_t u, uint64_t v, uint16_t w) { + void addEdge(uint32_t u, uint32_t v, uint16_t w) { edges[w - 1].emplace_back(u, v, w); //edges.emplace_back(u, v, w); } // Function to find MST using Kruskal's // MST algorithm - DisjointSets kruskalMSF(uint64_t bucketCnt, ifstream &file) { + DisjointSets kruskalMSF(uint32_t bucketCnt/*, ifstream &file*/) { int mst_wt = 0; // Initialize result // Sort edges in increasing order on basis of cost @@ -122,13 +149,14 @@ struct Graph { /*std::getline(file, tmp); while (file.good()) { file >> n1 >> n2 >> w;*/ + uint32_t edgeIdxInBucket = 0; for (auto it = edges[bucketCntr].begin(); it != edges[bucketCntr].end(); it++) { //if (w == bucketCntr) { w = it->weight; - uint64_t u = it->n1; - uint64_t v = it->n2; - uint64_t set_u = ds.find(u); - uint64_t set_v = ds.find(v); + uint32_t u = it->n1; + uint32_t v = it->n2; + uint32_t set_u = ds.find(u); + uint32_t set_v = ds.find(v); // Check if the selected edge is creating // a cycle or not (Cycle is created if u @@ -137,6 +165,8 @@ struct Graph { // Current edge will be in the MST // Merge two sets ds.merge(set_u, set_v, w); + mst[u].emplace_back(bucketCntr, edgeIdxInBucket); + mst[v].emplace_back(bucketCntr, edgeIdxInBucket); nodes[u] = 1; nodes[v] = 1; mergeCntr++; @@ -148,6 +178,7 @@ struct Graph { if (cntr % 1000000 == 0) { std::cerr << "edge " << cntr << " " << mergeCntr << "\n"; } + edgeIdxInBucket++; //} } /*file.clear(); @@ -176,7 +207,7 @@ struct Opts { std::string eqClsListFile; }; -void loadEqs(std::string filename, std::vector> &bvs) { +void loadEqs(std::string filename, std::vector> &bvs) { bvs.reserve(20); std::string eqfile; std::ifstream eqlist(filename); @@ -189,19 +220,19 @@ void loadEqs(std::string filename, std::vector> &bvs) { } } //BitVectorRRR bv(eqfile); - std::cerr << "loaded all the equivalence classes: " << ((bvs.size() - 1) * EQS_PER_SLOT + bvs.back().size()) + std::cerr << "loaded all the equivalence classes: " + << ((bvs.size() - 1) * EQS_PER_SLOT + bvs.back().size()) << "\n"; } -void buildColor(std::vector> &bvs, - std::vector &eq, - uint64_t eqid, - uint64_t num_samples) { +void buildColor(std::vector>&bvs, std::vector &eq, + uint64_t eqid, + uint64_t num_samples) { uint64_t i{0}, bitcnt{0}, wrdcnt{0}; uint64_t idx = eqid / EQS_PER_SLOT; uint64_t offset = eqid % EQS_PER_SLOT; -//std::cerr << eqid << " " << num_samples << " " << idx << " " << offset << "\n"; - while (i < num_samples) { + //std::cerr << eqid << " " << num_samples << " " << idx << " " << offset << "\n"; + while (i nodes; sdsl::bit_vector nodes(opt.numNodes, 0); std::getline(file, tmp); + for (uint64_t i = 0; i < opt.numNodes; i++) { + g.addEdge(zero, i, opt.numSamples); + } while (file.good()) { file >> n1 >> n2 >> w_; g.addEdge(n1, n2, w_); @@ -309,11 +345,12 @@ int main(int argc, char *argv[]) { // nodes.clear(); } g.V = opt.numNodes; + g.mst.resize(opt.numNodes); g.E = edgeCntr; //g.V = numNodes; //ifstream file(filename); - DisjointSets ds = g.kruskalMSF(opt.bucketCnt, file); + DisjointSets ds = g.kruskalMSF(opt.bucketCnt/*, file*/); if (selected == mode::ccInfo) { for (auto &el : ds.els) { @@ -324,6 +361,93 @@ int main(int argc, char *argv[]) { } if (selected == mode::build) { + std::queue q; + vector> p2c(opt.numNodes); + vector c2p(opt.numNodes); + + for (auto e = 0; e < g.mst.size(); e++) { + // put all the mst leaves into the queue + if (g.mst[e].size() == 1) { + q.push(e); // e is a leaf + } + } + // now go run the algorithm to find the root of the tree and all the edge directions + uint32_t root; + bool check = false; + uint64_t nodeCntr{0}; + while (!q.empty()) { + uint32_t node = q.front(); + q.pop(); + if (g.mst[node].size() == 0) { + // this node is the root + // and this should be the end of the loop + // just to confirm this, I'll continue + if (check) { + std::cerr << "A TERRIBLE BUG!!!\n" + << "finding a node with no edges should only happen once at the root\n"; + std::exit(1); + } + check = true; + root = node; + continue; + } + auto buck = g.mst[node][0].bucket; + auto idx = g.mst[node][0].idx; + uint64_t dest = g.edges[buck][idx].n1; + uint64_t src = g.edges[buck][idx].n2; + if (src == node) { // swap src & dest since src is the leaf + std::swap(src, dest); + } + p2c[src].emplace_back(dest, g.edges[buck][idx].weight); + g.mst[dest].erase(std::remove_if( + g.mst[dest].begin(), g.mst[dest].end(), + [buck, idx](const EdgePtr &x) { + return x.bucket == buck && x.idx == idx; + }), g.mst[dest].end()); + g.mst[src].erase(std::remove_if( + g.mst[src].begin(), g.mst[src].end(), + [buck, idx](const EdgePtr &x) { + return x.bucket == buck && x.idx == idx; + }), g.mst[src].end()); + if (g.mst[src].size() == 1) { + q.push(src); + } + nodeCntr++; + if (nodeCntr % 10000000 == 0) { + std::cerr << nodeCntr << " nodes processed toward root\n"; + } + } + + // calculate all the stats!! + // create the data structures + std::cerr << "Calculate Stats .. \n"; + nodeCntr = 0; + std::queue pq; + pq.push(Path(root, 0, 0)); + double avgDegree{0}; + uint64_t internalNodeCnt{0}; + std::cout << "-1\t" << root << "\t" << 0 << "\t" << 0 << "\n"; + while (!pq.empty()) { + Path p = pq.front(); + pq.pop(); + avgDegree += p2c[p.id].size(); + if (p2c[p.id].size() != 0) { + internalNodeCnt++; + } + for (auto &c : p2c[p.id]) { + Path cp(c.id, p.steps + 1, p.weight + c.weight); + std::cout << p.id << "\t" << cp.id << "\t" << cp.steps << "\t" << cp.weight << "\n"; + pq.push(cp); + } + nodeCntr++; + if (nodeCntr % 10000000 == 0) { + std::cerr << nodeCntr << " nodes processed from root\n"; + } + } + + std::cerr << "internal node count: " << internalNodeCnt << "\n" + << "total out degree: " << avgDegree << "\n" + << "average degree: " << avgDegree/internalNodeCnt << "\t" << avgDegree/opt.numNodes << "\n"; } From d2071526db93347c1a4c7f103a89345feac61b2c Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Thu, 19 Jul 2018 19:26:03 -0400 Subject: [PATCH 024/122] output edge weight --- src/CMakeLists.txt | 15 ++++++++------- src/MSF.cc | 11 +++++++---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d89e2ea..02e9adb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -79,10 +79,11 @@ add_executable(mstBoost MST_boost.cc) target_include_directories(mstBoost PUBLIC $) target_link_libraries(mstBoost mantis_core) -#link_directories($) -add_executable(walkEqcls walkEqcls.cc) -target_include_directories(walkEqcls PUBLIC - $ $) -find_library(compression_library NAMES SIMDCompressionAndIntersection HINTS "${CMAKE_SOURCE_DIR}/lib") -target_link_libraries(walkEqcls - mantis_core ${compression_library}) + + +#add_executable(walkEqcls walkEqcls.cc) +#target_include_directories(walkEqcls PUBLIC +# $ $) +#find_library(compression_library NAMES SIMDCompressionAndIntersection HINTS "${CMAKE_SOURCE_DIR}/lib") +#target_link_libraries(walkEqcls +# mantis_core ${compression_library}) diff --git a/src/MSF.cc b/src/MSF.cc index 882665e..7533793 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -426,7 +426,7 @@ int main(int argc, char *argv[]) { pq.push(Path(root, 0, 0)); double avgDegree{0}; uint64_t internalNodeCnt{0}; - std::cout << "-1\t" << root << "\t" << 0 << "\t" << 0 << "\n"; + std::cout << "-1\t" << root << "\t" << 0 << "\t" << 0 << "\t" << 0 << "\n"; while (!pq.empty()) { Path p = pq.front(); pq.pop(); @@ -436,7 +436,11 @@ int main(int argc, char *argv[]) { } for (auto &c : p2c[p.id]) { Path cp(c.id, p.steps + 1, p.weight + c.weight); - std::cout << p.id << "\t" << cp.id << "\t" << cp.steps << "\t" << cp.weight << "\n"; + std::cout << p.id << "\t" + << cp.id << "\t" + << cp.steps << "\t" + << c.weight << "\t" + << cp.weight << "\n"; pq.push(cp); } nodeCntr++; @@ -451,6 +455,5 @@ int main(int argc, char *argv[]) { } - return 0; -} + return 0;} From 1268e5cfd5919ffbcc5d95e26303458b594e895e Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Thu, 19 Jul 2018 19:37:08 -0400 Subject: [PATCH 025/122] output edge weight --- src/MSF.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MSF.cc b/src/MSF.cc index 7533793..371614f 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -455,5 +455,6 @@ int main(int argc, char *argv[]) { } - return 0;} + return 0; +} From 0b056608c49d61d62b5fa1b63b79aaf25bc5c830 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 20 Jul 2018 00:39:07 -0400 Subject: [PATCH 026/122] Added number of set bits as weights of 0-end edges while building the graph (before building MST) --- src/MSF.cc | 375 ++++++++++++++++++++++++++++------------------------- 1 file changed, 199 insertions(+), 176 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index 371614f..cffda3f 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -77,7 +77,7 @@ struct DisjointSets { DisjointSets(uint64_t n) { // Allocate memory this->n = n; - els.resize(n + 1); + els.resize(n); // Initially, all vertices are in // different sets and have rank 0. for (uint64_t i = 0; i <= n; i++) { @@ -88,7 +88,7 @@ struct DisjointSets { // Find the parent of a node 'u' // Path Compression - int find(uint32_t u) { + uint32_t find(uint32_t u) { /* Make the parent of the nodes in the path from u--> parent[u] point to parent[u] */ if (u != els[u].parent) @@ -113,11 +113,14 @@ struct DisjointSets { // Structure to represent a graph struct Graph { - uint64_t V, E; - //vector edges; + + uint64_t V; + vector> edges; vector> mst; + uint64_t mst_totalWeight{0}; + Graph(uint64_t bucketCnt) { edges.resize(bucketCnt); } // Utility function to add an edge @@ -128,14 +131,9 @@ struct Graph { // Function to find MST using Kruskal's // MST algorithm - DisjointSets kruskalMSF(uint32_t bucketCnt/*, ifstream &file*/) { + DisjointSets kruskalMSF(uint32_t bucketCnt) { int mst_wt = 0; // Initialize result - // Sort edges in increasing order on basis of cost - - /*sort(edges.begin(), edges.end(), - [](const Edge &e1, const Edge &e2) { return e1.weight < e2.weight; }); -*/ // Create disjoint sets DisjointSets ds(V); @@ -169,6 +167,7 @@ struct Graph { mst[v].emplace_back(bucketCntr, edgeIdxInBucket); nodes[u] = 1; nodes[v] = 1; + mst_totalWeight += w; mergeCntr++; }/* else { if (nodes.find(u) == nodes.end() || nodes.find(v) == nodes.end()) @@ -202,65 +201,95 @@ struct Graph { struct Opts { std::string filename; uint64_t numNodes; // = std::stoull(argv[2]); - uint64_t bucketCnt; // = std::stoull(argv[3]); - uint64_t numSamples; + uint32_t bucketCnt; // = std::stoull(argv[3]); + uint16_t numSamples; std::string eqClsListFile; }; -void loadEqs(std::string filename, std::vector> &bvs) { - bvs.reserve(20); - std::string eqfile; - std::ifstream eqlist(filename); - if (eqlist.is_open()) { - uint64_t accumTotalEqCls = 0; - while (getline(eqlist, eqfile)) { - sdsl::rrr_vector<63> bv; - bvs.push_back(bv); - sdsl::load_from_file(bvs.back(), eqfile); - } - } - //BitVectorRRR bv(eqfile); - std::cerr << "loaded all the equivalence classes: " - << ((bvs.size() - 1) * EQS_PER_SLOT + bvs.back().size()) - << "\n"; +void loadEqs(std::string filename, std::vector> + +&bvs) { +bvs.reserve(20); +std::string eqfile; +std::ifstream eqlist(filename); +if (eqlist. + +is_open() + +) { +uint64_t accumTotalEqCls = 0; +while ( +getline(eqlist, eqfile +)) { +sdsl::rrr_vector<63> bv; +bvs. +push_back(bv); +sdsl::load_from_file(bvs +. + +back(), eqfile + +); +} +} +//BitVectorRRR bv(eqfile); +std::cerr << "loaded all the equivalence classes: " +<< ((bvs. + +size() + +- 1) * EQS_PER_SLOT + bvs. + +back() + +. + +size() + +) +<< "\n"; } -void buildColor(std::vector>&bvs, std::vector &eq, +void buildColor(std::vector> &bvs, +std::vector &eq, uint64_t eqid, +uint64_t num_samples +) { +uint64_t i{0}, bitcnt{0}, wrdcnt{0}; +uint64_t idx = eqid / EQS_PER_SLOT; +uint64_t offset = eqid % EQS_PER_SLOT; +//std::cerr << eqid << " " << num_samples << " " << idx << " " << offset << "\n"; +while (i> &bvs, uint64_t eqid, uint64_t num_samples) { - uint64_t i{0}, bitcnt{0}, wrdcnt{0}; - uint64_t idx = eqid / EQS_PER_SLOT; - uint64_t offset = eqid % EQS_PER_SLOT; - //std::cerr << eqid << " " << num_samples << " " << idx << " " << offset << "\n"; - while (i eq; + eq.resize((uint64_t)std::ceil((double)num_samples/64.0)); + buildColor(bvs, eq, eqid, num_samples); + for (uint64_t i = 0; i < eq.size(); i += 1) { + res += (uint16_t)sdsl::bits::cnt(eq[i]); } + return res; } - int main(int argc, char *argv[]) { /* Let us create above shown weighted and undirected graph */ using namespace clipp; enum class mode { - build, ccInfo, help + build, help }; mode selected = mode::help; Opts opt; - auto ccInfo_mode = ( - command("ccInfo").set(selected, mode::ccInfo), - required("-e", "--edge-filename") & - value("edge_filename", opt.filename) % "file containing list of eq. class edges.", - required("-n", "--eqCls-cnt") & - value("equivalenceClass_count", opt.numNodes) % "Total number of equivalence (color) classes.", - required("-b", "--bucket-cnt") & - value("bucket_count", opt.bucketCnt) % "Total number of valid distances.", - required("-s", "--numSamples") & - value("numSamples", opt.numSamples) % "Total number of experiments (samples)." - ); auto build_mode = ( command("build").set(selected, mode::build), @@ -277,7 +306,7 @@ int main(int argc, char *argv[]) { ); auto cli = ( - (build_mode | ccInfo_mode | command("help").set(selected, mode::help) + (build_mode | command("help").set(selected, mode::help) ) ); @@ -290,170 +319,164 @@ int main(int argc, char *argv[]) { std::cout << make_man_page(cli, "MSF"); return 1; } - - //explore_options_verbose(res); - - if (res) { - switch (selected) { - //case mode::ccInfo: query_main(qopt); break; - //case mode::build: validate_main(vopt); break; - case mode::help: - std::cerr << make_man_page(cli, "MSF"); - break; - } + if (!res) { + std::cerr << "Cannot parse the input arguments\n"; + std::exit(1); } + if (selected == mode::help) { + std::cerr << make_man_page(cli, "MSF"); + std::exit(1); + } + //explore_options_verbose(res); std::cerr << "here are the inputs: \n" << opt.filename << "\n" << opt.numNodes << "\n" << opt.bucketCnt << "\n"; opt.numNodes++; // number of nodes is one more than input including the zero node - ifstream file(opt.filename); + std::cerr << "Loading all the equivalence classes first .. \n"; + std::vector> eqs; + eqs.reserve(20); + loadEqs(opt.eqClsListFile, eqs); + std::cerr << "Done loading list of equivalence class buckets\n"; + + ifstream file(opt.filename); Graph g(opt.bucketCnt); - uint32_t w_; + uint16_t w_; uint32_t n1, n2, edgeCntr{0}, zero{(uint32_t) opt.numNodes - 1}; - std::string tmp; { - //unordered_set nodes; - sdsl::bit_vector nodes(opt.numNodes, 0); - std::getline(file, tmp); - for (uint64_t i = 0; i < opt.numNodes; i++) { - g.addEdge(zero, i, opt.numSamples); + std::cerr << "Adding edges from 0 to each node with number of set bits in the node as weight .. \n"; + for (uint32_t i = 0; i < opt.numNodes; i++) { + uint16_t ones = sum1s(eqs, i, opt.numSamples); + g.addEdge(zero, i, ones); } + std::cerr << "Done adding 0-end edges.\n"; + std::cerr << "Adding edges between color classes .. \n"; while (file.good()) { file >> n1 >> n2 >> w_; g.addEdge(n1, n2, w_); - nodes[n1] = 1; - nodes[n2] = 1; edgeCntr++; - //nodes.insert(n1); - //nodes.insert(n2); } //file.clear(); //file.seekg(0, ios::beg); file.close(); + std::cerr << "Done adding edges between color classes .. \n"; - uint64_t distinctNodes{0}; - for (uint64_t i = 0; i < nodes.size(); i += 64) { - distinctNodes += sdsl::bits::cnt(nodes.get_int(i, 64)); - } - std::cerr << "# of nodes: " << distinctNodes//nodes.size() - << "\n# of edges: " << edgeCntr + std::cerr << "\n# of edges: " << edgeCntr << "\n"; // nodes.clear(); } g.V = opt.numNodes; g.mst.resize(opt.numNodes); - g.E = edgeCntr; //g.V = numNodes; //ifstream file(filename); - DisjointSets ds = g.kruskalMSF(opt.bucketCnt/*, file*/); + DisjointSets ds = g.kruskalMSF(opt.bucketCnt); + std::queue q; + vector> p2c(opt.numNodes); + vector c2p(opt.numNodes); - if (selected == mode::ccInfo) { - for (auto &el : ds.els) { - if (el.w != 0) { - std::cout << el.edges << "\t" << el.w << "\t" << el.rnk << "\n"; - } + for (auto e = 0; e < g.mst.size(); e++) { + // put all the mst leaves into the queue + if (g.mst[e].size() == 1) { + q.push(e); // e is a leaf } } - - if (selected == mode::build) { - std::queue q; - vector> p2c(opt.numNodes); - vector c2p(opt.numNodes); - - for (auto e = 0; e < g.mst.size(); e++) { - // put all the mst leaves into the queue - if (g.mst[e].size() == 1) { - q.push(e); // e is a leaf + // now go run the algorithm to find the root of the tree and all the edge directions + uint32_t root; + bool check = false; + uint64_t nodeCntr{0}; + while (!q.empty()) { + uint32_t node = q.front(); + q.pop(); + if (g.mst[node].size() == 0) { + // this node is the root + // and this should be the end of the loop + // just as a validation, I'll continue and expect the while to end here + if (check) { + std::cerr << "A TERRIBLE BUG!!!\n" + << "finding a node with no edges should only happen once at the root\n"; + std::exit(1); } + check = true; + root = node; + continue; } - // now go run the algorithm to find the root of the tree and all the edge directions - uint32_t root; - bool check = false; - uint64_t nodeCntr{0}; - while (!q.empty()) { - uint32_t node = q.front(); - q.pop(); - if (g.mst[node].size() == 0) { - // this node is the root - // and this should be the end of the loop - // just to confirm this, I'll continue - if (check) { - std::cerr << "A TERRIBLE BUG!!!\n" - << "finding a node with no edges should only happen once at the root\n"; - std::exit(1); - } - check = true; - root = node; - continue; - } - auto buck = g.mst[node][0].bucket; - auto idx = g.mst[node][0].idx; - uint64_t dest = g.edges[buck][idx].n1; - uint64_t src = g.edges[buck][idx].n2; - if (src == node) { // swap src & dest since src is the leaf - std::swap(src, dest); - } - p2c[src].emplace_back(dest, g.edges[buck][idx].weight); - g.mst[dest].erase(std::remove_if( - g.mst[dest].begin(), g.mst[dest].end(), - [buck, idx](const EdgePtr &x) { - return x.bucket == buck && x.idx == idx; - }), g.mst[dest].end()); - g.mst[src].erase(std::remove_if( - g.mst[src].begin(), g.mst[src].end(), - [buck, idx](const EdgePtr &x) { - return x.bucket == buck && x.idx == idx; - }), g.mst[src].end()); - if (g.mst[src].size() == 1) { - q.push(src); - } - nodeCntr++; - if (nodeCntr % 10000000 == 0) { - std::cerr << nodeCntr << " nodes processed toward root\n"; - } + // what ever is in q (node) is a leaf and has only one edge left + // fetch the pointer to that edge (bucket+idx) + auto buck = g.mst[node][0].bucket; + auto idx = g.mst[node][0].idx; + // fetch the two ends of the edge and based on the node id decide which one is the src/dest + // Destination is the one that represents current node which has been selected as leaf sooner than the other end + uint64_t dest = g.edges[buck][idx].n1; + uint64_t src = g.edges[buck][idx].n2; + if (src == node) { // swap src & dest since src is the leaf + std::swap(src, dest); + } + p2c[src].emplace_back(dest, g.edges[buck][idx].weight); + // erase the edge from src list of edges + g.mst[dest].erase(std::remove_if( + g.mst[dest].begin(), g.mst[dest].end(), + [buck, idx](const EdgePtr &x) { + return x.bucket == buck && x.idx == idx; + }), g.mst[dest].end()); + // erase the edge from dest list of edges + g.mst[src].erase(std::remove_if( + g.mst[src].begin(), g.mst[src].end(), + [buck, idx](const EdgePtr &x) { + return x.bucket == buck && x.idx == idx; + }), g.mst[src].end()); + // the destination has no edges left + // but if the src has turned to a leaf (node with one edge) after removal of this last edge + // add it to the queue + if (g.mst[src].size() == 1) { + q.push(src); } + nodeCntr++; // just a counter for the log + if (nodeCntr % 10000000 == 0) { + std::cerr << nodeCntr << " nodes processed toward root\n"; + } + } - // calculate all the stats!! - // create the data structures - std::cerr << "Calculate Stats .. \n"; - nodeCntr = 0; - std::queue pq; - pq.push(Path(root, 0, 0)); - double avgDegree{0}; - uint64_t internalNodeCnt{0}; - std::cout << "-1\t" << root << "\t" << 0 << "\t" << 0 << "\t" << 0 << "\n"; - while (!pq.empty()) { - Path p = pq.front(); - pq.pop(); - avgDegree += p2c[p.id].size(); - if (p2c[p.id].size() != 0) { - internalNodeCnt++; - } - for (auto &c : p2c[p.id]) { - Path cp(c.id, p.steps + 1, p.weight + c.weight); - std::cout << p.id << "\t" - << cp.id << "\t" - << cp.steps << "\t" - << c.weight << "\t" - << cp.weight << "\n"; - pq.push(cp); - } - nodeCntr++; - if (nodeCntr % 10000000 == 0) { - std::cerr << nodeCntr << " nodes processed from root\n"; - } + // calculate all the stats!! + // create the data structures + std::cerr << "Calculate Stats .. \n"; + std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n"; + nodeCntr = 0; + std::queue pq; + pq.push(Path(root, 0, 0)); + double avgDegree{0}; + uint64_t internalNodeCnt{0}; + std::cout << "-1\t" << root << "\t" << 0 << "\t" << 0 << "\t" << 0 << "\n"; + while (!pq.empty()) { + Path p = pq.front(); + pq.pop(); + avgDegree += p2c[p.id].size(); + if (p2c[p.id].size() != 0) { + internalNodeCnt++; + } + for (auto &c : p2c[p.id]) { + Path cp(c.id, p.steps + 1, p.weight + c.weight); + std::cout << p.id << "\t" + << cp.id << "\t" + << cp.steps << "\t" + << c.weight << "\t" + << cp.weight << "\n"; + pq.push(cp); } + nodeCntr++; + if (nodeCntr % 10000000 == 0) { + std::cerr << nodeCntr << " nodes processed from root\n"; + } + } - std::cerr << "internal node count: " << internalNodeCnt << "\n" - << "total out degree: " << avgDegree << "\n" - << "average degree: " << avgDegree/internalNodeCnt << "\t" << avgDegree/opt.numNodes << "\n"; + std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n" + << "internal node count: " << internalNodeCnt << "\n" + << "total out degree: " << avgDegree << "\n" + << "average degree: " << avgDegree / internalNodeCnt << "\t" << avgDegree / opt.numNodes << "\n"; - } return 0; } From 345b9772c2f6718ac9cdd6569d15b8544847d91b Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 20 Jul 2018 01:16:18 -0400 Subject: [PATCH 027/122] Storing all the required data structures to retrieve color classes. --- src/MSF.cc | 75 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index cffda3f..fb7fb60 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -17,6 +17,8 @@ using namespace std; +typedef std::vector> eqvec; + struct Edge { uint32_t n1; uint32_t n2; @@ -198,17 +200,7 @@ struct Graph { } }; -struct Opts { - std::string filename; - uint64_t numNodes; // = std::stoull(argv[2]); - uint32_t bucketCnt; // = std::stoull(argv[3]); - uint16_t numSamples; - std::string eqClsListFile; -}; - -void loadEqs(std::string filename, std::vector> - -&bvs) { +void loadEqs(std::string filename, eqvec &bvs) { bvs.reserve(20); std::string eqfile; std::ifstream eqlist(filename); @@ -250,7 +242,7 @@ size() << "\n"; } -void buildColor(std::vector> &bvs, +void buildColor(eqvec &bvs, std::vector &eq, uint64_t eqid, uint64_t num_samples @@ -269,11 +261,11 @@ bitcnt; } } -uint16_t sum1s(std::vector> &bvs, uint64_t eqid, - uint64_t num_samples) { +uint16_t sum1s(eqvec &bvs, uint64_t eqid, + uint64_t num_samples, uint64_t numWrds) { uint16_t res{0}; std::vector eq; - eq.resize((uint64_t)std::ceil((double)num_samples/64.0)); + eq.resize(numWrds); buildColor(bvs, eq, eqid, num_samples); for (uint64_t i = 0; i < eq.size(); i += 1) { res += (uint16_t)sdsl::bits::cnt(eq[i]); @@ -281,6 +273,36 @@ uint16_t sum1s(std::vector> &bvs, uint64_t eqid, return res; } +std::vector getDeltaList(eqvec &bvs, + uint64_t eqid1,uint64_t eqid2, uint64_t num_samples, uint64_t numWrds) { + std::vector res; + std::vector eq1, eq2; + eq1.resize(numWrds); + eq2.resize(numWrds); + buildColor(bvs, eq1, eqid1, num_samples); + buildColor(bvs, eq2, eqid2, num_samples); + + for (uint32_t i = 0; i < eq1.size(); i += 1) { + for (uint32_t j = 0; j < 64; j++) { + if ( (eq1[i] >> j) & 0x01 ) { + res.push_back(i*64+j); + } + } + } + + return res; // rely on c++ optimization +} + +struct Opts { + std::string filename; + uint64_t numNodes; // = std::stoull(argv[2]); + uint32_t bucketCnt; // = std::stoull(argv[3]); + uint16_t numSamples; + std::string eqClsListFile; + std::string outputDir; +}; + + int main(int argc, char *argv[]) { /* Let us create above shown weighted and undirected graph */ @@ -302,7 +324,9 @@ int main(int argc, char *argv[]) { required("-s", "--numSamples") & value("numSamples", opt.numSamples) % "Total number of experiments (samples).", required("-c", "--eqCls-lst") & - value("eqCls_list", opt.eqClsListFile) % "File containing list of equivalence (color) classes." + value("eqCls_list_filename", opt.eqClsListFile) % "File containing list of equivalence (color) classes.", + required("-o", "--output_dir") & + value("output directory", opt.outputDir) % "Directory that all the int_vectors will be stored in." ); auto cli = ( @@ -333,6 +357,7 @@ int main(int argc, char *argv[]) { << opt.filename << "\n" << opt.numNodes << "\n" << opt.bucketCnt << "\n"; + uint64_t numWrds = (uint64_t)std::ceil((double)opt.numSamples/64.0); opt.numNodes++; // number of nodes is one more than input including the zero node std::cerr << "Loading all the equivalence classes first .. \n"; @@ -349,7 +374,7 @@ int main(int argc, char *argv[]) { { std::cerr << "Adding edges from 0 to each node with number of set bits in the node as weight .. \n"; for (uint32_t i = 0; i < opt.numNodes; i++) { - uint16_t ones = sum1s(eqs, i, opt.numSamples); + uint16_t ones = sum1s(eqs, i, opt.numSamples, numWrds); g.addEdge(zero, i, ones); } std::cerr << "Done adding 0-end edges.\n"; @@ -449,7 +474,13 @@ int main(int argc, char *argv[]) { pq.push(Path(root, 0, 0)); double avgDegree{0}; uint64_t internalNodeCnt{0}; + + sdsl::int_vector<> parentbv(opt.numNodes, 0, ceil(log2(opt.numNodes))); + sdsl::int_vector<> deltabv(g.mst_totalWeight, 0, ceil(log2(opt.numSamples))); + sdsl::bit_vector bbv(g.mst_totalWeight, 0); + uint64_t deltaOffset{0}; std::cout << "-1\t" << root << "\t" << 0 << "\t" << 0 << "\t" << 0 << "\n"; + parentbv[root] = root; while (!pq.empty()) { Path p = pq.front(); pq.pop(); @@ -458,12 +489,19 @@ int main(int argc, char *argv[]) { internalNodeCnt++; } for (auto &c : p2c[p.id]) { + parentbv[c.id] = p.id; Path cp(c.id, p.steps + 1, p.weight + c.weight); std::cout << p.id << "\t" << cp.id << "\t" << cp.steps << "\t" << c.weight << "\t" << cp.weight << "\n"; + std::vector deltas = getDeltaList(eqs, p.id, cp.id, opt.numSamples, numWrds); + for (auto & v : deltas) { + deltabv[deltaOffset] = v; + deltaOffset++; + } + bbv[deltaOffset-1] = 1; pq.push(cp); } nodeCntr++; @@ -477,6 +515,9 @@ int main(int argc, char *argv[]) { << "total out degree: " << avgDegree << "\n" << "average degree: " << avgDegree / internalNodeCnt << "\t" << avgDegree / opt.numNodes << "\n"; + sdsl::store_to_file(parentbv, opt.outputDir+"/parents.bv"); + sdsl::store_to_file(deltabv, opt.outputDir+"/deltas.bv"); + sdsl::store_to_file(bbv, opt.outputDir+"/boundary.bv"); return 0; } From 76047be4feaa576c557304820ef19ecb94ea222d Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 20 Jul 2018 08:51:51 -0400 Subject: [PATCH 028/122] separating different delta calculations based on if a node is connected to the zero node or not. --- src/MSF.cc | 112 ++++++++++++++++++++++++++--------------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index fb7fb60..e9c0623 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -201,64 +201,36 @@ struct Graph { }; void loadEqs(std::string filename, eqvec &bvs) { -bvs.reserve(20); -std::string eqfile; -std::ifstream eqlist(filename); -if (eqlist. - -is_open() - -) { -uint64_t accumTotalEqCls = 0; -while ( -getline(eqlist, eqfile -)) { -sdsl::rrr_vector<63> bv; -bvs. -push_back(bv); -sdsl::load_from_file(bvs -. - -back(), eqfile - -); -} -} -//BitVectorRRR bv(eqfile); -std::cerr << "loaded all the equivalence classes: " -<< ((bvs. - -size() - -- 1) * EQS_PER_SLOT + bvs. - -back() - -. - -size() - -) -<< "\n"; + bvs.reserve(20); + std::string eqfile; + std::ifstream eqlist(filename); + if (eqlist.is_open()) { + uint64_t accumTotalEqCls = 0; + while (getline(eqlist, eqfile)) { + sdsl::rrr_vector<63> bv; + bvs.push_back(bv); + sdsl::load_from_file(bvs.back(), eqfile); + } + } + std::cerr << "loaded all the equivalence classes: " + << ((bvs.size() - 1) * EQS_PER_SLOT + bvs.back().size()) + << "\n"; } void buildColor(eqvec &bvs, -std::vector &eq, - uint64_t eqid, -uint64_t num_samples -) { -uint64_t i{0}, bitcnt{0}, wrdcnt{0}; -uint64_t idx = eqid / EQS_PER_SLOT; -uint64_t offset = eqid % EQS_PER_SLOT; -//std::cerr << eqid << " " << num_samples << " " << idx << " " << offset << "\n"; -while (i &eq, + uint64_t eqid, + uint64_t num_samples) { + uint64_t i{0}, bitcnt{0}, wrdcnt{0}; + uint64_t idx = eqid / EQS_PER_SLOT; + uint64_t offset = eqid % EQS_PER_SLOT; + //std::cerr << eqid << " " << num_samples << " " << idx << " " << offset << "\n"; + while (i getDeltaList(eqvec &bvs, uint64_t eqid1,uint64_t eqid2, uint64_t num_samples, uint64_t numWrds) { std::vector res; @@ -282,6 +255,26 @@ std::vector getDeltaList(eqvec &bvs, buildColor(bvs, eq1, eqid1, num_samples); buildColor(bvs, eq2, eqid2, num_samples); + for (uint32_t i = 0; i < eq1.size(); i += 1) { + uint64_t eq12xor = eq1[i] ^ eq2[i]; + for (uint32_t j = 0; j < 64; j++) { + if ( (eq12xor >> j) & 0x01 ) { + res.push_back(i*64+j); + } + } + } + + return res; // rely on c++ optimization +} + +// for those connected to node zero, delta list is position of set bits +std::vector getDeltaList(eqvec &bvs, + uint64_t eqid1, uint64_t num_samples, uint64_t numWrds) { + std::vector res; + std::vector eq1; + eq1.resize(numWrds); + buildColor(bvs, eq1, eqid1, num_samples); + for (uint32_t i = 0; i < eq1.size(); i += 1) { for (uint32_t j = 0; j < 64; j++) { if ( (eq1[i] >> j) & 0x01 ) { @@ -496,7 +489,14 @@ int main(int argc, char *argv[]) { << cp.steps << "\t" << c.weight << "\t" << cp.weight << "\n"; - std::vector deltas = getDeltaList(eqs, p.id, cp.id, opt.numSamples, numWrds); + std::vector deltas; + if (p.id == zero) { + deltas = getDeltaList(eqs, c.id, opt.numSamples, numWrds); + } else if (c.id == zero) { + deltas = getDeltaList(eqs, p.id, opt.numSamples, numWrds); + } else { + deltas = getDeltaList(eqs, p.id, c.id, opt.numSamples, numWrds); + } for (auto & v : deltas) { deltabv[deltaOffset] = v; deltaOffset++; From f255e53dcfcecbd371a47baeaf2c321fdeec1462 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 20 Jul 2018 14:13:02 -0400 Subject: [PATCH 029/122] Store the deltas in correct order. Don't calculate/output any stats --- src/MSF.cc | 85 ++++++++++++++++++++++-------------------------------- 1 file changed, 35 insertions(+), 50 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index e9c0623..2b7ea4a 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -403,7 +403,8 @@ int main(int argc, char *argv[]) { } } // now go run the algorithm to find the root of the tree and all the edge directions - uint32_t root; + sdsl::int_vector<> parentbv(opt.numNodes, 0, ceil(log2(opt.numNodes))); + bool check = false; uint64_t nodeCntr{0}; while (!q.empty()) { @@ -418,8 +419,13 @@ int main(int argc, char *argv[]) { << "finding a node with no edges should only happen once at the root\n"; std::exit(1); } + parentbv[node] = node; check = true; - root = node; + // Update the total weight to contain the delta of root from bv of zero + uint16_t ones = 1; // If the root is zero itself + if (node != zero) // otherwise + ones = sum1s(eqs, node, opt.numSamples, numWrds); + g.mst_totalWeight += ones; continue; } // what ever is in q (node) is a leaf and has only one edge left @@ -433,7 +439,15 @@ int main(int argc, char *argv[]) { if (src == node) { // swap src & dest since src is the leaf std::swap(src, dest); } - p2c[src].emplace_back(dest, g.edges[buck][idx].weight); + if (dest == zero) { // we break the tree from node zero + parentbv[zero] = zero; + // we're breaking the tree, so should remove the edge weight from total weights + // But instead we're gonna spend one slot to store 0 as the delta of the zero node from zero node + g.mst_totalWeight = g.mst_totalWeight - g.edges[buck][idx].weight + 1; + } + else { + parentbv[dest] = src; + } // erase the edge from src list of edges g.mst[dest].erase(std::remove_if( g.mst[dest].begin(), g.mst[dest].end(), @@ -447,7 +461,7 @@ int main(int argc, char *argv[]) { return x.bucket == buck && x.idx == idx; }), g.mst[src].end()); // the destination has no edges left - // but if the src has turned to a leaf (node with one edge) after removal of this last edge + // but if the src has turned to a leaf (node with one edge) // add it to the queue if (g.mst[src].size() == 1) { q.push(src); @@ -458,62 +472,33 @@ int main(int argc, char *argv[]) { } } + sdsl::int_vector<> deltabv(g.mst_totalWeight, 0, ceil(log2(opt.numSamples))); + sdsl::bit_vector bbv(g.mst_totalWeight, 0); + // calculate all the stats!! // create the data structures std::cerr << "Calculate Stats .. \n"; std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n"; nodeCntr = 0; - std::queue pq; - pq.push(Path(root, 0, 0)); - double avgDegree{0}; - uint64_t internalNodeCnt{0}; - - sdsl::int_vector<> parentbv(opt.numNodes, 0, ceil(log2(opt.numNodes))); - sdsl::int_vector<> deltabv(g.mst_totalWeight, 0, ceil(log2(opt.numSamples))); - sdsl::bit_vector bbv(g.mst_totalWeight, 0); uint64_t deltaOffset{0}; - std::cout << "-1\t" << root << "\t" << 0 << "\t" << 0 << "\t" << 0 << "\n"; - parentbv[root] = root; - while (!pq.empty()) { - Path p = pq.front(); - pq.pop(); - avgDegree += p2c[p.id].size(); - if (p2c[p.id].size() != 0) { - internalNodeCnt++; - } - for (auto &c : p2c[p.id]) { - parentbv[c.id] = p.id; - Path cp(c.id, p.steps + 1, p.weight + c.weight); - std::cout << p.id << "\t" - << cp.id << "\t" - << cp.steps << "\t" - << c.weight << "\t" - << cp.weight << "\n"; - std::vector deltas; - if (p.id == zero) { - deltas = getDeltaList(eqs, c.id, opt.numSamples, numWrds); - } else if (c.id == zero) { - deltas = getDeltaList(eqs, p.id, opt.numSamples, numWrds); - } else { - deltas = getDeltaList(eqs, p.id, c.id, opt.numSamples, numWrds); - } - for (auto & v : deltas) { - deltabv[deltaOffset] = v; - deltaOffset++; - } - bbv[deltaOffset-1] = 1; - pq.push(cp); + for (uint64_t i = 0; i < parentbv.size(); i++) { + std::vector deltas; + if (i == zero) { + deltaOffset++; + } else if (parentbv[i] == zero) { + deltas = getDeltaList(eqs, i, opt.numSamples, numWrds); + } else { + deltas = getDeltaList(eqs, parentbv[i], i, opt.numSamples, numWrds); } - nodeCntr++; - if (nodeCntr % 10000000 == 0) { - std::cerr << nodeCntr << " nodes processed from root\n"; + for (auto & v : deltas) { + deltabv[deltaOffset] = v; + deltaOffset++; } + bbv[deltaOffset-1] = 1; } - std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n" - << "internal node count: " << internalNodeCnt << "\n" - << "total out degree: " << avgDegree << "\n" - << "average degree: " << avgDegree / internalNodeCnt << "\t" << avgDegree / opt.numNodes << "\n"; + + std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n"; sdsl::store_to_file(parentbv, opt.outputDir+"/parents.bv"); sdsl::store_to_file(deltabv, opt.outputDir+"/deltas.bv"); From 57f77855a6251436f69a885ac26672e29984e307 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 20 Jul 2018 17:47:54 -0400 Subject: [PATCH 030/122] Separating MSF into .h and .cpp --- include/MSF.h | 288 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/MSF.cc | 281 +----------------------------------------------- 2 files changed, 289 insertions(+), 280 deletions(-) create mode 100644 include/MSF.h diff --git a/include/MSF.h b/include/MSF.h new file mode 100644 index 0000000..af8366f --- /dev/null +++ b/include/MSF.h @@ -0,0 +1,288 @@ +// +// Created by Fatemeh Almodaresi on 7/20/18. +// + +#ifndef MANTIS_MSF_H +#define MANTIS_MSF_H +#include +#include +#include +#include +#include "clipp.h" +#include "bitvector.h" +//#include "sdsl/bits.hpp" + +#define EQS_PER_SLOT 20000000 + +using namespace std; + +typedef std::vector> eqvec; + +struct Edge { + uint32_t n1; + uint32_t n2; + uint16_t weight; + + Edge(uint32_t inN1, uint32_t inN2, uint16_t inWeight) + : n1(inN1), n2(inN2), weight(inWeight) {} +}; + +struct EdgePtr { + uint16_t bucket; + uint32_t idx; + + EdgePtr(uint16_t bucketIn, uint32_t idxIn) : bucket(bucketIn), idx(idxIn) {} +}; + +struct Child { + uint32_t id; + uint16_t weight; + + Child(uint32_t inN1, uint16_t inWeight) : id(inN1), weight(inWeight) {} +}; + +struct Path { + uint32_t id; + uint32_t steps; + uint64_t weight; + + Path(uint32_t idIn, + uint32_t stepsIn, + uint64_t weightIn) : id(idIn), steps(stepsIn), weight(weightIn) {} +}; + +struct DisjointSetNode { + uint32_t parent{0}; + uint64_t rnk{0}, w{0}, edges{0}; + + void setParent(uint32_t p) { parent = p; } + + void mergeWith(DisjointSetNode &n, uint16_t edgeW, uint32_t id) { + n.setParent(parent); + w += (n.w + static_cast(edgeW)); + edges += (n.edges + 1); + n.edges = 0; + n.w = 0; + if (rnk == n.rnk) { + rnk++; + } + } +}; + +// To represent Disjoint Sets +struct DisjointSets { + std::vector els; + uint64_t n; + + // Constructor. + DisjointSets(uint64_t n) { + // Allocate memory + this->n = n; + els.resize(n); + // Initially, all vertices are in + // different sets and have rank 0. + for (uint64_t i = 0; i <= n; i++) { + //every element is parent of itself + els[i].setParent(i); + } + } + + // Find the parent of a node 'u' + // Path Compression + uint32_t find(uint32_t u) { + /* Make the parent of the nodes in the path + from u--> parent[u] point to parent[u] */ + if (u != els[u].parent) + els[u].parent = find(els[u].parent); + return els[u].parent; + } + + // Union by rank + void merge(uint32_t x, uint32_t y, uint16_t edgeW) { + x = find(x), y = find(y); + + /* Make tree with smaller height + a subtree of the other tree */ + if (els[x].rnk > els[y].rnk) { + els[x].mergeWith(els[y], edgeW, x); + + } else {// If rnk[x] <= rnk[y] + els[y].mergeWith(els[x], edgeW, y); + } + } +}; + +// Structure to represent a graph +struct Graph { + + uint64_t V; + + vector> edges; + vector> mst; + + uint64_t mst_totalWeight{0}; + + Graph(uint64_t bucketCnt) { edges.resize(bucketCnt); } + + // Utility function to add an edge + void addEdge(uint32_t u, uint32_t v, uint16_t w) { + edges[w - 1].emplace_back(u, v, w); + //edges.emplace_back(u, v, w); + } + + // Function to find MST using Kruskal's + // MST algorithm + DisjointSets kruskalMSF(uint32_t bucketCnt) { + int mst_wt = 0; // Initialize result + + // Create disjoint sets + DisjointSets ds(V); + + std::string tmp; + uint64_t n1{0}, n2{0}, cntr{0}, mergeCntr{0}; + uint32_t w{0}; + sdsl::bit_vector nodes(V, 0); + // Iterate through all sorted edges + for (auto bucketCntr = 0; bucketCntr < bucketCnt; bucketCntr++) { + //ifstream file(filename); + /*std::getline(file, tmp); + while (file.good()) { + file >> n1 >> n2 >> w;*/ + uint32_t edgeIdxInBucket = 0; + for (auto it = edges[bucketCntr].begin(); it != edges[bucketCntr].end(); it++) { + //if (w == bucketCntr) { + w = it->weight; + uint32_t u = it->n1; + uint32_t v = it->n2; + uint32_t set_u = ds.find(u); + uint32_t set_v = ds.find(v); + + // Check if the selected edge is creating + // a cycle or not (Cycle is created if u + // and v belong to same set) + if (set_u != set_v) { + // Current edge will be in the MST + // Merge two sets + ds.merge(set_u, set_v, w); + mst[u].emplace_back(bucketCntr, edgeIdxInBucket); + mst[v].emplace_back(bucketCntr, edgeIdxInBucket); + nodes[u] = 1; + nodes[v] = 1; + mst_totalWeight += w; + mergeCntr++; + }/* else { + if (nodes.find(u) == nodes.end() || nodes.find(v) == nodes.end()) + std::cerr << u << " " << v << " " << set_u << " " << set_v << "\n"; + }*/ + cntr++; + if (cntr % 1000000 == 0) { + std::cerr << "edge " << cntr << " " << mergeCntr << "\n"; + } + edgeIdxInBucket++; + //} + } + /*file.clear(); + file.seekg(0, file.beg);*/ + + } + //file.close(); + uint64_t distinctNodes{0}; + for (uint64_t i = 0; i < V; i += 64) { + distinctNodes += sdsl::bits::cnt(nodes.get_int(i, 64)); + } + + std::cerr << "final # of edges: " << cntr + << "\n# of merges: " << mergeCntr + << "\n# of distinct nodes: " << distinctNodes + << "\n"; + return ds; + } +}; + +void loadEqs(std::string filename, eqvec &bvs) { + bvs.reserve(20); + std::string eqfile; + std::ifstream eqlist(filename); + if (eqlist.is_open()) { + uint64_t accumTotalEqCls = 0; + while (getline(eqlist, eqfile)) { + sdsl::rrr_vector<63> bv; + bvs.push_back(bv); + sdsl::load_from_file(bvs.back(), eqfile); + } + } + std::cerr << "loaded all the equivalence classes: " + << ((bvs.size() - 1) * EQS_PER_SLOT + bvs.back().size()) + << "\n"; +} + +void buildColor(eqvec &bvs, + std::vector &eq, + uint64_t eqid, +uint64_t num_samples) { +uint64_t i{0}, bitcnt{0}, wrdcnt{0}; +uint64_t idx = eqid / EQS_PER_SLOT; +uint64_t offset = eqid % EQS_PER_SLOT; +//std::cerr << eqid << " " << num_samples << " " << idx << " " << offset << "\n"; +while (i eq; +eq.resize(numWrds); +buildColor(bvs, eq, eqid, num_samples); +for (uint64_t i = 0; i < eq.size(); i += 1) { +res += (uint16_t)sdsl::bits::cnt(eq[i]); +} +return res; +} + +// for two non-zero nodes, delta list is positions that xor of the bits was 1 +std::vector getDeltaList(eqvec &bvs, + uint64_t eqid1,uint64_t eqid2, uint64_t num_samples, uint64_t numWrds) { +std::vector res; +std::vector eq1, eq2; +eq1.resize(numWrds); +eq2.resize(numWrds); +buildColor(bvs, eq1, eqid1, num_samples); +buildColor(bvs, eq2, eqid2, num_samples); + +for (uint32_t i = 0; i < eq1.size(); i += 1) { +uint64_t eq12xor = eq1[i] ^ eq2[i]; +for (uint32_t j = 0; j < 64; j++) { +if ( (eq12xor >> j) & 0x01 ) { +res.push_back(i*64+j); +} +} +} + +return res; // rely on c++ optimization +} + +// for those connected to node zero, delta list is position of set bits +std::vector getDeltaList(eqvec &bvs, + uint64_t eqid1, uint64_t num_samples, uint64_t numWrds) { +std::vector res; +std::vector eq1; +eq1.resize(numWrds); +buildColor(bvs, eq1, eqid1, num_samples); + +for (uint32_t i = 0; i < eq1.size(); i += 1) { +for (uint32_t j = 0; j < 64; j++) { +if ( (eq1[i] >> j) & 0x01 ) { +res.push_back(i*64+j); +} +} +} + +return res; // rely on c++ optimization +} + +#endif //MANTIS_MSF_H diff --git a/src/MSF.cc b/src/MSF.cc index 2b7ea4a..1d965ee 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -5,286 +5,7 @@ // https://www.geeksforgeeks.org/kruskals-minimum-spanning-tree-using-stl-in-c/ // -#include -#include -#include -#include -#include "clipp.h" -#include "bitvector.h" -//#include "sdsl/bits.hpp" - -#define EQS_PER_SLOT 20000000 - -using namespace std; - -typedef std::vector> eqvec; - -struct Edge { - uint32_t n1; - uint32_t n2; - uint16_t weight; - - Edge(uint32_t inN1, uint32_t inN2, uint16_t inWeight) - : n1(inN1), n2(inN2), weight(inWeight) {} -}; - -struct EdgePtr { - uint16_t bucket; - uint32_t idx; - - EdgePtr(uint16_t bucketIn, uint32_t idxIn) : bucket(bucketIn), idx(idxIn) {} -}; - -struct Child { - uint32_t id; - uint16_t weight; - - Child(uint32_t inN1, uint16_t inWeight) : id(inN1), weight(inWeight) {} -}; - -struct Path { - uint32_t id; - uint32_t steps; - uint64_t weight; - - Path(uint32_t idIn, - uint32_t stepsIn, - uint64_t weightIn) : id(idIn), steps(stepsIn), weight(weightIn) {} -}; - -struct DisjointSetNode { - uint32_t parent{0}; - uint64_t rnk{0}, w{0}, edges{0}; - - void setParent(uint32_t p) { parent = p; } - - void mergeWith(DisjointSetNode &n, uint16_t edgeW, uint32_t id) { - n.setParent(parent); - w += (n.w + static_cast(edgeW)); - edges += (n.edges + 1); - n.edges = 0; - n.w = 0; - if (rnk == n.rnk) { - rnk++; - } - } -}; - -// To represent Disjoint Sets -struct DisjointSets { - std::vector els; - uint64_t n; - - // Constructor. - DisjointSets(uint64_t n) { - // Allocate memory - this->n = n; - els.resize(n); - // Initially, all vertices are in - // different sets and have rank 0. - for (uint64_t i = 0; i <= n; i++) { - //every element is parent of itself - els[i].setParent(i); - } - } - - // Find the parent of a node 'u' - // Path Compression - uint32_t find(uint32_t u) { - /* Make the parent of the nodes in the path - from u--> parent[u] point to parent[u] */ - if (u != els[u].parent) - els[u].parent = find(els[u].parent); - return els[u].parent; - } - - // Union by rank - void merge(uint32_t x, uint32_t y, uint16_t edgeW) { - x = find(x), y = find(y); - - /* Make tree with smaller height - a subtree of the other tree */ - if (els[x].rnk > els[y].rnk) { - els[x].mergeWith(els[y], edgeW, x); - - } else {// If rnk[x] <= rnk[y] - els[y].mergeWith(els[x], edgeW, y); - } - } -}; - -// Structure to represent a graph -struct Graph { - - uint64_t V; - - vector> edges; - vector> mst; - - uint64_t mst_totalWeight{0}; - - Graph(uint64_t bucketCnt) { edges.resize(bucketCnt); } - - // Utility function to add an edge - void addEdge(uint32_t u, uint32_t v, uint16_t w) { - edges[w - 1].emplace_back(u, v, w); - //edges.emplace_back(u, v, w); - } - - // Function to find MST using Kruskal's - // MST algorithm - DisjointSets kruskalMSF(uint32_t bucketCnt) { - int mst_wt = 0; // Initialize result - - // Create disjoint sets - DisjointSets ds(V); - - std::string tmp; - uint64_t n1{0}, n2{0}, cntr{0}, mergeCntr{0}; - uint32_t w{0}; - sdsl::bit_vector nodes(V, 0); - // Iterate through all sorted edges - for (auto bucketCntr = 0; bucketCntr < bucketCnt; bucketCntr++) { - //ifstream file(filename); - /*std::getline(file, tmp); - while (file.good()) { - file >> n1 >> n2 >> w;*/ - uint32_t edgeIdxInBucket = 0; - for (auto it = edges[bucketCntr].begin(); it != edges[bucketCntr].end(); it++) { - //if (w == bucketCntr) { - w = it->weight; - uint32_t u = it->n1; - uint32_t v = it->n2; - uint32_t set_u = ds.find(u); - uint32_t set_v = ds.find(v); - - // Check if the selected edge is creating - // a cycle or not (Cycle is created if u - // and v belong to same set) - if (set_u != set_v) { - // Current edge will be in the MST - // Merge two sets - ds.merge(set_u, set_v, w); - mst[u].emplace_back(bucketCntr, edgeIdxInBucket); - mst[v].emplace_back(bucketCntr, edgeIdxInBucket); - nodes[u] = 1; - nodes[v] = 1; - mst_totalWeight += w; - mergeCntr++; - }/* else { - if (nodes.find(u) == nodes.end() || nodes.find(v) == nodes.end()) - std::cerr << u << " " << v << " " << set_u << " " << set_v << "\n"; - }*/ - cntr++; - if (cntr % 1000000 == 0) { - std::cerr << "edge " << cntr << " " << mergeCntr << "\n"; - } - edgeIdxInBucket++; - //} - } - /*file.clear(); - file.seekg(0, file.beg);*/ - - } - //file.close(); - uint64_t distinctNodes{0}; - for (uint64_t i = 0; i < V; i += 64) { - distinctNodes += sdsl::bits::cnt(nodes.get_int(i, 64)); - } - - std::cerr << "final # of edges: " << cntr - << "\n# of merges: " << mergeCntr - << "\n# of distinct nodes: " << distinctNodes - << "\n"; - return ds; - } -}; - -void loadEqs(std::string filename, eqvec &bvs) { - bvs.reserve(20); - std::string eqfile; - std::ifstream eqlist(filename); - if (eqlist.is_open()) { - uint64_t accumTotalEqCls = 0; - while (getline(eqlist, eqfile)) { - sdsl::rrr_vector<63> bv; - bvs.push_back(bv); - sdsl::load_from_file(bvs.back(), eqfile); - } - } - std::cerr << "loaded all the equivalence classes: " - << ((bvs.size() - 1) * EQS_PER_SLOT + bvs.back().size()) - << "\n"; -} - -void buildColor(eqvec &bvs, - std::vector &eq, - uint64_t eqid, - uint64_t num_samples) { - uint64_t i{0}, bitcnt{0}, wrdcnt{0}; - uint64_t idx = eqid / EQS_PER_SLOT; - uint64_t offset = eqid % EQS_PER_SLOT; - //std::cerr << eqid << " " << num_samples << " " << idx << " " << offset << "\n"; - while (i eq; - eq.resize(numWrds); - buildColor(bvs, eq, eqid, num_samples); - for (uint64_t i = 0; i < eq.size(); i += 1) { - res += (uint16_t)sdsl::bits::cnt(eq[i]); - } - return res; -} - -// for two non-zero nodes, delta list is positions that xor of the bits was 1 -std::vector getDeltaList(eqvec &bvs, - uint64_t eqid1,uint64_t eqid2, uint64_t num_samples, uint64_t numWrds) { - std::vector res; - std::vector eq1, eq2; - eq1.resize(numWrds); - eq2.resize(numWrds); - buildColor(bvs, eq1, eqid1, num_samples); - buildColor(bvs, eq2, eqid2, num_samples); - - for (uint32_t i = 0; i < eq1.size(); i += 1) { - uint64_t eq12xor = eq1[i] ^ eq2[i]; - for (uint32_t j = 0; j < 64; j++) { - if ( (eq12xor >> j) & 0x01 ) { - res.push_back(i*64+j); - } - } - } - - return res; // rely on c++ optimization -} - -// for those connected to node zero, delta list is position of set bits -std::vector getDeltaList(eqvec &bvs, - uint64_t eqid1, uint64_t num_samples, uint64_t numWrds) { - std::vector res; - std::vector eq1; - eq1.resize(numWrds); - buildColor(bvs, eq1, eqid1, num_samples); - - for (uint32_t i = 0; i < eq1.size(); i += 1) { - for (uint32_t j = 0; j < 64; j++) { - if ( (eq1[i] >> j) & 0x01 ) { - res.push_back(i*64+j); - } - } - } - - return res; // rely on c++ optimization -} +#include "MSF.h" struct Opts { std::string filename; From 22ed85f95f696094b1572beac66f8fe4f013d8cd Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 20 Jul 2018 17:49:07 -0400 Subject: [PATCH 031/122] A validator for checking the accuracy of equivalence class encoding. Checks if the color class we retrieve by decoding the new data structure is the same as what we get from the old color class table. --- src/CMakeLists.txt | 6 +++ src/validateMSF.cc | 109 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 src/validateMSF.cc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 02e9adb..0be18e6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -75,6 +75,12 @@ target_include_directories(msf PUBLIC target_link_libraries(msf mantis_core) +add_executable(validateMsf validateMSF.cc) +target_include_directories(validateMsf PUBLIC + $) +target_link_libraries(validateMsf + mantis_core) + add_executable(mstBoost MST_boost.cc) target_include_directories(mstBoost PUBLIC $) diff --git a/src/validateMSF.cc b/src/validateMSF.cc new file mode 100644 index 0000000..26be76a --- /dev/null +++ b/src/validateMSF.cc @@ -0,0 +1,109 @@ +// +// Created by Fatemeh Almodaresi on 7/20/18. +// + +#include "MSF.h" +#include +#include + +class MSFQuery { +private: + uint64_t numSamples; + uint64_t numWrds; + uint32_t zero; + +public: + sdsl::int_vector<> parentbv; + sdsl::int_vector<> deltabv; + sdsl::bit_vector::select_1_type sbbv; + + MSFQuery(uint64_t numSamplesIn): numSamples(numSamplesIn) { + numWrds = (uint64_t)std::ceil((double)numSamples/64.0); + } + + void loadIdx(std::string indexDir) { + sdsl::load_from_file(parentbv, indexDir+"/parents.bv"); + sdsl::load_from_file(deltabv, indexDir+"/deltas.bv"); + sdsl::bit_vector bbv; + sdsl::load_from_file(bbv, indexDir+"/boundary.bv"); + sbbv = sdsl::bit_vector::select_1_type(&bbv); + zero = parentbv.size()-1; // maximum color id which + std::cerr << "Loaded the new color class index\n"; + std::cerr << "--> parent size: " << parentbv.size() << "\n" + << "--> delta size: " << deltabv.size() << "\n" + << "--> boundary size: " << bbv.size() << "\n"; + } + + std::vector buildColor(uint64_t eqid) { + std::vector flips(numSamples); + uint64_t i{eqid}, from{0}, to{0}; + while (parentbv[i] != i) { + std::cerr << i << " " << parentbv[i] << "\n"; + if (i > 0) + from = sbbv(i)+1; + to = sbbv(i+1); + std::cerr << "f" << from << " t" << to << "\n"; + for (auto j = from; j <= to; j++) { + flips[deltabv[j]] ^= 0x01; + } + i = parentbv[i]; + } + if (i != zero) { + if (i > 0) + from = sbbv(i)+1; + to = sbbv(i+1); + for (auto j = from; j <= to; j++) { + flips[deltabv[j]] ^= 0x01; + } + } + std::vector eq(numWrds); + uint64_t one = 1; + for (i = 0; i < numSamples; i++) { + if (flips[i]) { + uint64_t idx = i / numSamples; + eq[idx] = eq[idx] ^ (one << (i % numSamples)); + } + } + return eq; + } +}; + + +int main(int argc, char *argv[]) { + std::string indexDir = argv[1]; + std::string eqlistfile = argv[2]; + uint64_t numSamples = std::stoull(argv[3]); + uint64_t numWrds = (uint64_t)std::ceil((double)numSamples/64.0); + MSFQuery msfQuery(numSamples); + msfQuery.loadIdx(indexDir); + for (uint64_t i = 1; i < 100; i++) { + std::cerr < ids; + while (ids.size() < 10) { + ids.insert(rand() % eqCount); + } + + uint64_t cntr{0}; + for (auto idx : ids) { + std::vector newEq = msfQuery.buildColor(idx); + std::cerr <<"1\n"; + std::vector oldEq(numWrds); + buildColor(bvs, oldEq, idx, numSamples); + std::cerr <<"2\n"; + + if (newEq != oldEq) { + std::cerr << "AAAAA! LOOOSER!!\n"; + std::cerr << cntr << ": index=" << idx << "\n"; + std::exit(1); + } + cntr++; + } + std::cerr << "WOOOOW! Validation passed\n"; +} \ No newline at end of file From 71c00b5fe73baf2865d33d7bee736210d3bdbf5b Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 20 Jul 2018 18:42:57 -0400 Subject: [PATCH 032/122] fixed a bug --- src/validateMSF.cc | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index 26be76a..f9ceaa7 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -11,6 +11,7 @@ class MSFQuery { uint64_t numSamples; uint64_t numWrds; uint32_t zero; + sdsl::bit_vector bbv; public: sdsl::int_vector<> parentbv; @@ -24,7 +25,6 @@ class MSFQuery { void loadIdx(std::string indexDir) { sdsl::load_from_file(parentbv, indexDir+"/parents.bv"); sdsl::load_from_file(deltabv, indexDir+"/deltas.bv"); - sdsl::bit_vector bbv; sdsl::load_from_file(bbv, indexDir+"/boundary.bv"); sbbv = sdsl::bit_vector::select_1_type(&bbv); zero = parentbv.size()-1; // maximum color id which @@ -32,36 +32,42 @@ class MSFQuery { std::cerr << "--> parent size: " << parentbv.size() << "\n" << "--> delta size: " << deltabv.size() << "\n" << "--> boundary size: " << bbv.size() << "\n"; - } + } std::vector buildColor(uint64_t eqid) { std::vector flips(numSamples); uint64_t i{eqid}, from{0}, to{0}; while (parentbv[i] != i) { - std::cerr << i << " " << parentbv[i] << "\n"; + //std::cerr << i << " " << parentbv[i] << "\n"; if (i > 0) from = sbbv(i)+1; to = sbbv(i+1); - std::cerr << "f" << from << " t" << to << "\n"; for (auto j = from; j <= to; j++) { + //std::cerr << deltabv[j] << "\n"; flips[deltabv[j]] ^= 0x01; } i = parentbv[i]; } if (i != zero) { + std::cerr << "root not zero\n"; if (i > 0) from = sbbv(i)+1; to = sbbv(i+1); for (auto j = from; j <= to; j++) { + //std::cerr << deltabv[j] << "\n"; flips[deltabv[j]] ^= 0x01; } } + else { + std::cerr <<"root is zero\n"; + } std::vector eq(numWrds); uint64_t one = 1; for (i = 0; i < numSamples; i++) { if (flips[i]) { - uint64_t idx = i / numSamples; - eq[idx] = eq[idx] ^ (one << (i % numSamples)); + uint64_t idx = i / 64; + //std::cerr << "set " << i << " " << idx << " " << i % 64 << "\n"; + eq[idx] = eq[idx] | (one << (i % 64)); } } return eq; @@ -74,12 +80,9 @@ int main(int argc, char *argv[]) { std::string eqlistfile = argv[2]; uint64_t numSamples = std::stoull(argv[3]); uint64_t numWrds = (uint64_t)std::ceil((double)numSamples/64.0); - MSFQuery msfQuery(numSamples); - msfQuery.loadIdx(indexDir); - for (uint64_t i = 1; i < 100; i++) { - std::cerr < newEq = msfQuery.buildColor(idx); - std::cerr <<"1\n"; + std::vector newEq = query.buildColor(idx); std::vector oldEq(numWrds); buildColor(bvs, oldEq, idx, numSamples); - std::cerr <<"2\n"; if (newEq != oldEq) { std::cerr << "AAAAA! LOOOSER!!\n"; std::cerr << cntr << ": index=" << idx << "\n"; + std::cerr << "new size: " << newEq.size() << " old size: " << oldEq.size() << "\n"; + for (auto k = 0; k < newEq.size(); k++) { + std::cerr << newEq[k] << " " << oldEq[k] << "\n"; + } std::exit(1); } cntr++; From 5f6d4d44128508506e61d67a7523a7efaca0fc2c Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 20 Jul 2018 19:00:58 -0400 Subject: [PATCH 033/122] A few logs to trace the bug --- src/validateMSF.cc | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index f9ceaa7..37a54e0 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -34,18 +34,25 @@ class MSFQuery { << "--> boundary size: " << bbv.size() << "\n"; } - std::vector buildColor(uint64_t eqid) { + std::vector buildColor(uint64_t eqid, eqvec &bvs) { std::vector flips(numSamples); uint64_t i{eqid}, from{0}, to{0}; while (parentbv[i] != i) { - //std::cerr << i << " " << parentbv[i] << "\n"; + std::cerr << "c" << i << " p" << parentbv[i] << "\n"; if (i > 0) from = sbbv(i)+1; to = sbbv(i+1); + std::cerr << "delta list: \n"; + auto res = getDeltaList(bvs, i, parentbv[i], numSamples, numWrds); + for (auto v : res) { + std::cerr << v << " "; + } + std::cerr << "\nours:\n"; for (auto j = from; j <= to; j++) { - //std::cerr << deltabv[j] << "\n"; + std::cerr << deltabv[j] << " "; flips[deltabv[j]] ^= 0x01; } + std::cerr <<"\n"; i = parentbv[i]; } if (i != zero) { @@ -95,7 +102,7 @@ int main(int argc, char *argv[]) { uint64_t cntr{0}; for (auto idx : ids) { - std::vector newEq = query.buildColor(idx); + std::vector newEq = query.buildColor(idx, bvs); std::vector oldEq(numWrds); buildColor(bvs, oldEq, idx, numSamples); From 5142460522475bf660c9faa22b778919b51e8074 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 20 Jul 2018 19:57:10 -0400 Subject: [PATCH 034/122] resolved the bug at the root case --- src/MSF.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSF.cc b/src/MSF.cc index 1d965ee..6a1288f 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -206,7 +206,7 @@ int main(int argc, char *argv[]) { std::vector deltas; if (i == zero) { deltaOffset++; - } else if (parentbv[i] == zero) { + } else if (parentbv[i] == zero || parentbv[i] == i) { deltas = getDeltaList(eqs, i, opt.numSamples, numWrds); } else { deltas = getDeltaList(eqs, parentbv[i], i, opt.numSamples, numWrds); From ea28cf93803aa71d571e79829270d54e9d1bdaec Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 20 Jul 2018 22:44:59 -0400 Subject: [PATCH 035/122] resolved a bug in MSF validation/query code. Validation passes now --- src/MSF.cc | 4 +++- src/validateMSF.cc | 31 +++++++++++++++++++------------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index 6a1288f..c22ef41 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -200,7 +200,6 @@ int main(int argc, char *argv[]) { // create the data structures std::cerr << "Calculate Stats .. \n"; std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n"; - nodeCntr = 0; uint64_t deltaOffset{0}; for (uint64_t i = 0; i < parentbv.size(); i++) { std::vector deltas; @@ -216,6 +215,9 @@ int main(int argc, char *argv[]) { deltaOffset++; } bbv[deltaOffset-1] = 1; + if (i % 10000000 == 0) { + std::cerr << i << " nodes parents processed\n"; + } } diff --git a/src/validateMSF.cc b/src/validateMSF.cc index 37a54e0..f7233d9 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -38,25 +38,27 @@ class MSFQuery { std::vector flips(numSamples); uint64_t i{eqid}, from{0}, to{0}; while (parentbv[i] != i) { - std::cerr << "c" << i << " p" << parentbv[i] << "\n"; + //std::cerr << "c" << i << " p" << parentbv[i] << "\n"; if (i > 0) from = sbbv(i)+1; + else + from = 0; to = sbbv(i+1); - std::cerr << "delta list: \n"; - auto res = getDeltaList(bvs, i, parentbv[i], numSamples, numWrds); + /*std::cerr << "delta list: \n"; + auto res = getDeltaList(bvs, parentbv[i], i, numSamples, numWrds); for (auto v : res) { std::cerr << v << " "; } - std::cerr << "\nours:\n"; + std::cerr << "\nours from " << from << " to " << to << ":\n";*/ for (auto j = from; j <= to; j++) { - std::cerr << deltabv[j] << " "; + //std::cerr << deltabv[j] << " "; flips[deltabv[j]] ^= 0x01; } - std::cerr <<"\n"; + //std::cerr <<"\n"; i = parentbv[i]; } if (i != zero) { - std::cerr << "root not zero\n"; + //std::cerr << "root not zero\n"; if (i > 0) from = sbbv(i)+1; to = sbbv(i+1); @@ -65,9 +67,9 @@ class MSFQuery { flips[deltabv[j]] ^= 0x01; } } - else { + /*else { std::cerr <<"root is zero\n"; - } + }*/ std::vector eq(numWrds); uint64_t one = 1; for (i = 0; i < numSamples; i++) { @@ -94,14 +96,16 @@ int main(int argc, char *argv[]) { eqvec bvs; loadEqs(eqlistfile, bvs); + // choose 1M random eq classes std::unordered_set ids; - while (ids.size() < 10) { + /*while (ids.size() < 1000000) { ids.insert(rand() % eqCount); - } + }*/ + //ids.insert(12237996); uint64_t cntr{0}; - for (auto idx : ids) { + for (uint64_t idx = 0; idx < eqCount; idx++) { std::vector newEq = query.buildColor(idx, bvs); std::vector oldEq(numWrds); buildColor(bvs, oldEq, idx, numSamples); @@ -116,6 +120,9 @@ int main(int argc, char *argv[]) { std::exit(1); } cntr++; + if (cntr % 10000000 == 0) { + std::cerr << cntr << " eqs were the same\n"; + } } std::cerr << "WOOOOW! Validation passed\n"; } \ No newline at end of file From 198c459ec1a0498d44742b2e12cf4ec3df5c6569 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Sat, 21 Jul 2018 01:50:25 -0400 Subject: [PATCH 036/122] make validator more than just a validator! We can now evaluate # of steps required in addition to validate the encoding gonna add query timing to it soon too. --- src/CMakeLists.txt | 6 +- src/validateMSF.cc | 138 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 111 insertions(+), 33 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0be18e6..36fd551 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -75,10 +75,10 @@ target_include_directories(msf PUBLIC target_link_libraries(msf mantis_core) -add_executable(validateMsf validateMSF.cc) -target_include_directories(validateMsf PUBLIC +add_executable(loadColorCls validateMSF.cc) +target_include_directories(loadColorCls PUBLIC $) -target_link_libraries(validateMsf +target_link_libraries(loadColorCls mantis_core) add_executable(mstBoost MST_boost.cc) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index f7233d9..ee4841f 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -83,46 +83,124 @@ class MSFQuery { } }; +uint32_t recursiveSteps(uint32_t idx, sdsl::int_vector<> &parentbv, + std::vector &steps, + uint32_t zero) { + if (idx == zero) + return steps[idx]; // 0 + if (steps[idx] != 0) + return steps[idx]; + if (parentbv[idx] == idx) { + steps[idx]=1; // to retrieve the representative + return steps[idx]; + } + steps[idx] = recursiveSteps(parentbv[idx], parentbv, steps, zero)+1; + return steps[idx]; +} + +struct Opts { + std::string indexDir; + uint64_t numSamples; + std::string eqlistfile; +}; int main(int argc, char *argv[]) { - std::string indexDir = argv[1]; - std::string eqlistfile = argv[2]; - uint64_t numSamples = std::stoull(argv[3]); + + using namespace clipp; + enum class mode { + validate, steps, query, help + }; + mode selected = mode::help; + Opts opt; + + auto validate_mode = ( + command("validate").set(selected, mode::validate), + required("-i", "--indexDir") & + value("index_dir", opt.indexDir) % "Directory containing index files.", + required("-eq", "--eqCls-lst-file") & + value("eqCls_list_filename", opt.eqlistfile) % "File containing list of equivalence (color) classes.", + required("-s", "--numSamples") & + value("numSamples", opt.numSamples) % "Total number of experiments (samples)." + ); + + auto steps_mode = ( + command("steps").set(selected, mode::steps), + required("-i", "--indexDir") & + value("index_dir", opt.indexDir) % "Directory containing index files.", + required("-s", "--numSamples") & + value("numSamples", opt.numSamples) % "Total number of experiments (samples)." + ); + + auto cli = ( + (validate_mode | steps_mode | command("help").set(selected, mode::help) + ) + ); + + decltype(parse(argc, argv, cli)) res; + try { + res = parse(argc, argv, cli); + } catch (std::exception &e) { + std::cout << "\n\nParsing command line failed with exception: " << e.what() << "\n"; + std::cout << "\n\n"; + std::cout << make_man_page(cli, "MSF"); + return 1; + } + if (!res) { + std::cerr << "Cannot parse the input arguments\n"; + std::exit(1); + } + if (selected == mode::help) { + std::cerr << make_man_page(cli, "MSF"); + std::exit(1); + } + + std::string indexDir = opt.indexDir; + std::string eqlistfile = opt.eqlistfile; + uint64_t numSamples = opt.numSamples; uint64_t numWrds = (uint64_t)std::ceil((double)numSamples/64.0); MSFQuery query(numSamples); query.loadIdx(indexDir); uint64_t eqCount = query.parentbv.size()-1; std::cerr << "total # of equivalence classes is : " << eqCount << "\n"; - eqvec bvs; - loadEqs(eqlistfile, bvs); - - // choose 1M random eq classes - std::unordered_set ids; - /*while (ids.size() < 1000000) { - ids.insert(rand() % eqCount); - }*/ - //ids.insert(12237996); - - uint64_t cntr{0}; - for (uint64_t idx = 0; idx < eqCount; idx++) { - std::vector newEq = query.buildColor(idx, bvs); - std::vector oldEq(numWrds); - buildColor(bvs, oldEq, idx, numSamples); - - if (newEq != oldEq) { - std::cerr << "AAAAA! LOOOSER!!\n"; - std::cerr << cntr << ": index=" << idx << "\n"; - std::cerr << "new size: " << newEq.size() << " old size: " << oldEq.size() << "\n"; - for (auto k = 0; k < newEq.size(); k++) { - std::cerr << newEq[k] << " " << oldEq[k] << "\n"; + if (selected == mode::validate) { + eqvec bvs; + loadEqs(eqlistfile, bvs); + // choose 1M random eq classes + /*std::unordered_set ids; + while (ids.size() < 1000000) { + ids.insert(rand() % eqCount); + }*/ + + uint64_t cntr{0}; + for (uint64_t idx = 0; idx < eqCount; idx++) { + std::vector newEq = query.buildColor(idx, bvs); + std::vector oldEq(numWrds); + buildColor(bvs, oldEq, idx, numSamples); + + if (newEq != oldEq) { + std::cerr << "AAAAA! LOOOSER!!\n"; + std::cerr << cntr << ": index=" << idx << "\n"; + std::cerr << "new size: " << newEq.size() << " old size: " << oldEq.size() << "\n"; + for (auto k = 0; k < newEq.size(); k++) { + std::cerr << newEq[k] << " " << oldEq[k] << "\n"; + } + std::exit(1); + } + cntr++; + if (cntr % 10000000 == 0) { + std::cerr << cntr << " eqs were the same\n"; } - std::exit(1); } - cntr++; - if (cntr % 10000000 == 0) { - std::cerr << cntr << " eqs were the same\n"; + std::cerr << "WOOOOW! Validation passed\n"; + } else if (selected == mode::steps) { + uint32_t zero = query.parentbv.size()-1; + std::vector steps(eqCount, 0); + + for (uint64_t idx = 0; idx < eqCount; idx++) { + std::cout << query.parentbv[idx] << "\t" + << idx << "\t" + << recursiveSteps(idx, query.parentbv, steps, zero) << "\n"; } } - std::cerr << "WOOOOW! Validation passed\n"; } \ No newline at end of file From fbd0620f1c8ddb235b82ad6d9b77b9e2f8f95146 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Sat, 21 Jul 2018 02:04:15 -0400 Subject: [PATCH 037/122] Added the decoding command --- src/validateMSF.cc | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index ee4841f..40edd7c 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -34,48 +34,33 @@ class MSFQuery { << "--> boundary size: " << bbv.size() << "\n"; } - std::vector buildColor(uint64_t eqid, eqvec &bvs) { + std::vector buildColor(uint64_t eqid) { std::vector flips(numSamples); uint64_t i{eqid}, from{0}, to{0}; while (parentbv[i] != i) { - //std::cerr << "c" << i << " p" << parentbv[i] << "\n"; if (i > 0) from = sbbv(i)+1; else from = 0; to = sbbv(i+1); - /*std::cerr << "delta list: \n"; - auto res = getDeltaList(bvs, parentbv[i], i, numSamples, numWrds); - for (auto v : res) { - std::cerr << v << " "; - } - std::cerr << "\nours from " << from << " to " << to << ":\n";*/ for (auto j = from; j <= to; j++) { - //std::cerr << deltabv[j] << " "; flips[deltabv[j]] ^= 0x01; } - //std::cerr <<"\n"; i = parentbv[i]; } if (i != zero) { - //std::cerr << "root not zero\n"; if (i > 0) from = sbbv(i)+1; to = sbbv(i+1); for (auto j = from; j <= to; j++) { - //std::cerr << deltabv[j] << "\n"; flips[deltabv[j]] ^= 0x01; } } - /*else { - std::cerr <<"root is zero\n"; - }*/ std::vector eq(numWrds); uint64_t one = 1; for (i = 0; i < numSamples; i++) { if (flips[i]) { uint64_t idx = i / 64; - //std::cerr << "set " << i << " " << idx << " " << i % 64 << "\n"; eq[idx] = eq[idx] | (one << (i % 64)); } } @@ -108,7 +93,7 @@ int main(int argc, char *argv[]) { using namespace clipp; enum class mode { - validate, steps, query, help + validate, steps, decodeAllEqs, help }; mode selected = mode::help; Opts opt; @@ -131,8 +116,16 @@ int main(int argc, char *argv[]) { value("numSamples", opt.numSamples) % "Total number of experiments (samples)." ); + auto decodeAllEqs_mode = ( + command("decodeAllEqs").set(selected, mode::decodeAllEqs), + required("-i", "--indexDir") & + value("index_dir", opt.indexDir) % "Directory containing index files.", + required("-s", "--numSamples") & + value("numSamples", opt.numSamples) % "Total number of experiments (samples)." + ); + auto cli = ( - (validate_mode | steps_mode | command("help").set(selected, mode::help) + (validate_mode | steps_mode | decodeAllEqs_mode | command("help").set(selected, mode::help) ) ); @@ -174,7 +167,7 @@ int main(int argc, char *argv[]) { uint64_t cntr{0}; for (uint64_t idx = 0; idx < eqCount; idx++) { - std::vector newEq = query.buildColor(idx, bvs); + std::vector newEq = query.buildColor(idx); std::vector oldEq(numWrds); buildColor(bvs, oldEq, idx, numSamples); @@ -193,6 +186,13 @@ int main(int argc, char *argv[]) { } } std::cerr << "WOOOOW! Validation passed\n"; + } else if (selected == mode::decodeAllEqs) { + for (uint64_t idx = 0; idx < eqCount; idx++) { + std::vector newEq = query.buildColor(idx); + if (idx % 10000000 == 0) { + std::cerr << idx << " eqs decoded\n"; + } + } } else if (selected == mode::steps) { uint32_t zero = query.parentbv.size()-1; std::vector steps(eqCount, 0); From 620322b2fa076c45c3abef1311b3d6a168e661c3 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Mon, 23 Jul 2018 14:32:11 -0400 Subject: [PATCH 038/122] A little bit of code-cleaning. Prepare the basics for query command. --- src/validateMSF.cc | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index 40edd7c..aafc0f8 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -87,13 +87,14 @@ struct Opts { std::string indexDir; uint64_t numSamples; std::string eqlistfile; + std::string cqffile; }; int main(int argc, char *argv[]) { using namespace clipp; enum class mode { - validate, steps, decodeAllEqs, help + validate, steps, decodeAllEqs, query, help }; mode selected = mode::help; Opts opt; @@ -124,8 +125,18 @@ int main(int argc, char *argv[]) { value("numSamples", opt.numSamples) % "Total number of experiments (samples)." ); + auto query_mode = ( + command("query").set(selected, mode::query), + required("-i", "--indexDir") & + value("index_dir", opt.indexDir) % "Directory containing index files.", + required("-g", "--cqf") & + value("kmer-graph", opt.cqffile) % "cqf file containing the kmer mapping to color class ids.", + required("-s", "--numSamples") & + value("numSamples", opt.numSamples) % "Total number of experiments (samples)." + ); + auto cli = ( - (validate_mode | steps_mode | decodeAllEqs_mode | command("help").set(selected, mode::help) + (validate_mode | steps_mode | decodeAllEqs_mode | query_mode | command("help").set(selected, mode::help) ) ); @@ -147,30 +158,20 @@ int main(int argc, char *argv[]) { std::exit(1); } - std::string indexDir = opt.indexDir; - std::string eqlistfile = opt.eqlistfile; - uint64_t numSamples = opt.numSamples; - uint64_t numWrds = (uint64_t)std::ceil((double)numSamples/64.0); - MSFQuery query(numSamples); - query.loadIdx(indexDir); + uint64_t numWrds = (uint64_t)std::ceil((double)opt.numSamples/64.0); + MSFQuery query(opt.numSamples); + query.loadIdx(opt.indexDir); uint64_t eqCount = query.parentbv.size()-1; std::cerr << "total # of equivalence classes is : " << eqCount << "\n"; if (selected == mode::validate) { eqvec bvs; - loadEqs(eqlistfile, bvs); - // choose 1M random eq classes - /*std::unordered_set ids; - while (ids.size() < 1000000) { - ids.insert(rand() % eqCount); - }*/ - + loadEqs(opt.eqlistfile, bvs); uint64_t cntr{0}; for (uint64_t idx = 0; idx < eqCount; idx++) { std::vector newEq = query.buildColor(idx); std::vector oldEq(numWrds); - buildColor(bvs, oldEq, idx, numSamples); - + buildColor(bvs, oldEq, idx, opt.numSamples); if (newEq != oldEq) { std::cerr << "AAAAA! LOOOSER!!\n"; std::cerr << cntr << ": index=" << idx << "\n"; @@ -196,11 +197,12 @@ int main(int argc, char *argv[]) { } else if (selected == mode::steps) { uint32_t zero = query.parentbv.size()-1; std::vector steps(eqCount, 0); - for (uint64_t idx = 0; idx < eqCount; idx++) { std::cout << query.parentbv[idx] << "\t" << idx << "\t" << recursiveSteps(idx, query.parentbv, steps, zero) << "\n"; } + } else if (selected == mode::query) { + } } \ No newline at end of file From aa43aa0295509bc5d6c2cd8a394dac41539ffb5e Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 24 Jul 2018 11:31:25 -0400 Subject: [PATCH 039/122] Code to query using the new color class encoding. --- src/validateMSF.cc | 122 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 96 insertions(+), 26 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index aafc0f8..4544c41 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -2,9 +2,14 @@ // Created by Fatemeh Almodaresi on 7/20/18. // -#include "MSF.h" #include #include +#include "MSF.h" +#include "cqf.h" +#include "common_types.h" +#include "CLI/CLI.hpp" +#include "CLI/Timer.hpp" +#include "kmer.h" class MSFQuery { private: @@ -18,31 +23,31 @@ class MSFQuery { sdsl::int_vector<> deltabv; sdsl::bit_vector::select_1_type sbbv; - MSFQuery(uint64_t numSamplesIn): numSamples(numSamplesIn) { - numWrds = (uint64_t)std::ceil((double)numSamples/64.0); + MSFQuery(uint64_t numSamplesIn) : numSamples(numSamplesIn) { + numWrds = (uint64_t) std::ceil((double) numSamples / 64.0); } void loadIdx(std::string indexDir) { - sdsl::load_from_file(parentbv, indexDir+"/parents.bv"); - sdsl::load_from_file(deltabv, indexDir+"/deltas.bv"); - sdsl::load_from_file(bbv, indexDir+"/boundary.bv"); + sdsl::load_from_file(parentbv, indexDir + "/parents.bv"); + sdsl::load_from_file(deltabv, indexDir + "/deltas.bv"); + sdsl::load_from_file(bbv, indexDir + "/boundary.bv"); sbbv = sdsl::bit_vector::select_1_type(&bbv); - zero = parentbv.size()-1; // maximum color id which + zero = parentbv.size() - 1; // maximum color id which std::cerr << "Loaded the new color class index\n"; std::cerr << "--> parent size: " << parentbv.size() << "\n" << "--> delta size: " << deltabv.size() << "\n" << "--> boundary size: " << bbv.size() << "\n"; - } + } - std::vector buildColor(uint64_t eqid) { + std::vector buildColor(uint64_t eqid, bool all=true) { std::vector flips(numSamples); uint64_t i{eqid}, from{0}, to{0}; while (parentbv[i] != i) { if (i > 0) - from = sbbv(i)+1; + from = sbbv(i) + 1; else from = 0; - to = sbbv(i+1); + to = sbbv(i + 1); for (auto j = from; j <= to; j++) { flips[deltabv[j]] ^= 0x01; } @@ -50,12 +55,23 @@ class MSFQuery { } if (i != zero) { if (i > 0) - from = sbbv(i)+1; - to = sbbv(i+1); + from = sbbv(i) + 1; + to = sbbv(i + 1); for (auto j = from; j <= to; j++) { flips[deltabv[j]] ^= 0x01; } } + if (!all) { // return the indices of set bits + std::vector eq; + eq.reserve(numWrds); + uint64_t one = 1; + for (i = 0; i < numSamples; i++) { + if (flips[i]) { + eq.push_back(i); + } + } + return eq; + } std::vector eq(numWrds); uint64_t one = 1; for (i = 0; i < numSamples; i++) { @@ -66,8 +82,32 @@ class MSFQuery { } return eq; } + }; +mantis::QueryResult findSamples(const mantis::QuerySet& kmers, + CQF& dbg, MSFQuery& msfQuery) { + std::unordered_map query_eqclass_map; + for (auto k : kmers) { + KeyObject key(k, 0, 0); + uint64_t eqclass = dbg.query(key); + if (eqclass) + query_eqclass_map[eqclass] += 1; + } + + std::unordered_map sample_map; + for (auto it = query_eqclass_map.begin(); it != query_eqclass_map.end(); + ++it) { + auto eqclass_id = it->first-1; + auto count = it->second; + auto setbits = msfQuery.buildColor(eqclass_id, false); + for (auto sb : setbits) { + sample_map[sb] += count; + } + } + return sample_map; +} + uint32_t recursiveSteps(uint32_t idx, sdsl::int_vector<> &parentbv, std::vector &steps, uint32_t zero) { @@ -76,10 +116,10 @@ uint32_t recursiveSteps(uint32_t idx, sdsl::int_vector<> &parentbv, if (steps[idx] != 0) return steps[idx]; if (parentbv[idx] == idx) { - steps[idx]=1; // to retrieve the representative + steps[idx] = 1; // to retrieve the representative return steps[idx]; } - steps[idx] = recursiveSteps(parentbv[idx], parentbv, steps, zero)+1; + steps[idx] = recursiveSteps(parentbv[idx], parentbv, steps, zero) + 1; return steps[idx]; } @@ -88,6 +128,8 @@ struct Opts { uint64_t numSamples; std::string eqlistfile; std::string cqffile; + std::string outputfile; + std::string queryfile; }; int main(int argc, char *argv[]) { @@ -104,7 +146,8 @@ int main(int argc, char *argv[]) { required("-i", "--indexDir") & value("index_dir", opt.indexDir) % "Directory containing index files.", required("-eq", "--eqCls-lst-file") & - value("eqCls_list_filename", opt.eqlistfile) % "File containing list of equivalence (color) classes.", + value("eqCls_list_filename", opt.eqlistfile) % + "File containing list of equivalence (color) classes.", required("-s", "--numSamples") & value("numSamples", opt.numSamples) % "Total number of experiments (samples)." ); @@ -131,6 +174,10 @@ int main(int argc, char *argv[]) { value("index_dir", opt.indexDir) % "Directory containing index files.", required("-g", "--cqf") & value("kmer-graph", opt.cqffile) % "cqf file containing the kmer mapping to color class ids.", + required("-q", "--queryFile") & + value("query-file", opt.queryfile) % "query file containing list of sequences.", + required("-o", "--outputFile") & + value("query-output-file", opt.outputfile) % "file to write query results.", required("-s", "--numSamples") & value("numSamples", opt.numSamples) % "Total number of experiments (samples)." ); @@ -158,10 +205,10 @@ int main(int argc, char *argv[]) { std::exit(1); } - uint64_t numWrds = (uint64_t)std::ceil((double)opt.numSamples/64.0); - MSFQuery query(opt.numSamples); - query.loadIdx(opt.indexDir); - uint64_t eqCount = query.parentbv.size()-1; + uint64_t numWrds = (uint64_t) std::ceil((double) opt.numSamples / 64.0); + MSFQuery msfQuery(opt.numSamples); + msfQuery.loadIdx(opt.indexDir); + uint64_t eqCount = msfQuery.parentbv.size() - 1; std::cerr << "total # of equivalence classes is : " << eqCount << "\n"; if (selected == mode::validate) { @@ -169,7 +216,7 @@ int main(int argc, char *argv[]) { loadEqs(opt.eqlistfile, bvs); uint64_t cntr{0}; for (uint64_t idx = 0; idx < eqCount; idx++) { - std::vector newEq = query.buildColor(idx); + std::vector newEq = msfQuery.buildColor(idx); std::vector oldEq(numWrds); buildColor(bvs, oldEq, idx, opt.numSamples); if (newEq != oldEq) { @@ -189,20 +236,43 @@ int main(int argc, char *argv[]) { std::cerr << "WOOOOW! Validation passed\n"; } else if (selected == mode::decodeAllEqs) { for (uint64_t idx = 0; idx < eqCount; idx++) { - std::vector newEq = query.buildColor(idx); + std::vector newEq = msfQuery.buildColor(idx); if (idx % 10000000 == 0) { std::cerr << idx << " eqs decoded\n"; } } } else if (selected == mode::steps) { - uint32_t zero = query.parentbv.size()-1; + uint32_t zero = msfQuery.parentbv.size() - 1; std::vector steps(eqCount, 0); for (uint64_t idx = 0; idx < eqCount; idx++) { - std::cout << query.parentbv[idx] << "\t" + std::cout << msfQuery.parentbv[idx] << "\t" << idx << "\t" - << recursiveSteps(idx, query.parentbv, steps, zero) << "\n"; + << recursiveSteps(idx, msfQuery.parentbv, steps, zero) << "\n"; } } else if (selected == mode::query) { - + CQF dbg(opt.cqffile, false); + std::cerr << "Done loading cqf.\n"; + uint32_t seed = 2038074743; + uint64_t total_kmers = 0; + // loading kmers + mantis::QuerySets multi_kmers = Kmer::parse_kmers(opt.queryfile.c_str(), + seed, + dbg.range(), + 20, + total_kmers); + std::cerr << "Done loading query file : # of seqs: " << multi_kmers.size() << "\n"; + std::ofstream opfile(opt.outputfile); + uint32_t cnt = 0; + { + CLI::AutoTimer timer{"Query time ", CLI::Timer::Big}; + for (auto &kmers : multi_kmers) { + opfile << "seq" << cnt++ << '\t' << kmers.size() << '\n'; + mantis::QueryResult result = findSamples(kmers, dbg, msfQuery); + for (auto it = result.begin(); it != result.end(); ++it) { + opfile << it->first/*cdbg.get_sample(it->first)*/ << '\t' << it->second << '\n'; + } + } + opfile.close(); + } } } \ No newline at end of file From 94a398f7e156ed6215a28bd6ea7904d07db82e6a Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 24 Jul 2018 19:16:07 -0400 Subject: [PATCH 040/122] gathering some stats along the way of decoding. Also using a cache to store some hot eq. classes in the simplest way of it. --- src/validateMSF.cc | 174 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 142 insertions(+), 32 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index 4544c41..e647109 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -4,14 +4,27 @@ #include #include +#include #include "MSF.h" #include "cqf.h" #include "common_types.h" #include "CLI/CLI.hpp" #include "CLI/Timer.hpp" #include "kmer.h" +#include "lrucache.hpp" + +struct QueryStats { + uint32_t cnt = 0, cacheCntr = 0, noCacheCntr{0}; + uint64_t totSel{0}; + std::chrono::duration selectTime{0}; + std::chrono::duration flipTime{0}; + uint64_t totEqcls{0}; + uint64_t rootedNonZero{0}; + std::unordered_map numOcc; +}; class MSFQuery { + private: uint64_t numSamples; uint64_t numWrds; @@ -39,34 +52,85 @@ class MSFQuery { << "--> boundary size: " << bbv.size() << "\n"; } - std::vector buildColor(uint64_t eqid, bool all=true) { + std::vector buildColor(uint64_t eqid, QueryStats &queryStats, + cache::lru_cache > *lru_cache, + bool all = true) { + std::vector flips(numSamples); + std::vector xorflips(numSamples, 0); uint64_t i{eqid}, from{0}, to{0}; + std::vector froms; + froms.reserve(12000); + queryStats.totEqcls++; + auto sstart = std::chrono::system_clock::now(); while (parentbv[i] != i) { + if (lru_cache->exists(i)) { + const auto &vs = lru_cache->get(i); + for (auto v : vs) { + xorflips[v] = 1; + } + queryStats.cacheCntr++; + break; + } if (i > 0) from = sbbv(i) + 1; else from = 0; - to = sbbv(i + 1); - for (auto j = from; j <= to; j++) { - flips[deltabv[j]] ^= 0x01; - } + froms.push_back(from); + queryStats.numOcc[i]++; i = parentbv[i]; + ++queryStats.totSel; } if (i != zero) { if (i > 0) from = sbbv(i) + 1; - to = sbbv(i + 1); - for (auto j = from; j <= to; j++) { - flips[deltabv[j]] ^= 0x01; - } + else + from = 0; + froms.push_back(from); + ++queryStats.totSel; + queryStats.rootedNonZero++; } + queryStats.selectTime += std::chrono::system_clock::now() - sstart; + auto fstart = std::chrono::system_clock::now(); + for (auto f : froms) { + bool found = false; + uint64_t wrd{0}; + //auto j = f; + auto start = f; + do { + wrd = bbv.get_int(start, 64); + //j = 0; + for (uint64_t j = 0; j < 64; j++) { + flips[deltabv[start + j]] ^= 0x01; + //j++; + if ((wrd >> j) & 0x01) { + found = true; + break; + } + } + start += 64; + } while (!found/*bbv[j - 1] != 1*/); + } + queryStats.flipTime += std::chrono::system_clock::now() - fstart; + /*while (parentbv[i] != i) { + if (i > 0) + from = sbbv(i) + 1; + else + from = 0; + auto j = from; + do { + flips[deltabv[j]] ^= 0x01; + j++; + } while (bbv[j-1] != 1); + i = parentbv[i]; + }*/ + if (!all) { // return the indices of set bits std::vector eq; eq.reserve(numWrds); uint64_t one = 1; for (i = 0; i < numSamples; i++) { - if (flips[i]) { + if (flips[i] ^ xorflips[i]) { eq.push_back(i); } } @@ -85,8 +149,10 @@ class MSFQuery { }; -mantis::QueryResult findSamples(const mantis::QuerySet& kmers, - CQF& dbg, MSFQuery& msfQuery) { +mantis::QueryResult findSamples(const mantis::QuerySet &kmers, + CQF &dbg, MSFQuery &msfQuery, + cache::lru_cache > &lru_cache, + QueryStats &queryStats) { std::unordered_map query_eqclass_map; for (auto k : kmers) { KeyObject key(k, 0, 0); @@ -98,9 +164,18 @@ mantis::QueryResult findSamples(const mantis::QuerySet& kmers, std::unordered_map sample_map; for (auto it = query_eqclass_map.begin(); it != query_eqclass_map.end(); ++it) { - auto eqclass_id = it->first-1; + auto eqclass_id = it->first - 1; auto count = it->second; - auto setbits = msfQuery.buildColor(eqclass_id, false); + std::vector setbits; + if (lru_cache.exists(eqclass_id)) { + setbits = lru_cache.get(eqclass_id); + queryStats.cacheCntr++; + } else { + std::vector setbits = msfQuery.buildColor(eqclass_id, + queryStats, &lru_cache, false); + lru_cache.put(eqclass_id, setbits); + queryStats.noCacheCntr++; + } for (auto sb : setbits) { sample_map[sb] += count; } @@ -108,18 +183,23 @@ mantis::QueryResult findSamples(const mantis::QuerySet& kmers, return sample_map; } -uint32_t recursiveSteps(uint32_t idx, sdsl::int_vector<> &parentbv, - std::vector &steps, - uint32_t zero) { - if (idx == zero) +std::pair recursiveSteps(uint32_t idx, sdsl::int_vector<> &parentbv, + std::vector> &steps, + uint32_t zero) { + if (idx == zero) { + steps[idx].second = zero; return steps[idx]; // 0 - if (steps[idx] != 0) + } + if (steps[idx].first != 0) return steps[idx]; if (parentbv[idx] == idx) { - steps[idx] = 1; // to retrieve the representative + steps[idx].first = 1; // to retrieve the representative + steps[idx].second = idx; return steps[idx]; } - steps[idx] = recursiveSteps(parentbv[idx], parentbv, steps, zero) + 1; + auto ret = recursiveSteps(parentbv[idx], parentbv, steps, zero); + steps[idx].first = ret.first + 1; + steps[idx].second = ret.second; return steps[idx]; } @@ -211,12 +291,15 @@ int main(int argc, char *argv[]) { uint64_t eqCount = msfQuery.parentbv.size() - 1; std::cerr << "total # of equivalence classes is : " << eqCount << "\n"; + cache::lru_cache > cache_lru(100000); + QueryStats queryStats; + if (selected == mode::validate) { eqvec bvs; loadEqs(opt.eqlistfile, bvs); uint64_t cntr{0}; for (uint64_t idx = 0; idx < eqCount; idx++) { - std::vector newEq = msfQuery.buildColor(idx); + std::vector newEq = msfQuery.buildColor(idx, queryStats, &cache_lru); std::vector oldEq(numWrds); buildColor(bvs, oldEq, idx, opt.numSamples); if (newEq != oldEq) { @@ -235,19 +318,36 @@ int main(int argc, char *argv[]) { } std::cerr << "WOOOOW! Validation passed\n"; } else if (selected == mode::decodeAllEqs) { - for (uint64_t idx = 0; idx < eqCount; idx++) { - std::vector newEq = msfQuery.buildColor(idx); - if (idx % 10000000 == 0) { + std::random_device r; + + // Choose a random mean between 1 and 6 + std::default_random_engine e1(r()); + std::uniform_int_distribution uniform_dist(0, eqCount - 1); + for (uint64_t idx = 0; idx < 182169; idx++) { + std::vector newEq = msfQuery.buildColor(uniform_dist(e1), + queryStats, + & + false); + /*if (idx % 10000000 == 0) { std::cerr << idx << " eqs decoded\n"; - } + }*/ } + std::cerr << "cache was used " << queryStats.cacheCntr << " times " << queryStats.noCacheCntr << "\n"; + std::cerr << "select time was " << queryStats.selectTime.count() << "s, flip time was " + << queryStats.flipTime.count() << '\n'; + std::cerr << "total selects = " << queryStats.totSel << ", time per select = " + << queryStats.selectTime.count() / queryStats.totSel << '\n'; + std::cerr << "total # of queries = " << queryStats.totEqcls + << ", total # of queries rooted at a non-zero node = " << queryStats.rootedNonZero << "\n"; + } else if (selected == mode::steps) { uint32_t zero = msfQuery.parentbv.size() - 1; - std::vector steps(eqCount, 0); + std::vector> steps(eqCount, {0, 0}); for (uint64_t idx = 0; idx < eqCount; idx++) { + auto row = recursiveSteps(idx, msfQuery.parentbv, steps, zero); std::cout << msfQuery.parentbv[idx] << "\t" << idx << "\t" - << recursiveSteps(idx, msfQuery.parentbv, steps, zero) << "\n"; + << row.first << "\t" << row.second << "\n"; } } else if (selected == mode::query) { CQF dbg(opt.cqffile, false); @@ -262,17 +362,27 @@ int main(int argc, char *argv[]) { total_kmers); std::cerr << "Done loading query file : # of seqs: " << multi_kmers.size() << "\n"; std::ofstream opfile(opt.outputfile); - uint32_t cnt = 0; { - CLI::AutoTimer timer{"Query time ", CLI::Timer::Big}; + //CLI::AutoTimer timer{"Query time ", CLI::Timer::Big}; for (auto &kmers : multi_kmers) { - opfile << "seq" << cnt++ << '\t' << kmers.size() << '\n'; - mantis::QueryResult result = findSamples(kmers, dbg, msfQuery); + opfile << "seq" << queryStats.cnt++ << '\t' << kmers.size() << '\n'; + mantis::QueryResult result = findSamples(kmers, dbg, msfQuery, cache_lru, + queryStats); for (auto it = result.begin(); it != result.end(); ++it) { opfile << it->first/*cdbg.get_sample(it->first)*/ << '\t' << it->second << '\n'; } } opfile.close(); } + std::cerr << "cache was used " << queryStats.cacheCntr << " times " << queryStats.noCacheCntr << "\n"; + std::cerr << "select time was " << queryStats.selectTime.count() << "s, flip time was " + << queryStats.flipTime.count() << '\n'; + std::cerr << "total selects = " << queryStats.totSel << ", time per select = " + << queryStats.selectTime.count() / queryStats.totSel << '\n'; + std::cerr << "total # of queries = " << queryStats.totEqcls + << ", total # of queries rooted at a non-zero node = " << queryStats.rootedNonZero << "\n"; + for (auto &kv : queryStats.numOcc) { + std::cout << kv.first << '\t' << kv.second << '\n'; + } } } \ No newline at end of file From 5cc7ffd4c7d8f84857be337ed92c96f694de88dd Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 24 Jul 2018 19:19:52 -0400 Subject: [PATCH 041/122] minor change --- src/validateMSF.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index e647109..677afb3 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -326,7 +326,7 @@ int main(int argc, char *argv[]) { for (uint64_t idx = 0; idx < 182169; idx++) { std::vector newEq = msfQuery.buildColor(uniform_dist(e1), queryStats, - & + &cache_lru, false); /*if (idx % 10000000 == 0) { std::cerr << idx << " eqs decoded\n"; @@ -363,7 +363,7 @@ int main(int argc, char *argv[]) { std::cerr << "Done loading query file : # of seqs: " << multi_kmers.size() << "\n"; std::ofstream opfile(opt.outputfile); { - //CLI::AutoTimer timer{"Query time ", CLI::Timer::Big}; + CLI::AutoTimer timer{"Query time ", CLI::Timer::Big}; for (auto &kmers : multi_kmers) { opfile << "seq" << queryStats.cnt++ << '\t' << kmers.size() << '\n'; mantis::QueryResult result = findSamples(kmers, dbg, msfQuery, cache_lru, @@ -381,8 +381,8 @@ int main(int argc, char *argv[]) { << queryStats.selectTime.count() / queryStats.totSel << '\n'; std::cerr << "total # of queries = " << queryStats.totEqcls << ", total # of queries rooted at a non-zero node = " << queryStats.rootedNonZero << "\n"; - for (auto &kv : queryStats.numOcc) { + /*for (auto &kv : queryStats.numOcc) { std::cout << kv.first << '\t' << kv.second << '\n'; - } + }*/ } } \ No newline at end of file From 1c5f17be8dc4c6adc20e2ce0fbbac08c6b8b5bd0 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 24 Jul 2018 20:58:23 -0400 Subject: [PATCH 042/122] Adding the lruCache library --- include/lrucache.hpp | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 include/lrucache.hpp diff --git a/include/lrucache.hpp b/include/lrucache.hpp new file mode 100644 index 0000000..9e5e285 --- /dev/null +++ b/include/lrucache.hpp @@ -0,0 +1,72 @@ +/* + * File: lrucache.hpp + * Author: Alexander Ponomarev + * + * Created on June 20, 2013, 5:09 PM + */ + +#ifndef _LRUCACHE_HPP_INCLUDED_ +#define _LRUCACHE_HPP_INCLUDED_ + +#include +#include +#include +#include + +namespace cache { + +template +class lru_cache { +public: + typedef typename std::pair key_value_pair_t; + typedef typename std::list::iterator list_iterator_t; + + lru_cache(size_t max_size) : + _max_size(max_size) { + } + + void put(const key_t& key, const value_t& value) { + auto it = _cache_items_map.find(key); + _cache_items_list.push_front(key_value_pair_t(key, value)); + if (it != _cache_items_map.end()) { + _cache_items_list.erase(it->second); + _cache_items_map.erase(it); + } + _cache_items_map[key] = _cache_items_list.begin(); + + if (_cache_items_map.size() > _max_size) { + auto last = _cache_items_list.end(); + last--; + _cache_items_map.erase(last->first); + _cache_items_list.pop_back(); + } + } + + const value_t& get(const key_t& key) { + auto it = _cache_items_map.find(key); + if (it == _cache_items_map.end()) { + throw std::range_error("There is no such key in cache"); + } else { + _cache_items_list.splice(_cache_items_list.begin(), _cache_items_list, it->second); + return it->second->second; + } + } + + bool exists(const key_t& key) const { + return _cache_items_map.find(key) != _cache_items_map.end(); + } + + size_t size() const { + return _cache_items_map.size(); + } + +private: + std::list _cache_items_list; + std::unordered_map _cache_items_map; + size_t _max_size; +}; + +} // namespace cache + +#endif /* _LRUCACHE_HPP_INCLUDED_ */ + From 6dfaa8ffe5dae949eed2ef7c7824c5aa8428ee3b Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 25 Jul 2018 00:48:54 -0400 Subject: [PATCH 043/122] should not be fast --- src/validateMSF.cc | 106 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 12 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index 677afb3..43baedb 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -20,9 +20,35 @@ struct QueryStats { std::chrono::duration flipTime{0}; uint64_t totEqcls{0}; uint64_t rootedNonZero{0}; + uint64_t nextCacheUpdate{10000}; + uint64_t globalQueryNum{0}; std::unordered_map numOcc; }; +class RankScores { +public: + RankScores(uint64_t nranks) {rs_.resize(nranks);} + + std::unordered_map& operator[](uint32_t r) { + if (r > maxRank_) { + maxRank_ = std::min(r, static_cast(rs_.size()-1)); + } + return (r < rs_.size()) ? rs_[r] : rs_.back(); + } + + void clear() { + for (auto& m : rs_){ m.clear(); } + maxRank_ = 0; + } + + uint32_t maxRank() const { return maxRank_; } + +private: + std::vector> rs_; + uint32_t maxRank_{0}; +}; + + class MSFQuery { private: @@ -54,22 +80,28 @@ class MSFQuery { std::vector buildColor(uint64_t eqid, QueryStats &queryStats, cache::lru_cache > *lru_cache, + RankScores* rs, bool all = true) { std::vector flips(numSamples); std::vector xorflips(numSamples, 0); uint64_t i{eqid}, from{0}, to{0}; + uint64_t height{0}; std::vector froms; + std::vector parents; froms.reserve(12000); + parents.reserve(12000); queryStats.totEqcls++; auto sstart = std::chrono::system_clock::now(); - while (parentbv[i] != i) { - if (lru_cache->exists(i)) { + uint32_t iparent = parentbv[i]; + while (iparent != i) { + if (lru_cache and lru_cache->exists(i)) { const auto &vs = lru_cache->get(i); for (auto v : vs) { xorflips[v] = 1; } queryStats.cacheCntr++; + i = iparent; break; } if (i > 0) @@ -77,19 +109,31 @@ class MSFQuery { else from = 0; froms.push_back(from); - queryStats.numOcc[i]++; - i = parentbv[i]; + parents.push_back(i); + //queryStats.numOcc[i]++; + i = iparent; + iparent = parentbv[i]; ++queryStats.totSel; + ++height; } - if (i != zero) { + if (i != iparent and i != zero) { if (i > 0) from = sbbv(i) + 1; else from = 0; froms.push_back(from); + parents.push_back(i); ++queryStats.totSel; queryStats.rootedNonZero++; + ++height; } + // update ranks + if (rs) { + for (size_t idx = 0; idx < froms.size(); ++idx) { + (*rs)[(height - idx)][parents[idx]]++; + } + } + queryStats.selectTime += std::chrono::system_clock::now() - sstart; auto fstart = std::chrono::system_clock::now(); for (auto f : froms) { @@ -152,6 +196,7 @@ class MSFQuery { mantis::QueryResult findSamples(const mantis::QuerySet &kmers, CQF &dbg, MSFQuery &msfQuery, cache::lru_cache > &lru_cache, + RankScores& rs, QueryStats &queryStats) { std::unordered_map query_eqclass_map; for (auto k : kmers) { @@ -162,23 +207,57 @@ mantis::QueryResult findSamples(const mantis::QuerySet &kmers, } std::unordered_map sample_map; - for (auto it = query_eqclass_map.begin(); it != query_eqclass_map.end(); - ++it) { + size_t numPerLevel = 10; + for (auto it = query_eqclass_map.begin(); it != query_eqclass_map.end(); ++it) { auto eqclass_id = it->first - 1; auto count = it->second; + std::vector setbits; if (lru_cache.exists(eqclass_id)) { setbits = lru_cache.get(eqclass_id); queryStats.cacheCntr++; } else { - std::vector setbits = msfQuery.buildColor(eqclass_id, - queryStats, &lru_cache, false); + setbits = msfQuery.buildColor(eqclass_id, queryStats, &lru_cache, &rs, false); lru_cache.put(eqclass_id, setbits); queryStats.noCacheCntr++; } for (auto sb : setbits) { sample_map[sb] += count; } + + ++queryStats.globalQueryNum; + /* + if (queryStats.globalQueryNum > queryStats.nextCacheUpdate) { + for (int64_t i = rs.maxRank(); i > 50; i-=50) { + auto& m = rs[i]; + if (m.size() > numPerLevel) { + std::vector> pairs; + pairs.reserve(m.size()); + std::copy(m.begin(), m.end(), std::back_inserter(pairs)); + std::nth_element(pairs.begin(), pairs.begin()+numPerLevel, pairs.end(), + [](const std::pair &a, const std::pair &b) { + return a.second > b.second; + }); + for (auto pit = pairs.begin(); pit != pairs.begin()+numPerLevel; ++pit) { + if(!lru_cache.exists(pit->first)) { + auto v = msfQuery.buildColor(pit->first, queryStats, nullptr, nullptr, false); + lru_cache.put(pit->first, v); + } + } + } else if(m.size() > 0){ + for (auto pit = m.begin(); pit != m.end(); ++pit) { + if(!lru_cache.exists(pit->first)) { + auto v = msfQuery.buildColor(pit->first, queryStats, nullptr, nullptr, false); + lru_cache.put(pit->first, v); + } + } + } + + } + queryStats.nextCacheUpdate += 10000; + rs.clear(); + } + */ } return sample_map; } @@ -299,7 +378,7 @@ int main(int argc, char *argv[]) { loadEqs(opt.eqlistfile, bvs); uint64_t cntr{0}; for (uint64_t idx = 0; idx < eqCount; idx++) { - std::vector newEq = msfQuery.buildColor(idx, queryStats, &cache_lru); + std::vector newEq = msfQuery.buildColor(idx, queryStats, &cache_lru, nullptr); std::vector oldEq(numWrds); buildColor(bvs, oldEq, idx, opt.numSamples); if (newEq != oldEq) { @@ -327,6 +406,7 @@ int main(int argc, char *argv[]) { std::vector newEq = msfQuery.buildColor(uniform_dist(e1), queryStats, &cache_lru, + nullptr, false); /*if (idx % 10000000 == 0) { std::cerr << idx << " eqs decoded\n"; @@ -361,13 +441,15 @@ int main(int argc, char *argv[]) { 20, total_kmers); std::cerr << "Done loading query file : # of seqs: " << multi_kmers.size() << "\n"; + RankScores rs(12000); + std::cerr << "here\n"; std::ofstream opfile(opt.outputfile); { CLI::AutoTimer timer{"Query time ", CLI::Timer::Big}; for (auto &kmers : multi_kmers) { opfile << "seq" << queryStats.cnt++ << '\t' << kmers.size() << '\n'; mantis::QueryResult result = findSamples(kmers, dbg, msfQuery, cache_lru, - queryStats); + rs, queryStats); for (auto it = result.begin(); it != result.end(); ++it) { opfile << it->first/*cdbg.get_sample(it->first)*/ << '\t' << it->second << '\n'; } @@ -385,4 +467,4 @@ int main(int argc, char *argv[]) { std::cout << kv.first << '\t' << kv.second << '\n'; }*/ } -} \ No newline at end of file +} From b10ea398e0d3880e82f88052ede7f15366bbe8aa Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 25 Jul 2018 16:08:32 -0400 Subject: [PATCH 044/122] update --- src/validateMSF.cc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index 43baedb..07864f6 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -93,6 +93,7 @@ class MSFQuery { parents.reserve(12000); queryStats.totEqcls++; auto sstart = std::chrono::system_clock::now(); + bool foundCache = false; uint32_t iparent = parentbv[i]; while (iparent != i) { if (lru_cache and lru_cache->exists(i)) { @@ -101,7 +102,8 @@ class MSFQuery { xorflips[v] = 1; } queryStats.cacheCntr++; - i = iparent; + //i = iparent; + foundCache = true; break; } if (i > 0) @@ -116,7 +118,7 @@ class MSFQuery { ++queryStats.totSel; ++height; } - if (i != iparent and i != zero) { + if (!foundCache and i == iparent and i != zero) { if (i > 0) from = sbbv(i) + 1; else @@ -226,7 +228,7 @@ mantis::QueryResult findSamples(const mantis::QuerySet &kmers, } ++queryStats.globalQueryNum; - /* + if (queryStats.globalQueryNum > queryStats.nextCacheUpdate) { for (int64_t i = rs.maxRank(); i > 50; i-=50) { auto& m = rs[i]; @@ -257,7 +259,6 @@ mantis::QueryResult findSamples(const mantis::QuerySet &kmers, queryStats.nextCacheUpdate += 10000; rs.clear(); } - */ } return sample_map; } From 56c4ab148eff5d762fb67cc322a29209b4d4493f Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 25 Jul 2018 16:48:49 -0400 Subject: [PATCH 045/122] working --- src/validateMSF.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index 07864f6..a2b0df3 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -228,7 +228,7 @@ mantis::QueryResult findSamples(const mantis::QuerySet &kmers, } ++queryStats.globalQueryNum; - + /* if (queryStats.globalQueryNum > queryStats.nextCacheUpdate) { for (int64_t i = rs.maxRank(); i > 50; i-=50) { auto& m = rs[i]; @@ -258,8 +258,9 @@ mantis::QueryResult findSamples(const mantis::QuerySet &kmers, } queryStats.nextCacheUpdate += 10000; rs.clear(); - } + }*/ } + return sample_map; } From e0f4301fae886802e1cc0350dc8650aef94491f0 Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 25 Jul 2018 18:21:07 -0400 Subject: [PATCH 046/122] fast simple version --- include/lrucache.hpp | 8 +- include/tsl/bhopscotch_map.h | 675 +++++++++ include/tsl/bhopscotch_set.h | 529 ++++++++ include/tsl/hopscotch_growth_policy.h | 295 ++++ include/tsl/hopscotch_hash.h | 1801 +++++++++++++++++++++++++ include/tsl/hopscotch_map.h | 679 ++++++++++ include/tsl/hopscotch_set.h | 525 +++++++ src/validateMSF.cc | 36 +- 8 files changed, 4535 insertions(+), 13 deletions(-) create mode 100644 include/tsl/bhopscotch_map.h create mode 100644 include/tsl/bhopscotch_set.h create mode 100644 include/tsl/hopscotch_growth_policy.h create mode 100644 include/tsl/hopscotch_hash.h create mode 100644 include/tsl/hopscotch_map.h create mode 100644 include/tsl/hopscotch_set.h diff --git a/include/lrucache.hpp b/include/lrucache.hpp index 9e5e285..818d303 100644 --- a/include/lrucache.hpp +++ b/include/lrucache.hpp @@ -8,14 +8,15 @@ #ifndef _LRUCACHE_HPP_INCLUDED_ #define _LRUCACHE_HPP_INCLUDED_ -#include +//#include +#include "tsl/hopscotch_map.h" #include #include #include namespace cache { -template + template> class lru_cache { public: typedef typename std::pair key_value_pair_t; @@ -62,7 +63,8 @@ class lru_cache { private: std::list _cache_items_list; - std::unordered_map _cache_items_map; + //std::unordered_map _cache_items_map; + tsl::hopscotch_map _cache_items_map; size_t _max_size; }; diff --git a/include/tsl/bhopscotch_map.h b/include/tsl/bhopscotch_map.h new file mode 100644 index 0000000..e5bcbf3 --- /dev/null +++ b/include/tsl/bhopscotch_map.h @@ -0,0 +1,675 @@ +/** + * MIT License + * + * Copyright (c) 2017 Tessil + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef TSL_BHOPSCOTCH_MAP_H +#define TSL_BHOPSCOTCH_MAP_H + + +#include +#include +#include +#include +#include +#include +#include +#include +#include "hopscotch_hash.h" + + +namespace tsl { + + +/** + * Similar to tsl::hopscotch_map but instead of using a list for overflowing elements it uses + * a binary search tree. It thus needs an additional template parameter Compare. Compare should + * be arithmetically coherent with KeyEqual. + * + * The binary search tree allows the map to have a worst-case scenario of O(log n) for search + * and delete, even if the hash function maps all the elements to the same bucket. + * For insert, the amortized worst case is O(log n), but the worst case is O(n) in case of rehash. + * + * This makes the map resistant to DoS attacks (but doesn't preclude you to have a good hash function, + * as an element in the bucket array is faster to retrieve than in the tree). + * + * @copydoc hopscotch_map + */ +template, + class KeyEqual = std::equal_to, + class Compare = std::less, + class Allocator = std::allocator>, + unsigned int NeighborhoodSize = 62, + bool StoreHash = false, + class GrowthPolicy = tsl::hh::power_of_two_growth_policy<2>> +class bhopscotch_map { +private: + template + using has_is_transparent = tsl::detail_hopscotch_hash::has_is_transparent; + + class KeySelect { + public: + using key_type = Key; + + const key_type& operator()(const std::pair& key_value) const { + return key_value.first; + } + + const key_type& operator()(std::pair& key_value) { + return key_value.first; + } + }; + + class ValueSelect { + public: + using value_type = T; + + const value_type& operator()(const std::pair& key_value) const { + return key_value.second; + } + + value_type& operator()(std::pair& key_value) { + return key_value.second; + } + }; + + + // TODO Not optimal as we have to use std::pair as ValueType which forbid + // us to move the key in the bucket array, we have to use copy. Optimize. + using overflow_container_type = std::map; + using ht = detail_hopscotch_hash::hopscotch_hash, KeySelect, ValueSelect, + Hash, KeyEqual, + Allocator, NeighborhoodSize, + StoreHash, GrowthPolicy, + overflow_container_type>; + +public: + using key_type = typename ht::key_type; + using mapped_type = T; + using value_type = typename ht::value_type; + using size_type = typename ht::size_type; + using difference_type = typename ht::difference_type; + using hasher = typename ht::hasher; + using key_equal = typename ht::key_equal; + using key_compare = Compare; + using allocator_type = typename ht::allocator_type; + using reference = typename ht::reference; + using const_reference = typename ht::const_reference; + using pointer = typename ht::pointer; + using const_pointer = typename ht::const_pointer; + using iterator = typename ht::iterator; + using const_iterator = typename ht::const_iterator; + + + /* + * Constructors + */ + bhopscotch_map() : bhopscotch_map(ht::DEFAULT_INIT_BUCKETS_SIZE) { + } + + explicit bhopscotch_map(size_type bucket_count, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator(), + const Compare& comp = Compare()) : + m_ht(bucket_count, hash, equal, alloc, ht::DEFAULT_MAX_LOAD_FACTOR, comp) + { + } + + bhopscotch_map(size_type bucket_count, + const Allocator& alloc) : bhopscotch_map(bucket_count, Hash(), KeyEqual(), alloc) + { + } + + bhopscotch_map(size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : bhopscotch_map(bucket_count, hash, KeyEqual(), alloc) + { + } + + explicit bhopscotch_map(const Allocator& alloc) : bhopscotch_map(ht::DEFAULT_INIT_BUCKETS_SIZE, alloc) { + } + + template + bhopscotch_map(InputIt first, InputIt last, + size_type bucket_count = ht::DEFAULT_INIT_BUCKETS_SIZE, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator()) : bhopscotch_map(bucket_count, hash, equal, alloc) + { + insert(first, last); + } + + template + bhopscotch_map(InputIt first, InputIt last, + size_type bucket_count, + const Allocator& alloc) : bhopscotch_map(first, last, bucket_count, Hash(), KeyEqual(), alloc) + { + } + + template + bhopscotch_map(InputIt first, InputIt last, + size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : bhopscotch_map(first, last, bucket_count, hash, KeyEqual(), alloc) + { + } + + bhopscotch_map(std::initializer_list init, + size_type bucket_count = ht::DEFAULT_INIT_BUCKETS_SIZE, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator()) : + bhopscotch_map(init.begin(), init.end(), bucket_count, hash, equal, alloc) + { + } + + bhopscotch_map(std::initializer_list init, + size_type bucket_count, + const Allocator& alloc) : + bhopscotch_map(init.begin(), init.end(), bucket_count, Hash(), KeyEqual(), alloc) + { + } + + bhopscotch_map(std::initializer_list init, + size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : + bhopscotch_map(init.begin(), init.end(), bucket_count, hash, KeyEqual(), alloc) + { + } + + + bhopscotch_map& operator=(std::initializer_list ilist) { + m_ht.clear(); + + m_ht.reserve(ilist.size()); + m_ht.insert(ilist.begin(), ilist.end()); + + return *this; + } + + allocator_type get_allocator() const { return m_ht.get_allocator(); } + + + /* + * Iterators + */ + iterator begin() noexcept { return m_ht.begin(); } + const_iterator begin() const noexcept { return m_ht.begin(); } + const_iterator cbegin() const noexcept { return m_ht.cbegin(); } + + iterator end() noexcept { return m_ht.end(); } + const_iterator end() const noexcept { return m_ht.end(); } + const_iterator cend() const noexcept { return m_ht.cend(); } + + + /* + * Capacity + */ + bool empty() const noexcept { return m_ht.empty(); } + size_type size() const noexcept { return m_ht.size(); } + size_type max_size() const noexcept { return m_ht.max_size(); } + + /* + * Modifiers + */ + void clear() noexcept { m_ht.clear(); } + + + + + std::pair insert(const value_type& value) { + return m_ht.insert(value); + } + + template::value>::type* = nullptr> + std::pair insert(P&& value) { + return m_ht.insert(std::forward

(value)); + } + + std::pair insert(value_type&& value) { + return m_ht.insert(std::move(value)); + } + + + iterator insert(const_iterator hint, const value_type& value) { + return m_ht.insert(hint, value); + } + + template::value>::type* = nullptr> + iterator insert(const_iterator hint, P&& value) { + return m_ht.insert(hint, std::forward

(value)); + } + + iterator insert(const_iterator hint, value_type&& value) { + return m_ht.insert(hint, std::move(value)); + } + + + template + void insert(InputIt first, InputIt last) { + m_ht.insert(first, last); + } + + void insert(std::initializer_list ilist) { + m_ht.insert(ilist.begin(), ilist.end()); + } + + + + + template + std::pair insert_or_assign(const key_type& k, M&& obj) { + return m_ht.insert_or_assign(k, std::forward(obj)); + } + + template + std::pair insert_or_assign(key_type&& k, M&& obj) { + return m_ht.insert_or_assign(std::move(k), std::forward(obj)); + } + + template + iterator insert_or_assign(const_iterator hint, const key_type& k, M&& obj) { + return m_ht.insert_or_assign(hint, k, std::forward(obj)); + } + + template + iterator insert_or_assign(const_iterator hint, key_type&& k, M&& obj) { + return m_ht.insert_or_assign(hint, std::move(k), std::forward(obj)); + } + + + + /** + * Due to the way elements are stored, emplace will need to move or copy the key-value once. + * The method is equivalent to insert(value_type(std::forward(args)...)); + * + * Mainly here for compatibility with the std::unordered_map interface. + */ + template + std::pair emplace(Args&&... args) { + return m_ht.emplace(std::forward(args)...); + } + + + + + /** + * Due to the way elements are stored, emplace_hint will need to move or copy the key-value once. + * The method is equivalent to insert(hint, value_type(std::forward(args)...)); + * + * Mainly here for compatibility with the std::unordered_map interface. + */ + template + iterator emplace_hint(const_iterator hint, Args&&... args) { + return m_ht.emplace_hint(hint, std::forward(args)...); + } + + + + + template + std::pair try_emplace(const key_type& k, Args&&... args) { + return m_ht.try_emplace(k, std::forward(args)...); + } + + template + std::pair try_emplace(key_type&& k, Args&&... args) { + return m_ht.try_emplace(std::move(k), std::forward(args)...); + } + + template + iterator try_emplace(const_iterator hint, const key_type& k, Args&&... args) { + return m_ht.try_emplace(hint, k, std::forward(args)...); + } + + template + iterator try_emplace(const_iterator hint, key_type&& k, Args&&... args) { + return m_ht.try_emplace(hint, std::move(k), std::forward(args)...); + } + + + + + iterator erase(iterator pos) { return m_ht.erase(pos); } + iterator erase(const_iterator pos) { return m_ht.erase(pos); } + iterator erase(const_iterator first, const_iterator last) { return m_ht.erase(first, last); } + size_type erase(const key_type& key) { return m_ht.erase(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup to the value if you already have the hash. + */ + size_type erase(const key_type& key, std::size_t precalculated_hash) { + return m_ht.erase(key, precalculated_hash); + } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent + * and Compare::is_transparent exist. + * If so, K must be hashable and comparable to Key. + */ + template::value && has_is_transparent::value>::type* = nullptr> + size_type erase(const K& key) { return m_ht.erase(key); } + + /** + * @copydoc erase(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup to the value if you already have the hash. + */ + template::value && has_is_transparent::value>::type* = nullptr> + size_type erase(const K& key, std::size_t precalculated_hash) { return m_ht.erase(key, precalculated_hash); } + + + + + void swap(bhopscotch_map& other) { other.m_ht.swap(m_ht); } + + /* + * Lookup + */ + T& at(const Key& key) { return m_ht.at(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + T& at(const Key& key, std::size_t precalculated_hash) { return m_ht.at(key, precalculated_hash); } + + const T& at(const Key& key) const { return m_ht.at(key); } + + /** + * @copydoc at(const Key& key, std::size_t precalculated_hash) + */ + const T& at(const Key& key, std::size_t precalculated_hash) const { return m_ht.at(key, precalculated_hash); } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent + * and Compare::is_transparent exist. + * If so, K must be hashable and comparable to Key. + */ + template::value && has_is_transparent::value>::type* = nullptr> + T& at(const K& key) { return m_ht.at(key); } + + /** + * @copydoc at(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value && has_is_transparent::value>::type* = nullptr> + T& at(const K& key, std::size_t precalculated_hash) { return m_ht.at(key, precalculated_hash); } + + /** + * @copydoc at(const K& key) + */ + template::value && has_is_transparent::value>::type* = nullptr> + const T& at(const K& key) const { return m_ht.at(key); } + + /** + * @copydoc at(const K& key, std::size_t precalculated_hash) + */ + template::value && has_is_transparent::value>::type* = nullptr> + const T& at(const K& key, std::size_t precalculated_hash) const { return m_ht.at(key, precalculated_hash); } + + + + + T& operator[](const Key& key) { return m_ht[key]; } + T& operator[](Key&& key) { return m_ht[std::move(key)]; } + + + + + size_type count(const Key& key) const { return m_ht.count(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + size_type count(const Key& key, std::size_t precalculated_hash) const { return m_ht.count(key, precalculated_hash); } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent + * and Compare::is_transparent exist. + * If so, K must be hashable and comparable to Key. + */ + template::value && has_is_transparent::value>::type* = nullptr> + size_type count(const K& key) const { return m_ht.count(key); } + + /** + * @copydoc count(const K& key) const + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value && has_is_transparent::value>::type* = nullptr> + size_type count(const K& key, std::size_t precalculated_hash) const { return m_ht.count(key, precalculated_hash); } + + + + + iterator find(const Key& key) { return m_ht.find(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + iterator find(const Key& key, std::size_t precalculated_hash) { return m_ht.find(key, precalculated_hash); } + + const_iterator find(const Key& key) const { return m_ht.find(key); } + + /** + * @copydoc find(const Key& key, std::size_t precalculated_hash) + */ + const_iterator find(const Key& key, std::size_t precalculated_hash) const { return m_ht.find(key, precalculated_hash); } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent + * and Compare::is_transparent exist. + * If so, K must be hashable and comparable to Key. + */ + template::value && has_is_transparent::value>::type* = nullptr> + iterator find(const K& key) { return m_ht.find(key); } + + /** + * @copydoc find(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value && has_is_transparent::value>::type* = nullptr> + iterator find(const K& key, std::size_t precalculated_hash) { return m_ht.find(key, precalculated_hash); } + + /** + * @copydoc find(const K& key) + */ + template::value && has_is_transparent::value>::type* = nullptr> + const_iterator find(const K& key) const { return m_ht.find(key); } + + /** + * @copydoc find(const K& key, std::size_t precalculated_hash) + */ + template::value && has_is_transparent::value>::type* = nullptr> + const_iterator find(const K& key, std::size_t precalculated_hash) const { return m_ht.find(key, precalculated_hash); } + + + + + std::pair equal_range(const Key& key) { return m_ht.equal_range(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + std::pair equal_range(const Key& key, std::size_t precalculated_hash) { + return m_ht.equal_range(key, precalculated_hash); + } + + std::pair equal_range(const Key& key) const { return m_ht.equal_range(key); } + + /** + * @copydoc equal_range(const Key& key, std::size_t precalculated_hash) + */ + std::pair equal_range(const Key& key, std::size_t precalculated_hash) const { + return m_ht.equal_range(key, precalculated_hash); + } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent + * and Compare::is_transparent exist. + * If so, K must be hashable and comparable to Key. + */ + template::value && has_is_transparent::value>::type* = nullptr> + std::pair equal_range(const K& key) { return m_ht.equal_range(key); } + + /** + * @copydoc equal_range(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value && has_is_transparent::value>::type* = nullptr> + std::pair equal_range(const K& key, std::size_t precalculated_hash) { + return m_ht.equal_range(key, precalculated_hash); + } + + /** + * @copydoc equal_range(const K& key) + */ + template::value && has_is_transparent::value>::type* = nullptr> + std::pair equal_range(const K& key) const { return m_ht.equal_range(key); } + + /** + * @copydoc equal_range(const K& key, std::size_t precalculated_hash) + */ + template::value && has_is_transparent::value>::type* = nullptr> + std::pair equal_range(const K& key, std::size_t precalculated_hash) const { + return m_ht.equal_range(key, precalculated_hash); + } + + + + + /* + * Bucket interface + */ + size_type bucket_count() const { return m_ht.bucket_count(); } + size_type max_bucket_count() const { return m_ht.max_bucket_count(); } + + + /* + * Hash policy + */ + float load_factor() const { return m_ht.load_factor(); } + float max_load_factor() const { return m_ht.max_load_factor(); } + void max_load_factor(float ml) { m_ht.max_load_factor(ml); } + + void rehash(size_type count_) { m_ht.rehash(count_); } + void reserve(size_type count_) { m_ht.reserve(count_); } + + + /* + * Observers + */ + hasher hash_function() const { return m_ht.hash_function(); } + key_equal key_eq() const { return m_ht.key_eq(); } + key_compare key_comp() const { return m_ht.key_comp(); } + + /* + * Other + */ + + /** + * Convert a const_iterator to an iterator. + */ + iterator mutable_iterator(const_iterator pos) { + return m_ht.mutable_iterator(pos); + } + + size_type overflow_size() const noexcept { return m_ht.overflow_size(); } + + friend bool operator==(const bhopscotch_map& lhs, const bhopscotch_map& rhs) { + if(lhs.size() != rhs.size()) { + return false; + } + + for(const auto& element_lhs : lhs) { + const auto it_element_rhs = rhs.find(element_lhs.first); + if(it_element_rhs == rhs.cend() || element_lhs.second != it_element_rhs->second) { + return false; + } + } + + return true; + } + + friend bool operator!=(const bhopscotch_map& lhs, const bhopscotch_map& rhs) { + return !operator==(lhs, rhs); + } + + friend void swap(bhopscotch_map& lhs, bhopscotch_map& rhs) { + lhs.swap(rhs); + } + + + +private: + ht m_ht; +}; + + +/** + * Same as `tsl::bhopscotch_map`. + */ +template, + class KeyEqual = std::equal_to, + class Compare = std::less, + class Allocator = std::allocator>, + unsigned int NeighborhoodSize = 62, + bool StoreHash = false> +using bhopscotch_pg_map = bhopscotch_map; + +} // end namespace tsl + +#endif diff --git a/include/tsl/bhopscotch_set.h b/include/tsl/bhopscotch_set.h new file mode 100644 index 0000000..86d563c --- /dev/null +++ b/include/tsl/bhopscotch_set.h @@ -0,0 +1,529 @@ +/** + * MIT License + * + * Copyright (c) 2017 Tessil + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef TSL_BHOPSCOTCH_SET_H +#define TSL_BHOPSCOTCH_SET_H + + +#include +#include +#include +#include +#include +#include +#include +#include +#include "hopscotch_hash.h" + + +namespace tsl { + + +/** + * Similar to tsl::hopscotch_set but instead of using a list for overflowing elements it uses + * a binary search tree. It thus needs an additional template parameter Compare. Compare should + * be arithmetically coherent with KeyEqual. + * + * The binary search tree allows the set to have a worst-case scenario of O(log n) for search + * and delete, even if the hash function maps all the elements to the same bucket. + * For insert, the amortized worst case is O(log n), but the worst case is O(n) in case of rehash. + * + * This makes the set resistant to DoS attacks (but doesn't preclude you to have a good hash function, + * as an element in the bucket array is faster to retrieve than in the tree). + * + * @copydoc hopscotch_set + */ +template, + class KeyEqual = std::equal_to, + class Compare = std::less, + class Allocator = std::allocator, + unsigned int NeighborhoodSize = 62, + bool StoreHash = false, + class GrowthPolicy = tsl::hh::power_of_two_growth_policy<2>> +class bhopscotch_set { +private: + template + using has_is_transparent = tsl::detail_hopscotch_hash::has_is_transparent; + + class KeySelect { + public: + using key_type = Key; + + const key_type& operator()(const Key& key) const { + return key; + } + + key_type& operator()(Key& key) { + return key; + } + }; + + + using overflow_container_type = std::set; + using ht = tsl::detail_hopscotch_hash::hopscotch_hash; + +public: + using key_type = typename ht::key_type; + using value_type = typename ht::value_type; + using size_type = typename ht::size_type; + using difference_type = typename ht::difference_type; + using hasher = typename ht::hasher; + using key_equal = typename ht::key_equal; + using key_compare = Compare; + using allocator_type = typename ht::allocator_type; + using reference = typename ht::reference; + using const_reference = typename ht::const_reference; + using pointer = typename ht::pointer; + using const_pointer = typename ht::const_pointer; + using iterator = typename ht::iterator; + using const_iterator = typename ht::const_iterator; + + + /* + * Constructors + */ + bhopscotch_set() : bhopscotch_set(ht::DEFAULT_INIT_BUCKETS_SIZE) { + } + + explicit bhopscotch_set(size_type bucket_count, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator(), + const Compare& comp = Compare()) : + m_ht(bucket_count, hash, equal, alloc, ht::DEFAULT_MAX_LOAD_FACTOR, comp) + { + } + + bhopscotch_set(size_type bucket_count, + const Allocator& alloc) : bhopscotch_set(bucket_count, Hash(), KeyEqual(), alloc) + { + } + + bhopscotch_set(size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : bhopscotch_set(bucket_count, hash, KeyEqual(), alloc) + { + } + + explicit bhopscotch_set(const Allocator& alloc) : bhopscotch_set(ht::DEFAULT_INIT_BUCKETS_SIZE, alloc) { + } + + template + bhopscotch_set(InputIt first, InputIt last, + size_type bucket_count = ht::DEFAULT_INIT_BUCKETS_SIZE, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator()) : bhopscotch_set(bucket_count, hash, equal, alloc) + { + insert(first, last); + } + + template + bhopscotch_set(InputIt first, InputIt last, + size_type bucket_count, + const Allocator& alloc) : bhopscotch_set(first, last, bucket_count, Hash(), KeyEqual(), alloc) + { + } + + template + bhopscotch_set(InputIt first, InputIt last, + size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : bhopscotch_set(first, last, bucket_count, hash, KeyEqual(), alloc) + { + } + + bhopscotch_set(std::initializer_list init, + size_type bucket_count = ht::DEFAULT_INIT_BUCKETS_SIZE, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator()) : + bhopscotch_set(init.begin(), init.end(), bucket_count, hash, equal, alloc) + { + } + + bhopscotch_set(std::initializer_list init, + size_type bucket_count, + const Allocator& alloc) : + bhopscotch_set(init.begin(), init.end(), bucket_count, Hash(), KeyEqual(), alloc) + { + } + + bhopscotch_set(std::initializer_list init, + size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : + bhopscotch_set(init.begin(), init.end(), bucket_count, hash, KeyEqual(), alloc) + { + } + + + bhopscotch_set& operator=(std::initializer_list ilist) { + m_ht.clear(); + + m_ht.reserve(ilist.size()); + m_ht.insert(ilist.begin(), ilist.end()); + + return *this; + } + + allocator_type get_allocator() const { return m_ht.get_allocator(); } + + + /* + * Iterators + */ + iterator begin() noexcept { return m_ht.begin(); } + const_iterator begin() const noexcept { return m_ht.begin(); } + const_iterator cbegin() const noexcept { return m_ht.cbegin(); } + + iterator end() noexcept { return m_ht.end(); } + const_iterator end() const noexcept { return m_ht.end(); } + const_iterator cend() const noexcept { return m_ht.cend(); } + + + /* + * Capacity + */ + bool empty() const noexcept { return m_ht.empty(); } + size_type size() const noexcept { return m_ht.size(); } + size_type max_size() const noexcept { return m_ht.max_size(); } + + /* + * Modifiers + */ + void clear() noexcept { m_ht.clear(); } + + + + + std::pair insert(const value_type& value) { return m_ht.insert(value); } + std::pair insert(value_type&& value) { return m_ht.insert(std::move(value)); } + + iterator insert(const_iterator hint, const value_type& value) { return m_ht.insert(hint, value); } + iterator insert(const_iterator hint, value_type&& value) { return m_ht.insert(hint, std::move(value)); } + + template + void insert(InputIt first, InputIt last) { m_ht.insert(first, last); } + void insert(std::initializer_list ilist) { m_ht.insert(ilist.begin(), ilist.end()); } + + + + + /** + * Due to the way elements are stored, emplace will need to move or copy the key-value once. + * The method is equivalent to insert(value_type(std::forward(args)...)); + * + * Mainly here for compatibility with the std::unordered_map interface. + */ + template + std::pair emplace(Args&&... args) { return m_ht.emplace(std::forward(args)...); } + + + + + /** + * Due to the way elements are stored, emplace_hint will need to move or copy the key-value once. + * The method is equivalent to insert(hint, value_type(std::forward(args)...)); + * + * Mainly here for compatibility with the std::unordered_map interface. + */ + template + iterator emplace_hint(const_iterator hint, Args&&... args) { + return m_ht.emplace_hint(hint, std::forward(args)...); + } + + + + + iterator erase(iterator pos) { return m_ht.erase(pos); } + iterator erase(const_iterator pos) { return m_ht.erase(pos); } + iterator erase(const_iterator first, const_iterator last) { return m_ht.erase(first, last); } + size_type erase(const key_type& key) { return m_ht.erase(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup to the value if you already have the hash. + */ + size_type erase(const key_type& key, std::size_t precalculated_hash) { + return m_ht.erase(key, precalculated_hash); + } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent + * and Compare::is_transparent exist. + * If so, K must be hashable and comparable to Key. + */ + template::value && has_is_transparent::value>::type* = nullptr> + size_type erase(const K& key) { return m_ht.erase(key); } + + /** + * @copydoc erase(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup to the value if you already have the hash. + */ + template::value && has_is_transparent::value>::type* = nullptr> + size_type erase(const K& key, std::size_t precalculated_hash) { return m_ht.erase(key, precalculated_hash); } + + + + + void swap(bhopscotch_set& other) { other.m_ht.swap(m_ht); } + + + /* + * Lookup + */ + size_type count(const Key& key) const { return m_ht.count(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + size_type count(const Key& key, std::size_t precalculated_hash) const { return m_ht.count(key, precalculated_hash); } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent + * and Compare::is_transparent exist. + * If so, K must be hashable and comparable to Key. + */ + template::value && has_is_transparent::value>::type* = nullptr> + size_type count(const K& key) const { return m_ht.count(key); } + + /** + * @copydoc count(const K& key) const + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value && has_is_transparent::value>::type* = nullptr> + size_type count(const K& key, std::size_t precalculated_hash) const { return m_ht.count(key, precalculated_hash); } + + + + + iterator find(const Key& key) { return m_ht.find(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + iterator find(const Key& key, std::size_t precalculated_hash) { return m_ht.find(key, precalculated_hash); } + + const_iterator find(const Key& key) const { return m_ht.find(key); } + + /** + * @copydoc find(const Key& key, std::size_t precalculated_hash) + */ + const_iterator find(const Key& key, std::size_t precalculated_hash) const { return m_ht.find(key, precalculated_hash); } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent + * and Compare::is_transparent exist. + * If so, K must be hashable and comparable to Key. + */ + template::value && has_is_transparent::value>::type* = nullptr> + iterator find(const K& key) { return m_ht.find(key); } + + /** + * @copydoc find(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value && has_is_transparent::value>::type* = nullptr> + iterator find(const K& key, std::size_t precalculated_hash) { return m_ht.find(key, precalculated_hash); } + + /** + * @copydoc find(const K& key) + */ + template::value && has_is_transparent::value>::type* = nullptr> + const_iterator find(const K& key) const { return m_ht.find(key); } + + /** + * @copydoc find(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value && has_is_transparent::value>::type* = nullptr> + const_iterator find(const K& key, std::size_t precalculated_hash) const { return m_ht.find(key, precalculated_hash); } + + + + + std::pair equal_range(const Key& key) { return m_ht.equal_range(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + std::pair equal_range(const Key& key, std::size_t precalculated_hash) { + return m_ht.equal_range(key, precalculated_hash); + } + + std::pair equal_range(const Key& key) const { return m_ht.equal_range(key); } + + /** + * @copydoc equal_range(const Key& key, std::size_t precalculated_hash) + */ + std::pair equal_range(const Key& key, std::size_t precalculated_hash) const { + return m_ht.equal_range(key, precalculated_hash); + } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent + * and Compare::is_transparent exist. + * If so, K must be hashable and comparable to Key. + */ + template::value && has_is_transparent::value>::type* = nullptr> + std::pair equal_range(const K& key) { return m_ht.equal_range(key); } + + /** + * @copydoc equal_range(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value && has_is_transparent::value>::type* = nullptr> + std::pair equal_range(const K& key, std::size_t precalculated_hash) { + return m_ht.equal_range(key, precalculated_hash); + } + + /** + * @copydoc equal_range(const K& key) + */ + template::value && has_is_transparent::value>::type* = nullptr> + std::pair equal_range(const K& key) const { return m_ht.equal_range(key); } + + /** + * @copydoc equal_range(const K& key, std::size_t precalculated_hash) + */ + template::value && has_is_transparent::value>::type* = nullptr> + std::pair equal_range(const K& key, std::size_t precalculated_hash) const { + return m_ht.equal_range(key, precalculated_hash); + } + + + + + /* + * Bucket interface + */ + size_type bucket_count() const { return m_ht.bucket_count(); } + size_type max_bucket_count() const { return m_ht.max_bucket_count(); } + + + /* + * Hash policy + */ + float load_factor() const { return m_ht.load_factor(); } + float max_load_factor() const { return m_ht.max_load_factor(); } + void max_load_factor(float ml) { m_ht.max_load_factor(ml); } + + void rehash(size_type count_) { m_ht.rehash(count_); } + void reserve(size_type count_) { m_ht.reserve(count_); } + + + /* + * Observers + */ + hasher hash_function() const { return m_ht.hash_function(); } + key_equal key_eq() const { return m_ht.key_eq(); } + key_compare key_comp() const { return m_ht.key_comp(); } + + + /* + * Other + */ + + /** + * Convert a const_iterator to an iterator. + */ + iterator mutable_iterator(const_iterator pos) { + return m_ht.mutable_iterator(pos); + } + + size_type overflow_size() const noexcept { return m_ht.overflow_size(); } + + friend bool operator==(const bhopscotch_set& lhs, const bhopscotch_set& rhs) { + if(lhs.size() != rhs.size()) { + return false; + } + + for(const auto& element_lhs : lhs) { + const auto it_element_rhs = rhs.find(element_lhs); + if(it_element_rhs == rhs.cend()) { + return false; + } + } + + return true; + } + + friend bool operator!=(const bhopscotch_set& lhs, const bhopscotch_set& rhs) { + return !operator==(lhs, rhs); + } + + friend void swap(bhopscotch_set& lhs, bhopscotch_set& rhs) { + lhs.swap(rhs); + } + +private: + ht m_ht; +}; + + +/** + * Same as `tsl::bhopscotch_set`. + */ +template, + class KeyEqual = std::equal_to, + class Compare = std::less, + class Allocator = std::allocator, + unsigned int NeighborhoodSize = 62, + bool StoreHash = false> +using bhopscotch_pg_set = bhopscotch_set; + +} // end namespace tsl + +#endif diff --git a/include/tsl/hopscotch_growth_policy.h b/include/tsl/hopscotch_growth_policy.h new file mode 100644 index 0000000..65b1845 --- /dev/null +++ b/include/tsl/hopscotch_growth_policy.h @@ -0,0 +1,295 @@ +/** + * MIT License + * + * Copyright (c) 2018 Tessil + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef TSL_HOPSCOTCH_GROWTH_POLICY_H +#define TSL_HOPSCOTCH_GROWTH_POLICY_H + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace tsl { +namespace hh { + +/** + * Grow the hash table by a factor of GrowthFactor keeping the bucket count to a power of two. It allows + * the table to use a mask operation instead of a modulo operation to map a hash to a bucket. + * + * GrowthFactor must be a power of two >= 2. + */ +template +class power_of_two_growth_policy { +public: + /** + * Called on the hash table creation and on rehash. The number of buckets for the table is passed in parameter. + * This number is a minimum, the policy may update this value with a higher value if needed (but not lower). + * + * If 0 is given, min_bucket_count_in_out must still be 0 after the policy creation and + * bucket_for_hash must always return 0 in this case. + */ + explicit power_of_two_growth_policy(std::size_t& min_bucket_count_in_out) { + if(min_bucket_count_in_out > max_bucket_count()) { + throw std::length_error("The hash table exceeds its maxmimum size."); + } + + if(min_bucket_count_in_out > 0) { + min_bucket_count_in_out = round_up_to_power_of_two(min_bucket_count_in_out); + m_mask = min_bucket_count_in_out - 1; + } + else { + m_mask = 0; + } + } + + /** + * Return the bucket [0, bucket_count()) to which the hash belongs. + * If bucket_count() is 0, it must always return 0. + */ + std::size_t bucket_for_hash(std::size_t hash) const noexcept { + return hash & m_mask; + } + + /** + * Return the bucket count to use when the bucket array grows on rehash. + */ + std::size_t next_bucket_count() const { + if((m_mask + 1) > max_bucket_count() / GrowthFactor) { + throw std::length_error("The hash table exceeds its maxmimum size."); + } + + return (m_mask + 1) * GrowthFactor; + } + + /** + * Return the maximum number of buckets supported by the policy. + */ + std::size_t max_bucket_count() const { + // Largest power of two. + return (std::numeric_limits::max() / 2) + 1; + } + + /** + * Reset the growth policy as if it was created with a bucket count of 0. + * After a clear, the policy must always return 0 when bucket_for_hash is called. + */ + void clear() noexcept { + m_mask = 0; + } + +private: + static std::size_t round_up_to_power_of_two(std::size_t value) { + if(is_power_of_two(value)) { + return value; + } + + if(value == 0) { + return 1; + } + + --value; + for(std::size_t i = 1; i < sizeof(std::size_t) * CHAR_BIT; i *= 2) { + value |= value >> i; + } + + return value + 1; + } + + static constexpr bool is_power_of_two(std::size_t value) { + return value != 0 && (value & (value - 1)) == 0; + } + +private: + static_assert(is_power_of_two(GrowthFactor) && GrowthFactor >= 2, "GrowthFactor must be a power of two >= 2."); + + std::size_t m_mask; +}; + + +/** + * Grow the hash table by GrowthFactor::num / GrowthFactor::den and use a modulo to map a hash + * to a bucket. Slower but it can be useful if you want a slower growth. + */ +template> +class mod_growth_policy { +public: + explicit mod_growth_policy(std::size_t& min_bucket_count_in_out) { + if(min_bucket_count_in_out > max_bucket_count()) { + throw std::length_error("The hash table exceeds its maxmimum size."); + } + + if(min_bucket_count_in_out > 0) { + m_mod = min_bucket_count_in_out; + } + else { + m_mod = 1; + } + } + + std::size_t bucket_for_hash(std::size_t hash) const noexcept { + return hash % m_mod; + } + + std::size_t next_bucket_count() const { + if(m_mod == max_bucket_count()) { + throw std::length_error("The hash table exceeds its maxmimum size."); + } + + const double next_bucket_count = std::ceil(double(m_mod) * REHASH_SIZE_MULTIPLICATION_FACTOR); + if(!std::isnormal(next_bucket_count)) { + throw std::length_error("The hash table exceeds its maxmimum size."); + } + + if(next_bucket_count > double(max_bucket_count())) { + return max_bucket_count(); + } + else { + return std::size_t(next_bucket_count); + } + } + + std::size_t max_bucket_count() const { + return MAX_BUCKET_COUNT; + } + + void clear() noexcept { + m_mod = 1; + } + +private: + static constexpr double REHASH_SIZE_MULTIPLICATION_FACTOR = 1.0 * GrowthFactor::num / GrowthFactor::den; + static const std::size_t MAX_BUCKET_COUNT = + std::size_t(double( + std::numeric_limits::max() / REHASH_SIZE_MULTIPLICATION_FACTOR + )); + + static_assert(REHASH_SIZE_MULTIPLICATION_FACTOR >= 1.1, "Growth factor should be >= 1.1."); + + std::size_t m_mod; +}; + + + +namespace detail { + +static constexpr const std::array PRIMES = {{ + 1ul, 5ul, 17ul, 29ul, 37ul, 53ul, 67ul, 79ul, 97ul, 131ul, 193ul, 257ul, 389ul, 521ul, 769ul, 1031ul, + 1543ul, 2053ul, 3079ul, 6151ul, 12289ul, 24593ul, 49157ul, 98317ul, 196613ul, 393241ul, 786433ul, + 1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul, 50331653ul, 100663319ul, 201326611ul, + 402653189ul, 805306457ul, 1610612741ul, 3221225473ul, 4294967291ul +}}; + +template +static constexpr std::size_t mod(std::size_t hash) { return hash % PRIMES[IPrime]; } + +// MOD_PRIME[iprime](hash) returns hash % PRIMES[iprime]. This table allows for faster modulo as the +// compiler can optimize the modulo code better with a constant known at the compilation. +static constexpr const std::array MOD_PRIME = {{ + &mod<0>, &mod<1>, &mod<2>, &mod<3>, &mod<4>, &mod<5>, &mod<6>, &mod<7>, &mod<8>, &mod<9>, &mod<10>, + &mod<11>, &mod<12>, &mod<13>, &mod<14>, &mod<15>, &mod<16>, &mod<17>, &mod<18>, &mod<19>, &mod<20>, + &mod<21>, &mod<22>, &mod<23>, &mod<24>, &mod<25>, &mod<26>, &mod<27>, &mod<28>, &mod<29>, &mod<30>, + &mod<31>, &mod<32>, &mod<33>, &mod<34>, &mod<35>, &mod<36>, &mod<37> , &mod<38>, &mod<39> +}}; + +} + +/** + * Grow the hash table by using prime numbers as bucket count. Slower than tsl::hh::power_of_two_growth_policy in + * general but will probably distribute the values around better in the buckets with a poor hash function. + * + * To allow the compiler to optimize the modulo operation, a lookup table is used with constant primes numbers. + * + * With a switch the code would look like: + * \code + * switch(iprime) { // iprime is the current prime of the hash table + * case 0: hash % 5ul; + * break; + * case 1: hash % 17ul; + * break; + * case 2: hash % 29ul; + * break; + * ... + * } + * \endcode + * + * Due to the constant variable in the modulo the compiler is able to optimize the operation + * by a series of multiplications, substractions and shifts. + * + * The 'hash % 5' could become something like 'hash - (hash * 0xCCCCCCCD) >> 34) * 5' in a 64 bits environement. + */ +class prime_growth_policy { +public: + explicit prime_growth_policy(std::size_t& min_bucket_count_in_out) { + auto it_prime = std::lower_bound(detail::PRIMES.begin(), + detail::PRIMES.end(), min_bucket_count_in_out); + if(it_prime == detail::PRIMES.end()) { + throw std::length_error("The hash table exceeds its maxmimum size."); + } + + m_iprime = static_cast(std::distance(detail::PRIMES.begin(), it_prime)); + if(min_bucket_count_in_out > 0) { + min_bucket_count_in_out = *it_prime; + } + else { + min_bucket_count_in_out = 0; + } + } + + std::size_t bucket_for_hash(std::size_t hash) const noexcept { + return detail::MOD_PRIME[m_iprime](hash); + } + + std::size_t next_bucket_count() const { + if(m_iprime + 1 >= detail::PRIMES.size()) { + throw std::length_error("The hash table exceeds its maxmimum size."); + } + + return detail::PRIMES[m_iprime + 1]; + } + + std::size_t max_bucket_count() const { + return detail::PRIMES.back(); + } + + void clear() noexcept { + m_iprime = 0; + } + +private: + unsigned int m_iprime; + + static_assert(std::numeric_limits::max() >= detail::PRIMES.size(), + "The type of m_iprime is not big enough."); +}; + +} +} + +#endif diff --git a/include/tsl/hopscotch_hash.h b/include/tsl/hopscotch_hash.h new file mode 100644 index 0000000..1031c0c --- /dev/null +++ b/include/tsl/hopscotch_hash.h @@ -0,0 +1,1801 @@ +/** + * MIT License + * + * Copyright (c) 2017 Tessil + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef TSL_HOPSCOTCH_HASH_H +#define TSL_HOPSCOTCH_HASH_H + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "hopscotch_growth_policy.h" + + + +#if (defined(__GNUC__) && (__GNUC__ == 4) && (__GNUC_MINOR__ < 9)) +#define TSL_NO_RANGE_ERASE_WITH_CONST_ITERATOR +#endif + + + +/* + * Only activate tsl_assert if TSL_DEBUG is defined. + * This way we avoid the performance hit when NDEBUG is not defined with assert as tsl_assert is used a lot + * (people usually compile with "-O3" and not "-O3 -DNDEBUG"). + */ +#ifndef tsl_assert + #ifdef TSL_DEBUG + #define tsl_assert(expr) assert(expr) + #else + #define tsl_assert(expr) (static_cast(0)) + #endif +#endif + +namespace tsl { + +namespace detail_hopscotch_hash { + + +template +struct make_void { + using type = void; +}; + + +template +struct has_is_transparent : std::false_type { +}; + +template +struct has_is_transparent::type> : std::true_type { +}; + + +template +struct has_key_compare : std::false_type { +}; + +template +struct has_key_compare::type> : std::true_type { +}; + + +template +struct is_power_of_two_policy: std::false_type { +}; + +template +struct is_power_of_two_policy>: std::true_type { +}; + + + + + +/* + * smallest_type_for_min_bits::type returns the smallest type that can fit MinBits. + */ +static const std::size_t SMALLEST_TYPE_MAX_BITS_SUPPORTED = 64; +template +class smallest_type_for_min_bits { +}; + +template +class smallest_type_for_min_bits 0) && (MinBits <= 8)>::type> { +public: + using type = std::uint_least8_t; +}; + +template +class smallest_type_for_min_bits 8) && (MinBits <= 16)>::type> { +public: + using type = std::uint_least16_t; +}; + +template +class smallest_type_for_min_bits 16) && (MinBits <= 32)>::type> { +public: + using type = std::uint_least32_t; +}; + +template +class smallest_type_for_min_bits 32) && (MinBits <= 64)>::type> { +public: + using type = std::uint_least64_t; +}; + + + +/* + * Each bucket may store up to three elements: + * - An aligned storage to store a value_type object with placement-new. + * - An (optional) hash of the value in the bucket. + * - An unsigned integer of type neighborhood_bitmap used to tell us which buckets in the neighborhood of the + * current bucket contain a value with a hash belonging to the current bucket. + * + * For a bucket 'bct', a bit 'i' (counting from 0 and from the least significant bit to the most significant) + * set to 1 means that the bucket 'bct + i' contains a value with a hash belonging to bucket 'bct'. + * The bits used for that, start from the third least significant bit. + * The two least significant bits are reserved: + * - The least significant bit is set to 1 if there is a value in the bucket storage. + * - The second least significant bit is set to 1 if there is an overflow. More than NeighborhoodSize values + * give the same hash, all overflow values are stored in the m_overflow_elements list of the map. + * + * Details regarding hopscotch hashing an its implementation can be found here: + * https://tessil.github.io/2016/08/29/hopscotch-hashing.html + */ +static const std::size_t NB_RESERVED_BITS_IN_NEIGHBORHOOD = 2; + + +using truncated_hash_type = std::uint_least32_t; + +/** + * Helper class that store a truncated hash if StoreHash is true and nothing otherwise. + */ +template +class hopscotch_bucket_hash { +public: + bool bucket_hash_equal(std::size_t /*hash*/) const noexcept { + return true; + } + + truncated_hash_type truncated_bucket_hash() const noexcept { + return 0; + } + +protected: + void copy_hash(const hopscotch_bucket_hash& ) noexcept { + } + + void set_hash(truncated_hash_type /*hash*/) noexcept { + } +}; + +template<> +class hopscotch_bucket_hash { +public: + bool bucket_hash_equal(std::size_t hash) const noexcept { + return m_hash == truncated_hash_type(hash); + } + + truncated_hash_type truncated_bucket_hash() const noexcept { + return m_hash; + } + +protected: + void copy_hash(const hopscotch_bucket_hash& bucket) noexcept { + m_hash = bucket.m_hash; + } + + void set_hash(truncated_hash_type hash) noexcept { + m_hash = hash; + } + +private: + truncated_hash_type m_hash; +}; + + +template +class hopscotch_bucket: public hopscotch_bucket_hash { +private: + static const std::size_t MIN_NEIGHBORHOOD_SIZE = 4; + static const std::size_t MAX_NEIGHBORHOOD_SIZE = SMALLEST_TYPE_MAX_BITS_SUPPORTED - NB_RESERVED_BITS_IN_NEIGHBORHOOD; + + + static_assert(NeighborhoodSize >= 4, "NeighborhoodSize should be >= 4."); + // We can't put a variable in the message, ensure coherence + static_assert(MIN_NEIGHBORHOOD_SIZE == 4, ""); + + static_assert(NeighborhoodSize <= 62, "NeighborhoodSize should be <= 62."); + // We can't put a variable in the message, ensure coherence + static_assert(MAX_NEIGHBORHOOD_SIZE == 62, ""); + + + static_assert(!StoreHash || NeighborhoodSize <= 30, + "NeighborhoodSize should be <= 30 if StoreHash is true."); + // We can't put a variable in the message, ensure coherence + static_assert(MAX_NEIGHBORHOOD_SIZE - 32 == 30, ""); + + using bucket_hash = hopscotch_bucket_hash; + +public: + using value_type = ValueType; + using neighborhood_bitmap = + typename smallest_type_for_min_bits::type; + + + hopscotch_bucket() noexcept: bucket_hash(), m_neighborhood_infos(0) { + tsl_assert(empty()); + } + + + hopscotch_bucket(const hopscotch_bucket& bucket) + noexcept(std::is_nothrow_copy_constructible::value): bucket_hash(bucket), + m_neighborhood_infos(0) + { + if(!bucket.empty()) { + ::new (static_cast(std::addressof(m_value))) value_type(bucket.value()); + } + + m_neighborhood_infos = bucket.m_neighborhood_infos; + } + + hopscotch_bucket(hopscotch_bucket&& bucket) + noexcept(std::is_nothrow_move_constructible::value) : bucket_hash(std::move(bucket)), + m_neighborhood_infos(0) + { + if(!bucket.empty()) { + ::new (static_cast(std::addressof(m_value))) value_type(std::move(bucket.value())); + } + + m_neighborhood_infos = bucket.m_neighborhood_infos; + } + + hopscotch_bucket& operator=(const hopscotch_bucket& bucket) + noexcept(std::is_nothrow_copy_constructible::value) + { + if(this != &bucket) { + remove_value(); + + bucket_hash::operator=(bucket); + if(!bucket.empty()) { + ::new (static_cast(std::addressof(m_value))) value_type(bucket.value()); + } + + m_neighborhood_infos = bucket.m_neighborhood_infos; + } + + return *this; + } + + hopscotch_bucket& operator=(hopscotch_bucket&& ) = delete; + + ~hopscotch_bucket() noexcept { + if(!empty()) { + destroy_value(); + } + } + + neighborhood_bitmap neighborhood_infos() const noexcept { + return neighborhood_bitmap(m_neighborhood_infos >> NB_RESERVED_BITS_IN_NEIGHBORHOOD); + } + + void set_overflow(bool has_overflow) noexcept { + if(has_overflow) { + m_neighborhood_infos = neighborhood_bitmap(m_neighborhood_infos | 2); + } + else { + m_neighborhood_infos = neighborhood_bitmap(m_neighborhood_infos & ~2); + } + } + + bool has_overflow() const noexcept { + return (m_neighborhood_infos & 2) != 0; + } + + bool empty() const noexcept { + return (m_neighborhood_infos & 1) == 0; + } + + void toggle_neighbor_presence(std::size_t ineighbor) noexcept { + tsl_assert(ineighbor <= NeighborhoodSize); + m_neighborhood_infos = neighborhood_bitmap( + m_neighborhood_infos ^ (1ull << (ineighbor + NB_RESERVED_BITS_IN_NEIGHBORHOOD))); + } + + bool check_neighbor_presence(std::size_t ineighbor) const noexcept { + tsl_assert(ineighbor <= NeighborhoodSize); + if(((m_neighborhood_infos >> (ineighbor + NB_RESERVED_BITS_IN_NEIGHBORHOOD)) & 1) == 1) { + return true; + } + + return false; + } + + value_type& value() noexcept { + tsl_assert(!empty()); + return *reinterpret_cast(std::addressof(m_value)); + } + + const value_type& value() const noexcept { + tsl_assert(!empty()); + return *reinterpret_cast(std::addressof(m_value)); + } + + template + void set_value_of_empty_bucket(truncated_hash_type hash, Args&&... value_type_args) { + tsl_assert(empty()); + + ::new (static_cast(std::addressof(m_value))) value_type(std::forward(value_type_args)...); + set_empty(false); + this->set_hash(hash); + } + + void swap_value_into_empty_bucket(hopscotch_bucket& empty_bucket) { + tsl_assert(empty_bucket.empty()); + if(!empty()) { + ::new (static_cast(std::addressof(empty_bucket.m_value))) value_type(std::move(value())); + empty_bucket.copy_hash(*this); + empty_bucket.set_empty(false); + + destroy_value(); + set_empty(true); + } + } + + void remove_value() noexcept { + if(!empty()) { + destroy_value(); + set_empty(true); + } + } + + void clear() noexcept { + if(!empty()) { + destroy_value(); + } + + m_neighborhood_infos = 0; + tsl_assert(empty()); + } + + static std::size_t max_size() noexcept { + if(StoreHash) { + return std::numeric_limits::max(); + } + else { + return std::numeric_limits::max(); + } + } + + static truncated_hash_type truncate_hash(std::size_t hash) noexcept { + return truncated_hash_type(hash); + } + +private: + void set_empty(bool is_empty) noexcept { + if(is_empty) { + m_neighborhood_infos = neighborhood_bitmap(m_neighborhood_infos & ~1); + } + else { + m_neighborhood_infos = neighborhood_bitmap(m_neighborhood_infos | 1); + } + } + + void destroy_value() noexcept { + tsl_assert(!empty()); + value().~value_type(); + } + +private: + using storage = typename std::aligned_storage::type; + + neighborhood_bitmap m_neighborhood_infos; + storage m_value; +}; + + +/** + * Internal common class used by (b)hopscotch_map and (b)hopscotch_set. + * + * ValueType is what will be stored by hopscotch_hash (usually std::pair for a map and Key for a set). + * + * KeySelect should be a FunctionObject which takes a ValueType in parameter and returns a reference to the key. + * + * ValueSelect should be a FunctionObject which takes a ValueType in parameter and returns a reference to the value. + * ValueSelect should be void if there is no value (in a set for example). + * + * OverflowContainer will be used as containers for overflown elements. Usually it should be a list + * or a set/map. + */ +template +class hopscotch_hash: private Hash, private KeyEqual, private GrowthPolicy { +private: + template + using has_mapped_type = typename std::integral_constant::value>; + + static_assert(noexcept(std::declval().bucket_for_hash(std::size_t(0))), "GrowthPolicy::bucket_for_hash must be noexcept."); + static_assert(noexcept(std::declval().clear()), "GrowthPolicy::clear must be noexcept."); + +public: + template + class hopscotch_iterator; + + using key_type = typename KeySelect::key_type; + using value_type = ValueType; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using hasher = Hash; + using key_equal = KeyEqual; + using allocator_type = Allocator; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = value_type*; + using const_pointer = const value_type*; + using iterator = hopscotch_iterator; + using const_iterator = hopscotch_iterator; + +private: + using hopscotch_bucket = tsl::detail_hopscotch_hash::hopscotch_bucket; + using neighborhood_bitmap = typename hopscotch_bucket::neighborhood_bitmap; + + using buckets_allocator = typename std::allocator_traits::template rebind_alloc; + using buckets_container_type = std::vector; + + using overflow_container_type = OverflowContainer; + + static_assert(std::is_same::value, + "OverflowContainer should have ValueType as type."); + + static_assert(std::is_same::value, + "Invalid allocator, not the same type as the value_type."); + + + using iterator_buckets = typename buckets_container_type::iterator; + using const_iterator_buckets = typename buckets_container_type::const_iterator; + + using iterator_overflow = typename overflow_container_type::iterator; + using const_iterator_overflow = typename overflow_container_type::const_iterator; + +public: + /** + * The `operator*()` and `operator->()` methods return a const reference and const pointer respectively to the + * stored value type. + * + * In case of a map, to get a modifiable reference to the value associated to a key (the `.second` in the + * stored pair), you have to call `value()`. + */ + template + class hopscotch_iterator { + friend class hopscotch_hash; + private: + using iterator_bucket = typename std::conditional::type; + using iterator_overflow = typename std::conditional::type; + + + hopscotch_iterator(iterator_bucket buckets_iterator, iterator_bucket buckets_end_iterator, + iterator_overflow overflow_iterator) noexcept : + m_buckets_iterator(buckets_iterator), m_buckets_end_iterator(buckets_end_iterator), + m_overflow_iterator(overflow_iterator) + { + } + + public: + using iterator_category = std::forward_iterator_tag; + using value_type = const typename hopscotch_hash::value_type; + using difference_type = std::ptrdiff_t; + using reference = value_type&; + using pointer = value_type*; + + + hopscotch_iterator() noexcept { + } + + hopscotch_iterator(const hopscotch_iterator& other) noexcept : + m_buckets_iterator(other.m_buckets_iterator), m_buckets_end_iterator(other.m_buckets_end_iterator), + m_overflow_iterator(other.m_overflow_iterator) + { + } + + const typename hopscotch_hash::key_type& key() const { + if(m_buckets_iterator != m_buckets_end_iterator) { + return KeySelect()(m_buckets_iterator->value()); + } + + return KeySelect()(*m_overflow_iterator); + } + + template::value>::type* = nullptr> + typename std::conditional< + IsConst, + const typename U::value_type&, + typename U::value_type&>::type value() const + { + if(m_buckets_iterator != m_buckets_end_iterator) { + return U()(m_buckets_iterator->value()); + } + + return U()(*m_overflow_iterator); + } + + reference operator*() const { + if(m_buckets_iterator != m_buckets_end_iterator) { + return m_buckets_iterator->value(); + } + + return *m_overflow_iterator; + } + + pointer operator->() const { + if(m_buckets_iterator != m_buckets_end_iterator) { + return std::addressof(m_buckets_iterator->value()); + } + + return std::addressof(*m_overflow_iterator); + } + + hopscotch_iterator& operator++() { + if(m_buckets_iterator == m_buckets_end_iterator) { + ++m_overflow_iterator; + return *this; + } + + do { + ++m_buckets_iterator; + } while(m_buckets_iterator != m_buckets_end_iterator && m_buckets_iterator->empty()); + + return *this; + } + + hopscotch_iterator operator++(int) { + hopscotch_iterator tmp(*this); + ++*this; + + return tmp; + } + + friend bool operator==(const hopscotch_iterator& lhs, const hopscotch_iterator& rhs) { + return lhs.m_buckets_iterator == rhs.m_buckets_iterator && + lhs.m_overflow_iterator == rhs.m_overflow_iterator; + } + + friend bool operator!=(const hopscotch_iterator& lhs, const hopscotch_iterator& rhs) { + return !(lhs == rhs); + } + + private: + iterator_bucket m_buckets_iterator; + iterator_bucket m_buckets_end_iterator; + iterator_overflow m_overflow_iterator; + }; + +public: + template::value>::type* = nullptr> + hopscotch_hash(size_type bucket_count, + const Hash& hash, + const KeyEqual& equal, + const Allocator& alloc, + float max_load_factor) : Hash(hash), + KeyEqual(equal), + GrowthPolicy(bucket_count), + m_buckets(alloc), + m_overflow_elements(alloc), + m_first_or_empty_bucket(static_empty_bucket_ptr()), + m_nb_elements(0) + { + if(bucket_count > max_bucket_count()) { + throw std::length_error("The map exceeds its maxmimum size."); + } + + if(bucket_count > 0) { + static_assert(NeighborhoodSize - 1 > 0, ""); + + // Can't directly construct with the appropriate size in the initializer + // as m_buckets(bucket_count, alloc) is not supported by GCC 4.8 + m_buckets.resize(bucket_count + NeighborhoodSize - 1); + m_first_or_empty_bucket = m_buckets.data(); + } + + + this->max_load_factor(max_load_factor); + + + // Check in the constructor instead of outside of a function to avoi compilation issues + // when value_type is not complete. + static_assert(std::is_nothrow_move_constructible::value || + std::is_copy_constructible::value, + "value_type must be either copy constructible or nothrow move constructible."); + } + + template::value>::type* = nullptr> + hopscotch_hash(size_type bucket_count, + const Hash& hash, + const KeyEqual& equal, + const Allocator& alloc, + float max_load_factor, + const typename OC::key_compare& comp) : Hash(hash), + KeyEqual(equal), + GrowthPolicy(bucket_count), + m_buckets(alloc), + m_overflow_elements(comp, alloc), + m_first_or_empty_bucket(static_empty_bucket_ptr()), + m_nb_elements(0) + { + + if(bucket_count > max_bucket_count()) { + throw std::length_error("The map exceeds its maxmimum size."); + } + + if(bucket_count > 0) { + static_assert(NeighborhoodSize - 1 > 0, ""); + + // Can't directly construct with the appropriate size in the initializer + // as m_buckets(bucket_count, alloc) is not supported by GCC 4.8 + m_buckets.resize(bucket_count + NeighborhoodSize - 1); + m_first_or_empty_bucket = m_buckets.data(); + } + + + this->max_load_factor(max_load_factor); + + + // Check in the constructor instead of outside of a function to avoi compilation issues + // when value_type is not complete. + static_assert(std::is_nothrow_move_constructible::value || + std::is_copy_constructible::value, + "value_type must be either copy constructible or nothrow move constructible."); + } + + hopscotch_hash(const hopscotch_hash& other): + Hash(other), + KeyEqual(other), + GrowthPolicy(other), + m_buckets(other.m_buckets), + m_overflow_elements(other.m_overflow_elements), + m_first_or_empty_bucket(m_buckets.empty()?static_empty_bucket_ptr(): + m_buckets.data()), + m_nb_elements(other.m_nb_elements), + m_max_load_factor(other.m_max_load_factor), + m_max_load_threshold_rehash(other.m_max_load_threshold_rehash), + m_min_load_threshold_rehash(other.m_min_load_threshold_rehash) + { + } + + hopscotch_hash(hopscotch_hash&& other) + noexcept( + std::is_nothrow_move_constructible::value && + std::is_nothrow_move_constructible::value && + std::is_nothrow_move_constructible::value && + std::is_nothrow_move_constructible::value && + std::is_nothrow_move_constructible::value + ): + Hash(std::move(static_cast(other))), + KeyEqual(std::move(static_cast(other))), + GrowthPolicy(std::move(static_cast(other))), + m_buckets(std::move(other.m_buckets)), + m_overflow_elements(std::move(other.m_overflow_elements)), + m_first_or_empty_bucket(m_buckets.empty()?static_empty_bucket_ptr(): + m_buckets.data()), + m_nb_elements(other.m_nb_elements), + m_max_load_factor(other.m_max_load_factor), + m_max_load_threshold_rehash(other.m_max_load_threshold_rehash), + m_min_load_threshold_rehash(other.m_min_load_threshold_rehash) + { + other.GrowthPolicy::clear(); + other.m_buckets.clear(); + other.m_overflow_elements.clear(); + other.m_first_or_empty_bucket = static_empty_bucket_ptr(); + other.m_nb_elements = 0; + other.m_max_load_threshold_rehash = 0; + other.m_min_load_threshold_rehash = 0; + } + + hopscotch_hash& operator=(const hopscotch_hash& other) { + if(&other != this) { + Hash::operator=(other); + KeyEqual::operator=(other); + GrowthPolicy::operator=(other); + + m_buckets = other.m_buckets; + m_overflow_elements = other.m_overflow_elements; + m_first_or_empty_bucket = m_buckets.empty()?static_empty_bucket_ptr(): + m_buckets.data(); + m_nb_elements = other.m_nb_elements; + m_max_load_factor = other.m_max_load_factor; + m_max_load_threshold_rehash = other.m_max_load_threshold_rehash; + m_min_load_threshold_rehash = other.m_min_load_threshold_rehash; + } + + return *this; + } + + hopscotch_hash& operator=(hopscotch_hash&& other) { + other.swap(*this); + other.clear(); + + return *this; + } + + allocator_type get_allocator() const { + return m_buckets.get_allocator(); + } + + + /* + * Iterators + */ + iterator begin() noexcept { + auto begin = m_buckets.begin(); + while(begin != m_buckets.end() && begin->empty()) { + ++begin; + } + + return iterator(begin, m_buckets.end(), m_overflow_elements.begin()); + } + + const_iterator begin() const noexcept { + return cbegin(); + } + + const_iterator cbegin() const noexcept { + auto begin = m_buckets.cbegin(); + while(begin != m_buckets.cend() && begin->empty()) { + ++begin; + } + + return const_iterator(begin, m_buckets.cend(), m_overflow_elements.cbegin()); + } + + iterator end() noexcept { + return iterator(m_buckets.end(), m_buckets.end(), m_overflow_elements.end()); + } + + const_iterator end() const noexcept { + return cend(); + } + + const_iterator cend() const noexcept { + return const_iterator(m_buckets.cend(), m_buckets.cend(), m_overflow_elements.cend()); + } + + + /* + * Capacity + */ + bool empty() const noexcept { + return m_nb_elements == 0; + } + + size_type size() const noexcept { + return m_nb_elements; + } + + size_type max_size() const noexcept { + return hopscotch_bucket::max_size(); + } + + /* + * Modifiers + */ + void clear() noexcept { + for(auto& bucket: m_buckets) { + bucket.clear(); + } + + m_overflow_elements.clear(); + m_nb_elements = 0; + } + + + std::pair insert(const value_type& value) { + return insert_impl(value); + } + + template::value>::type* = nullptr> + std::pair insert(P&& value) { + return insert_impl(value_type(std::forward

(value))); + } + + std::pair insert(value_type&& value) { + return insert_impl(std::move(value)); + } + + + iterator insert(const_iterator hint, const value_type& value) { + if(hint != cend() && compare_keys(KeySelect()(*hint), KeySelect()(value))) { + return mutable_iterator(hint); + } + + return insert(value).first; + } + + template::value>::type* = nullptr> + iterator insert(const_iterator hint, P&& value) { + return emplace_hint(hint, std::forward

(value)); + } + + iterator insert(const_iterator hint, value_type&& value) { + if(hint != cend() && compare_keys(KeySelect()(*hint), KeySelect()(value))) { + return mutable_iterator(hint); + } + + return insert(std::move(value)).first; + } + + + template + void insert(InputIt first, InputIt last) { + if(std::is_base_of::iterator_category>::value) + { + const auto nb_elements_insert = std::distance(first, last); + const std::size_t nb_elements_in_buckets = m_nb_elements - m_overflow_elements.size(); + const std::size_t nb_free_buckets = m_max_load_threshold_rehash - nb_elements_in_buckets; + tsl_assert(m_nb_elements >= m_overflow_elements.size()); + tsl_assert(m_max_load_threshold_rehash >= nb_elements_in_buckets); + + if(nb_elements_insert > 0 && nb_free_buckets < std::size_t(nb_elements_insert)) { + reserve(nb_elements_in_buckets + std::size_t(nb_elements_insert)); + } + } + + for(; first != last; ++first) { + insert(*first); + } + } + + + template + std::pair insert_or_assign(const key_type& k, M&& obj) { + return insert_or_assign_impl(k, std::forward(obj)); + } + + template + std::pair insert_or_assign(key_type&& k, M&& obj) { + return insert_or_assign_impl(std::move(k), std::forward(obj)); + } + + + template + iterator insert_or_assign(const_iterator hint, const key_type& k, M&& obj) { + if(hint != cend() && compare_keys(KeySelect()(*hint), k)) { + auto it = mutable_iterator(hint); + it.value() = std::forward(obj); + + return it; + } + + return insert_or_assign(k, std::forward(obj)).first; + } + + template + iterator insert_or_assign(const_iterator hint, key_type&& k, M&& obj) { + if(hint != cend() && compare_keys(KeySelect()(*hint), k)) { + auto it = mutable_iterator(hint); + it.value() = std::forward(obj); + + return it; + } + + return insert_or_assign(std::move(k), std::forward(obj)).first; + } + + + template + std::pair emplace(Args&&... args) { + return insert(value_type(std::forward(args)...)); + } + + template + iterator emplace_hint(const_iterator hint, Args&&... args) { + return insert(hint, value_type(std::forward(args)...)); + } + + template + std::pair try_emplace(const key_type& k, Args&&... args) { + return try_emplace_impl(k, std::forward(args)...); + } + + template + std::pair try_emplace(key_type&& k, Args&&... args) { + return try_emplace_impl(std::move(k), std::forward(args)...); + } + + template + iterator try_emplace(const_iterator hint, const key_type& k, Args&&... args) { + if(hint != cend() && compare_keys(KeySelect()(*hint), k)) { + return mutable_iterator(hint); + } + + return try_emplace(k, std::forward(args)...).first; + } + + template + iterator try_emplace(const_iterator hint, key_type&& k, Args&&... args) { + if(hint != cend() && compare_keys(KeySelect()(*hint), k)) { + return mutable_iterator(hint); + } + + return try_emplace(std::move(k), std::forward(args)...).first; + } + + + /** + * Here to avoid `template size_type erase(const K& key)` being used when + * we use an iterator instead of a const_iterator. + */ + iterator erase(iterator pos) { + return erase(const_iterator(pos)); + } + + iterator erase(const_iterator pos) { + const std::size_t ibucket_for_hash = bucket_for_hash(hash_key(pos.key())); + + if(pos.m_buckets_iterator != pos.m_buckets_end_iterator) { + auto it_bucket = m_buckets.begin() + std::distance(m_buckets.cbegin(), pos.m_buckets_iterator); + erase_from_bucket(*it_bucket, ibucket_for_hash); + + return ++iterator(it_bucket, m_buckets.end(), m_overflow_elements.begin()); + } + else { + auto it_next_overflow = erase_from_overflow(pos.m_overflow_iterator, ibucket_for_hash); + return iterator(m_buckets.end(), m_buckets.end(), it_next_overflow); + } + } + + iterator erase(const_iterator first, const_iterator last) { + if(first == last) { + return mutable_iterator(first); + } + + auto to_delete = erase(first); + while(to_delete != last) { + to_delete = erase(to_delete); + } + + return to_delete; + } + + template + size_type erase(const K& key) { + return erase(key, hash_key(key)); + } + + template + size_type erase(const K& key, std::size_t hash) { + const std::size_t ibucket_for_hash = bucket_for_hash(hash); + + hopscotch_bucket* bucket_found = find_in_buckets(key, hash, m_first_or_empty_bucket + ibucket_for_hash); + if(bucket_found != nullptr) { + erase_from_bucket(*bucket_found, ibucket_for_hash); + + return 1; + } + + if((m_first_or_empty_bucket + ibucket_for_hash)->has_overflow()) { + auto it_overflow = find_in_overflow(key); + if(it_overflow != m_overflow_elements.end()) { + erase_from_overflow(it_overflow, ibucket_for_hash); + + return 1; + } + } + + return 0; + } + + void swap(hopscotch_hash& other) { + using std::swap; + + swap(static_cast(*this), static_cast(other)); + swap(static_cast(*this), static_cast(other)); + swap(static_cast(*this), static_cast(other)); + swap(m_buckets, other.m_buckets); + swap(m_overflow_elements, other.m_overflow_elements); + swap(m_first_or_empty_bucket, other.m_first_or_empty_bucket); + swap(m_nb_elements, other.m_nb_elements); + swap(m_max_load_factor, other.m_max_load_factor); + swap(m_max_load_threshold_rehash, other.m_max_load_threshold_rehash); + swap(m_min_load_threshold_rehash, other.m_min_load_threshold_rehash); + } + + + /* + * Lookup + */ + template::value>::type* = nullptr> + typename U::value_type& at(const K& key) { + return at(key, hash_key(key)); + } + + template::value>::type* = nullptr> + typename U::value_type& at(const K& key, std::size_t hash) { + return const_cast(static_cast(this)->at(key, hash)); + } + + + template::value>::type* = nullptr> + const typename U::value_type& at(const K& key) const { + return at(key, hash_key(key)); + } + + template::value>::type* = nullptr> + const typename U::value_type& at(const K& key, std::size_t hash) const { + using T = typename U::value_type; + + const T* value = find_value_impl(key, hash, m_first_or_empty_bucket + bucket_for_hash(hash)); + if(value == nullptr) { + throw std::out_of_range("Couldn't find key."); + } + else { + return *value; + } + } + + + template::value>::type* = nullptr> + typename U::value_type& operator[](K&& key) { + using T = typename U::value_type; + + const std::size_t hash = hash_key(key); + const std::size_t ibucket_for_hash = bucket_for_hash(hash); + + T* value = find_value_impl(key, hash, m_first_or_empty_bucket + ibucket_for_hash); + if(value != nullptr) { + return *value; + } + else { + return insert_impl(ibucket_for_hash, hash, std::piecewise_construct, + std::forward_as_tuple(std::forward(key)), + std::forward_as_tuple()).first.value(); + } + } + + + template + size_type count(const K& key) const { + return count(key, hash_key(key)); + } + + template + size_type count(const K& key, std::size_t hash) const { + return count_impl(key, hash, m_first_or_empty_bucket + bucket_for_hash(hash)); + } + + + template + iterator find(const K& key) { + return find(key, hash_key(key)); + } + + template + iterator find(const K& key, std::size_t hash) { + return find_impl(key, hash, m_first_or_empty_bucket + bucket_for_hash(hash)); + } + + + template + const_iterator find(const K& key) const { + return find(key, hash_key(key)); + } + + template + const_iterator find(const K& key, std::size_t hash) const { + return find_impl(key, hash, m_first_or_empty_bucket + bucket_for_hash(hash)); + } + + + template + std::pair equal_range(const K& key) { + return equal_range(key, hash_key(key)); + } + + template + std::pair equal_range(const K& key, std::size_t hash) { + iterator it = find(key, hash); + return std::make_pair(it, (it == end())?it:std::next(it)); + } + + + template + std::pair equal_range(const K& key) const { + return equal_range(key, hash_key(key)); + } + + template + std::pair equal_range(const K& key, std::size_t hash) const { + const_iterator it = find(key, hash); + return std::make_pair(it, (it == cend())?it:std::next(it)); + } + + /* + * Bucket interface + */ + size_type bucket_count() const { + /* + * So that the last bucket can have NeighborhoodSize neighbors, the size of the bucket array is a little + * bigger than the real number of buckets when not empty. + * We could use some of the buckets at the beginning, but it is faster this way as we avoid extra checks. + */ + if(m_buckets.empty()) { + return 0; + } + + return m_buckets.size() - NeighborhoodSize + 1; + } + + size_type max_bucket_count() const { + const std::size_t max_bucket_count = std::min(GrowthPolicy::max_bucket_count(), m_buckets.max_size()); + return max_bucket_count - NeighborhoodSize + 1; + } + + + /* + * Hash policy + */ + float load_factor() const { + if(bucket_count() == 0) { + return 0; + } + + return float(m_nb_elements)/float(bucket_count()); + } + + float max_load_factor() const { + return m_max_load_factor; + } + + void max_load_factor(float ml) { + m_max_load_factor = std::max(0.1f, std::min(ml, 0.95f)); + m_max_load_threshold_rehash = size_type(float(bucket_count())*m_max_load_factor); + m_min_load_threshold_rehash = size_type(float(bucket_count())*MIN_LOAD_FACTOR_FOR_REHASH); + } + + void rehash(size_type count_) { + count_ = std::max(count_, size_type(std::ceil(float(size())/max_load_factor()))); + rehash_impl(count_); + } + + void reserve(size_type count_) { + rehash(size_type(std::ceil(float(count_)/max_load_factor()))); + } + + + /* + * Observers + */ + hasher hash_function() const { + return static_cast(*this); + } + + key_equal key_eq() const { + return static_cast(*this); + } + + /* + * Other + */ + iterator mutable_iterator(const_iterator pos) { + if(pos.m_buckets_iterator != pos.m_buckets_end_iterator) { + // Get a non-const iterator + auto it = m_buckets.begin() + std::distance(m_buckets.cbegin(), pos.m_buckets_iterator); + return iterator(it, m_buckets.end(), m_overflow_elements.begin()); + } + else { + // Get a non-const iterator + auto it = mutable_overflow_iterator(pos.m_overflow_iterator); + return iterator(m_buckets.end(), m_buckets.end(), it); + } + } + + size_type overflow_size() const noexcept { + return m_overflow_elements.size(); + } + + template::value>::type* = nullptr> + typename U::key_compare key_comp() const { + return m_overflow_elements.key_comp(); + } + + +private: + template + std::size_t hash_key(const K& key) const { + return Hash::operator()(key); + } + + template + bool compare_keys(const K1& key1, const K2& key2) const { + return KeyEqual::operator()(key1, key2); + } + + std::size_t bucket_for_hash(std::size_t hash) const { + const std::size_t bucket = GrowthPolicy::bucket_for_hash(hash); + tsl_assert(bucket < m_buckets.size() || (bucket == 0 && m_buckets.empty())); + + return bucket; + } + + template::value>::type* = nullptr> + void rehash_impl(size_type count_) { + hopscotch_hash new_map = new_hopscotch_hash(count_); + + if(!m_overflow_elements.empty()) { + new_map.m_overflow_elements.swap(m_overflow_elements); + new_map.m_nb_elements += new_map.m_overflow_elements.size(); + + for(const value_type& value : new_map.m_overflow_elements) { + const std::size_t ibucket_for_hash = new_map.bucket_for_hash(new_map.hash_key(KeySelect()(value))); + new_map.m_buckets[ibucket_for_hash].set_overflow(true); + } + } + + try { + const bool use_stored_hash = USE_STORED_HASH_ON_REHASH(new_map.bucket_count()); + for(auto it_bucket = m_buckets.begin(); it_bucket != m_buckets.end(); ++it_bucket) { + if(it_bucket->empty()) { + continue; + } + + const std::size_t hash = use_stored_hash? + it_bucket->truncated_bucket_hash(): + new_map.hash_key(KeySelect()(it_bucket->value())); + const std::size_t ibucket_for_hash = new_map.bucket_for_hash(hash); + + new_map.insert_impl(ibucket_for_hash, hash, std::move(it_bucket->value())); + + + erase_from_bucket(*it_bucket, bucket_for_hash(hash)); + } + } + /* + * The call to insert_impl may throw an exception if an element is added to the overflow + * list. Rollback the elements in this case. + */ + catch(...) { + m_overflow_elements.swap(new_map.m_overflow_elements); + + const bool use_stored_hash = USE_STORED_HASH_ON_REHASH(new_map.bucket_count()); + for(auto it_bucket = new_map.m_buckets.begin(); it_bucket != new_map.m_buckets.end(); ++it_bucket) { + if(it_bucket->empty()) { + continue; + } + + const std::size_t hash = use_stored_hash? + it_bucket->truncated_bucket_hash(): + hash_key(KeySelect()(it_bucket->value())); + const std::size_t ibucket_for_hash = bucket_for_hash(hash); + + // The elements we insert were not in the overflow list before the switch. + // They will not be go in the overflow list if we rollback the switch. + insert_impl(ibucket_for_hash, hash, std::move(it_bucket->value())); + } + + throw; + } + + new_map.swap(*this); + } + + template::value && + !std::is_nothrow_move_constructible::value>::type* = nullptr> + void rehash_impl(size_type count_) { + hopscotch_hash new_map = new_hopscotch_hash(count_); + + const bool use_stored_hash = USE_STORED_HASH_ON_REHASH(new_map.bucket_count()); + for(const hopscotch_bucket& bucket: m_buckets) { + if(bucket.empty()) { + continue; + } + + const std::size_t hash = use_stored_hash? + bucket.truncated_bucket_hash(): + new_map.hash_key(KeySelect()(bucket.value())); + const std::size_t ibucket_for_hash = new_map.bucket_for_hash(hash); + + new_map.insert_impl(ibucket_for_hash, hash, bucket.value()); + } + + for(const value_type& value: m_overflow_elements) { + const std::size_t hash = new_map.hash_key(KeySelect()(value)); + const std::size_t ibucket_for_hash = new_map.bucket_for_hash(hash); + + new_map.insert_impl(ibucket_for_hash, hash, value); + } + + new_map.swap(*this); + } + +#ifdef TSL_NO_RANGE_ERASE_WITH_CONST_ITERATOR + iterator_overflow mutable_overflow_iterator(const_iterator_overflow it) { + return std::next(m_overflow_elements.begin(), std::distance(m_overflow_elements.cbegin(), it)); + } +#else + iterator_overflow mutable_overflow_iterator(const_iterator_overflow it) { + return m_overflow_elements.erase(it, it); + } +#endif + + // iterator is in overflow list + iterator_overflow erase_from_overflow(const_iterator_overflow pos, std::size_t ibucket_for_hash) { +#ifdef TSL_NO_RANGE_ERASE_WITH_CONST_ITERATOR + auto it_next = m_overflow_elements.erase(mutable_overflow_iterator(pos)); +#else + auto it_next = m_overflow_elements.erase(pos); +#endif + m_nb_elements--; + + + // Check if we can remove the overflow flag + tsl_assert(m_buckets[ibucket_for_hash].has_overflow()); + for(const value_type& value: m_overflow_elements) { + const std::size_t bucket_for_value = bucket_for_hash(hash_key(KeySelect()(value))); + if(bucket_for_value == ibucket_for_hash) { + return it_next; + } + } + + m_buckets[ibucket_for_hash].set_overflow(false); + return it_next; + } + + + /** + * bucket_for_value is the bucket in which the value is. + * ibucket_for_hash is the bucket where the value belongs. + */ + void erase_from_bucket(hopscotch_bucket& bucket_for_value, std::size_t ibucket_for_hash) noexcept { + const std::size_t ibucket_for_value = std::distance(m_buckets.data(), &bucket_for_value); + tsl_assert(ibucket_for_value >= ibucket_for_hash); + + bucket_for_value.remove_value(); + m_buckets[ibucket_for_hash].toggle_neighbor_presence(ibucket_for_value - ibucket_for_hash); + m_nb_elements--; + } + + + + template + std::pair insert_or_assign_impl(K&& key, M&& obj) { + auto it = try_emplace_impl(std::forward(key), std::forward(obj)); + if(!it.second) { + it.first.value() = std::forward(obj); + } + + return it; + } + + template + std::pair try_emplace_impl(P&& key, Args&&... args_value) { + const std::size_t hash = hash_key(key); + const std::size_t ibucket_for_hash = bucket_for_hash(hash); + + // Check if already presents + auto it_find = find_impl(key, hash, m_first_or_empty_bucket + ibucket_for_hash); + if(it_find != end()) { + return std::make_pair(it_find, false); + } + + return insert_impl(ibucket_for_hash, hash, std::piecewise_construct, + std::forward_as_tuple(std::forward

(key)), + std::forward_as_tuple(std::forward(args_value)...)); + } + + template + std::pair insert_impl(P&& value) { + const std::size_t hash = hash_key(KeySelect()(value)); + const std::size_t ibucket_for_hash = bucket_for_hash(hash); + + // Check if already presents + auto it_find = find_impl(KeySelect()(value), hash, m_first_or_empty_bucket + ibucket_for_hash); + if(it_find != end()) { + return std::make_pair(it_find, false); + } + + + return insert_impl(ibucket_for_hash, hash, std::forward

(value)); + } + + template + std::pair insert_impl(std::size_t ibucket_for_hash, std::size_t hash, Args&&... value_type_args) { + if((m_nb_elements - m_overflow_elements.size()) >= m_max_load_threshold_rehash) { + rehash(GrowthPolicy::next_bucket_count()); + ibucket_for_hash = bucket_for_hash(hash); + } + + std::size_t ibucket_empty = find_empty_bucket(ibucket_for_hash); + if(ibucket_empty < m_buckets.size()) { + do { + tsl_assert(ibucket_empty >= ibucket_for_hash); + + // Empty bucket is in range of NeighborhoodSize, use it + if(ibucket_empty - ibucket_for_hash < NeighborhoodSize) { + auto it = insert_in_bucket(ibucket_empty, ibucket_for_hash, + hash, std::forward(value_type_args)...); + return std::make_pair(iterator(it, m_buckets.end(), m_overflow_elements.begin()), true); + } + } + // else, try to swap values to get a closer empty bucket + while(swap_empty_bucket_closer(ibucket_empty)); + } + + // Load factor is too low or a rehash will not change the neighborhood, put the value in overflow list + if(size() < m_min_load_threshold_rehash || !will_neighborhood_change_on_rehash(ibucket_for_hash)) { + auto it = insert_in_overflow(ibucket_for_hash, std::forward(value_type_args)...); + return std::make_pair(iterator(m_buckets.end(), m_buckets.end(), it), true); + } + + rehash(GrowthPolicy::next_bucket_count()); + ibucket_for_hash = bucket_for_hash(hash); + + return insert_impl(ibucket_for_hash, hash, std::forward(value_type_args)...); + } + + /* + * Return true if a rehash will change the position of a key-value in the neighborhood of + * ibucket_neighborhood_check. In this case a rehash is needed instead of puting the value in overflow list. + */ + bool will_neighborhood_change_on_rehash(size_t ibucket_neighborhood_check) const { + std::size_t expand_bucket_count = GrowthPolicy::next_bucket_count(); + GrowthPolicy expand_growth_policy(expand_bucket_count); + + const bool use_stored_hash = USE_STORED_HASH_ON_REHASH(expand_bucket_count); + for(size_t ibucket = ibucket_neighborhood_check; + ibucket < m_buckets.size() && (ibucket - ibucket_neighborhood_check) < NeighborhoodSize; + ++ibucket) + { + tsl_assert(!m_buckets[ibucket].empty()); + + const size_t hash = use_stored_hash? + m_buckets[ibucket].truncated_bucket_hash(): + hash_key(KeySelect()(m_buckets[ibucket].value())); + if(bucket_for_hash(hash) != expand_growth_policy.bucket_for_hash(hash)) { + return true; + } + } + + return false; + } + + /* + * Return the index of an empty bucket in m_buckets. + * If none, the returned index equals m_buckets.size() + */ + std::size_t find_empty_bucket(std::size_t ibucket_start) const { + const std::size_t limit = std::min(ibucket_start + MAX_PROBES_FOR_EMPTY_BUCKET, m_buckets.size()); + for(; ibucket_start < limit; ibucket_start++) { + if(m_buckets[ibucket_start].empty()) { + return ibucket_start; + } + } + + return m_buckets.size(); + } + + /* + * Insert value in ibucket_empty where value originally belongs to ibucket_for_hash + * + * Return bucket iterator to ibucket_empty + */ + template + iterator_buckets insert_in_bucket(std::size_t ibucket_empty, std::size_t ibucket_for_hash, + std::size_t hash, Args&&... value_type_args) + { + tsl_assert(ibucket_empty >= ibucket_for_hash ); + tsl_assert(m_buckets[ibucket_empty].empty()); + m_buckets[ibucket_empty].set_value_of_empty_bucket(hopscotch_bucket::truncate_hash(hash), std::forward(value_type_args)...); + + tsl_assert(!m_buckets[ibucket_for_hash].empty()); + m_buckets[ibucket_for_hash].toggle_neighbor_presence(ibucket_empty - ibucket_for_hash); + m_nb_elements++; + + return m_buckets.begin() + ibucket_empty; + } + + template::value>::type* = nullptr> + iterator_overflow insert_in_overflow(std::size_t ibucket_for_hash, Args&&... value_type_args) { + auto it = m_overflow_elements.emplace(m_overflow_elements.end(), std::forward(value_type_args)...); + + m_buckets[ibucket_for_hash].set_overflow(true); + m_nb_elements++; + + return it; + } + + template::value>::type* = nullptr> + iterator_overflow insert_in_overflow(std::size_t ibucket_for_hash, Args&&... value_type_args) { + auto it = m_overflow_elements.emplace(std::forward(value_type_args)...).first; + + m_buckets[ibucket_for_hash].set_overflow(true); + m_nb_elements++; + + return it; + } + + /* + * Try to swap the bucket ibucket_empty_in_out with a bucket preceding it while keeping the neighborhood + * conditions correct. + * + * If a swap was possible, the position of ibucket_empty_in_out will be closer to 0 and true will re returned. + */ + bool swap_empty_bucket_closer(std::size_t& ibucket_empty_in_out) { + tsl_assert(ibucket_empty_in_out >= NeighborhoodSize); + const std::size_t neighborhood_start = ibucket_empty_in_out - NeighborhoodSize + 1; + + for(std::size_t to_check = neighborhood_start; to_check < ibucket_empty_in_out; to_check++) { + neighborhood_bitmap neighborhood_infos = m_buckets[to_check].neighborhood_infos(); + std::size_t to_swap = to_check; + + while(neighborhood_infos != 0 && to_swap < ibucket_empty_in_out) { + if((neighborhood_infos & 1) == 1) { + tsl_assert(m_buckets[ibucket_empty_in_out].empty()); + tsl_assert(!m_buckets[to_swap].empty()); + + m_buckets[to_swap].swap_value_into_empty_bucket(m_buckets[ibucket_empty_in_out]); + + tsl_assert(!m_buckets[to_check].check_neighbor_presence(ibucket_empty_in_out - to_check)); + tsl_assert(m_buckets[to_check].check_neighbor_presence(to_swap - to_check)); + + m_buckets[to_check].toggle_neighbor_presence(ibucket_empty_in_out - to_check); + m_buckets[to_check].toggle_neighbor_presence(to_swap - to_check); + + + ibucket_empty_in_out = to_swap; + + return true; + } + + to_swap++; + neighborhood_infos = neighborhood_bitmap(neighborhood_infos >> 1); + } + } + + return false; + } + + + + template::value>::type* = nullptr> + typename U::value_type* find_value_impl(const K& key, std::size_t hash, hopscotch_bucket* bucket_for_hash) { + return const_cast( + static_cast(this)->find_value_impl(key, hash, bucket_for_hash)); + } + + /* + * Avoid the creation of an iterator to just get the value for operator[] and at() in maps. Faster this way. + * + * Return null if no value for the key (TODO use std::optional when available). + */ + template::value>::type* = nullptr> + const typename U::value_type* find_value_impl(const K& key, std::size_t hash, + const hopscotch_bucket* bucket_for_hash) const + { + const hopscotch_bucket* bucket_found = find_in_buckets(key, hash, bucket_for_hash); + if(bucket_found != nullptr) { + return std::addressof(ValueSelect()(bucket_found->value())); + } + + if(bucket_for_hash->has_overflow()) { + auto it_overflow = find_in_overflow(key); + if(it_overflow != m_overflow_elements.end()) { + return std::addressof(ValueSelect()(*it_overflow)); + } + } + + return nullptr; + } + + template + size_type count_impl(const K& key, std::size_t hash, const hopscotch_bucket* bucket_for_hash) const { + if(find_in_buckets(key, hash, bucket_for_hash) != nullptr) { + return 1; + } + else if(bucket_for_hash->has_overflow() && find_in_overflow(key) != m_overflow_elements.cend()) { + return 1; + } + else { + return 0; + } + } + + template + iterator find_impl(const K& key, std::size_t hash, hopscotch_bucket* bucket_for_hash) { + hopscotch_bucket* bucket_found = find_in_buckets(key, hash, bucket_for_hash); + if(bucket_found != nullptr) { + return iterator(m_buckets.begin() + std::distance(m_buckets.data(), bucket_found), + m_buckets.end(), m_overflow_elements.begin()); + } + + if(!bucket_for_hash->has_overflow()) { + return end(); + } + + return iterator(m_buckets.end(), m_buckets.end(), find_in_overflow(key)); + } + + template + const_iterator find_impl(const K& key, std::size_t hash, const hopscotch_bucket* bucket_for_hash) const { + const hopscotch_bucket* bucket_found = find_in_buckets(key, hash, bucket_for_hash); + if(bucket_found != nullptr) { + return const_iterator(m_buckets.cbegin() + std::distance(m_buckets.data(), bucket_found), + m_buckets.cend(), m_overflow_elements.cbegin()); + } + + if(!bucket_for_hash->has_overflow()) { + return cend(); + } + + + return const_iterator(m_buckets.cend(), m_buckets.cend(), find_in_overflow(key)); + } + + template + hopscotch_bucket* find_in_buckets(const K& key, std::size_t hash, hopscotch_bucket* bucket_for_hash) { + const hopscotch_bucket* bucket_found = + static_cast(this)->find_in_buckets(key, hash, bucket_for_hash); + return const_cast(bucket_found); + } + + + /** + * Return a pointer to the bucket which has the value, nullptr otherwise. + */ + template + const hopscotch_bucket* find_in_buckets(const K& key, std::size_t hash, const hopscotch_bucket* bucket_for_hash) const { + (void) hash; // Avoid warning of unused variable when StoreHash is false; + + // TODO Try to optimize the function. + // I tried to use ffs and __builtin_ffs functions but I could not reduce the time the function + // takes with -march=native + + neighborhood_bitmap neighborhood_infos = bucket_for_hash->neighborhood_infos(); + while(neighborhood_infos != 0) { + if((neighborhood_infos & 1) == 1) { + // Check StoreHash before calling bucket_hash_equal. Functionally it doesn't change anythin. + // If StoreHash is false, bucket_hash_equal is a no-op. Avoiding the call is there to help + // GCC optimizes `hash` parameter away, it seems to not be able to do without this hint. + if((!StoreHash || bucket_for_hash->bucket_hash_equal(hash)) && + compare_keys(KeySelect()(bucket_for_hash->value()), key)) + { + return bucket_for_hash; + } + } + + ++bucket_for_hash; + neighborhood_infos = neighborhood_bitmap(neighborhood_infos >> 1); + } + + return nullptr; + } + + + + template::value>::type* = nullptr> + iterator_overflow find_in_overflow(const K& key) { + return std::find_if(m_overflow_elements.begin(), m_overflow_elements.end(), + [&](const value_type& value) { + return compare_keys(key, KeySelect()(value)); + }); + } + + template::value>::type* = nullptr> + const_iterator_overflow find_in_overflow(const K& key) const { + return std::find_if(m_overflow_elements.cbegin(), m_overflow_elements.cend(), + [&](const value_type& value) { + return compare_keys(key, KeySelect()(value)); + }); + } + + template::value>::type* = nullptr> + iterator_overflow find_in_overflow(const K& key) { + return m_overflow_elements.find(key); + } + + template::value>::type* = nullptr> + const_iterator_overflow find_in_overflow(const K& key) const { + return m_overflow_elements.find(key); + } + + + + template::value>::type* = nullptr> + hopscotch_hash new_hopscotch_hash(size_type bucket_count) { + return hopscotch_hash(bucket_count, static_cast(*this), static_cast(*this), + get_allocator(), m_max_load_factor); + } + + template::value>::type* = nullptr> + hopscotch_hash new_hopscotch_hash(size_type bucket_count) { + return hopscotch_hash(bucket_count, static_cast(*this), static_cast(*this), + get_allocator(), m_max_load_factor, m_overflow_elements.key_comp()); + } + +public: + static const size_type DEFAULT_INIT_BUCKETS_SIZE = 16; + static constexpr float DEFAULT_MAX_LOAD_FACTOR = (NeighborhoodSize <= 30)?0.8f:0.9f; + +private: + static const std::size_t MAX_PROBES_FOR_EMPTY_BUCKET = 12*NeighborhoodSize; + static constexpr float MIN_LOAD_FACTOR_FOR_REHASH = 0.1f; + + static bool USE_STORED_HASH_ON_REHASH(size_type bucket_count) { + (void) bucket_count; + if(StoreHash && is_power_of_two_policy::value) { + tsl_assert(bucket_count > 0); + return (bucket_count - 1) <= std::numeric_limits::max(); + } + else { + return false; + } + } + + /** + * Return an always valid pointer to an static empty hopscotch_bucket. + */ + hopscotch_bucket* static_empty_bucket_ptr() { + static hopscotch_bucket empty_bucket; + return &empty_bucket; + } + +private: + buckets_container_type m_buckets; + overflow_container_type m_overflow_elements; + + /** + * Points to m_buckets.data() if !m_buckets.empty() otherwise points to static_empty_bucket_ptr. + * This variable is useful to avoid the cost of checking if m_buckets is empty when trying + * to find an element. + */ + hopscotch_bucket* m_first_or_empty_bucket; + + size_type m_nb_elements; + + float m_max_load_factor; + + /** + * Max size of the hash table before a rehash occurs automatically to grow the table. + */ + size_type m_max_load_threshold_rehash; + + /** + * Min size of the hash table before a rehash can occurs automatically (except if m_max_load_threshold_rehash os reached). + * If the neighborhood of a bucket is full before the min is reacher, the elements are put into m_overflow_elements. + */ + size_type m_min_load_threshold_rehash; +}; + +} // end namespace detail_hopscotch_hash + + +} // end namespace tsl + +#endif diff --git a/include/tsl/hopscotch_map.h b/include/tsl/hopscotch_map.h new file mode 100644 index 0000000..acd7a79 --- /dev/null +++ b/include/tsl/hopscotch_map.h @@ -0,0 +1,679 @@ +/** + * MIT License + * + * Copyright (c) 2017 Tessil + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef TSL_HOPSCOTCH_MAP_H +#define TSL_HOPSCOTCH_MAP_H + + +#include +#include +#include +#include +#include +#include +#include +#include +#include "hopscotch_hash.h" + + +namespace tsl { + +/** + * Implementation of a hash map using the hopscotch hashing algorithm. + * + * The Key and the value T must be either nothrow move-constructible, copy-constuctible or both. + * + * The size of the neighborhood (NeighborhoodSize) must be > 0 and <= 62 if StoreHash is false. + * When StoreHash is true, 32-bits of the hash will be stored alongside the neighborhood limiting + * the NeighborhoodSize to <= 30. There is no memory usage difference between + * 'NeighborhoodSize 62; StoreHash false' and 'NeighborhoodSize 30; StoreHash true'. + * + * Storing the hash may improve performance on insert during the rehash process if the hash takes time + * to compute. It may also improve read performance if the KeyEqual function takes time (or incurs a cache-miss). + * If used with simple Hash and KeyEqual it may slow things down. + * + * StoreHash can only be set if the GrowthPolicy is set to tsl::power_of_two_growth_policy. + * + * GrowthPolicy defines how the map grows and consequently how a hash value is mapped to a bucket. + * By default the map uses tsl::power_of_two_growth_policy. This policy keeps the number of buckets + * to a power of two and uses a mask to map the hash to a bucket instead of the slow modulo. + * You may define your own growth policy, check tsl::power_of_two_growth_policy for the interface. + * + * If the destructors of Key or T throw an exception, behaviour of the class is undefined. + * + * Iterators invalidation: + * - clear, operator=, reserve, rehash: always invalidate the iterators. + * - insert, emplace, emplace_hint, operator[]: if there is an effective insert, invalidate the iterators + * if a displacement is needed to resolve a collision (which mean that most of the time, + * insert will invalidate the iterators). Or if there is a rehash. + * - erase: iterator on the erased element is the only one which become invalid. + */ +template, + class KeyEqual = std::equal_to, + class Allocator = std::allocator>, + unsigned int NeighborhoodSize = 62, + bool StoreHash = false, + class GrowthPolicy = tsl::hh::power_of_two_growth_policy<2>> +class hopscotch_map { +private: + template + using has_is_transparent = tsl::detail_hopscotch_hash::has_is_transparent; + + class KeySelect { + public: + using key_type = Key; + + const key_type& operator()(const std::pair& key_value) const { + return key_value.first; + } + + key_type& operator()(std::pair& key_value) { + return key_value.first; + } + }; + + class ValueSelect { + public: + using value_type = T; + + const value_type& operator()(const std::pair& key_value) const { + return key_value.second; + } + + value_type& operator()(std::pair& key_value) { + return key_value.second; + } + }; + + + using overflow_container_type = std::list, Allocator>; + using ht = detail_hopscotch_hash::hopscotch_hash, KeySelect, ValueSelect, + Hash, KeyEqual, + Allocator, NeighborhoodSize, + StoreHash, GrowthPolicy, + overflow_container_type>; + +public: + using key_type = typename ht::key_type; + using mapped_type = T; + using value_type = typename ht::value_type; + using size_type = typename ht::size_type; + using difference_type = typename ht::difference_type; + using hasher = typename ht::hasher; + using key_equal = typename ht::key_equal; + using allocator_type = typename ht::allocator_type; + using reference = typename ht::reference; + using const_reference = typename ht::const_reference; + using pointer = typename ht::pointer; + using const_pointer = typename ht::const_pointer; + using iterator = typename ht::iterator; + using const_iterator = typename ht::const_iterator; + + + + /* + * Constructors + */ + hopscotch_map() : hopscotch_map(ht::DEFAULT_INIT_BUCKETS_SIZE) { + } + + explicit hopscotch_map(size_type bucket_count, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator()) : + m_ht(bucket_count, hash, equal, alloc, ht::DEFAULT_MAX_LOAD_FACTOR) + { + } + + hopscotch_map(size_type bucket_count, + const Allocator& alloc) : hopscotch_map(bucket_count, Hash(), KeyEqual(), alloc) + { + } + + hopscotch_map(size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : hopscotch_map(bucket_count, hash, KeyEqual(), alloc) + { + } + + explicit hopscotch_map(const Allocator& alloc) : hopscotch_map(ht::DEFAULT_INIT_BUCKETS_SIZE, alloc) { + } + + template + hopscotch_map(InputIt first, InputIt last, + size_type bucket_count = ht::DEFAULT_INIT_BUCKETS_SIZE, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator()) : hopscotch_map(bucket_count, hash, equal, alloc) + { + insert(first, last); + } + + template + hopscotch_map(InputIt first, InputIt last, + size_type bucket_count, + const Allocator& alloc) : hopscotch_map(first, last, bucket_count, Hash(), KeyEqual(), alloc) + { + } + + template + hopscotch_map(InputIt first, InputIt last, + size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : hopscotch_map(first, last, bucket_count, hash, KeyEqual(), alloc) + { + } + + hopscotch_map(std::initializer_list init, + size_type bucket_count = ht::DEFAULT_INIT_BUCKETS_SIZE, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator()) : + hopscotch_map(init.begin(), init.end(), bucket_count, hash, equal, alloc) + { + } + + hopscotch_map(std::initializer_list init, + size_type bucket_count, + const Allocator& alloc) : + hopscotch_map(init.begin(), init.end(), bucket_count, Hash(), KeyEqual(), alloc) + { + } + + hopscotch_map(std::initializer_list init, + size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : + hopscotch_map(init.begin(), init.end(), bucket_count, hash, KeyEqual(), alloc) + { + } + + + hopscotch_map& operator=(std::initializer_list ilist) { + m_ht.clear(); + + m_ht.reserve(ilist.size()); + m_ht.insert(ilist.begin(), ilist.end()); + + return *this; + } + + allocator_type get_allocator() const { return m_ht.get_allocator(); } + + + /* + * Iterators + */ + iterator begin() noexcept { return m_ht.begin(); } + const_iterator begin() const noexcept { return m_ht.begin(); } + const_iterator cbegin() const noexcept { return m_ht.cbegin(); } + + iterator end() noexcept { return m_ht.end(); } + const_iterator end() const noexcept { return m_ht.end(); } + const_iterator cend() const noexcept { return m_ht.cend(); } + + + /* + * Capacity + */ + bool empty() const noexcept { return m_ht.empty(); } + size_type size() const noexcept { return m_ht.size(); } + size_type max_size() const noexcept { return m_ht.max_size(); } + + /* + * Modifiers + */ + void clear() noexcept { m_ht.clear(); } + + + + + std::pair insert(const value_type& value) { + return m_ht.insert(value); + } + + template::value>::type* = nullptr> + std::pair insert(P&& value) { + return m_ht.insert(std::forward

(value)); + } + + std::pair insert(value_type&& value) { + return m_ht.insert(std::move(value)); + } + + + iterator insert(const_iterator hint, const value_type& value) { + return m_ht.insert(hint, value); + } + + template::value>::type* = nullptr> + iterator insert(const_iterator hint, P&& value) { + return m_ht.insert(hint, std::forward

(value)); + } + + iterator insert(const_iterator hint, value_type&& value) { + return m_ht.insert(hint, std::move(value)); + } + + + template + void insert(InputIt first, InputIt last) { + m_ht.insert(first, last); + } + + void insert(std::initializer_list ilist) { + m_ht.insert(ilist.begin(), ilist.end()); + } + + + + + template + std::pair insert_or_assign(const key_type& k, M&& obj) { + return m_ht.insert_or_assign(k, std::forward(obj)); + } + + template + std::pair insert_or_assign(key_type&& k, M&& obj) { + return m_ht.insert_or_assign(std::move(k), std::forward(obj)); + } + + template + iterator insert_or_assign(const_iterator hint, const key_type& k, M&& obj) { + return m_ht.insert_or_assign(hint, k, std::forward(obj)); + } + + template + iterator insert_or_assign(const_iterator hint, key_type&& k, M&& obj) { + return m_ht.insert_or_assign(hint, std::move(k), std::forward(obj)); + } + + + + + /** + * Due to the way elements are stored, emplace will need to move or copy the key-value once. + * The method is equivalent to insert(value_type(std::forward(args)...)); + * + * Mainly here for compatibility with the std::unordered_map interface. + */ + template + std::pair emplace(Args&&... args) { + return m_ht.emplace(std::forward(args)...); + } + + + + + /** + * Due to the way elements are stored, emplace_hint will need to move or copy the key-value once. + * The method is equivalent to insert(hint, value_type(std::forward(args)...)); + * + * Mainly here for compatibility with the std::unordered_map interface. + */ + template + iterator emplace_hint(const_iterator hint, Args&&... args) { + return m_ht.emplace_hint(hint, std::forward(args)...); + } + + + + + template + std::pair try_emplace(const key_type& k, Args&&... args) { + return m_ht.try_emplace(k, std::forward(args)...); + } + + template + std::pair try_emplace(key_type&& k, Args&&... args) { + return m_ht.try_emplace(std::move(k), std::forward(args)...); + } + + template + iterator try_emplace(const_iterator hint, const key_type& k, Args&&... args) { + return m_ht.try_emplace(hint, k, std::forward(args)...); + } + + template + iterator try_emplace(const_iterator hint, key_type&& k, Args&&... args) { + return m_ht.try_emplace(hint, std::move(k), std::forward(args)...); + } + + + + + iterator erase(iterator pos) { return m_ht.erase(pos); } + iterator erase(const_iterator pos) { return m_ht.erase(pos); } + iterator erase(const_iterator first, const_iterator last) { return m_ht.erase(first, last); } + size_type erase(const key_type& key) { return m_ht.erase(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup to the value if you already have the hash. + */ + size_type erase(const key_type& key, std::size_t precalculated_hash) { + return m_ht.erase(key, precalculated_hash); + } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent exists. + * If so, K must be hashable and comparable to Key. + */ + template::value>::type* = nullptr> + size_type erase(const K& key) { return m_ht.erase(key); } + + /** + * @copydoc erase(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup to the value if you already have the hash. + */ + template::value>::type* = nullptr> + size_type erase(const K& key, std::size_t precalculated_hash) { + return m_ht.erase(key, precalculated_hash); + } + + + + + void swap(hopscotch_map& other) { other.m_ht.swap(m_ht); } + + /* + * Lookup + */ + T& at(const Key& key) { return m_ht.at(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + T& at(const Key& key, std::size_t precalculated_hash) { return m_ht.at(key, precalculated_hash); } + + + const T& at(const Key& key) const { return m_ht.at(key); } + + /** + * @copydoc at(const Key& key, std::size_t precalculated_hash) + */ + const T& at(const Key& key, std::size_t precalculated_hash) const { return m_ht.at(key, precalculated_hash); } + + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent exists. + * If so, K must be hashable and comparable to Key. + */ + template::value>::type* = nullptr> + T& at(const K& key) { return m_ht.at(key); } + + /** + * @copydoc at(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value>::type* = nullptr> + T& at(const K& key, std::size_t precalculated_hash) { return m_ht.at(key, precalculated_hash); } + + + /** + * @copydoc at(const K& key) + */ + template::value>::type* = nullptr> + const T& at(const K& key) const { return m_ht.at(key); } + + /** + * @copydoc at(const K& key, std::size_t precalculated_hash) + */ + template::value>::type* = nullptr> + const T& at(const K& key, std::size_t precalculated_hash) const { return m_ht.at(key, precalculated_hash); } + + + + + T& operator[](const Key& key) { return m_ht[key]; } + T& operator[](Key&& key) { return m_ht[std::move(key)]; } + + + + + size_type count(const Key& key) const { return m_ht.count(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + size_type count(const Key& key, std::size_t precalculated_hash) const { + return m_ht.count(key, precalculated_hash); + } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent exists. + * If so, K must be hashable and comparable to Key. + */ + template::value>::type* = nullptr> + size_type count(const K& key) const { return m_ht.count(key); } + + /** + * @copydoc count(const K& key) const + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value>::type* = nullptr> + size_type count(const K& key, std::size_t precalculated_hash) const { return m_ht.count(key, precalculated_hash); } + + + + + iterator find(const Key& key) { return m_ht.find(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + iterator find(const Key& key, std::size_t precalculated_hash) { return m_ht.find(key, precalculated_hash); } + + const_iterator find(const Key& key) const { return m_ht.find(key); } + + /** + * @copydoc find(const Key& key, std::size_t precalculated_hash) + */ + const_iterator find(const Key& key, std::size_t precalculated_hash) const { + return m_ht.find(key, precalculated_hash); + } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent exists. + * If so, K must be hashable and comparable to Key. + */ + template::value>::type* = nullptr> + iterator find(const K& key) { return m_ht.find(key); } + + /** + * @copydoc find(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value>::type* = nullptr> + iterator find(const K& key, std::size_t precalculated_hash) { return m_ht.find(key, precalculated_hash); } + + /** + * @copydoc find(const K& key) + */ + template::value>::type* = nullptr> + const_iterator find(const K& key) const { return m_ht.find(key); } + + /** + * @copydoc find(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value>::type* = nullptr> + const_iterator find(const K& key, std::size_t precalculated_hash) const { + return m_ht.find(key, precalculated_hash); + } + + + + + std::pair equal_range(const Key& key) { return m_ht.equal_range(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + std::pair equal_range(const Key& key, std::size_t precalculated_hash) { + return m_ht.equal_range(key, precalculated_hash); + } + + std::pair equal_range(const Key& key) const { return m_ht.equal_range(key); } + + /** + * @copydoc equal_range(const Key& key, std::size_t precalculated_hash) + */ + std::pair equal_range(const Key& key, std::size_t precalculated_hash) const { + return m_ht.equal_range(key, precalculated_hash); + } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent exists. + * If so, K must be hashable and comparable to Key. + */ + template::value>::type* = nullptr> + std::pair equal_range(const K& key) { return m_ht.equal_range(key); } + + + /** + * @copydoc equal_range(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value>::type* = nullptr> + std::pair equal_range(const K& key, std::size_t precalculated_hash) { + return m_ht.equal_range(key, precalculated_hash); + } + + /** + * @copydoc equal_range(const K& key) + */ + template::value>::type* = nullptr> + std::pair equal_range(const K& key) const { return m_ht.equal_range(key); } + + /** + * @copydoc equal_range(const K& key, std::size_t precalculated_hash) + */ + template::value>::type* = nullptr> + std::pair equal_range(const K& key, std::size_t precalculated_hash) const { + return m_ht.equal_range(key, precalculated_hash); + } + + + + + /* + * Bucket interface + */ + size_type bucket_count() const { return m_ht.bucket_count(); } + size_type max_bucket_count() const { return m_ht.max_bucket_count(); } + + + /* + * Hash policy + */ + float load_factor() const { return m_ht.load_factor(); } + float max_load_factor() const { return m_ht.max_load_factor(); } + void max_load_factor(float ml) { m_ht.max_load_factor(ml); } + + void rehash(size_type count_) { m_ht.rehash(count_); } + void reserve(size_type count_) { m_ht.reserve(count_); } + + + /* + * Observers + */ + hasher hash_function() const { return m_ht.hash_function(); } + key_equal key_eq() const { return m_ht.key_eq(); } + + /* + * Other + */ + + /** + * Convert a const_iterator to an iterator. + */ + iterator mutable_iterator(const_iterator pos) { + return m_ht.mutable_iterator(pos); + } + + size_type overflow_size() const noexcept { return m_ht.overflow_size(); } + + friend bool operator==(const hopscotch_map& lhs, const hopscotch_map& rhs) { + if(lhs.size() != rhs.size()) { + return false; + } + + for(const auto& element_lhs : lhs) { + const auto it_element_rhs = rhs.find(element_lhs.first); + if(it_element_rhs == rhs.cend() || element_lhs.second != it_element_rhs->second) { + return false; + } + } + + return true; + } + + friend bool operator!=(const hopscotch_map& lhs, const hopscotch_map& rhs) { + return !operator==(lhs, rhs); + } + + friend void swap(hopscotch_map& lhs, hopscotch_map& rhs) { + lhs.swap(rhs); + } + + + +private: + ht m_ht; +}; + + +/** + * Same as `tsl::hopscotch_map`. + */ +template, + class KeyEqual = std::equal_to, + class Allocator = std::allocator>, + unsigned int NeighborhoodSize = 62, + bool StoreHash = false> +using hopscotch_pg_map = hopscotch_map; + +} // end namespace tsl + +#endif diff --git a/include/tsl/hopscotch_set.h b/include/tsl/hopscotch_set.h new file mode 100644 index 0000000..4013d33 --- /dev/null +++ b/include/tsl/hopscotch_set.h @@ -0,0 +1,525 @@ +/** + * MIT License + * + * Copyright (c) 2017 Tessil + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef TSL_HOPSCOTCH_SET_H +#define TSL_HOPSCOTCH_SET_H + + +#include +#include +#include +#include +#include +#include +#include +#include +#include "hopscotch_hash.h" + + +namespace tsl { + +/** + * Implementation of a hash set using the hopscotch hashing algorithm. + * + * The Key must be either nothrow move-constructible, copy-constuctible or both. + * + * The size of the neighborhood (NeighborhoodSize) must be > 0 and <= 62 if StoreHash is false. + * When StoreHash is true, 32-bits of the hash will be stored alongside the neighborhood limiting + * the NeighborhoodSize to <= 30. There is no memory usage difference between + * 'NeighborhoodSize 62; StoreHash false' and 'NeighborhoodSize 30; StoreHash true'. + * + * Storing the hash may improve performance on insert during the rehash process if the hash takes time + * to compute. It may also improve read performance if the KeyEqual function takes time (or incurs a cache-miss). + * If used with simple Hash and KeyEqual it may slow things down. + * + * StoreHash can only be set if the GrowthPolicy is set to tsl::power_of_two_growth_policy. + * + * GrowthPolicy defines how the set grows and consequently how a hash value is mapped to a bucket. + * By default the set uses tsl::power_of_two_growth_policy. This policy keeps the number of buckets + * to a power of two and uses a mask to set the hash to a bucket instead of the slow modulo. + * You may define your own growth policy, check tsl::power_of_two_growth_policy for the interface. + * + * If the destructor of Key throws an exception, behaviour of the class is undefined. + * + * Iterators invalidation: + * - clear, operator=, reserve, rehash: always invalidate the iterators. + * - insert, emplace, emplace_hint, operator[]: if there is an effective insert, invalidate the iterators + * if a displacement is needed to resolve a collision (which mean that most of the time, + * insert will invalidate the iterators). Or if there is a rehash. + * - erase: iterator on the erased element is the only one which become invalid. + */ +template, + class KeyEqual = std::equal_to, + class Allocator = std::allocator, + unsigned int NeighborhoodSize = 62, + bool StoreHash = false, + class GrowthPolicy = tsl::hh::power_of_two_growth_policy<2>> +class hopscotch_set { +private: + template + using has_is_transparent = tsl::detail_hopscotch_hash::has_is_transparent; + + class KeySelect { + public: + using key_type = Key; + + const key_type& operator()(const Key& key) const { + return key; + } + + key_type& operator()(Key& key) { + return key; + } + }; + + + using overflow_container_type = std::list; + using ht = detail_hopscotch_hash::hopscotch_hash; + +public: + using key_type = typename ht::key_type; + using value_type = typename ht::value_type; + using size_type = typename ht::size_type; + using difference_type = typename ht::difference_type; + using hasher = typename ht::hasher; + using key_equal = typename ht::key_equal; + using allocator_type = typename ht::allocator_type; + using reference = typename ht::reference; + using const_reference = typename ht::const_reference; + using pointer = typename ht::pointer; + using const_pointer = typename ht::const_pointer; + using iterator = typename ht::iterator; + using const_iterator = typename ht::const_iterator; + + + /* + * Constructors + */ + hopscotch_set() : hopscotch_set(ht::DEFAULT_INIT_BUCKETS_SIZE) { + } + + explicit hopscotch_set(size_type bucket_count, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator()) : + m_ht(bucket_count, hash, equal, alloc, ht::DEFAULT_MAX_LOAD_FACTOR) + { + } + + hopscotch_set(size_type bucket_count, + const Allocator& alloc) : hopscotch_set(bucket_count, Hash(), KeyEqual(), alloc) + { + } + + hopscotch_set(size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : hopscotch_set(bucket_count, hash, KeyEqual(), alloc) + { + } + + explicit hopscotch_set(const Allocator& alloc) : hopscotch_set(ht::DEFAULT_INIT_BUCKETS_SIZE, alloc) { + } + + template + hopscotch_set(InputIt first, InputIt last, + size_type bucket_count = ht::DEFAULT_INIT_BUCKETS_SIZE, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator()) : hopscotch_set(bucket_count, hash, equal, alloc) + { + insert(first, last); + } + + template + hopscotch_set(InputIt first, InputIt last, + size_type bucket_count, + const Allocator& alloc) : hopscotch_set(first, last, bucket_count, Hash(), KeyEqual(), alloc) + { + } + + template + hopscotch_set(InputIt first, InputIt last, + size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : hopscotch_set(first, last, bucket_count, hash, KeyEqual(), alloc) + { + } + + hopscotch_set(std::initializer_list init, + size_type bucket_count = ht::DEFAULT_INIT_BUCKETS_SIZE, + const Hash& hash = Hash(), + const KeyEqual& equal = KeyEqual(), + const Allocator& alloc = Allocator()) : + hopscotch_set(init.begin(), init.end(), bucket_count, hash, equal, alloc) + { + } + + hopscotch_set(std::initializer_list init, + size_type bucket_count, + const Allocator& alloc) : + hopscotch_set(init.begin(), init.end(), bucket_count, Hash(), KeyEqual(), alloc) + { + } + + hopscotch_set(std::initializer_list init, + size_type bucket_count, + const Hash& hash, + const Allocator& alloc) : + hopscotch_set(init.begin(), init.end(), bucket_count, hash, KeyEqual(), alloc) + { + } + + + hopscotch_set& operator=(std::initializer_list ilist) { + m_ht.clear(); + + m_ht.reserve(ilist.size()); + m_ht.insert(ilist.begin(), ilist.end()); + + return *this; + } + + allocator_type get_allocator() const { return m_ht.get_allocator(); } + + + /* + * Iterators + */ + iterator begin() noexcept { return m_ht.begin(); } + const_iterator begin() const noexcept { return m_ht.begin(); } + const_iterator cbegin() const noexcept { return m_ht.cbegin(); } + + iterator end() noexcept { return m_ht.end(); } + const_iterator end() const noexcept { return m_ht.end(); } + const_iterator cend() const noexcept { return m_ht.cend(); } + + + /* + * Capacity + */ + bool empty() const noexcept { return m_ht.empty(); } + size_type size() const noexcept { return m_ht.size(); } + size_type max_size() const noexcept { return m_ht.max_size(); } + + /* + * Modifiers + */ + void clear() noexcept { m_ht.clear(); } + + + + + std::pair insert(const value_type& value) { return m_ht.insert(value); } + std::pair insert(value_type&& value) { return m_ht.insert(std::move(value)); } + + iterator insert(const_iterator hint, const value_type& value) { return m_ht.insert(hint, value); } + iterator insert(const_iterator hint, value_type&& value) { return m_ht.insert(hint, std::move(value)); } + + template + void insert(InputIt first, InputIt last) { m_ht.insert(first, last); } + void insert(std::initializer_list ilist) { m_ht.insert(ilist.begin(), ilist.end()); } + + + + + /** + * Due to the way elements are stored, emplace will need to move or copy the key-value once. + * The method is equivalent to insert(value_type(std::forward(args)...)); + * + * Mainly here for compatibility with the std::unordered_map interface. + */ + template + std::pair emplace(Args&&... args) { return m_ht.emplace(std::forward(args)...); } + + + + + /** + * Due to the way elements are stored, emplace_hint will need to move or copy the key-value once. + * The method is equivalent to insert(hint, value_type(std::forward(args)...)); + * + * Mainly here for compatibility with the std::unordered_map interface. + */ + template + iterator emplace_hint(const_iterator hint, Args&&... args) { + return m_ht.emplace_hint(hint, std::forward(args)...); + } + + + + + iterator erase(iterator pos) { return m_ht.erase(pos); } + iterator erase(const_iterator pos) { return m_ht.erase(pos); } + iterator erase(const_iterator first, const_iterator last) { return m_ht.erase(first, last); } + size_type erase(const key_type& key) { return m_ht.erase(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup to the value if you already have the hash. + */ + size_type erase(const key_type& key, std::size_t precalculated_hash) { + return m_ht.erase(key, precalculated_hash); + } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent exists. + * If so, K must be hashable and comparable to Key. + */ + template::value>::type* = nullptr> + size_type erase(const K& key) { return m_ht.erase(key); } + + /** + * @copydoc erase(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup to the value if you already have the hash. + */ + template::value>::type* = nullptr> + size_type erase(const K& key, std::size_t precalculated_hash) { + return m_ht.erase(key, precalculated_hash); + } + + + + + void swap(hopscotch_set& other) { other.m_ht.swap(m_ht); } + + + /* + * Lookup + */ + size_type count(const Key& key) const { return m_ht.count(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + size_type count(const Key& key, std::size_t precalculated_hash) const { return m_ht.count(key, precalculated_hash); } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent exists. + * If so, K must be hashable and comparable to Key. + */ + template::value>::type* = nullptr> + size_type count(const K& key) const { return m_ht.count(key); } + + /** + * @copydoc count(const K& key) const + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value>::type* = nullptr> + size_type count(const K& key, std::size_t precalculated_hash) const { return m_ht.count(key, precalculated_hash); } + + + + + iterator find(const Key& key) { return m_ht.find(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + iterator find(const Key& key, std::size_t precalculated_hash) { return m_ht.find(key, precalculated_hash); } + + const_iterator find(const Key& key) const { return m_ht.find(key); } + + /** + * @copydoc find(const Key& key, std::size_t precalculated_hash) + */ + const_iterator find(const Key& key, std::size_t precalculated_hash) const { return m_ht.find(key, precalculated_hash); } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent exists. + * If so, K must be hashable and comparable to Key. + */ + template::value>::type* = nullptr> + iterator find(const K& key) { return m_ht.find(key); } + + /** + * @copydoc find(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value>::type* = nullptr> + iterator find(const K& key, std::size_t precalculated_hash) { return m_ht.find(key, precalculated_hash); } + + /** + * @copydoc find(const K& key) + */ + template::value>::type* = nullptr> + const_iterator find(const K& key) const { return m_ht.find(key); } + + /** + * @copydoc find(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value>::type* = nullptr> + const_iterator find(const K& key, std::size_t precalculated_hash) const { return m_ht.find(key, precalculated_hash); } + + + + + std::pair equal_range(const Key& key) { return m_ht.equal_range(key); } + + /** + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + std::pair equal_range(const Key& key, std::size_t precalculated_hash) { + return m_ht.equal_range(key, precalculated_hash); + } + + std::pair equal_range(const Key& key) const { return m_ht.equal_range(key); } + + /** + * @copydoc equal_range(const Key& key, std::size_t precalculated_hash) + */ + std::pair equal_range(const Key& key, std::size_t precalculated_hash) const { + return m_ht.equal_range(key, precalculated_hash); + } + + /** + * This overload only participates in the overload resolution if the typedef KeyEqual::is_transparent exists. + * If so, K must be hashable and comparable to Key. + */ + template::value>::type* = nullptr> + std::pair equal_range(const K& key) { return m_ht.equal_range(key); } + + /** + * @copydoc equal_range(const K& key) + * + * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same + * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. + */ + template::value>::type* = nullptr> + std::pair equal_range(const K& key, std::size_t precalculated_hash) { + return m_ht.equal_range(key, precalculated_hash); + } + + /** + * @copydoc equal_range(const K& key) + */ + template::value>::type* = nullptr> + std::pair equal_range(const K& key) const { return m_ht.equal_range(key); } + + /** + * @copydoc equal_range(const K& key, std::size_t precalculated_hash) + */ + template::value>::type* = nullptr> + std::pair equal_range(const K& key, std::size_t precalculated_hash) const { + return m_ht.equal_range(key, precalculated_hash); + } + + + + + /* + * Bucket interface + */ + size_type bucket_count() const { return m_ht.bucket_count(); } + size_type max_bucket_count() const { return m_ht.max_bucket_count(); } + + + /* + * Hash policy + */ + float load_factor() const { return m_ht.load_factor(); } + float max_load_factor() const { return m_ht.max_load_factor(); } + void max_load_factor(float ml) { m_ht.max_load_factor(ml); } + + void rehash(size_type count_) { m_ht.rehash(count_); } + void reserve(size_type count_) { m_ht.reserve(count_); } + + + /* + * Observers + */ + hasher hash_function() const { return m_ht.hash_function(); } + key_equal key_eq() const { return m_ht.key_eq(); } + + + /* + * Other + */ + + /** + * Convert a const_iterator to an iterator. + */ + iterator mutable_iterator(const_iterator pos) { + return m_ht.mutable_iterator(pos); + } + + size_type overflow_size() const noexcept { return m_ht.overflow_size(); } + + friend bool operator==(const hopscotch_set& lhs, const hopscotch_set& rhs) { + if(lhs.size() != rhs.size()) { + return false; + } + + for(const auto& element_lhs : lhs) { + const auto it_element_rhs = rhs.find(element_lhs); + if(it_element_rhs == rhs.cend()) { + return false; + } + } + + return true; + } + + friend bool operator!=(const hopscotch_set& lhs, const hopscotch_set& rhs) { + return !operator==(lhs, rhs); + } + + friend void swap(hopscotch_set& lhs, hopscotch_set& rhs) { + lhs.swap(rhs); + } + +private: + ht m_ht; +}; + + +/** + * Same as `tsl::hopscotch_set`. + */ +template, + class KeyEqual = std::equal_to, + class Allocator = std::allocator, + unsigned int NeighborhoodSize = 62, + bool StoreHash = false> +using hopscotch_pg_set = hopscotch_set; + +} // end namespace tsl + +#endif diff --git a/src/validateMSF.cc b/src/validateMSF.cc index a2b0df3..330ad55 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -12,6 +12,7 @@ #include "CLI/Timer.hpp" #include "kmer.h" #include "lrucache.hpp" +#include "hashutil.h" struct QueryStats { uint32_t cnt = 0, cacheCntr = 0, noCacheCntr{0}; @@ -25,6 +26,20 @@ struct QueryStats { std::unordered_map numOcc; }; + +namespace mantis{ + namespace util { + class int_hasher { + public: + size_t operator()(uint64_t i) const { + return HashUtil::MurmurHash64A(reinterpret_cast(&i), sizeof(i), 8675309); + } + }; + } +} + +using LRUCacheMap = cache::lru_cache , mantis::util::int_hasher>; + class RankScores { public: RankScores(uint64_t nranks) {rs_.resize(nranks);} @@ -79,10 +94,10 @@ class MSFQuery { } std::vector buildColor(uint64_t eqid, QueryStats &queryStats, - cache::lru_cache > *lru_cache, + LRUCacheMap *lru_cache, RankScores* rs, bool all = true) { - + (void)rs; std::vector flips(numSamples); std::vector xorflips(numSamples, 0); uint64_t i{eqid}, from{0}, to{0}; @@ -90,7 +105,7 @@ class MSFQuery { std::vector froms; std::vector parents; froms.reserve(12000); - parents.reserve(12000); + //parents.reserve(12000); queryStats.totEqcls++; auto sstart = std::chrono::system_clock::now(); bool foundCache = false; @@ -111,30 +126,32 @@ class MSFQuery { else from = 0; froms.push_back(from); - parents.push_back(i); + //parents.push_back(i); //queryStats.numOcc[i]++; i = iparent; iparent = parentbv[i]; ++queryStats.totSel; ++height; } - if (!foundCache and i == iparent and i != zero) { + if (!foundCache and i != zero) { if (i > 0) from = sbbv(i) + 1; else from = 0; froms.push_back(from); - parents.push_back(i); + //parents.push_back(i); ++queryStats.totSel; queryStats.rootedNonZero++; ++height; } // update ranks + /* if (rs) { for (size_t idx = 0; idx < froms.size(); ++idx) { (*rs)[(height - idx)][parents[idx]]++; } } + */ queryStats.selectTime += std::chrono::system_clock::now() - sstart; auto fstart = std::chrono::system_clock::now(); @@ -197,7 +214,7 @@ class MSFQuery { mantis::QueryResult findSamples(const mantis::QuerySet &kmers, CQF &dbg, MSFQuery &msfQuery, - cache::lru_cache > &lru_cache, + LRUCacheMap& lru_cache, RankScores& rs, QueryStats &queryStats) { std::unordered_map query_eqclass_map; @@ -372,7 +389,7 @@ int main(int argc, char *argv[]) { uint64_t eqCount = msfQuery.parentbv.size() - 1; std::cerr << "total # of equivalence classes is : " << eqCount << "\n"; - cache::lru_cache > cache_lru(100000); + LRUCacheMap cache_lru(100000); QueryStats queryStats; if (selected == mode::validate) { @@ -443,8 +460,7 @@ int main(int argc, char *argv[]) { 20, total_kmers); std::cerr << "Done loading query file : # of seqs: " << multi_kmers.size() << "\n"; - RankScores rs(12000); - std::cerr << "here\n"; + RankScores rs(1); std::ofstream opfile(opt.outputfile); { CLI::AutoTimer timer{"Query time ", CLI::Timer::Big}; From ddcbd681946c87bc8b068d31bb23db76377c60a9 Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 25 Jul 2018 21:00:42 -0400 Subject: [PATCH 047/122] remove some allocs --- include/coloreddbg.h | 6 +++--- include/common_types.h | 3 ++- src/validateMSF.cc | 18 ++++++++++-------- src/validatemantis.cc | 4 ++-- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 8b9f1a4..6064e5a 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -75,7 +75,7 @@ class ColoredDbg { uint32_t seed(void) const { return dbg.seed(); } uint64_t range(void) const { return dbg.range(); } - std::unordered_map + mantis::QueryResult find_samples(const mantis::QuerySet& kmers); void serialize(); @@ -275,7 +275,7 @@ void ColoredDbg::serialize() { } template -std::unordered_map +mantis::QueryResult ColoredDbg::find_samples(const mantis::QuerySet& kmers) { // Find a list of eq classes and the number of kmers that belong those eq // classes. @@ -287,7 +287,7 @@ ColoredDbg::find_samples(const mantis::QuerySet& kmers) { query_eqclass_map[eqclass] += 1; } - std::unordered_map sample_map; + mantis::QueryResult sample_map; for (auto it = query_eqclass_map.begin(); it != query_eqclass_map.end(); ++it) { auto eqclass_id = it->first; diff --git a/include/common_types.h b/include/common_types.h index 63b6040..bac7e41 100644 --- a/include/common_types.h +++ b/include/common_types.h @@ -4,6 +4,7 @@ #include #include #include +#include "tsl/hopscotch_map.h" namespace mantis { using KmerHash = uint64_t; @@ -16,7 +17,7 @@ namespace mantis { }; - using QueryResult = std::unordered_map; + using QueryResult = tsl::hopscotch_map; using QueryResults = std::vector; } diff --git a/src/validateMSF.cc b/src/validateMSF.cc index 330ad55..aea1739 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -23,7 +23,8 @@ struct QueryStats { uint64_t rootedNonZero{0}; uint64_t nextCacheUpdate{10000}; uint64_t globalQueryNum{0}; - std::unordered_map numOcc; + std::vector buffer; + //std::unordered_map numOcc; }; @@ -102,9 +103,10 @@ class MSFQuery { std::vector xorflips(numSamples, 0); uint64_t i{eqid}, from{0}, to{0}; uint64_t height{0}; - std::vector froms; - std::vector parents; - froms.reserve(12000); + auto& froms = queryStats.buffer; + froms.clear(); + //std::vector froms; + //froms.reserve(12000); //parents.reserve(12000); queryStats.totEqcls++; auto sstart = std::chrono::system_clock::now(); @@ -215,7 +217,7 @@ class MSFQuery { mantis::QueryResult findSamples(const mantis::QuerySet &kmers, CQF &dbg, MSFQuery &msfQuery, LRUCacheMap& lru_cache, - RankScores& rs, + RankScores* rs, QueryStats &queryStats) { std::unordered_map query_eqclass_map; for (auto k : kmers) { @@ -225,7 +227,7 @@ mantis::QueryResult findSamples(const mantis::QuerySet &kmers, query_eqclass_map[eqclass] += 1; } - std::unordered_map sample_map; + mantis::QueryResult sample_map; size_t numPerLevel = 10; for (auto it = query_eqclass_map.begin(); it != query_eqclass_map.end(); ++it) { auto eqclass_id = it->first - 1; @@ -236,7 +238,7 @@ mantis::QueryResult findSamples(const mantis::QuerySet &kmers, setbits = lru_cache.get(eqclass_id); queryStats.cacheCntr++; } else { - setbits = msfQuery.buildColor(eqclass_id, queryStats, &lru_cache, &rs, false); + setbits = msfQuery.buildColor(eqclass_id, queryStats, &lru_cache, rs, false); lru_cache.put(eqclass_id, setbits); queryStats.noCacheCntr++; } @@ -467,7 +469,7 @@ int main(int argc, char *argv[]) { for (auto &kmers : multi_kmers) { opfile << "seq" << queryStats.cnt++ << '\t' << kmers.size() << '\n'; mantis::QueryResult result = findSamples(kmers, dbg, msfQuery, cache_lru, - rs, queryStats); + nullptr, queryStats); for (auto it = result.begin(); it != result.end(); ++it) { opfile << it->first/*cdbg.get_sample(it->first)*/ << '\t' << it->second << '\n'; } diff --git a/src/validatemantis.cc b/src/validatemantis.cc index 6aa2ddd..9f09ca8 100644 --- a/src/validatemantis.cc +++ b/src/validatemantis.cc @@ -148,7 +148,7 @@ validate_main ( ValidateOpts& opt ) // Query kmers in each experiment CQF ignoring kmers below the cutoff. // Maintain the fraction of kmers present in each experiment CQF. std::vector> ground_truth; - std::vector> cdbg_output; + std::vector cdbg_output; bool fail{false}; for (auto kmers : multi_kmers) { std::unordered_map fraction_present; @@ -164,7 +164,7 @@ validate_main ( ValidateOpts& opt ) } } // Query kmers in the cdbg - std::unordered_map result = cdbg.find_samples(kmers); + auto result = cdbg.find_samples(kmers); // Validate the cdbg output for (uint64_t i = 0; i < nqf; i++) From ed942104d2d370d80f4938ecf9824e2a03c29301 Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 25 Jul 2018 21:38:00 -0400 Subject: [PATCH 048/122] the tsl hashtable yieleded different results ... bad --- include/common_types.h | 2 +- src/CMakeLists.txt | 4 ++-- src/validateMSF.cc | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/include/common_types.h b/include/common_types.h index bac7e41..9e2390b 100644 --- a/include/common_types.h +++ b/include/common_types.h @@ -17,7 +17,7 @@ namespace mantis { }; - using QueryResult = tsl::hopscotch_map; + using QueryResult = std::unordered_map;//tsl::hopscotch_map; using QueryResults = std::vector; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 36fd551..9e5c6af 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,8 +14,8 @@ add_library(mantis_core STATIC set(MANTIS_DEBUG_CFLAGS "${MANTIS_C_FLAGS};-g") set(MANTIS_DEBUG_CXXFLAGS "${MANTIS_CXX_FLAGS};-g") -set(MANTIS_RELEASE_CFLAGS "${MANTIS_C_FLAGS};-O3") -set(MANTIS_RELEASE_CXXFLAGS "${MANTIS_CXX_FLAGS};-O3") +set(MANTIS_RELEASE_CFLAGS "${MANTIS_C_FLAGS};-O3;-g") +set(MANTIS_RELEASE_CXXFLAGS "${MANTIS_CXX_FLAGS};-O3;-g") target_include_directories(mantis_core PUBLIC $) target_compile_options(mantis_core PUBLIC "$<$,$>:${MANTIS_DEBUG_CFLAGS}>") diff --git a/src/validateMSF.cc b/src/validateMSF.cc index aea1739..da4242e 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -161,20 +161,25 @@ class MSFQuery { bool found = false; uint64_t wrd{0}; //auto j = f; + uint64_t offset{0}; auto start = f; do { wrd = bbv.get_int(start, 64); + //while (wrd == 0) { offset+= 64; wrd = bbv.get_int(start+offset, 64); } + //offset += __builtin_clzll(wrd); //j = 0; for (uint64_t j = 0; j < 64; j++) { flips[deltabv[start + j]] ^= 0x01; //j++; + if ((wrd >> j) & 0x01) { found = true; break; } + } start += 64; - } while (!found/*bbv[j - 1] != 1*/); + } while (!found/*bbv[j - 1] != 1*/); } queryStats.flipTime += std::chrono::system_clock::now() - fstart; /*while (parentbv[i] != i) { From 4cf6d813d0e252341a0906c8698a0cd1f170d172 Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 25 Jul 2018 21:43:03 -0400 Subject: [PATCH 049/122] the tsl hashtable yieleded different results ... bad --- src/validateMSF.cc | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index da4242e..bd4bb52 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -163,23 +163,24 @@ class MSFQuery { //auto j = f; uint64_t offset{0}; auto start = f; - do { + //do { wrd = bbv.get_int(start, 64); - //while (wrd == 0) { offset+= 64; wrd = bbv.get_int(start+offset, 64); } - //offset += __builtin_clzll(wrd); + while (wrd == 0) { offset+= 64; wrd = bbv.get_int(start+offset, 64); } + offset += __builtin_clzll(wrd); //j = 0; - for (uint64_t j = 0; j < 64; j++) { + //for (uint64_t j = 0; j < 64; j++) { + for (uint64_t j = 0; j <= offset; j++) { flips[deltabv[start + j]] ^= 0x01; //j++; - + /* if ((wrd >> j) & 0x01) { found = true; break; } - + */ } - start += 64; - } while (!found/*bbv[j - 1] != 1*/); + //start += 64; + //} while (!found/*bbv[j - 1] != 1*/); } queryStats.flipTime += std::chrono::system_clock::now() - fstart; /*while (parentbv[i] != i) { From b54965fd3d7ad4123f626533291ff37554f73117 Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 25 Jul 2018 22:10:48 -0400 Subject: [PATCH 050/122] replace output hash with vector, order is different, but contents are the same --- include/coloreddbg.h | 2 +- include/common_types.h | 2 +- src/CMakeLists.txt | 4 ++-- src/query.cc | 19 +++++++++++++++---- src/validateMSF.cc | 42 +++++++++++++++++++++++------------------- 5 files changed, 42 insertions(+), 27 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 6064e5a..199cace 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -287,7 +287,7 @@ ColoredDbg::find_samples(const mantis::QuerySet& kmers) { query_eqclass_map[eqclass] += 1; } - mantis::QueryResult sample_map; + mantis::QueryResult sample_map(num_samples, 0); for (auto it = query_eqclass_map.begin(); it != query_eqclass_map.end(); ++it) { auto eqclass_id = it->first; diff --git a/include/common_types.h b/include/common_types.h index 9e2390b..b1c622c 100644 --- a/include/common_types.h +++ b/include/common_types.h @@ -17,7 +17,7 @@ namespace mantis { }; - using QueryResult = std::unordered_map;//tsl::hopscotch_map; + using QueryResult = std::vector;//std::unordered_map;//tsl::hopscotch_map; using QueryResults = std::vector; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9e5c6af..5857d0c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,8 +14,8 @@ add_library(mantis_core STATIC set(MANTIS_DEBUG_CFLAGS "${MANTIS_C_FLAGS};-g") set(MANTIS_DEBUG_CXXFLAGS "${MANTIS_CXX_FLAGS};-g") -set(MANTIS_RELEASE_CFLAGS "${MANTIS_C_FLAGS};-O3;-g") -set(MANTIS_RELEASE_CXXFLAGS "${MANTIS_CXX_FLAGS};-O3;-g") +set(MANTIS_RELEASE_CFLAGS "${MANTIS_C_FLAGS};-O3;-march=native") +set(MANTIS_RELEASE_CXXFLAGS "${MANTIS_CXX_FLAGS};-O3;-march=native") target_include_directories(mantis_core PUBLIC $) target_compile_options(mantis_core PUBLIC "$<$,$>:${MANTIS_DEBUG_CFLAGS}>") diff --git a/src/query.cc b/src/query.cc index f90ff5b..756fa01 100644 --- a/src/query.cc +++ b/src/query.cc @@ -68,7 +68,11 @@ void output_results(mantis::QuerySets& multi_kmers, opfile << cnt++ << '\t' << kmers.size() << '\n'; mantis::QueryResult result = cdbg.find_samples(kmers); for (auto it = result.begin(); it != result.end(); ++it) { - opfile << cdbg.get_sample(it->first) << '\t' << it->second << '\n'; + if (*it > 0) { + auto i = std::distance(result.begin(), it); + opfile << i << '\t' << *it << '\n'; + } + //opfile << cdbg.get_sample(it->first) << '\t' << it->second << '\n'; } //++qctr; } @@ -90,10 +94,17 @@ void output_results_json(mantis::QuerySets& multi_kmers, //std::sort(kmers.begin(), kmers.end()); opfile << "{ \"qnum\": " << cnt++ << ", \"num_kmers\": " << kmers.size() << ", \"res\": {\n"; mantis::QueryResult result = cdbg.find_samples(kmers); + bool first{true}; for (auto it = result.begin(); it != result.end(); ++it) { - opfile << " \"" <first) << "\": " << it->second ; - if (std::next(it) != result.end()) { - opfile << ",\n"; + if (*it > 0) { + if (!first) {opfile << ",\n"; first=false;} + auto i = std::distance(result.begin(), it); + opfile << " \"" < buffer; + uint64_t numSamples{0}; //std::unordered_map numOcc; }; @@ -163,24 +164,22 @@ class MSFQuery { //auto j = f; uint64_t offset{0}; auto start = f; - //do { - wrd = bbv.get_int(start, 64); - while (wrd == 0) { offset+= 64; wrd = bbv.get_int(start+offset, 64); } - offset += __builtin_clzll(wrd); - //j = 0; - //for (uint64_t j = 0; j < 64; j++) { - for (uint64_t j = 0; j <= offset; j++) { - flips[deltabv[start + j]] ^= 0x01; - //j++; - /* - if ((wrd >> j) & 0x01) { - found = true; - break; - } - */ + do { + wrd = bbv.get_int(start, 64); + //while (wrd == 0) { offset+= 64; wrd = bbv.get_int(start+offset, 64); } + //offset += __builtin_ctzll(wrd); + //j = 0; + for (uint64_t j = 0; j < 64; j++) { + //for (uint64_t j = 0; j <= offset; j++) { + flips[deltabv[start + j]] ^= 0x01; + //j++; + if ((wrd >> j) & 0x01) { + found = true; + break; } - //start += 64; - //} while (!found/*bbv[j - 1] != 1*/); + } + start += 64; + } while (!found/*bbv[j - 1] != 1*/); } queryStats.flipTime += std::chrono::system_clock::now() - fstart; /*while (parentbv[i] != i) { @@ -233,7 +232,7 @@ mantis::QueryResult findSamples(const mantis::QuerySet &kmers, query_eqclass_map[eqclass] += 1; } - mantis::QueryResult sample_map; + mantis::QueryResult sample_map(queryStats.numSamples,0); size_t numPerLevel = 10; for (auto it = query_eqclass_map.begin(); it != query_eqclass_map.end(); ++it) { auto eqclass_id = it->first - 1; @@ -399,6 +398,7 @@ int main(int argc, char *argv[]) { LRUCacheMap cache_lru(100000); QueryStats queryStats; + queryStats.numSamples = opt.numSamples; if (selected == mode::validate) { eqvec bvs; @@ -477,7 +477,11 @@ int main(int argc, char *argv[]) { mantis::QueryResult result = findSamples(kmers, dbg, msfQuery, cache_lru, nullptr, queryStats); for (auto it = result.begin(); it != result.end(); ++it) { - opfile << it->first/*cdbg.get_sample(it->first)*/ << '\t' << it->second << '\n'; + if (*it > 0) { + auto i = std::distance(result.begin(), it); + opfile << i << '\t' << *it << '\n'; + } + //opfile << it->first/*cdbg.get_sample(it->first)*/ << '\t' << it->second << '\n'; } } opfile.close(); From 51d990e5732a1dcb4af34845f423a9a9bfcf6d4c Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 25 Jul 2018 23:06:42 -0400 Subject: [PATCH 051/122] more advanced lru cache --- CMakeLists.txt | 2 +- include/lru/cache-tags.hpp | 40 + include/lru/cache.hpp | 207 +++ include/lru/entry.hpp | 134 ++ include/lru/error.hpp | 107 ++ include/lru/insertion-result.hpp | 76 + include/lru/internal/base-cache.hpp | 1588 +++++++++++++++++ include/lru/internal/base-iterator.hpp | 216 +++ .../lru/internal/base-ordered-iterator.hpp | 338 ++++ .../lru/internal/base-unordered-iterator.hpp | 216 +++ include/lru/internal/callback-manager.hpp | 159 ++ include/lru/internal/definitions.hpp | 88 + include/lru/internal/hash.hpp | 62 + include/lru/internal/information.hpp | 145 ++ include/lru/internal/last-accessed.hpp | 254 +++ include/lru/internal/optional.hpp | 207 +++ include/lru/internal/statistics-mutator.hpp | 150 ++ include/lru/internal/timed-information.hpp | 116 ++ include/lru/internal/utility.hpp | 178 ++ include/lru/iterator-tags.hpp | 40 + include/lru/key-statistics.hpp | 67 + include/lru/lowercase.hpp | 34 + include/lru/lru.hpp | 33 + include/lru/statistics.hpp | 256 +++ include/lru/timed-cache.hpp | 391 ++++ include/lru/wrap.hpp | 99 + src/validateMSF.cc | 13 +- 27 files changed, 5209 insertions(+), 7 deletions(-) create mode 100644 include/lru/cache-tags.hpp create mode 100644 include/lru/cache.hpp create mode 100644 include/lru/entry.hpp create mode 100644 include/lru/error.hpp create mode 100644 include/lru/insertion-result.hpp create mode 100644 include/lru/internal/base-cache.hpp create mode 100644 include/lru/internal/base-iterator.hpp create mode 100644 include/lru/internal/base-ordered-iterator.hpp create mode 100644 include/lru/internal/base-unordered-iterator.hpp create mode 100644 include/lru/internal/callback-manager.hpp create mode 100644 include/lru/internal/definitions.hpp create mode 100644 include/lru/internal/hash.hpp create mode 100644 include/lru/internal/information.hpp create mode 100644 include/lru/internal/last-accessed.hpp create mode 100644 include/lru/internal/optional.hpp create mode 100644 include/lru/internal/statistics-mutator.hpp create mode 100644 include/lru/internal/timed-information.hpp create mode 100644 include/lru/internal/utility.hpp create mode 100644 include/lru/iterator-tags.hpp create mode 100644 include/lru/key-statistics.hpp create mode 100644 include/lru/lowercase.hpp create mode 100644 include/lru/lru.hpp create mode 100644 include/lru/statistics.hpp create mode 100644 include/lru/timed-cache.hpp create mode 100644 include/lru/wrap.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e175847..4c4a7c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ if (NOT CMAKE_BUILD_TYPE) endif() # We require C++11 -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/include/lru/cache-tags.hpp b/include/lru/cache-tags.hpp new file mode 100644 index 0000000..924a669 --- /dev/null +++ b/include/lru/cache-tags.hpp @@ -0,0 +1,40 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_CACHE_TAGS_HPP +#define LRU_CACHE_TAGS_HPP + +namespace LRU { +namespace Tag { +struct BasicCache {}; +struct TimedCache {}; +} // namespace Tag + +namespace Lowercase { +namespace tag { +using basic_cache = ::LRU::Tag::BasicCache; +using timed_cache = ::LRU::Tag::TimedCache; +} // namespace tag +} // namespace Lowercase + +} // namespace LRU + +#endif // LRU_CACHE_TAGS_HPP diff --git a/include/lru/cache.hpp b/include/lru/cache.hpp new file mode 100644 index 0000000..7150bfc --- /dev/null +++ b/include/lru/cache.hpp @@ -0,0 +1,207 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_CACHE_HPP +#define LRU_CACHE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace LRU { +namespace Internal { +template +using UntimedCacheBase = Internal::BaseCache; +} // namespace Internal + +/// A basic LRU cache implementation. +/// +/// An LRU cache is a fixed-size cache that remembers the order in which +/// elements were inserted into it. When the size of the cache exceeds its +/// capacity, the "least-recently-used" (LRU) element is erased. In our +/// implementation, usage is defined as insertion, but not lookup. That is, +/// looking up an element does not move it to the "front" of the cache (making +/// the operation faster). Only insertions (and erasures) can change the order +/// of elements. The capacity of the cache can be modified at any time. +/// +/// \see LRU::TimedCache +template , + typename KeyEqual = std::equal_to> +class Cache + : public Internal::UntimedCacheBase { + private: + using super = Internal::UntimedCacheBase; + using PRIVATE_BASE_CACHE_MEMBERS; + + public: + using PUBLIC_BASE_CACHE_MEMBERS; + using typename super::size_t; + + /// \copydoc BaseCache::BaseCache(size_t,const HashFunction&,const KeyEqual&) + /// \detailss The capacity defaults to an internal constant, currently 128. + explicit Cache(size_t capacity = Internal::DEFAULT_CAPACITY, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) + : super(capacity, hash, equal) { + } + + /// \copydoc BaseCache(size_t,Iterator,Iterator,const HashFunction&,const + /// KeyEqual&) + template + Cache(size_t capacity, + Iterator begin, + Iterator end, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) + : super(capacity, begin, end, hash, equal) { + } + + /// \copydoc BaseCache(Iterator,Iterator,const HashFunction&,const + /// KeyEqual&) + template + Cache(Iterator begin, + Iterator end, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) + : super(begin, end, hash, equal) { + } + + /// Constructor. + /// + /// \param capacity The capacity of the cache. + /// \param range A range to construct the cache with. + /// \param hash The hash function to use for the internal map. + /// \param key_equal The key equality function to use for the internal map. + template > + Cache(size_t capacity, + Range&& range, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) + : super(capacity, std::forward(range), hash, equal) { + } + + /// Constructor. + /// + /// \param range A range to construct the cache with. + /// \param hash The hash function to use for the internal map. + /// \param key_equal The key equality function to use for the internal map. + template > + explicit Cache(Range&& range, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) + : super(std::forward(range), hash, equal) { + } + + /// \copydoc BaseCache(InitializerList,const HashFunction&,const + /// KeyEqual&) + Cache(InitializerList list, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) // NOLINT(runtime/explicit) + : super(list, hash, equal) { + } + + /// \copydoc BaseCache(size_t,InitializerList,const HashFunction&,const + /// KeyEqual&) + Cache(size_t capacity, + InitializerList list, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) // NOLINT(runtime/explicit) + : super(capacity, list, hash, equal) { + } + + /// \copydoc BaseCache::find(const Key&) + UnorderedIterator find(const Key& key) override { + auto iterator = _map.find(key); + if (iterator != _map.end()) { + _register_hit(key, iterator->second.value); + _move_to_front(iterator->second.order); + _last_accessed = iterator; + } else { + _register_miss(key); + } + + return {*this, iterator}; + } + + /// \copydoc BaseCache::find(const Key&) const + UnorderedConstIterator find(const Key& key) const override { + auto iterator = _map.find(key); + if (iterator != _map.end()) { + _register_hit(key, iterator->second.value); + _move_to_front(iterator->second.order); + _last_accessed = iterator; + } else { + _register_miss(key); + } + + return {*this, iterator}; + } + + /// \returns The most-recently inserted element. + const Key& front() const noexcept { + if (is_empty()) { + throw LRU::Error::EmptyCache("front"); + } else { + // The queue is reversed for natural order of iteration. + return _order.back(); + } + } + + /// \returns The least-recently inserted element. + const Key& back() const noexcept { + if (is_empty()) { + throw LRU::Error::EmptyCache("back"); + } else { + // The queue is reversed for natural order of iteration. + return _order.front(); + } + } +}; + +namespace Lowercase { +template +using cache = Cache; +} // namespace Lowercase + +} // namespace LRU + +#endif // LRU_CACHE_HPP diff --git a/include/lru/entry.hpp b/include/lru/entry.hpp new file mode 100644 index 0000000..425d7eb --- /dev/null +++ b/include/lru/entry.hpp @@ -0,0 +1,134 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_PAIR_HPP +#define LRU_PAIR_HPP + +#include +#include +#include + +namespace LRU { + +/// A entry of references to the key and value of an entry in a cache. +/// +/// Instances of this class are usually the result of dereferencing an iterator. +/// +/// \tparam Key The key type of the pair. +/// \tparam Value The value type of the pair. +template +struct Entry final { + using KeyType = Key; + using ValueType = Value; + using first_type = Key; + using second_type = Value; + + /// Constructor. + /// + /// \param key The key of the entry. + /// \param value The value of the entry. + Entry(const Key& key, Value& value) : first(key), second(value) { + } + + /// Generalized copy constructor. + /// + /// Mainly for conversion from non-const values to const values. + /// + /// \param other The entry to construct from. + template ::value && + std::is_convertible::value)>> + Entry(const Entry& other) + : first(other.first), second(other.second) { + } + + /// Compares two entrys for equality. + /// + /// \param first The first entry to compare. + /// \param second The second entry to compare. + /// \returns True if the firest entry equals the second, else false. + template + friend bool operator==(const Entry& first, const Pair& second) noexcept { + return first.first == second.first && first.second == second.second; + } + + /// Compares two entrys for equality. + /// + /// \param first The first entry to compare. + /// \param second The second entry to compare. + /// \returns True if the first entry equals the second, else false. + template + friend bool operator==(const Pair& first, const Entry& second) noexcept { + return second == first; + } + + /// Compares two entrys for inequality. + /// + /// \param first The first entry to compare. + /// \param second The second entry to compare. + /// \returns True if the first entry does not equal the second, else false. + template + friend bool operator!=(const Entry& first, const Pair& second) noexcept { + return !(first == second); + } + + /// Compares two entrys for inequality. + /// + /// \param first The first entry to compare. + /// \param second The second entry to compare.fdas + /// \returns True if the first entry does not equal the second, else false. + template + friend bool operator!=(const Pair& first, const Entry& second) noexcept { + return second != first; + } + + /// \returns A `std::pair` instance with the key and value of this entry. + operator std::pair() noexcept { + return {first, second}; + } + + /// \returns The key of the entry (`first`). + const Key& key() const noexcept { + return first; + } + + /// \returns The value of the entry (`second`). + Value& value() noexcept { + return second; + } + + /// \returns The value of the entry (`second`). + const Value& value() const noexcept { + return second; + } + + /// The key of the entry. + const Key& first; + + /// The value of the entry. + Value& second; +}; +} // namespace LRU + + +#endif // LRU_PAIR_HPP diff --git a/include/lru/error.hpp b/include/lru/error.hpp new file mode 100644 index 0000000..9d1c865 --- /dev/null +++ b/include/lru/error.hpp @@ -0,0 +1,107 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_INTERNAL_ERRORS_HPP +#define LRU_INTERNAL_ERRORS_HPP + +#include +#include + +namespace LRU { +namespace Error { + +/// Exception thrown when the value of an invalid key was requested. +struct KeyNotFound : public std::runtime_error { + using super = std::runtime_error; + + KeyNotFound() : super("Failed to find key") { + } + + explicit KeyNotFound(const std::string& key) + : super("Failed to find key: " + key) { + } +}; + +/// Exception thrown when the value of an expired key was requested. +struct KeyExpired : public std::runtime_error { + using super = std::runtime_error; + + explicit KeyExpired(const std::string& key) + : super("Key found, but expired: " + key) { + } + + KeyExpired() : super("Key found, but expired") { + } +}; + +/// Exception thrown when requesting the front or end key of an empty cache. +struct EmptyCache : public std::runtime_error { + using super = std::runtime_error; + explicit EmptyCache(const std::string& what_was_expected) + : super("Requested " + what_was_expected + " of empty cache") { + } +}; + +/// Exception thrown when attempting to convert an invalid unordered iterator to +/// an ordered iterator. +struct InvalidIteratorConversion : public std::runtime_error { + using super = std::runtime_error; + InvalidIteratorConversion() + : super("Cannot convert past-the-end unordered to ordered iterator") { + } +}; + +/// Exception thrown when attempting to erase the past-the-end iterator. +struct InvalidIterator : public std::runtime_error { + using super = std::runtime_error; + InvalidIterator() : super("Past-the-end iterator is invalid here") { + } +}; + +/// Exception thrown when requesting statistics about an unmonitored key. +struct UnmonitoredKey : public std::runtime_error { + using super = std::runtime_error; + UnmonitoredKey() : super("Requested statistics for unmonitored key") { + } +}; + +/// Exception thrown when requesting the statistics object of a cache when none +/// was registered. +struct NotMonitoring : public std::runtime_error { + using super = std::runtime_error; + NotMonitoring() : super("Statistics monitoring not enabled for this cache") { + } +}; + +namespace Lowercase { +using key_not_found = KeyNotFound; +using key_expired = KeyExpired; +using empty_cache = EmptyCache; +using invalid_iterator_conversion = InvalidIteratorConversion; +using invalid_iterator = InvalidIterator; +using unmonitored_key = UnmonitoredKey; +using not_monitoring = NotMonitoring; +} // namespace Lowercase + +} // namespace Error +} // namespace LRU + +#endif // LRU_INTERNAL_ERRORS_HPP diff --git a/include/lru/insertion-result.hpp b/include/lru/insertion-result.hpp new file mode 100644 index 0000000..846bc08 --- /dev/null +++ b/include/lru/insertion-result.hpp @@ -0,0 +1,76 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_INSERTION_RESULT_HPP +#define LRU_INSERTION_RESULT_HPP + +#include +#include +#include + +namespace LRU { + +/// The result of an insertion into a cache. +/// +/// This is a semantically nicer alternative to a generic `std::pair`, as is +/// returned by `std::unordered_map` or so. It still has the same static +/// interface as the `std::pair` (with `first` and `second` members), but adds +/// nicer `was_inserted()` and `iterator()` accessors. +/// +/// \tparam Iterator The class of the iterator contained in the result. +template +struct InsertionResult final { + using IteratorType = Iterator; + + /// Constructor. + /// + /// \param result Whether the result was successful. + /// \param iterator The iterator pointing to the inserted or updated key. + InsertionResult(bool result, Iterator iterator) + : first(result), second(iterator) { + } + + /// \returns True if the key was newly inserted, false if it was only updated. + bool was_inserted() const noexcept { + return first; + } + + /// \returns The iterator pointing to the inserted or updated key. + Iterator iterator() const noexcept { + return second; + } + + /// \copydoc was_inserted + explicit operator bool() const noexcept { + return was_inserted(); + } + + /// Whether the result was successful. + bool first; + + /// The iterator pointing to the inserted or updated key. + Iterator second; +}; + +} // namespace LRU + + +#endif // LRU_INSERTION_RESULT_HPP diff --git a/include/lru/internal/base-cache.hpp b/include/lru/internal/base-cache.hpp new file mode 100644 index 0000000..fe49a72 --- /dev/null +++ b/include/lru/internal/base-cache.hpp @@ -0,0 +1,1588 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_INTERNAL_BASE_CACHE_HPP +#define LRU_INTERNAL_BASE_CACHE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace LRU { +namespace Internal { + +// Macros are bad, but also more readable sometimes: +// Without this macro, it becomes a pain to have a `using` directive for every +// new member we add to the `BaseCache` and rename or remove every such +// directive when we make a change to the `BaseCache`. +// With this macro, you can simply do: +// using super = BaseCache; +// using BASE_CACHE_MEMBERS; +#define PUBLIC_BASE_CACHE_MEMBERS \ + super::is_full; \ + using super::is_empty; \ + using super::clear; \ + using super::end; \ + using super::cend; \ + using super::operator=; \ + using typename super::Information; \ + using typename super::UnorderedIterator; \ + using typename super::UnorderedConstIterator; \ + using typename super::OrderedIterator; \ + using typename super::OrderedConstIterator; \ + using typename super::InitializerList; + +#define PRIVATE_BASE_CACHE_MEMBERS \ + super::_map; \ + using typename super::Map; \ + using typename super::MapIterator; \ + using typename super::MapConstIterator; \ + using typename super::Queue; \ + using typename super::QueueIterator; \ + using super::_order; \ + using super::_last_accessed; \ + using super::_capacity; \ + using super::_erase; \ + using super::_erase_lru; \ + using super::_move_to_front; \ + using super::_value_from_result; \ + using super::_last_accessed_is_ok; \ + using super::_register_miss; \ + using super::_register_hit; + +/// The base class for the LRU::Cache and LRU::TimedCache. +/// +/// This base class (base as opposed to abstract, because it is not intended to +/// be used polymorphically) provides the great bulk of the implementation of +/// both the LRU::Cache and the timed version. For example, it builds the +/// `contains()`, `lookup()` and `operator[]()` functions on top of the pure +/// virtual `find()` methods, making the final implementation of the LRU::Cache +/// much less strenuous. +/// +/// This class also defines all concrete iterator classes and provides the main +/// iterator interface of all caches via ordered and unordered iterators and +/// appropriate `begin()`, `end()` and similar methods. +/// +/// Lastly, the `BaseCache` provides a statistics interface to register and +/// access shared or owned statistics. +/// +/// \tparam Key The key type of the cache. +/// \tparam Value The value type of the cache. +/// \tparam InformationType The internal information class to be used. +/// \tparam HashFunction The hash function type for the internal map. +/// \tparam KeyEqual The type of the key equality function for the internal map. +/// \tparam TagType The cache tag type of the concrete derived class. +template class InformationType, + typename HashFunction, + typename KeyEqual, + typename TagType> +class BaseCache { + protected: + using Information = InformationType; + using Queue = Internal::Queue; + using QueueIterator = typename Queue::const_iterator; + + using Map = Internal::Map; + using MapIterator = typename Map::iterator; + using MapConstIterator = typename Map::const_iterator; + + using CallbackManagerType = CallbackManager; + using HitCallback = typename CallbackManagerType::HitCallback; + using MissCallback = typename CallbackManagerType::MissCallback; + using AccessCallback = typename CallbackManagerType::AccessCallback; + using HitCallbackContainer = + typename CallbackManagerType::HitCallbackContainer; + using MissCallbackContainer = + typename CallbackManagerType::MissCallbackContainer; + using AccessCallbackContainer = + typename CallbackManagerType::AccessCallbackContainer; + + public: + using Tag = TagType; + using InitializerList = std::initializer_list>; + using StatisticsPointer = std::shared_ptr>; + using size_t = std::size_t; + + static constexpr Tag tag() noexcept { + return {}; + } + + ///////////////////////////////////////////////////////////////////////////// + // ITERATORS CLASSES + ///////////////////////////////////////////////////////////////////////////// + + /// A non-const unordered iterator. + /// + /// Unordered iterators provide faster lookup than ordered iterators because + /// they have direct access to the underlying map. Also, they can convert to + /// ordered iterators cheaply. + struct UnorderedIterator + : public BaseUnorderedIterator { + using super = BaseUnorderedIterator; + friend BaseCache; + + /// Default constructor. + UnorderedIterator() = default; + + /// Constructs a new UnorderedIterator from an unordered base iterator. + /// + /// \param iterator The iterator to initialize this one from. + UnorderedIterator(BaseUnorderedIterator + iterator) // NOLINT(runtime/explicit) + : super(std::move(iterator)) { + // Note that this only works because these derived iterator + // classes dont' have any members of their own. + // It is necessary because the increment operators return base iterators. + } + + /// Constructs a new UnorderedIterator. + /// + /// \param cache The cache this iterator references. + /// \param iterator The underlying map iterator. + UnorderedIterator(BaseCache& cache, + MapIterator iterator) // NOLINT(runtime/explicit) + : super(cache, iterator) { + } + }; + + /// A const unordered iterator. + /// + /// Unordered iterators provide faster lookup than ordered iterators because + /// they have direct access to the underlying map. Also, they can convert to + /// ordered iterators cheaply. + struct UnorderedConstIterator + : public BaseUnorderedIterator { + using super = BaseUnorderedIterator; + friend BaseCache; + + /// Default constructor. + UnorderedConstIterator() = default; + + /// Constructs a new UnorderedConstIterator from any unordered base + /// iterator. + /// + /// \param iterator The iterator to initialize this one from. + template + UnorderedConstIterator( + BaseUnorderedIterator iterator) + : super(std::move(iterator)) { + // Note that this only works because these derived iterator + // classes dont' have any members of their own. + } + + /// Constructs a new UnorderedConstIterator from a non-const iterator. + /// + /// \param iterator The non-const iterator to initialize this one from. + UnorderedConstIterator( + UnorderedIterator iterator) // NOLINT(runtime/explicit) + : super(std::move(iterator)) { + } + + /// Constructs a new UnorderedConstIterator. + /// + /// \param cache The cache this iterator references. + /// \param iterator The underlying map iterator. + UnorderedConstIterator( + const BaseCache& cache, + MapConstIterator iterator) // NOLINT(runtime/explicit) + : super(cache, iterator) { + } + }; + + /// An ordered iterator. + /// + /// Ordered iterators have a performance disadvantage compared to unordered + /// iterators the first time they are dereferenced. However, they may be + /// constructed or assigned from unordered iterators (of compatible + /// qualifiers). + struct OrderedIterator + : public BaseOrderedIterator { + using super = BaseOrderedIterator; + using UnderlyingIterator = typename super::UnderlyingIterator; + friend BaseCache; + + /// Default constructor. + OrderedIterator() = default; + + /// Constructs an ordered iterator from an unordered iterator. + /// + /// \param unordered_iterator The unordered iterator to construct from. + explicit OrderedIterator(UnorderedIterator unordered_iterator) + : super(std::move(unordered_iterator)) { + } + + /// Constructs a new OrderedIterator from an unordered base iterator. + /// + /// \param iterator The iterator to initialize this one from. + OrderedIterator(BaseOrderedIterator + iterator) // NOLINT(runtime/explicit) + : super(std::move(iterator)) { + // Note that this only works because these derived iterator + // classes dont' have any members of their own. + // It is necessary because the increment operators return base iterators. + } + + /// Constructs a new ordered iterator. + /// + /// \param cache The cache this iterator references. + /// \param iterator The underlying iterator. + OrderedIterator(BaseCache& cache, UnderlyingIterator iterator) + : super(cache, iterator) { + } + }; + + /// A const ordered iterator. + /// + /// Ordered iterators have a performance disadvantage compared to unordered + /// iterators the first time they are dereferenced. However, they may be + /// constructed or assigned from unordered iterators (of compatible + /// qualifiers). + struct OrderedConstIterator + : public BaseOrderedIterator { + using super = BaseOrderedIterator; + using UnderlyingIterator = typename super::UnderlyingIterator; + + friend BaseCache; + + /// Default constructor. + OrderedConstIterator() = default; + + /// Constructs a new OrderedConstIterator from a compatible ordered + /// iterator. + /// + /// \param iterator The iterator to initialize this one from. + template + OrderedConstIterator(BaseOrderedIterator + iterator) // NOLINT(runtime/explicit) + : super(iterator) { + // Note that this only works because these derived iterator + // classes dont' have any members of their own. + } + + /// Constructs a new const ordered iterator from a non-const one. + /// + /// \param iterator The non-const ordered iterator to construct from. + OrderedConstIterator(OrderedIterator iterator) // NOLINT(runtime/explicit) + : super(std::move(iterator)) { + } + + /// Constructs a new const ordered iterator from an unordered iterator. + /// + /// \param unordered_iterator The unordered iterator to construct from. + explicit OrderedConstIterator(UnorderedIterator unordered_iterator) + : super(std::move(unordered_iterator)) { + } + + /// Constructs a new const ordered iterator from a const unordered iterator. + /// + /// \param unordered_iterator The unordered iterator to construct from. + explicit OrderedConstIterator( + UnorderedConstIterator unordered_iterator) // NOLINT(runtime/explicit) + : super(std::move(unordered_iterator)) { + } + + /// Constructs a new const ordered iterator. + /// + /// \param cache The cache this iterator references. + /// \param iterator The underlying iterator. + OrderedConstIterator(const BaseCache& cache, UnderlyingIterator iterator) + : super(cache, iterator) { + } + }; + + using InsertionResultType = InsertionResult; + + // Can't put these in LRU::Lowercase because they are nested, unfortunately + using ordered_iterator = OrderedIterator; + using ordered_const_iterator = OrderedConstIterator; + using unordered_iterator = UnorderedIterator; + using unordered_const_iterator = UnorderedConstIterator; + + ///////////////////////////////////////////////////////////////////////////// + // SPECIAL MEMBER FUNCTIONS + ///////////////////////////////////////////////////////////////////////////// + + /// Constructor. + /// + /// \param capacity The capacity of the cache. + /// \param hash The hash function to use for the internal map. + /// \param key_equal The key equality function to use for the internal map. + BaseCache(size_t capacity, + const HashFunction& hash, + const KeyEqual& key_equal) + : _map(0, hash, key_equal), _capacity(capacity), _last_accessed(key_equal) { + } + + /// Constructor. + /// + /// \param capacity The capacity of the cache. + /// \param begin The start of a range to construct the cache with. + /// \param end The end of a range to construct the cache with. + /// \param hash The hash function to use for the internal map. + /// \param key_equal The key equality function to use for the internal map. + template + BaseCache(size_t capacity, + Iterator begin, + Iterator end, + const HashFunction& hash, + const KeyEqual& key_equal) + : BaseCache(capacity, hash, key_equal) { + insert(begin, end); + } + + /// Constructor. + /// + /// The capacity is inferred from the distance between the two iterators and + /// lower-bounded by an internal constant $c_0$, usually 128 (i.e. the actual + /// capacity will be $\max(\text{distance}, c_0)$). + /// This may be expensive for iterators that are not random-access. + /// + /// \param begin The start of a range to construct the cache with. + /// \param end The end of a range to construct the cache with. + /// \param hash The hash function to use for the internal map. + /// \param key_equal The key equality function to use for the internal map. + template + BaseCache(Iterator begin, + Iterator end, + const HashFunction& hash, + const KeyEqual& key_equal) + // This may be expensive + : BaseCache(std::max(std::distance(begin, end), + Internal::DEFAULT_CAPACITY), + begin, + end, + hash, + key_equal) { + } + + /// Constructor. + /// + /// \param capacity The capacity of the cache. + /// \param range A range to construct the cache with. + /// \param hash The hash function to use for the internal map. + /// \param key_equal The key equality function to use for the internal map. + template > + BaseCache(size_t capacity, + Range& range, + const HashFunction& hash, + const KeyEqual& key_equal) + : BaseCache(capacity, hash, key_equal) { + insert(range); + } + + /// Constructor. + /// + /// The capacity is inferred from the distance between the beginning and end + /// of the range. This may be expensive for iterators that are not + /// random-access. + /// + /// \param range A range to construct the cache with. + /// \param hash The hash function to use for the internal map. + /// \param key_equal The key equality function to use for the internal map. + template > + explicit BaseCache(Range& range, + const HashFunction& hash, + const KeyEqual& key_equal) + : BaseCache(std::begin(range), std::end(range), hash, key_equal) { + } + + /// Constructor. + /// + /// Elements of the range will be moved into the cache. + /// + /// \param capacity The capacity of the cache. + /// \param range A range to construct the cache with. + /// \param hash The hash function to use for the internal map. + /// \param key_equal The key equality function to use for the internal map. + template > + BaseCache(size_t capacity, + Range&& range, + const HashFunction& hash, + const KeyEqual& key_equal) + : BaseCache(capacity, hash, key_equal) { + insert(std::move(range)); + } + + /// Constructor. + /// + /// The capacity is inferred from the distance between the beginning and end + /// of the range. This may be expensive for iterators that are not + /// random-access. + /// + /// Elements of the range will be moved into the cache. + /// + /// \param range A range to construct the cache with. + /// \param hash The hash function to use for the internal map. + /// \param key_equal The key equality function to use for the internal map. + template > + explicit BaseCache(Range&& range, + const HashFunction& hash, + const KeyEqual& key_equal) + : BaseCache(std::distance(std::begin(range), std::end(range)), + std::move(range), + hash, + key_equal) { + } + + /// Constructor. + /// + /// \param capacity The capacity of the cache. + /// \param list The initializer list to construct the cache with. + /// \param hash The hash function to use for the internal map. + /// \param key_equal The key equality function to use for the internal map. + BaseCache(size_t capacity, + InitializerList list, + const HashFunction& hash, + const KeyEqual& key_equal) + : BaseCache(capacity, list.begin(), list.end(), hash, key_equal) { + } + + /// Constructor. + /// + /// \param list The initializer list to construct the cache with. + /// \param hash The hash function to use for the internal map. + /// \param key_equal The key equality function to use for the internal map. + BaseCache(InitializerList list, + const HashFunction& hash, + const KeyEqual& key_equal) // NOLINT(runtime/explicit) + : BaseCache(list.size(), list.begin(), list.end(), hash, key_equal) { + } + + /// Copy constructor. + BaseCache(const BaseCache& other) + : _map(other._map) + , _order(other._order) + , _stats(other._stats) + , _last_accessed(other._last_accessed) + , _callback_manager(other._callback_manager) + , _capacity(other._capacity) { + _reassign_references(); + } + + /// Move constructor. + BaseCache(BaseCache&& other) { + // Following the copy-swap idiom. + swap(other); + } + + /// Copy assignment operator. + BaseCache& operator=(const BaseCache& other) noexcept { + if (this != &other) { + _map = other._map; + _order = other._order; + _stats = other._stats; + _last_accessed = other._last_accessed; + _callback_manager = other._callback_manager; + _capacity = other._capacity; + _reassign_references(); + } + + return *this; + } + + /// Move assignment operator. + BaseCache& operator=(BaseCache&& other) noexcept { + // Following the copy-swap idiom. + swap(other); + return *this; + } + + /// Destructor. + virtual ~BaseCache() = default; + + /// Sets the contents of the cache to a range. + /// + /// If the size of the range is greater than the current capacity, + /// the capacity is increased to match the range's size. If the size of + /// the range is less than the current capacity, the cache's capacity is *not* + /// changed. + /// + /// \param range A range of pairs to assign to the cache. + /// \returns The cache instance. + template > + BaseCache& operator=(const Range& range) { + _clear_and_increase_capacity(range); + insert(range); + return *this; + } + + /// Sets the contents of the cache to an rvalue range. + /// + /// Pairs of the range are moved into the cache. + /// + /// \param range A range of pairs to assign to the cache. + /// \returns The cache instance. + template > + BaseCache& operator=(Range&& range) { + _clear_and_increase_capacity(range); + insert(std::move(range)); + return *this; + } + + /// Sets the contents of the cache to pairs from a list. + /// + /// \param list The list to assign to the cache. + /// \returns The cache instance. + BaseCache& operator=(InitializerList list) { + return operator=(list); + } + + /// Swaps the contents of the cache with another cache. + /// + /// \param other The other cache to swap with. + virtual void swap(BaseCache& other) noexcept { + using std::swap; + + swap(_order, other._order); + swap(_map, other._map); + swap(_last_accessed, other._last_accessed); + swap(_capacity, other._capacity); + } + + /// Swaps the contents of one cache with another cache. + /// + /// \param first The first cache to swap. + /// \param second The second cache to swap. + friend void swap(BaseCache& first, BaseCache& second) noexcept { + first.swap(second); + } + + /// Compares the cache for equality with another cache. + /// + /// \complexity O(N) + /// \param other The other cache to compare with. + /// \returns True if the keys __and values__ of the cache are identical to the + /// other, else false. + bool operator==(const BaseCache& other) const noexcept { + if (this == &other) return true; + if (this->_map != other._map) return false; + // clang-format off + return std::equal( + this->_order.begin(), + this->_order.end(), + other._order.begin(), + other._order.end(), + [](const auto& first, const auto& second) { + return first.get() == second.get(); + }); + // clang-format on + } + + /// Compares the cache for inequality with another cache. + /// + /// \complexity O(N) + /// \param other The other cache to compare with. + /// \returns True if there is any mismatch in keys __or their values__ + /// betweent + /// the two caches, else false. + bool operator!=(const BaseCache& other) const noexcept { + return !(*this == other); + } + + ///////////////////////////////////////////////////////////////////////////// + // ITERATOR INTERFACE + ///////////////////////////////////////////////////////////////////////////// + + /// \returns An unordered iterator to the beginning of the cache (this need + /// not be the first key inserted). + UnorderedIterator unordered_begin() noexcept { + return {*this, _map.begin()}; + } + + /// \returns A const unordered iterator to the beginning of the cache (this + /// need not be the key least recently inserted). + UnorderedConstIterator unordered_begin() const noexcept { + return unordered_cbegin(); + } + + /// \returns A const unordered iterator to the beginning of the cache (this + /// need not be the key least recently inserted). + UnorderedConstIterator unordered_cbegin() const noexcept { + return {*this, _map.cbegin()}; + } + + /// \returns An unordered iterator to the end of the cache (this + /// need not be one past the key most recently inserted). + UnorderedIterator unordered_end() noexcept { + return {*this, _map.end()}; + } + + /// \returns A const unordered iterator to the end of the cache (this + /// need not be one past the key most recently inserted). + UnorderedConstIterator unordered_end() const noexcept { + return unordered_cend(); + } + + /// \returns A const unordered iterator to the end of the cache (this + /// need not be one past the key most recently inserted). + UnorderedConstIterator unordered_cend() const noexcept { + return {*this, _map.cend()}; + } + + /// \returns An ordered iterator to the beginning of the cache (the key least + /// recently inserted). + OrderedIterator ordered_begin() noexcept { + return {*this, _order.begin()}; + } + + /// \returns A const ordered iterator to the beginning of the cache (the key + /// least recently inserted). + OrderedConstIterator ordered_begin() const noexcept { + return ordered_cbegin(); + } + + /// \returns A const ordered iterator to the beginning of the cache (the key + /// least recently inserted). + OrderedConstIterator ordered_cbegin() const noexcept { + return {*this, _order.cbegin()}; + } + + /// \returns An ordered iterator to the end of the cache (one past the key + /// most recently inserted). + OrderedIterator ordered_end() noexcept { + return {*this, _order.end()}; + } + + /// \returns A const ordered iterator to the end of the cache (one past the + /// key least recently inserted). + OrderedConstIterator ordered_end() const noexcept { + return ordered_cend(); + } + + /// \returns A const ordered iterator to the end of the cache (one past the + /// key least recently inserted). + OrderedConstIterator ordered_cend() const noexcept { + return {*this, _order.cend()}; + } + + /// \copydoc unordered_begin() + UnorderedIterator begin() noexcept { + return unordered_begin(); + } + + /// \copydoc unordered_cbegin() + UnorderedConstIterator begin() const noexcept { + return cbegin(); + } + + /// \copydoc unordered_cbegin() + UnorderedConstIterator cbegin() const noexcept { + return unordered_begin(); + } + + /// \copydoc unordered_end() const + UnorderedIterator end() noexcept { + return unordered_end(); + } + + /// \copydoc unordered_cend() const + UnorderedConstIterator end() const noexcept { + return cend(); + } + + /// \copydoc unordered_cend() const + UnorderedConstIterator cend() const noexcept { + return unordered_cend(); + } + + /// \returns True if the given iterator may be safely dereferenced, else + /// false. + /// \details Behavior is undefined if the iterator does not point into this + /// cache. + /// \param unordered_iterator The iterator to check. + virtual bool is_valid(UnorderedConstIterator unordered_iterator) const + noexcept { + return unordered_iterator != unordered_end(); + } + + /// \returns True if the given iterator may be safely dereferenced, else + /// false. + /// \details Behavior is undefined if the iterator does not point into this + /// cache. + /// \param ordered_iterator The iterator to check. + virtual bool is_valid(OrderedConstIterator ordered_iterator) const noexcept { + return ordered_iterator != ordered_end(); + } + + /// Checks if the given iterator may be dereferencend and throws an exception + /// if not. + /// + /// The exception thrown, if any, depends on the state of the iterator. + /// + /// \param unordered_iterator The iterator to check. + /// \throws LRU::Error::InvalidIterator if the iterator is the end iterator. + virtual void + throw_if_invalid(UnorderedConstIterator unordered_iterator) const { + if (unordered_iterator == unordered_end()) { + throw LRU::Error::InvalidIterator(); + } + } + + /// Checks if the given iterator may be dereferencend and throws an exception + /// if not. + /// + /// The exception thrown, if any, depends on the state of the iterator. + /// + /// \param ordered_iterator The iterator to check. + /// \throws LRU::Error::InvalidIterator if the iterator is the end iterator. + virtual void throw_if_invalid(OrderedConstIterator ordered_iterator) const { + if (ordered_iterator == ordered_end()) { + throw LRU::Error::InvalidIterator(); + } + } + + ///////////////////////////////////////////////////////////////////////////// + // CACHE INTERFACE + ///////////////////////////////////////////////////////////////////////////// + + /// Tests if the given key is contained in the cache. + /// + /// This function may return false even if the key is actually currently + /// stored in the cache, but the concrete cache class places some additional + /// constraint as to when a key may be accessed (such as a time limit). + /// + /// \complexity O(1) expected and amortized. + /// \param key The key to check for. + /// \returns True if the key's value may be accessed via `lookup()` without an + /// error, else false. + virtual bool contains(const Key& key) const { + if (key == _last_accessed) { + if (_last_accessed_is_ok(key)) { + _register_hit(key, _last_accessed.value()); + // If this is the last accessed key, it's at the front anyway + return true; + } else { + return false; + } + } + + return find(key) != end(); + } + + /// Looks up the value for the given key. + /// + /// If the key is found in the cache, it is moved to the front. Any iterators + /// pointing to that key are still valid, but the subsequent order of + /// iteration may be different from what it was before. + /// + /// \complexity O(1) expected and amortized. + /// \param key The key whose value to look for. + /// \throws LRU::Error::KeyNotFound if the key's value may not be accessed. + /// \returns The value stored in the cache for the given key. + /// \see contains() + virtual const Value& lookup(const Key& key) const { + if (key == _last_accessed) { + auto& value = _value_for_last_accessed(); + _register_hit(key, value); + // If this is the last accessed key, it's at the front anyway + return value; + } + + auto iterator = find(key); + if (iterator == end()) { + throw LRU::Error::KeyNotFound(); + } else { + return iterator.value(); + } + } + + /// Looks up the value for the given key. + /// + /// If the key is found in the cache, it is moved to the front. Any iterators + /// pointing to that key are still valid, but the subsequent order of + /// iteration may be different from what it was before. + /// + /// \complexity O(1) expected and amortized. + /// \param key The key whose value to look for. + /// \throws LRU::Error::KeyNotFound if the key's value may not be accessed. + /// \returns The value stored in the cache for the given key. + /// \see contains() + virtual Value& lookup(const Key& key) { + if (key == _last_accessed) { + auto& value = _value_for_last_accessed(); + _register_hit(key, value); + // If this is the last accessed key, it's at the front anyway + return value; + } + + auto iterator = find(key); + if (iterator == end()) { + throw LRU::Error::KeyNotFound(); + } else { + return iterator.value(); + } + } + + /// Attempts to return an iterator to the given key in the cache. + /// + /// If the key is found in the cache, it is moved to the front. Any iterators + /// pointing to that key are still valid, but the subsequent order of + /// iteration may be different from what it was before. + /// + /// \complexity O(1) expected and amortized. + /// \param key The key whose value to look for. + /// \returns An iterator pointing to the entry with the given key, if one + /// exists, else the end iterator. + virtual UnorderedIterator find(const Key& key) = 0; + + /// Attempts to return a const iterator to the given key in the cache. + /// + /// If the key is found in the cache, it is moved to the front. Any iterators + /// pointing to that key are still valid, but the subsequent order of + /// iteration may be different from what it was before. + /// + /// \complexity O(1) expected and amortized. + /// \param key The key whose value to look for. + /// \returns A const iterator pointing to the entry with the given key, if one + /// exists, else the end iterator. + virtual UnorderedConstIterator find(const Key& key) const = 0; + + /// \copydoc lookup(const Key&) + virtual Value& operator[](const Key& key) { + return lookup(key); + } + + /// \copydoc lookup(const Key&) const + virtual const Value& operator[](const Key& key) const { + return lookup(key); + } + + /// Inserts the given `(key, value)` pair into the cache. + /// + /// If the cache's capacity is reached, the most recently used element will be + /// evicted. Any iterators pointing to that element will be invalidated. + /// Iterators pointing to other elements are not affected. + /// + /// \complexity O(1) expected and amortized. + /// \param key The key to insert. + /// \param value The value to insert with the key. + /// \returns An `InsertionResult`, holding a boolean indicating whether the + /// key was newly inserted (true) or only updated (false) as well as an + /// iterator pointing to the entry for the key. + virtual InsertionResultType insert(const Key& key, const Value& value) { + if (_capacity == 0) return {false, end()}; + + auto iterator = _map.find(key); + + // To insert, we first check if the key is already present in the cache + // and if so, update its value and move its order iterator to the front + // of the queue. Else, we insert the key at the end of the queue and + // possibly pop the front if the cache has reached its capacity. + + if (iterator == _map.end()) { + auto result = _map.emplace(key, Information(value)); + assert(result.second); + auto order = _insert_new_key(result.first->first); + result.first->second.order = order; + + _last_accessed = result.first; + return {true, {*this, result.first}}; + } else { + _move_to_front(iterator, value); + _last_accessed = iterator; + return {false, {*this, iterator}}; + } + } + + /// Inserts a range of `(key, value)` pairs. + /// + /// If, at any point, the cache's capacity is reached, the most recently used + /// element will be evicted. Any iterators pointing to that element will + /// be invalidated. Iterators pointing to other elements are not affected. + /// + /// Note: This operation has no performance benefits over + /// element-wise insertion via `insert()`. + /// + /// \param begin An iterator for the start of the range to insert. + /// \param end An iterator for the end of the range to insert. + /// \returns The number of elements newly inserted (as opposed to only + /// updated). + template > + size_t insert(Iterator begin, Iterator end) { + size_t newly_inserted = 0; + for (; begin != end; ++begin) { + const auto result = insert(begin->first, begin->second); + newly_inserted += result.was_inserted(); + } + + return newly_inserted; + } + + /// Inserts a range of `(key, value)` pairs. + /// + /// If, at any point, the cache's capacity is reached, the most recently used + /// element will be evicted. Any iterators pointing to that element will + /// be invalidated. Iterators pointing to other elements are not affected. + /// + /// This operation has no performance benefits over + /// element-wise insertion via `insert()`. + /// + /// \param range The range of `(key, value)` pairs to insert. + /// \returns The number of elements newly inserted (as opposed to only + /// updated). + template > + size_t insert(Range& range) { + using std::begin; + using std::end; + + return insert(begin(range), end(range)); + } + + /// Moves the elements of the range into the cache. + /// + /// If, at any point, the cache's capacity is reached, the most recently used + /// element will be evicted. Any iterators pointing to that element will + /// be invalidated. Iterators pointing to other elements are not affected. + /// + /// \param range The range of `(key, value)` pairs to move into the cache. + /// \returns The number of elements newly inserted (as opposed to only + /// updated). + template > + size_t insert(Range&& range) { + size_t newly_inserted = 0; + for (auto& pair : range) { + const auto result = + emplace(std::move(pair.first), std::move(pair.second)); + newly_inserted += result.was_inserted(); + } + + return newly_inserted; + } + + /// Inserts a list `(key, value)` pairs. + /// + /// If the cache's capacity is reached, the most recently used element will be + /// evicted (one or more times). Any iterators pointing to that element will + /// be invalidated. Iterators pointing to other elements are not affected. + /// + /// This operation has no performance benefits over + /// element-wise insertion via `insert()`. + /// + /// \param list The list of `(key, value)` pairs to insert. + /// \returns The number of elements newly inserted (as opposed to only + /// updated). + virtual size_t insert(InitializerList list) { + return insert(list.begin(), list.end()); + } + + /// Emplaces a new `(key, value)` pair into the cache. + /// + /// This emplacement function allows perfectly forwarding an arbitrary number + /// of arguments to the constructor of both the key and value type, via + /// appropriate tuples. The intended usage is with `std::forward_as_tuple`, + /// for example: + /// \code{.cpp} + /// struct A { A(int, const std::string&) { } }; + /// struct B { B(double) {} }; + /// + /// LRU::Cache cache; + /// + /// cache.emplace( + /// std::piecewise_construct, + /// std::forward_as_tuple(1, "hello"), + /// std::forward_as_tuple(5.0), + /// ); + /// \endcode + /// + /// There is a convenience overload that requires much less overhead, if both + /// constructors expect only a single argument. + /// + /// If the cache's capacity is reached, the most recently used element will be + /// evicted. Any iterators pointing to that element will be invalidated. + /// Iterators pointing to other elements are not affected. + /// + /// \complexity O(1) expected and amortized. + /// \param _ A dummy parameter to work around overload resolution. + /// \param key_arguments A tuple of arguments to construct a key object with. + /// \param value_arguments A tuple of arguments to construct a value object + /// with. + /// \returns An `InsertionResult`, holding a boolean indicating whether the + /// key was newly inserted (true) or only updated (false) as well as an + /// iterator pointing to the entry for the key. + template + InsertionResultType emplace(std::piecewise_construct_t _, + const std::tuple& key_arguments, + const std::tuple& value_arguments) { + if (_capacity == 0) return {false, end()}; + + auto key = Internal::construct_from_tuple(key_arguments); + auto iterator = _map.find(key); + + if (iterator == _map.end()) { + auto result = _map.emplace(std::move(key), Information(value_arguments)); + auto order = _insert_new_key(result.first->first); + result.first->second.order = order; + assert(result.second); + + _last_accessed = result.first; + return {true, {*this, result.first}}; + } else { + auto value = Internal::construct_from_tuple(value_arguments); + _move_to_front(iterator, value); + _last_accessed = iterator; + return {false, {*this, iterator}}; + } + } + + /// Emplaces a `(key, value)` pair. + /// + /// This is a convenience overload removing the necessity for + /// `std::piecewise_construct` and `std::forward_as_tuple` that may be used in + /// the case that both the key and value have constructors expecting only a + /// single argument. + /// + /// If the cache's capacity is reached, the most recently used element will be + /// evicted. Any iterators pointing to that element will be invalidated. + /// Iterators pointing to other elements are not affected. + /// + /// \param key_argument The argument to construct a key object with. + /// \param value_argument The argument to construct a value object with. + /// \returns An `InsertionResult`, holding a boolean indicating whether the + /// key was newly inserted (true) or only updated (false) as well as an + /// iterator pointing to the entry for the key. + template + InsertionResultType emplace(K&& key_argument, V&& value_argument) { + auto key_tuple = std::forward_as_tuple(std::forward(key_argument)); + auto value_tuple = std::forward_as_tuple(std::forward(value_argument)); + return emplace(std::piecewise_construct, key_tuple, value_tuple); + } + + /// Erases the given key from the cache, if it is present. + /// + /// If the key is not present in the cache, this is a no-op. + /// All iterators pointing to the given key are invalidated. + /// Other iterators are not affected. + /// + /// \param key The key to erase. + /// \returns True if the key was erased, else false. + virtual bool erase(const Key& key) { + // No need to use _last_accessed_is_ok here, because even + // if it has expired, it's no problem to erase it anyway + if (_last_accessed == key) { + _erase(_last_accessed.key(), _last_accessed.information()); + return true; + } + + auto iterator = _map.find(key); + if (iterator != _map.end()) { + _erase(iterator); + return true; + } + + return false; + } + + /// Erases the key pointed to by the given iterator. + /// + /// \param iterator The iterator whose key to erase. + /// \throws LRU::Error::InvalidIterator if the iterator is the end iterator. + virtual void erase(UnorderedConstIterator iterator) { + /// We have this overload to avoid the extra conversion-construction from + /// unordered to ordered iterator (and renewed hash lookup) + if (iterator == unordered_cend()) { + throw LRU::Error::InvalidIterator(); + } else { + _erase(iterator._iterator); + } + } + + + /// Erases the key pointed to by the given iterator. + /// + /// \param iterator The iterator whose key to erase. + /// \throws LRU::Error::InvalidIterator if the iterator is the end iterator. + virtual void erase(OrderedConstIterator iterator) { + if (iterator == ordered_cend()) { + throw LRU::Error::InvalidIterator(); + } else { + _erase(_map.find(iterator.key())); + } + } + + /// Clears the cache entirely. + virtual void clear() { + _map.clear(); + _order.clear(); + _last_accessed.invalidate(); + } + + /// Requests shrinkage of the cache to the given size. + /// + /// If the passed size is 0, this operation is equivalent to `clear()`. If the + /// size is greater than the current size, it is a no-op. Otherwise, the size + /// of the cache is reduzed to the given size by repeatedly removing the least + /// recent element. + /// + /// \param new_size The size to (maybe) shrink to. + virtual void shrink(size_t new_size) { + if (new_size >= size()) return; + if (new_size == 0) { + clear(); + return; + } + + while (size() > new_size) { + _erase_lru(); + } + } + + ///////////////////////////////////////////////////////////////////////////// + // SIZE AND CAPACITY INTERFACE + ///////////////////////////////////////////////////////////////////////////// + + /// \returns The number of keys present in the cache. + virtual size_t size() const noexcept { + return _map.size(); + } + + /// Sets the capacity of the cache to the given value. + /// + /// If the given capacity is less than the current capacity of the cache, + /// the least-recently inserted element is removed repeatedly until the + /// capacity is equal to the given value. + /// + /// \param new_capacity The capacity to shrink or grow to. + virtual void capacity(size_t new_capacity) { + // Pop the front of the cache if we have to resize + while (size() > new_capacity) { + _erase_lru(); + } + _capacity = new_capacity; + } + + /// Returns the current capacity of the cache. + virtual size_t capacity() const noexcept { + return _capacity; + } + + /// \returns the number of slots left in the cache. + /// + /// \details After this number of elements have been inserted, the next one + /// insertion is preceded by an erasure of the least-recently inserted + /// element. + virtual size_t space_left() const noexcept { + return _capacity - size(); + } + + /// \returns True if the cache contains no elements, else false. + virtual bool is_empty() const noexcept { + return size() == 0; + } + + /// \returns True if the cache's size equals its capacity, else false. + /// + /// \details If `is_full()` returns `true`, the next insertion is preceded by + /// an erasure of the least-recently inserted element. + virtual bool is_full() const noexcept { + return size() == _capacity; + } + + /// \returns The function used to hash keys. + virtual HashFunction hash_function() const { + return _map.hash_function(); + } + + /// \returns The function used to compare keys. + virtual KeyEqual key_equal() const { + return _map.key_eq(); + } + + ///////////////////////////////////////////////////////////////////////////// + // STATISTICS INTERFACE + ///////////////////////////////////////////////////////////////////////////// + + /// Registers the given statistics object for monitoring. + /// + /// This method is useful if the statistics object is to + /// be shared between caches. + /// + /// Ownership of the statistics object remains with the user and __not__ with + /// the cache object. Also, behavior is undefined if the lifetime of the cache + /// exceeds that of the registered statistics object. + /// + /// \param statistics The statistics object to register. + virtual void monitor(const StatisticsPointer& statistics) { + _stats = statistics; + } + + /// Registers the given statistics object for monitoring. + /// + /// Ownership of the statistics object is transferred to the cache. + /// + /// \param statistics The statistics object to register. + virtual void monitor(StatisticsPointer&& statistics) { + _stats = std::move(statistics); + } + + /// Constructs a new statistics in-place in the cache. + /// + /// This method is useful if the cache is to have exclusive ownership of the + /// statistics and out-of-place construction and move is inconvenient. + /// + /// \param args Arguments to be forwarded to the constructor of the statistics + /// object. + template >> + void monitor(Args&&... args) { + _stats = std::make_shared>(std::forward(args)...); + } + + /// Stops any monitoring being performed with a statistics object. + /// + /// If the cache is not currently monitoring at all, this is a no-op. + virtual void stop_monitoring() { + _stats.reset(); + } + + /// \returns True if the cache is currently monitoring statistics, else + /// false. + bool is_monitoring() const noexcept { + return _stats.has_stats(); + } + + /// \returns The statistics object currently in use by the cache. + /// \throws LRU::Error::NotMonitoring if the cache is currently not + /// monitoring. + virtual Statistics& stats() { + if (!is_monitoring()) { + throw LRU::Error::NotMonitoring(); + } + return _stats.get(); + } + + /// \returns The statistics object currently in use by the cache. + /// \throws LRU::Error::NotMonitoring if the cache is currently not + /// monitoring. + virtual const Statistics& stats() const { + if (!is_monitoring()) { + throw LRU::Error::NotMonitoring(); + } + return _stats.get(); + } + + /// \returns A `shared_ptr` to the statistics currently in use by the cache. + virtual StatisticsPointer& shared_stats() { + return _stats.shared(); + } + + /// \returns A `shared_ptr` to the statistics currently in use by the cache. + virtual const StatisticsPointer& shared_stats() const { + return _stats.shared(); + } + + ///////////////////////////////////////////////////////////////////////////// + // CALLBACK INTERFACE + ///////////////////////////////////////////////////////////////////////////// + + /// Registers a new hit callback. + /// + /// \param hit_callback The hit callback function to register with the cache. + template > + void hit_callback(Callback&& hit_callback) { + _callback_manager.hit_callback(std::forward(hit_callback)); + } + + /// Registers a new miss callback. + /// + /// \param miss_callback The miss callback function to register with the + /// cache. + template > + void miss_callback(Callback&& miss_callback) { + _callback_manager.miss_callback(std::forward(miss_callback)); + } + + /// Registers a new access callback. + /// + /// \param access_callback The access callback function to register with the + /// cache. + template > + void access_callback(Callback&& access_callback) { + _callback_manager.access_callback(std::forward(access_callback)); + } + + /// Clears all hit callbacks. + void clear_hit_callbacks() { + _callback_manager.clear_hit_callbacks(); + } + + /// Clears all miss callbacks. + void clear_miss_callbacks() { + _callback_manager.clear_miss_callbacks(); + } + + /// Clears all access callbacks. + void clear_access_callbacks() { + _callback_manager.clear_access_callbacks(); + } + + /// Clears all callbacks. + void clear_all_callbacks() { + _callback_manager.clear(); + } + + /// \returns All hit callbacks. + const HitCallbackContainer& hit_callbacks() const noexcept { + return _callback_manager.hit_callbacks(); + } + + /// \returns All miss callbacks. + const MissCallbackContainer& miss_callbacks() const noexcept { + return _callback_manager.miss_callbacks(); + } + + /// \returns All access callbacks. + const AccessCallbackContainer& access_callbacks() const noexcept { + return _callback_manager.access_callbacks(); + } + + protected: + // The ordered iterators need to perform lookups without changing + // the order of elements or affecting statistics. + template + friend class BaseOrderedIterator; + + using MapInsertionResult = decltype(Map().emplace()); + using LastAccessed = + typename Internal::LastAccessed; + + /// Moves the key pointed to by the iterator to the front of the order. + /// + /// \param iterator The iterator pointing to the key to move. + virtual void _move_to_front(QueueIterator iterator) const { + if (size() == 1) return; + // Extract the current linked-list node and insert (splice it) at the end + // The original iterator is not invalidated and now points to the new + // position (which is still the same node). + _order.splice(_order.end(), _order, iterator); + } + + /// Moves the key pointed to by the iterator to the front of the order and + /// assigns a new value. + /// + /// \param iterator The iterator pointing to the key to move. + /// \param new_value The updated value to move the key with. + virtual void _move_to_front(MapIterator iterator, const Value& new_value) { + // Extract the current linked-list node and insert (splice it) at the end + // The original iterator is not invalidated and now points to the new + // position (which is still the same node). + _move_to_front(iterator->second.order); + iterator->second.value = new_value; + } + + /// Erases the element most recently inserted into the cache. + virtual void _erase_lru() { + _erase(_map.find(_order.front())); + } + + /// Erases the element pointed to by the iterator. + /// + /// \param iterator The iterator pointing to the key to erase. + virtual void _erase(MapConstIterator iterator) { + if (_last_accessed == iterator) { + _last_accessed.invalidate(); + } + + _order.erase(iterator->second.order); + _map.erase(iterator); + } + + /// Erases the given key. + /// + /// This method is useful if the key and information are already present, to + /// avoid an additional hash lookup to get an iterator to the corresponding + /// map entry. + /// + /// \param key The key to erase. + /// \param information The information associated with the key to erase. + virtual void _erase(const Key& key, const Information& information) { + if (key == _last_accessed) { + _last_accessed.invalidate(); + } + + // To be sure, we should do this first, since the order stores a reference + // to the key in the map. + _order.erase(information.order); + + // Requires an additional hash-lookup, whereas erase(iterator) doesn't + _map.erase(key); + } + + /// Convenience methhod to get the value for an insertion result into a map. + /// \returns The value for the given result. + virtual Value& _value_from_result(MapInsertionResult& result) noexcept { + // `result.first` is the map iterator (to a pair), whose `second` member + // is + // the information object, whose `value` member is the value stored. + return result.first->second.value; + } + + /// The main use of this method is that it may be override by a base class + /// if + /// there are any stronger constraints (such as time expiration) as to when + /// the last-accessed object may be used to access a key. + /// + /// \param key The key to compare the last accessed object against. + /// \returns True if the last-accessed object is valid. + virtual bool _last_accessed_is_ok(const Key& key) const noexcept { + return true; + } + + /// \copydoc _value_for_last_accessed() const + virtual Value& _value_for_last_accessed() { + return _last_accessed.value(); + } + + /// Attempts to access the last accessed key's value. + /// \returns The value of the last accessed object. + /// \details This method exists so that derived classes may perform + /// additional + /// checks (and possibly throw exceptions) or perform other operations to + /// retrieve the value. + virtual const Value& _value_for_last_accessed() const { + return _last_accessed.value(); + } + + /// Registers a hit for the key and performs appropriate actions. + /// \param key The key to register a hit for. + /// \param value The value that was found for the key. + virtual void _register_hit(const Key& key, const Value& value) const { + if (is_monitoring()) { + _stats.register_hit(key); + } + + _callback_manager.hit(key, value); + } + + /// Registers a miss for the key and performs appropriate actions. + /// \param key The key to register a miss for. + virtual void _register_miss(const Key& key) const { + if (is_monitoring()) { + _stats.register_miss(key); + } + + _callback_manager.miss(key); + } + + /// The common part of both range assignment operators. + /// + /// \param range The range to assign to. + template + void _clear_and_increase_capacity(const Range& range) { + using std::begin; + using std::end; + + clear(); + + auto distance = std::distance(begin(range), end(range)); + if (distance > _capacity) { + _capacity = distance; + } + } + + /// Looks up each key in the queue and re-assigns it to the proper key in the + /// map. + /// + /// After a copy, the reference (wrappers) in the order queue point + /// to the keys of the other cache's map. Thus we need to re-assign them. + void _reassign_references() noexcept { + for (auto& key_reference : _order) { + key_reference = std::ref(_map.find(key_reference)->first); + } + } + + /// Inserts a new key into the queue. + /// + /// If the cache is full, the LRU node is re-used. + /// Else a node is inserted at the order. + /// + /// \returns The resulting iterator. + QueueIterator _insert_new_key(const Key& key) { + if (_is_too_full()) { + _evict_lru_for(key); + } else { + _order.emplace_back(key); + } + + return std::prev(_order.end()); + } + + /// Evicts the LRU element for the given new key. + /// + /// \param key The new key to insert into the queue. + void _evict_lru_for(const Key& key) { + _map.erase(_order.front()); + _order.front() = std::ref(key); + _move_to_front(_order.begin()); + } + + /// \returns True if the cache is too full and an element must be evicted, + /// else false. + bool _is_too_full() const noexcept { + return size() > _capacity; + } + + /// The map from keys to information objects. + Map _map; + + /// The queue keeping track of the insertion order of elements. + mutable Queue _order; + + /// The object to mutate statistics if any are registered. + mutable StatisticsMutator _stats; + + /// The last-accessed cache object. + mutable LastAccessed _last_accessed; + + /// The callback manager to store any callbacks. + mutable CallbackManagerType _callback_manager; + + /// The current capacity of the cache. + size_t _capacity; +}; +} // namespace Internal +} // namespace LRU + +#endif // LRU_INTERNAL_BASE_CACHE_HPP diff --git a/include/lru/internal/base-iterator.hpp b/include/lru/internal/base-iterator.hpp new file mode 100644 index 0000000..b695a72 --- /dev/null +++ b/include/lru/internal/base-iterator.hpp @@ -0,0 +1,216 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_INTERNAL_BASE_ITERATOR_HPP +#define LRU_INTERNAL_BASE_ITERATOR_HPP + +#include +#include + +#include +#include + +#define PUBLIC_BASE_ITERATOR_MEMBERS \ + typename super::Entry; \ + using typename super::KeyType; \ + using typename super::ValueType; + +#define PRIVATE_BASE_ITERATOR_MEMBERS \ + super::_iterator; \ + using super::_entry; \ + using super::_cache; + + +namespace LRU { +namespace Internal { + +/// The base class for all (ordered and unordered) iterators. +/// +/// All iterators over our LRU caches store a reference to the cache they point +/// into, an underlying iterator they adapt (e.g. a map iterator or list +/// iterator) as well as a entry, a reference to which is returned when +/// dereferencing the iterator. +/// +/// \tparam IteratorTag A standard iterator category tag. +/// \tparam Key The key type over which instances of the iterator iterate. +/// \tparam Value The value type over which instances of the iterator iterate. +/// \tparam Cache The type of the cache instances of the iterator point into. +/// \tparam UnderlyingIterator The underlying iterator class used to implement +/// the iterator. +template +class BaseIterator : public std::iterator> { + public: + using KeyType = Key; + using ValueType = + std::conditional_t::value, const Value, Value>; + using Entry = LRU::Entry; + + /// Default constructor. + BaseIterator() noexcept : _cache(nullptr) { + } + + /// Constructor. + /// + /// \param cache The cache this iterator points into. + /// \param iterator The underlying iterator to adapt. + BaseIterator(Cache& cache, const UnderlyingIterator& iterator) noexcept + : _iterator(iterator), _cache(&cache) { + } + + /// Copy constructor. + /// + /// Differs from the default copy constructor in that it does not copy the + /// entry. + /// + /// \param other The base iterator to copy. + BaseIterator(const BaseIterator& other) noexcept + : _iterator(other._iterator), _cache(other._cache) { + // Note: we do not copy the entry, as it would require a new allocation. + // Since iterators are often taken by value, this may incur a high cost. + // As such we delay the retrieval of the entry to the first call to entry(). + } + + /// Copy assignment operator. + /// + /// Differs from the default copy assignment + /// operator in that it does not copy the entry. + /// + /// \param other The base iterator to copy. + /// \return The base iterator instance. + BaseIterator& operator=(const BaseIterator& other) noexcept { + if (this != &other) { + _iterator = other._iterator; + _cache = other._cache; + _entry.reset(); + } + return *this; + } + + /// Move constructor. + BaseIterator(BaseIterator&& other) noexcept = default; + + /// Move assignment operator. + BaseIterator& operator=(BaseIterator&& other) noexcept = default; + + /// Generalized copy constructor. + /// + /// Mainly necessary for non-const to const conversion. + /// + /// \param other The base iterator to copy from. + template + BaseIterator(const BaseIterator& other) + : _iterator(other._iterator), _entry(other._entry), _cache(other._cache) { + } + + /// Generalized move constructor. + /// + /// Mainly necessary for non-const to const conversion. + /// + /// \param other The base iterator to move into this one. + template + BaseIterator(BaseIterator&& other) noexcept + : _iterator(std::move(other._iterator)) + , _entry(std::move(other._entry)) + , _cache(std::move(other._cache)) { + } + + /// Destructor. + virtual ~BaseIterator() = default; + + /// Swaps this base iterator with another one. + /// + /// \param other The other iterator to swap with. + void swap(BaseIterator& other) noexcept { + // Enable ADL + using std::swap; + + swap(_iterator, other._iterator); + swap(_entry, other._entry); + swap(_cache, other._cache); + } + + /// Swaps two base iterator. + /// + /// \param first The first iterator to swap. + /// \param second The second iterator to swap. + friend void swap(BaseIterator& first, BaseIterator& second) noexcept { + first.swap(second); + } + + /// \returns A reference to the current entry pointed to by the iterator. + virtual Entry& operator*() noexcept = 0; + + /// \returns A pointer to the current entry pointed to by the iterator. + Entry* operator->() noexcept { + return &(**this); + } + + /// \copydoc operator*() + virtual Entry& entry() = 0; + + /// \returns A reference to the value of the entry currently pointed to by the + /// iterator. + virtual ValueType& value() = 0; + + /// \returns A reference to the key of the entry currently pointed to by the + /// iterator. + virtual const Key& key() = 0; + + protected: + template + friend class BaseIterator; + + /// The underlying iterator this iterator class adapts. + UnderlyingIterator _iterator; + + /// The entry optionally being stored. + Optional _entry; + + /// A pointer to the cache this iterator points into. + /// Pointer and not reference because it's cheap to copy. + /// Pointer and not `std::reference_wrapper` because the class needs to be + /// default-constructible. + Cache* _cache; +}; +} // namespace Internal +} // namespace LRU + +#endif // LRU_INTERNAL_BASE_ITERATOR_HPP diff --git a/include/lru/internal/base-ordered-iterator.hpp b/include/lru/internal/base-ordered-iterator.hpp new file mode 100644 index 0000000..a0ed951 --- /dev/null +++ b/include/lru/internal/base-ordered-iterator.hpp @@ -0,0 +1,338 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef BASE_ORDERED_ITERATOR_HPP +#define BASE_ORDERED_ITERATOR_HPP + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace LRU { +namespace Internal { + +template +using BaseForBaseOrderedIterator = + BaseIterator::const_iterator>; + +/// The base class for all const and non-const ordered iterators. +/// +/// Ordered iterators are bidirectional iterators that iterate over the keys of +/// a cache in the order in which they were inserted into the cache. As they +/// only iterate over the keys, they must perform hash lookups to retrieve the +/// value the first time they are dereferenced. This makes them slightly less +/// efficient than unordered iterators. However, they also have the additional +/// property that they may be constructed from unordered iterators, and that +/// they can be decremented. +/// +/// \tparam Key The key type over which instances of the iterator iterate. +/// \tparam Value The value type over which instances of the iterator iterate. +/// \tparam Cache The type of the cache instances of the iterator point into. +template +class BaseOrderedIterator + : public BaseForBaseOrderedIterator { + protected: + using super = BaseForBaseOrderedIterator; + using PRIVATE_BASE_ITERATOR_MEMBERS; + using UnderlyingIterator = typename Queue::const_iterator; + + public: + using Tag = LRU::Tag::OrderedIterator; + using PUBLIC_BASE_ITERATOR_MEMBERS; + + /// Constructor. + BaseOrderedIterator() noexcept = default; + + /// \copydoc BaseIterator::BaseIterator(Cache,UnderlyingIterator) + BaseOrderedIterator(Cache& cache, UnderlyingIterator iterator) + : super(cache, iterator) { + } + + /// Generalized copy constructor. + /// + /// \param other The ordered iterator to contruct from. + template + BaseOrderedIterator( + const BaseOrderedIterator& other) + : super(other) { + } + + /// Generalized move constructor. + /// + /// \param other The ordered iterator to move into this one. + template + BaseOrderedIterator(BaseOrderedIterator&& other) + : super(std::move(other)) { + } + + /// Generalized conversion copy constructor. + /// + /// \param unordered_iterator The unordered iterator to construct from. + template < + typename AnyCache, + typename UnderlyingIterator, + typename = std::enable_if_t< + std::is_same, std::decay_t>::value>> + BaseOrderedIterator(const BaseUnorderedIterator& + unordered_iterator) { + // Atomicity + _throw_if_at_invalid(unordered_iterator); + _cache = unordered_iterator._cache; + _iterator = unordered_iterator._iterator->second.order; + } + + /// Generalized conversion move constructor. + /// + /// \param unordered_iterator The unordered iterator to move-construct from. + template < + typename AnyCache, + typename UnderlyingIterator, + typename = std::enable_if_t< + std::is_same, std::decay_t>::value>> + BaseOrderedIterator(BaseUnorderedIterator&& + unordered_iterator) { + // Atomicity + _throw_if_at_invalid(unordered_iterator); + _cache = std::move(unordered_iterator._cache); + _entry = std::move(unordered_iterator._entry); + _iterator = std::move(unordered_iterator._iterator->second.order); + } + + /// Copy constructor. + BaseOrderedIterator(const BaseOrderedIterator& other) = default; + + /// Move constructor. + BaseOrderedIterator(BaseOrderedIterator&& other) = default; + + /// Copy assignment operator. + BaseOrderedIterator& operator=(const BaseOrderedIterator& other) = default; + + /// Move assignment operator. + BaseOrderedIterator& operator=(BaseOrderedIterator&& other) = default; + + /// Destructor. + virtual ~BaseOrderedIterator() = default; + + /// Checks for equality between this iterator and another ordered iterator. + /// + /// \param other The other ordered iterator. + /// \returns True if both iterators point to the same entry, else false. + bool operator==(const BaseOrderedIterator& other) const noexcept { + return this->_iterator == other._iterator; + } + + /// Checks for inequality between this iterator another ordered iterator. + /// + /// \param other The other ordered iterator. + /// \returns True if the iterators point to different entries, else false. + bool operator!=(const BaseOrderedIterator& other) const noexcept { + return !(*this == other); + } + + /// Checks for inequality between this iterator and another unordered + /// iterator. + /// + /// \param other The other unordered iterator. + /// \returns True if both iterators point to the end of the same cache, else + /// the result of comparing with the unordered iterator, converted to an + /// ordered iterator. + template + bool operator==( + const BaseUnorderedIterator& other) const + noexcept { + if (this->_cache != other._cache) return false; + + // The past-the-end iterators of the same cache should compare equal. + // This is an exceptional guarantee we make. This is also the reason + // why we can't rely on the conversion from unordered to ordered iterators + // because construction of an ordered iterator from the past-the-end + // unordered iterator will fail (with an InvalidIteratorConversion error) + if (other == other._cache->unordered_end()) { + return *this == this->_cache->ordered_end(); + } + + // Will call the other overload + return *this == static_cast(other); + } + + /// Checks for equality between an unordered iterator and an ordered iterator. + /// + /// \param first The unordered iterator. + /// \param second The ordered iterator. + /// \returns True if both iterators point to the end of the same cache, else + /// the result of comparing with the unordered iterator, converted to an + /// ordered iterator. + template + friend bool operator==( + const BaseUnorderedIterator& first, + const BaseOrderedIterator& second) noexcept { + return second == first; + } + + /// Checks for inequality between an unordered + /// iterator and an ordered iterator. + /// + /// \param first The ordered iterator. + /// \param second The unordered iterator. + /// \returns True if the iterators point to different entries, else false. + template + friend bool + operator!=(const BaseOrderedIterator& first, + const BaseUnorderedIterator& + second) noexcept { + return !(first == second); + } + + /// Checks for inequality between an unordered + /// iterator and an ordered iterator. + /// + /// \param first The unordered iterator. + /// \param second The ordered iterator. + /// \returns True if the iterators point to different entries, else false. + template + friend bool operator!=( + const BaseUnorderedIterator& first, + const BaseOrderedIterator& second) noexcept { + return second != first; + } + + /// Increments the iterator to the next entry. + /// + /// If the iterator already pointed to the end any number of increments + /// before, behavior is undefined. + /// + /// \returns The resulting iterator. + BaseOrderedIterator& operator++() { + ++_iterator; + _entry.reset(); + return *this; + } + + /// Increments the iterator and returns a copy of the previous one. + /// + /// If the iterator already pointed to the end any number of increments + /// before, behavior is undefined. + /// + /// \returns A copy of the previous iterator. + BaseOrderedIterator operator++(int) { + auto previous = *this; + ++*this; + return previous; + } + + /// Decrements the iterator to the previous entry. + /// + /// \returns The resulting iterator. + BaseOrderedIterator& operator--() { + --_iterator; + _entry.reset(); + return *this; + } + + /// Decrements the iterator and returns a copy of the previous entry. + /// + /// \returns The previous iterator. + BaseOrderedIterator operator--(int) { + auto previous = *this; + --*this; + return previous; + } + + Entry& operator*() noexcept override { + return _maybe_lookup(); + } + + /// \returns A reference to the entry the iterator points to. + /// \details If the iterator is invalid, behavior is undefined. + Entry& entry() override { + _cache->throw_if_invalid(*this); + return _maybe_lookup(); + } + + /// \returns A reference to the value the iterator points to. + /// \details If the iterator is invalid, behavior is undefined. + Value& value() override { + return entry().value(); + } + + /// \returns A reference to the key the iterator points to. + /// \details If the iterator is invalid, behavior is undefined. + const Key& key() override { + // No lookup required + _cache->throw_if_invalid(*this); + return *_iterator; + } + + protected: + template + friend class BaseOrderedIterator; + + /// Looks up the entry for a key if this was not done already. + /// + /// \returns The entry, which was possibly newly looked up. + Entry& _maybe_lookup() { + if (!_entry.has_value()) { + _lookup(); + } + + return *_entry; + } + + /// Looks up the entry for a key and sets the internal entry member. + void _lookup() { + auto iterator = _cache->_map.find(*_iterator); + _entry.emplace(iterator->first, iterator->second.value); + } + + private: + /// Throws an exception if the given unordered iterator is invalid. + /// + /// \param unordered_iterator The iterator to check. + /// \throws LRU::Error::InvalidIteratorConversion if the iterator is invalid. + template + void _throw_if_at_invalid(const UnorderedIterator& unordered_iterator) { + // For atomicity of the copy assignment, we assign the cache pointer only + // after this check in the copy/move constructor and use the iterator's + // cache. If an exception is thrown, the state of the ordered iterator is + // unchanged compared to before the assignment. + if (unordered_iterator == unordered_iterator._cache->unordered_end()) { + throw LRU::Error::InvalidIteratorConversion(); + } + } +}; + +} // namespace Internal +} // namespace LRU + +#endif // BASE_ORDERED_ITERATOR_HPP diff --git a/include/lru/internal/base-unordered-iterator.hpp b/include/lru/internal/base-unordered-iterator.hpp new file mode 100644 index 0000000..f294b13 --- /dev/null +++ b/include/lru/internal/base-unordered-iterator.hpp @@ -0,0 +1,216 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef BASE_UNORDERED_ITERATOR_HPP +#define BASE_UNORDERED_ITERATOR_HPP + +#include +#include +#include + +#include +#include +#include +#include +#include + + +namespace LRU { + +// Forward declaration. +template +class TimedCache; + +namespace Internal { +template +using BaseForBaseUnorderedIterator = + BaseIteratorfirst), + decltype(UnderlyingIterator()->second.value), + Cache, + UnderlyingIterator>; + +/// The base class for all const and non-const unordered iterators. +/// +/// An unordered iterator is a wrapper around an `unordered_map` iterator with +/// ForwardIterator category. As such, it is (nearly) as fast to access the pair +/// as through the unordered iterator as through the map iterator directly. +/// However, the order of keys is unspecified. For this reason, unordered +/// iterators have the special property that they may be used to construct +/// ordered iterators, after which the order of insertion is respected. +/// +/// \tparam Cache The type of the cache instances of the iterator point into. +/// \tparam UnderlyingIterator The underlying iterator class used to implement +/// the iterator. +template +class BaseUnorderedIterator + : public BaseForBaseUnorderedIterator { + protected: + using super = BaseForBaseUnorderedIterator; + using PRIVATE_BASE_ITERATOR_MEMBERS; + // These are the key and value types the BaseIterator extracts + using Key = typename super::KeyType; + using Value = typename super::ValueType; + + public: + using Tag = LRU::Tag::UnorderedIterator; + using PUBLIC_BASE_ITERATOR_MEMBERS; + + /// Constructor. + BaseUnorderedIterator() noexcept = default; + + /// \copydoc BaseIterator::BaseIterator(Cache,UnderlyingIterator) + explicit BaseUnorderedIterator(Cache& cache, + const UnderlyingIterator& iterator) noexcept + : super(cache, iterator) { + } + + /// Generalized copy constructor. + /// + /// Useful mainly for non-const to const conversion. + /// + /// \param other The iterator to copy from. + template + BaseUnorderedIterator( + const BaseUnorderedIterator& + other) noexcept + : super(other) { + } + + /// Copy constructor. + BaseUnorderedIterator(const BaseUnorderedIterator& other) noexcept = default; + + /// Move constructor. + BaseUnorderedIterator(BaseUnorderedIterator&& other) noexcept = default; + + /// Copy assignment operator. + BaseUnorderedIterator& + operator=(const BaseUnorderedIterator& other) noexcept = default; + + /// Move assignment operator. + template + BaseUnorderedIterator& + operator=(BaseUnorderedIterator + unordered_iterator) noexcept { + swap(unordered_iterator); + return *this; + } + + /// Destructor. + virtual ~BaseUnorderedIterator() = default; + + /// Compares this iterator for equality with another unordered iterator. + /// + /// \param other Another unordered iterator. + /// \returns True if both iterators point to the same entry, else false. + template + bool + operator==(const BaseUnorderedIterator& other) const + noexcept { + return this->_iterator == other._iterator; + } + + /// Compares this iterator for inequality with another unordered iterator. + /// + /// \param other Another unordered iterator. + /// \returns True if the iterators point to different entries, else false. + template + bool + operator!=(const BaseUnorderedIterator& other) const + noexcept { + return !(*this == other); + } + + /// Increments the iterator to the next entry. + /// + /// If the iterator already pointed to the end any number of increments + /// before, behavior is undefined. + /// + /// \returns The resulting iterator. + BaseUnorderedIterator& operator++() { + ++_iterator; + _entry.reset(); + return *this; + } + + /// Increments the iterator and returns a copy of the previous one. + /// + /// If the iterator already pointed to the end any number of increments + /// before, behavior is undefined. + /// + /// \returns A copy of the previous iterator. + BaseUnorderedIterator operator++(int) { + auto previous = *this; + ++*this; + return previous; + } + + /// \copydoc BaseIterator::operator* + /// \details If the iterator is invalid, behavior is undefined. No exception + /// handling is performed. + Entry& operator*() noexcept override { + if (!_entry.has_value()) { + _entry.emplace(_iterator->first, _iterator->second.value); + } + + return *_entry; + } + + /// \returns A reference to the entry the iterator points to. + /// \throws LRU::Error::InvalidIterator if the iterator is the end iterator. + /// \throws LRU::Error::KeyExpired if the key pointed to by the iterator has + /// expired. + Entry& entry() override { + if (!_entry.has_value()) { + _entry.emplace(_iterator->first, _iterator->second.value); + } + + _cache->throw_if_invalid(*this); + return *_entry; + } + + /// \returns A reference to the key the iterator points to. + /// \throws LRU::Error::InvalidIterator if the iterator is the end iterator. + /// \throws LRU::Error::KeyExpired if the key pointed to by the iterator has + /// expired. + const Key& key() override { + return entry().key(); + } + + /// \returns A reference to the value the iterator points to. + /// \throws LRU::Error::InvalidIterator if the iterator is the end iterator. + /// \throws LRU::Error::KeyExpired if the key pointed to by the iterator has + /// expired. + Value& value() override { + return entry().value(); + } + + protected: + template + friend class BaseOrderedIterator; + + template + friend class LRU::TimedCache; +}; +} // namespace Internal +} // namespace LRU + +#endif // BASE_UNORDERED_ITERATOR_HPP diff --git a/include/lru/internal/callback-manager.hpp b/include/lru/internal/callback-manager.hpp new file mode 100644 index 0000000..c1fc889 --- /dev/null +++ b/include/lru/internal/callback-manager.hpp @@ -0,0 +1,159 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_INTERNAL_CALLBACK_MANAGER_HPP +#define LRU_INTERNAL_CALLBACK_MANAGER_HPP + +#include +#include + +#include +#include + +namespace LRU { +namespace Internal { + +/// Manages hit, miss and access callbacks for a cache. +/// +/// The callback manager implements the "publisher" of the observer pattern we +/// implement. It stores and calls three kinds of callbacks: +/// 1. Hit callbacks, taking a key and value after a cache hit. +/// 2. Miss callbacks, taking only a key, that was not found in a cache. +/// 3. Access callbacks, taking a key and a boolean indicating a hit or a miss. +/// +/// Callbacks can be added, accessed and cleared. +template +class CallbackManager { + public: + using HitCallback = std::function; + using MissCallback = std::function; + using AccessCallback = std::function; + + using HitCallbackContainer = std::vector; + using MissCallbackContainer = std::vector; + using AccessCallbackContainer = std::vector; + + /// Calls all callbacks registered for a hit, with the given key and value. + /// + /// \param key The key for which a cache hit ocurred. + /// \param value The value that was found for the key. + void hit(const Key& key, const Value& value) { + _call_each(_hit_callbacks, key, value); + _call_each(_access_callbacks, key, true); + } + + /// Calls all callbacks registered for a miss, with the given key. + /// + /// \param key The key for which a cache miss ocurred. + void miss(const Key& key) { + _call_each(_miss_callbacks, key); + _call_each(_access_callbacks, key, false); + } + + /// Registers a new hit callback. + /// + /// \param hit_callback The hit callback function to register with the + /// manager. + template + void hit_callback(Callback&& hit_callback) { + _hit_callbacks.emplace_back(std::forward(hit_callback)); + } + + /// Registers a new miss callback. + /// + /// \param miss_callback The miss callback function to register with the + /// manager. + template + void miss_callback(Callback&& miss_callback) { + _miss_callbacks.emplace_back(std::forward(miss_callback)); + } + + /// Registers a new access callback. + /// + /// \param access_callback The access callback function to register with the + /// manager. + template + void access_callback(Callback&& access_callback) { + _access_callbacks.emplace_back(std::forward(access_callback)); + } + + /// Clears all hit callbacks. + void clear_hit_callbacks() { + _hit_callbacks.clear(); + } + + /// Clears all miss callbacks. + void clear_miss_callbacks() { + _miss_callbacks.clear(); + } + + /// Clears all access callbacks. + void clear_access_callbacks() { + _access_callbacks.clear(); + } + + /// Clears all callbacks. + void clear() { + clear_hit_callbacks(); + clear_miss_callbacks(); + clear_access_callbacks(); + } + + /// \returns All hit callbacks. + const HitCallbackContainer& hit_callbacks() const noexcept { + return _hit_callbacks; + } + + /// \returns All miss callbacks. + const MissCallbackContainer& miss_callbacks() const noexcept { + return _miss_callbacks; + } + + /// \returns All access callbacks. + const AccessCallbackContainer& access_callbacks() const noexcept { + return _access_callbacks; + } + + private: + /// Calls each function in the given container with the given arguments. + /// + /// \param callbacks The container of callbacks to call. + /// \param args The arguments to call the callbacks with. + template + void _call_each(const CallbackContainer& callbacks, Args&&... args) { + for (const auto& callback : callbacks) { + callback(std::forward(args)...); + } + } + + /// The container of hit callbacks registered. + HitCallbackContainer _hit_callbacks; + + /// The container of miss callbacks registered. + MissCallbackContainer _miss_callbacks; + + /// The container of access callbacks registered. + AccessCallbackContainer _access_callbacks; +}; +} // namespace Internal +} // namespace LRU + +#endif // LRU_INTERNAL_CALLBACK_MANAGER_HPP diff --git a/include/lru/internal/definitions.hpp b/include/lru/internal/definitions.hpp new file mode 100644 index 0000000..cf0d1c7 --- /dev/null +++ b/include/lru/internal/definitions.hpp @@ -0,0 +1,88 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_INTERNAL_DEFINITIONS_HPP +#define LRU_INTERNAL_DEFINITIONS_HPP + +#include +#include +#include +#include +#include +#include + +namespace LRU { +namespace Internal { + +/// The default capacity for all caches. +const std::size_t DEFAULT_CAPACITY = 128; + +/// The reference type use to store keys in the order queue. +template +using Reference = std::reference_wrapper; + +/// Compares two References for equality. +/// +/// This is necessary because `std::reference_wrapper` does not define any +/// operator overloads. We do need them, however (e.g. for container +/// comparison). +/// +/// \param first The first reference to compare. +/// \param second The second reference to compare. +template +bool operator==(const Reference& first, const Reference& second) { + return first.get() == second.get(); +} + +/// Compares two References for inequality. +/// +/// This is necessary because `std::reference_wrapper` does not define any +/// operator overloads. We do need them, however (e.g. for container +/// comparison). +/// +/// \param first The first reference to compare. +/// \param second The second reference to compare. +template +bool operator!=(const Reference& first, const Reference& second) { + return !(first == second); +} + +/// The default queue type used internally. +template +using Queue = std::list>; + +/// The default map type used internally. +template +using Map = std::unordered_map; + +/// The default clock used internally. +using Clock = std::chrono::steady_clock; + +/// The default timestamp (time point) used internally. +using Timestamp = Clock::time_point; +} // namespace Internal +} // namespace LRU + + +#endif // LRU_INTERNAL_DEFINITIONS_HPP diff --git a/include/lru/internal/hash.hpp b/include/lru/internal/hash.hpp new file mode 100644 index 0000000..49dcc0f --- /dev/null +++ b/include/lru/internal/hash.hpp @@ -0,0 +1,62 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_INTERNAL_HASH_HPP +#define LRU_INTERNAL_HASH_HPP + +#include +#include +#include + +/// `std::hash` specialization to allow storing tuples as keys +/// in `std::unordered_map`. +/// +/// Essentially hashes all tuple elements and jumbles the +/// individual hashes together. +namespace std { +template +struct hash> { + using argument_type = std::tuple; + using result_type = std::size_t; + + result_type operator()(const argument_type& argument) const { + return hash_tuple(argument, std::make_index_sequence()); + } + + private: + template + result_type + hash_tuple(const argument_type& tuple, std::index_sequence) const { + auto value = std::get(tuple); + auto current = std::hash{}(value); + auto seed = hash_tuple(tuple, std::index_sequence()); + + // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/hash_combine_id241013.html + return current + 0x9e3779b9 + (seed << 6) + (seed >> 2); + } + + result_type hash_tuple(const argument_type&, std::index_sequence<>) const { + return 0; + } +}; +} // namespace std + +#endif // LRU_INTERNAL_HASH_HPP diff --git a/include/lru/internal/information.hpp b/include/lru/internal/information.hpp new file mode 100644 index 0000000..4a28a98 --- /dev/null +++ b/include/lru/internal/information.hpp @@ -0,0 +1,145 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_INTERNAL_INFORMATION_HPP +#define LRU_INTERNAL_INFORMATION_HPP + +#include +#include +#include + +#include +#include + +namespace LRU { +namespace Internal { + +/// The value type of internal maps, used to store a value and iterator. +/// +/// This information object is the basis of an LRU cache, which must associated +/// a value and such an order iterator with a key, such that the iterator may be +/// moved to the front of the order when the key is updated with a new value. +/// +/// \tparam Key The key type of the information. +/// \tparam Value The value type of the information. +template +struct Information { + using KeyType = Key; + using ValueType = Value; + using QueueIterator = typename Internal::Queue::const_iterator; + + /// Constructor. + /// + /// \param value_ The value for the information. + /// \param order_ The order iterator for the information. + explicit Information(const Value& value_, + QueueIterator order_ = QueueIterator()) + : value(value_), order(order_) { + } + + /// Constructor. + /// + /// \param order_ The order iterator for the information. + /// \param value_arguments Any number of arguments to perfectly forward to the + /// value type's constructor. + // template + // Information(QueueIterator order_, ValueArguments&&... value_arguments) + // : value(std::forward(value_arguments)...), order(order_) { + // } + + /// Constructor. + /// + /// \param order_ The order iterator for the information. + /// \param value_arguments A tuple of arguments to perfectly forward to the + /// value type's constructor. + /// + template + explicit Information(const std::tuple& value_arguments, + QueueIterator order_ = QueueIterator()) + : Information( + order_, value_arguments, Internal::tuple_indices(value_arguments)) { + } + + /// Copy constructor. + Information(const Information& other) = default; + + /// Move constructor. + Information(Information&& other) = default; + + /// Copy assignment operator. + Information& operator=(const Information& other) = default; + + /// Move assignment operator. + Information& operator=(Information&& other) = default; + + /// Destructor. + virtual ~Information() = default; + + /// Compares the information for equality with another information object. + /// + /// \param other The other information object to compare to. + /// \returns True if key and value (not the iterator itself) of the two + /// information objects are equal, else false. + virtual bool operator==(const Information& other) const noexcept { + if (this == &other) return true; + if (this->value != other.value) return false; + // We do not compare the iterator (because otherwise two containers + // holding information would never be equal). We also do not compare + // the key stored in the iterator, because keys will always have been + // compared before this operator is called. + return true; + } + + /// Compares the information for inequality with another information object. + /// + /// \param other The other information object to compare for. + /// \returns True if key and value (not the iterator itself) of the two + /// information objects are unequal, else false. + virtual bool operator!=(const Information& other) const noexcept { + return !(*this == other); + } + + /// The value of the information. + Value value; + + /// The order iterator of the information. + QueueIterator order; + + private: + /// Implementation for the constructor taking a tuple of arguments for the + /// value. + /// + /// \param order_ The order iterator for the information. + /// \param value_argument The tuple of arguments to perfectly forward to the + /// value type's constructor. + /// \param _ An index sequence to access the elements of the tuple + template + Information(const QueueIterator& order_, + const std::tuple& value_argument, + std::index_sequence _) + : value(std::forward(std::get(value_argument))...) + , order(order_) { + } +}; +} // namespace Internal +} // namespace LRU + +#endif // LRU_INTERNAL_INFORMATION_HPP diff --git a/include/lru/internal/last-accessed.hpp b/include/lru/internal/last-accessed.hpp new file mode 100644 index 0000000..c6bfb25 --- /dev/null +++ b/include/lru/internal/last-accessed.hpp @@ -0,0 +1,254 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_INTERNAL_LAST_ACCESSED_HPP +#define LRU_INTERNAL_LAST_ACCESSED_HPP + +#include +#include +#include + +#include + +namespace LRU { +namespace Internal { + +/// Provides a simple iterator-compatible pointer object for a key and +/// information. +/// +/// The easisest idea for this class, theoretically, would be to just store an s +/// iterator to the internal cache map (i.e. template the class on the iterator +/// type). However, the major trouble with that approach is that this class +/// should be 100% *mutable*, as in "always non-const", so that keys and +/// informations +/// we store for fast access can be (quickly) retrieved as either const or +/// non-const (iterators for example). This is not possible, since the +/// const-ness of `const_iterators` are not the usual idea of const in C++, +/// meaning especially it cannot be cast away with a `const_cast` as is required +/// for the mutability. As such, we *must* store the plain keys and +/// informations. +/// This, however, means that iterators cannot be stored efficiently, since a +/// new hash table lookup would be required to go from a key to its iterator. +/// However, since the main use case of this class is to avoid a second lookup +/// in the usual `if (cache.contains(key)) return cache.lookup(key)`, which is +/// not an issue for iterators since they can be compared to the `end` iterator +/// in constant time (equivalent to the call to `contains()`). +/// +/// WARNING: This class stores *pointers* to keys and informations. As such +/// lifetime +/// of the pointed-to objects must be cared for by the user of this class. +/// +/// \tparam Key The type of key being accessed. +/// \tparam InformationType The type of information being accessed. +/// \tparam KeyEqual The type of the key comparison function. +template > +class LastAccessed { + public: + /// Constructor. + /// + /// \param key_equal The function to compare keys with. + explicit LastAccessed(const KeyEqual& key_equal = KeyEqual()) + : _key(nullptr) + , _information(nullptr) + , _is_valid(false) + , _key_equal(key_equal) { + } + + /// Constructor. + /// + /// \param key The key to store a reference to. + /// \param information The information to store a reference to. + /// \param key_equal The function to compare keys with. + LastAccessed(const Key& key, + const InformationType& information, + const KeyEqual& key_equal = KeyEqual()) + : _key(const_cast(&key)) + , _information(const_cast(&information)) + , _is_valid(true) + , _key_equal(key_equal) { + } + + /// Constructor. + /// + /// \param iterator An iterator pointing to a key and information to use for + /// constructing the instance. + /// \param key_equal The function to compare keys with. + template + explicit LastAccessed(Iterator iterator, + const KeyEqual& key_equal = KeyEqual()) + : LastAccessed(iterator->first, iterator->second, key_equal) { + } + + /// Copy assignment operator for iterators. + /// + /// \param iterator An iterator pointing to a key and value to use for the + /// `LastAccessed` instance. + /// \return The resulting `LastAccessed` instance. + template + LastAccessed& operator=(Iterator iterator) { + _key = const_cast(&(iterator->first)); + _information = const_cast(&(iterator->second)); + _is_valid = true; + + return *this; + } + + /// Compares a `LastAccessed` object for equality with a key. + /// + /// \param last_accessed The `LastAccessed` instance to compare. + /// \param key The key instance to compare. + /// \returns True if the key of the `LastAccessed` object's key equals the + /// given key, else false. + friend bool + operator==(const LastAccessed& last_accessed, const Key& key) noexcept { + if (!last_accessed._is_valid) return false; + return last_accessed._key_equal(key, last_accessed.key()); + } + + /// \copydoc operator==(const LastAccessed&,const Key&) + friend bool + operator==(const Key& key, const LastAccessed& last_accessed) noexcept { + return last_accessed == key; + } + + /// Compares a `LastAccessed` object for equality with an iterator. + /// + /// \param last_accessed The `LastAccessed` instance to compare. + /// \param iterator The iterator to compare with. + /// \returns True if the `LastAccessed` object's key equals that of the + /// iterator, else false. + template > + friend bool + operator==(const LastAccessed& last_accessed, Iterator iterator) noexcept { + /// Fast comparisons to an iterator (not relying on implicit conversion) + return last_accessed == iterator->first; + } + + /// \copydoc operator==(const LastAccessed&,Iterator) + template > + friend bool + operator==(Iterator iterator, const LastAccessed& last_accessed) noexcept { + return last_accessed == iterator; + } + + /// Compares a `LastAccessed` object for inequality with something. + /// + /// \param last_accessed The `LastAccessed` instance to compare. + /// \param other Something else to compare to. + /// \returns True if the key of the `LastAccessed` object's key does not equal + /// the given other object's key, else false. + template + friend bool + operator!=(const LastAccessed& last_accessed, const T& other) noexcept { + return !(last_accessed == other); + } + + /// \copydoc operator!=(const LastAccessed&,const T&) + template + friend bool + operator!=(const T& other, const LastAccessed& last_accessed) noexcept { + return !(other == last_accessed); + } + + /// \returns The last accessed key. + Key& key() noexcept { + assert(is_valid()); + return *_key; + } + + /// \returns The last accessed key. + const Key& key() const noexcept { + assert(is_valid()); + return *_key; + } + + /// \returns The last accessed information. + InformationType& information() noexcept { + assert(is_valid()); + return *_information; + } + + /// \returns The last accessed information. + const InformationType& information() const noexcept { + assert(is_valid()); + return *_information; + } + + /// \returns The last accessed information. + auto& iterator() noexcept { + assert(is_valid()); + return _information->order; + } + + /// \returns The last accessed value. + auto& value() noexcept { + assert(is_valid()); + return _information->value; + } + + /// \returns The last accessed value. + const auto& value() const noexcept { + assert(is_valid()); + return _information->value; + } + + /// \returns True if the key and information of the instance may be accessed, + /// else false. + bool is_valid() const noexcept { + return _is_valid; + } + + /// \copydoc is_valid() + explicit operator bool() const noexcept { + return is_valid(); + } + + /// Invalidates the instance. + void invalidate() noexcept { + _is_valid = false; + _key = nullptr; + _information = nullptr; + } + + /// \returns The key comparison function used. + const KeyEqual& key_equal() const noexcept { + return _key_equal; + } + + private: + /// A pointer to the key that was last accessed (if any). + Key* _key; + + /// A pointer to the information that was last accessed (if any). + InformationType* _information; + + /// True if the key and information pointers are valid, else false. + bool _is_valid; + + /// The function used to compare keys. + KeyEqual _key_equal; +}; +} // namespace Internal +} // namespace LRU + +#endif // LRU_INTERNAL_LAST_ACCESSED_HPP diff --git a/include/lru/internal/optional.hpp b/include/lru/internal/optional.hpp new file mode 100644 index 0000000..8336d3b --- /dev/null +++ b/include/lru/internal/optional.hpp @@ -0,0 +1,207 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_INTERNAL_OPTIONAL_HPP +#define LRU_INTERNAL_OPTIONAL_HPP + +#ifndef __has_include +#define USE_LRU_OPTIONAL +#elif __has_include() + +#include + +namespace LRU { +namespace Internal { +template +using Optional = std::optional; +} // namespace Internal +} // namespace LRU + +#else +#define USE_LRU_OPTIONAL +#endif + +#ifdef USE_LRU_OPTIONAL +#include +#include + +namespace LRU { +namespace Internal { + +// A roll-your-own replacement of `std::optional`. +// +// This class is only to be used if `std::optional` is unavailable. It +// implements an optional type simply on top of a `unique_ptr`. It is +// API-compatible with `std::optional`, as required for our purposes. +template +class Optional { + public: + /// Constructor. + Optional() = default; + + /// Copy constructor. + /// + /// \param other The other optional object to copy from. + Optional(const Optional& other) { + if (other) emplace(*other); + } + + /// Generalized copy constructor. + /// + /// \param other The other optional object to copy from. + template ::value>> + Optional(const Optional& other) { + if (other) emplace(*other); + } + + /// Move constructor. + /// + /// \param other The other optional object to move into this one. + Optional(Optional&& other) noexcept { + swap(other); + } + + /// Generalized move constructor. + /// + /// \param other The other optional object to move into this one. + template ::value>> + Optional(Optional&& other) noexcept { + if (other) { + _value = std::make_unique(std::move(*other)); + } + } + + /// Assignment operator. + /// + /// \param other The other object to assign from. + /// \returns The resulting optional instance. + Optional& operator=(Optional other) noexcept { + swap(other); + return *this; + } + + /// Swaps the contents of this optional with another one. + /// + /// \param other The other optional to swap with. + void swap(Optional& other) { + _value.swap(other._value); + } + + /// Swaps the contents of two optionals. + /// + /// \param first The first optional to swap. + /// \param second The second optional to swap. + friend void swap(Optional& first, Optional& second) /* NOLINT */ { + first.swap(second); + } + + /// \returns True if the `Optional` has a value, else false. + bool has_value() const noexcept { + return static_cast(_value); + } + + /// \copydoc has_value() + explicit operator bool() const noexcept { + return has_value(); + } + + /// \returns A pointer to the current value. Behavior is undefined if the + /// optional has no value. + T* operator->() { + return _value.get(); + } + + /// \returns A const pointer to the current value. Behavior is undefined if + /// the `Optional` has no value. + const T* operator->() const { + return _value.get(); + } + + /// \returns A const reference to the current value. Behavior is undefined if + /// the `Optional` has no value. + const T& operator*() const { + return *_value; + } + + /// \returns A reference to the current value. Behavior is undefined if + /// the `Optional` has no value. + T& operator*() { + return *_value; + } + + /// \returns A reference to the current value. + /// \throws std::runtime_error If the `Optional` currently has no value. + T& value() { + if (!has_value()) { + // Actually std::bad_optional_access + throw std::runtime_error("optional has no value"); + } + + return *_value; + } + + /// \returns A const reference to the current value. + /// \throws std::runtime_error If the `Optional` currently has no value. + const T& value() const { + if (!has_value()) { + // Actually std::bad_optional_access + throw std::runtime_error("optional has no value"); + } + + return *_value; + } + + /// \returns The current value, or the given argument if there is no value. + /// \param default_value The value to return if this `Optional` currently has + /// no value. + template + T value_or(U&& default_value) const { + return *this ? **this : static_cast(std::forward(default_value)); + } + + /// Resets the `Optional` to have no value. + void reset() noexcept { + _value.reset(); + } + + /// Constructs the `Optional`'s value with the given arguments. + /// + /// \param args Arguments to perfeclty forward to the value's constructor. + template + void emplace(Args&&... args) { + _value = std::make_unique(std::forward(args)...); + } + + private: + template + friend class Optional; + + /// The value, as we implement it. + std::unique_ptr _value; +}; +} // namespace Internal +} // namespace LRU + +#endif + +#endif // LRU_INTERNAL_OPTIONAL_HPP diff --git a/include/lru/internal/statistics-mutator.hpp b/include/lru/internal/statistics-mutator.hpp new file mode 100644 index 0000000..4dfd52a --- /dev/null +++ b/include/lru/internal/statistics-mutator.hpp @@ -0,0 +1,150 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_STATISTICS_MUTATOR_HPP +#define LRU_STATISTICS_MUTATOR_HPP + +#include +#include +#include +#include + +#include +#include + +namespace LRU { +namespace Internal { + +/// A mutable proxy interface to a statistics object. +/// +/// The `StatisticsMutator` allows modification of the members of a statistics +/// object via a narrow interface, available only to internal classes. The point +/// of this is that while we don't want the user to be able to modify the hit or +/// miss count on a statistics object (it is "getter-only" in that sense), it's +/// also not ideal, from an encapsulation standpoint, to make the cache classes +/// (which do need to access and modify the hit and miss counts) friends of the +/// statistics. This is especially true since the caches should only need to +/// register hits or misses and not have to increment the count of total +/// accesses. As such, we really require a "package-level" interface that is not +/// visible to the end user, while at the same time providing an interface to +/// internal classes. The `StatisticsMutator` is a proxy/adapter class that +/// serves exactly this purpose. It is friends with the `Statistics` and can +/// thus access its members. At the same time the interface it defines is narrow +/// and provides only the necessary interface for the cache classes to register +/// hits and misses. +template +class StatisticsMutator { + public: + using StatisticsPointer = std::shared_ptr>; + + /// Constructor. + StatisticsMutator() noexcept = default; + + /// Constructor. + /// + /// \param stats A shared pointer lvalue reference. + StatisticsMutator(const StatisticsPointer& stats) // NOLINT(runtime/explicit) + : _stats(stats) { + } + + /// Constructor. + /// + /// \param stats A shared pointer rvalue reference to move into the + /// mutator. + StatisticsMutator(StatisticsPointer&& stats) // NOLINT(runtime/explicit) + : _stats(std::move(stats)) { + } + + /// Registers a hit for the given key with the internal statistics. + /// + /// \param key The key to register a hit for. + void register_hit(const Key& key) { + assert(has_stats()); + + _stats->_total_accesses += 1; + _stats->_total_hits += 1; + + auto iterator = _stats->_key_map.find(key); + if (iterator != _stats->_key_map.end()) { + iterator->second.hits += 1; + } + } + + /// Registers a miss for the given key with the internal statistics. + /// + /// \param key The key to register a miss for. + void register_miss(const Key& key) { + assert(has_stats()); + + _stats->_total_accesses += 1; + + auto iterator = _stats->_key_map.find(key); + if (iterator != _stats->_key_map.end()) { + iterator->second.misses += 1; + } + } + + /// \returns A reference to the statistics object. + Statistics& get() noexcept { + assert(has_stats()); + return *_stats; + } + + /// \returns A const reference to the statistics object. + const Statistics& get() const noexcept { + assert(has_stats()); + return *_stats; + } + + /// \returns A `shared_ptr` to the statistics object. + StatisticsPointer& shared() noexcept { + return _stats; + } + + /// \returns A const `shared_ptr` to the statistics object. + const StatisticsPointer& shared() const noexcept { + return _stats; + } + + /// \returns True if the mutator has a statistics object, else false. + bool has_stats() const noexcept { + return _stats != nullptr; + } + + /// \copydoc has_stats() + explicit operator bool() const noexcept { + return has_stats(); + } + + /// Resets the internal statistics pointer. + void reset() { + _stats.reset(); + } + + private: + /// A shared pointer to a statistics object. + std::shared_ptr> _stats; +}; + +} // namespace Internal +} // namespace LRU + +#endif // LRU_STATISTICS_MUTATOR_HPP diff --git a/include/lru/internal/timed-information.hpp b/include/lru/internal/timed-information.hpp new file mode 100644 index 0000000..72865a6 --- /dev/null +++ b/include/lru/internal/timed-information.hpp @@ -0,0 +1,116 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_INTERNAL_TIMED_INFORMATION_HPP +#define LRU_INTERNAL_TIMED_INFORMATION_HPP + +#include +#include +#include + +#include +#include +#include + +namespace LRU { +namespace Internal { + +/// The information object for timed caches. +/// +/// TimedInformation differs from plain information only in that it stores the +/// creation time, to know when a key has expired. +/// +/// \tparam Key The key type of the information. +/// \tparam Value The value type of the information. +template +struct TimedInformation : public Information { + using super = Information; + using typename super::QueueIterator; + using Timestamp = Internal::Timestamp; + + /// Constructor. + /// + /// \param value_ The value for the information. + /// \param insertion_time_ The insertion timestamp of the key. + /// \param order_ The order iterator for the information. + TimedInformation(const Value& value_, + const Timestamp& insertion_time_, + QueueIterator order_ = QueueIterator()) + : super(value_, order_), insertion_time(insertion_time_) { + } + + /// Constructor. + /// + /// Uses the current time as the insertion timestamp. + /// + /// \param value_ The value for the information. + /// \param order_ The order iterator for the information. + explicit TimedInformation(const Value& value_, + QueueIterator order_ = QueueIterator()) + : TimedInformation(value_, Internal::Clock::now(), order_) { + } + + /// \copydoc Information::Information(QueueIterator,ValueArguments&&) + template + TimedInformation(QueueIterator order_, ValueArguments&&... value_argument) + : super(std::forward(value_argument)..., order_) + , insertion_time(Internal::Clock::now()) { + } + + /// \copydoc Information::Information(QueueIterator,const + /// std::tuple&) + template + explicit TimedInformation( + const std::tuple& value_arguments, + QueueIterator order_ = QueueIterator()) + : super(value_arguments, order_), insertion_time(Internal::Clock::now()) { + } + + /// Compares this timed information for equality with another one. + /// + /// Additionally to key and value equality, the timed information requires + /// that the insertion timestamps be equal. + /// + /// \param other The other timed information. + /// \returns True if this information equals the other one, else false. + bool operator==(const TimedInformation& other) const noexcept { + if (super::operator!=(other)) return false; + return this->insertion_time == other.insertion_time; + } + + /// Compares this timed information for inequality with another one. + /// + /// \param other The other timed information. + /// \returns True if this information does not equal the other one, else + /// false. + /// \see operator==() + bool operator!=(const TimedInformation& other) const noexcept { + return !(*this == other); + } + + /// The time at which the key of the information was insterted into a cache. + const Timestamp insertion_time; +}; + +} // namespace Internal +} // namespace LRU + +#endif // LRU_INTERNAL_TIMED_INFORMATION_HPP diff --git a/include/lru/internal/utility.hpp b/include/lru/internal/utility.hpp new file mode 100644 index 0000000..2be5a8d --- /dev/null +++ b/include/lru/internal/utility.hpp @@ -0,0 +1,178 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_UTILITY_HPP +#define LRU_UTILITY_HPP + +#include +#include +#include +#include + +namespace LRU { +namespace Internal { + +/// Generates an index sequence for a tuple. +/// +/// \tparam Ts The types of the tuple (to deduce the size). +template +constexpr auto tuple_indices(const std::tuple&) { + return std::make_index_sequence(); +} + +/// Applies (in the functional sense) a tuple to the constructor of a class. +/// +/// \tparam T The type to construct. +/// \tparam Indices The indices into the tuple (generated from an index +/// sequence). +/// \param args The tuple of arguments to construct the object with. +template +constexpr T construct_from_tuple(const std::tuple& arguments, + std::index_sequence) { + return T(std::forward(std::get(arguments))...); +} + +/// Applies (in the functional sense) a tuple to the constructor of a class. +/// +/// \tparam T The type to construct. +/// \param args The tuple of arguments to construct the object with. +template +constexpr T construct_from_tuple(const std::tuple& args) { + return construct_from_tuple(args, tuple_indices(args)); +} + +/// Applies (in the functional sense) a tuple to the constructor of a class. +/// +/// \tparam T The type to construct. +/// \param args The tuple of arguments to construct the object with. +template +constexpr T construct_from_tuple(std::tuple&& args) { + return construct_from_tuple(std::move(args), tuple_indices(args)); +} + +/// A type trait that disables a template overload if a type is not an iterator. +/// +/// \tparam T the type to check. +template +using enable_if_iterator = typename std::iterator_traits::value_type; + +/// A type trait that disables a template overload if a type is not a range. +/// +/// \tparam T the type to check. +template +using enable_if_range = std::pair().begin()), + decltype(std::declval().end())>; + +/// A type trait that disables a template overload if a type is not an iterator +/// over a pair. +/// +/// \tparam T the type to check. +template +using enable_if_iterator_over_pair = + std::pair::value_type::first_type, + typename std::iterator_traits::value_type::first_type>; + + +/// A type trait that disables a template overload if a type is not convertible +/// to a target type. +/// +/// \tparam Target The type one wants to check against. +/// \tparam T The type to check. +template +using enable_if_same = std::enable_if_t::value>; + +/// Base case for `static_all_of` (the neutral element of AND is true). +constexpr bool static_all_of() noexcept { + return true; +} + +/// Checks if all the given parameters evaluate to true. +/// +/// \param head The first expression to check. +/// \param tail The remaining expression to check. +template +constexpr bool static_all_of(Head&& head, Tail&&... tail) { + // Replace with (ts && ...) when the time is right + return std::forward(head) && static_all_of(std::forward(tail)...); +} + +/// Base case for `static_any_of` (the neutral element of OR is false). +constexpr bool static_any_of() noexcept { + return false; +} + +/// Checks if any the given parameters evaluate to true. +/// +/// \param head The first expression to check. +/// \param tail The remaining expression to check. +/// \returns True if any of the given parameters evaluate to true. +template +constexpr bool static_any_of(Head&& head, Tail&&... tail) { + // Replace with (ts || ...) when the time is right + return std::forward(head) || static_any_of(std::forward(tail)...); +} + +/// Checks if none the given parameters evaluate to true. +/// +/// \param ts The expressions to check. +/// \returns True if any of the given parameters evaluate to true. +template +constexpr bool static_none_of(Ts&&... ts) { + // Replace with (!ts && ...) when the time is right + return !static_any_of(std::forward(ts)...); +} + +/// Checks if all the given types are convertible to the first type. +/// +/// \tparam T the first type. +/// \tparam Ts The types to check against the first. +template +constexpr bool + all_of_type = static_all_of(std::is_convertible::value...); + +/// Checks if none of the given types are convertible to the first type. +/// +/// \tparam T the first type. +/// \tparam Ts The types to check against the first. +template +constexpr bool + none_of_type = static_none_of(std::is_convertible::value...); + +/// Base case for `for_each`. +template +void for_each(Function) noexcept { +} + +/// Calls a function for each of the given variadic arguments. +/// +/// \param function The function to call for each argument. +/// \param head The first value to call the function with. +/// \param tail The remaining values to call the function with. +template +void for_each(Function function, Head&& head, Tail&&... tail) { + function(std::forward(head)); + for_each(function, std::forward(tail)...); +} + +} // namespace Internal +} // namespace LRU + +#endif // LRU_UTILITY_HPP diff --git a/include/lru/iterator-tags.hpp b/include/lru/iterator-tags.hpp new file mode 100644 index 0000000..d4115ce --- /dev/null +++ b/include/lru/iterator-tags.hpp @@ -0,0 +1,40 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_ITERATOR_TAGS_HPP +#define LRU_ITERATOR_TAGS_HPP + +namespace LRU { +namespace Tag { +struct OrderedIterator {}; +struct UnorderedIterator {}; +} // namespace Tag + +namespace Lowercase { +namespace tag { +using ordered_iterator = ::LRU::Tag::OrderedIterator; +using unordered_iterator = ::LRU::Tag::UnorderedIterator; +} // namespace tag +} // namespace Lowercase + +} // namespace LRU + +#endif // LRU_ITERATOR_TAGS_HPP diff --git a/include/lru/key-statistics.hpp b/include/lru/key-statistics.hpp new file mode 100644 index 0000000..6989db0 --- /dev/null +++ b/include/lru/key-statistics.hpp @@ -0,0 +1,67 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + + +#ifndef LRU_KEY_STATISTICS_HPP +#define LRU_KEY_STATISTICS_HPP + +#include + +namespace LRU { + +/// Stores statistics for a single key. +/// +/// The statistics stored are the total number of hits and the total number of +/// misses. The total number of acccesses (the sum of hits and misses) may be +/// accessed as well. +struct KeyStatistics { + using size_t = std::size_t; + + /// Constructor. + /// + /// \param hits_ The initial number of hits for the key. + /// \param misses_ The initial number of misses for the key. + explicit KeyStatistics(size_t hits_ = 0, size_t misses_ = 0) + : hits(hits_), misses(misses_) { + } + + /// \returns The total number of accesses made for the key. + /// \details This is the sum of the hits and misses. + size_t accesses() const noexcept { + return hits + misses; + } + + /// Resets the statistics for a key (sets them to zero). + void reset() { + hits = 0; + misses = 0; + } + + /// The number of hits for the key. + size_t hits; + + /// The number of misses for the key. + size_t misses; +}; + +} // namespace LRU + +#endif // LRU_KEY_STATISTICS_HPP diff --git a/include/lru/lowercase.hpp b/include/lru/lowercase.hpp new file mode 100644 index 0000000..1f0c996 --- /dev/null +++ b/include/lru/lowercase.hpp @@ -0,0 +1,34 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_LOWERCASE_HPP +#define LRU_LOWERCASE_HPP + +#include + +namespace LRU { +using namespace Lowercase; // NOLINT(build/namespaces) +} // namespace LRU + +namespace lru = LRU; + + +#endif // LRU_LOWERCASE_HPP diff --git a/include/lru/lru.hpp b/include/lru/lru.hpp new file mode 100644 index 0000000..f3acae0 --- /dev/null +++ b/include/lru/lru.hpp @@ -0,0 +1,33 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_HPP +#define LRU_HPP + +#include +#include +#include +#include +#include +#include +#include + +#endif // LRU_HPP diff --git a/include/lru/statistics.hpp b/include/lru/statistics.hpp new file mode 100644 index 0000000..0c97016 --- /dev/null +++ b/include/lru/statistics.hpp @@ -0,0 +1,256 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + + +#ifndef LRU_STATISTICS_HPP +#define LRU_STATISTICS_HPP + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace LRU { +namespace Internal { +template +class StatisticsMutator; +} + +/// Stores statistics about LRU cache utilization and efficiency. +/// +/// The statistics object stores the number of misses and hits were recorded for +/// a cache in total. Furthemore, it is possibly to register a number of keys +/// for *monitoring*. For each of these keys, an additional hit and miss count +/// is maintained, that can keep insight into the utiliization of a particular +/// cache. Note that accesses only mean lookups -- insertions or erasures will +/// never signify an "access". +/// +/// \tparam Key The type of the keys being monitored. +template +class Statistics { + public: + using size_t = std::size_t; + using InitializerList = std::initializer_list; + + /// Constructor. + Statistics() noexcept : _total_accesses(0), _total_hits(0) { + } + + /// Constructor. + /// + /// \param keys Any number of keys to monitor. + template >> + explicit Statistics(Keys&&... keys) : Statistics() { + // clang-format off + Internal::for_each([this](auto&& key) { + this->monitor(std::forward(key)); + }, std::forward(keys)...); + // clang-format on + } + + /// Constructor. + /// + /// \param range A range of keys to monitor. + template > + explicit Statistics(const Range& range) + : Statistics(std::begin(range), std::end(range)) { + } + + /// Constructor. + /// + /// \param begin The start iterator of a range of keys to monitor. + /// \param end The end iterator of a range of keys to monitor. + template > + Statistics(Iterator begin, Iterator end) : Statistics() { + for (; begin != end; ++begin) { + monitor(*begin); + } + } + + /// Constructor. + /// + /// \param list A list of keys to monitor. + Statistics(InitializerList list) // NOLINT(runtime/explicit) + : Statistics(list.begin(), list.end()) { + } + + /// \returns The total number of accesses (hits + misses) made to the cache. + size_t total_accesses() const noexcept { + return _total_accesses; + } + + /// \returns The total number of hits made to the cache. + size_t total_hits() const noexcept { + return _total_hits; + } + + /// \returns The total number of misses made to the cache. + size_t total_misses() const noexcept { + return total_accesses() - total_hits(); + } + + /// \returns The ratio of hits ($\in [0, 1]$) relative to all accesses. + double hit_rate() const noexcept { + return static_cast(total_hits()) / total_accesses(); + } + + /// \returns The ratio of misses ($\in [0, 1]$) relative to all accesses. + double miss_rate() const noexcept { + return 1 - hit_rate(); + } + + /// \returns The number of hits for the given key. + /// \param key The key to retrieve the hits for. + /// \throws LRU::UnmonitoredKey if the key was not registered for monitoring. + size_t hits_for(const Key& key) const { + return stats_for(key).hits; + } + + /// \returns The number of misses for the given key. + /// \param key The key to retrieve the misses for. + /// \throws LRU::UnmonitoredKey if the key was not registered for monitoring. + size_t misses_for(const Key& key) const { + return stats_for(key).misses; + } + + /// \returns The number of accesses (hits + misses) for the given key. + /// \param key The key to retrieve the accesses for. + /// \throws LRU::UnmonitoredKey if the key was not registered for monitoring. + size_t accesses_for(const Key& key) const { + return stats_for(key).accesses(); + } + + /// \returns A `KeyStatistics` object for the given key. + /// \param key The key to retrieve the stats for. + /// \throws LRU::UnmonitoredKey if the key was not registered for monitoring. + const KeyStatistics& stats_for(const Key& key) const { + auto iterator = _key_map.find(key); + if (iterator == _key_map.end()) { + throw LRU::Error::UnmonitoredKey(); + } + + return iterator->second; + } + + /// \copydoc stats_for() + const KeyStatistics& operator[](const Key& key) const { + return stats_for(key); + } + + /// Registers the key for monitoring. + /// + /// If the key was already registered, this is a no-op (most importantly, the + /// old statistics are __not__ wiped). + /// + /// \param key The key to register. + void monitor(const Key& key) { + // emplace does nothing if the key is already present + _key_map.emplace(key, KeyStatistics()); + } + + /// Unregisters the given key from monitoring. + /// + /// \param key The key to unregister. + /// \throws LRU::Error::UnmonitoredKey if the key was never registered for + /// monitoring. + void unmonitor(const Key& key) { + auto iterator = _key_map.find(key); + if (iterator == _key_map.end()) { + throw LRU::Error::UnmonitoredKey(); + } else { + _key_map.erase(iterator); + } + } + + /// Unregisters all keys from monitoring. + void unmonitor_all() { + _key_map.clear(); + } + + /// Clears all statistics for the given key, but keeps on monitoring it. + /// + /// \param key The key to reset. + void reset_key(const Key& key) { + auto iterator = _key_map.find(key); + if (iterator == _key_map.end()) { + throw LRU::Error::UnmonitoredKey(); + } else { + iterator->second.reset(); + } + } + + /// Clears the statistics of all keys, but keeps on monitoring it them. + void reset_all() { + for (auto& pair : _key_map) { + _key_map.second.reset(); + } + } + + /// \returns True if the given key is currently registered for monitoring, + /// else false. + /// \param key The key to check for. + bool is_monitoring(const Key& key) const noexcept { + return _key_map.count(key); + } + + /// \returns The number of keys currnetly being monitored. + size_t number_of_monitored_keys() const noexcept { + return _key_map.size(); + } + + /// \returns True if currently any keys at all are being monitored, else + /// false. + bool is_monitoring_keys() const noexcept { + return !_key_map.empty(); + } + + private: + template + friend class Internal::StatisticsMutator; + + using HitMap = std::unordered_map; + + /// The total number of accesses made for any key. + size_t _total_accesses; + + /// The total number of htis made for any key. + size_t _total_hits; + + /// The map to keep track of statistics for monitored keys. + HitMap _key_map; +}; + +namespace Lowercase { +template +using statistics = Statistics; +} // namespace Lowercase + +} // namespace LRU + +#endif // LRU_STATISTICS_HPP diff --git a/include/lru/timed-cache.hpp b/include/lru/timed-cache.hpp new file mode 100644 index 0000000..0038932 --- /dev/null +++ b/include/lru/timed-cache.hpp @@ -0,0 +1,391 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_TIMED_CACHE_HPP +#define LRU_TIMED_CACHE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace LRU { +namespace Internal { +template +using TimedCacheBase = BaseCache; +} // namespace Internal + + +/// A timed LRU cache. +/// +/// A timed LRU cache behaves like a regular LRU cache, but adds the concept of +/// "expiration". The cache now not only remembers the order of insertion, but +/// also the point in time at which each element was inserted into the cache. +/// The cache then has an additional "time to live" property, which designates +/// the time after which a key in the cache is said to be "expired". Once a key +/// has expired, the cache will behave as if the key were not present in the +/// cache at all and, for example, return false on calls to `contains()` or +/// throw on calls to `lookup()`. +/// +/// \see LRU::Cache +template , + typename HashFunction = std::hash, + typename KeyEqual = std::equal_to> +class TimedCache + : public Internal::TimedCacheBase { + private: + using super = Internal::TimedCacheBase; + using PRIVATE_BASE_CACHE_MEMBERS; + + public: + using Tag = LRU::Tag::TimedCache; + using PUBLIC_BASE_CACHE_MEMBERS; + using super::ordered_end; + using super::unordered_end; + using typename super::size_t; + + /// \param time_to_live The time to live for keys in the cache. + /// \copydoc BaseCache::BaseCache(size_t,const HashFunction&,const KeyEqual&) + template + explicit TimedCache(const AnyDurationType& time_to_live, + size_t capacity = Internal::DEFAULT_CAPACITY, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) + : super(capacity, hash, equal) + , _time_to_live(std::chrono::duration_cast(time_to_live)) { + } + + /// \param time_to_live The time to live for keys in the cache. + /// \copydoc BaseCache::BaseCache(size_t,Iterator,Iterator,const + /// HashFunction&,const + /// KeyEqual&) + template + TimedCache(const AnyDurationType& time_to_live, + size_t capacity, + Iterator begin, + Iterator end, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) + : super(capacity, begin, end, hash, equal) + , _time_to_live(std::chrono::duration_cast(time_to_live)) { + } + + /// \param time_to_live The time to live for keys in the cache. + /// \copydoc BaseCache::BaseCache(Iterator,Iterator,const HashFunction&,const + /// KeyEqual&) + template + TimedCache(const AnyDurationType& time_to_live, + Iterator begin, + Iterator end, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) + : super(begin, end, hash, equal) + , _time_to_live(std::chrono::duration_cast(time_to_live)) { + } + + /// \param time_to_live The time to live for keys in the cache. + /// \copydoc BaseCache::BaseCache(Range,size_t,const HashFunction&,const + /// KeyEqual&) + template > + TimedCache(const AnyDurationType& time_to_live, + size_t capacity, + Range&& range, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) + : super(capacity, std::forward(range), hash, equal) + , _time_to_live(std::chrono::duration_cast(time_to_live)) { + } + + /// \param time_to_live The time to live for keys in the cache. + /// \copydoc BaseCache::BaseCache(Range,const HashFunction&,const + /// KeyEqual&) + template > + explicit TimedCache(const AnyDurationType& time_to_live, + Range&& range, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) + : super(std::forward(range), hash, equal) + , _time_to_live(std::chrono::duration_cast(time_to_live)) { + } + + /// \param time_to_live The time to live for keys in the cache. + /// \copydoc BaseCache::BaseCache(InitializerList,const HashFunction&,const + /// KeyEqual&) + template + TimedCache(const AnyDurationType& time_to_live, + InitializerList list, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) // NOLINT(runtime/explicit) + : super(list, hash, equal), + _time_to_live(std::chrono::duration_cast(time_to_live)) { + } + + /// \param time_to_live The time to live for keys in the cache. + /// \copydoc BaseCache::BaseCache(InitializerList,size_t,const + /// HashFunction&,const + /// KeyEqual&) + template + TimedCache(const AnyDurationType& time_to_live, + size_t capacity, + InitializerList list, + const HashFunction& hash = HashFunction(), + const KeyEqual& equal = KeyEqual()) // NOLINT(runtime/explicit) + : super(capacity, list, hash, equal), + _time_to_live(std::chrono::duration_cast(time_to_live)) { + } + + /// \copydoc BaseCache::swap + void swap(TimedCache& other) noexcept { + using std::swap; + + super::swap(other); + swap(_time_to_live, other._time_to_live); + } + + /// Swaps the contents of one cache with another cache. + /// + /// \param first The first cache to swap. + /// \param second The second cache to swap. + friend void swap(TimedCache& first, TimedCache& second) noexcept { + first.swap(second); + } + + /// \copydoc BaseCache::find(const Key&) + UnorderedIterator find(const Key& key) override { + auto iterator = _map.find(key); + if (iterator != _map.end()) { + if (!_has_expired(iterator->second)) { + _register_hit(key, iterator->second.value); + _move_to_front(iterator->second.order); + _last_accessed = iterator; + return {*this, iterator}; + } + } + + _register_miss(key); + + return end(); + } + + /// \copydoc BaseCache::find(const Key&) const + UnorderedConstIterator find(const Key& key) const override { + auto iterator = _map.find(key); + if (iterator != _map.end()) { + if (!_has_expired(iterator->second)) { + _register_hit(key, iterator->second.value); + _move_to_front(iterator->second.order); + _last_accessed = iterator; + return {*this, iterator}; + } + } + + _register_miss(key); + + return cend(); + } + + // no front() because we may have to erase the + // entire cache if everything happens to be expired + + /// \returns True if all keys in the cache have expired, else false. + bool all_expired() const { + // By the laws of predicate logic, any statement about any empty set is true + if (is_empty()) return true; + + /// If the most-recently inserted key has expired, all others must have too. + auto latest = _map.find(_order.back()); + return _has_expired(latest->second); + } + + /// Erases all expired elements from the cache. + /// + /// \complexity O(N) + /// \returns The number of elements erased. + size_t clear_expired() { + // We have to do a linear search here because linked lists do not + // support O(log N) binary searches given their node-based nature. + // Either way, in the worst case the entire cache has expired and + // we would have to do O(N) erasures. + + if (is_empty()) return 0; + + auto iterator = _order.begin(); + size_t number_of_erasures = 0; + + while (iterator != _order.end()) { + auto map_iterator = _map.find(*iterator); + + // If the current element hasn't expired, also all elements inserted + // after will not have, so we can stop. + if (!_has_expired(map_iterator->second)) break; + + _erase(map_iterator); + + iterator = _order.begin(); + number_of_erasures += 1; + } + + return number_of_erasures; + } + + /// \returns True if the given key is contained in the cache and has expired. + /// \param key The key to test expiration for. + bool has_expired(const Key& key) const noexcept { + auto iterator = _map.find(key); + return iterator != _map.end() && _has_expired(iterator->second); + } + + /// \returns True if the key pointed to by the iterator has expired. + /// \param ordered_iterator The ordered iterator to check. + /// \details If this is the end iterator, this method returns false. + bool has_expired(OrderedConstIterator ordered_iterator) const noexcept { + if (ordered_iterator == ordered_end()) return false; + auto iterator = _map.find(ordered_iterator->key()); + assert(iterator != _map.end()); + + return _has_expired(iterator->second); + } + + /// \returns True if the key pointed to by the iterator has expired. + /// \param unordered_iterator The unordered iterator to check. + /// \details If this is the end iterator, this method returns false. + bool has_expired(UnorderedConstIterator unordered_iterator) const noexcept { + if (unordered_iterator == unordered_end()) return false; + assert(unordered_iterator._iterator != _map.end()); + + return _has_expired(unordered_iterator._iterator->second); + } + + /// \copydoc BaseCache::is_valid(UnorderedConstIterator) + bool is_valid(UnorderedConstIterator unordered_iterator) const + noexcept override { + if (!super::is_valid(unordered_iterator)) return false; + if (has_expired(unordered_iterator)) return false; + return true; + } + + /// \copydoc BaseCache::is_valid(OrderedConstIterator) + bool is_valid(OrderedConstIterator ordered_iterator) const noexcept override { + if (!super::is_valid(ordered_iterator)) return false; + if (has_expired(ordered_iterator)) return false; + return true; + } + + /// \copydoc BaseCache::is_valid(UnorderedConstIterator) + /// \throws LRU::Error::KeyExpired if the key pointed to by the iterator has + /// expired. + void + throw_if_invalid(UnorderedConstIterator unordered_iterator) const override { + super::throw_if_invalid(unordered_iterator); + if (has_expired(unordered_iterator)) { + throw LRU::Error::KeyExpired(); + } + } + + /// \copydoc BaseCache::is_valid(OrderedConstIterator) + /// \throws LRU::Error::KeyExpired if the key pointed to by the iterator has + /// expired. + void throw_if_invalid(OrderedConstIterator ordered_iterator) const override { + super::throw_if_invalid(ordered_iterator); + if (has_expired(ordered_iterator)) { + throw LRU::Error::KeyExpired(); + } + } + + private: + using Clock = Internal::Clock; + + /// \returns True if the last accessed object is valid. + /// \details Next to performing the base cache's action, this method also + /// checks for expiration of the last accessed key. + bool _last_accessed_is_ok(const Key& key) const noexcept override { + if (!super::_last_accessed_is_ok(key)) return false; + return !_has_expired(_last_accessed.information()); + } + + /// \copydoc _value_for_last_accessed() const + Value& _value_for_last_accessed() override { + auto& information = _last_accessed.information(); + if (_has_expired(information)) { + throw LRU::Error::KeyExpired(); + } else { + return information.value; + } + } + + /// Attempts to access the last accessed key's value. + /// \throws LRU::Error::KeyExpired if the key has expired. + /// \returns The value of the last accessed key. + const Value& _value_for_last_accessed() const override { + const auto& information = _last_accessed.information(); + if (_has_expired(information)) { + throw LRU::Error::KeyExpired(); + } else { + return information.value; + } + } + + /// Checks if a key has expired, given its information. + /// + /// \param information The information to check expiration with. + /// \returns True if the key has expired, else false. + bool _has_expired(const Information& information) const noexcept { + auto elapsed = Clock::now() - information.insertion_time; + return std::chrono::duration_cast(elapsed) > _time_to_live; + } + + /// The duration after which a key is said to be expired. + Duration _time_to_live; +}; + +namespace Lowercase { +template +using timed_cache = TimedCache; +} // namespace Lowercase + +} // namespace LRU + +#endif // LRU_TIMED_CACHE_HPP diff --git a/include/lru/wrap.hpp b/include/lru/wrap.hpp new file mode 100644 index 0000000..e384968 --- /dev/null +++ b/include/lru/wrap.hpp @@ -0,0 +1,99 @@ +/// The MIT License (MIT) +/// Copyright (c) 2016 Peter Goldsborough +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to +/// deal in the Software without restriction, including without limitation the +/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +/// sell copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +/// IN THE SOFTWARE. + +#ifndef LRU_WRAP_HPP +#define LRU_WRAP_HPP + +#include +#include +#include +#include + +#include +#include +#include + +namespace LRU { + +/// Wraps a function with a "shallow" LRU cache. +/// +/// Given a function, this function will return a new function, where +/// "top-level" calls are cached. With "top-level" or "shallow", we mean +/// that recursive calls to the same function are not cached, since those +/// will call the original function symbol, not the wrapped one. +/// +/// \tparam CacheType The cache template class to use. +/// \param original_function The function to wrap. +/// \param args Any arguments to forward to the cache. +/// \returns A new function with a shallow LRU cache. +template class CacheType = Cache, + typename... Args> +auto wrap(Function original_function, Args&&... args) { + return [ + original_function, + cache_args = std::forward_as_tuple(std::forward(args)...) + ](auto&&... arguments) mutable { + using Arguments = std::tuple...>; + using ReturnType = decltype( + original_function(std::forward(arguments)...)); + + static_assert(!std::is_void::value, + "Return type of wrapped function must not be void"); + + static auto cache = + Internal::construct_from_tuple>( + cache_args); + + auto key = std::make_tuple(arguments...); + auto iterator = cache.find(key); + + if (iterator != cache.end()) { + return iterator->second; + } + + auto value = + original_function(std::forward(arguments)...); + cache.emplace(key, value); + + return value; + }; +} + +/// Wraps a function with a "shallow" LRU timed cache. +/// +/// Given a function, this function will return a new function, where +/// "top-level" calls are cached. With "top-level" or "shallow", we mean +/// that recursive calls to the same function are not cached, since those +/// will call the original function symbol, not the wrapped one. +/// +/// \param original_function The function to wrap. +/// \param args Any arguments to forward to the cache. +/// \returns A new function with a shallow LRU cache. +template +auto timed_wrap(Function original_function, Duration duration, Args&&... args) { + return wrap( + original_function, duration, std::forward(args)...); +} + +} // namespace LRU + +#endif // LRU_WRAP_HPP diff --git a/src/validateMSF.cc b/src/validateMSF.cc index 9cb5c7d..ab14018 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -13,6 +13,7 @@ #include "kmer.h" #include "lrucache.hpp" #include "hashutil.h" +#include "lru/lru.hpp" struct QueryStats { uint32_t cnt = 0, cacheCntr = 0, noCacheCntr{0}; @@ -40,7 +41,7 @@ namespace mantis{ } } -using LRUCacheMap = cache::lru_cache , mantis::util::int_hasher>; +using LRUCacheMap = LRU::Cache>;// cache::lru_cache , mantis::util::int_hasher>; class RankScores { public: @@ -114,8 +115,8 @@ class MSFQuery { bool foundCache = false; uint32_t iparent = parentbv[i]; while (iparent != i) { - if (lru_cache and lru_cache->exists(i)) { - const auto &vs = lru_cache->get(i); + if (lru_cache and lru_cache->contains(i)) { + const auto &vs = (*lru_cache)[i];//->get(i); for (auto v : vs) { xorflips[v] = 1; } @@ -239,12 +240,12 @@ mantis::QueryResult findSamples(const mantis::QuerySet &kmers, auto count = it->second; std::vector setbits; - if (lru_cache.exists(eqclass_id)) { - setbits = lru_cache.get(eqclass_id); + if (lru_cache.contains(eqclass_id)) { + setbits = lru_cache[eqclass_id];//.get(eqclass_id); queryStats.cacheCntr++; } else { setbits = msfQuery.buildColor(eqclass_id, queryStats, &lru_cache, rs, false); - lru_cache.put(eqclass_id, setbits); + lru_cache.emplace(eqclass_id, setbits); queryStats.noCacheCntr++; } for (auto sb : setbits) { From a5cff36985dd74373a731f3840c2ca6ef90b5cc4 Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Wed, 25 Jul 2018 23:25:25 -0400 Subject: [PATCH 052/122] remove some instrumentation --- src/validateMSF.cc | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index ab14018..0d23e8b 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -111,7 +111,7 @@ class MSFQuery { //froms.reserve(12000); //parents.reserve(12000); queryStats.totEqcls++; - auto sstart = std::chrono::system_clock::now(); + //auto sstart = std::chrono::system_clock::now(); bool foundCache = false; uint32_t iparent = parentbv[i]; while (iparent != i) { @@ -157,8 +157,8 @@ class MSFQuery { } */ - queryStats.selectTime += std::chrono::system_clock::now() - sstart; - auto fstart = std::chrono::system_clock::now(); + //queryStats.selectTime += std::chrono::system_clock::now() - sstart; + //auto fstart = std::chrono::system_clock::now(); for (auto f : froms) { bool found = false; uint64_t wrd{0}; @@ -182,7 +182,7 @@ class MSFQuery { start += 64; } while (!found/*bbv[j - 1] != 1*/); } - queryStats.flipTime += std::chrono::system_clock::now() - fstart; + //queryStats.flipTime += std::chrono::system_clock::now() - fstart; /*while (parentbv[i] != i) { if (i > 0) from = sbbv(i) + 1; @@ -441,10 +441,10 @@ int main(int argc, char *argv[]) { }*/ } std::cerr << "cache was used " << queryStats.cacheCntr << " times " << queryStats.noCacheCntr << "\n"; - std::cerr << "select time was " << queryStats.selectTime.count() << "s, flip time was " - << queryStats.flipTime.count() << '\n'; - std::cerr << "total selects = " << queryStats.totSel << ", time per select = " - << queryStats.selectTime.count() / queryStats.totSel << '\n'; + //std::cerr << "select time was " << queryStats.selectTime.count() << "s, flip time was " + // << queryStats.flipTime.count() << '\n'; + //std::cerr << "total selects = " << queryStats.totSel << ", time per select = " + // << queryStats.selectTime.count() / queryStats.totSel << '\n'; std::cerr << "total # of queries = " << queryStats.totEqcls << ", total # of queries rooted at a non-zero node = " << queryStats.rootedNonZero << "\n"; From c43d619beae97f1e107a5613cbf90c893c77153a Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Thu, 26 Jul 2018 10:12:47 -0400 Subject: [PATCH 053/122] some cleanup --- src/validateMSF.cc | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/src/validateMSF.cc b/src/validateMSF.cc index 0d23e8b..25319b4 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -107,21 +107,16 @@ class MSFQuery { uint64_t height{0}; auto& froms = queryStats.buffer; froms.clear(); - //std::vector froms; - //froms.reserve(12000); - //parents.reserve(12000); queryStats.totEqcls++; - //auto sstart = std::chrono::system_clock::now(); bool foundCache = false; uint32_t iparent = parentbv[i]; while (iparent != i) { if (lru_cache and lru_cache->contains(i)) { - const auto &vs = (*lru_cache)[i];//->get(i); + const auto &vs = (*lru_cache)[i]; for (auto v : vs) { xorflips[v] = 1; } queryStats.cacheCntr++; - //i = iparent; foundCache = true; break; } @@ -130,8 +125,6 @@ class MSFQuery { else from = 0; froms.push_back(from); - //parents.push_back(i); - //queryStats.numOcc[i]++; i = iparent; iparent = parentbv[i]; ++queryStats.totSel; @@ -143,7 +136,6 @@ class MSFQuery { else from = 0; froms.push_back(from); - //parents.push_back(i); ++queryStats.totSel; queryStats.rootedNonZero++; ++height; @@ -157,8 +149,6 @@ class MSFQuery { } */ - //queryStats.selectTime += std::chrono::system_clock::now() - sstart; - //auto fstart = std::chrono::system_clock::now(); for (auto f : froms) { bool found = false; uint64_t wrd{0}; @@ -167,34 +157,16 @@ class MSFQuery { auto start = f; do { wrd = bbv.get_int(start, 64); - //while (wrd == 0) { offset+= 64; wrd = bbv.get_int(start+offset, 64); } - //offset += __builtin_ctzll(wrd); - //j = 0; for (uint64_t j = 0; j < 64; j++) { - //for (uint64_t j = 0; j <= offset; j++) { flips[deltabv[start + j]] ^= 0x01; - //j++; if ((wrd >> j) & 0x01) { found = true; break; } } start += 64; - } while (!found/*bbv[j - 1] != 1*/); + } while (!found); } - //queryStats.flipTime += std::chrono::system_clock::now() - fstart; - /*while (parentbv[i] != i) { - if (i > 0) - from = sbbv(i) + 1; - else - from = 0; - auto j = from; - do { - flips[deltabv[j]] ^= 0x01; - j++; - } while (bbv[j-1] != 1); - i = parentbv[i]; - }*/ if (!all) { // return the indices of set bits std::vector eq; @@ -210,7 +182,7 @@ class MSFQuery { std::vector eq(numWrds); uint64_t one = 1; for (i = 0; i < numSamples; i++) { - if (flips[i]) { + if (flips[i] ^ xorflips[i]) { uint64_t idx = i / 64; eq[idx] = eq[idx] | (one << (i % 64)); } @@ -319,7 +291,7 @@ struct Opts { }; int main(int argc, char *argv[]) { - + ios_base::sync_with_stdio(false); using namespace clipp; enum class mode { validate, steps, decodeAllEqs, query, help From f921e2699f7fa2483856bc366492b8fd50232dd9 Mon Sep 17 00:00:00 2001 From: Rob Patro Date: Thu, 26 Jul 2018 18:14:33 -0400 Subject: [PATCH 054/122] enable LTO --- CMakeLists.txt | 2 +- include/nonstd/optional.hpp | 1214 +++++++++++++++++++++++++++++++++++ src/CMakeLists.txt | 8 +- src/validateMSF.cc | 58 +- 4 files changed, 1263 insertions(+), 19 deletions(-) create mode 100644 include/nonstd/optional.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c4a7c7..98c70ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ # https://rix0r.nl/blog/2015/08/13/cmake-guide/ # -cmake_minimum_required(VERSION 3.5 FATAL_ERROR) +cmake_minimum_required(VERSION 3.9 FATAL_ERROR) project(mantis VERSION 0.2 LANGUAGES C CXX) if (NOT CMAKE_BUILD_TYPE) set (CMAKE_BUILD_TYPE "Release") diff --git a/include/nonstd/optional.hpp b/include/nonstd/optional.hpp new file mode 100644 index 0000000..b460791 --- /dev/null +++ b/include/nonstd/optional.hpp @@ -0,0 +1,1214 @@ +// +// Copyright (c) 2014-2018 Martin Moene +// +// https://github.com/martinmoene/optional-lite +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#pragma once + +#ifndef NONSTD_OPTIONAL_LITE_HPP +#define NONSTD_OPTIONAL_LITE_HPP + +#define optional_lite_VERSION "3.0.0" + +// Compiler detection (C++20 is speculative): +// Note: MSVC supports C++14 since it supports C++17. + +#ifdef _MSVC_LANG +# define optional_MSVC_LANG _MSVC_LANG +#else +# define optional_MSVC_LANG 0 +#endif + +#define optional_CPP11 (__cplusplus == 201103L ) +#define optional_CPP11_OR_GREATER (__cplusplus >= 201103L || optional_MSVC_LANG >= 201103L ) +#define optional_CPP14_OR_GREATER (__cplusplus >= 201402L || optional_MSVC_LANG >= 201703L ) +#define optional_CPP17_OR_GREATER (__cplusplus >= 201703L || optional_MSVC_LANG >= 201703L ) +#define optional_CPP20_OR_GREATER (__cplusplus >= 202000L || optional_MSVC_LANG >= 202000L ) + +// use C++17 std::optional if available: + +#if defined( __has_include ) +# define optional_HAS_INCLUDE( arg ) __has_include( arg ) +#else +# define optional_HAS_INCLUDE( arg ) 0 +#endif + +#define optional_HAVE_STD_OPTIONAL ( optional_CPP17_OR_GREATER && optional_HAS_INCLUDE( ) ) + +#if optional_HAVE_STD_OPTIONAL + +#include + +namespace nonstd { + + using std::optional; + using std::bad_optional_access; + using std::hash; + + using std::nullopt; + using std::nullopt_t; + using std::in_place; + using std::in_place_type; + using std::in_place_index; + using std::in_place_t; + using std::in_place_type_t; + using std::in_place_index_t; + + using std::operator==; + using std::operator!=; + using std::operator<; + using std::operator<=; + using std::operator>; + using std::operator>=; + using std::make_optional; + using std::swap; +} + +#else // C++17 std::optional + +#include +#include +#include + +// optional-lite alignment configuration: + +#ifndef optional_CONFIG_MAX_ALIGN_HACK +# define optional_CONFIG_MAX_ALIGN_HACK 0 +#endif + +#ifndef optional_CONFIG_ALIGN_AS +// no default, used in #if defined() +#endif + +#ifndef optional_CONFIG_ALIGN_AS_FALLBACK +# define optional_CONFIG_ALIGN_AS_FALLBACK double +#endif + +// Compiler warning suppression: + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wundef" +#elif defined __GNUC__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wundef" +#endif + +// half-open range [lo..hi): +#define optional_BETWEEN( v, lo, hi ) ( lo <= v && v < hi ) + +#if defined(_MSC_VER) && !defined(__clang__) +# define optional_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * ( 5 + (_MSC_VER < 1900)) ) +#else +# define optional_COMPILER_MSVC_VERSION 0 +#endif + +#define optional_COMPILER_VERSION( major, minor, patch ) ( 10 * (10 * major + minor ) + patch ) + +#if defined __GNUC__ +# define optional_COMPILER_GNUC_VERSION optional_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#else +# define optional_COMPILER_GNUC_VERSION 0 +#endif + +#if defined __clang__ +# define optional_COMPILER_CLANG_VERSION optional_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__) +#else +# define optional_COMPILER_CLANG_VERSION 0 +#endif + +#if optional_BETWEEN(optional_COMPILER_MSVC_VERSION, 70, 140 ) +# pragma warning( push ) +# pragma warning( disable: 4345 ) // initialization behavior changed +#endif + +#if optional_BETWEEN(optional_COMPILER_MSVC_VERSION, 70, 150 ) +# pragma warning( push ) +# pragma warning( disable: 4814 ) // in C++14 'constexpr' will not imply 'const' +#endif + +// Presence of language and library features: + +#define optional_HAVE(FEATURE) ( optional_HAVE_##FEATURE ) + +// Presence of C++11 language features: + +#if optional_CPP11_OR_GREATER || optional_COMPILER_MSVC_VERSION >= 100 +# define optional_HAVE_AUTO 1 +# define optional_HAVE_NULLPTR 1 +# define optional_HAVE_STATIC_ASSERT 1 +#endif + +#if optional_CPP11_OR_GREATER || optional_COMPILER_MSVC_VERSION >= 120 +# define optional_HAVE_DEFAULT_FUNCTION_TEMPLATE_ARG 1 +# define optional_HAVE_INITIALIZER_LIST 1 +#endif + +#if optional_CPP11_OR_GREATER || optional_COMPILER_MSVC_VERSION >= 140 +# define optional_HAVE_ALIAS_TEMPLATE 1 +# define optional_HAVE_CONSTEXPR_11 1 +# define optional_HAVE_ENUM_CLASS 1 +# define optional_HAVE_EXPLICIT_CONVERSION 1 +# define optional_HAVE_IS_DEFAULT 1 +# define optional_HAVE_IS_DELETE 1 +# define optional_HAVE_NOEXCEPT 1 +# define optional_HAVE_REF_QUALIFIER 1 +#endif + +// Presence of C++14 language features: + +#if optional_CPP14_OR_GREATER +# define optional_HAVE_CONSTEXPR_14 1 +#endif + +// Presence of C++17 language features: + +#if optional_CPP17_OR_GREATER +# define optional_HAVE_ENUM_CLASS_CONSTRUCTION_FROM_UNDERLYING_TYPE 1 +#endif + +// Presence of C++ library features: + +#if optional_COMPILER_GNUC_VERSION +# define optional_HAVE_TR1_TYPE_TRAITS 1 +# define optional_HAVE_TR1_ADD_POINTER 1 +#endif + +#if optional_CPP11_OR_GREATER || optional_COMPILER_MSVC_VERSION >= 90 +# define optional_HAVE_TYPE_TRAITS 1 +# define optional_HAVE_STD_ADD_POINTER 1 +#endif + +#if optional_CPP11_OR_GREATER || optional_COMPILER_MSVC_VERSION >= 110 +# define optional_HAVE_ARRAY 1 +#endif + +#if optional_CPP11_OR_GREATER || optional_COMPILER_MSVC_VERSION >= 120 +# define optional_HAVE_CONDITIONAL 1 +#endif + +#if optional_CPP11_OR_GREATER || optional_COMPILER_MSVC_VERSION >= 140 || (optional_COMPILER_MSVC_VERSION >= 90 && _HAS_CPP0X) +# define optional_HAVE_CONTAINER_DATA_METHOD 1 +#endif + +#if optional_CPP11_OR_GREATER || optional_COMPILER_MSVC_VERSION >= 120 +# define optional_HAVE_REMOVE_CV 1 +#endif + +#if optional_CPP11_OR_GREATER || optional_COMPILER_MSVC_VERSION >= 140 +# define optional_HAVE_SIZED_TYPES 1 +#endif + +// For the rest, consider VC14 as C++11 for optional-lite: + +#if optional_COMPILER_MSVC_VERSION >= 140 +# undef optional_CPP11_OR_GREATER +# define optional_CPP11_OR_GREATER 1 +#endif + +// C++ feature usage: + +#if optional_HAVE( CONSTEXPR_11 ) +# define optional_constexpr constexpr +#else +# define optional_constexpr /*constexpr*/ +#endif + +#if optional_HAVE( CONSTEXPR_14 ) +# define optional_constexpr14 constexpr +#else +# define optional_constexpr14 /*constexpr*/ +#endif + +#if optional_HAVE( NOEXCEPT ) +# define optional_noexcept noexcept +#else +# define optional_noexcept /*noexcept*/ +#endif + +#if optional_HAVE( NULLPTR ) +# define optional_nullptr nullptr +#else +# define optional_nullptr NULL +#endif + +#if optional_HAVE( REF_QUALIFIER ) +# define optional_ref_qual & +# define optional_refref_qual && +#else +# define optional_ref_qual /*&*/ +# define optional_refref_qual /*&&*/ +#endif + +// additional includes: + +#if optional_CPP11_OR_GREATER +# include +#endif + +#if optional_HAVE( INITIALIZER_LIST ) +# include +#endif + +#if optional_HAVE( TYPE_TRAITS ) +# include +#elif optional_HAVE( TR1_TYPE_TRAITS ) +# include +#endif + +// type traits needed: + +namespace nonstd { namespace optional_lite { namespace detail { + +#if optional_HAVE( CONDITIONAL ) + using std::conditional; +#else + template< bool B, typename T, typename F > struct conditional { typedef T type; }; + template< typename T, typename F > struct conditional { typedef F type; }; +#endif // optional_HAVE_CONDITIONAL + +}}} + +// +// in_place: code duplicated in any-lite, optional-lite, variant-lite: +// + +#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES + +namespace nonstd { + +namespace detail { + +template< class T > +struct in_place_type_tag {}; + +template< std::size_t I > +struct in_place_index_tag {}; + +} // namespace detail + +struct in_place_t {}; + +template< class T > +inline in_place_t in_place( detail::in_place_type_tag = detail::in_place_type_tag() ) +{ + return in_place_t(); +} + +template< std::size_t I > +inline in_place_t in_place( detail::in_place_index_tag = detail::in_place_index_tag() ) +{ + return in_place_t(); +} + +template< class T > +inline in_place_t in_place_type( detail::in_place_type_tag = detail::in_place_type_tag() ) +{ + return in_place_t(); +} + +template< std::size_t I > +inline in_place_t in_place_index( detail::in_place_index_tag = detail::in_place_index_tag() ) +{ + return in_place_t(); +} + +// mimic templated typedef: + +#define nonstd_lite_in_place_type_t( T) nonstd::in_place_t(&)( nonstd::detail::in_place_type_tag ) +#define nonstd_lite_in_place_index_t(T) nonstd::in_place_t(&)( nonstd::detail::in_place_index_tag ) + +#define nonstd_lite_HAVE_IN_PLACE_TYPES 1 + +} // namespace nonstd + +#endif // nonstd_lite_HAVE_IN_PLACE_TYPES + +// +// optional: +// + +namespace nonstd { namespace optional_lite { + +/// class optional + +template< typename T > +class optional; + +namespace detail { + +// C++11 emulation: + +struct nulltype{}; + +template< typename Head, typename Tail > +struct typelist +{ + typedef Head head; + typedef Tail tail; +}; + +#if optional_CONFIG_MAX_ALIGN_HACK + +// Max align, use most restricted type for alignment: + +#define optional_UNIQUE( name ) optional_UNIQUE2( name, __LINE__ ) +#define optional_UNIQUE2( name, line ) optional_UNIQUE3( name, line ) +#define optional_UNIQUE3( name, line ) name ## line + +#define optional_ALIGN_TYPE( type ) \ + type optional_UNIQUE( _t ); struct_t< type > optional_UNIQUE( _st ) + +template< typename T > +struct struct_t { T _; }; + +union max_align_t +{ + optional_ALIGN_TYPE( char ); + optional_ALIGN_TYPE( short int ); + optional_ALIGN_TYPE( int ); + optional_ALIGN_TYPE( long int ); + optional_ALIGN_TYPE( float ); + optional_ALIGN_TYPE( double ); + optional_ALIGN_TYPE( long double ); + optional_ALIGN_TYPE( char * ); + optional_ALIGN_TYPE( short int * ); + optional_ALIGN_TYPE( int * ); + optional_ALIGN_TYPE( long int * ); + optional_ALIGN_TYPE( float * ); + optional_ALIGN_TYPE( double * ); + optional_ALIGN_TYPE( long double * ); + optional_ALIGN_TYPE( void * ); + +#ifdef HAVE_LONG_LONG + optional_ALIGN_TYPE( long long ); +#endif + + struct Unknown; + + Unknown ( * optional_UNIQUE(_) )( Unknown ); + Unknown * Unknown::* optional_UNIQUE(_); + Unknown ( Unknown::* optional_UNIQUE(_) )( Unknown ); + + struct_t< Unknown ( * )( Unknown) > optional_UNIQUE(_); + struct_t< Unknown * Unknown::* > optional_UNIQUE(_); + struct_t< Unknown ( Unknown::* )(Unknown) > optional_UNIQUE(_); +}; + +#undef optional_UNIQUE +#undef optional_UNIQUE2 +#undef optional_UNIQUE3 + +#undef optional_ALIGN_TYPE + +#elif defined( optional_CONFIG_ALIGN_AS ) // optional_CONFIG_MAX_ALIGN_HACK + +// Use user-specified type for alignment: + +#define optional_ALIGN_AS( unused ) \ + optional_CONFIG_ALIGN_AS + +#else // optional_CONFIG_MAX_ALIGN_HACK + +// Determine POD type to use for alignment: + +#define optional_ALIGN_AS( to_align ) \ + typename type_of_size< alignment_types, alignment_of< to_align >::value >::type + +template +struct alignment_of; + +template +struct alignment_of_hack +{ + char c; + T t; + alignment_of_hack(); +}; + +template +struct alignment_logic +{ + enum { value = A < S ? A : S }; +}; + +template< typename T > +struct alignment_of +{ + enum { value = alignment_logic< + sizeof( alignment_of_hack ) - sizeof(T), sizeof(T) >::value, }; +}; + +template< typename List, size_t N > +struct type_of_size +{ + typedef typename conditional< + N == sizeof( typename List::head ), + typename List::head, + typename type_of_size::type >::type type; +}; + +template< size_t N > +struct type_of_size< nulltype, N > +{ + typedef optional_CONFIG_ALIGN_AS_FALLBACK type; +}; + +template< typename T> +struct struct_t { T _; }; + +#define optional_ALIGN_TYPE( type ) \ + typelist< type , typelist< struct_t< type > + +struct Unknown; + +typedef + optional_ALIGN_TYPE( char ), + optional_ALIGN_TYPE( short ), + optional_ALIGN_TYPE( int ), + optional_ALIGN_TYPE( long ), + optional_ALIGN_TYPE( float ), + optional_ALIGN_TYPE( double ), + optional_ALIGN_TYPE( long double ), + + optional_ALIGN_TYPE( char *), + optional_ALIGN_TYPE( short * ), + optional_ALIGN_TYPE( int * ), + optional_ALIGN_TYPE( long * ), + optional_ALIGN_TYPE( float * ), + optional_ALIGN_TYPE( double * ), + optional_ALIGN_TYPE( long double * ), + + optional_ALIGN_TYPE( Unknown ( * )( Unknown ) ), + optional_ALIGN_TYPE( Unknown * Unknown::* ), + optional_ALIGN_TYPE( Unknown ( Unknown::* )( Unknown ) ), + + nulltype + > > > > > > > > > > > > > > + > > > > > > > > > > > > > > + > > > > > > + alignment_types; + +#undef optional_ALIGN_TYPE + +#endif // optional_CONFIG_MAX_ALIGN_HACK + +/// C++03 constructed union to hold value. + +template< typename T > +union storage_t +{ +private: + friend class optional; + + typedef T value_type; + + storage_t() {} + + storage_t( value_type const & v ) + { + construct_value( v ); + } + + void construct_value( value_type const & v ) + { + ::new( value_ptr() ) value_type( v ); + } + +#if optional_CPP11_OR_GREATER + + storage_t( value_type && v ) + { + construct_value( std::move( v ) ); + } + + void construct_value( value_type && v ) + { + ::new( value_ptr() ) value_type( std::move( v ) ); + } + + template< class... Args > + void emplace( Args&&... args ) + { + ::new( value_ptr() ) value_type( std::forward(args)... ); + } + + template< class U, class... Args > + void emplace( std::initializer_list il, Args&&... args ) + { + ::new( value_ptr() ) value_type( il, std::forward(args)... ); + } + +#endif + + void destruct_value() + { + value_ptr()->~T(); + } + + value_type const * value_ptr() const + { + return as(); + } + + value_type * value_ptr() + { + return as(); + } + + value_type const & value() const optional_ref_qual + { + return * value_ptr(); + } + + value_type & value() optional_ref_qual + { + return * value_ptr(); + } + +#if optional_CPP11_OR_GREATER + + value_type const && value() const optional_refref_qual + { + return * value_ptr(); + } + + value_type && value() optional_refref_qual + { + return * value_ptr(); + } + +#endif + +#if optional_CPP11_OR_GREATER + + using aligned_storage_t = typename std::aligned_storage< sizeof(value_type), alignof(value_type) >::type; + aligned_storage_t data; + +#elif optional_CONFIG_MAX_ALIGN_HACK + + typedef struct { unsigned char data[ sizeof(value_type) ]; } aligned_storage_t; + + max_align_t hack; + aligned_storage_t data; + +#else + typedef optional_ALIGN_AS(value_type) align_as_type; + + typedef struct { align_as_type data[ 1 + ( sizeof(value_type) - 1 ) / sizeof(align_as_type) ]; } aligned_storage_t; + aligned_storage_t data; + +# undef optional_ALIGN_AS + +#endif // optional_CONFIG_MAX_ALIGN_HACK + + void * ptr() optional_noexcept + { + return &data; + } + + void const * ptr() const optional_noexcept + { + return &data; + } + + template + U * as() + { + return reinterpret_cast( ptr() ); + } + + template + U const * as() const + { + return reinterpret_cast( ptr() ); + } +}; + +} // namespace detail + +/// disengaged state tag + +struct nullopt_t +{ + struct init{}; + optional_constexpr nullopt_t( init ) {} +}; + +#if optional_HAVE( CONSTEXPR_11 ) +constexpr nullopt_t nullopt{ nullopt_t::init{} }; +#else +// extra parenthesis to prevent the most vexing parse: +const nullopt_t nullopt(( nullopt_t::init() )); +#endif + +/// optional access error + +class bad_optional_access : public std::logic_error +{ +public: + explicit bad_optional_access() + : logic_error( "bad optional access" ) {} +}; + +/// optional + +template< typename T> +class optional +{ +private: + typedef void (optional::*safe_bool)() const; + +public: + typedef T value_type; + + optional_constexpr optional() optional_noexcept + : has_value_( false ) + , contained() + {} + + optional_constexpr optional( nullopt_t ) optional_noexcept + : has_value_( false ) + , contained() + {} + + optional( optional const & rhs ) + : has_value_( rhs.has_value() ) + { + if ( rhs.has_value() ) + contained.construct_value( rhs.contained.value() ); + } + +#if optional_CPP11_OR_GREATER + optional_constexpr14 optional( optional && rhs ) noexcept( std::is_nothrow_move_constructible::value ) + : has_value_( rhs.has_value() ) + { + if ( rhs.has_value() ) + contained.construct_value( std::move( rhs.contained.value() ) ); + } +#endif + + optional_constexpr optional( value_type const & value ) + : has_value_( true ) + , contained( value ) + {} + +#if optional_CPP11_OR_GREATER + + optional_constexpr optional( value_type && value ) + : has_value_( true ) + , contained( std::move( value ) ) + {} + + template< class... Args > + optional_constexpr explicit optional( nonstd_lite_in_place_type_t(T), Args&&... args ) + : has_value_( true ) + , contained( T( std::forward(args)...) ) + {} + + template< class U, class... Args > + optional_constexpr explicit optional( nonstd_lite_in_place_type_t(T), std::initializer_list il, Args&&... args ) + : has_value_( true ) + , contained( T( il, std::forward(args)...) ) + {} + +#endif // optional_CPP11_OR_GREATER + + ~optional() + { + if ( has_value() ) + contained.destruct_value(); + } + + // assignment + + optional & operator=( nullopt_t ) optional_noexcept + { + reset(); + return *this; + } + + optional & operator=( optional const & rhs ) +#if optional_CPP11_OR_GREATER + noexcept( std::is_nothrow_move_assignable::value && std::is_nothrow_move_constructible::value ) +#endif + { + if ( has_value() == true && rhs.has_value() == false ) reset(); + else if ( has_value() == false && rhs.has_value() == true ) initialize( *rhs ); + else if ( has_value() == true && rhs.has_value() == true ) contained.value() = *rhs; + return *this; + } + +#if optional_CPP11_OR_GREATER + + optional & operator=( optional && rhs ) noexcept + { + if ( has_value() == true && rhs.has_value() == false ) reset(); + else if ( has_value() == false && rhs.has_value() == true ) initialize( std::move( *rhs ) ); + else if ( has_value() == true && rhs.has_value() == true ) contained.value() = std::move( *rhs ); + return *this; + } + + template< class U, + typename = typename std::enable_if< std::is_same< typename std::decay::type, T>::value >::type > + optional & operator=( U && v ) + { + if ( has_value() ) contained.value() = std::forward( v ); + else initialize( T( std::forward( v ) ) ); + return *this; + } + + template< class... Args > + void emplace( Args&&... args ) + { + *this = nullopt; + contained.emplace( std::forward(args)... ); + has_value_ = true; + } + + + template< class U, class... Args > + void emplace( std::initializer_list il, Args&&... args ) + { + *this = nullopt; + contained.emplace( il, std::forward(args)... ); + has_value_ = true; + } + +#endif // optional_CPP11_OR_GREATER + + // swap + + void swap( optional & rhs ) +#if optional_CPP11_OR_GREATER + noexcept( std::is_nothrow_move_constructible::value && noexcept( std::swap( std::declval(), std::declval() ) ) ) +#endif + { + using std::swap; + if ( has_value() == true && rhs.has_value() == true ) { swap( **this, *rhs ); } + else if ( has_value() == false && rhs.has_value() == true ) { initialize( *rhs ); rhs.reset(); } + else if ( has_value() == true && rhs.has_value() == false ) { rhs.initialize( **this ); reset(); } + } + + // observers + + optional_constexpr value_type const * operator ->() const + { + return assert( has_value() ), + contained.value_ptr(); + } + + optional_constexpr14 value_type * operator ->() + { + return assert( has_value() ), + contained.value_ptr(); + } + + optional_constexpr value_type const & operator *() const optional_ref_qual + { + return assert( has_value() ), + contained.value(); + } + + optional_constexpr14 value_type & operator *() optional_ref_qual + { + return assert( has_value() ), + contained.value(); + } + +#if optional_CPP11_OR_GREATER + + optional_constexpr value_type const && operator *() const optional_refref_qual + { + return assert( has_value() ), + std::move( contained.value() ); + } + + optional_constexpr14 value_type && operator *() optional_refref_qual + { + return assert( has_value() ), + std::move( contained.value() ); + } + +#endif + +#if optional_CPP11_OR_GREATER + optional_constexpr explicit operator bool() const optional_noexcept + { + return has_value(); + } +#else + optional_constexpr operator safe_bool() const optional_noexcept + { + return has_value() ? &optional::this_type_does_not_support_comparisons : 0; + } +#endif + + optional_constexpr bool has_value() const optional_noexcept + { + return has_value_; + } + + optional_constexpr14 value_type const & value() const optional_ref_qual + { + if ( ! has_value() ) + throw bad_optional_access(); + + return contained.value(); + } + + optional_constexpr14 value_type & value() optional_ref_qual + { + if ( ! has_value() ) + throw bad_optional_access(); + + return contained.value(); + } + +#if optional_HAVE( REF_QUALIFIER ) + + optional_constexpr14 value_type const && value() const optional_refref_qual + { + if ( ! has_value() ) + throw bad_optional_access(); + + return std::move( contained.value() ); + } + + optional_constexpr14 value_type && value() optional_refref_qual + { + if ( ! has_value() ) + throw bad_optional_access(); + + return std::move( contained.value() ); + } + +#endif + +#if optional_CPP11_OR_GREATER + + template< class U > + optional_constexpr value_type value_or( U && v ) const optional_ref_qual + { + return has_value() ? contained.value() : static_cast(std::forward( v ) ); + } + + template< class U > + optional_constexpr value_type value_or( U && v ) const optional_refref_qual + { + return has_value() ? std::move( contained.value() ) : static_cast(std::forward( v ) ); + } + +#else + + template< class U > + optional_constexpr value_type value_or( U const & v ) const + { + return has_value() ? contained.value() : static_cast( v ); + } + +#endif // optional_CPP11_OR_GREATER + + // modifiers + + void reset() optional_noexcept + { + if ( has_value() ) + contained.destruct_value(); + + has_value_ = false; + } + +private: + void this_type_does_not_support_comparisons() const {} + + template< typename V > + void initialize( V const & value ) + { + assert( ! has_value() ); + contained.construct_value( value ); + has_value_ = true; + } + +#if optional_CPP11_OR_GREATER + template< typename V > + void initialize( V && value ) + { + assert( ! has_value() ); + contained.construct_value( std::move( value ) ); + has_value_ = true; + } + +#endif + +private: + bool has_value_; + detail::storage_t< value_type > contained; + +}; + +// Relational operators + +template< typename T, typename U > +inline optional_constexpr bool operator==( optional const & x, optional const & y ) +{ + return bool(x) != bool(y) ? false : !bool( x ) ? true : *x == *y; +} + +template< typename T, typename U > +inline optional_constexpr bool operator!=( optional const & x, optional const & y ) +{ + return !(x == y); +} + +template< typename T, typename U > +inline optional_constexpr bool operator<( optional const & x, optional const & y ) +{ + return (!y) ? false : (!x) ? true : *x < *y; +} + +template< typename T, typename U > +inline optional_constexpr bool operator>( optional const & x, optional const & y ) +{ + return (y < x); +} + +template< typename T, typename U > +inline optional_constexpr bool operator<=( optional const & x, optional const & y ) +{ + return !(y < x); +} + +template< typename T, typename U > +inline optional_constexpr bool operator>=( optional const & x, optional const & y ) +{ + return !(x < y); +} + +// Comparison with nullopt + +template< typename T > +inline optional_constexpr bool operator==( optional const & x, nullopt_t ) optional_noexcept +{ + return (!x); +} + +template< typename T > +inline optional_constexpr bool operator==( nullopt_t, optional const & x ) optional_noexcept +{ + return (!x); +} + +template< typename T > +inline optional_constexpr bool operator!=( optional const & x, nullopt_t ) optional_noexcept +{ + return bool(x); +} + +template< typename T > +inline optional_constexpr bool operator!=( nullopt_t, optional const & x ) optional_noexcept +{ + return bool(x); +} + +template< typename T > +inline optional_constexpr bool operator<( optional const &, nullopt_t ) optional_noexcept +{ + return false; +} + +template< typename T > +inline optional_constexpr bool operator<( nullopt_t, optional const & x ) optional_noexcept +{ + return bool(x); +} + +template< typename T > +inline optional_constexpr bool operator<=( optional const & x, nullopt_t ) optional_noexcept +{ + return (!x); +} + +template< typename T > +inline optional_constexpr bool operator<=( nullopt_t, optional const & ) optional_noexcept +{ + return true; +} + +template< typename T > +inline optional_constexpr bool operator>( optional const & x, nullopt_t ) optional_noexcept +{ + return bool(x); +} + +template< typename T > +inline optional_constexpr bool operator>( nullopt_t, optional const & ) optional_noexcept +{ + return false; +} + +template< typename T > +inline optional_constexpr bool operator>=( optional const &, nullopt_t ) optional_noexcept +{ + return true; +} + +template< typename T > +inline optional_constexpr bool operator>=( nullopt_t, optional const & x ) optional_noexcept +{ + return (!x); +} + +// Comparison with T + +template< typename T, typename U > +inline optional_constexpr bool operator==( optional const & x, U const & v ) +{ + return bool(x) ? *x == v : false; +} + +template< typename T, typename U > +inline optional_constexpr bool operator==( U const & v, optional const & x ) +{ + return bool(x) ? v == *x : false; +} + +template< typename T, typename U > +inline optional_constexpr bool operator!=( optional const & x, U const & v ) +{ + return bool(x) ? *x != v : true; +} + +template< typename T, typename U > +inline optional_constexpr bool operator!=( U const & v, optional const & x ) +{ + return bool(x) ? v != *x : true; +} + +template< typename T, typename U > +inline optional_constexpr bool operator<( optional const & x, U const & v ) +{ + return bool(x) ? *x < v : true; +} + +template< typename T, typename U > +inline optional_constexpr bool operator<( U const & v, optional const & x ) +{ + return bool(x) ? v < *x : false; +} + +template< typename T, typename U > +inline optional_constexpr bool operator<=( optional const & x, U const & v ) +{ + return bool(x) ? *x <= v : true; +} + +template< typename T, typename U > +inline optional_constexpr bool operator<=( U const & v, optional const & x ) +{ + return bool(x) ? v <= *x : false; +} + +template< typename T, typename U > +inline optional_constexpr bool operator>( optional const & x, U const & v ) +{ + return bool(x) ? *x > v : false; +} + +template< typename T, typename U > +inline optional_constexpr bool operator>( U const & v, optional const & x ) +{ + return bool(x) ? v > *x : true; +} + +template< typename T, typename U > +inline optional_constexpr bool operator>=( optional const & x, U const & v ) +{ + return bool(x) ? *x >= v : false; +} + +template< typename T, typename U > +inline optional_constexpr bool operator>=( U const & v, optional const & x ) +{ + return bool(x) ? v >= *x : true; +} + +// Specialized algorithms + +template< typename T > +void swap( optional & x, optional & y ) +#if optional_CPP11_OR_GREATER + noexcept( noexcept( x.swap(y) ) ) +#endif +{ + x.swap( y ); +} + +#if optional_CPP11_OR_GREATER + +template< class T > +optional_constexpr optional< typename std::decay::type > make_optional( T && v ) +{ + return optional< typename std::decay::type >( std::forward( v ) ); +} + +template< class T, class...Args > +optional_constexpr optional make_optional( Args&&... args ) +{ + return optional( in_place, std::forward(args)...); +} + +template< class T, class U, class... Args > +optional_constexpr optional make_optional( std::initializer_list il, Args&&... args ) +{ + return optional( in_place, il, std::forward(args)...); +} + +#else + +template< typename T > +optional make_optional( T const & v ) +{ + return optional( v ); +} + +#endif // optional_CPP11_OR_GREATER + +} // namespace optional + +using namespace optional_lite; + +} // namespace nonstd + +#if optional_CPP11_OR_GREATER + +// specialize the std::hash algorithm: + +namespace std { + +template< class T > +struct hash< nonstd::optional > +{ +public: + std::size_t operator()( nonstd::optional const & v ) const optional_noexcept + { + return bool( v ) ? hash()( *v ) : 0; + } +}; + +} //namespace std + +#endif // optional_CPP11_OR_GREATER + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined __GNUC__ +# pragma GCC diagnostic pop +#endif + +#endif // have C++17 std::optional + +#endif // NONSTD_OPTIONAL_LITE_HPP diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5857d0c..425581a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,7 @@ target_compile_options(mantis_core PUBLIC "$<$,$,$>:${MANTIS_RELEASE_CFLAGS}>") target_compile_options(mantis_core PUBLIC "$<$,$>:${MANTIS_RELEASE_CXXFLAGS}>") target_compile_definitions(mantis_core PUBLIC "${ARCH_DEFS}") +set_property(TARGET mantis_core PROPERTY INTERPROCEDURAL_OPTIMIZATION True) # link libmantis_core with the required libraries target_link_libraries(mantis_core @@ -42,6 +43,7 @@ target_compile_options(mantis PUBLIC "$<$,$,$>:${MANTIS_RELEASE_CFLAGS}>") target_compile_options(mantis PUBLIC "$<$,$>:${MANTIS_RELEASE_CXXFLAGS}>") target_compile_definitions(mantis PUBLIC "${ARCH_DEFS}") +set_property(TARGET mantis PROPERTY INTERPROCEDURAL_OPTIMIZATION True) add_executable(build_eq_graph build_eq_graph.cc) target_include_directories(build_eq_graph PUBLIC $) @@ -51,6 +53,7 @@ target_compile_options(build_eq_graph PUBLIC "$<$,$,$>:${MANTIS_RELEASE_CFLAGS}>") target_compile_options(build_eq_graph PUBLIC "$<$,$>:${MANTIS_RELEASE_CXXFLAGS}>") target_compile_definitions(build_eq_graph PUBLIC "${ARCH_DEFS}") +set_property(TARGET build_eq_graph PROPERTY INTERPROCEDURAL_OPTIMIZATION True) # TODO: look more into why this is necessary if (SDSL_INSTALL_PATH) @@ -67,6 +70,7 @@ target_include_directories(monochromatic_component_iterator PUBLIC $) target_link_libraries(monochromatic_component_iterator mantis_core) +set_property(TARGET monochromatic_component_iterator PROPERTY INTERPROCEDURAL_OPTIMIZATION True) add_executable(msf MSF.cc) @@ -74,18 +78,20 @@ target_include_directories(msf PUBLIC $) target_link_libraries(msf mantis_core) +set_property(TARGET msf PROPERTY INTERPROCEDURAL_OPTIMIZATION True) add_executable(loadColorCls validateMSF.cc) target_include_directories(loadColorCls PUBLIC $) target_link_libraries(loadColorCls mantis_core) +set_property(TARGET loadColorCls PROPERTY INTERPROCEDURAL_OPTIMIZATION True) add_executable(mstBoost MST_boost.cc) target_include_directories(mstBoost PUBLIC $) target_link_libraries(mstBoost mantis_core) - +set_property(TARGET mstBoost PROPERTY INTERPROCEDURAL_OPTIMIZATION True) #add_executable(walkEqcls walkEqcls.cc) #target_include_directories(walkEqcls PUBLIC diff --git a/src/validateMSF.cc b/src/validateMSF.cc index 25319b4..4fb5509 100644 --- a/src/validateMSF.cc +++ b/src/validateMSF.cc @@ -14,6 +14,8 @@ #include "lrucache.hpp" #include "hashutil.h" #include "lru/lru.hpp" +#include "tsl/hopscotch_map.h" +#include "nonstd/optional.hpp" struct QueryStats { uint32_t cnt = 0, cacheCntr = 0, noCacheCntr{0}; @@ -26,6 +28,8 @@ struct QueryStats { uint64_t globalQueryNum{0}; std::vector buffer; uint64_t numSamples{0}; + tsl::hopscotch_map numOcc; + bool trySample{false}; //std::unordered_map numOcc; }; @@ -99,12 +103,13 @@ class MSFQuery { std::vector buildColor(uint64_t eqid, QueryStats &queryStats, LRUCacheMap *lru_cache, RankScores* rs, + nonstd::optional& toDecode, // output param. Also decode these bool all = true) { (void)rs; std::vector flips(numSamples); std::vector xorflips(numSamples, 0); uint64_t i{eqid}, from{0}, to{0}; - uint64_t height{0}; + int64_t height{0}; auto& froms = queryStats.buffer; froms.clear(); queryStats.totEqcls++; @@ -120,25 +125,31 @@ class MSFQuery { foundCache = true; break; } - if (i > 0) - from = sbbv(i) + 1; - else - from = 0; + from = (i > 0) ? (sbbv(i) + 1) : 0; froms.push_back(from); + + if (queryStats.trySample) { + auto& occ = queryStats.numOcc[iparent]; + ++occ; + if ((!toDecode) and + (occ > 10) and + (height > 10) and + (lru_cache and + !lru_cache->contains(iparent))) { + toDecode = iparent; + } + } i = iparent; iparent = parentbv[i]; ++queryStats.totSel; ++height; } if (!foundCache and i != zero) { - if (i > 0) - from = sbbv(i) + 1; - else - from = 0; - froms.push_back(from); - ++queryStats.totSel; - queryStats.rootedNonZero++; - ++height; + from = (i > 0) ? (sbbv(i) + 1) : 0; + froms.push_back(from); + ++queryStats.totSel; + queryStats.rootedNonZero++; + ++height; } // update ranks /* @@ -148,7 +159,7 @@ class MSFQuery { } } */ - + uint64_t pctr{0}; for (auto f : froms) { bool found = false; uint64_t wrd{0}; @@ -207,6 +218,9 @@ mantis::QueryResult findSamples(const mantis::QuerySet &kmers, mantis::QueryResult sample_map(queryStats.numSamples,0); size_t numPerLevel = 10; + nonstd::optional toDecode{nonstd::nullopt}; + nonstd::optional dummy{nonstd::nullopt}; + for (auto it = query_eqclass_map.begin(); it != query_eqclass_map.end(); ++it) { auto eqclass_id = it->first - 1; auto count = it->second; @@ -216,9 +230,16 @@ mantis::QueryResult findSamples(const mantis::QuerySet &kmers, setbits = lru_cache[eqclass_id];//.get(eqclass_id); queryStats.cacheCntr++; } else { - setbits = msfQuery.buildColor(eqclass_id, queryStats, &lru_cache, rs, false); - lru_cache.emplace(eqclass_id, setbits); queryStats.noCacheCntr++; + toDecode.reset(); + dummy.reset(); + queryStats.trySample = (queryStats.noCacheCntr % 10 == 0); + setbits = msfQuery.buildColor(eqclass_id, queryStats, &lru_cache, rs, toDecode, false); + lru_cache.emplace(eqclass_id, setbits); + if ((queryStats.trySample) and toDecode) { + auto s = msfQuery.buildColor(*toDecode, queryStats, nullptr, nullptr, dummy, false); + lru_cache.emplace(*toDecode, s); + } } for (auto sb : setbits) { sample_map[sb] += count; @@ -378,7 +399,8 @@ int main(int argc, char *argv[]) { loadEqs(opt.eqlistfile, bvs); uint64_t cntr{0}; for (uint64_t idx = 0; idx < eqCount; idx++) { - std::vector newEq = msfQuery.buildColor(idx, queryStats, &cache_lru, nullptr); + nonstd::optional dummy{nonstd::nullopt}; + std::vector newEq = msfQuery.buildColor(idx, queryStats, &cache_lru, nullptr, dummy); std::vector oldEq(numWrds); buildColor(bvs, oldEq, idx, opt.numSamples); if (newEq != oldEq) { @@ -403,10 +425,12 @@ int main(int argc, char *argv[]) { std::default_random_engine e1(r()); std::uniform_int_distribution uniform_dist(0, eqCount - 1); for (uint64_t idx = 0; idx < 182169; idx++) { + nonstd::optional dummy{nonstd::nullopt}; std::vector newEq = msfQuery.buildColor(uniform_dist(e1), queryStats, &cache_lru, nullptr, + dummy, false); /*if (idx % 10000000 == 0) { std::cerr << idx << " eqs decoded\n"; From b6c71d804f4afe81155c2ccf53c82320497d2899 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Mon, 30 Jul 2018 15:59:37 -0400 Subject: [PATCH 055/122] prepare the environment for fillGraph part. --- src/MSF.cc | 295 ++++++++++++++++++++++++++++------------------------- 1 file changed, 155 insertions(+), 140 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index c22ef41..92b04b3 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -10,7 +10,6 @@ struct Opts { std::string filename; uint64_t numNodes; // = std::stoull(argv[2]); - uint32_t bucketCnt; // = std::stoull(argv[3]); uint16_t numSamples; std::string eqClsListFile; std::string outputDir; @@ -22,7 +21,7 @@ int main(int argc, char *argv[]) { and undirected graph */ using namespace clipp; enum class mode { - build, help + build, fillGraph, help }; mode selected = mode::help; Opts opt; @@ -33,8 +32,20 @@ int main(int argc, char *argv[]) { value("edge_filename", opt.filename) % "File containing list of eq. class edges.", required("-n", "--eqCls-cnt") & value("equivalenceClass_count", opt.numNodes) % "Total number of equivalence (color) classes.", - required("-b", "--bucket-cnt") & - value("bucket_count", opt.bucketCnt) % "Total number of valid distances.", + required("-s", "--numSamples") & + value("numSamples", opt.numSamples) % "Total number of experiments (samples).", + required("-c", "--eqCls-lst") & + value("eqCls_list_filename", opt.eqClsListFile) % "File containing list of equivalence (color) classes.", + required("-o", "--output_dir") & + value("output directory", opt.outputDir) % "Directory that all the int_vectors will be stored in." + ); + + auto fillGraph_mode = ( + command("fillGraph").set(selected, mode::fillGraph), + required("-e", "--edge-filename") & + value("edge_filename", opt.filename) % "File containing list of eq. class edges.", + required("-n", "--eqCls-cnt") & + value("equivalenceClass_count", opt.numNodes) % "Total number of equivalence (color) classes.", required("-s", "--numSamples") & value("numSamples", opt.numSamples) % "Total number of experiments (samples).", required("-c", "--eqCls-lst") & @@ -44,7 +55,7 @@ int main(int argc, char *argv[]) { ); auto cli = ( - (build_mode | command("help").set(selected, mode::help) + (build_mode | fillGraph_mode | command("help").set(selected, mode::help) ) ); @@ -70,7 +81,7 @@ int main(int argc, char *argv[]) { std::cerr << "here are the inputs: \n" << opt.filename << "\n" << opt.numNodes << "\n" - << opt.bucketCnt << "\n"; + << opt.numSamples << "\n"; uint64_t numWrds = (uint64_t)std::ceil((double)opt.numSamples/64.0); opt.numNodes++; // number of nodes is one more than input including the zero node @@ -81,152 +92,156 @@ int main(int argc, char *argv[]) { std::cerr << "Done loading list of equivalence class buckets\n"; ifstream file(opt.filename); - Graph g(opt.bucketCnt); - - uint16_t w_; - uint32_t n1, n2, edgeCntr{0}, zero{(uint32_t) opt.numNodes - 1}; - { - std::cerr << "Adding edges from 0 to each node with number of set bits in the node as weight .. \n"; - for (uint32_t i = 0; i < opt.numNodes; i++) { - uint16_t ones = sum1s(eqs, i, opt.numSamples, numWrds); - g.addEdge(zero, i, ones); - } - std::cerr << "Done adding 0-end edges.\n"; - std::cerr << "Adding edges between color classes .. \n"; - while (file.good()) { - file >> n1 >> n2 >> w_; - g.addEdge(n1, n2, w_); - edgeCntr++; - } - //file.clear(); - //file.seekg(0, ios::beg); - file.close(); - std::cerr << "Done adding edges between color classes .. \n"; - std::cerr << "\n# of edges: " << edgeCntr - << "\n"; + if (selected == mode::build) { + Graph g(opt.numSamples); + + uint16_t w_; + uint32_t n1, n2, edgeCntr{0}, zero{(uint32_t) opt.numNodes - 1}; + { + std::cerr << "Adding edges from 0 to each node with number of set bits in the node as weight .. \n"; + for (uint32_t i = 0; i < opt.numNodes; i++) { + uint16_t ones = sum1s(eqs, i, opt.numSamples, numWrds); + g.addEdge(zero, i, ones); + } + std::cerr << "Done adding 0-end edges.\n"; + std::cerr << "Adding edges between color classes .. \n"; + while (file.good()) { + file >> n1 >> n2 >> w_; + g.addEdge(n1, n2, w_); + edgeCntr++; + } + //file.clear(); + //file.seekg(0, ios::beg); + file.close(); + std::cerr << "Done adding edges between color classes .. \n"; + + std::cerr << "\n# of edges: " << edgeCntr + << "\n"; // nodes.clear(); - } - g.V = opt.numNodes; - g.mst.resize(opt.numNodes); - //g.V = numNodes; - //ifstream file(filename); - - DisjointSets ds = g.kruskalMSF(opt.bucketCnt); - std::queue q; - vector> p2c(opt.numNodes); - vector c2p(opt.numNodes); - - for (auto e = 0; e < g.mst.size(); e++) { - // put all the mst leaves into the queue - if (g.mst[e].size() == 1) { - q.push(e); // e is a leaf } - } - // now go run the algorithm to find the root of the tree and all the edge directions - sdsl::int_vector<> parentbv(opt.numNodes, 0, ceil(log2(opt.numNodes))); - - bool check = false; - uint64_t nodeCntr{0}; - while (!q.empty()) { - uint32_t node = q.front(); - q.pop(); - if (g.mst[node].size() == 0) { - // this node is the root - // and this should be the end of the loop - // just as a validation, I'll continue and expect the while to end here - if (check) { - std::cerr << "A TERRIBLE BUG!!!\n" - << "finding a node with no edges should only happen once at the root\n"; - std::exit(1); + g.V = opt.numNodes; + g.mst.resize(opt.numNodes); + //g.V = numNodes; + //ifstream file(filename); + + DisjointSets ds = g.kruskalMSF(opt.numSamples); + std::queue q; + vector> p2c(opt.numNodes); + vector c2p(opt.numNodes); + + for (auto e = 0; e < g.mst.size(); e++) { + // put all the mst leaves into the queue + if (g.mst[e].size() == 1) { + q.push(e); // e is a leaf } - parentbv[node] = node; - check = true; - // Update the total weight to contain the delta of root from bv of zero - uint16_t ones = 1; // If the root is zero itself - if (node != zero) // otherwise - ones = sum1s(eqs, node, opt.numSamples, numWrds); - g.mst_totalWeight += ones; - continue; - } - // what ever is in q (node) is a leaf and has only one edge left - // fetch the pointer to that edge (bucket+idx) - auto buck = g.mst[node][0].bucket; - auto idx = g.mst[node][0].idx; - // fetch the two ends of the edge and based on the node id decide which one is the src/dest - // Destination is the one that represents current node which has been selected as leaf sooner than the other end - uint64_t dest = g.edges[buck][idx].n1; - uint64_t src = g.edges[buck][idx].n2; - if (src == node) { // swap src & dest since src is the leaf - std::swap(src, dest); - } - if (dest == zero) { // we break the tree from node zero - parentbv[zero] = zero; - // we're breaking the tree, so should remove the edge weight from total weights - // But instead we're gonna spend one slot to store 0 as the delta of the zero node from zero node - g.mst_totalWeight = g.mst_totalWeight - g.edges[buck][idx].weight + 1; - } - else { - parentbv[dest] = src; - } - // erase the edge from src list of edges - g.mst[dest].erase(std::remove_if( - g.mst[dest].begin(), g.mst[dest].end(), - [buck, idx](const EdgePtr &x) { - return x.bucket == buck && x.idx == idx; - }), g.mst[dest].end()); - // erase the edge from dest list of edges - g.mst[src].erase(std::remove_if( - g.mst[src].begin(), g.mst[src].end(), - [buck, idx](const EdgePtr &x) { - return x.bucket == buck && x.idx == idx; - }), g.mst[src].end()); - // the destination has no edges left - // but if the src has turned to a leaf (node with one edge) - // add it to the queue - if (g.mst[src].size() == 1) { - q.push(src); } - nodeCntr++; // just a counter for the log - if (nodeCntr % 10000000 == 0) { - std::cerr << nodeCntr << " nodes processed toward root\n"; + // now go run the algorithm to find the root of the tree and all the edge directions + sdsl::int_vector<> parentbv(opt.numNodes, 0, ceil(log2(opt.numNodes))); + + bool check = false; + uint64_t nodeCntr{0}; + while (!q.empty()) { + uint32_t node = q.front(); + q.pop(); + if (g.mst[node].size() == 0) { + // this node is the root + // and this should be the end of the loop + // just as a validation, I'll continue and expect the while to end here + if (check) { + std::cerr << "A TERRIBLE BUG!!!\n" + << "finding a node with no edges should only happen once at the root\n"; + std::exit(1); + } + parentbv[node] = node; + check = true; + // Update the total weight to contain the delta of root from bv of zero + uint16_t ones = 1; // If the root is zero itself + if (node != zero) // otherwise + ones = sum1s(eqs, node, opt.numSamples, numWrds); + g.mst_totalWeight += ones; + continue; + } + // what ever is in q (node) is a leaf and has only one edge left + // fetch the pointer to that edge (bucket+idx) + auto buck = g.mst[node][0].bucket; + auto idx = g.mst[node][0].idx; + // fetch the two ends of the edge and based on the node id decide which one is the src/dest + // Destination is the one that represents current node which has been selected as leaf sooner than the other end + uint64_t dest = g.edges[buck][idx].n1; + uint64_t src = g.edges[buck][idx].n2; + if (src == node) { // swap src & dest since src is the leaf + std::swap(src, dest); + } + if (dest == zero) { // we break the tree from node zero + parentbv[zero] = zero; + // we're breaking the tree, so should remove the edge weight from total weights + // But instead we're gonna spend one slot to store 0 as the delta of the zero node from zero node + g.mst_totalWeight = g.mst_totalWeight - g.edges[buck][idx].weight + 1; + } else { + parentbv[dest] = src; + } + // erase the edge from src list of edges + g.mst[dest].erase(std::remove_if( + g.mst[dest].begin(), g.mst[dest].end(), + [buck, idx](const EdgePtr &x) { + return x.bucket == buck && x.idx == idx; + }), g.mst[dest].end()); + // erase the edge from dest list of edges + g.mst[src].erase(std::remove_if( + g.mst[src].begin(), g.mst[src].end(), + [buck, idx](const EdgePtr &x) { + return x.bucket == buck && x.idx == idx; + }), g.mst[src].end()); + // the destination has no edges left + // but if the src has turned to a leaf (node with one edge) + // add it to the queue + if (g.mst[src].size() == 1) { + q.push(src); + } + nodeCntr++; // just a counter for the log + if (nodeCntr % 10000000 == 0) { + std::cerr << nodeCntr << " nodes processed toward root\n"; + } } - } - sdsl::int_vector<> deltabv(g.mst_totalWeight, 0, ceil(log2(opt.numSamples))); - sdsl::bit_vector bbv(g.mst_totalWeight, 0); - - // calculate all the stats!! - // create the data structures - std::cerr << "Calculate Stats .. \n"; - std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n"; - uint64_t deltaOffset{0}; - for (uint64_t i = 0; i < parentbv.size(); i++) { - std::vector deltas; - if (i == zero) { - deltaOffset++; - } else if (parentbv[i] == zero || parentbv[i] == i) { - deltas = getDeltaList(eqs, i, opt.numSamples, numWrds); - } else { - deltas = getDeltaList(eqs, parentbv[i], i, opt.numSamples, numWrds); - } - for (auto & v : deltas) { - deltabv[deltaOffset] = v; - deltaOffset++; - } - bbv[deltaOffset-1] = 1; - if (i % 10000000 == 0) { - std::cerr << i << " nodes parents processed\n"; + sdsl::int_vector<> deltabv(g.mst_totalWeight, 0, ceil(log2(opt.numSamples))); + sdsl::bit_vector bbv(g.mst_totalWeight, 0); + + // calculate all the stats!! + // create the data structures + std::cerr << "Calculate Stats .. \n"; + std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n"; + uint64_t deltaOffset{0}; + for (uint64_t i = 0; i < parentbv.size(); i++) { + std::vector deltas; + if (i == zero) { + deltaOffset++; + } else if (parentbv[i] == zero || parentbv[i] == i) { + deltas = getDeltaList(eqs, i, opt.numSamples, numWrds); + } else { + deltas = getDeltaList(eqs, parentbv[i], i, opt.numSamples, numWrds); + } + for (auto &v : deltas) { + deltabv[deltaOffset] = v; + deltaOffset++; + } + bbv[deltaOffset - 1] = 1; + if (i % 10000000 == 0) { + std::cerr << i << " nodes parents processed\n"; + } } - } - std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n"; + std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n"; - sdsl::store_to_file(parentbv, opt.outputDir+"/parents.bv"); - sdsl::store_to_file(deltabv, opt.outputDir+"/deltas.bv"); - sdsl::store_to_file(bbv, opt.outputDir+"/boundary.bv"); + sdsl::store_to_file(parentbv, opt.outputDir + "/parents.bv"); + sdsl::store_to_file(deltabv, opt.outputDir + "/deltas.bv"); + sdsl::store_to_file(bbv, opt.outputDir + "/boundary.bv"); + } + else if (selected == mode::fillGraph) { + } return 0; } From c4ce1fcbfe62a1662664a6de0271e8ce15a79156 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Mon, 30 Jul 2018 23:27:22 -0400 Subject: [PATCH 056/122] Finding shorter direct links for any hubs of h steps away for each node. h is given as input --- include/MSF.h | 13 +++++++++ src/MSF.cc | 81 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/include/MSF.h b/include/MSF.h index af8366f..bc52c97 100644 --- a/include/MSF.h +++ b/include/MSF.h @@ -285,4 +285,17 @@ res.push_back(i*64+j); return res; // rely on c++ optimization } +uint64_t manhattanDist(eqvec &bvs, uint64_t eqid1, uint64_t eqid2, uint64_t num_samples) { + uint64_t dist{0}; + std::vector eq1(((num_samples - 1) / 64) + 1), eq2(((num_samples - 1) / 64) + 1); + buildColor(bvs, eq1, eqid1, num_samples); + buildColor(bvs, eq2, eqid2, num_samples); + + for (uint64_t i = 0; i < eq1.size(); i++) { + if (eq1[i] != eq2[i]) + dist += sdsl::bits::cnt(eq1[i] ^ eq2[i]); + } + return dist; +} + #endif //MANTIS_MSF_H diff --git a/src/MSF.cc b/src/MSF.cc index 92b04b3..b79eefd 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -5,7 +5,24 @@ // https://www.geeksforgeeks.org/kruskals-minimum-spanning-tree-using-stl-in-c/ // +#include +#include #include "MSF.h" +#include "sparsepp/spp.h" + +struct Hub { + uint32_t id; + uint32_t dist; + uint16_t level; + + Hub(uint32_t id, uint32_t dist, uint16_t level) : id(id), dist(dist), level(level) {} + + bool operator<(const Hub& rhs) const + { + return level < rhs.level; + } + +}; struct Opts { std::string filename; @@ -13,6 +30,7 @@ struct Opts { uint16_t numSamples; std::string eqClsListFile; std::string outputDir; + uint16_t hubs; }; @@ -44,14 +62,14 @@ int main(int argc, char *argv[]) { command("fillGraph").set(selected, mode::fillGraph), required("-e", "--edge-filename") & value("edge_filename", opt.filename) % "File containing list of eq. class edges.", - required("-n", "--eqCls-cnt") & - value("equivalenceClass_count", opt.numNodes) % "Total number of equivalence (color) classes.", required("-s", "--numSamples") & value("numSamples", opt.numSamples) % "Total number of experiments (samples).", required("-c", "--eqCls-lst") & value("eqCls_list_filename", opt.eqClsListFile) % "File containing list of equivalence (color) classes.", required("-o", "--output_dir") & - value("output directory", opt.outputDir) % "Directory that all the int_vectors will be stored in." + value("output directory", opt.outputDir) % "Directory that all the int_vectors will be stored in.", + required("-h", "--hubs") & + value("hubs", opt.hubs) % "# of hubs to search for each node for a direct link with smaller weight." ); auto cli = ( @@ -93,11 +111,11 @@ int main(int argc, char *argv[]) { ifstream file(opt.filename); + uint16_t w_; + uint32_t n1, n2, edgeCntr{0}, zero{(uint32_t) opt.numNodes - 1}; if (selected == mode::build) { Graph g(opt.numSamples); - uint16_t w_; - uint32_t n1, n2, edgeCntr{0}, zero{(uint32_t) opt.numNodes - 1}; { std::cerr << "Adding edges from 0 to each node with number of set bits in the node as weight .. \n"; for (uint32_t i = 0; i < opt.numNodes; i++) { @@ -240,7 +258,60 @@ int main(int argc, char *argv[]) { sdsl::store_to_file(bbv, opt.outputDir + "/boundary.bv"); } else if (selected == mode::fillGraph) { + spp::sparse_hash_map>> nodes; + std::cerr << "Adding edges between color classes .. \n"; + while (file.good()) { + file >> n1 >> n2 >> w_; + nodes[n1].push_back(std::make_pair(n2, w_)); + nodes[n2].push_back(std::make_pair(n1, w_)); + edgeCntr++; + if (edgeCntr % 10000000 == 0) { + std::cerr << edgeCntr << " passed\n"; + } + } + file.close(); + std::ofstream of(opt.outputDir+"/extraEdges.lst"); + std::cerr << "Done adding edges between color classes .. \n"; + std::cerr << "Total # of nodes : " << nodes.size() << "\n"; + std::cerr << "Hubs required: " << opt.hubs << "\n"; + uint64_t nodeCntr{0}; + for (auto& kv : nodes) { + auto& id = kv.first; + auto& neis = kv.second; + std::set visited; + visited.insert(id); + std::priority_queue hubs; + //std::cerr << "n " << id << " " << neis.size() << "\n"; + for (auto& nei : neis) { + visited.insert(nei.first); + Hub nh(nei.first, nei.second, 1); + hubs.push(nh); + } + while (!hubs.empty() && hubs.top().level != opt.hubs) { + auto nei = hubs.top(); + hubs.pop(); + //std::cerr << nei.level << " " << nei.id << " " << nei.dist << " " << nodes[nei.id].size() << "\n"; + for (auto &neinei : nodes[nei.id]) { + //std::cerr << " " << neinei.first << "\n"; + if (visited.find(neinei.first) == visited.end()) { + visited.insert(neinei.first); + Hub nh(neinei.first, neinei.second + nei.dist, nei.level + 1); + if (id < nh.id) { + auto directDist = manhattanDist(eqs, id, nh.id, opt.numSamples); + if (directDist < nh.dist) { + of << id << "\t" << nh.id << "\t" << directDist << "\n"; + } + } + hubs.push(nh); + } + } + } + nodeCntr++; + if (nodeCntr % 100000 == 0) { + std::cerr << nodeCntr << " passed\n"; + } + } } return 0; } From 38059b0694c33cd3e4dce0084ad02d93f35bbdb2 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Mon, 30 Jul 2018 23:52:15 -0400 Subject: [PATCH 057/122] Hamming! --- include/MSF.h | 2 +- src/MSF.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/MSF.h b/include/MSF.h index bc52c97..c0af9c1 100644 --- a/include/MSF.h +++ b/include/MSF.h @@ -285,7 +285,7 @@ res.push_back(i*64+j); return res; // rely on c++ optimization } -uint64_t manhattanDist(eqvec &bvs, uint64_t eqid1, uint64_t eqid2, uint64_t num_samples) { +uint64_t hammingDist(eqvec &bvs, uint64_t eqid1, uint64_t eqid2, uint64_t num_samples) { uint64_t dist{0}; std::vector eq1(((num_samples - 1) / 64) + 1), eq2(((num_samples - 1) / 64) + 1); buildColor(bvs, eq1, eqid1, num_samples); diff --git a/src/MSF.cc b/src/MSF.cc index b79eefd..8807a19 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -298,7 +298,7 @@ int main(int argc, char *argv[]) { visited.insert(neinei.first); Hub nh(neinei.first, neinei.second + nei.dist, nei.level + 1); if (id < nh.id) { - auto directDist = manhattanDist(eqs, id, nh.id, opt.numSamples); + auto directDist = hammingDist(eqs, id, nh.id, opt.numSamples); if (directDist < nh.dist) { of << id << "\t" << nh.id << "\t" << directDist << "\n"; } From 67f6603eed02c63828ec37e689f20c0a36b7f3bc Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 31 Jul 2018 12:22:58 -0400 Subject: [PATCH 058/122] Limited the search for better direct edges to those nodes with degree smaller than d(=100) --- src/MSF.cc | 82 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index 8807a19..6c75c6d 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -10,15 +10,14 @@ #include "MSF.h" #include "sparsepp/spp.h" -struct Hub { +struct Hop { uint32_t id; uint32_t dist; uint16_t level; - Hub(uint32_t id, uint32_t dist, uint16_t level) : id(id), dist(dist), level(level) {} + Hop(uint32_t id, uint32_t dist, uint16_t level) : id(id), dist(dist), level(level) {} - bool operator<(const Hub& rhs) const - { + bool operator<(const Hop &rhs) const { return level < rhs.level; } @@ -30,7 +29,7 @@ struct Opts { uint16_t numSamples; std::string eqClsListFile; std::string outputDir; - uint16_t hubs; + uint16_t hops; }; @@ -53,7 +52,8 @@ int main(int argc, char *argv[]) { required("-s", "--numSamples") & value("numSamples", opt.numSamples) % "Total number of experiments (samples).", required("-c", "--eqCls-lst") & - value("eqCls_list_filename", opt.eqClsListFile) % "File containing list of equivalence (color) classes.", + value("eqCls_list_filename", opt.eqClsListFile) % + "File containing list of equivalence (color) classes.", required("-o", "--output_dir") & value("output directory", opt.outputDir) % "Directory that all the int_vectors will be stored in." ); @@ -64,12 +64,15 @@ int main(int argc, char *argv[]) { value("edge_filename", opt.filename) % "File containing list of eq. class edges.", required("-s", "--numSamples") & value("numSamples", opt.numSamples) % "Total number of experiments (samples).", + required("-n", "--eqCls-cnt") & + value("equivalenceClass_count", opt.numNodes) % "Total number of equivalence (color) classes.", required("-c", "--eqCls-lst") & - value("eqCls_list_filename", opt.eqClsListFile) % "File containing list of equivalence (color) classes.", + value("eqCls_list_filename", opt.eqClsListFile) % + "File containing list of equivalence (color) classes.", required("-o", "--output_dir") & value("output directory", opt.outputDir) % "Directory that all the int_vectors will be stored in.", - required("-h", "--hubs") & - value("hubs", opt.hubs) % "# of hubs to search for each node for a direct link with smaller weight." + required("-h", "--hops") & + value("hops", opt.hops) % "# of hops to search for each node for a direct link with smaller weight." ); auto cli = ( @@ -100,11 +103,12 @@ int main(int argc, char *argv[]) { << opt.filename << "\n" << opt.numNodes << "\n" << opt.numSamples << "\n"; - uint64_t numWrds = (uint64_t)std::ceil((double)opt.numSamples/64.0); + uint64_t numWrds = (uint64_t) std::ceil((double) opt.numSamples / 64.0); opt.numNodes++; // number of nodes is one more than input including the zero node std::cerr << "Loading all the equivalence classes first .. \n"; - std::vector> eqs; + std::vector> + eqs; eqs.reserve(20); loadEqs(opt.eqClsListFile, eqs); std::cerr << "Done loading list of equivalence class buckets\n"; @@ -256,9 +260,9 @@ int main(int argc, char *argv[]) { sdsl::store_to_file(parentbv, opt.outputDir + "/parents.bv"); sdsl::store_to_file(deltabv, opt.outputDir + "/deltas.bv"); sdsl::store_to_file(bbv, opt.outputDir + "/boundary.bv"); - } - else if (selected == mode::fillGraph) { - spp::sparse_hash_map>> nodes; + } else if (selected == mode::fillGraph) { + //spp::sparse_hash_map>> nodes; + std::vector>> nodes(opt.numNodes); std::cerr << "Adding edges between color classes .. \n"; while (file.good()) { file >> n1 >> n2 >> w_; @@ -270,45 +274,59 @@ int main(int argc, char *argv[]) { } } file.close(); - std::ofstream of(opt.outputDir+"/extraEdges.lst"); + std::ofstream of(opt.outputDir + "/extraEdges.lst"); std::cerr << "Done adding edges between color classes .. \n"; std::cerr << "Total # of nodes : " << nodes.size() << "\n"; - std::cerr << "Hubs required: " << opt.hubs << "\n"; - uint64_t nodeCntr{0}; - for (auto& kv : nodes) { - auto& id = kv.first; - auto& neis = kv.second; - std::set visited; + std::cerr << "Hops required: " << opt.hops << "\n"; + uint64_t nodeCntr{0}, eqWrds{static_cast(((opt.numSamples - 1) / 64) + 1)}; + //std::vector> nodeEqs(nodes.size()); + std::cerr << "initialized\n"; + for (auto &kv : nodes) { + auto &id = nodeCntr;//kv.first; + auto &neis = kv;//kv.second; + std::unordered_set visited; visited.insert(id); - std::priority_queue hubs; + std::priority_queue hops; + std::vector eq1(eqWrds); + buildColor(eqs, eq1, id, opt.numSamples); + //std::cerr << "n " << id << " " << neis.size() << "\n"; - for (auto& nei : neis) { + uint64_t maxNei{100}; + if (neis.size() > maxNei) continue; + for (auto &nei : neis) { visited.insert(nei.first); - Hub nh(nei.first, nei.second, 1); - hubs.push(nh); + Hop nh(nei.first, nei.second, 1); + hops.push(nh); } - while (!hubs.empty() && hubs.top().level != opt.hubs) { - auto nei = hubs.top(); - hubs.pop(); + while (!hops.empty() && hops.top().level != opt.hops) { + auto nei = hops.top(); + hops.pop(); //std::cerr << nei.level << " " << nei.id << " " << nei.dist << " " << nodes[nei.id].size() << "\n"; + if (nodes[nei.id].size() > maxNei) continue; for (auto &neinei : nodes[nei.id]) { //std::cerr << " " << neinei.first << "\n"; if (visited.find(neinei.first) == visited.end()) { visited.insert(neinei.first); - Hub nh(neinei.first, neinei.second + nei.dist, nei.level + 1); + Hop nh(neinei.first, neinei.second + nei.dist, nei.level + 1); if (id < nh.id) { - auto directDist = hammingDist(eqs, id, nh.id, opt.numSamples); + std::vector eq2(eqWrds); + buildColor(eqs, eq2, nh.id, opt.numSamples); + uint64_t directDist = 0;//hammingDist(eqs, id, nh.id, opt.numSamples); + for (uint64_t i = 0; i < eq1.size(); i++) { + if (eq1[i] != eq2[i]) + directDist += sdsl::bits::cnt(eq1[i] ^ eq2[i]); + } if (directDist < nh.dist) { of << id << "\t" << nh.id << "\t" << directDist << "\n"; } } - hubs.push(nh); + hops.push(nh); } } } nodeCntr++; - if (nodeCntr % 100000 == 0) { + if (nodeCntr % 1000000 == 0) { std::cerr << nodeCntr << " passed\n"; } } From 93666f847f173187c2ccc0c463904cbe1f6adf1b Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 31 Jul 2018 12:52:07 -0400 Subject: [PATCH 059/122] minor changes --- src/MSF.cc | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index 6c75c6d..4cc81e6 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -159,6 +159,7 @@ int main(int argc, char *argv[]) { } } // now go run the algorithm to find the root of the tree and all the edge directions + std::cerr << "Creating parentBV...\n"; sdsl::int_vector<> parentbv(opt.numNodes, 0, ceil(log2(opt.numNodes))); bool check = false; @@ -227,13 +228,12 @@ int main(int argc, char *argv[]) { } } + // create the data structures + std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n"; + std::cerr << "Creating deltaBV and bBV...\n"; sdsl::int_vector<> deltabv(g.mst_totalWeight, 0, ceil(log2(opt.numSamples))); sdsl::bit_vector bbv(g.mst_totalWeight, 0); - // calculate all the stats!! - // create the data structures - std::cerr << "Calculate Stats .. \n"; - std::cerr << "Sum of MST weights: " << g.mst_totalWeight << "\n"; uint64_t deltaOffset{0}; for (uint64_t i = 0; i < parentbv.size(); i++) { std::vector deltas; @@ -278,19 +278,18 @@ int main(int argc, char *argv[]) { std::cerr << "Done adding edges between color classes .. \n"; std::cerr << "Total # of nodes : " << nodes.size() << "\n"; std::cerr << "Hops required: " << opt.hops << "\n"; - uint64_t nodeCntr{0}, eqWrds{static_cast(((opt.numSamples - 1) / 64) + 1)}; + uint32_t nodeCntr{0}; + uint64_t eqWrds{static_cast(((opt.numSamples - 1) / 64) + 1)}; //std::vector> nodeEqs(nodes.size()); std::cerr << "initialized\n"; - for (auto &kv : nodes) { - auto &id = nodeCntr;//kv.first; - auto &neis = kv;//kv.second; + for (auto &neis : nodes) { std::unordered_set visited; - visited.insert(id); + visited.insert(nodeCntr); std::priority_queue hops; std::vector eq1(eqWrds); - buildColor(eqs, eq1, id, opt.numSamples); + buildColor(eqs, eq1, nodeCntr, opt.numSamples); - //std::cerr << "n " << id << " " << neis.size() << "\n"; + //std::cerr << "n " << nodeCntr << " " << neis.size() << "\n"; uint64_t maxNei{100}; if (neis.size() > maxNei) continue; for (auto &nei : neis) { @@ -309,7 +308,7 @@ int main(int argc, char *argv[]) { if (visited.find(neinei.first) == visited.end()) { visited.insert(neinei.first); Hop nh(neinei.first, neinei.second + nei.dist, nei.level + 1); - if (id < nh.id) { + if (nodeCntr < nh.id) { std::vector eq2(eqWrds); buildColor(eqs, eq2, nh.id, opt.numSamples); uint64_t directDist = 0;//hammingDist(eqs, id, nh.id, opt.numSamples); @@ -318,7 +317,7 @@ int main(int argc, char *argv[]) { directDist += sdsl::bits::cnt(eq1[i] ^ eq2[i]); } if (directDist < nh.dist) { - of << id << "\t" << nh.id << "\t" << directDist << "\n"; + of << nodeCntr << "\t" << nh.id << "\t" << directDist << "\n"; } } hops.push(nh); From f1da2f3544a1a40b46330534c5c9461923e35d39 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 31 Jul 2018 15:22:03 -0400 Subject: [PATCH 060/122] getting rid of the one-by-one erasing of laaaarge sets of neighbors --- src/MSF.cc | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index 4cc81e6..95d6228 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -107,8 +107,7 @@ int main(int argc, char *argv[]) { opt.numNodes++; // number of nodes is one more than input including the zero node std::cerr << "Loading all the equivalence classes first .. \n"; - std::vector> - eqs; + std::vector> eqs; eqs.reserve(20); loadEqs(opt.eqClsListFile, eqs); std::cerr << "Done loading list of equivalence class buckets\n"; @@ -149,12 +148,12 @@ int main(int argc, char *argv[]) { DisjointSets ds = g.kruskalMSF(opt.numSamples); std::queue q; - vector> p2c(opt.numNodes); - vector c2p(opt.numNodes); + std::vector degrees(g.mst.size()); for (auto e = 0; e < g.mst.size(); e++) { // put all the mst leaves into the queue - if (g.mst[e].size() == 1) { + degrees[e] = g.mst[e].size(); // size of list of neighbors of each node is the degree of the node + if (degrees[e] == 1) { q.push(e); // e is a leaf } } @@ -167,7 +166,7 @@ int main(int argc, char *argv[]) { while (!q.empty()) { uint32_t node = q.front(); q.pop(); - if (g.mst[node].size() == 0) { + if (degrees[node] == 0) { // this node is the root // and this should be the end of the loop // just as a validation, I'll continue and expect the while to end here @@ -204,22 +203,10 @@ int main(int argc, char *argv[]) { } else { parentbv[dest] = src; } - // erase the edge from src list of edges - g.mst[dest].erase(std::remove_if( - g.mst[dest].begin(), g.mst[dest].end(), - [buck, idx](const EdgePtr &x) { - return x.bucket == buck && x.idx == idx; - }), g.mst[dest].end()); - // erase the edge from dest list of edges - g.mst[src].erase(std::remove_if( - g.mst[src].begin(), g.mst[src].end(), - [buck, idx](const EdgePtr &x) { - return x.bucket == buck && x.idx == idx; - }), g.mst[src].end()); - // the destination has no edges left - // but if the src has turned to a leaf (node with one edge) - // add it to the queue - if (g.mst[src].size() == 1) { + // decrease the degrees for src and dest and add the src if it is now a leaf (no children left) + degrees[dest]--; + degrees[src]--; + if (degrees[src] == 1) { q.push(src); } nodeCntr++; // just a counter for the log @@ -283,6 +270,7 @@ int main(int argc, char *argv[]) { //std::vector> nodeEqs(nodes.size()); std::cerr << "initialized\n"; for (auto &neis : nodes) { + std::unordered_set immediateNeighbors; std::unordered_set visited; visited.insert(nodeCntr); std::priority_queue hops; @@ -293,7 +281,8 @@ int main(int argc, char *argv[]) { uint64_t maxNei{100}; if (neis.size() > maxNei) continue; for (auto &nei : neis) { - visited.insert(nei.first); + immediateNeighbors.insert(nei.first); + //visited.insert(nei.first); Hop nh(nei.first, nei.second, 1); hops.push(nh); } From b66a984399dfdfcf7a492eccdc31e852f4da5c53 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 31 Jul 2018 16:12:15 -0400 Subject: [PATCH 061/122] Improving the k-hop direct min distance detector algorithm --- src/MSF.cc | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index 95d6228..74e37f9 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -7,6 +7,7 @@ #include #include +#include #include "MSF.h" #include "sparsepp/spp.h" @@ -237,7 +238,7 @@ int main(int argc, char *argv[]) { } bbv[deltaOffset - 1] = 1; if (i % 10000000 == 0) { - std::cerr << i << " nodes parents processed\n"; + std::cerr << i << " nodes processed\n"; } } @@ -271,8 +272,8 @@ int main(int argc, char *argv[]) { std::cerr << "initialized\n"; for (auto &neis : nodes) { std::unordered_set immediateNeighbors; - std::unordered_set visited; - visited.insert(nodeCntr); + std::unordered_map visited; + immediateNeighbors.insert(nodeCntr); std::priority_queue hops; std::vector eq1(eqWrds); buildColor(eqs, eq1, nodeCntr, opt.numSamples); @@ -294,25 +295,31 @@ int main(int argc, char *argv[]) { if (nodes[nei.id].size() > maxNei) continue; for (auto &neinei : nodes[nei.id]) { //std::cerr << " " << neinei.first << "\n"; - if (visited.find(neinei.first) == visited.end()) { - visited.insert(neinei.first); + if (immediateNeighbors.find(neinei.first) == immediateNeighbors.end()) { Hop nh(neinei.first, neinei.second + nei.dist, nei.level + 1); - if (nodeCntr < nh.id) { - std::vector eq2(eqWrds); - buildColor(eqs, eq2, nh.id, opt.numSamples); - uint64_t directDist = 0;//hammingDist(eqs, id, nh.id, opt.numSamples); - for (uint64_t i = 0; i < eq1.size(); i++) { - if (eq1[i] != eq2[i]) - directDist += sdsl::bits::cnt(eq1[i] ^ eq2[i]); - } - if (directDist < nh.dist) { - of << nodeCntr << "\t" << nh.id << "\t" << directDist << "\n"; - } + if (visited.find(nh.id) == visited.end()) { + visited[nh.id] = nh.dist; + } else if (visited[nh.id] > nh.dist) { + visited[nh.id] = nh.dist; } hops.push(nh); } } } + for (auto &kv : visited) { + if (nodeCntr < kv.first) { + std::vector eq2(eqWrds); + buildColor(eqs, eq2, kv.first, opt.numSamples); + uint64_t directDist = 0;//hammingDist(eqs, id, nh.id, opt.numSamples); + for (uint64_t i = 0; i < eq1.size(); i++) { + if (eq1[i] != eq2[i]) + directDist += sdsl::bits::cnt(eq1[i] ^ eq2[i]); + } + if (directDist < kv.second) { + of << nodeCntr << "\t" << kv.first << "\t" << directDist << "\n"; + } + } + } nodeCntr++; if (nodeCntr % 1000000 == 0) { std::cerr << nodeCntr << " passed\n"; From 4714950b8f513e9c4fc0280652ef2c7b55c39786 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 31 Jul 2018 16:58:12 -0400 Subject: [PATCH 062/122] bug fix in MST constructor. Adding the bool vector --- src/MSF.cc | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/MSF.cc b/src/MSF.cc index 74e37f9..990fefd 100644 --- a/src/MSF.cc +++ b/src/MSF.cc @@ -151,6 +151,7 @@ int main(int argc, char *argv[]) { std::queue q; std::vector degrees(g.mst.size()); + std::vector visited(g.mst.size(), false); for (auto e = 0; e < g.mst.size(); e++) { // put all the mst leaves into the queue degrees[e] = g.mst[e].size(); // size of list of neighbors of each node is the degree of the node @@ -167,6 +168,7 @@ int main(int argc, char *argv[]) { while (!q.empty()) { uint32_t node = q.front(); q.pop(); + visited[node] = true; if (degrees[node] == 0) { // this node is the root // and this should be the end of the loop @@ -187,20 +189,26 @@ int main(int argc, char *argv[]) { } // what ever is in q (node) is a leaf and has only one edge left // fetch the pointer to that edge (bucket+idx) - auto buck = g.mst[node][0].bucket; - auto idx = g.mst[node][0].idx; // fetch the two ends of the edge and based on the node id decide which one is the src/dest // Destination is the one that represents current node which has been selected as leaf sooner than the other end - uint64_t dest = g.edges[buck][idx].n1; - uint64_t src = g.edges[buck][idx].n2; - if (src == node) { // swap src & dest since src is the leaf - std::swap(src, dest); + uint64_t src, dest, weight; + for (auto& n : g.mst[node]) { + auto &e = g.edges[n.bucket][n.idx]; + if (!visited[e.n1] or !visited[e.n2]) { + src = e.n1; + dest = e.n2; + if (!visited[e.n2]) { + std::swap(src, dest); + } + weight = e.weight; + break; + } } if (dest == zero) { // we break the tree from node zero parentbv[zero] = zero; // we're breaking the tree, so should remove the edge weight from total weights // But instead we're gonna spend one slot to store 0 as the delta of the zero node from zero node - g.mst_totalWeight = g.mst_totalWeight - g.edges[buck][idx].weight + 1; + g.mst_totalWeight = g.mst_totalWeight - weight + 1; } else { parentbv[dest] = src; } @@ -272,7 +280,6 @@ int main(int argc, char *argv[]) { std::cerr << "initialized\n"; for (auto &neis : nodes) { std::unordered_set immediateNeighbors; - std::unordered_map visited; immediateNeighbors.insert(nodeCntr); std::priority_queue hops; std::vector eq1(eqWrds); @@ -283,11 +290,11 @@ int main(int argc, char *argv[]) { if (neis.size() > maxNei) continue; for (auto &nei : neis) { immediateNeighbors.insert(nei.first); - //visited.insert(nei.first); Hop nh(nei.first, nei.second, 1); hops.push(nh); } + std::unordered_map visited; while (!hops.empty() && hops.top().level != opt.hops) { auto nei = hops.top(); hops.pop(); From 954b5b8a50b0dee35f5150ab9c14864336bdc929 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Wed, 1 Aug 2018 23:01:18 -0400 Subject: [PATCH 063/122] Adding walkCqf to the most recent/accurate/active branch --- src/walkCqf.cc | 182 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/walkCqf.cc diff --git a/src/walkCqf.cc b/src/walkCqf.cc new file mode 100644 index 0000000..7f6cd6a --- /dev/null +++ b/src/walkCqf.cc @@ -0,0 +1,182 @@ +/* + * =========================================================================== + * + * Filename: kmer_query.cc + * + * Description: + * + * Version: 1.0 + * Created: 04/27/2016 08:48:26 AM + * Revision: none + * Compiler: gcc + * + * Author: Prashant Pandey (), ppandey@cs.stonybrook.edu + * Organization: Stony Brook University + * + * =========================================================================== + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cqf.h" +#include "hashutil.h" +#include "kmer.h" + + +#define BITMASK(nbits) ((nbits) == 64 ? 0xffffffffffffffff : (1ULL << (nbits)) - 1ULL) + +/* Print elapsed time using the start and end timeval */ +void print_time_elapsed(string desc, struct timeval* start, struct timeval* end) +{ + struct timeval elapsed; + if (start->tv_usec > end->tv_usec) { + end->tv_usec += 1000000; + end->tv_sec--; + } + elapsed.tv_usec = end->tv_usec - start->tv_usec; + elapsed.tv_sec = end->tv_sec - start->tv_sec; + float time_elapsed = (elapsed.tv_sec * 1000000 + elapsed.tv_usec)/1000000.f; + std::cout << desc << "Total Time Elapsed: " << to_string(time_elapsed) << " seconds" << std::endl; +} + +void run_filter(std::string ds_file, + std::string out_file, + uint64_t cutoff, + uint64_t approximate_num_of_kmers_greater_than_cutoff) { + struct timeval start, end; + struct timezone tzp; + + //Initialize the QF + gettimeofday(&start, &tzp); + cout << "Reading the input CQF off disk" << std::endl; + CQF cqf(ds_file, false); + cout << "Done loading cqf in time: "; + gettimeofday(&end, &tzp); + print_time_elapsed("", &start, &end); + typename CQF::Iterator it = cqf.begin(0); + + uint64_t quotientBits = std::ceil(std::log2(approximate_num_of_kmers_greater_than_cutoff))+1; + std::cout << "quotientBits : " << quotientBits << "\n"; + std::cout << "keybits : " << cqf.keybits() << "\n"; + CQF newCqf(quotientBits, cqf.keybits(), cqf.seed()); + gettimeofday(&start, &tzp); + uint64_t cntr = 1; + uint64_t insertedCntr = 0; + while (!it.done()) { + KeyObject k = *it; + if (k.count >= cutoff) { + k.count = 1;//cutoff; + newCqf.insert(k); + insertedCntr++; + } + ++it; + if (cntr % 10000000 == 0) { + std::cout << cntr << " kmers passed, " + << insertedCntr << " kmers inserted, " + << newCqf.noccupied_slots() << " slots occupied, " + << " load factor: " + << static_cast(newCqf.noccupied_slots())/static_cast(1ULL << quotientBits) << "\n"; + } + cntr++; + } + newCqf.serialize(out_file); + gettimeofday(&end, &tzp); + print_time_elapsed("", &start, &end); + +} + +void run_list_kmers(std::string ds_file, + std::string out_file) { + struct timeval start, end; + struct timezone tzp; + + //Initialize the QF + gettimeofday(&start, &tzp); + cout << "Reading the input CQF off disk" << std::endl; + CQF cqf(ds_file, false); + cout << "Done loading cqf in time: "; + gettimeofday(&end, &tzp); + print_time_elapsed("", &start, &end); + typename CQF::Iterator it = cqf.begin(0); + + gettimeofday(&start, &tzp); + std::ofstream fout(out_file, ios::out); + while (!it.done()) { + KeyObject k = *it; + // kmers.push_back(HashUtil::hash_64i(k.key, BITMASK(cqf.keybits()))); + uint64_t kint = HashUtil::hash_64i(k.key, BITMASK(cqf.keybits())); + //if (k.count >= cutoff) { + std::string kstr = Kmer::int_to_str(kint); + fout << kstr << "\t" << k.count << "\n"; + //} + ++it; + } + fout.close(); + gettimeofday(&end, &tzp); + print_time_elapsed("", &start, &end); +} + +/* + * === FUNCTION ============================================================ + * Name: main + * Description: + * =========================================================================== + */ + +int main ( int argc, char *argv[] ) +{ + std::cout << argc << " ....... \n"; + std::string command = argv[1]; + if (command != "filter" and command != "list_kmers") { + std::cerr << "ERROR: command can only be filter or list_kmers.\n"; + exit(1); + } + + std::string ds_file = argv[2]; + cout << ds_file << "\n"; + std::string out_file = argv[3]; + cout << out_file << "\n"; + uint64_t cutoff = 0; + uint64_t approximate_num_of_kmers_greater_than_cutoff = 0; + if (command == "filter") { + if (argc < 6) { + std::cerr << "ERROR: missing last argument for filter command\n"; + exit(1); + } + cutoff = stoi(argv[4]); + cout << cutoff << "\n"; + cout << argv[5] << "\n"; + approximate_num_of_kmers_greater_than_cutoff = stoull(argv[5]); + } + + if (command == "filter") { + run_filter(ds_file, out_file, cutoff, approximate_num_of_kmers_greater_than_cutoff); + } + else if (command == "listKmers") { + run_list_kmers(ds_file, out_file); + } + + return EXIT_SUCCESS; +} /* ---------- end of function main ---------- */ + From f750935e4739153ceb865ddda19a1475ae364b26 Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Thu, 2 Aug 2018 01:59:18 -0400 Subject: [PATCH 064/122] remove confusing BitVector and BitVectorRRR classes --- include/bitvector.h | 87 -------------------------------------------- include/coloreddbg.h | 30 ++++++++------- src/CMakeLists.txt | 1 - src/bitvector.cc | 43 ---------------------- 4 files changed, 17 insertions(+), 144 deletions(-) delete mode 100644 include/bitvector.h delete mode 100644 src/bitvector.cc diff --git a/include/bitvector.h b/include/bitvector.h deleted file mode 100644 index 67728a2..0000000 --- a/include/bitvector.h +++ /dev/null @@ -1,87 +0,0 @@ -/* - * ============================================================================ - * - * Filename: bitvector.h - * - * Description: - * - * Version: 1.0 - * Created: 2017-10-25 01:59:49 PM - * Revision: none - * Compiler: gcc - * - * Author: Prashant Pandey (), ppandey@cs.stonybrook.edu - * Organization: Stony Brook University - * - * ============================================================================ - */ - -#ifndef _BIT_VECTOR_H_ -#define _BIT_VECTOR_H_ - -#include -#include - -#include - -#include "util.h" -#include "hashutil.h" -#include "sdsl/bit_vectors.hpp" - -class BitVector { - public: - BitVector() : bits(), size(0) {}; - BitVector(uint64_t size); - BitVector(const BitVector& bv) : bits(bv.bits), size(bv.size) - {}; - - sdsl::bit_vector get_bits() const { - return bits; - } - - void reset(); - bool operator[](uint64_t idx); - void set(const uint64_t idx); - uint64_t capacity(void) const { return bits.capacity() / 8; } - uint64_t bit_size(void) const { return bits.bit_size(); } - const uint64_t *data(void) const { return bits.data(); } - void resize(const uint64_t len); - uint64_t get_int(uint64_t startP, uint64_t len=64) {return bits.get_int(startP, len);} - bool operator==(const BitVector& b) const { return bits == b.bits; } - - private: - sdsl::bit_vector bits; - uint64_t size; -}; - -class BitVectorRRR { - public: - BitVectorRRR() : rrr_bits(), size(0) {}; - BitVectorRRR(const BitVector& bv) : rrr_bits(bv.get_bits()), - size(bv.bit_size()) {}; - BitVectorRRR(std::string& filename); - - bool operator[](uint64_t idx); - bool serialize(std::string& filename); - uint64_t bit_size(void) const { return rrr_bits.size(); } - uint64_t get_int(uint64_t startP, uint64_t len=64) { - return rrr_bits.get_int(startP, len); - } - - private: - sdsl::rrr_vector<63> rrr_bits; - uint64_t size; -}; - -template -struct sdslhash { - uint64_t operator()(const T& vector) const - { - // Using the same seed as we use in k-mer hashing. - return HashUtil::MurmurHash64A((void*)vector.data(), vector.capacity(), - 2038074743); - } -}; - -#endif - diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 8b9f1a4..d5399b2 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -31,12 +31,14 @@ #include "sparsepp/spp.h" #include "tsl/sparse_map.h" #include "sdsl/bit_vectors.hpp" -#include "bitvector.h" #include "cqf.h" #include "hashutil.h" #include "common_types.h" #include "mantisconfig.hpp" +typedef sdsl::bit_vector BitVector; +typedef sdsl::rrr_vector<63> BitVectorRRR; + struct hash128 { uint64_t operator()(const __uint128_t& val128) const { @@ -86,7 +88,7 @@ class ColoredDbg { // returns true if adding this k-mer increased the number of equivalence classes // and false otherwise. bool add_kmer(key_obj& hash, BitVector& vector); - void add_bitvector(BitVector& vector, uint64_t eq_id); + void add_bitvector(const BitVector& vector, uint64_t eq_id); void add_eq_class(BitVector vector, uint64_t id); uint64_t get_next_available_id(void); void bv_buffer_serialize(); @@ -148,7 +150,7 @@ template uint64_t ColoredDbg::get_num_bitvectors(void) const { uint64_t total = 0; for (uint32_t i = 0; i < num_serializations; i++) - total += eqclasses[i].bit_size(); + total += eqclasses[i].size(); return total / num_samples; } @@ -170,7 +172,7 @@ void ColoredDbg::add_kmer(key_obj& k, BitVector& vector) { // A kmer (hash) is seen only once during the merge process. // So we insert every kmer in the dbg - uint64_t eq_id = 1; + uint64_t eq_id; __uint128_t vec_hash = HashUtil::MurmurHash128A((void*)vector.data(), - vector.capacity(), 2038074743, + vector.capacity()/8, 2038074743, 2038074751); auto it = eqclass_map.find(vec_hash); @@ -227,12 +229,12 @@ bool ColoredDbg::add_kmer(key_obj& k, BitVector& } template -void ColoredDbg::add_bitvector(BitVector& vector, uint64_t +void ColoredDbg::add_bitvector(const BitVector& vector, uint64_t eq_id) { uint64_t start_idx = (eq_id % mantis::NUM_BV_BUFFER) * num_samples; for (uint32_t i = 0; i < num_samples; i++, start_idx++) if (vector[i]) - bv_buffer.set(start_idx); + bv_buffer[start_idx] = 1; } template @@ -245,8 +247,8 @@ void ColoredDbg::bv_buffer_serialize() { BitVectorRRR final_com_bv(bv_temp); std::string bv_file(prefix + std::to_string(num_serializations) + "_" + mantis::EQCLASS_FILE); - final_com_bv.serialize(bv_file); - bv_buffer.reset(); + sdsl::store_to_file(final_com_bv, bv_file); + bv_buffer = BitVector(bv_buffer.bit_size()); num_serializations++; } @@ -343,14 +345,14 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg::ColoredDbg(std::string& cqf_file, } eqclasses.reserve(sorted_files.size()); + BitVectorRRR bv; for (auto file : sorted_files) { - eqclasses.push_back(BitVectorRRR(file.second)); + sdsl::load_from_file(bv, file.second); + eqclasses.push_back(bv); num_serializations++; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 44ae727..c651923 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,7 +6,6 @@ add_library(mantis_core STATIC hashutil.cc query.cc util.cc - bitvector.cc validatemantis.cc coloreddbg.cc MantisFS.cc diff --git a/src/bitvector.cc b/src/bitvector.cc deleted file mode 100644 index 22c10d5..0000000 --- a/src/bitvector.cc +++ /dev/null @@ -1,43 +0,0 @@ -#include "bitvector.h" - -BitVector::BitVector(uint64_t size) : size(size) { - bits = sdsl::bit_vector(size); -} - -void BitVector::reset() { - bits = sdsl::bit_vector(size); -} - -bool BitVector::operator[](uint64_t idx) { - assert(idx < size); - return bits[idx]; -} - -void BitVector::set(uint64_t idx) { - assert(idx < size); - bits[idx] = 1; -} - -void BitVector::resize(const uint64_t len) { - bits.bit_resize(len); - size = len; -} - -BitVectorRRR::BitVectorRRR(std::string& filename) { - sdsl::load_from_file(rrr_bits, filename); - size = rrr_bits.size(); - DEBUG_CDBG("Read rrr bit vector of size " << size << " from file " << - filename); -} - -bool BitVectorRRR::operator[](uint64_t idx) { - assert(idx < size); - return rrr_bits[idx]; -} - -bool BitVectorRRR::serialize(std::string& filename) { - DEBUG_CDBG("Serializing rrr bit vector of size " << size << " to file " << - filename); - return sdsl::store_to_file(rrr_bits, filename); -} - From 6e60e09e443ddf8e329cd7d10638b33ffbcc58f7 Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Thu, 2 Aug 2018 03:03:54 -0400 Subject: [PATCH 065/122] simplify Iterator, replace PQ implementation with decrease_top, simplify core logic --- include/coloreddbg.h | 96 ++++++++--------- include/cqf.h | 241 ++++++------------------------------------- 2 files changed, 73 insertions(+), 264 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index d5399b2..0b9670c 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -39,6 +39,9 @@ typedef sdsl::bit_vector BitVector; typedef sdsl::rrr_vector<63> BitVectorRRR; +template std::vector CQF::qfi; +template std::vector CQF::keys; + struct hash128 { uint64_t operator()(const __uint128_t& val128) const { @@ -87,7 +90,7 @@ class ColoredDbg { private: // returns true if adding this k-mer increased the number of equivalence classes // and false otherwise. - bool add_kmer(key_obj& hash, BitVector& vector); + bool add_kmer(const typename key_obj::kmer_t& hash, const BitVector& vector); void add_bitvector(const BitVector& vector, uint64_t eq_id); void add_eq_class(BitVector vector, uint64_t id); uint64_t get_next_available_id(void); @@ -195,7 +198,7 @@ void ColoredDbg::reinit(cdbg_bv_map_t<__uint128_t, } template -bool ColoredDbg::add_kmer(key_obj& k, BitVector& +bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, const BitVector& vector) { // A kmer (hash) is seen only once during the merge process. // So we insert every kmer in the dbg @@ -223,8 +226,7 @@ bool ColoredDbg::add_kmer(key_obj& k, BitVector& it->second.second += 1; // update the abundance. } - k.count = eq_id; // we use the count to store the eqclass ids - dbg.insert(k); + dbg.insert(KeyObject(key,0,eq_id)); // we use the count to store the eqclass ids return added_eq_class; } @@ -314,65 +316,53 @@ template cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg::construct(qf_obj *incqfs, uint64_t num_kmers) { - uint32_t nqf = 0; uint64_t counter = 0; bool is_sampling = (num_kmers < std::numeric_limits::max()); - // merge all input CQFs into the final QF - std::vector::Iterator> it_incqfs; - it_incqfs.reserve(num_samples); - // Initialize all iterators with sample specific cutoffs. - for (uint32_t i = 0; i < num_samples; i++) { - it_incqfs.emplace_back(incqfs[i].obj->begin(incqfs[i].cutoff)); - } + struct Minheap_PQ { + void push(const typename CQF::Iterator& obj) { + c.emplace_back(obj); + std::push_heap(c.begin(), c.end(), std::greater::Iterator>()); + } + void pop() { + std::pop_heap(c.begin(), c.end(), std::greater::Iterator>()); + c.pop_back(); + } + void replace_top(const typename CQF::Iterator& obj) { + c.emplace_back(obj); + pop(); + } + typename CQF::Iterator& top() { return c.front(); } + bool empty() const { return c.empty(); } + private: + std::vector::Iterator> c; + }; + Minheap_PQ minheap; - std::priority_queue, - std::vector>, compare> minheap; - // Insert the first key from each CQF in minheap. for (uint32_t i = 0; i < num_samples; i++) { - if (it_incqfs[i].done()) - continue; - KeyObject key = *it_incqfs[i]; - minheap.emplace(key, incqfs[i].cutoff, incqfs[i].sample_id, i); - nqf++; - } + typename CQF::Iterator qfi(i, *incqfs[i].obj); + if (qfi.end()) continue; + minheap.push(qfi); + } while (!minheap.empty()) { - assert(minheap.size() == nqf); - SampleObject cur; BitVector eq_class(num_samples); - // Get the smallest key from minheap and update the eqclass vector - cur = minheap.top(); - eq_class[cur.id] = 1; - minheap.pop(); - // Keep poping keys from minheap until you see a different key. - // While poping keys build the eq class for cur. - // Increment iterators for all CQFs whose keys are popped. - while (!minheap.empty() && cur.obj.key == minheap.top().obj.key) { - uint32_t id = minheap.top().id; - eq_class[id] = 1; - minheap.pop(); - ++it_incqfs[id]; - if (it_incqfs[id].done()) // If the iterator is done then decrement nqf - nqf--; - else { // Insert the current iterator head in minHeap - KeyObject key = *it_incqfs[id]; - minheap.emplace(key, incqfs[id].cutoff, incqfs[id].sample_id, id); - } - } - // Move the iterator of the smallest key. - ++it_incqfs[cur.id]; - if (it_incqfs[cur.id].done()) // If the iterator is done then decrement nqf - nqf--; - else { // Insert the current iterator head in minHeap - KeyObject key = *it_incqfs[cur.id]; - minheap.emplace(key, incqfs[cur.id].cutoff, incqfs[cur.id].sample_id, cur.id); - } - // Add in the cdbg - bool added_eq_class = add_kmer(cur.obj, eq_class); - counter++; + KeyObject::kmer_t last_key; + do { + typename CQF::Iterator& cur = minheap.top(); + last_key = cur.key(); + eq_class[cur.id] = 1; + if (cur.advance()) + minheap.replace_top(cur); + else + minheap.pop(); + } while(!minheap.empty() && last_key == minheap.top().key()); + + bool added_eq_class = add_kmer(last_key, eq_class); + ++counter; + if (dbg.size() > 75000000) { console->info("exiting for quick profile run"); break; } // Progress tracker static uint64_t last_size = 0; diff --git a/include/cqf.h b/include/cqf.h index 39fccec..ea676be 100644 --- a/include/cqf.h +++ b/include/cqf.h @@ -69,34 +69,38 @@ class CQF { void drop_pages(uint64_t cur); - class Iterator { - public: - Iterator(QFi it, uint32_t cutoff, uint64_t end_hash); - ~Iterator(); - key_obj operator*(void) const; - void operator++(void); - bool done(void) const; - - QFi iter; - int64_t last_prefetch_offset; - uint64_t buffer_size; - private: - /* global buffer to perform read ahead */ - unsigned char *buffer; - uint32_t num_pages; - uint32_t cutoff; - uint64_t end_hash; - struct aiocb aiocb; + static std::vector qfi; + static std::vector keys; + struct Iterator { + Iterator(uint32_t id, const CQF& cqf): id(id) { + if (qfi.size() <= id) qfi.resize(id*2+1), keys.resize(id*2+1); + if (qf_iterator(&cqf.cqf, &qfi[id], 0)) get_key(); + } + bool advance() { + if (qfi_next(&qfi[id])) return false; + get_key(); + return true; + } + bool end() const { + return qfi_end(&qfi[id]); + } + uint32_t id; + bool operator>(const Iterator& rhs) const { + return key() > rhs.key(); + } + const typename key_obj::kmer_t& key() const { + return keys[id]; + } + private: + void get_key() { + uint64_t value, count; + qfi_get(&qfi[id], &keys[id], &value, &count); + } }; - - Iterator limits(uint64_t start_hash, uint64_t end_hash, uint32_t cutoff) - const; - Iterator begin(uint32_t cutoff) const; - Iterator end(uint32_t cutoff) const; + friend class Iterator; private: QF cqf; - //std::unordered_set set; }; class KeyObject { @@ -110,7 +114,8 @@ class KeyObject { bool operator==(KeyObject k) { return key == k.key; } - uint64_t key; + typedef uint64_t kmer_t; + kmer_t key; uint64_t value; uint64_t count; }; @@ -155,190 +160,4 @@ uint64_t CQF::get_index(const key_obj& k) { return get_bucket_index(k.key); } -template -CQF::Iterator::Iterator(QFi it, uint32_t cutoff, uint64_t end_hash) - : iter(it), last_prefetch_offset(LLONG_MIN), cutoff(cutoff), - end_hash(end_hash) { - buffer_size = (((it.qf->metadata->size / 2048 - - (rand() % (it.qf->metadata->size / 4096))) - + 4095) / 4096) * 4096; - buffer = (unsigned char*)mmap(NULL, buffer_size, PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - if (buffer == MAP_FAILED) { - perror("buffer malloc"); - std::cerr << "Can't allocate buffer space." << std::endl; - exit(1); - } - }; - -template -CQF::Iterator::~Iterator(void) { - struct aiocb *aio_list[1]; - aio_list[0] = &aiocb; - int ret = aio_suspend(aio_list, 1, 0); - if (ret < 0) { - perror("aio_suspend"); - exit(1); - } -} - -template -key_obj CQF::Iterator::operator*(void) const { - uint64_t key = 0, value = 0, count = 0; - qfi_get(&iter, &key, &value, &count); - return key_obj(key, value, count); -} - -// This function read one byte from each page in the iterator buffer. -//void handler_function(union sigval sv); - -template -void CQF::Iterator::operator++(void) { - uint64_t last_read_offset; - qfi_nextx(&iter, &last_read_offset); - - // Read next "buffer_size" bytes from the file offset. - if ((int64_t)last_read_offset >= last_prefetch_offset) { - //DEBUG_CDBG("last_read_offset>last_prefetch_offset for " << iter.qf->mem->fd - //<< " " << last_read_offset << ">" << last_prefetch_offset); - if (aiocb.aio_buf) { - int res = aio_error(&aiocb); - if (res == EINPROGRESS) { - //DEBUG_CDBG("didn't read fast enough for " << aiocb.aio_fildes << - //" at " << last_read_offset << "(until " << last_prefetch_offset << - //" buffer size: "<< buffer_size << ")..."); - const struct aiocb *const aiocb_list[1] = {&aiocb}; - aio_suspend(aiocb_list, 1, NULL); - //DEBUG_CDBG(" finished it"); - } else if (res > 0) { - //DEBUG_CDBG("aio_error() returned " << std::dec << res); - } else if (res == 0) { - //DEBUG_CDBG("prefetch was OK for " << aiocb.aio_fildes << " at " << - //std::hex << aiocb.aio_offset << std::dec); - } else if (res == 0) { - unsigned char *start = (unsigned char*)(iter.qf->metadata) + - last_prefetch_offset; - unsigned char *counter = (unsigned char*)(iter.qf->metadata) + - last_prefetch_offset; - for (;counter < start + buffer_size; counter += 4096) { - tmp_sum_local += *counter; - } - } - } - - if ((last_prefetch_offset - (int64_t)buffer_size) > 0) { - madvise((unsigned char *)(iter.qf->metadata) + last_prefetch_offset - - buffer_size, buffer_size, MADV_DONTNEED); - posix_fadvise(iter.qf->mem->fd, (off_t)(last_prefetch_offset - - (int64_t)buffer_size), - buffer_size, POSIX_FADV_DONTNEED); - } - - memset(&aiocb, 0, sizeof(struct aiocb)); - aiocb.aio_fildes = iter.qf->mem->fd; - aiocb.aio_buf = (volatile void*)buffer; - aiocb.aio_nbytes = buffer_size; - if ((last_prefetch_offset + (int64_t)buffer_size) < - (int64_t)last_read_offset) { - if (last_prefetch_offset != 0) - //DEBUG_CDBG("resetting.. lpo:" << last_prefetch_offset << " lro:" << - //last_read_offset); - last_prefetch_offset = ( last_read_offset & ~(4095ULL) ) + - PAGE_BUFFER_SIZE; - } else { - last_prefetch_offset += buffer_size; - } - aiocb.aio_offset = (__off_t)last_prefetch_offset; - //DEBUG_CDBG("prefetch in " << iter.qf->mem->fd << " from " << std::hex << - //last_prefetch_offset << std::dec << " ... " << " buffer size: " - //<< buffer_size << " into buffer at " << std::hex << - //((uint64_t)buffer) << std::dec); - // to touch each page in the buffer. - //aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD; - //aiocb.aio_sigevent.sigev_notify_function = &handler_function; - //aiocb.aio_sigevent.sigev_value.sival_ptr = (void*)this; - - uint32_t ret = aio_read(&aiocb); - //uint32_t ret = posix_fadvise(iter.qf->mem->fd, last_read_offset, - //buffer_size, POSIX_FADV_WILLNEED); - //DEBUG_CDBG("prefetch issued"); - if (ret) { - std::cerr << "aio_read failed at " << iter.current << " total size " << - iter.qf->metadata->nslots << std::endl; - perror("aio_read"); - } - } - - // Skip past the cutoff - do { - uint64_t key = 0, value = 0, count = 0; - qfi_get(&iter, &key, &value, &count); - if (count < cutoff) - qfi_next(&iter); - else - break; - } while(!qfi_end(&iter)); - - // drop pages of the last million slots. - //static uint64_t last_marker = 1; - //if (iter.current / PAGE_DROP_GRANULARITY > last_marker + 1) { - //uint64_t start_idx = last_marker * PAGE_DROP_GRANULARITY; - //uint64_t end_idx = (last_marker + 1) * PAGE_DROP_GRANULARITY; - //qf_drop_pages(iter.qf, start_idx, end_idx); - //last_marker += 1; - //} -} - -/* Currently, the iterator only traverses forward. So, we only need to check - * the right side limit. - */ -template -bool CQF::Iterator::done(void) const { - uint64_t key = 0, value = 0, count = 0; - qfi_get(&iter, &key, &value, &count); - return key >= end_hash || qfi_end(&iter); -} - -template -typename CQF::Iterator CQF::begin(uint32_t cutoff) const { - QFi qfi; - qf_iterator(&this->cqf, &qfi, 0); - // Skip past the cutoff - do { - uint64_t key = 0, value = 0, count = 0; - qfi_get(&qfi, &key, &value, &count); - if (count < cutoff) - qfi_next(&qfi); - else - break; - } while(!qfi_end(&qfi)); - - return Iterator(qfi, cutoff, UINT64_MAX); -} - -template -typename CQF::Iterator CQF::end(uint32_t cutoff) const { - QFi qfi; - qf_iterator(&this->cqf, &qfi, 0xffffffffffffffff); - return Iterator(qfi, cutoff, UINT64_MAX); -} - -template -typename CQF::Iterator CQF::limits(uint64_t start_hash, - uint64_t end_hash, - uint32_t cutoff) const { - QFi qfi; - qf_iterator_hash(&this->cqf, &qfi, start_hash); - // Skip past the cutoff - do { - uint64_t key = 0, value = 0, count = 0; - qfi_get(&qfi, &key, &value, &count); - if (count < cutoff) - qfi_next(&qfi); - else - break; - } while(!qfi_end(&qfi)); - - return Iterator(qfi, cutoff, end_hash); -} #endif From b967527510a288f4b2e7f5c61a61e3a6bfb6d7e7 Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Thu, 2 Aug 2018 03:06:41 -0400 Subject: [PATCH 066/122] remove cutoff --- include/coloreddbg.h | 11 +++++------ src/coloreddbg.cc | 7 +++---- src/validatemantis.cc | 25 ++++++------------------- 3 files changed, 14 insertions(+), 29 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 0b9670c..9825f54 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -115,14 +115,13 @@ class ColoredDbg { template class SampleObject { public: - SampleObject() : obj(), cutoff(0), sample_id(), id(0) {}; - SampleObject(T o, uint32_t c = 0, std::string& s = std::string(), - uint32_t id = 0) : obj(o), cutoff(c), sample_id(s), id(id) {}; - SampleObject(const SampleObject& o) : obj(o.obj), cutoff(o.cutoff), - sample_id(o.sample_id), id(o.id) {} ; + SampleObject() : obj(), sample_id(), id(0) {} + SampleObject(T o, std::string& s = std::string(), + uint32_t id = 0) : obj(o), sample_id(s), id(id) {} + SampleObject(const SampleObject& o) : obj(o.obj), + sample_id(o.sample_id), id(o.id) {} T obj; - uint32_t cutoff; std::string sample_id; uint32_t id; }; diff --git a/src/coloreddbg.cc b/src/coloreddbg.cc index 1701b78..c759581 100644 --- a/src/coloreddbg.cc +++ b/src/coloreddbg.cc @@ -144,9 +144,8 @@ build_main ( BuildOpts& opt ) // mmap all the input cqfs std::string cqf_file; uint32_t nqf = 0; - uint32_t cutoff; console->info("Reading input Squeakr files."); - while (infile >> cqf_file >> cutoff) { + while (infile >> cqf_file) { if (!mantis::fs::FileExists(cqf_file.c_str())) { console->error("Squeakr file {} does not exist.", cqf_file); exit(1); @@ -155,9 +154,9 @@ build_main ( BuildOpts& opt ) std::string sample_id = first_part(first_part(last_part(cqf_file, '/'), '.'), '_'); console->info("Reading CQF {} Seed {}",nqf, cqfs[nqf].seed()); - console->info("Sample id {} cut off {}", sample_id, cutoff); + console->info("Sample id {} cut off {}", sample_id); cqfs.back().dump_metadata(); - inobjects.emplace_back(&cqfs[nqf], cutoff, sample_id, nqf); + inobjects.emplace_back(&cqfs[nqf], sample_id, nqf); nqf++; } diff --git a/src/validatemantis.cc b/src/validatemantis.cc index 6aa2ddd..dc47321 100644 --- a/src/validatemantis.cc +++ b/src/validatemantis.cc @@ -61,7 +61,7 @@ validate_main ( ValidateOpts& opt ) { spdlog::logger* console = opt.console.get(); - // Read experiment CQFs and cutoffs + // Read experiment CQFs std::ifstream infile(opt.inlist); uint64_t num_samples{0}; if (infile.is_open()) { @@ -82,19 +82,10 @@ validate_main ( ValidateOpts& opt ) cqfs = (CQF*)calloc(num_samples, sizeof(CQF)); - // Read cutoffs files - //std::unordered_map cutoffs; - //std::string sample_id; - //while (cutofffile >> sample_id >> cutoff) { - //std::pair pair(last_part(sample_id, '/'), cutoff); - //cutoffs.insert(pair); - //} - // mmap all the input cqfs std::string cqf_file; uint32_t nqf = 0; - uint32_t cutoff; - while (infile >> cqf_file >> cutoff) { + while (infile >> cqf_file) { if (!mantis::fs::FileExists(cqf_file.c_str())) { console->error("Squeakr file {} does not exist.", cqf_file); exit(1); @@ -103,9 +94,9 @@ validate_main ( ValidateOpts& opt ) std::string sample_id = first_part(first_part(last_part(cqf_file, '/'), '.'), '_'); console->info("Reading CQF {} Seed {}", nqf, cqfs[nqf].seed()); - console->info("Sample id {} cut-off {}", sample_id, cutoff); + console->info("Sample id {} cut-off {}", sample_id); cqfs[nqf].dump_metadata(); - inobjects[nqf] = SampleObject*>(&cqfs[nqf], cutoff, + inobjects[nqf] = SampleObject*>(&cqfs[nqf], sample_id, nqf); nqf++; } @@ -145,7 +136,7 @@ validate_main ( ValidateOpts& opt ) total_kmers); console->info("Total k-mers to query: {}", total_kmers); - // Query kmers in each experiment CQF ignoring kmers below the cutoff. + // Query kmers in each experiment CQF // Maintain the fraction of kmers present in each experiment CQF. std::vector> ground_truth; std::vector> cdbg_output; @@ -153,14 +144,10 @@ validate_main ( ValidateOpts& opt ) for (auto kmers : multi_kmers) { std::unordered_map fraction_present; for (uint64_t i = 0; i < nqf; i++) { - uint32_t cutoff = inobjects[i].cutoff; for (auto kmer : kmers) { KeyObject k(kmer, 0, 0); uint64_t count = cqfs[i].query(k); - if (count < cutoff) - continue; - else - fraction_present[inobjects[i].id] += 1; + fraction_present[inobjects[i].id] += 1; } } // Query kmers in the cdbg From 55ef403b45066e9794c1f1922961717816b0ca65 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Thu, 2 Aug 2018 17:51:09 -0400 Subject: [PATCH 067/122] Adding walkCqf target to the CMake file but commented it! --- src/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 425581a..879aaea 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -93,6 +93,13 @@ add_executable(mstBoost MST_boost.cc) target_link_libraries(mstBoost mantis_core) set_property(TARGET mstBoost PROPERTY INTERPROCEDURAL_OPTIMIZATION True) +#add_executable(walkCqf walkCqf.cc) +#target_include_directories(walkCqf PUBLIC +# $) +#target_link_libraries(walkCqf +# mantis_core) +#set_property(TARGET walkCqf PROPERTY INTERPROCEDURAL_OPTIMIZATION True) + #add_executable(walkEqcls walkEqcls.cc) #target_include_directories(walkEqcls PUBLIC # $ $) From a3d96a19b57ddd1e2212c03e8bc118242130e67f Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Thu, 2 Aug 2018 21:04:51 -0400 Subject: [PATCH 068/122] adjust logic to avoid using result of qfi_next (returned 1 on 'last' element) --- include/coloreddbg.h | 16 ++++++---------- include/cqf.h | 25 ++++++++++--------------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 9825f54..a5755c9 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -39,9 +39,6 @@ typedef sdsl::bit_vector BitVector; typedef sdsl::rrr_vector<63> BitVectorRRR; -template std::vector CQF::qfi; -template std::vector CQF::keys; - struct hash128 { uint64_t operator()(const __uint128_t& val128) const { @@ -351,17 +348,14 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg::Iterator& cur = minheap.top(); - last_key = cur.key(); + last_key = cur.key.key; eq_class[cur.id] = 1; - if (cur.advance()) - minheap.replace_top(cur); - else - minheap.pop(); - } while(!minheap.empty() && last_key == minheap.top().key()); + cur.next(); + minheap.replace_top(cur); + } while(!minheap.empty() && last_key == minheap.top().key.key); bool added_eq_class = add_kmer(last_key, eq_class); ++counter; - if (dbg.size() > 75000000) { console->info("exiting for quick profile run"); break; } // Progress tracker static uint64_t last_size = 0; @@ -387,6 +381,8 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg qfi; - static std::vector keys; struct Iterator { + QFi qfi; + key_obj key; + uint32_t id; Iterator(uint32_t id, const CQF& cqf): id(id) { - if (qfi.size() <= id) qfi.resize(id*2+1), keys.resize(id*2+1); - if (qf_iterator(&cqf.cqf, &qfi[id], 0)) get_key(); + if (qf_iterator(&cqf.cqf, &qfi, 0)) get_key(); } - bool advance() { - if (qfi_next(&qfi[id])) return false; + void next() { + qfi_next(&qfi); get_key(); - return true; } bool end() const { - return qfi_end(&qfi[id]); + return qfi_end(&qfi); } - uint32_t id; bool operator>(const Iterator& rhs) const { - return key() > rhs.key(); - } - const typename key_obj::kmer_t& key() const { - return keys[id]; + return key.key > rhs.key.key; } private: void get_key() { - uint64_t value, count; - qfi_get(&qfi[id], &keys[id], &value, &count); + //uint64_t value, count; + qfi_get(&qfi, &key.key, &key.value, &key.count); } }; friend class Iterator; From fb1bc1909aa53458029c42500f405d3eebf6337b Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Thu, 2 Aug 2018 22:50:17 -0400 Subject: [PATCH 069/122] compacting for readability --- include/cqf.h | 92 ++++++++++----------------------------------------- 1 file changed, 18 insertions(+), 74 deletions(-) diff --git a/include/cqf.h b/include/cqf.h index be23fca..66cbc0d 100644 --- a/include/cqf.h +++ b/include/cqf.h @@ -41,21 +41,28 @@ static uint64_t tmp_sum_local; template class CQF { public: - CQF(); - CQF(uint64_t qbits, uint64_t key_bits, uint32_t seed); - CQF(std::string& filename, bool flag); - CQF(const CQF& copy_cqf); + CQF() { qf_init(&cqf, 1ULL << NUM_Q_BITS, NUM_HASH_BITS, 0, true, "", 23423); } + CQF(uint64_t qbits, uint64_t key_bits, uint32_t seed) { qf_init(&cqf, 1ULL << qbits, key_bits, 0, true, "", seed); } + CQF(const CQF& copy_cqf) { memcpy(reinterpret_cast(&cqf), reinterpret_cast(const_cast(©_cqf.cqf)), sizeof(QF)); } + CQF(std::string& filename, bool flag) { + if (flag) + qf_read(&cqf, filename.c_str()); + else + qf_deserialize(&cqf, filename.c_str()); + } - void insert(const key_obj& k); + void insert(const key_obj& k) { + qf_insert(&cqf, k.key, k.value, k.count, LOCK_AND_SPIN); + // To validate the CQF + //set.insert(k.key); + } /* Will return the count. */ - uint64_t query(const key_obj& k); + uint64_t query(const key_obj& k) { return qf_count_key_value(&cqf, k.key, k.value); } - uint64_t get_index(const key_obj& k); + uint64_t get_index(const key_obj& k) { return get_bucket_index(k.key); } - void serialize(std::string filename) { - qf_serialize(&cqf, filename.c_str()); - } + void serialize(std::string filename) { qf_serialize(&cqf, filename.c_str()); } uint64_t range(void) const { return cqf.metadata->range; } uint32_t seed(void) const { return cqf.metadata->seed; } @@ -69,30 +76,7 @@ class CQF { void drop_pages(uint64_t cur); - struct Iterator { - QFi qfi; - key_obj key; - uint32_t id; - Iterator(uint32_t id, const CQF& cqf): id(id) { - if (qf_iterator(&cqf.cqf, &qfi, 0)) get_key(); - } - void next() { - qfi_next(&qfi); - get_key(); - } - bool end() const { - return qfi_end(&qfi); - } - bool operator>(const Iterator& rhs) const { - return key.key > rhs.key.key; - } - private: - void get_key() { - //uint64_t value, count; - qfi_get(&qfi, &key.key, &key.value, &key.count); - } - }; - friend class Iterator; + const QF& get_QF() const { return cqf; } private: QF cqf; @@ -115,44 +99,4 @@ class KeyObject { uint64_t count; }; -template -CQF::CQF() { - qf_init(&cqf, 1ULL << NUM_Q_BITS, NUM_HASH_BITS, 0, true, "", 23423); -} - -template -CQF::CQF(uint64_t qbits, uint64_t key_bits, uint32_t seed) { - qf_init(&cqf, 1ULL << qbits, key_bits, 0, true, "", seed); -} - -template -CQF::CQF(std::string& filename, bool flag) { - if (flag) - qf_read(&cqf, filename.c_str()); - else - qf_deserialize(&cqf, filename.c_str()); -} - -template -CQF::CQF(const CQF& copy_cqf) { - memcpy(reinterpret_cast(&cqf), reinterpret_cast(const_cast(©_cqf.cqf)), sizeof(QF)); -} - -template -void CQF::insert(const key_obj& k) { - qf_insert(&cqf, k.key, k.value, k.count, LOCK_AND_SPIN); - // To validate the CQF - //set.insert(k.key); -} - -template -uint64_t CQF::query(const key_obj& k) { - return qf_count_key_value(&cqf, k.key, k.value); -} - -template -uint64_t CQF::get_index(const key_obj& k) { - return get_bucket_index(k.key); -} - #endif From 2e5e9f4956c6aad8e89d0665aeb05305ba23732e Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Thu, 2 Aug 2018 22:53:11 -0400 Subject: [PATCH 070/122] compacting for readability --- include/cqf.h | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/include/cqf.h b/include/cqf.h index 66cbc0d..b11077f 100644 --- a/include/cqf.h +++ b/include/cqf.h @@ -36,8 +36,6 @@ #define PAGE_DROP_GRANULARITY (1ULL << 21) #define PAGE_BUFFER_SIZE 4096 -static uint64_t tmp_sum_local; - template class CQF { public: @@ -51,15 +49,8 @@ class CQF { qf_deserialize(&cqf, filename.c_str()); } - void insert(const key_obj& k) { - qf_insert(&cqf, k.key, k.value, k.count, LOCK_AND_SPIN); - // To validate the CQF - //set.insert(k.key); - } - - /* Will return the count. */ + void insert(const key_obj& k) { qf_insert(&cqf, k.key, k.value, k.count, LOCK_AND_SPIN); } uint64_t query(const key_obj& k) { return qf_count_key_value(&cqf, k.key, k.value); } - uint64_t get_index(const key_obj& k) { return get_bucket_index(k.key); } void serialize(std::string filename) { qf_serialize(&cqf, filename.c_str()); } @@ -69,7 +60,7 @@ class CQF { uint32_t keybits(void) const { return cqf.metadata->key_bits; } uint64_t size(void) const { return cqf.metadata->ndistinct_elts; } uint64_t capacity() const {return cqf.metadata->nslots; } - //uint64_t set_size(void) const { return set.size(); } + void reset(void) { qf_reset(&cqf); } void dump_metadata(void) const { DEBUG_DUMP(&cqf); } From dfa08be10e814c48e35b363573fd0df85a0c03d3 Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Fri, 3 Aug 2018 17:33:36 -0400 Subject: [PATCH 071/122] move Iterator definition --- include/coloreddbg.h | 53 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index a5755c9..41184dc 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -316,29 +316,57 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg::max()); + struct Iterator { + struct IteratorData { + qf::QFi qfi; + key_obj key; + uint32_t id; + } d; + Iterator(uint32_t id, const qf::QF& cqf) { + d.id = id; + if (qf::qf_iterator(&cqf, &d.qfi, 0)) get_key(); + } + void next() { + qf::qfi_next(&d.qfi); + get_key(); + } + bool end() const { + return qf::qfi_end(&d.qfi); + } + bool operator>(const Iterator& rhs) const { + return key() > rhs.key(); + } + const uint32_t& id() const { return d.id; } + const typename key_obj::kmer_t& key() const { return d.key.key; } + private: + void get_key() { + //uint64_t value, count; + qf::qfi_get(&d.qfi, &d.key.key, &d.key.value, &d.key.count); + } + }; struct Minheap_PQ { - void push(const typename CQF::Iterator& obj) { + void push(const Iterator& obj) { c.emplace_back(obj); - std::push_heap(c.begin(), c.end(), std::greater::Iterator>()); + std::push_heap(c.begin(), c.end(), std::greater()); } void pop() { - std::pop_heap(c.begin(), c.end(), std::greater::Iterator>()); + std::pop_heap(c.begin(), c.end(), std::greater()); c.pop_back(); } - void replace_top(const typename CQF::Iterator& obj) { + void replace_top(const Iterator& obj) { c.emplace_back(obj); pop(); } - typename CQF::Iterator& top() { return c.front(); } + Iterator& top() { return c.front(); } bool empty() const { return c.empty(); } private: - std::vector::Iterator> c; + std::vector c; }; Minheap_PQ minheap; for (uint32_t i = 0; i < num_samples; i++) { - typename CQF::Iterator qfi(i, *incqfs[i].obj); + Iterator qfi(i, incqfs[i].obj->cqf); if (qfi.end()) continue; minheap.push(qfi); } @@ -347,19 +375,20 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg::Iterator& cur = minheap.top(); - last_key = cur.key.key; - eq_class[cur.id] = 1; + Iterator& cur = minheap.top(); + last_key = cur.key(); + eq_class[cur.id()] = 1; cur.next(); minheap.replace_top(cur); - } while(!minheap.empty() && last_key == minheap.top().key.key); + } while(!minheap.empty() && last_key == minheap.top().key()); bool added_eq_class = add_kmer(last_key, eq_class); ++counter; + if (dbg.size() > 10000000) { console->info("exiting for quick profile run"); exit(0); } // Progress tracker static uint64_t last_size = 0; - if (dbg.size() % 10000000 == 0 && + if (dbg.size() % 1000000 == 0 && dbg.size() != last_size) { last_size = dbg.size(); console->info("Kmers merged: {} Num eq classes: {} Total time: {}", From 59c4edca33911e8163d0546cc4ed5718d796a410 Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Fri, 3 Aug 2018 17:33:47 -0400 Subject: [PATCH 072/122] move Iterator definition --- include/cqf.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/include/cqf.h b/include/cqf.h index b11077f..e52d309 100644 --- a/include/cqf.h +++ b/include/cqf.h @@ -37,7 +37,7 @@ #define PAGE_BUFFER_SIZE 4096 template -class CQF { +class CQF : public qf { public: CQF() { qf_init(&cqf, 1ULL << NUM_Q_BITS, NUM_HASH_BITS, 0, true, "", 23423); } CQF(uint64_t qbits, uint64_t key_bits, uint32_t seed) { qf_init(&cqf, 1ULL << qbits, key_bits, 0, true, "", seed); } @@ -63,13 +63,10 @@ class CQF { void reset(void) { qf_reset(&cqf); } - void dump_metadata(void) const { DEBUG_DUMP(&cqf); } + void dump_metadata(void) { DEBUG_DUMP(&cqf); } void drop_pages(uint64_t cur); - const QF& get_QF() const { return cqf; } - - private: QF cqf; }; From 3213926f5c3e7f1323fef1579b74015d7d00c092 Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Fri, 3 Aug 2018 19:40:41 -0400 Subject: [PATCH 073/122] working copy --- include/coloreddbg.h | 12 ++++++------ include/cqf.h | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 41184dc..4184bfb 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -318,20 +318,20 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg(const Iterator& rhs) const { return key() > rhs.key(); @@ -341,7 +341,7 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg -class CQF : public qf { +class CQF { public: CQF() { qf_init(&cqf, 1ULL << NUM_Q_BITS, NUM_HASH_BITS, 0, true, "", 23423); } CQF(uint64_t qbits, uint64_t key_bits, uint32_t seed) { qf_init(&cqf, 1ULL << qbits, key_bits, 0, true, "", seed); } From 18e1971c49b0d09ec8f4f9c5f022e16d79371fbf Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Sat, 4 Aug 2018 01:37:16 -0400 Subject: [PATCH 074/122] remove useless fields from Iterator --- include/coloreddbg.h | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 4184bfb..0fb971c 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -317,31 +317,27 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg::max()); struct Iterator { - struct IteratorData { - QFi qfi; - key_obj key; - uint32_t id; - } d; - Iterator(uint32_t id, const QF& cqf) { - d.id = id; - if (qf_iterator(&cqf, &d.qfi, 0)) get_key(); + QFi qfi; + typename key_obj::kmer_t kmer; + uint32_t id; + Iterator(uint32_t id, const QF& cqf): id(id) { + if (qf_iterator(&cqf, &qfi, 0)) get_key(); } void next() { - qfi_next(&d.qfi); + qfi_next(&qfi); get_key(); } bool end() const { - return qfi_end(&d.qfi); + return qfi_end(&qfi); } bool operator>(const Iterator& rhs) const { return key() > rhs.key(); } - const uint32_t& id() const { return d.id; } - const typename key_obj::kmer_t& key() const { return d.key.key; } + const typename key_obj::kmer_t& key() const { return kmer; } private: void get_key() { - //uint64_t value, count; - qfi_get(&d.qfi, &d.key.key, &d.key.value, &d.key.count); + uint64_t value, count; + qfi_get(&qfi, &kmer, &value, &count); } }; @@ -377,14 +373,14 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg 10000000) { console->info("exiting for quick profile run"); exit(0); } + if (dbg.size() > 50000000) { console->info("exiting for quick profile run"); exit(0); } // Progress tracker static uint64_t last_size = 0; From 152113b1e82fb0f9f23b406df487468c9bfc6dab Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Sat, 4 Aug 2018 16:39:37 -0400 Subject: [PATCH 075/122] Patch replacing simple copy with get_int/set_int --- include/bitvector.h | 1 + include/coloreddbg.h | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/include/bitvector.h b/include/bitvector.h index 67728a2..6f804ac 100644 --- a/include/bitvector.h +++ b/include/bitvector.h @@ -48,6 +48,7 @@ class BitVector { void resize(const uint64_t len); uint64_t get_int(uint64_t startP, uint64_t len=64) {return bits.get_int(startP, len);} bool operator==(const BitVector& b) const { return bits == b.bits; } + void set_int(uint64_t startP, uint64_t wrd, uint64_t len=64) {return bits.set_int(startP, wrd, len);} private: sdsl::bit_vector bits; diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 199cace..c204d02 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -103,6 +103,7 @@ class ColoredDbg { uint64_t num_samples; uint64_t num_serializations; bool flush_eqclass_dis{false}; + std::vector wrdLengths; std::time_t start_time_; spdlog::logger* console; }; @@ -168,9 +169,19 @@ void ColoredDbgsecond.first - 1) * num_samples); uint64_t dest_idx = ((it_input.second.first - 1) * num_samples); - for (uint32_t i = 0; i < num_samples; i++, src_idx++, dest_idx++) + // here: + uint64_t i{0}, wrdCntr{0}; + while (i < num_samples) { + auto bitCnt = wrdLengths[wrdCntr]; + uint64_t wrd = bv_buffer.get_int(src_idx+i, bitCnt); + //std::cerr << wrd << " "; + new_bv_buffer.set_int(dest_idx+i, wrd, bitCnt); + i+=bitCnt; + wrdCntr++; + } + /*for (uint32_t i = 0; i < num_samples; i++, src_idx++, dest_idx++) if (bv_buffer[src_idx]) - new_bv_buffer.set(dest_idx); + new_bv_buffer.set(dest_idx);*/ } } bv_buffer = new_bv_buffer; @@ -446,6 +457,11 @@ ColoredDbg::ColoredDbg(std::string& cqf_file, num_samples++; } sampleid.close(); + wrdLengths.resize( (num_samples-1)/64+1); + for (auto i = 0; i < wrdLengths.size()-1; i++) { + wrdLengths[i] = 64; + } + wrdLengths[wrdLengths.size()-1] = num_samples-((wrdLengths.size()-1)*64); } #endif From f6b41a21b6e271bf4017e8e5d84bc74d33bc56b9 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Sat, 4 Aug 2018 16:45:12 -0400 Subject: [PATCH 076/122] Patch2 (more effective) replacing simple copy with get_int/set_int --- include/coloreddbg.h | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index c204d02..12f870e 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -241,9 +241,18 @@ template void ColoredDbg::add_bitvector(BitVector& vector, uint64_t eq_id) { uint64_t start_idx = (eq_id % mantis::NUM_BV_BUFFER) * num_samples; - for (uint32_t i = 0; i < num_samples; i++, start_idx++) + /*for (uint32_t i = 0; i < num_samples; i++, start_idx++) if (vector[i]) - bv_buffer.set(start_idx); + bv_buffer.set(start_idx);*/ + uint64_t i{0}, wrdCntr{0}; + while (i < num_samples) { + auto bitCnt = vector[wrdCntr]; + uint64_t wrd = bv_buffer.get_int(i, bitCnt); + //std::cerr << wrd << " "; + bv_buffer.set_int(start_idx+i, wrd, bitCnt); + i+=bitCnt; + wrdCntr++; + } } template From b36ef02978d30f23607db9268137c9d5ab66293b Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Sat, 4 Aug 2018 16:48:34 -0400 Subject: [PATCH 077/122] Patch2 (more effective) replacing simple copy with get_int/set_int --- include/coloreddbg.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 12f870e..b3dd57a 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -174,7 +174,6 @@ void ColoredDbg::add_bitvector(BitVector& vector, uint64_t bv_buffer.set(start_idx);*/ uint64_t i{0}, wrdCntr{0}; while (i < num_samples) { - auto bitCnt = vector[wrdCntr]; - uint64_t wrd = bv_buffer.get_int(i, bitCnt); - //std::cerr << wrd << " "; + auto bitCnt = wrdLengths[wrdCntr]; + uint64_t wrd = vector.get_int(i, bitCnt); bv_buffer.set_int(start_idx+i, wrd, bitCnt); i+=bitCnt; wrdCntr++; From e653c50f18ba7dadd4cfae33400c0a5c62a419f0 Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Mon, 6 Aug 2018 01:40:25 -0400 Subject: [PATCH 078/122] play with QF metadata cache in QFi --- include/cqf/gqf.h | 9 +++ src/cqf/gqf.c | 157 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 152 insertions(+), 14 deletions(-) diff --git a/include/cqf/gqf.h b/include/cqf/gqf.h index cc954e1..4339226 100644 --- a/include/cqf/gqf.h +++ b/include/cqf/gqf.h @@ -78,6 +78,15 @@ extern "C" { typedef struct quotient_filter_iterator { const QF *qf; + struct { + uint64_t nslots; + uint64_t xnslots; + uint64_t bits_per_slot; + uint64_t sizeof_qfblock_SLOTS_PER_BLOCK_times_bits_per_slot_div_8; + uint64_t value_bits; + uint64_t key_remainder_bits; + uint64_t nblocks; + } cache; uint64_t run; uint64_t current; uint64_t cur_start_index; diff --git a/src/cqf/gqf.c b/src/cqf/gqf.c index 3715ed7..ad06a78 100644 --- a/src/cqf/gqf.c +++ b/src/cqf/gqf.c @@ -32,6 +32,8 @@ #define METADATA_WORD(qf,field,slot_index) (get_block((qf), (slot_index) / \ SLOTS_PER_BLOCK)->field[((slot_index) % SLOTS_PER_BLOCK) / 64]) +#define QFI_METADATA_WORD(qfi,field,slot_index) (qfi_get_block((qfi), (slot_index) / \ + SLOTS_PER_BLOCK)->field[((slot_index) % SLOTS_PER_BLOCK) / 64]) #define MAX_VALUE(nbits) ((1ULL << (nbits)) - 1) #define BILLION 1000000000L @@ -462,6 +464,11 @@ static inline qfblock * get_block(const QF *qf, uint64_t block_index) return (qfblock *)(((char *)qf->blocks) + block_index * (sizeof(qfblock) + SLOTS_PER_BLOCK * qf->metadata->bits_per_slot / 8)); } +static inline qfblock * qfi_get_block(const QFi *qfi, uint64_t block_index) +{ + return (qfblock *)(((char *)qfi->qf->blocks) + block_index * + qfi->cache.sizeof_qfblock_SLOTS_PER_BLOCK_times_bits_per_slot_div_8); +} #endif static inline int is_runend(const QF *qf, uint64_t index) @@ -470,6 +477,12 @@ static inline int is_runend(const QF *qf, uint64_t index) 64)) & 1ULL; } +static inline int qfi_is_runend(const QFi *qfi, uint64_t index) +{ + return (QFI_METADATA_WORD(qfi, runends, index) >> ((index % SLOTS_PER_BLOCK) % + 64)) & 1ULL; +} + static inline int is_occupied(const QF *qf, uint64_t index) { return (METADATA_WORD(qf, occupieds, index) >> ((index % SLOTS_PER_BLOCK) % @@ -484,6 +497,12 @@ static inline uint64_t get_slot(const QF *qf, uint64_t index) return get_block(qf, index / SLOTS_PER_BLOCK)->slots[index % SLOTS_PER_BLOCK]; } +static inline uint64_t qfi_get_slot(const QFi *qfi, uint64_t index) +{ + assert(index < qfi->qf->metadata->xnslots); + return qfi_get_block(qfi, index / SLOTS_PER_BLOCK)->slots[index % SLOTS_PER_BLOCK]; +} + static inline void set_slot(const QF *qf, uint64_t index, uint64_t value) { assert(index < qf->metadata->xnslots); @@ -508,6 +527,19 @@ static inline uint64_t get_slot(const QF *qf, uint64_t index) 8)) & BITMASK(BITS_PER_SLOT)); } +static inline uint64_t qfi_get_slot(const QFi *qfi, uint64_t index) +{ + /* Should use __uint128_t to support up to 64-bit remainders, but gcc seems + * to generate buggy code. :/ */ + assert(index < qfi->qf->metadata->xnslots); + uint64_t *p = (uint64_t *)&qfi_get_block(qfi, index / + SLOTS_PER_BLOCK)->slots[(index % + SLOTS_PER_BLOCK) + * BITS_PER_SLOT / 8]; + return (uint64_t)(((*p) >> (((index % SLOTS_PER_BLOCK) * BITS_PER_SLOT) % + 8)) & BITMASK(BITS_PER_SLOT)); +} + static inline void set_slot(const QF *qf, uint64_t index, uint64_t value) { /* Should use __uint128_t to support up to 64-bit remainders, but gcc seems @@ -546,6 +578,20 @@ static inline uint64_t get_slot(const QF *qf, uint64_t index) BITMASK(qf->metadata->bits_per_slot)); } +static inline uint64_t qfi_get_slot(const QFi *qfi, uint64_t index) +{ + assert(index < qfi->qf->metadata->xnslots); + /* Should use __uint128_t to support up to 64-bit remainders, but gcc seems + * to generate buggy code. :/ */ + uint64_t *p = (uint64_t *)&qfi_get_block(qfi, index / + SLOTS_PER_BLOCK)->slots[(index % + SLOTS_PER_BLOCK) + * qfi->cache.bits_per_slot / 8]; + return (uint64_t)(((*p) >> (((index % SLOTS_PER_BLOCK) * + qfi->cache.bits_per_slot) % 8)) & + BITMASK(qfi->cache.bits_per_slot)); +} + static inline void set_slot(const QF *qf, uint64_t index, uint64_t value) { assert(index < qf->metadata->xnslots); @@ -1203,6 +1249,80 @@ static inline uint64_t decode_counter(const QF *qf, uint64_t index, uint64_t return end + 1; } +/* Returns the length of the encoding. +REQUIRES: index points to first slot of a counter. */ +static inline uint64_t qfi_decode_counter(const QFi *qfi, uint64_t index, uint64_t + *remainder, uint64_t *count) +{ + uint64_t base; + uint64_t rem; + uint64_t cnt; + uint64_t digit; + uint64_t end; + + *remainder = rem = qfi_get_slot(qfi, index); + + if (qfi_is_runend(qfi, index)) { /* Entire run is "0" */ + *count = 1; + return index; + } + + digit = qfi_get_slot(qfi, index + 1); + + if (qfi_is_runend(qfi, index + 1)) { + *count = digit == rem ? 2 : 1; + return index + (digit == rem ? 1 : 0); + } + + if (rem > 0 && digit >= rem) { + *count = digit == rem ? 2 : 1; + return index + (digit == rem ? 1 : 0); + } + + if (rem > 0 && digit == 0 && qfi_get_slot(qfi, index + 2) == rem) { + *count = 3; + return index + 2; + } + + if (rem == 0 && digit == 0) { + if (qfi_get_slot(qfi, index + 2) == 0) { + *count = 3; + return index + 2; + } else { + *count = 2; + return index + 1; + } + } + + cnt = 0; + base = (1ULL << qfi->qf->metadata->bits_per_slot) - (rem ? 2 : 1); + + end = index + 1; + while (digit != rem && !qfi_is_runend(qfi, end)) { + if (digit > rem) + digit--; + if (digit && rem) + digit--; + cnt = cnt * base + digit; + + end++; + digit = qfi_get_slot(qfi, end); + } + + if (rem) { + *count = cnt + 3; + return end; + } + + if (qfi_is_runend(qfi, end) || qfi_get_slot(qfi, end + 1) != 0) { + *count = 1; + return index; + } + + *count = cnt + 4; + return end + 1; +} + /* return the next slot which corresponds to a * different element * */ @@ -1975,6 +2095,15 @@ bool qf_iterator(const QF *qf, QFi *qfi, uint64_t position) if (qfi->current < position) qfi->current = position; + qfi->cache.nslots = qf->metadata->nslots; + qfi->cache.xnslots = qf->metadata->xnslots; + qfi->cache.sizeof_qfblock_SLOTS_PER_BLOCK_times_bits_per_slot_div_8 = + (sizeof(qfblock) + SLOTS_PER_BLOCK * qf->metadata->bits_per_slot / 8); + qfi->cache.bits_per_slot = qf->metadata->bits_per_slot; + qfi->cache.value_bits = qf->metadata->value_bits; + qfi->cache.key_remainder_bits = qf->metadata->key_remainder_bits; + qfi->cache.nblocks = qf->metadata->nblocks; + #ifdef LOG_CLUSTER_LENGTH qfi->c_info = (cluster_data* )calloc(qf->metadata->nslots/32, sizeof(cluster_data)); @@ -2057,11 +2186,11 @@ int qfi_get(const QFi *qfi, uint64_t *key, uint64_t *value, uint64_t *count) assert(qfi->current < qfi->qf->metadata->nslots); uint64_t current_remainder, current_count; - decode_counter(qfi->qf, qfi->current, ¤t_remainder, ¤t_count); + qfi_decode_counter(qfi, qfi->current, ¤t_remainder, ¤t_count); - *value = current_remainder & BITMASK(qfi->qf->metadata->value_bits); - current_remainder = current_remainder >> qfi->qf->metadata->value_bits; - *key = (qfi->run << qfi->qf->metadata->key_remainder_bits) | current_remainder; + *value = current_remainder & BITMASK(qfi->cache.value_bits); + current_remainder = current_remainder >> qfi->cache.value_bits; + *key = (qfi->run << qfi->cache.key_remainder_bits) | current_remainder; *count = current_count; /*qfi->current = end_index;*/ //get should not change the current index @@ -2075,7 +2204,7 @@ int qfi_next(QFi *qfi) { int qfi_nextx(QFi *qfi, uint64_t* read_offset) { uint64_t block_index = qfi->run / SLOTS_PER_BLOCK; - qfblock* addr = get_block(qfi->qf, block_index); + qfblock* addr = qfi_get_block(qfi, block_index); if (read_offset) *read_offset = (char*)addr - (char*)(qfi->qf->metadata); if (qfi_end(qfi)) @@ -2083,15 +2212,15 @@ int qfi_nextx(QFi *qfi, uint64_t* read_offset) else { /* move to the end of the current counter*/ uint64_t current_remainder, current_count; - qfi->current = decode_counter(qfi->qf, qfi->current, ¤t_remainder, + qfi->current = qfi_decode_counter(qfi, qfi->current, ¤t_remainder, ¤t_count); - if (!is_runend(qfi->qf, qfi->current)) { + if (!qfi_is_runend(qfi, qfi->current)) { qfi->current++; #ifdef LOG_CLUSTER_LENGTH qfi->cur_length++; #endif - if (qfi->current > qfi->qf->metadata->nslots) + if (qfi->current > qfi->cache.nslots) return 1; return 0; } @@ -2102,20 +2231,20 @@ int qfi_nextx(QFi *qfi, uint64_t* read_offset) #endif uint64_t rank = bitrank(addr->occupieds[0], qfi->run % SLOTS_PER_BLOCK); - uint64_t next_run = bitselect(get_block(qfi->qf, + uint64_t next_run = bitselect(qfi_get_block(qfi, block_index)->occupieds[0], rank); if (next_run == 64) { rank = 0; - while (next_run == 64 && block_index < qfi->qf->metadata->nblocks) { + while (next_run == 64 && block_index < qfi->cache.nblocks) { block_index++; - next_run = bitselect(get_block(qfi->qf, block_index)->occupieds[0], + next_run = bitselect(qfi_get_block(qfi, block_index)->occupieds[0], rank); } } - if (block_index == qfi->qf->metadata->nblocks) { + if (block_index == qfi->cache.nblocks) { /* set the index values to max. */ - qfi->run = qfi->current = qfi->qf->metadata->xnslots; + qfi->run = qfi->current = qfi->cache.xnslots; return 1; } qfi->run = block_index * SLOTS_PER_BLOCK + next_run; @@ -2142,7 +2271,7 @@ int qfi_nextx(QFi *qfi, uint64_t* read_offset) inline int qfi_end(const QFi *qfi) { - if (qfi->current >= qfi->qf->metadata->xnslots /*&& is_runend(qfi->qf, qfi->current)*/) + if (qfi->current >= qfi->cache.xnslots /*&& is_runend(qfi->qf, qfi->current)*/) return 1; else return 0; From 1e9819cbc26c2ec50fb51c8b7ffe8d7c43f61c02 Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Mon, 6 Aug 2018 02:09:07 -0400 Subject: [PATCH 079/122] faster add_bitvector that copies 64 bits at a time --- include/coloreddbg.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 0fb971c..5d60f46 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -230,9 +230,10 @@ template void ColoredDbg::add_bitvector(const BitVector& vector, uint64_t eq_id) { uint64_t start_idx = (eq_id % mantis::NUM_BV_BUFFER) * num_samples; - for (uint32_t i = 0; i < num_samples; i++, start_idx++) - if (vector[i]) - bv_buffer[start_idx] = 1; + for (uint32_t i = 0; i < num_samples/64*64; i+=64) + bv_buffer.set_int(start_idx+i, vector.get_int(i, 64), 64); + if (num_samples%64) + bv_buffer.set_int(num_samples/64*64, vector.get_int(num_samples, num_samples%64), num_samples%64); } template From 0db6a4d17928967cfbffb91fd444d097e2c33512 Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Tue, 7 Aug 2018 15:55:29 -0400 Subject: [PATCH 080/122] fix add_bitvector bug --- include/coloreddbg.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 5d60f46..55253a4 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -233,7 +233,7 @@ void ColoredDbg::add_bitvector(const BitVector& vector, uint64_ for (uint32_t i = 0; i < num_samples/64*64; i+=64) bv_buffer.set_int(start_idx+i, vector.get_int(i, 64), 64); if (num_samples%64) - bv_buffer.set_int(num_samples/64*64, vector.get_int(num_samples, num_samples%64), num_samples%64); + bv_buffer.set_int(start_idx+num_samples/64*64, vector.get_int(num_samples/64*64, num_samples%64), num_samples%64); } template From ac53131fc96650de8143a4779bdd034bc4d91bd8 Mon Sep 17 00:00:00 2001 From: prashantpandey Date: Tue, 7 Aug 2018 16:03:41 -0400 Subject: [PATCH 081/122] remove debug early exit --- include/coloreddbg.h | 1 - 1 file changed, 1 deletion(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 55253a4..1b028bc 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -381,7 +381,6 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg 50000000) { console->info("exiting for quick profile run"); exit(0); } // Progress tracker static uint64_t last_size = 0; From b909b6ecf61e17f03adbfd68dedf34d7c58b28e0 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Wed, 15 Aug 2018 14:06:08 -0400 Subject: [PATCH 082/122] Starting point for dynamicMST branch. Merged Mike's branch with RobColorGraph. --- include/coloreddbg.h | 192 +++++---- include/cqf.h | 274 +----------- include/cqf/gqf.h | 9 + src/CMakeLists.txt | 14 +- src/bitvector.cc | 43 -- src/build_eq_graph.cc | 166 -------- src/coloreddbg.cc | 25 +- src/cqf/gqf.c | 157 ++++++- src/validatemantis.cc | 25 +- src/walkEqcls.cc | 937 ------------------------------------------ 10 files changed, 286 insertions(+), 1556 deletions(-) delete mode 100644 src/bitvector.cc delete mode 100644 src/build_eq_graph.cc delete mode 100644 src/walkEqcls.cc diff --git a/include/coloreddbg.h b/include/coloreddbg.h index b3dd57a..9a35a2c 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -31,12 +31,14 @@ #include "sparsepp/spp.h" #include "tsl/sparse_map.h" #include "sdsl/bit_vectors.hpp" -#include "bitvector.h" #include "cqf.h" #include "hashutil.h" #include "common_types.h" #include "mantisconfig.hpp" +typedef sdsl::bit_vector BitVector; +typedef sdsl::rrr_vector<63> BitVectorRRR; + struct hash128 { uint64_t operator()(const __uint128_t& val128) const { @@ -85,8 +87,8 @@ class ColoredDbg { private: // returns true if adding this k-mer increased the number of equivalence classes // and false otherwise. - bool add_kmer(key_obj& hash, BitVector& vector); - void add_bitvector(BitVector& vector, uint64_t eq_id); + bool add_kmer(const typename key_obj::kmer_t& hash, const BitVector& vector); + void add_bitvector(const BitVector& vector, uint64_t eq_id); void add_eq_class(BitVector vector, uint64_t id); uint64_t get_next_available_id(void); void bv_buffer_serialize(); @@ -103,7 +105,7 @@ class ColoredDbg { uint64_t num_samples; uint64_t num_serializations; bool flush_eqclass_dis{false}; - std::vector wrdLengths; + std::time_t start_time_; spdlog::logger* console; }; @@ -111,14 +113,13 @@ class ColoredDbg { template class SampleObject { public: - SampleObject() : obj(), cutoff(0), sample_id(), id(0) {}; - SampleObject(T o, uint32_t c = 0, std::string& s = std::string(), - uint32_t id = 0) : obj(o), cutoff(c), sample_id(s), id(id) {}; - SampleObject(const SampleObject& o) : obj(o.obj), cutoff(o.cutoff), - sample_id(o.sample_id), id(o.id) {} ; + SampleObject() : obj(), sample_id(), id(0) {} + SampleObject(T o, std::string& s = std::string(), + uint32_t id = 0) : obj(o), sample_id(s), id(id) {} + SampleObject(const SampleObject& o) : obj(o.obj), + sample_id(o.sample_id), id(o.id) {} T obj; - uint32_t cutoff; std::string sample_id; uint32_t id; }; @@ -149,7 +150,7 @@ template uint64_t ColoredDbg::get_num_bitvectors(void) const { uint64_t total = 0; for (uint32_t i = 0; i < num_serializations; i++) - total += eqclasses[i].bit_size(); + total += eqclasses[i].size(); return total / num_samples; } @@ -169,18 +170,9 @@ void ColoredDbgsecond.first - 1) * num_samples); uint64_t dest_idx = ((it_input.second.first - 1) * num_samples); - // here: - uint64_t i{0}, wrdCntr{0}; - while (i < num_samples) { - auto bitCnt = wrdLengths[wrdCntr]; - uint64_t wrd = bv_buffer.get_int(src_idx+i, bitCnt); - new_bv_buffer.set_int(dest_idx+i, wrd, bitCnt); - i+=bitCnt; - wrdCntr++; - } - /*for (uint32_t i = 0; i < num_samples; i++, src_idx++, dest_idx++) + for (uint32_t i = 0; i < num_samples; i++, src_idx++, dest_idx++) if (bv_buffer[src_idx]) - new_bv_buffer.set(dest_idx);*/ + new_bv_buffer[dest_idx] = 1; } } bv_buffer = new_bv_buffer; @@ -203,13 +195,13 @@ void ColoredDbg::reinit(cdbg_bv_map_t<__uint128_t, } template -bool ColoredDbg::add_kmer(key_obj& k, BitVector& +bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, const BitVector& vector) { // A kmer (hash) is seen only once during the merge process. // So we insert every kmer in the dbg - uint64_t eq_id = 1; + uint64_t eq_id; __uint128_t vec_hash = HashUtil::MurmurHash128A((void*)vector.data(), - vector.capacity(), 2038074743, + vector.capacity()/8, 2038074743, 2038074751); auto it = eqclass_map.find(vec_hash); @@ -231,26 +223,18 @@ bool ColoredDbg::add_kmer(key_obj& k, BitVector& it->second.second += 1; // update the abundance. } - k.count = eq_id; // we use the count to store the eqclass ids - dbg.insert(k); + dbg.insert(KeyObject(key,0,eq_id)); // we use the count to store the eqclass ids return added_eq_class; } template -void ColoredDbg::add_bitvector(BitVector& vector, uint64_t +void ColoredDbg::add_bitvector(const BitVector& vector, uint64_t eq_id) { uint64_t start_idx = (eq_id % mantis::NUM_BV_BUFFER) * num_samples; - /*for (uint32_t i = 0; i < num_samples; i++, start_idx++) - if (vector[i]) - bv_buffer.set(start_idx);*/ - uint64_t i{0}, wrdCntr{0}; - while (i < num_samples) { - auto bitCnt = wrdLengths[wrdCntr]; - uint64_t wrd = vector.get_int(i, bitCnt); - bv_buffer.set_int(start_idx+i, wrd, bitCnt); - i+=bitCnt; - wrdCntr++; - } + for (uint32_t i = 0; i < num_samples/64*64; i+=64) + bv_buffer.set_int(start_idx+i, vector.get_int(i, 64), 64); + if (num_samples%64) + bv_buffer.set_int(start_idx+num_samples/64*64, vector.get_int(num_samples/64*64, num_samples%64), num_samples%64); } template @@ -263,8 +247,8 @@ void ColoredDbg::bv_buffer_serialize() { BitVectorRRR final_com_bv(bv_temp); std::string bv_file(prefix + std::to_string(num_serializations) + "_" + mantis::EQCLASS_FILE); - final_com_bv.serialize(bv_file); - bv_buffer.reset(); + sdsl::store_to_file(final_com_bv, bv_file); + bv_buffer = BitVector(bv_buffer.bit_size()); num_serializations++; } @@ -305,7 +289,7 @@ ColoredDbg::find_samples(const mantis::QuerySet& kmers) { query_eqclass_map[eqclass] += 1; } - mantis::QueryResult sample_map(num_samples, 0); + mantis::QueryResult sample_vec(num_samples, 0); for (auto it = query_eqclass_map.begin(); it != query_eqclass_map.end(); ++it) { auto eqclass_id = it->first; @@ -319,80 +303,89 @@ ColoredDbg::find_samples(const mantis::QuerySet& kmers) { uint64_t wrd = eqclasses[bucket_idx].get_int(bucket_offset, len); for (uint32_t i = 0, sCntr = w * 64; i < len; i++, sCntr++) if ((wrd >> i) & 0x01) - sample_map[sCntr] += count; + sample_vec[sCntr] += count; bucket_offset += len; } } - return sample_map; + return sample_vec; } template cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg::construct(qf_obj *incqfs, uint64_t num_kmers) { - uint32_t nqf = 0; uint64_t counter = 0; bool is_sampling = (num_kmers < std::numeric_limits::max()); - // merge all input CQFs into the final QF - std::vector::Iterator> it_incqfs; - it_incqfs.reserve(num_samples); + struct Iterator { + QFi qfi; + typename key_obj::kmer_t kmer; + uint32_t id; + Iterator(uint32_t id, const QF& cqf): id(id) { + if (qf_iterator(&cqf, &qfi, 0)) get_key(); + } + void next() { + qfi_next(&qfi); + get_key(); + } + bool end() const { + return qfi_end(&qfi); + } + bool operator>(const Iterator& rhs) const { + return key() > rhs.key(); + } + const typename key_obj::kmer_t& key() const { return kmer; } + private: + void get_key() { + uint64_t value, count; + qfi_get(&qfi, &kmer, &value, &count); + } + }; - // Initialize all iterators with sample specific cutoffs. - for (uint32_t i = 0; i < num_samples; i++) { - it_incqfs.emplace_back(incqfs[i].obj->begin(incqfs[i].cutoff)); - } + struct Minheap_PQ { + void push(const Iterator& obj) { + c.emplace_back(obj); + std::push_heap(c.begin(), c.end(), std::greater()); + } + void pop() { + std::pop_heap(c.begin(), c.end(), std::greater()); + c.pop_back(); + } + void replace_top(const Iterator& obj) { + c.emplace_back(obj); + pop(); + } + Iterator& top() { return c.front(); } + bool empty() const { return c.empty(); } + private: + std::vector c; + }; + Minheap_PQ minheap; - std::priority_queue, - std::vector>, compare> minheap; - // Insert the first key from each CQF in minheap. for (uint32_t i = 0; i < num_samples; i++) { - if (it_incqfs[i].done()) - continue; - KeyObject key = *it_incqfs[i]; - minheap.emplace(key, incqfs[i].cutoff, incqfs[i].sample_id, i); - nqf++; - } + Iterator qfi(i, incqfs[i].obj->cqf); + if (qfi.end()) continue; + minheap.push(qfi); + } while (!minheap.empty()) { - assert(minheap.size() == nqf); - SampleObject cur; BitVector eq_class(num_samples); - // Get the smallest key from minheap and update the eqclass vector - cur = minheap.top(); - eq_class.set(cur.id); - minheap.pop(); - // Keep poping keys from minheap until you see a different key. - // While poping keys build the eq class for cur. - // Increment iterators for all CQFs whose keys are popped. - while (!minheap.empty() && cur.obj.key == minheap.top().obj.key) { - uint32_t id = minheap.top().id; - eq_class.set(id); - minheap.pop(); - ++it_incqfs[id]; - if (it_incqfs[id].done()) // If the iterator is done then decrement nqf - nqf--; - else { // Insert the current iterator head in minHeap - KeyObject key = *it_incqfs[id]; - minheap.emplace(key, incqfs[id].cutoff, incqfs[id].sample_id, id); - } - } - // Move the iterator of the smallest key. - ++it_incqfs[cur.id]; - if (it_incqfs[cur.id].done()) // If the iterator is done then decrement nqf - nqf--; - else { // Insert the current iterator head in minHeap - KeyObject key = *it_incqfs[cur.id]; - minheap.emplace(key, incqfs[cur.id].cutoff, incqfs[cur.id].sample_id, cur.id); - } - // Add in the cdbg - bool added_eq_class = add_kmer(cur.obj, eq_class); - counter++; + KeyObject::kmer_t last_key; + do { + Iterator& cur = minheap.top(); + last_key = cur.key(); + eq_class[cur.id] = 1; + cur.next(); + minheap.replace_top(cur); + } while(!minheap.empty() && last_key == minheap.top().key()); + + bool added_eq_class = add_kmer(last_key, eq_class); + ++counter; // Progress tracker static uint64_t last_size = 0; - if (dbg.size() % 10000000 == 0 && + if (dbg.size() % 1000000 == 0 && dbg.size() != last_size) { last_size = dbg.size(); console->info("Kmers merged: {} Num eq classes: {} Total time: {}", @@ -414,6 +407,8 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg::ColoredDbg(std::string& cqf_file, } eqclasses.reserve(sorted_files.size()); + BitVectorRRR bv; for (auto file : sorted_files) { - eqclasses.push_back(BitVectorRRR(file.second)); + sdsl::load_from_file(bv, file.second); + eqclasses.push_back(bv); num_serializations++; } @@ -464,11 +461,6 @@ ColoredDbg::ColoredDbg(std::string& cqf_file, num_samples++; } sampleid.close(); - wrdLengths.resize( (num_samples-1)/64+1); - for (auto i = 0; i < wrdLengths.size()-1; i++) { - wrdLengths[i] = 64; - } - wrdLengths[wrdLengths.size()-1] = num_samples-((wrdLengths.size()-1)*64); } #endif diff --git a/include/cqf.h b/include/cqf.h index e2134c2..b526689 100644 --- a/include/cqf.h +++ b/include/cqf.h @@ -39,62 +39,35 @@ template class CQF { public: - CQF(); - CQF(uint64_t qbits, uint64_t key_bits, uint32_t seed); - CQF(std::string& filename, bool flag); - CQF(const CQF& copy_cqf); - - void insert(const key_obj& k); - - /* Will return the count. */ - uint64_t query(const key_obj& k); + CQF() { qf_init(&cqf, 1ULL << NUM_Q_BITS, NUM_HASH_BITS, 0, true, "", 23423); } + CQF(uint64_t qbits, uint64_t key_bits, uint32_t seed) { qf_init(&cqf, 1ULL << qbits, key_bits, 0, true, "", seed); } + CQF(const CQF& copy_cqf) { memcpy(reinterpret_cast(&cqf), reinterpret_cast(const_cast(©_cqf.cqf)), sizeof(QF)); } + CQF(std::string& filename, bool flag) { + if (flag) + qf_read(&cqf, filename.c_str()); + else + qf_deserialize(&cqf, filename.c_str()); + } - uint64_t get_index(const key_obj& k); + void insert(const key_obj& k) { qf_insert(&cqf, k.key, k.value, k.count, LOCK_AND_SPIN); } + uint64_t query(const key_obj& k) { return qf_count_key_value(&cqf, k.key, k.value); } + uint64_t get_index(const key_obj& k) { return get_bucket_index(k.key); } - void serialize(std::string filename) { - qf_serialize(&cqf, filename.c_str()); - } + void serialize(std::string filename) { qf_serialize(&cqf, filename.c_str()); } - std::pair queryValAndIdx( key_obj& k) const; uint64_t range(void) const { return cqf.metadata->range; } uint32_t seed(void) const { return cqf.metadata->seed; } uint32_t keybits(void) const { return cqf.metadata->key_bits; } uint64_t size(void) const { return cqf.metadata->ndistinct_elts; } uint64_t capacity() const {return cqf.metadata->nslots; } - //uint64_t set_size(void) const { return set.size(); } + void reset(void) { qf_reset(&cqf); } - void dump_metadata(void) const { DEBUG_DUMP(&cqf); } + void dump_metadata(void) { DEBUG_DUMP(&cqf); } void drop_pages(uint64_t cur); - class Iterator { - public: - Iterator(QFi it, uint32_t cutoff, uint64_t end_hash); - key_obj operator*(void) const; - void operator++(void); - bool done(void) const; - - QFi iter; - int64_t last_prefetch_offset; - uint64_t buffer_size; - private: - /* global buffer to perform read ahead */ - unsigned char *buffer; - uint32_t num_pages; - uint32_t cutoff; - uint64_t end_hash; - struct aiocb aiocb; - }; - - Iterator limits(uint64_t start_hash, uint64_t end_hash, uint32_t cutoff) - const; - Iterator begin(uint32_t cutoff) const; - Iterator end(uint32_t cutoff) const; - - private: QF cqf; - //std::unordered_set set; }; class KeyObject { @@ -108,223 +81,10 @@ class KeyObject { bool operator==(KeyObject k) { return key == k.key; } - uint64_t key; + typedef uint64_t kmer_t; + kmer_t key; uint64_t value; uint64_t count; }; -template -CQF::CQF() { - qf_init(&cqf, 1ULL << NUM_Q_BITS, NUM_HASH_BITS, 0, true, "", 23423); -} - -template -CQF::CQF(uint64_t qbits, uint64_t key_bits, uint32_t seed) { - qf_init(&cqf, 1ULL << qbits, key_bits, 0, true, "", seed); -} - -template -CQF::CQF(std::string& filename, bool flag) { - if (flag) - qf_read(&cqf, filename.c_str()); - else - qf_deserialize(&cqf, filename.c_str()); -} - -template -CQF::CQF(const CQF& copy_cqf) { - memcpy(reinterpret_cast(&cqf), reinterpret_cast(const_cast(©_cqf.cqf)), sizeof(QF)); -} - -template -void CQF::insert(const key_obj& k) { - qf_insert(&cqf, k.key, k.value, k.count, LOCK_AND_SPIN); - // To validate the CQF - //set.insert(k.key); -} - -template -uint64_t CQF::query(const key_obj& k) { - return qf_count_key_value(&cqf, k.key, k.value); -} - -template -std::pair CQF::queryValAndIdx(key_obj& k) const { - uint64_t eq, idx; - eq = qf_key_value_index(&cqf, k.key, k.value, &idx); - return std::make_pair(eq, idx); -} - -template -uint64_t CQF::get_index(const key_obj& k) { - return get_bucket_index(k.key); -} - -template -CQF::Iterator::Iterator(QFi it, uint32_t cutoff, uint64_t end_hash) - : iter(it), last_prefetch_offset(LLONG_MIN), cutoff(cutoff), - end_hash(end_hash) { - buffer_size = (((it.qf->metadata->size / 2048 - - (rand() % (it.qf->metadata->size / 4096))) - + 4095) / 4096) * 4096; - buffer = (unsigned char*)mmap(NULL, buffer_size, PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - if (buffer == MAP_FAILED) { - perror("buffer malloc"); - std::cerr << "Can't allocate buffer space." << std::endl; - exit(1); - } - }; - -template -key_obj CQF::Iterator::operator*(void) const { - uint64_t key = 0, value = 0, count = 0; - qfi_get(&iter, &key, &value, &count); - return key_obj(key, value, count); -} - -// This function read one byte from each page in the iterator buffer. -void handler_function(union sigval sv); - -template -void CQF::Iterator::operator++(void) { - uint64_t last_read_offset; - qfi_nextx(&iter, &last_read_offset); - - // Read next "buffer_size" bytes from the file offset. - if ((int64_t)last_read_offset >= last_prefetch_offset) { - //DEBUG_CDBG("last_read_offset>last_prefetch_offset for " << iter.qf->mem->fd - //<< " " << last_read_offset << ">" << last_prefetch_offset); - if (aiocb.aio_buf) { - int res = aio_error(&aiocb); - if (res == EINPROGRESS) { - //DEBUG_CDBG("didn't read fast enough for " << aiocb.aio_fildes << - //" at " << last_read_offset << "(until " << last_prefetch_offset << - //" buffer size: "<< buffer_size << ")..."); - // const struct aiocb *const aiocb_list[1] = {&aiocb}; - // aio_suspend(aiocb_list, 1, NULL); - //DEBUG_CDBG(" finished it"); - } else if (res > 0) { - //DEBUG_CDBG("aio_error() returned " << std::dec << res); - } else if (res == 0) { - //DEBUG_CDBG("prefetch was OK for " << aiocb.aio_fildes << " at " << - //std::hex << aiocb.aio_offset << std::dec); - } - } - - if ((last_prefetch_offset - (int64_t)buffer_size) > 0) { - madvise((unsigned char *)(iter.qf->metadata) + last_prefetch_offset - - buffer_size, buffer_size, MADV_DONTNEED); - posix_fadvise(iter.qf->mem->fd, (off_t)(last_prefetch_offset - - (int64_t)buffer_size), - buffer_size, POSIX_FADV_DONTNEED); - } - - memset(&aiocb, 0, sizeof(struct aiocb)); - aiocb.aio_fildes = iter.qf->mem->fd; - aiocb.aio_buf = (volatile void*)buffer; - aiocb.aio_nbytes = buffer_size; - if ((last_prefetch_offset + (int64_t)buffer_size) < - (int64_t)last_read_offset) { - if (last_prefetch_offset != 0) - //DEBUG_CDBG("resetting.. lpo:" << last_prefetch_offset << " lro:" << - //last_read_offset); - last_prefetch_offset = ( last_read_offset & ~(4095ULL) ) + - PAGE_BUFFER_SIZE; - } else { - last_prefetch_offset += buffer_size; - } - aiocb.aio_offset = (__off_t)last_prefetch_offset; - //DEBUG_CDBG("prefetch in " << iter.qf->mem->fd << " from " << std::hex << - //last_prefetch_offset << std::dec << " ... " << " buffer size: " - //<< buffer_size << " into buffer at " << std::hex << - //((uint64_t)buffer) << std::dec); - // to touch each page in the buffer. - aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD; - aiocb.aio_sigevent.sigev_notify_function = &handler_function; - aiocb.aio_sigevent.sigev_value.sival_ptr = (void*)this; - - uint32_t ret = aio_read(&aiocb); - //uint32_t ret = posix_fadvise(iter.qf->mem->fd, last_read_offset, - //buffer_size, POSIX_FADV_WILLNEED); - //DEBUG_CDBG("prefetch issued"); - if (ret) { - std::cerr << "aio_read failed at " << iter.current << " total size " << - iter.qf->metadata->nslots << std::endl; - perror("aio_read"); - } - } - - // Skip past the cutoff - do { - uint64_t key = 0, value = 0, count = 0; - qfi_get(&iter, &key, &value, &count); - if (count < cutoff) - qfi_next(&iter); - else - break; - } while(!qfi_end(&iter)); - - // drop pages of the last million slots. - //static uint64_t last_marker = 1; - //if (iter.current / PAGE_DROP_GRANULARITY > last_marker + 1) { - //uint64_t start_idx = last_marker * PAGE_DROP_GRANULARITY; - //uint64_t end_idx = (last_marker + 1) * PAGE_DROP_GRANULARITY; - //qf_drop_pages(iter.qf, start_idx, end_idx); - //last_marker += 1; - //} -} - -/* Currently, the iterator only traverses forward. So, we only need to check - * the right side limit. - */ -template -bool CQF::Iterator::done(void) const { - uint64_t key = 0, value = 0, count = 0; - qfi_get(&iter, &key, &value, &count); - return key >= end_hash || qfi_end(&iter); -} - -template -typename CQF::Iterator CQF::begin(uint32_t cutoff) const { - QFi qfi; - qf_iterator(&this->cqf, &qfi, 0); - // Skip past the cutoff - do { - uint64_t key = 0, value = 0, count = 0; - qfi_get(&qfi, &key, &value, &count); - if (count < cutoff) - qfi_next(&qfi); - else - break; - } while(!qfi_end(&qfi)); - - return Iterator(qfi, cutoff, UINT64_MAX); -} - -template -typename CQF::Iterator CQF::end(uint32_t cutoff) const { - QFi qfi; - qf_iterator(&this->cqf, &qfi, 0xffffffffffffffff); - return Iterator(qfi, cutoff, UINT64_MAX); -} - -template -typename CQF::Iterator CQF::limits(uint64_t start_hash, - uint64_t end_hash, - uint32_t cutoff) const { - QFi qfi; - qf_iterator_hash(&this->cqf, &qfi, start_hash); - // Skip past the cutoff - do { - uint64_t key = 0, value = 0, count = 0; - qfi_get(&qfi, &key, &value, &count); - if (count < cutoff) - qfi_next(&qfi); - else - break; - } while(!qfi_end(&qfi)); - - return Iterator(qfi, cutoff, end_hash); -} #endif diff --git a/include/cqf/gqf.h b/include/cqf/gqf.h index 266469d..9565cb3 100644 --- a/include/cqf/gqf.h +++ b/include/cqf/gqf.h @@ -78,6 +78,15 @@ extern "C" { typedef struct quotient_filter_iterator { const QF *qf; + struct { + uint64_t nslots; + uint64_t xnslots; + uint64_t bits_per_slot; + uint64_t sizeof_qfblock_SLOTS_PER_BLOCK_times_bits_per_slot_div_8; + uint64_t value_bits; + uint64_t key_remainder_bits; + uint64_t nblocks; + } cache; uint64_t run; uint64_t current; uint64_t cur_start_index; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 879aaea..8954af6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -64,13 +64,13 @@ endif() install(TARGETS mantis RUNTIME DESTINATION bin) -add_executable(monochromatic_component_iterator - monochromatic_component_iterator.cc) -target_include_directories(monochromatic_component_iterator PUBLIC - $) -target_link_libraries(monochromatic_component_iterator - mantis_core) -set_property(TARGET monochromatic_component_iterator PROPERTY INTERPROCEDURAL_OPTIMIZATION True) +#add_executable(monochromatic_component_iterator +# monochromatic_component_iterator.cc) +#target_include_directories(monochromatic_component_iterator PUBLIC +# $) +#target_link_libraries(monochromatic_component_iterator +# mantis_core) +#set_property(TARGET monochromatic_component_iterator PROPERTY INTERPROCEDURAL_OPTIMIZATION True) add_executable(msf MSF.cc) diff --git a/src/bitvector.cc b/src/bitvector.cc deleted file mode 100644 index 22c10d5..0000000 --- a/src/bitvector.cc +++ /dev/null @@ -1,43 +0,0 @@ -#include "bitvector.h" - -BitVector::BitVector(uint64_t size) : size(size) { - bits = sdsl::bit_vector(size); -} - -void BitVector::reset() { - bits = sdsl::bit_vector(size); -} - -bool BitVector::operator[](uint64_t idx) { - assert(idx < size); - return bits[idx]; -} - -void BitVector::set(uint64_t idx) { - assert(idx < size); - bits[idx] = 1; -} - -void BitVector::resize(const uint64_t len) { - bits.bit_resize(len); - size = len; -} - -BitVectorRRR::BitVectorRRR(std::string& filename) { - sdsl::load_from_file(rrr_bits, filename); - size = rrr_bits.size(); - DEBUG_CDBG("Read rrr bit vector of size " << size << " from file " << - filename); -} - -bool BitVectorRRR::operator[](uint64_t idx) { - assert(idx < size); - return rrr_bits[idx]; -} - -bool BitVectorRRR::serialize(std::string& filename) { - DEBUG_CDBG("Serializing rrr bit vector of size " << size << " to file " << - filename); - return sdsl::store_to_file(rrr_bits, filename); -} - diff --git a/src/build_eq_graph.cc b/src/build_eq_graph.cc deleted file mode 100644 index 4bb85ec..0000000 --- a/src/build_eq_graph.cc +++ /dev/null @@ -1,166 +0,0 @@ -#include -#include -#include -#include -#include - -#include "bitvector.h" -#include "sdsl/rrr_vector.hpp" -#include "hashutil.h" -#include "clipp.h" - -uint32_t hamming_dist(BitVectorRRR& v, uint64_t vec_length, std::vector& blengths, - std::vector& boffs, uint32_t i, uint32_t j, uint32_t thresh) { - auto d = [](uint64_t x, uint64_t y) -> int { - uint64_t res = x ^ y; - return __builtin_popcountll (res); - }; - - auto idx_i = i * vec_length; - auto idx_j = j * vec_length; - - uint64_t dist{0}; - uint64_t eqwords{0}; - for (size_t idx = 0; idx < blengths.size(); ++idx) { - auto wx = v.get_int(idx_i + boffs[idx], blengths[idx]); - auto wy = v.get_int(idx_j + boffs[idx], blengths[idx]); - dist += d(wx, wy); - eqwords += (wx == wy) ? 1 : 0; - if (dist > thresh) { return dist; } - } - if (dist == 0 ) { - if (eqwords < blengths.size()) { - std::cerr << "dist = " << dist << ", but eqwords = " << eqwords << " / " << blengths.size() << "\n"; - } - std::cerr << "for " << i << ", " << j << " eqwords = " << eqwords << "\n"; - } - return dist; -} - -int main(int argc, char* argv[]){ - using namespace clipp; using std::cout; using std::string; - uint32_t thresh{5}; - std::string fname;//{argv[1]}; - uint64_t vec_length{0};//= std::stoul(argv[2]); - uint64_t num_to_process{std::numeric_limits::max()};// = std::stoul(argv[3]); - bool help{false}; - auto cli = ( - value("input file", fname), - required("-l", "--length") & value("vec_length", vec_length) % "vector length", - option("-n") & value("nproc", num_to_process) % "maximum vectors to process (default = all)", - option("-t") & value("thresh", thresh) % "threshold of distance to report edges (default = 5)", - option("-h", "--help").set(help) - ); - - if (!parse(argc, argv, cli)) { - std::cerr << make_man_page(cli, argv[0]); - std::exit(1); - } else if (help) { - std::cerr << make_man_page(cli, argv[0]); - std::exit(1); - } - - BitVectorRRR v(fname); - - std::vector>> pattern_map; - auto num_buckets = std::ceil(vec_length/64.0); - pattern_map.resize(num_buckets); - - std::vector blengths; - std::vector boffs; - size_t offset{0}; - std::cerr << "["; - for (size_t i = 0; i < num_buckets; ++i) { - auto s = std::min(vec_length - offset, static_cast(64u)); - boffs.push_back(offset); - blengths.push_back(s); - std::cerr << " (" << offset << ", " << s << ")"; - offset += s; - } - std::cerr << " ]\n"; - std::cerr << "pattern map has " << num_buckets << " buckets\n"; - - auto tot_bits = v.bit_size(); - std::cerr << "length = " << tot_bits << "\n"; - - offset = 0; - uint64_t vec_idx{0}; - size_t max_num_vec = num_to_process; - while (offset < tot_bits and vec_idx < max_num_vec) { - for (size_t idx = 0; idx < blengths.size(); ++idx) { - auto w = v.get_int(offset + boffs[idx], blengths[idx]); - pattern_map[idx][w].push_back(vec_idx); - } - ++vec_idx; - offset = vec_idx * vec_length; - if ((vec_idx > 0) and (vec_idx % 500000 == 0)) { - std::cerr << "processed " << vec_idx << " vectors\n"; - } - } - - uint64_t s{0}; - std::vector means; - for (size_t b = 0; b < num_buckets; ++b) { - std::cerr << "bucket " << b << " has " << pattern_map[b].size() << " patterns (" << (static_cast(vec_idx) / pattern_map[b].size()) << ")\n"; - s += std::ceil((static_cast(vec_idx) / pattern_map[b].size())); - means.push_back((static_cast(vec_idx) / pattern_map[b].size())); - } - - std::unordered_map candidates; - std::unordered_set skip; - std::vector cd; - cd.reserve(s); - size_t nn{0}; - size_t nc{0}; - size_t m{std::numeric_limits::max()}; - std::cout << max_num_vec << '\t' << vec_length << '\n'; - for (size_t x = 0; x < max_num_vec; ++x) { - offset = vec_length * x; - for (size_t idx = 0; idx < blengths.size(); ++idx) { - auto w = v.get_int(offset + boffs[idx], blengths[idx]); - auto& c1 = pattern_map[idx][w]; - //cd.insert(cd.end(), c1.begin(), c1.end()); - if (c1.size() < 2*means[idx]) { - for(auto c : c1) { - if (c > x) { candidates[c] += 1; } - } - } - } - //std::sort(cd.begin(), cd.end()); - //auto last = std::unique(cd.begin(), cd.end()); - //nc += std::distance(cd.begin(), last); - - //std::cerr << "x = " << x << ", num candidates = " << candidates.size() << '\n'; - - for (auto& kv : candidates) { - //if (kv.second >= 35) { - nn++; - auto d = hamming_dist(v, vec_length, blengths, boffs, x, kv.first, thresh); - if (d==0) { - std::cerr << "WHAT?! " << x << " = " << kv.first << ", num matching blocks = " << kv.second << "\n"; - for(size_t j = 0; j < vec_length; ++j) { - std::cerr << v[x*vec_length + j]; - } - std::cerr << "\n\n"; - for(size_t j = 0; j < vec_length; ++j) { - std::cerr << v[kv.first*vec_length + j]; - } - std::cerr << "\n\n"; - } - bool within_bound = d <= thresh; - nc += (within_bound); - m = (d < m) ? d : m; - if (within_bound) { std::cout << x << '\t' << kv.first << '\t' << d << '\n'; } - //} - } - if (x > 0 and x % 10000 == 0) { - std::cerr << "x = " << x << " ( " << (nc / static_cast(x)) << ", " << (nn / static_cast(x)) << " : " << m << ")\n"; - } - candidates.clear(); - //cd.clear(); - } - - - - return 0; -} diff --git a/src/coloreddbg.cc b/src/coloreddbg.cc index 8b7c500..c759581 100644 --- a/src/coloreddbg.cc +++ b/src/coloreddbg.cc @@ -51,15 +51,15 @@ #include "mantisconfig.hpp" // This function read one byte from each page in the iterator buffer. -uint64_t tmp_sum; -void handler_function(union sigval sv) { - CQF::Iterator& it(*((CQF::Iterator*)sv.sival_ptr)); - unsigned char *start = (unsigned char*)(it.iter.qf->metadata) + it.last_prefetch_offset; - unsigned char *counter = (unsigned char*)(it.iter.qf->metadata) + it.last_prefetch_offset; - for (;counter < start + it.buffer_size; counter += 4096) { - tmp_sum += *counter; - } -} +//uint64_t tmp_sum; +//void handler_function(union sigval sv) { + //CQF::Iterator& it(*((CQF::Iterator*)sv.sival_ptr)); + //unsigned char *start = (unsigned char*)(it.iter.qf->metadata) + it.last_prefetch_offset; + //unsigned char *counter = (unsigned char*)(it.iter.qf->metadata) + it.last_prefetch_offset; + //for (;counter < start + it.buffer_size; counter += 4096) { + //tmp_sum += *counter; + //} +//} /* * === FUNCTION ============================================================= @@ -144,9 +144,8 @@ build_main ( BuildOpts& opt ) // mmap all the input cqfs std::string cqf_file; uint32_t nqf = 0; - uint32_t cutoff; console->info("Reading input Squeakr files."); - while (infile >> cqf_file >> cutoff) { + while (infile >> cqf_file) { if (!mantis::fs::FileExists(cqf_file.c_str())) { console->error("Squeakr file {} does not exist.", cqf_file); exit(1); @@ -155,9 +154,9 @@ build_main ( BuildOpts& opt ) std::string sample_id = first_part(first_part(last_part(cqf_file, '/'), '.'), '_'); console->info("Reading CQF {} Seed {}",nqf, cqfs[nqf].seed()); - console->info("Sample id {} cut off {}", sample_id, cutoff); + console->info("Sample id {} cut off {}", sample_id); cqfs.back().dump_metadata(); - inobjects.emplace_back(&cqfs[nqf], cutoff, sample_id, nqf); + inobjects.emplace_back(&cqfs[nqf], sample_id, nqf); nqf++; } diff --git a/src/cqf/gqf.c b/src/cqf/gqf.c index cb8f58e..5ebb915 100644 --- a/src/cqf/gqf.c +++ b/src/cqf/gqf.c @@ -32,6 +32,8 @@ #define METADATA_WORD(qf,field,slot_index) (get_block((qf), (slot_index) / \ SLOTS_PER_BLOCK)->field[((slot_index) % SLOTS_PER_BLOCK) / 64]) +#define QFI_METADATA_WORD(qfi,field,slot_index) (qfi_get_block((qfi), (slot_index) / \ + SLOTS_PER_BLOCK)->field[((slot_index) % SLOTS_PER_BLOCK) / 64]) #define MAX_VALUE(nbits) ((1ULL << (nbits)) - 1) #define BILLION 1000000000L @@ -462,6 +464,11 @@ static inline qfblock * get_block(const QF *qf, uint64_t block_index) return (qfblock *)(((char *)qf->blocks) + block_index * (sizeof(qfblock) + SLOTS_PER_BLOCK * qf->metadata->bits_per_slot / 8)); } +static inline qfblock * qfi_get_block(const QFi *qfi, uint64_t block_index) +{ + return (qfblock *)(((char *)qfi->qf->blocks) + block_index * + qfi->cache.sizeof_qfblock_SLOTS_PER_BLOCK_times_bits_per_slot_div_8); +} #endif static inline int is_runend(const QF *qf, uint64_t index) @@ -470,6 +477,12 @@ static inline int is_runend(const QF *qf, uint64_t index) 64)) & 1ULL; } +static inline int qfi_is_runend(const QFi *qfi, uint64_t index) +{ + return (QFI_METADATA_WORD(qfi, runends, index) >> ((index % SLOTS_PER_BLOCK) % + 64)) & 1ULL; +} + static inline int is_occupied(const QF *qf, uint64_t index) { return (METADATA_WORD(qf, occupieds, index) >> ((index % SLOTS_PER_BLOCK) % @@ -484,6 +497,12 @@ static inline uint64_t get_slot(const QF *qf, uint64_t index) return get_block(qf, index / SLOTS_PER_BLOCK)->slots[index % SLOTS_PER_BLOCK]; } +static inline uint64_t qfi_get_slot(const QFi *qfi, uint64_t index) +{ + assert(index < qfi->qf->metadata->xnslots); + return qfi_get_block(qfi, index / SLOTS_PER_BLOCK)->slots[index % SLOTS_PER_BLOCK]; +} + static inline void set_slot(const QF *qf, uint64_t index, uint64_t value) { assert(index < qf->metadata->xnslots); @@ -508,6 +527,19 @@ static inline uint64_t get_slot(const QF *qf, uint64_t index) 8)) & BITMASK(BITS_PER_SLOT)); } +static inline uint64_t qfi_get_slot(const QFi *qfi, uint64_t index) +{ + /* Should use __uint128_t to support up to 64-bit remainders, but gcc seems + * to generate buggy code. :/ */ + assert(index < qfi->qf->metadata->xnslots); + uint64_t *p = (uint64_t *)&qfi_get_block(qfi, index / + SLOTS_PER_BLOCK)->slots[(index % + SLOTS_PER_BLOCK) + * BITS_PER_SLOT / 8]; + return (uint64_t)(((*p) >> (((index % SLOTS_PER_BLOCK) * BITS_PER_SLOT) % + 8)) & BITMASK(BITS_PER_SLOT)); +} + static inline void set_slot(const QF *qf, uint64_t index, uint64_t value) { /* Should use __uint128_t to support up to 64-bit remainders, but gcc seems @@ -546,6 +578,20 @@ static inline uint64_t get_slot(const QF *qf, uint64_t index) BITMASK(qf->metadata->bits_per_slot)); } +static inline uint64_t qfi_get_slot(const QFi *qfi, uint64_t index) +{ + assert(index < qfi->qf->metadata->xnslots); + /* Should use __uint128_t to support up to 64-bit remainders, but gcc seems + * to generate buggy code. :/ */ + uint64_t *p = (uint64_t *)&qfi_get_block(qfi, index / + SLOTS_PER_BLOCK)->slots[(index % + SLOTS_PER_BLOCK) + * qfi->cache.bits_per_slot / 8]; + return (uint64_t)(((*p) >> (((index % SLOTS_PER_BLOCK) * + qfi->cache.bits_per_slot) % 8)) & + BITMASK(qfi->cache.bits_per_slot)); +} + static inline void set_slot(const QF *qf, uint64_t index, uint64_t value) { assert(index < qf->metadata->xnslots); @@ -1203,6 +1249,80 @@ static inline uint64_t decode_counter(const QF *qf, uint64_t index, uint64_t return end + 1; } +/* Returns the length of the encoding. +REQUIRES: index points to first slot of a counter. */ +static inline uint64_t qfi_decode_counter(const QFi *qfi, uint64_t index, uint64_t + *remainder, uint64_t *count) +{ + uint64_t base; + uint64_t rem; + uint64_t cnt; + uint64_t digit; + uint64_t end; + + *remainder = rem = qfi_get_slot(qfi, index); + + if (qfi_is_runend(qfi, index)) { /* Entire run is "0" */ + *count = 1; + return index; + } + + digit = qfi_get_slot(qfi, index + 1); + + if (qfi_is_runend(qfi, index + 1)) { + *count = digit == rem ? 2 : 1; + return index + (digit == rem ? 1 : 0); + } + + if (rem > 0 && digit >= rem) { + *count = digit == rem ? 2 : 1; + return index + (digit == rem ? 1 : 0); + } + + if (rem > 0 && digit == 0 && qfi_get_slot(qfi, index + 2) == rem) { + *count = 3; + return index + 2; + } + + if (rem == 0 && digit == 0) { + if (qfi_get_slot(qfi, index + 2) == 0) { + *count = 3; + return index + 2; + } else { + *count = 2; + return index + 1; + } + } + + cnt = 0; + base = (1ULL << qfi->qf->metadata->bits_per_slot) - (rem ? 2 : 1); + + end = index + 1; + while (digit != rem && !qfi_is_runend(qfi, end)) { + if (digit > rem) + digit--; + if (digit && rem) + digit--; + cnt = cnt * base + digit; + + end++; + digit = qfi_get_slot(qfi, end); + } + + if (rem) { + *count = cnt + 3; + return end; + } + + if (qfi_is_runend(qfi, end) || qfi_get_slot(qfi, end + 1) != 0) { + *count = 1; + return index; + } + + *count = cnt + 4; + return end + 1; +} + /* return the next slot which corresponds to a * different element * */ @@ -2006,6 +2126,15 @@ bool qf_iterator(const QF *qf, QFi *qfi, uint64_t position) if (qfi->current < position) qfi->current = position; + qfi->cache.nslots = qf->metadata->nslots; + qfi->cache.xnslots = qf->metadata->xnslots; + qfi->cache.sizeof_qfblock_SLOTS_PER_BLOCK_times_bits_per_slot_div_8 = + (sizeof(qfblock) + SLOTS_PER_BLOCK * qf->metadata->bits_per_slot / 8); + qfi->cache.bits_per_slot = qf->metadata->bits_per_slot; + qfi->cache.value_bits = qf->metadata->value_bits; + qfi->cache.key_remainder_bits = qf->metadata->key_remainder_bits; + qfi->cache.nblocks = qf->metadata->nblocks; + #ifdef LOG_CLUSTER_LENGTH qfi->c_info = (cluster_data* )calloc(qf->metadata->nslots/32, sizeof(cluster_data)); @@ -2088,11 +2217,11 @@ int qfi_get(const QFi *qfi, uint64_t *key, uint64_t *value, uint64_t *count) assert(qfi->current < qfi->qf->metadata->nslots); uint64_t current_remainder, current_count; - decode_counter(qfi->qf, qfi->current, ¤t_remainder, ¤t_count); + qfi_decode_counter(qfi, qfi->current, ¤t_remainder, ¤t_count); - *value = current_remainder & BITMASK(qfi->qf->metadata->value_bits); - current_remainder = current_remainder >> qfi->qf->metadata->value_bits; - *key = (qfi->run << qfi->qf->metadata->key_remainder_bits) | current_remainder; + *value = current_remainder & BITMASK(qfi->cache.value_bits); + current_remainder = current_remainder >> qfi->cache.value_bits; + *key = (qfi->run << qfi->cache.key_remainder_bits) | current_remainder; *count = current_count; /*qfi->current = end_index;*/ //get should not change the current index @@ -2106,7 +2235,7 @@ int qfi_next(QFi *qfi) { int qfi_nextx(QFi *qfi, uint64_t* read_offset) { uint64_t block_index = qfi->run / SLOTS_PER_BLOCK; - qfblock* addr = get_block(qfi->qf, block_index); + qfblock* addr = qfi_get_block(qfi, block_index); if (read_offset) *read_offset = (char*)addr - (char*)(qfi->qf->metadata); if (qfi_end(qfi)) @@ -2114,15 +2243,15 @@ int qfi_nextx(QFi *qfi, uint64_t* read_offset) else { /* move to the end of the current counter*/ uint64_t current_remainder, current_count; - qfi->current = decode_counter(qfi->qf, qfi->current, ¤t_remainder, + qfi->current = qfi_decode_counter(qfi, qfi->current, ¤t_remainder, ¤t_count); - if (!is_runend(qfi->qf, qfi->current)) { + if (!qfi_is_runend(qfi, qfi->current)) { qfi->current++; #ifdef LOG_CLUSTER_LENGTH qfi->cur_length++; #endif - if (qfi->current > qfi->qf->metadata->nslots) + if (qfi->current > qfi->cache.nslots) return 1; return 0; } @@ -2133,20 +2262,20 @@ int qfi_nextx(QFi *qfi, uint64_t* read_offset) #endif uint64_t rank = bitrank(addr->occupieds[0], qfi->run % SLOTS_PER_BLOCK); - uint64_t next_run = bitselect(get_block(qfi->qf, + uint64_t next_run = bitselect(qfi_get_block(qfi, block_index)->occupieds[0], rank); if (next_run == 64) { rank = 0; - while (next_run == 64 && block_index < qfi->qf->metadata->nblocks) { + while (next_run == 64 && block_index < qfi->cache.nblocks) { block_index++; - next_run = bitselect(get_block(qfi->qf, block_index)->occupieds[0], + next_run = bitselect(qfi_get_block(qfi, block_index)->occupieds[0], rank); } } - if (block_index == qfi->qf->metadata->nblocks) { + if (block_index == qfi->cache.nblocks) { /* set the index values to max. */ - qfi->run = qfi->current = qfi->qf->metadata->xnslots; + qfi->run = qfi->current = qfi->cache.xnslots; return 1; } qfi->run = block_index * SLOTS_PER_BLOCK + next_run; @@ -2173,7 +2302,7 @@ int qfi_nextx(QFi *qfi, uint64_t* read_offset) inline int qfi_end(const QFi *qfi) { - if (qfi->current >= qfi->qf->metadata->xnslots /*&& is_runend(qfi->qf, qfi->current)*/) + if (qfi->current >= qfi->cache.xnslots /*&& is_runend(qfi->qf, qfi->current)*/) return 1; else return 0; diff --git a/src/validatemantis.cc b/src/validatemantis.cc index 9f09ca8..2645dd2 100644 --- a/src/validatemantis.cc +++ b/src/validatemantis.cc @@ -61,7 +61,7 @@ validate_main ( ValidateOpts& opt ) { spdlog::logger* console = opt.console.get(); - // Read experiment CQFs and cutoffs + // Read experiment CQFs std::ifstream infile(opt.inlist); uint64_t num_samples{0}; if (infile.is_open()) { @@ -82,19 +82,10 @@ validate_main ( ValidateOpts& opt ) cqfs = (CQF*)calloc(num_samples, sizeof(CQF)); - // Read cutoffs files - //std::unordered_map cutoffs; - //std::string sample_id; - //while (cutofffile >> sample_id >> cutoff) { - //std::pair pair(last_part(sample_id, '/'), cutoff); - //cutoffs.insert(pair); - //} - // mmap all the input cqfs std::string cqf_file; uint32_t nqf = 0; - uint32_t cutoff; - while (infile >> cqf_file >> cutoff) { + while (infile >> cqf_file) { if (!mantis::fs::FileExists(cqf_file.c_str())) { console->error("Squeakr file {} does not exist.", cqf_file); exit(1); @@ -103,9 +94,9 @@ validate_main ( ValidateOpts& opt ) std::string sample_id = first_part(first_part(last_part(cqf_file, '/'), '.'), '_'); console->info("Reading CQF {} Seed {}", nqf, cqfs[nqf].seed()); - console->info("Sample id {} cut-off {}", sample_id, cutoff); + console->info("Sample id {} cut-off {}", sample_id); cqfs[nqf].dump_metadata(); - inobjects[nqf] = SampleObject*>(&cqfs[nqf], cutoff, + inobjects[nqf] = SampleObject*>(&cqfs[nqf], sample_id, nqf); nqf++; } @@ -145,7 +136,7 @@ validate_main ( ValidateOpts& opt ) total_kmers); console->info("Total k-mers to query: {}", total_kmers); - // Query kmers in each experiment CQF ignoring kmers below the cutoff. + // Query kmers in each experiment CQF // Maintain the fraction of kmers present in each experiment CQF. std::vector> ground_truth; std::vector cdbg_output; @@ -153,14 +144,10 @@ validate_main ( ValidateOpts& opt ) for (auto kmers : multi_kmers) { std::unordered_map fraction_present; for (uint64_t i = 0; i < nqf; i++) { - uint32_t cutoff = inobjects[i].cutoff; for (auto kmer : kmers) { KeyObject k(kmer, 0, 0); uint64_t count = cqfs[i].query(k); - if (count < cutoff) - continue; - else - fraction_present[inobjects[i].id] += 1; + fraction_present[inobjects[i].id] += 1; } } // Query kmers in the cdbg diff --git a/src/walkEqcls.cc b/src/walkEqcls.cc deleted file mode 100644 index 66310cd..0000000 --- a/src/walkEqcls.cc +++ /dev/null @@ -1,937 +0,0 @@ - -#include "compressedSetBit.h" -#include "hashutil.h" -#include "bitvector.h" -#include "sdsl/bits.hpp" -#include "sparsepp/spp.h" - -#include -#include -#include -#include - - -void print_time_elapsed(string desc, struct timeval *start, struct timeval *end) { - struct timeval elapsed; - if (start->tv_usec > end->tv_usec) { - end->tv_usec += 1000000; - end->tv_sec--; - } - elapsed.tv_usec = end->tv_usec - start->tv_usec; - elapsed.tv_sec = end->tv_sec - start->tv_sec; - float time_elapsed = (elapsed.tv_sec * 1000000 + elapsed.tv_usec) / 1000000.f; - std::cout << desc << "Total Time Elapsed: " << to_string(time_elapsed) << " seconds" << std::endl; -} - -// @input -// cnt: setbit cnt -// num_samples: total number of bits -// @return true if the output of delta_compression/decompression is the same as bitvector_rrr -//template -bool validate(uint16_t cnt, uint16_t num_samples = 2586) { - std::cout << "\n[Validate]\n"; - - std::cout << "validate for " << cnt << " set bits out of " << num_samples << "\n"; - std::set randIdx; - - BitVector bv(num_samples); - std::vector idxList(cnt); - size_t i = 0; - while (i < cnt) { - //std::cout << "i:"< bvr_idxList; - uint16_t wrdCnt = 64; - for (uint16_t i = 0; i < num_samples; i += wrdCnt) { - wrdCnt = std::min((uint16_t) 64, (uint16_t) (num_samples - i)); - uint64_t wrd = bvr.get_int(i, wrdCnt); - for (uint16_t j = 0, idx = i; j < wrdCnt; j++, idx++) { - if (wrd >> j & 0x01) { - //std::cout << i << " " << j << "\n"; - bvr_idxList.push_back(idx); - } - } - } - //std::cout <<"\nidx size: " << idxList.size() << "\n"; - CompressedSetBit setBitList(idxList); - - vector output; - setBitList.uncompress(output); - //std::cout << "\nAfter compress&decompress size is " << output.size() << "\n"; - if (output.size() != bvr_idxList.size()) { - std::cout << "rrr idx list size: " << bvr_idxList.size() << " deltac size: " << output.size() << "\n"; - return false; - } - for (size_t i = 0; i < output.size(); i++) - if (output[i] != bvr_idxList[i]) { - std::cout << i << " rrr idx: " << bvr_idxList[i] << " deltac: " << output[i] << "\n"; - return false; - } - return true; -} - -/* - * validates if the rrr bvs in the input directory all together have unique rows - */ -void validate_uniqueness(std::string &directory, size_t num_samples) { - spp::sparse_hash_map> eqMap; - uint64_t cntr = 0; - - for (auto f = 0; f < 12; f++) { - uint64_t curCntr = 0; - std::string filename = directory + std::to_string(f) + "_eqclass_rrr.cls"; - BitVectorRRR eqcls(filename); - //subPattern.calcHash(); - uint64_t bitcntr = 0; - while (bitcntr < eqcls.bit_size()) { - //std::cerr << "\n" << curCntr << "\n"; - - BitVector eq(num_samples); - size_t i = 0; - while (i < num_samples) { - size_t bitCnt = std::min(num_samples - i, (size_t) 64); - size_t wrd = eqcls.get_int(curCntr * num_samples + i, bitCnt); - for (size_t j = 0, curIdx = i; j < bitCnt; j++, curIdx++) { - if ((wrd >> j) & 0x01) { - eq.set(curIdx); - - } /*else { - std::cerr << curIdx << " "; - }*/ - } - i += bitCnt; - bitcntr += bitCnt; - } - // Find if the eqclass of the kmer is already there. - // If it is there then increment the abundance. - // Else create a new eq class. - if (eqMap.find(eq) == eqMap.end()) { - eqMap.emplace(std::piecewise_construct, std::forward_as_tuple(eq), std::forward_as_tuple(cntr)); - } else { // eq class is seen before. Throw exception - std::cerr << "\nFound an already existing equivalence class pattern. eq. number " - << cntr << " file " << f << " cur cntr: " << curCntr - << " before: " << eqMap[eq] << "\n"; - std::exit(1); - } - cntr++; - curCntr++; - if (curCntr % 1000000 == 0) { - std::cerr << "passed " << curCntr << "th eq.\n"; - } - } - } - std::cerr << "Validation passed!!! Found " << cntr << " unique eq classes.\n"; - - -} - -void compareCompressions(std::string &filename, size_t num_samples) { - std::cout << "\n[CompareCompressions]\n"; - size_t gtBV = 0; - size_t gtBVR = 0; - size_t bvrGTbv = 0; - size_t bvEQbvr = 0; - size_t compressedSum = 0; - size_t bvSum = 0; - size_t bvrSum = 0; - - size_t roundIdxCnt = 0; - size_t totalIdxCnt = 0; - BitVectorRRR eqcls(filename); - size_t totalEqClsCnt = eqcls.bit_size() / num_samples; //222584822; - std::cout << "Total bit size: " << eqcls.bit_size() << "\ntotal # of equivalence classes: " << totalEqClsCnt - << "\n"; - for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { - BitVector bv(num_samples); - std::vector idxList; - idxList.reserve(num_samples); - size_t i = 0; - while (i < num_samples) { - size_t bitCnt = std::min(num_samples - i, (size_t) 64); - size_t wrd = eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); - for (size_t j = 0, curIdx = i; j < bitCnt; j++, curIdx++) { - if ((wrd >> j) & 0x01) { - bv.set(curIdx); - idxList.push_back(curIdx); - } - } - i += bitCnt; - } - //if (idxList.size() == 0) std::cerr << "Error!! " << eqclsCntr << " Shouldn't ever happen\n"; - totalIdxCnt += idxList.size(); - roundIdxCnt += idxList.size(); - if (eqclsCntr != 0 && (eqclsCntr) % 1000000 == 0) { - std::cout << "\n\nTotal number of experiments are : " << eqclsCntr << - "\nTotal average set bits: " << totalIdxCnt / eqclsCntr << - "\nThis round average set bits: " << roundIdxCnt / 1000000 << - "\nbv average size : " << bvSum / eqclsCntr << - "\nbvr average size : " << bvrSum / eqclsCntr << - "\ncompressed average size : " << compressedSum / eqclsCntr << - "\ncompressed > bv : " << gtBV << " or " << (gtBV * 100) / eqclsCntr << "% of the time" << - "\ncompressed > bvr : " << gtBVR << " or " << (gtBVR * 100) / eqclsCntr << "% of the time" << - "\nbvr > bv : " << bvrGTbv << " or " << (bvrGTbv * 100) / eqclsCntr << "% of the time" << - "\n"; - roundIdxCnt = 0; - } - - BitVectorRRR bvr(bv); - CompressedSetBit setBitList(idxList); - - size_t bvSize = bv.bit_size()/8;//size_in_bytes(); - size_t bvrSize = bvr.bit_size()/8;//size_in_bytes(); - size_t compressedSize = setBitList.size_in_bytes(); - bvSum += bvSize; - bvrSum += bvrSize; - compressedSum += compressedSize; - if (compressedSize > bvSize) gtBV++; - if (compressedSize > bvrSize) gtBVR++; - if (bvrSize > bvSize) bvrGTbv++; - if (bvrSize == bvSize) bvEQbvr++; - //vector output; - //setBitList.uncompress(output); - } - std::cout << "\n\n\nFinalResults:\n" << - "Total number of experiments are : " << totalEqClsCnt << "\n" << - "\nbv average size : " << bvSum / totalEqClsCnt << - "\nbvr average size : " << bvrSum / totalEqClsCnt << - "\ncompressed average size : " << compressedSum / totalEqClsCnt << - "\nHow many times compressed > bv : " << gtBV << " or " << (gtBV * 100) / totalEqClsCnt << "% of the time" - << - "\nHow many times compressed > bvr : " << gtBVR << " or " << (gtBVR * 100) / totalEqClsCnt - << "% of the time" << - "\nHow many times bvr > bv : " << bvrGTbv << " or " << (bvrGTbv * 100) / totalEqClsCnt << "% of the time" - << - "\nHow many times bvr == bv : " << bvEQbvr << " or " << (bvEQbvr * 100) / totalEqClsCnt << "% of the time" - << - "\n"; -} - -void compareCopies(size_t num_samples) { - std::cout << "\n[CompareCopies]\n"; - - struct timeval start, end; - struct timezone tzp; - - size_t numOfCopies = 10; - - size_t bit_cnt = 1000000000; - size_t i; - sdsl::bit_vector bits(bit_cnt); - srand(time(NULL)); - - gettimeofday(&start, &tzp); - for (auto i = 0; i < 1000000; i++) { - bits[rand() % bit_cnt] = 1; - } - gettimeofday(&end, &tzp); - print_time_elapsed("Set random bits done: ", &start, &end); - - sdsl::bit_vector bits2(bit_cnt); - sdsl::bit_vector bits3(bit_cnt); - sdsl::bit_vector bits4(bit_cnt + 1); - - gettimeofday(&start, &tzp); - for (auto c = 0; c < numOfCopies; c++) { - bits2 = bits; - } - gettimeofday(&end, &tzp); - print_time_elapsed("MemCpy: ", &start, &end); - - gettimeofday(&start, &tzp); - for (auto c = 0; c < numOfCopies; c++) { - i = 0; - while (i < bit_cnt) { - if (bits[i]) - bits3[i] = 1; - i++; - } - } - - gettimeofday(&end, &tzp); - print_time_elapsed("Index by index!: ", &start, &end); - - gettimeofday(&start, &tzp); - for (auto c = 0; c < numOfCopies; c++) { - i = 0; - size_t j = 1; - while (i < bit_cnt) { - size_t bitCnt = std::min(bit_cnt - i, (size_t) 64); - size_t wrd = bits.get_int(i, bitCnt); - bits4.set_int(j, wrd, bitCnt); - i += bitCnt; - j += bitCnt; - } - } - gettimeofday(&end, &tzp); - print_time_elapsed("Read int and write int: ", &start, &end); - -} - -void splitRows(std::string filename, - std::string outdir, - uint64_t num_samples = 2586) { - //std::cerr << "1\n"; - std::string eqfile; - std::ifstream eqlist(filename); - std::ofstream bvlist(outdir+"/list.txt"); - uint64_t totalEqCls = 0; - uint64_t rowcntr = 0; - if (eqlist.is_open()) { - while (getline(eqlist, eqfile)) { - sdsl::rrr_vector<63> bvr; - sdsl::load_from_file(bvr, eqfile); - std::cerr << "loaded " << bvr.size() << "\n"; - totalEqCls = bvr.size()/num_samples; - std::cerr << "total eq in this set: " << totalEqCls << "\t"; - std::cerr << "created\n"; - for (uint64_t eqcntr = 0; eqcntr < totalEqCls; eqcntr++) { - sdsl::bit_vector eqcls(num_samples, 0); - uint64_t i = 0; - while (i < num_samples) { - uint64_t bitCnt = std::min(num_samples - i, (uint64_t) 64); - uint64_t wrd = bvr.get_int(eqcntr*num_samples+i, bitCnt); - //std::cerr << wrd << " "; - eqcls.set_int(i, wrd, bitCnt); - i+=bitCnt; - } - std::string outbvfile = outdir + "/row" + std::to_string(rowcntr) + ".sim.bf.bv"; - sdsl::store_to_file(eqcls, outbvfile); - bvlist << outbvfile << "\n"; - rowcntr++; - } - } - eqlist.close(); - bvlist.close(); - } - -} - -void splitColumns(std::string filename, - std::string outdir, - uint64_t totalEqClsCnt = 222000000, - uint64_t num_samples = 2586) { - std::string eqfile; - std::ifstream eqlist(filename); - std::ofstream bvlist(outdir+"/collist.txt"); - std::vector cols(num_samples); - //std::vector wrds(num_samples); - for (auto &bv : cols) bv = sdsl::bit_vector(totalEqClsCnt, 0); - if (eqlist.is_open()) { - uint64_t accumTotalEqCls = 0, lineCntr{0}; - while (getline(eqlist, eqfile)) { - //if (lineCntr >= 10) { - sdsl::rrr_vector<63> bvr; - std::cerr << "file: " << eqfile << "\n"; - sdsl::load_from_file(bvr, eqfile); - std::cerr << "loaded " << bvr.size() - << " accumulative Eq Cnt: " << accumTotalEqCls << "\n"; - //std::cerr << "total eq in this set: " << totalEqCls << "\t"; - //std::cerr << "created\n"; - uint64_t b = 0; - while (b < bvr.size() and accumTotalEqCls < totalEqClsCnt) { - uint64_t c{0}, bitcnt{0}, wrd{0}, sampleCntr{0}; - while (c < num_samples) { - bitcnt = std::min(num_samples - c, (uint64_t) 64); - wrd = bvr.get_int(b + c, bitcnt); - for (uint64_t j = 0; j < bitcnt; j++) { - if ((wrd >> j) & 0x01) { - cols[sampleCntr][accumTotalEqCls] = 1; - } - sampleCntr++; - } - c += bitcnt; - } - /*for (uint64_t c = 0; c < num_samples; c++) { - if (bvr[(b + c)]) { - cols[c][accumTotalEqCls] = 1; - } - }*/ - //std::cerr << "\n"; - accumTotalEqCls++; - if (accumTotalEqCls % 1000000 == 0 || accumTotalEqCls > 222584820) { - std::cerr << accumTotalEqCls << " processed\n"; - } - b += num_samples; - } - /*} else { - lineCntr++; - accumTotalEqCls += 20000000; - }*/ - } - std::cerr << "\n\nColumn bvs ready to be stored to file .. \n"; - uint64_t colcntr{0}; - for (auto& v : cols) { - std::string outbvfile = outdir + "/col" + std::to_string(colcntr) + ".sim.bf.bv"; - sdsl::store_to_file(v, outbvfile); - bvlist << outbvfile << "\n"; - colcntr++; - } - - eqlist.close(); - bvlist.close(); - } - -} - - -void decompress(std::string filename, - std::string outfilename, - uint64_t totalEqCls, - uint64_t num_samples = 2586) { - //std::cerr << "1\n"; - sdsl::rrr_vector<63> bvr; - sdsl::load_from_file(bvr, filename); - //sdsl::rank_support_rrr<1, 63> ranks(&bvr); - //std::cerr << "1\n"; - //totalEqCls = 1; - size_t prev = 0; - size_t cur = 0; - //std::cerr << "1\n"; - /*for (uint64_t i = 1; i < 21; i++) { - cur = ranks(2586 * i); - std::cout << "rank: " << cur << " " << cur - prev << "\n"; - prev = cur; - }*/ - //std::exit(1); - std::cerr << "loaded " << bvr.size() << "\n"; - sdsl::bit_vector eqcls(totalEqCls * num_samples, 0); - std::cerr << "created\n"; - for (uint64_t i = 0; i < totalEqCls * num_samples; i += 64) { - uint64_t bitCnt = std::min(totalEqCls * num_samples - i, (uint64_t) 64); - uint64_t wrd = bvr.get_int(i, bitCnt); - //std::cerr << wrd << " "; - eqcls.set_int(i, wrd, bitCnt); - } - sdsl::store_to_file(eqcls, outfilename); -} - -void compare_reordered_original(std::string filename1, - std::string filename2, - std::string order_filename, - uint64_t num_samples) { - std::cout << "\n[compare_reordered_original]\n"; - - struct timeval start, end; - struct timezone tzp; - - uint64_t setBitCnt1{0}, setBitCnt2{0}; - std::ifstream orderFile(order_filename); - std::vector newOrder; - while (!orderFile.eof()) { - size_t val; - orderFile >> val; - //std::cout << val << " "; - newOrder.push_back(val); - } - std::cout << "\n"; - sdsl::bit_vector eqcls; - sdsl::bit_vector reordered_eqcls; - sdsl::load_from_file(eqcls, filename1); - sdsl::load_from_file(reordered_eqcls, filename2); - size_t totalEqClsCnt = reordered_eqcls.size() / num_samples; //222584822; - std::cout << "Total number of Eq. Clss: " << totalEqClsCnt << "\n"; - //totalEqClsCnt = 1000001; - gettimeofday(&start, &tzp); - - for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { - - std::vector eqcls_setbits; - std::vector reordered_eqcls_setbits; - eqcls_setbits.reserve(num_samples); - reordered_eqcls_setbits.reserve(num_samples); - - // transpose the matrix of eq. classes - size_t i = 0; - while (i < num_samples) { - size_t bitCnt = std::min(num_samples - i, (size_t) 64); - size_t wrd1 = eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); - size_t wrd2 = reordered_eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); - for (size_t j = 0, curIdx = i; j < bitCnt; j++, curIdx++) { - if ((wrd1 >> j) & 0x01) { - setBitCnt1++; - eqcls_setbits.push_back(newOrder[curIdx]); - } - if ((wrd2 >> j) & 0x01) { - setBitCnt2++; - reordered_eqcls_setbits.push_back(curIdx); - } - - } - i += bitCnt; - } - std::sort(eqcls_setbits.begin(), eqcls_setbits.end()); - std::sort(reordered_eqcls_setbits.begin(), reordered_eqcls_setbits.end()); - - if (eqcls_setbits != reordered_eqcls_setbits) { - std::cout << "ERROR! EQ CLSES in ROW: " << eqclsCntr << " NOT THE SAME.\n"; - std::exit(1); - } - if (eqclsCntr != 0 && (eqclsCntr) % 1000000 == 0) { - gettimeofday(&end, &tzp); - std::stringstream ss; - ss << eqclsCntr << " eqclses processed, "; - print_time_elapsed(ss.str(), &start, &end); - gettimeofday(&start, &tzp); - } - - } - if (setBitCnt1 != setBitCnt2) { - std::cout << "ERROR! EQ CLSES SET BITS NOT THE SAME: " << setBitCnt1 << " VS " << setBitCnt2 << "\n"; - std::exit(1); - } - std::cout << "\nValidation Passed!\n\n"; -} - -void reorder(std::string filename, - std::string out_filename, - std::vector &newOrder, - uint64_t num_samples) { - std::cout << "\n[Reorder]\n"; - struct timeval start, end; - struct timezone tzp; - - //std::cout << "\n"; - sdsl::bit_vector eqcls; - sdsl::load_from_file(eqcls, filename); - size_t totalEqClsCnt = eqcls.size() / num_samples; //222584822; - std::cout << "Total number of Eq. Clss: " << totalEqClsCnt << "\n"; - //totalEqClsCnt = 1280000; - sdsl::bit_vector reordered_eqcls(eqcls.size(), 0); - gettimeofday(&start, &tzp); - - for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { - - // transpose the matrix of eq. classes - size_t i = 0; - while (i < num_samples) { - size_t bitCnt = std::min(num_samples - i, (size_t) 64); - size_t wrd = eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); - for (size_t j = 0, curIdx = i; j < bitCnt; j++, curIdx++) { - if ((wrd >> j) & 0x01) { - //std::cout << curIdx << ":" << newOrder[curIdx] << " "; - reordered_eqcls[eqclsCntr * num_samples + newOrder[curIdx]] = 1; - } - } - i += bitCnt; - } - - //std::cout << "\n"; - //std::exit(1); - if (eqclsCntr != 0 && (eqclsCntr) % 1000000 == 0) { - gettimeofday(&end, &tzp); - std::stringstream ss; - ss << eqclsCntr << " eqclses processed, "; - print_time_elapsed(ss.str(), &start, &end); - gettimeofday(&start, &tzp); - } - } - std::cout << "\noutfilename: " << out_filename << "\n"; - for (auto idx = 0; idx < num_samples; idx++) { - std::cout << reordered_eqcls[idx]; - } - std::cout << "\n"; - for (size_t eqclsCntr = 0; eqclsCntr < 2; eqclsCntr++) { - for (size_t i = 0; i < num_samples; i++) { - std::cout << reordered_eqcls[num_samples * eqclsCntr + i]; - } - std::cout << "\n"; - } - sdsl::store_to_file(reordered_eqcls, out_filename); - -} - -void reorder(std::string filename, - std::string out_filename, - std::string order_filename, - uint64_t num_samples) { - - std::ifstream orderFile(order_filename); - std::vector newOrder; - newOrder.resize(num_samples); - uint64_t cntr = 0; - while (!orderFile.eof()) { - size_t val; - orderFile >> val; - //std::cout << val << " "; - newOrder[val] = cntr++; - //newOrder.push_back(val); - } - - reorder(filename, out_filename, newOrder, num_samples); -} - -void build_fakeEq_And_randReord(std::string outfile, - uint64_t totalEqClsCnt = 1280000, - uint64_t num_samples = 2586, - uint64_t wordSize = 8) { - std::cout << "\n[Build_fakeEq_And_randReord]\n"; - uint64_t bvSize = num_samples * totalEqClsCnt; - sdsl::bit_vector eqcls(bvSize, 0); - - for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { - size_t i = 0; - while (i < num_samples) { - uint64_t wrd = rand() % 2 ? 0 : -1; - size_t bitCnt = std::min(num_samples - i, (size_t) wordSize); - eqcls.set_int(eqclsCntr * num_samples + i, wrd, bitCnt); - //wrd = wrd?0:-1; - i += bitCnt; - } - } - - for (size_t eqclsCntr = 0; eqclsCntr < 2; eqclsCntr++) { - for (size_t i = 0; i < num_samples; i++) { - std::cout << eqcls[num_samples * eqclsCntr + i]; - } - std::cout << "\n"; - } - sdsl::store_to_file(eqcls, outfile + ".eq"); - - std::vector randomizedIndex(num_samples); - for (uint64_t i = 0; i < num_samples; i++) - randomizedIndex[i] = i; - std::random_shuffle(randomizedIndex.begin(), randomizedIndex.end()); - for (size_t i = 0; i < num_samples; i++) { - std::cout << randomizedIndex[i] << " "; - } - std::cout << "\n"; - reorder(outfile + ".eq", outfile + "_randOrder.eq", randomizedIndex, num_samples); -} - -void build_distMat(std::string filename, - std::string out_filename, - uint64_t num_samples) { - std::cout << "\n[Build_distMat]\n"; - struct timeval start, end, row2colStart, colPairCompStart; - struct timezone tzp; - - std::vector> hamDistMat(num_samples); - for (auto &v : hamDistMat) { - v.resize(num_samples); - for (auto &d : v) d = 0; - } - - std::vector> mutInfoMat(num_samples); - std::vector> jointMat(num_samples); - for (auto &v : mutInfoMat) { - v.resize(num_samples); - for (auto &d : v) d = 0; - } - for (auto &v : jointMat) { - v.resize(num_samples); - for (auto &d : v) d = 0; - } - - //BitVectorRRR eqcls(filename); - sdsl::bit_vector eqcls; - sdsl::load_from_file(eqcls, filename); - - size_t totalEqClsCnt = eqcls.bit_size() / num_samples; //222584822; - //totalEqClsCnt = 1280000; - //sdsl::bit_vector uncompressed(totalEqClsCnt*num_samples, 0); - std::cout << "Total bit size: " << eqcls.bit_size() - << "\ntotal # of equivalence classes: " << totalEqClsCnt << "\n"; - - std::vector cols(num_samples); - for (auto &bv : cols) bv = sdsl::bit_vector(totalEqClsCnt, 0); - - gettimeofday(&start, &tzp); - gettimeofday(&row2colStart, &tzp); - - for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { - - // transpose the matrix of eq. classes - size_t i = 0; - while (i < num_samples) { - size_t bitCnt = std::min(num_samples - i, (size_t) 64); - size_t wrd = eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); - //uncompressed.set_int(eqclsCntr*num_samples+i, wrd, bitCnt); - for (size_t j = 0, curIdx = i; j < bitCnt; j++, curIdx++) { - if ((wrd >> j) & 0x01) { - cols[curIdx][eqclsCntr] = 1; - } - } - i += bitCnt; - } - - if (eqclsCntr != 0 && (eqclsCntr) % 1000000 == 0) { - gettimeofday(&end, &tzp); - std::stringstream ss; - ss << eqclsCntr << " eqclses processed, "; - print_time_elapsed(ss.str(), &start, &end); - gettimeofday(&start, &tzp); - } - } - //sdsl::store_to_file(uncompressed, "test.eq"); - gettimeofday(&end, &tzp); - print_time_elapsed("DONE!, ", &row2colStart, &end); - gettimeofday(&start, &tzp); - - gettimeofday(&colPairCompStart, &tzp); - - // calc ham distance - size_t bitCnt = 64; - for (size_t k = 0; k < num_samples; k++) { - for (size_t j = k + 1; j < num_samples; j++) { - uint64_t dist = 0; - uint64_t mutprob[4]; - uint64_t denum = totalEqClsCnt; - for (int b = 0; b < 4; b++) mutprob[b] = 0; - uint64_t indivprob[2]; - for (int b = 0; b < 2; b++) indivprob[b] = 0; - //calculate distance between column i and j - size_t i = 0; - while (i < totalEqClsCnt) { - bitCnt = std::min(totalEqClsCnt - i, (size_t) 64); - uint64_t wrd1 = cols[k].get_int(i, bitCnt); - uint64_t wrd2 = cols[j].get_int(i, bitCnt); - - // compare words - // xor & pop count - dist += sdsl::bits::cnt(wrd1 ^ wrd2); - - indivprob[0] += sdsl::bits::cnt(wrd1); - indivprob[1] += sdsl::bits::cnt(wrd2); - mutprob[0] += sdsl::bits::cnt((~wrd1) & (~wrd2)) - (64 - bitCnt); - mutprob[1] += sdsl::bits::cnt((~wrd1) & wrd2); - mutprob[2] += sdsl::bits::cnt(wrd1 & (~wrd2)); - mutprob[3] += sdsl::bits::cnt(wrd1 & wrd2); - i += bitCnt; - } - hamDistMat[k][j] = dist; - hamDistMat[j][k] = dist; - - - /* mutInfoMat[k][j] = - (mutprob[0]?mutprob[0]*(log2(denum)+log2(mutprob[0])- log2(denum - indivprob[0])-log2(denum - indivprob[1])):0) + - (mutprob[1]?mutprob[1]*(log2(denum)+log2(mutprob[1])- log2(denum - indivprob[0])-log2(indivprob[1])):0) + - (mutprob[2]?mutprob[2]*(log2(denum)+log2(mutprob[2])- log2(indivprob[0])-log2(denum - indivprob[1])):0) + - (mutprob[3]?mutprob[3]*(log2(denum)+log2(mutprob[3])- log2(indivprob[0])-log2(indivprob[1])):0); - mutInfoMat[k][j] /= (double)(denum); - mutInfoMat[j][k] = mutInfoMat[k][j]; */ - - mutInfoMat[k][j] = - (mutprob[0] ? mutprob[0] * log2((double) (denum * mutprob[0]) / - (double) ((denum - indivprob[0]) * (denum - indivprob[1]))) : 0) + - (mutprob[1] ? mutprob[1] * - log2((double) (denum * mutprob[1]) / (double) ((denum - indivprob[0]) * indivprob[1])) - : 0) + - (mutprob[2] ? mutprob[2] * log2((double) (denum * mutprob[2]) / - (double) ((indivprob[0]) * (denum - indivprob[1]))) : 0) + - (mutprob[3] ? mutprob[3] * - log2((double) (denum * mutprob[3]) / (double) (indivprob[0] * (indivprob[1]))) : 0); - mutInfoMat[k][j] /= (double) (denum); - mutInfoMat[j][k] = mutInfoMat[k][j]; - - jointMat[k][j] = - (mutprob[0] ? mutprob[0] * log2((double) (mutprob[0]) / (double) denum) : 0) + - (mutprob[1] ? mutprob[1] * log2((double) (mutprob[1]) / (double) denum) : 0) + - (mutprob[2] ? mutprob[2] * log2((double) (mutprob[2]) / (double) denum) : 0) + - (mutprob[3] ? mutprob[3] * log2((double) (mutprob[3]) / (double) denum) : 0); - jointMat[k][j] /= (-1.0 * (double) (denum)); - jointMat[j][k] = jointMat[k][j]; - /* std::cout << mutInfoMat[k][j] << " : " - << mutprob[0] << " " - << mutprob[1] << " " - << mutprob[2] << " " - << mutprob[3] << " " - << indivprob[0] << " " - << indivprob[1] << " " - << denum << "\n"; */ - - //if (mutInfoMat[k][j]) std::cout << mutInfoMat[k][j] << "\n"; - } - if (k % 100 == 0) { - gettimeofday(&end, &tzp); - std::stringstream ss; - ss << k << ",";//<< j << ", "; - print_time_elapsed(ss.str(), &start, &end); - gettimeofday(&start, &tzp); - } - } - gettimeofday(&end, &tzp); - - print_time_elapsed(" DONE!, ", &start, &end); - gettimeofday(&colPairCompStart, &tzp); - - std::ofstream out(out_filename + ".ham"); - std::ofstream mutout(out_filename + ".mutinf"/* ".mutinfo" */); - //std::ofstream jout(out_filename+".joint"); - //out << hamDistMat.size() << "\n"; - double mutInfoInv = 0; - - for (size_t i = 0; i < hamDistMat.size(); i++) { - out << hamDistMat[i][0]; - mutInfoInv = (jointMat[i][0] - mutInfoMat[i][0]) < 0 ? 0 : (jointMat[i][0] - mutInfoMat[i][0]); - mutout << mutInfoInv; - for (size_t j = 1; j < hamDistMat[i].size(); j++) { - out << "\t" << hamDistMat[i][j]; - mutInfoInv = (jointMat[i][j] - mutInfoMat[i][j]) < 0 ? 0 : (jointMat[i][j] - mutInfoMat[i][j]); - mutout << "\t" << mutInfoInv; - //jout << jointMat[i][j] << "\t"; - //if (hamDistMat[i][j] != 0) { - - //std::cout << j << " "; - // out << i << "\t" << j << "\t" << hamDistMat[i][j] << "\n"; - //} - //if (mutInfoMat[i][j] != 0) { - // mutout << i << "\t" << j << "\t" << mutInfoMat[i][j] << "\t" << jointMat[i][j] << "\n"; - //} - } - out << "\n"; - mutout << "\n"; - //jout << "\n"; - } -} - -void writeEq(std::string filename, - std::string out_filename, - uint64_t num_samples, - uint64_t totalEqCls) { - sdsl::rrr_vector<63> eqcls; - sdsl::load_from_file(eqcls, filename); - std::ofstream out(out_filename + ".matrix"); - size_t totalEqClsCnt = eqcls.size() / num_samples; //222584822; - if (totalEqCls > 0) { - totalEqClsCnt = totalEqCls; - } - for (size_t eqclsCntr = 0; eqclsCntr < totalEqClsCnt; eqclsCntr++) { - size_t i = 0; - while (i < num_samples) { - size_t bitCnt = std::min(num_samples - i, (size_t) 64); - size_t wrd = eqcls.get_int(eqclsCntr * num_samples + i, bitCnt); - for (size_t j = 0; j < bitCnt; j++) { - if (((wrd >> j) & 0x01)) - out << "1"; - else - out <<" "; - //out << ((wrd >> j) & 0x01) << "\t"; - } - i += bitCnt; - } - out << "\n"; - } - out.close(); -} - -int main(int argc, char *argv[]) { - - uint64_t num_samples = 2586; - std::string command = argv[1]; - - if (command == "validate") { - std::cout << "validate for different number of set bits\n"; - for (uint16_t i = 1; i <= num_samples; i++) { - if (!validate(i, num_samples)) { - std::cerr << "ERROR: NOT VALIDATED\n"; - std::exit(1); - } - } - std::cout - << "SUCCESSFULLY VALIDATED THE COMPRESSION/DECOMPRESSION PROCESS.\n\n#####NEXT STEP#####\nCompare Sizes:\n"; - } else if (command == "decompress") { - std::string filename = argv[2]; - std::string outfilename = argv[3]; - uint64_t totalEqCls = std::stoull(argv[4]); - decompress(filename, outfilename, totalEqCls, num_samples); - } else if (command == "compareCompressions") { - std::string filename = argv[2]; - compareCompressions(filename, num_samples); - } else if (command == "compareCopies") - compareCopies(num_samples); - else if (command == "distMat") { - if (argc < 4) { - std::cerr << "ERROR: MISSING LAST ARGUMENT\n"; - std::exit(1); - } - std::string filename = argv[2]; - std::string output_filename = argv[3]; - build_distMat(filename, output_filename, num_samples); - } else if (command == "writeEq") { - if (argc < 4) { - std::cerr << "ERROR: MISSING LAST ARGUMENT\n"; - std::exit(1); - } - std::string filename = argv[2]; - std::string output_filename = argv[3]; - num_samples = stoull(argv[4]); - uint64_t totalEqCls = 0; - if (argc == 6) - totalEqCls = std::stoull(argv[5]); - - writeEq(filename, output_filename, num_samples, totalEqCls); - } else if (command == "reorder") { - if (argc < 5) { - std::cerr << "ERROR: MISSING ARGUMENT. 4 needed.\n"; - std::exit(1); - } - std::string filename = argv[2]; - std::string output_filename = argv[3]; - std::string order_filename = argv[4]; - reorder(filename, output_filename, order_filename, num_samples); - } else if (command == "compare_reordered_original") { - if (argc < 5) { - std::cerr << "ERROR: MISSING ARGUMENT. 4 needed.\n"; - std::exit(1); - } - std::string filename1 = argv[2]; - std::string filename2 = argv[3]; - std::string order_filename = argv[4]; - compare_reordered_original(filename1, filename2, order_filename, num_samples); - } else if (command == "fakeEq_randOrder") { - std::string outfile = argv[2]; - uint64_t rows = 1048576;//2^20; - build_fakeEq_And_randReord(outfile, - rows, - num_samples, - 8); - } else if (command == "validate_uniqueness") { - std::string dir = argv[2]; - num_samples = stoull(argv[3]); - validate_uniqueness(dir, num_samples); - } else if (command == "splitRows") { - std::string filename = argv[2]; - std::string outdir = argv[3]; - num_samples = stoull(argv[4]); - splitRows(filename, outdir, num_samples); - } else if (command == "splitColumns") { - std::string filename = argv[2]; - std::string outdir = argv[3]; - num_samples = stoull(argv[4]); - uint64_t num_eqs = stoull(argv[5]); - splitColumns(filename, outdir, num_eqs, num_samples); - } else if (command == "quickvalidation") { - std::string filename = argv[2]; - sdsl::bit_vector eqcls; - sdsl::load_from_file(eqcls, filename); - std::ofstream out(filename + ".seq"); - uint64_t i = 0; - out << "start " << eqcls.bit_size() << "\n"; - while (i < eqcls.bit_size()) { - size_t wrd = eqcls.get_int(i, 64); - for (size_t j = 0; j < 64; j++) { - if (((wrd >> j) & 0x01)) { - std::cerr << (i+j) << " "; - } - } - i += 64; - } - out.close(); - - } else { - std::cerr << "ERROR: NO COMMANDS PROVIDED\n" - << "OPTIONS ARE: validate, compareCompressions, compareCopies, reorder\n"; - std::exit(1); - } - -} - From e42078014af07c33874ae93cdc611f0f96168c2b Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Wed, 15 Aug 2018 15:35:11 -0400 Subject: [PATCH 083/122] Adding walkCqf back which for now contains just filtering cqf --- src/CMakeLists.txt | 12 +++++----- src/walkCqf.cc | 58 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8954af6..b884a7e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -93,12 +93,12 @@ add_executable(mstBoost MST_boost.cc) target_link_libraries(mstBoost mantis_core) set_property(TARGET mstBoost PROPERTY INTERPROCEDURAL_OPTIMIZATION True) -#add_executable(walkCqf walkCqf.cc) -#target_include_directories(walkCqf PUBLIC -# $) -#target_link_libraries(walkCqf -# mantis_core) -#set_property(TARGET walkCqf PROPERTY INTERPROCEDURAL_OPTIMIZATION True) +add_executable(walkCqf walkCqf.cc) +target_include_directories(walkCqf PUBLIC + $) +target_link_libraries(walkCqf + mantis_core) +set_property(TARGET walkCqf PROPERTY INTERPROCEDURAL_OPTIMIZATION True) #add_executable(walkEqcls walkEqcls.cc) #target_include_directories(walkEqcls PUBLIC diff --git a/src/walkCqf.cc b/src/walkCqf.cc index 7f6cd6a..95a087a 100644 --- a/src/walkCqf.cc +++ b/src/walkCqf.cc @@ -46,6 +46,34 @@ #define BITMASK(nbits) ((nbits) == 64 ? 0xffffffffffffffff : (1ULL << (nbits)) - 1ULL) +struct Iterator { + QFi qfi; + uint64_t kmer; + uint64_t val; + uint64_t cnt; + Iterator(const QF& cqf) { + if (qf_iterator(&cqf, &qfi, 0)) get_key(); + } + void next() { + qfi_next(&qfi); + get_key(); + } + bool end() const { + return qfi_end(&qfi); + } + bool operator>(const Iterator& rhs) const { + return key() > rhs.key(); + } + const uint64_t key() const { return kmer; } + const uint64_t value() const { return val; } + const uint64_t count() const { return cnt; } + +private: + void get_key() { + qfi_get(&qfi, &kmer, &val, &cnt); + } +}; + /* Print elapsed time using the start and end timeval */ void print_time_elapsed(string desc, struct timeval* start, struct timeval* end) { @@ -74,7 +102,7 @@ void run_filter(std::string ds_file, cout << "Done loading cqf in time: "; gettimeofday(&end, &tzp); print_time_elapsed("", &start, &end); - typename CQF::Iterator it = cqf.begin(0); + Iterator it(cqf.cqf);//= cqf.begin(0); uint64_t quotientBits = std::ceil(std::log2(approximate_num_of_kmers_greater_than_cutoff))+1; std::cout << "quotientBits : " << quotientBits << "\n"; @@ -83,20 +111,22 @@ void run_filter(std::string ds_file, gettimeofday(&start, &tzp); uint64_t cntr = 1; uint64_t insertedCntr = 0; - while (!it.done()) { - KeyObject k = *it; + while (!it.end()) { + //KeyObject k = *it; + KeyObject k(it.key(), it.value(), it.count()); if (k.count >= cutoff) { k.count = 1;//cutoff; newCqf.insert(k); insertedCntr++; } - ++it; + //++it; + it.next(); if (cntr % 10000000 == 0) { std::cout << cntr << " kmers passed, " - << insertedCntr << " kmers inserted, " - << newCqf.noccupied_slots() << " slots occupied, " - << " load factor: " - << static_cast(newCqf.noccupied_slots())/static_cast(1ULL << quotientBits) << "\n"; + << insertedCntr << " kmers inserted" ; + //<< newCqf.noccupied_slots() << " slots occupied, " + //<< " load factor: " + //<< static_cast(newCqf.noccupied_slots())/static_cast(1ULL << quotientBits) << "\n"; } cntr++; } @@ -108,6 +138,7 @@ void run_filter(std::string ds_file, void run_list_kmers(std::string ds_file, std::string out_file) { +/* struct timeval start, end; struct timezone tzp; @@ -118,23 +149,26 @@ void run_list_kmers(std::string ds_file, cout << "Done loading cqf in time: "; gettimeofday(&end, &tzp); print_time_elapsed("", &start, &end); - typename CQF::Iterator it = cqf.begin(0); + //typename CQF::Iterator it = cqf.begin(0); gettimeofday(&start, &tzp); std::ofstream fout(out_file, ios::out); - while (!it.done()) { - KeyObject k = *it; + while (!it.end()) { + //KeyObject k = *it; + KeyObject k(it.key(), it.value(), it.count()); // kmers.push_back(HashUtil::hash_64i(k.key, BITMASK(cqf.keybits()))); uint64_t kint = HashUtil::hash_64i(k.key, BITMASK(cqf.keybits())); //if (k.count >= cutoff) { std::string kstr = Kmer::int_to_str(kint); fout << kstr << "\t" << k.count << "\n"; //} - ++it; + //++it; + it.next(); } fout.close(); gettimeofday(&end, &tzp); print_time_elapsed("", &start, &end); +*/ } /* From f3351bde71d16c4c48af6af1942d953494c3e493 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Thu, 16 Aug 2018 13:17:56 -0400 Subject: [PATCH 084/122] change the name of the file validateMSF to walkMSF since it does more things than just validating (querying etc.) --- src/CMakeLists.txt | 8 ++++---- src/{validateMSF.cc => walkMSF.cc} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename src/{validateMSF.cc => walkMSF.cc} (100%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b884a7e..51dbbd6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -80,12 +80,12 @@ target_link_libraries(msf mantis_core) set_property(TARGET msf PROPERTY INTERPROCEDURAL_OPTIMIZATION True) -add_executable(loadColorCls validateMSF.cc) -target_include_directories(loadColorCls PUBLIC +add_executable(walkMSF walkMSF.cc) +target_include_directories(walkMSF PUBLIC $) -target_link_libraries(loadColorCls +target_link_libraries(walkMSF mantis_core) -set_property(TARGET loadColorCls PROPERTY INTERPROCEDURAL_OPTIMIZATION True) +set_property(TARGET walkMSF PROPERTY INTERPROCEDURAL_OPTIMIZATION True) add_executable(mstBoost MST_boost.cc) target_include_directories(mstBoost PUBLIC diff --git a/src/validateMSF.cc b/src/walkMSF.cc similarity index 100% rename from src/validateMSF.cc rename to src/walkMSF.cc From d724904f4fa2aa930e2727f8feae13238ad1e487 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Thu, 16 Aug 2018 14:55:07 -0400 Subject: [PATCH 085/122] Just reverting added functionality to gqf (which is not used anymore) --- include/cqf/gqf.h | 1 - src/cqf/gqf.c | 31 ------------------------------- 2 files changed, 32 deletions(-) diff --git a/include/cqf/gqf.h b/include/cqf/gqf.h index 9565cb3..4339226 100644 --- a/include/cqf/gqf.h +++ b/include/cqf/gqf.h @@ -150,7 +150,6 @@ extern "C" { value, into qf. */ uint64_t qf_count_key_value(const QF *qf, uint64_t key, uint64_t value); - uint64_t qf_key_value_index(const QF *qf, uint64_t key, uint64_t value, uint64_t* idx); /* Initialize an iterator */ bool qf_iterator(const QF *qf, QFi *qfi, uint64_t position); diff --git a/src/cqf/gqf.c b/src/cqf/gqf.c index 5ebb915..ad06a78 100644 --- a/src/cqf/gqf.c +++ b/src/cqf/gqf.c @@ -2065,37 +2065,6 @@ uint64_t qf_count_key_value(const QF *qf, uint64_t key, uint64_t value) return 0; } - -uint64_t qf_key_value_index(const QF* qf, uint64_t key, uint64_t value, uint64_t* idx) { - uint64_t hash = (key << qf->metadata->value_bits) | (value & - BITMASK(qf->metadata->value_bits)); - uint64_t hash_remainder = hash & BITMASK(qf->metadata->bits_per_slot); - int64_t hash_bucket_index = hash >> qf->metadata->bits_per_slot; - - if (!is_occupied(qf, hash_bucket_index)) - return 0; - - int64_t runstart_index = hash_bucket_index == 0 ? 0 : run_end(qf, - hash_bucket_index-1) - + 1; - if (runstart_index < hash_bucket_index) - runstart_index = hash_bucket_index; - - /* printf("MC RUNSTART: %02lx RUNEND: %02lx\n", runstart_index, runend_index); */ - - uint64_t current_remainder, current_count, current_end; - do { - current_end = decode_counter(qf, runstart_index, ¤t_remainder, - ¤t_count); - if (current_remainder == hash_remainder) { - (*idx) = current_end; - return current_count; // FIXME not sure about this - } - runstart_index = current_end + 1; - } while (!is_runend(qf, current_end)); - - return 0; -} /* initialize the iterator at the run corresponding * to the position index */ From 5ed15a554d4ae53662ef5772e00877ed3296cd6b Mon Sep 17 00:00:00 2001 From: Prashant Pandey Date: Sat, 18 Aug 2018 12:22:22 -0700 Subject: [PATCH 086/122] Moving the new CQF code in mantis. compiles, not tested. --- include/coloreddbg.h | 42 +- include/cqf.h | 90 -- include/cqf/gqf.h | 207 ---- include/gqf/gqf.h | 331 +++++++ include/gqf/gqf_file.h | 45 + include/gqf/gqf_int.h | 133 +++ include/gqf/hashutil.h | 42 + include/gqf_cpp.h | 232 +++++ include/hashutil.h | 36 - include/kmer.h | 20 +- include/util.h | 21 +- src/CMakeLists.txt | 5 +- src/coloreddbg.cc | 26 +- src/{cqf => gqf}/gqf.c | 1429 +++++++++++++++------------ src/gqf/gqf_file.c | 254 +++++ src/{hashutil.cc => gqf/hashutil.c} | 33 +- src/kmer.cc | 70 +- src/query.cc | 13 +- src/validatemantis.cc | 17 +- 19 files changed, 1912 insertions(+), 1134 deletions(-) delete mode 100644 include/cqf.h delete mode 100644 include/cqf/gqf.h create mode 100644 include/gqf/gqf.h create mode 100644 include/gqf/gqf_file.h create mode 100644 include/gqf/gqf_int.h create mode 100644 include/gqf/hashutil.h create mode 100644 include/gqf_cpp.h delete mode 100644 include/hashutil.h rename src/{cqf => gqf}/gqf.c (63%) create mode 100644 src/gqf/gqf_file.c rename src/{hashutil.cc => gqf/hashutil.c} (89%) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 1b028bc..2ed8358 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -31,8 +31,8 @@ #include "sparsepp/spp.h" #include "tsl/sparse_map.h" #include "sdsl/bit_vectors.hpp" -#include "cqf.h" -#include "hashutil.h" +#include "gqf_cpp.h" +#include "gqf/hashutil.h" #include "common_types.h" #include "mantisconfig.hpp" @@ -44,7 +44,7 @@ struct hash128 { { __uint128_t val = val128; // Using the same seed as we use in k-mer hashing. - return HashUtil::MurmurHash64A((void*)&val, sizeof(__uint128_t), + return MurmurHash64A((void*)&val, sizeof(__uint128_t), 2038074743); } }; @@ -60,8 +60,8 @@ class ColoredDbg { ColoredDbg(std::string& cqf_file, std::vector& eqclass_files, std::string& sample_file); - ColoredDbg(uint64_t qbits, uint64_t key_bits, uint32_t seed, - std::string& prefix, uint64_t nqf); + ColoredDbg(uint64_t qbits, uint64_t key_bits, enum qf_hashmode hashmode, + uint32_t seed, std::string& prefix, uint64_t nqf); void build_sampleid_map(qf_obj *incqfs); @@ -199,9 +199,9 @@ bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, // A kmer (hash) is seen only once during the merge process. // So we insert every kmer in the dbg uint64_t eq_id; - __uint128_t vec_hash = HashUtil::MurmurHash128A((void*)vector.data(), - vector.capacity()/8, 2038074743, - 2038074751); + __uint128_t vec_hash = MurmurHash128A((void*)vector.data(), + vector.capacity()/8, 2038074743, + 2038074751); auto it = eqclass_map.find(vec_hash); bool added_eq_class{false}; @@ -222,7 +222,7 @@ bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, it->second.second += 1; // update the abundance. } - dbg.insert(KeyObject(key,0,eq_id)); // we use the count to store the eqclass ids + dbg.insert(KeyObject(key,0,eq_id), QF_KEY_IS_HASH); // we use the count to store the eqclass ids return added_eq_class; } @@ -283,7 +283,7 @@ ColoredDbg::find_samples(const mantis::QuerySet& kmers) { std::unordered_map query_eqclass_map; for (auto k : kmers) { key_obj key(k, 0, 0); - uint64_t eqclass = dbg.query(key); + uint64_t eqclass = dbg.query(key, 0); if (eqclass) query_eqclass_map[eqclass] += 1; } @@ -321,8 +321,8 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg>& ColoredDbg>& ColoredDbgcqf); + Iterator qfi(i, incqfs[i].obj->get_cqf()); if (qfi.end()) continue; minheap.push(qfi); } @@ -384,11 +384,12 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbginfo("Kmers merged: {} Num eq classes: {} Total time: {}", - dbg.size(), get_num_eqclasses(), time(nullptr) - start_time_); + dbg.dist_elts(), get_num_eqclasses(), time(nullptr) - + start_time_); } // Check if the bit vector buffer is full and needs to be serialized. @@ -423,9 +424,10 @@ void ColoredDbg::build_sampleid_map(qf_obj *incqfs) { template ColoredDbg::ColoredDbg(uint64_t qbits, uint64_t key_bits, + enum qf_hashmode hashmode, uint32_t seed, std::string& prefix, uint64_t nqf) : - dbg(qbits, key_bits, seed), bv_buffer(mantis::NUM_BV_BUFFER * nqf), + dbg(qbits, key_bits, hashmode, seed), bv_buffer(mantis::NUM_BV_BUFFER * nqf), prefix(prefix), num_samples(nqf), num_serializations(0), start_time_(std::time(nullptr)) {} template @@ -433,7 +435,7 @@ ColoredDbg::ColoredDbg(std::string& cqf_file, std::vector& eqclass_files, std::string& sample_file) - : dbg(cqf_file, false), bv_buffer(), start_time_(std::time(nullptr)) { + : dbg(cqf_file, CQF_FREAD), bv_buffer(), start_time_(std::time(nullptr)) { num_samples = 0; num_serializations = 0; diff --git a/include/cqf.h b/include/cqf.h deleted file mode 100644 index b526689..0000000 --- a/include/cqf.h +++ /dev/null @@ -1,90 +0,0 @@ -/* - * ============================================================================ - * - * Filename: cqf.h - * - * Description: - * - * Version: 1.0 - * Created: 2017-10-26 11:50:04 AM - * Revision: none - * Compiler: gcc - * - * Author: Prashant Pandey (), ppandey@cs.stonybrook.edu - * Organization: Stony Brook University - * - * ============================================================================ - */ - -#ifndef _CQF_H_ -#define _CQF_H_ - -#include -#include -#include - -#include -#include -#include -#include - -#include "cqf/gqf.h" -#include "util.h" - -#define NUM_HASH_BITS 28 -#define NUM_Q_BITS 20 -#define PAGE_DROP_GRANULARITY (1ULL << 21) -#define PAGE_BUFFER_SIZE 4096 - -template -class CQF { - public: - CQF() { qf_init(&cqf, 1ULL << NUM_Q_BITS, NUM_HASH_BITS, 0, true, "", 23423); } - CQF(uint64_t qbits, uint64_t key_bits, uint32_t seed) { qf_init(&cqf, 1ULL << qbits, key_bits, 0, true, "", seed); } - CQF(const CQF& copy_cqf) { memcpy(reinterpret_cast(&cqf), reinterpret_cast(const_cast(©_cqf.cqf)), sizeof(QF)); } - CQF(std::string& filename, bool flag) { - if (flag) - qf_read(&cqf, filename.c_str()); - else - qf_deserialize(&cqf, filename.c_str()); - } - - void insert(const key_obj& k) { qf_insert(&cqf, k.key, k.value, k.count, LOCK_AND_SPIN); } - uint64_t query(const key_obj& k) { return qf_count_key_value(&cqf, k.key, k.value); } - uint64_t get_index(const key_obj& k) { return get_bucket_index(k.key); } - - void serialize(std::string filename) { qf_serialize(&cqf, filename.c_str()); } - - uint64_t range(void) const { return cqf.metadata->range; } - uint32_t seed(void) const { return cqf.metadata->seed; } - uint32_t keybits(void) const { return cqf.metadata->key_bits; } - uint64_t size(void) const { return cqf.metadata->ndistinct_elts; } - uint64_t capacity() const {return cqf.metadata->nslots; } - - void reset(void) { qf_reset(&cqf); } - - void dump_metadata(void) { DEBUG_DUMP(&cqf); } - - void drop_pages(uint64_t cur); - - QF cqf; -}; - -class KeyObject { - public: - KeyObject() : key(0), value(0), count(0) {}; - - KeyObject(uint64_t k, uint64_t v, uint64_t c) : key(k), - value(v), count(c) {}; - - KeyObject(const KeyObject& k) : key(k.key), value(k.value), count(k.count) {}; - - bool operator==(KeyObject k) { return key == k.key; } - - typedef uint64_t kmer_t; - kmer_t key; - uint64_t value; - uint64_t count; -}; - -#endif diff --git a/include/cqf/gqf.h b/include/cqf/gqf.h deleted file mode 100644 index 4339226..0000000 --- a/include/cqf/gqf.h +++ /dev/null @@ -1,207 +0,0 @@ -#ifndef QF_H -#define QF_H - -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - - /* Can be - 0 (choose size at run-time), - 8, 16, 32, or 64 (for optimized versions), - or other integer <= 56 (for compile-time-optimized bit-shifting-based versions) - */ -#define BITS_PER_SLOT 0 - -#define BITMASK(nbits) ((nbits) == 64 ? 0xffffffffffffffff : (1ULL << (nbits)) \ - - 1ULL) - - struct __attribute__ ((__packed__)) qfblock; - typedef struct qfblock qfblock; - - enum lock { - LOCK_NO_SPIN, - LOCK_AND_SPIN, - NO_LOCK - }; - - typedef struct { - uint64_t total_time_single; - uint64_t total_time_spinning; - uint64_t locks_taken; - uint64_t locks_acquired_single_attempt; - } wait_time_data; - - typedef struct quotient_filter_mem { - int fd; - volatile int metadata_lock; - volatile int *locks; - wait_time_data *wait_times; - } quotient_filter_mem; - - typedef quotient_filter_mem qfmem; - - typedef struct quotient_filter_metadata { - uint64_t size; - uint32_t seed; - uint64_t nslots; - uint64_t xnslots; - uint64_t key_bits; - uint64_t value_bits; - uint64_t key_remainder_bits; - uint64_t bits_per_slot; - __uint128_t range; - uint64_t nblocks; - uint64_t nelts; - uint64_t ndistinct_elts; - uint64_t noccupied_slots; - uint64_t num_locks; - } quotient_filter_metadata; - - typedef quotient_filter_metadata qfmetadata; - - typedef struct quotient_filter { - qfmem *mem; - qfmetadata *metadata; - qfblock *blocks; - } quotient_filter; - - typedef quotient_filter QF; - - typedef struct { - uint64_t start_index; - uint16_t length; - } cluster_data; - - typedef struct quotient_filter_iterator { - const QF *qf; - struct { - uint64_t nslots; - uint64_t xnslots; - uint64_t bits_per_slot; - uint64_t sizeof_qfblock_SLOTS_PER_BLOCK_times_bits_per_slot_div_8; - uint64_t value_bits; - uint64_t key_remainder_bits; - uint64_t nblocks; - } cache; - uint64_t run; - uint64_t current; - uint64_t cur_start_index; - uint16_t cur_length; - uint32_t num_clusters; - cluster_data *c_info; - } quotient_filter_iterator; - - typedef quotient_filter_iterator QFi; - - /* Forward declaration for the macro. */ - void qf_dump_metadata(const QF *qf); - -#define DEBUG_CQF(fmt, ...) \ - do { if (PRINT_DEBUG) fprintf(stderr, fmt, __VA_ARGS__); } while (0) - -#define DEBUG_DUMP(qf) \ - do { if (PRINT_DEBUG) qf_dump_metadata(qf); } while (0) - - void qf_init(QF *qf, uint64_t nslots, uint64_t key_bits, uint64_t - value_bits, bool mem, const char *path, uint32_t seed); - - void qf_reset(QF *qf); - - void qf_destroy(QF *qf, bool mem); - - void qf_copy(QF *dest, QF *src); - - /* Increment the counter for this key/value pair by count. */ - bool qf_insert(QF *qf, uint64_t key, uint64_t value, uint64_t count, - enum lock flag); - - /* Remove count instances of this key/value combination. */ - void qf_remove(QF *qf, uint64_t key, uint64_t value, uint64_t count, enum - lock flag); - - /* Remove all instances of this key/value pair. */ - void qf_delete_key_value(QF *qf, uint64_t key, uint64_t value); - - /* Remove all instances of this key. */ - void qf_delete_key(QF *qf, uint64_t key); - - /* Replace the association (key, oldvalue, count) with the association - (key, newvalue, count). If there is already an association (key, - newvalue, count'), then the two associations will be merged and - their counters will be summed, resulting in association (key, - newvalue, count' + count). */ - void qf_replace(QF *qf, uint64_t key, uint64_t oldvalue, uint64_t newvalue); - - /* Lookup the value associated with key. Returns the count of that - key/value pair in the QF. If it returns 0, then, the key is not - present in the QF. Only returns the first value associated with key - in the QF. If you want to see others, use an iterator. */ - uint64_t qf_query(const QF *qf, uint64_t key, uint64_t *value); - - /* Return the number of times key has been inserted, with any value, - into qf. */ - uint64_t qf_count_key(const QF *qf, uint64_t key); - - /* Return the number of times key has been inserted, with the given - value, into qf. */ - uint64_t qf_count_key_value(const QF *qf, uint64_t key, uint64_t value); - - /* Initialize an iterator */ - bool qf_iterator(const QF *qf, QFi *qfi, uint64_t position); - - /* Initialize an iterator and position it at the smallest index containing a - * hash value greater than or equal to "hash". */ - bool qf_iterator_hash(const QF *qf, QFi *qfi, uint64_t hash); - - /* Returns 0 if the iterator is still valid (i.e. has not reached the - end of the QF. */ - int qfi_get(const QFi *qfi, uint64_t *key, uint64_t *value, uint64_t *count); - - /* Advance to next entry. Returns whether or not another entry is - found. */ - int qfi_next(QFi *qfi); - int qfi_nextx(QFi *qfi, uint64_t* read_offset); - - /* Check to see if the if the end of the QF */ - int qfi_end(const QFi *qfi); - - /* For debugging */ - void qf_dump(const QF *); - - /* write data structure of to the disk */ - void qf_serialize(const QF *qf, const char *filename); - - /* read data structure off the disk */ - void qf_deserialize(QF *qf, const char *filename); - - /* mmap the QF from disk. */ - void qf_read(QF *qf, const char *path); - - /* merge two QFs into the third one. */ - void qf_merge(QF *qfa, QF *qfb, QF *qfc, enum lock flag); - - /* merge multiple QFs into the final QF one. */ - void qf_multi_merge(QF *qf_arr[], int nqf, QF *qfr, enum lock flag); - - /* find cosine similarity between two QFs. */ - uint64_t qf_inner_product(QF *qfa, QF *qfb); - - /* magnitude of a QF. */ - uint64_t qf_magnitude(QF *qf); - - /* use madvice to drop pages corresponding to blocks from start_idx to - * last_slot_idx. */ - void qf_drop_pages(const QF *qf, uint64_t start_idx, uint64_t end_idx); - - /* return the addr of the slot in qfblock */ - const unsigned char *qf_get_addr(const QF *qf, uint64_t idx); - -#ifdef __cplusplus -} -#endif - -#endif /* QF_H */ diff --git a/include/gqf/gqf.h b/include/gqf/gqf.h new file mode 100644 index 0000000..8fe0e7a --- /dev/null +++ b/include/gqf/gqf.h @@ -0,0 +1,331 @@ +/* + * ============================================================================ + * + * Authors: Prashant Pandey + * Rob Johnson + * + * ============================================================================ + */ + +#ifndef _GQF_H_ +#define _GQF_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + + typedef struct quotient_filter quotient_filter; + typedef quotient_filter QF; + + /* CQFs support three hashing modes: + + - DEFAULT uses a hash that may introduce false positives, but + this can be useful when inserting large keys that need to be + hashed down to a small fingerprint. With this type of hash, + you can iterate over the hash values of all the keys in the + CQF, but you cannot iterate over the keys themselves. + + - INVERTIBLE has no false positives, but the size of the hash + output must be the same as the size of the hash input, + e.g. 17-bit keys hashed to 17-bit outputs. So this mode is + generally only useful when storing small keys in the CQF. With + this hashing mode, you can use iterators to enumerate both all + the hashes in the CQF, or all the keys. + + - NONE, for when you've done the hashing yourself. WARNING: the + CQF can exhibit very bad performance if you insert a skewed + distribution of intputs. + */ + + enum qf_hashmode { + QF_HASH_DEFAULT, + QF_HASH_INVERTIBLE, + QF_HASH_NONE + }; + + /* The CQF supports concurrent insertions and queries. Only the + portion of the CQF being examined or modified is locked, so it + supports high throughput even with many threads. + + The CQF operations support 3 locking modes: + + - NO_LOCK: for single-threaded applications or applications + that do their own concurrency management. + + - WAIT_FOR_LOCK: Spin until you get the lock, then do the query + or update. + + - TRY_ONCE_LOCK: If you can't grab the lock on the first try, + return with an error code. + */ +#define QF_NO_LOCK (0x01) +#define QF_TRY_ONCE_LOCK (0x02) +#define QF_WAIT_FOR_LOCK (0x04) + + /* It is sometimes useful to insert a key that has already been + hashed. */ +#define QF_KEY_IS_HASH (0x08) + + /****************************************** + The CQF defines low-level constructor and destructor operations + that are designed to enable the application to manage the memory + used by the CQF. + *******************************************/ + + /* + * Create an empty CQF in "buffer". If there is not enough space at + * buffer then it will return the total size needed in bytes to + * initialize the CQF. This function takes ownership of buffer. + */ + uint64_t qf_init(QF *qf, uint64_t nslots, uint64_t key_bits, uint64_t + value_bits, enum qf_hashmode hash, uint32_t seed, void* + buffer, uint64_t buffer_len); + + /* Create a CQF in "buffer". Note that this does not initialize the + contents of bufferss Use this function if you have read a CQF, e.g. + off of disk or network, and want to begin using that stream of + bytes as a CQF. The CQF takes ownership of buffer. */ + uint64_t qf_use(QF* qf, void* buffer, uint64_t buffer_len); + + /* Destroy this CQF. Returns a pointer to the memory that the CQF was + using (i.e. passed into qf_init or qf_use) so that the application + can release that memory. */ + void *qf_destroy(QF *qf); + + /* Allocate a new CQF using "nslots" at "buffer" and copy elements from "qf" + * into it. + * If there is not enough space at buffer then it will return the total size + * needed in bytes to initialize the new CQF. + * */ + uint64_t qf_resize(QF* qf, uint64_t nslots, void* buffer, uint64_t + buffer_len); + + /*********************************** + The following convenience functions create and destroy CQFs by + using malloc/free to obtain and release the memory for the CQF. + ************************************/ + + /* Initialize the CQF and allocate memory for the CQF. */ + bool qf_malloc(QF *qf, uint64_t nslots, uint64_t key_bits, uint64_t + value_bits, enum qf_hashmode hash, uint32_t seed); + + bool qf_free(QF *qf); + + /* Resize the QF to the specified number of slots. Uses malloc() to + * obtain the new memory, and calls free() on the old memory. + * Return value: + * >= 0: number of keys copied during resizing. + * */ + int64_t qf_resize_malloc(QF *qf, uint64_t nslots); + + /* Turn on automatic resizing. Resizing is performed by calling + qf_resize_malloc, so the CQF must meet the requirements of that + function. */ + void qf_set_auto_resize(QF* qf, bool enabled); + + /*********************************** + Functions for modifying the CQF. + ***********************************/ + +#define QF_NO_SPACE (-1) +#define QF_COULDNT_LOCK (-2) +#define QF_DOESNT_EXIST (-3) + + /* Increment the counter for this key/value pair by count. + * Return value: + * >= 0: distance from the home slot to the slot in which the key is + * inserted (or 0 if count == 0). + * == QF_NO_SPACE: the CQF has reached capacity. + * == QF_COULDNT_LOCK: TRY_ONCE_LOCK has failed to acquire the lock. + */ + int qf_insert(QF *qf, uint64_t key, uint64_t value, uint64_t count, uint8_t + flags); + + /* Set the counter for this key/value pair to count. + Return value: Same as qf_insert. + Returns 0 if new count is equal to old count. + */ + int qf_set_count(QF *qf, uint64_t key, uint64_t value, uint64_t count, + uint8_t flags); + + /* Remove up to count instances of this key/value combination. + * If the CQF contains <= count instances, then they will all be + * removed, which is not an error. + * Return value: + * >= 0: number of slots freed. + * == QF_DOESNT_EXIST: Specified item did not exist. + * == QF_COULDNT_LOCK: TRY_ONCE_LOCK has failed to acquire the lock. + */ + int qf_remove(QF *qf, uint64_t key, uint64_t value, uint64_t count, uint8_t + flags); + + /* Remove all instances of this key/value pair. */ + int qf_delete_key_value(QF *qf, uint64_t key, uint64_t value, uint8_t flags); + + /* Remove all instances of this key. */ + /* NOT IMPLEMENTED YET. */ + //void qf_delete_key(QF *qf, uint64_t key); + + /* Replace the association (key, oldvalue, count) with the association + (key, newvalue, count). If there is already an association (key, + newvalue, count'), then the two associations will be merged and + their counters will be summed, resulting in association (key, + newvalue, count' + count). */ + /* NOT IMPLEMENTED YET. */ + //void qf_replace(QF *qf, uint64_t key, uint64_t oldvalue, uint64_t newvalue); + + /**************************************** + Query functions + ****************************************/ + + /* Lookup the value associated with key. Returns the count of that + key/value pair in the QF. If it returns 0, then, the key is not + present in the QF. Only returns the first value associated with key + in the QF. If you want to see others, use an iterator. + May return QF_COULDNT_LOCK if called with QF_TRY_LOCK. */ + uint64_t qf_query(const QF *qf, uint64_t key, uint64_t *value, uint8_t + flags); + + /* Return the number of times key has been inserted, with any value, + into qf. */ + /* NOT IMPLEMENTED YET. */ + //uint64_t qf_count_key(const QF *qf, uint64_t key); + + /* Return the number of times key has been inserted, with the given + value, into qf. + May return QF_COULDNT_LOCK if called with QF_TRY_LOCK. */ + uint64_t qf_count_key_value(const QF *qf, uint64_t key, uint64_t value, + uint8_t flags); + + /* Returns a unique index corresponding to the key in the CQF. Note + that this can change if further modifications are made to the + CQF. + + If the key is not found then returns QF_DOESNT_EXIST. + May return QF_COULDNT_LOCK if called with QF_TRY_LOCK. + */ + int64_t qf_get_unique_index(const QF *qf, uint64_t key, uint64_t value, + uint8_t flags); + + + /**************************************** + Metadata accessors. + ****************************************/ + + /* Hashing info */ + enum qf_hashmode qf_get_hashmode(const QF *qf); + uint64_t qf_get_hash_seed(const QF *qf); + __uint128_t qf_get_hash_range(const QF *qf); + + /* Space usage info. */ + bool qf_is_auto_resize_enabled(const QF *qf); + uint64_t qf_get_total_size_in_bytes(const QF *qf); + uint64_t qf_get_nslots(const QF *qf); + uint64_t qf_get_num_occupied_slots(const QF *qf); + + /* Bit-sizes info. */ + uint64_t qf_get_num_key_bits(const QF *qf); + uint64_t qf_get_num_value_bits(const QF *qf); + uint64_t qf_get_num_key_remainder_bits(const QF *qf); + uint64_t qf_get_bits_per_slot(const QF *qf); + + /* Number of (distinct) key-value pairs. */ + uint64_t qf_get_sum_of_counts(const QF *qf); + uint64_t qf_get_num_distinct_key_value_pairs(const QF *qf); + + /**************************************** + Iterators + *****************************************/ + + typedef struct quotient_filter_iterator quotient_filter_iterator; + typedef quotient_filter_iterator QFi; + +#define QF_INVALID (-4) +#define QFI_INVALID (-5) + + /* Initialize an iterator starting at the given position. + * Return value: + * >= 0: iterator is initialized and positioned at the returned slot. + * = QFI_INVALID: iterator has reached end. + */ + int64_t qf_iterator_from_position(const QF *qf, QFi *qfi, uint64_t position); + + /* Initialize an iterator and position it at the smallest index + * containing a key-value pair whose hash is greater than or equal + * to the specified key-value pair. + * Return value: + * >= 0: iterator is initialized and position at the returned slot. + * = QFI_INVALID: iterator has reached end. + */ + int64_t qf_iterator_from_key_value(const QF *qf, QFi *qfi, uint64_t key, + uint64_t value, uint8_t flags); + + /* Requires that the hash mode of the CQF is INVERTIBLE or NONE. + * If the hash mode is DEFAULT then returns QF_INVALID. + * Return value: + * = 0: Iterator is still valid. + * = QFI_INVALID: iterator has reached end. + * = QF_INVALID: hash mode is QF_DEFAULT_HASH + */ + int qfi_get_key(const QFi *qfi, uint64_t *key, uint64_t *value, uint64_t + *count); + + /* Return value: + * = 0: Iterator is still valid. + * = QFI_INVALID: iterator has reached end. + */ + int qfi_get_hash(const QFi *qfi, uint64_t *hash, uint64_t *value, uint64_t + *count); + + /* Advance to next entry. + * Return value: + * = 0: Iterator is still valid. + * = QFI_INVALID: iterator has reached end. + */ + int qfi_next(QFi *qfi); + + /* Check to see if the if the end of the QF */ + bool qfi_end(const QFi *qfi); + + /************************************ + Miscellaneous convenience functions. + *************************************/ + + /* Reset the CQF to an empty filter. */ + void qf_reset(QF *qf); + + /* The caller should call qf_init on the dest QF using the same + * parameters as the src QF before calling this function. Note: src + * and dest must be exactly the same, including number of slots. */ + void qf_copy(QF *dest, const QF *src); + + /* merge two QFs into the third one. Note: merges with any existing + values in qfc. */ + void qf_merge(const QF *qfa, const QF *qfb, QF *qfc); + + /* merge multiple QFs into the final QF one. */ + void qf_multi_merge(const QF *qf_arr[], int nqf, QF *qfr); + + /* find cosine similarity between two QFs. */ + uint64_t qf_inner_product(const QF *qfa, const QF *qfb); + + /* square of the L_2 norm of a QF (i.e. sum of squares of counts of + all items in the CQF). */ + uint64_t qf_magnitude(const QF *qf); + + /*********************************** + Debugging functions. + ************************************/ + + void qf_dump(const QF *); + void qf_dump_metadata(const QF *qf); + + +#ifdef __cplusplus +} +#endif + +#endif /* _GQF_H_ */ diff --git a/include/gqf/gqf_file.h b/include/gqf/gqf_file.h new file mode 100644 index 0000000..c4fdf87 --- /dev/null +++ b/include/gqf/gqf_file.h @@ -0,0 +1,45 @@ +/* + * ============================================================================ + * + * Authors: Prashant Pandey + * Rob Johnson + * + * ============================================================================ + */ + +#ifndef _GQF_FILE_H_ +#define _GQF_FILE_H_ + +#include +#include +#include + +#include "gqf.h" + +#ifdef __cplusplus +extern "C" { +#endif + + /* Initialize a file-backed CQF at "filename". */ + bool qf_initfile(QF *qf, uint64_t nslots, uint64_t key_bits, uint64_t + value_bits, enum qf_hashmode hash, uint32_t seed, char* + filename, int prot); + + /* Read "filename" into "qf". */ + uint64_t qf_usefile(QF* qf, const char* filename, int prot); + + bool qf_closefile(QF* qf); + + bool qf_deletefile(QF* qf); + + /* write data structure of to the disk */ + uint64_t qf_serialize(const QF *qf, const char *filename); + + /* read data structure off the disk */ + uint64_t qf_deserialize(QF *qf, const char *filename); + +#ifdef __cplusplus +} +#endif + +#endif // _GQF_FILE_H_ diff --git a/include/gqf/gqf_int.h b/include/gqf/gqf_int.h new file mode 100644 index 0000000..ca34e8d --- /dev/null +++ b/include/gqf/gqf_int.h @@ -0,0 +1,133 @@ +/* + * ============================================================================ + * + * Authors: Prashant Pandey + * Rob Johnson + * + * ============================================================================ + */ + +#ifndef _GQF_INT_H_ +#define _GQF_INT_H_ + +#include +#include + +#include "gqf.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define MAGIC_NUMBER 1018874902021329732 + +/* Can be + 0 (choose size at run-time), + 8, 16, 32, or 64 (for optimized versions), + or other integer <= 56 (for compile-time-optimized bit-shifting-based versions) +*/ +#define QF_BITS_PER_SLOT 0 + +/* Must be >= 6. 6 seems fastest. */ +#define QF_BLOCK_OFFSET_BITS (6) + +#define QF_SLOTS_PER_BLOCK (1ULL << QF_BLOCK_OFFSET_BITS) +#define QF_METADATA_WORDS_PER_BLOCK ((QF_SLOTS_PER_BLOCK + 63) / 64) + + typedef struct __attribute__ ((__packed__)) qfblock { + /* Code works with uint16_t, uint32_t, etc, but uint8_t seems just as fast as + * anything else */ + uint8_t offset; + uint64_t occupieds[QF_METADATA_WORDS_PER_BLOCK]; + uint64_t runends[QF_METADATA_WORDS_PER_BLOCK]; + +#if QF_BITS_PER_SLOT == 8 + uint8_t slots[QF_SLOTS_PER_BLOCK]; +#elif QF_BITS_PER_SLOT == 16 + uint16_t slots[QF_SLOTS_PER_BLOCK]; +#elif QF_BITS_PER_SLOT == 32 + uint32_t slots[QF_SLOTS_PER_BLOCK]; +#elif QF_BITS_PER_SLOT == 64 + uint64_t slots[QF_SLOTS_PER_BLOCK]; +#elif QF_BITS_PER_SLOT != 0 + uint8_t slots[QF_SLOTS_PER_BLOCK * QF_BITS_PER_SLOT / 8]; +#else + uint8_t slots[]; +#endif + } qfblock; + + typedef struct file_info { + int fd; + char *filepath; + } file_info; + + // The below struct is used to instrument the code. + // It is not used in normal operations of the CQF. + typedef struct { + uint64_t total_time_single; + uint64_t total_time_spinning; + uint64_t locks_taken; + uint64_t locks_acquired_single_attempt; + } wait_time_data; + + typedef struct quotient_filter_runtime_data { + file_info f_info; + uint64_t num_locks; + volatile int metadata_lock; + volatile int *locks; + wait_time_data *wait_times; + } quotient_filter_runtime_data; + + typedef quotient_filter_runtime_data qfruntime; + + typedef struct quotient_filter_metadata { + uint64_t magic_endian_number; + enum qf_hashmode hash_mode; + uint32_t auto_resize; + uint64_t total_size_in_bytes; + uint32_t seed; + uint64_t nslots; + uint64_t xnslots; + uint64_t key_bits; + uint64_t value_bits; + uint64_t key_remainder_bits; + uint64_t bits_per_slot; + __uint128_t range; + uint64_t nblocks; + uint64_t nelts; + uint64_t ndistinct_elts; + uint64_t noccupied_slots; + } quotient_filter_metadata; + + typedef quotient_filter_metadata qfmetadata; + + typedef struct quotient_filter { + qfruntime *runtimedata; + qfmetadata *metadata; + qfblock *blocks; + } quotient_filter; + + typedef quotient_filter QF; + + // The below struct is used to instrument the code. + // It is not used in normal operations of the CQF. + typedef struct { + uint64_t start_index; + uint16_t length; + } cluster_data; + + typedef struct quotient_filter_iterator { + const QF *qf; + uint64_t run; + uint64_t current; + uint64_t cur_start_index; + uint16_t cur_length; + uint32_t num_clusters; + cluster_data *c_info; + } quotient_filter_iterator; + +#ifdef __cplusplus +} +#endif + +#endif /* _GQF_INT_H_ */ diff --git a/include/gqf/hashutil.h b/include/gqf/hashutil.h new file mode 100644 index 0000000..06d1b1a --- /dev/null +++ b/include/gqf/hashutil.h @@ -0,0 +1,42 @@ +/* + * ============================================================================ + * + * Authors: Prashant Pandey + * Rob Johnson + * + * ============================================================================ + */ + +#ifndef _HASHUTIL_H_ +#define _HASHUTIL_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// MurmurHash2 +uint32_t MurmurHash(const void *buf, size_t length, uint32_t seed); +uint64_t MurmurHash64B ( const void * key, int len, unsigned int + seed ); +uint64_t MurmurHash64A ( const void * key, int len, unsigned int + seed ); +__uint128_t MurmurHash128A ( const void * key, int len, unsigned + int seed, unsigned int seed2 ); + +// AES hash +uint64_t AES_HASH(uint64_t x); + +uint64_t hash_64(uint64_t key, uint64_t mask); +uint64_t hash_64i(uint64_t key, uint64_t mask); + +#ifdef __cplusplus +} +#endif + +#endif // #ifndef _HASHUTIL_H_ + + diff --git a/include/gqf_cpp.h b/include/gqf_cpp.h new file mode 100644 index 0000000..c64e703 --- /dev/null +++ b/include/gqf_cpp.h @@ -0,0 +1,232 @@ +/* + * ============================================================================ + * + * Authors: Prashant Pandey + * Rob Johnson + * Rob Patro (rob.patro@cs.stonybrook.edu) + * + * ============================================================================ + */ + +#ifndef _CQF_H_ +#define _CQF_H_ + +#include +#include +#include + +#include +#include +#include +#include + +#include "gqf/gqf.h" +#include "gqf/gqf_int.h" +#include "gqf/gqf_file.h" +#include "util.h" + +#define NUM_HASH_BITS 24 +#define NUM_Q_BITS 16 +#define SEED 2038074761 + +enum readmode { + CQF_MMAP, + CQF_FREAD +}; + +template +class CQF { + public: + CQF(); + CQF(uint64_t q_bits, uint64_t key_bits, enum qf_hashmode hash, uint32_t seed); + CQF(std::string& filename, enum readmode flag); + CQF(const CQF& copy_cqf); + + int insert(const key_obj& k, uint8_t flags); + + /* Will return the count. */ + uint64_t query(const key_obj& k, uint8_t flags); + + uint64_t inner_prod(const CQF& in_cqf); + + void serialize(std::string filename) { + qf_serialize(&cqf, filename.c_str()); + } + + void set_auto_resize(void) { qf_set_auto_resize(&cqf, true); } + int64_t get_unique_index(const key_obj& k, uint8_t flags) const { + return qf_get_unique_index(&cqf, k.key, k.value, flags); + } + + bool is_exact(void) const; + enum qf_hashmode hash_mode(void) const { return cqf.metadata->hash_mode; } + bool check_similarity(const CQF *other_cqf) const; + + const QF* get_cqf(void) const { return &cqf; } + uint64_t range(void) const { return cqf.metadata->range; } + uint32_t seed(void) const { return cqf.metadata->seed; } + uint64_t numslots(void) const { return cqf.metadata->nslots; } + uint32_t keybits(void) const { return cqf.metadata->key_bits; } + uint64_t total_elts(void) const { return cqf.metadata->nelts; } + uint64_t dist_elts(void) const { return cqf.metadata->ndistinct_elts; } + //uint64_t set_size(void) const { return set.size(); } + void reset(void) { qf_reset(&cqf); } + + void dump_metadata(void) const { qf_dump_metadata(&cqf); } + + void drop_pages(uint64_t cur); + + class Iterator { + public: + Iterator(QFi it); + key_obj operator*(void) const; + void operator++(void); + bool done(void) const; + + key_obj get_cur_hash(void) const; + + QFi iter; + private: + uint64_t end_hash; + }; + + Iterator begin(void) const; + Iterator end(void) const; + + private: + QF cqf; + //std::unordered_set set; +}; + +class KeyObject { + public: + KeyObject() : key(0), value(0), count(0) {}; + + KeyObject(uint64_t k, uint64_t v, uint64_t c) : key(k), + value(v), count(c) {}; + + KeyObject(const KeyObject& k) : key(k.key), value(k.value), count(k.count) {}; + + bool operator==(KeyObject k) { return key == k.key; } + + typedef uint64_t kmer_t; + kmer_t key; + uint64_t value; + uint64_t count; +}; + +template +CQF::CQF() { + if (!qf_malloc(&cqf, 1ULL << NUM_Q_BITS, NUM_HASH_BITS, 0, QF_HASH_DEFAULT, + SEED)) { + ERROR("Can't allocate the CQF"); + exit(EXIT_FAILURE); + } +} + +template +CQF::CQF(uint64_t q_bits, uint64_t key_bits, enum qf_hashmode hash, + uint32_t seed) { + if (!qf_malloc(&cqf, 1ULL << q_bits, key_bits, 0, hash, SEED)) { + ERROR("Can't allocate the CQF"); + exit(EXIT_FAILURE); + } +} + +template +CQF::CQF(std::string& filename, enum readmode flag) { + uint64_t size = 0; + if (flag == CQF_MMAP) + size = qf_usefile(&cqf, filename.c_str(), PROT_READ); + else + size = qf_deserialize(&cqf, filename.c_str()); + + if (size == 0) { + ERROR("Can't read/deserialize the CQF"); + exit(EXIT_FAILURE); + } +} + +template CQF::CQF(const CQF& copy_cqf) { + memcpy(reinterpret_cast(&cqf), + reinterpret_cast(const_cast(©_cqf.cqf)), sizeof(QF)); +} + +template +int CQF::insert(const key_obj& k, uint8_t flags) { + return qf_insert(&cqf, k.key, k.value, k.count, flags); + // To validate the CQF + //set.insert(k.key); +} + +template +uint64_t CQF::query(const key_obj& k, uint8_t flags) { + return qf_count_key_value(&cqf, k.key, k.value, flags); +} + +template +uint64_t CQF::inner_prod(const CQF& in_cqf) { + return qf_inner_product(&cqf, in_cqf.get_cqf()); +} + +template +bool CQF::is_exact(void) const { + if (cqf.metadata->hash_mode == QF_HASH_INVERTIBLE) + return true; + return false; +} + +template +bool CQF::check_similarity(const CQF *other_cqf) const { + if (hash_mode() != other_cqf->hash_mode() || seed() != other_cqf->seed() || + keybits() != other_cqf->keybits() || range() != other_cqf->range()) + return false; + return true; +} + +template +CQF::Iterator::Iterator(QFi it) + : iter(it) {}; + +template +key_obj CQF::Iterator::operator*(void) const { + uint64_t key = 0, value = 0, count = 0; + qfi_get_key(&iter, &key, &value, &count); + return key_obj(key, value, count); +} + +template +key_obj CQF::Iterator::get_cur_hash(void) const { + uint64_t key = 0, value = 0, count = 0; + qfi_get_hash(&iter, &key, &value, &count); + return key_obj(key, value, count); +} + +template +void CQF::Iterator::operator++(void) { + qfi_next(&iter); +} + +/* Currently, the iterator only traverses forward. So, we only need to check + * the right side limit. + */ +template +bool CQF::Iterator::done(void) const { + return qfi_end(&iter); +} + +template +typename CQF::Iterator CQF::begin(void) const { + QFi qfi; + qf_iterator_from_position(&this->cqf, &qfi, 0); + return Iterator(qfi); +} + +template +typename CQF::Iterator CQF::end(void) const { + QFi qfi; + qf_iterator_from_position(&this->cqf, &qfi, 0xffffffffffffffff); + return Iterator(qfi, UINT64_MAX); +} + +#endif diff --git a/include/hashutil.h b/include/hashutil.h deleted file mode 100644 index 472d458..0000000 --- a/include/hashutil.h +++ /dev/null @@ -1,36 +0,0 @@ -/* -*- Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil -*- */ -#ifndef _HASHUTIL_H_ -#define _HASHUTIL_H_ - -#include -#include -#include -#include - -class HashUtil { - public: - - // MurmurHash2 - static uint32_t MurmurHash(const void *buf, size_t length, uint32_t seed = - 0); - static uint32_t MurmurHash(const std::string &s, uint32_t seed = 0); - static uint64_t MurmurHash64B ( const void * key, int len, unsigned int - seed ); - static uint64_t MurmurHash64A ( const void * key, int len, unsigned int - seed ); - static __uint128_t MurmurHash128A ( const void * key, int len, unsigned - int seed, unsigned int seed2 ); - - // AES hash - static uint64_t AES_HASH(uint64_t x); - - static uint64_t hash_64(uint64_t key, uint64_t mask); - static uint64_t hash_64i(uint64_t key, uint64_t mask); - - private: - HashUtil(); -}; - -#endif // #ifndef _HASHUTIL_H_ - - diff --git a/include/kmer.h b/include/kmer.h index c7dbdea..97f4bfb 100644 --- a/include/kmer.h +++ b/include/kmer.h @@ -22,7 +22,6 @@ #include #include -#include "hashutil.h" #include "common_types.h" #define BITMASK(nbits) ((nbits) == 64 ? 0xffffffffffffffff : (1ULL << (nbits)) \ @@ -33,18 +32,15 @@ using namespace std; class Kmer { public: - static inline char map_int(uint8_t base); - static inline uint8_t map_base(char base); - static uint64_t str_to_int(string str); - static string int_to_str(uint64_t kmer, uint64_t kmer_size); - static inline int reverse_complement_base(int x); - static uint64_t reverse_complement(uint64_t kmer, uint64_t kmer_size); - static inline bool compare_kmers(uint64_t kmer, uint64_t kmer_rev); - static inline unsigned __int128 word_reverse_complement(unsigned __int128 w); - static inline int64_t word_reverse_complement(uint64_t w); - static inline uint32_t word_reverse_complement(uint32_t w); + static char map_int(uint8_t base); + static uint8_t map_base(char base); + static __int128_t str_to_int(std::string str); + static std::string int_to_str(__int128_t kmer, uint64_t kmer_size); + static int reverse_complement_base(int x); + static __int128_t reverse_complement(__int128_t kmer, uint64_t kmer_size); + static bool compare_kmers(__int128_t kmer, __int128_t kmer_rev); + static mantis::QuerySets parse_kmers(const char *filename, - uint32_t seed, uint64_t range, uint64_t kmer_size, uint64_t& total_kmers); static std::string generate_random_string(uint64_t len); diff --git a/include/util.h b/include/util.h index 396184a..fce583b 100644 --- a/include/util.h +++ b/include/util.h @@ -1,15 +1,6 @@ /* * ============================================================================ * - * Filename: util.h - * - * Description: - * - * Version: 1.0 - * Created: 2017-09-21 12:39:52 PM - * Revision: none - * Compiler: gcc - * * Author: Prashant Pandey (), ppandey@cs.stonybrook.edu * Organization: Stony Brook University * @@ -33,6 +24,18 @@ #define PRINT_DEBUG 0 #endif +#define DEBUG(x) do { \ + if (PRINT_DEBUG) { std::cerr << x << std::endl; } \ +} while (0) + +#define ERROR(x) do { \ + { std::cerr << x << std::endl; } \ +} while (0) + +#define PRINT(x) do { \ + { std::cout << x << std::endl; } \ +} while (0) + #define DEBUG_CDBG(x) do { \ if (PRINT_DEBUG) { std::cerr << x << std::endl; } \ } while (0) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c651923..399a0c1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,13 +3,14 @@ # most of the relevant API add_library(mantis_core STATIC kmer.cc - hashutil.cc query.cc util.cc validatemantis.cc coloreddbg.cc MantisFS.cc - cqf/gqf.c) + gqf/gqf.c + gqf/gqf_file.c + gqf/hashutil.c) set(MANTIS_DEBUG_CFLAGS "${MANTIS_C_FLAGS};-g") set(MANTIS_DEBUG_CXXFLAGS "${MANTIS_CXX_FLAGS};-g") diff --git a/src/coloreddbg.cc b/src/coloreddbg.cc index c759581..4bab65b 100644 --- a/src/coloreddbg.cc +++ b/src/coloreddbg.cc @@ -1,15 +1,6 @@ /* * ============================================================================ * - * Filename: main.cc - * - * Description: - * - * Version: 1.0 - * Created: 2016-11-10 03:31:54 PM - * Revision: none - * Compiler: gcc - * * Author: Prashant Pandey (), ppandey@cs.stonybrook.edu * Organization: Stony Brook University * @@ -70,14 +61,6 @@ int build_main ( BuildOpts& opt ) { - /* calling asyc read init */ - struct aioinit aioinit; - memset(&aioinit, 0, sizeof(struct aioinit)); - aioinit.aio_num = 2500; - aioinit.aio_threads = 100; - aioinit.aio_idle_time = 60; - aio_init(&aioinit); - spdlog::logger* console = opt.console.get(); std::ifstream infile(opt.inlist); uint64_t num_samples{0}; @@ -150,18 +133,23 @@ build_main ( BuildOpts& opt ) console->error("Squeakr file {} does not exist.", cqf_file); exit(1); } - cqfs.emplace_back(cqf_file, true); + cqfs.emplace_back(cqf_file, CQF_MMAP); std::string sample_id = first_part(first_part(last_part(cqf_file, '/'), '.'), '_'); console->info("Reading CQF {} Seed {}",nqf, cqfs[nqf].seed()); console->info("Sample id {} cut off {}", sample_id); cqfs.back().dump_metadata(); inobjects.emplace_back(&cqfs[nqf], sample_id, nqf); + if (!cqfs.front().check_similarity(&cqfs.back())) { + console->error("Passed Squeakr files are not similar.", cqf_file); + exit(1); + } nqf++; } ColoredDbg*>, KeyObject> cdbg(opt.qbits, inobjects[0].obj->keybits(), + cqfs[0].hash_mode(), inobjects[0].obj->seed(), prefix, nqf); cdbg.set_console(console); @@ -208,7 +196,7 @@ build_main ( BuildOpts& opt ) cdbg.construct(inobjects.data(), std::numeric_limits::max()); console->info("Final colored dBG has {} k-mers and {} equivalence classes", - cdbg.get_cqf()->size(), cdbg.get_num_eqclasses()); + cdbg.get_cqf()->dist_elts(), cdbg.get_num_eqclasses()); //cdbg.get_cqf()->dump_metadata(); //DEBUG_CDBG(cdbg.get_cqf()->set_size()); diff --git a/src/cqf/gqf.c b/src/gqf/gqf.c similarity index 63% rename from src/cqf/gqf.c rename to src/gqf/gqf.c index ad06a78..f2cc817 100644 --- a/src/cqf/gqf.c +++ b/src/gqf/gqf.c @@ -1,3 +1,12 @@ +/* + * ============================================================================ + * + * Authors: Prashant Pandey + * Rob Johnson + * + * ============================================================================ + */ + #include #if 0 # include @@ -14,53 +23,31 @@ #include #include -#include "cqf/gqf.h" +#include "gqf/hashutil.h" +#include "gqf/gqf.h" +#include "gqf/gqf_int.h" /****************************************************************** * Code for managing the metadata bits and slots w/o interpreting * * the content of the slots. ******************************************************************/ -/* Must be >= 6. 6 seems fastest. */ -#define BLOCK_OFFSET_BITS (6) - -#define SLOTS_PER_BLOCK (1ULL << BLOCK_OFFSET_BITS) -#define METADATA_WORDS_PER_BLOCK ((SLOTS_PER_BLOCK + 63) / 64) - +#define MAX_VALUE(nbits) ((1ULL << (nbits)) - 1) +#define BITMASK(nbits) \ + ((nbits) == 64 ? 0xffffffffffffffff : MAX_VALUE(nbits)) #define NUM_SLOTS_TO_LOCK (1ULL<<16) #define CLUSTER_SIZE (1ULL<<14) +#define METADATA_WORD(qf,field,slot_index) \ + (get_block((qf), (slot_index) / \ + QF_SLOTS_PER_BLOCK)->field[((slot_index) % QF_SLOTS_PER_BLOCK) / 64]) -#define METADATA_WORD(qf,field,slot_index) (get_block((qf), (slot_index) / \ - SLOTS_PER_BLOCK)->field[((slot_index) % SLOTS_PER_BLOCK) / 64]) -#define QFI_METADATA_WORD(qfi,field,slot_index) (qfi_get_block((qfi), (slot_index) / \ - SLOTS_PER_BLOCK)->field[((slot_index) % SLOTS_PER_BLOCK) / 64]) +#define GET_NO_LOCK(flag) (flag & QF_NO_LOCK) +#define GET_TRY_ONCE_LOCK(flag) (flag & QF_TRY_ONCE_LOCK) +#define GET_WAIT_FOR_LOCK(flag) (flag & QF_WAIT_FOR_LOCK) +#define GET_KEY_HASH(flag) (flag & QF_KEY_IS_HASH) -#define MAX_VALUE(nbits) ((1ULL << (nbits)) - 1) +#define DISTANCE_FROM_HOME_SLOT_CUTOFF 1000 #define BILLION 1000000000L -#define PAGE_SIZE (1ULL << 12) -#define PAGE_ALIGN(virt) (virt & ~(PAGE_SIZE - 1)) - -typedef struct __attribute__ ((__packed__)) qfblock { - /* Code works with uint16_t, uint32_t, etc, but uint8_t seems just as fast as - * anything else */ - uint8_t offset; - uint64_t occupieds[METADATA_WORDS_PER_BLOCK]; - uint64_t runends[METADATA_WORDS_PER_BLOCK]; - -#if BITS_PER_SLOT == 8 - uint8_t slots[SLOTS_PER_BLOCK]; -#elif BITS_PER_SLOT == 16 - uint16_t slots[SLOTS_PER_BLOCK]; -#elif BITS_PER_SLOT == 32 - uint32_t slots[SLOTS_PER_BLOCK]; -#elif BITS_PER_SLOT == 64 - uint64_t slots[SLOTS_PER_BLOCK]; -#elif BITS_PER_SLOT != 0 - uint8_t slots[SLOTS_PER_BLOCK * BITS_PER_SLOT / 8]; -#else - uint8_t slots[]; -#endif -} qfblock; #ifdef DEBUG #define PRINT_DEBUG 1 @@ -71,8 +58,8 @@ typedef struct __attribute__ ((__packed__)) qfblock { #define DEBUG_CQF(fmt, ...) \ do { if (PRINT_DEBUG) fprintf(stderr, fmt, __VA_ARGS__); } while (0) -#define PRINT_CQF(fmt, ...) \ - do { fprintf(stdout, fmt, __VA_ARGS__); } while (0) +#define DEBUG_DUMP(qf) \ + do { if (PRINT_DEBUG) qf_dump_metadata(qf); } while (0) static __inline__ unsigned long long rdtsc(void) { @@ -82,57 +69,57 @@ static __inline__ unsigned long long rdtsc(void) } #ifdef LOG_WAIT_TIME -static inline bool qf_spin_lock(QF *cf, volatile int *lock, uint64_t idx, - enum lock flag) +static inline bool qf_spin_lock(QF *qf, volatile int *lock, uint64_t idx, + uint8_t flag) { struct timespec start, end; bool ret; clock_gettime(CLOCK_THREAD_CPUTIME_ID, &start); - if (flag != LOCK_AND_SPIN) { + if (GET_WAIT_FOR_LOCK(flag) != QF_WAIT_FOR_LOCK) { ret = !__sync_lock_test_and_set(lock, 1); clock_gettime(CLOCK_THREAD_CPUTIME_ID, &end); - cf->mem->wait_times[idx].locks_acquired_single_attempt++; - cf->mem->wait_times[idx].total_time_single += BILLION * (end.tv_sec - + qf->runtimedata->wait_times[idx].locks_acquired_single_attempt++; + qf->runtimedata->wait_times[idx].total_time_single += BILLION * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec; } else { if (!__sync_lock_test_and_set(lock, 1)) { clock_gettime(CLOCK_THREAD_CPUTIME_ID, &end); - cf->mem->wait_times[idx].locks_acquired_single_attempt++; - cf->mem->wait_times[idx].total_time_single += BILLION * (end.tv_sec - + qf->runtimedata->wait_times[idx].locks_acquired_single_attempt++; + qf->runtimedata->wait_times[idx].total_time_single += BILLION * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec; } else { while (__sync_lock_test_and_set(lock, 1)) while (*lock); clock_gettime(CLOCK_THREAD_CPUTIME_ID, &end); - cf->mem->wait_times[idx].total_time_spinning += BILLION * (end.tv_sec - + qf->runtimedata->wait_times[idx].total_time_spinning += BILLION * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec; } ret = true; } - cf->mem->wait_times[idx].locks_taken++; + qf->runtimedata->wait_times[idx].locks_taken++; return ret; /*start = rdtsc();*/ /*if (!__sync_lock_test_and_set(lock, 1)) {*/ /*clock_gettime(CLOCK_THREAD_CPUTIME_ID, &end);*/ - /*cf->mem->wait_times[idx].locks_acquired_single_attempt++;*/ - /*cf->mem->wait_times[idx].total_time_single += BILLION * (end.tv_sec - + /*qf->runtimedata->wait_times[idx].locks_acquired_single_attempt++;*/ + /*qf->runtimedata->wait_times[idx].total_time_single += BILLION * (end.tv_sec - * start.tv_sec) + end.tv_nsec - start.tv_nsec;*/ /*} else {*/ /*while (__sync_lock_test_and_set(lock, 1))*/ /*while (*lock);*/ /*clock_gettime(CLOCK_THREAD_CPUTIME_ID, &end);*/ - /*cf->mem->wait_times[idx].total_time_spinning += BILLION * (end.tv_sec - + /*qf->runtimedata->wait_times[idx].total_time_spinning += BILLION * (end.tv_sec - * start.tv_sec) + end.tv_nsec - start.tv_nsec;*/ /*}*/ /*end = rdtsc();*/ - /*cf->mem->wait_times[idx].locks_taken++;*/ + /*qf->runtimedata->wait_times[idx].locks_taken++;*/ /*return;*/ } #else @@ -140,9 +127,9 @@ static inline bool qf_spin_lock(QF *cf, volatile int *lock, uint64_t idx, * Try to acquire a lock once and return even if the lock is busy. * If spin flag is set, then spin until the lock is available. */ -static inline bool qf_spin_lock(volatile int *lock, enum lock flag) +static inline bool qf_spin_lock(volatile int *lock, uint8_t flag) { - if (flag != LOCK_AND_SPIN) { + if (GET_WAIT_FOR_LOCK(flag) != QF_WAIT_FOR_LOCK) { return !__sync_lock_test_and_set(lock, 1); } else { while (__sync_lock_test_and_set(lock, 1)) @@ -160,30 +147,32 @@ static inline void qf_spin_unlock(volatile int *lock) return; } -static bool qf_lock(QF *cf, uint64_t hash_bucket_index, enum lock flag, bool - small) +static bool qf_lock(QF *qf, uint64_t hash_bucket_index, bool small, uint8_t + runtime_lock) { uint64_t hash_bucket_lock_offset = hash_bucket_index % NUM_SLOTS_TO_LOCK; if (small) { #ifdef LOG_WAIT_TIME - if (!qf_spin_lock(cf, &cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK], - hash_bucket_index/NUM_SLOTS_TO_LOCK, flag)) + if (!qf_spin_lock(qf, &qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK], + hash_bucket_index/NUM_SLOTS_TO_LOCK, + runtime_lock)) return false; if (NUM_SLOTS_TO_LOCK - hash_bucket_lock_offset <= CLUSTER_SIZE) { - if (!qf_spin_lock(cf, &cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1], - hash_bucket_index/NUM_SLOTS_TO_LOCK+1, flag)) { - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); + if (!qf_spin_lock(qf, &qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1], + hash_bucket_index/NUM_SLOTS_TO_LOCK+1, + runtime_lock)) { + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); return false; } } #else - if (!qf_spin_lock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK], - flag)) + if (!qf_spin_lock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK], + runtime_lock)) return false; if (NUM_SLOTS_TO_LOCK - hash_bucket_lock_offset <= CLUSTER_SIZE) { - if (!qf_spin_lock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1], - flag)) { - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); + if (!qf_spin_lock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1], + runtime_lock)) { + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); return false; } } @@ -192,48 +181,48 @@ static bool qf_lock(QF *cf, uint64_t hash_bucket_index, enum lock flag, bool #ifdef LOG_WAIT_TIME if (hash_bucket_index >= NUM_SLOTS_TO_LOCK && hash_bucket_lock_offset <= CLUSTER_SIZE) { - if (!qf_spin_lock(cf, - &cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1], - flag)) + if (!qf_spin_lock(qf, + &qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1], + runtime_lock)) return false; } - if (!qf_spin_lock(cf, - &cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK], - flag)) { + if (!qf_spin_lock(qf, + &qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK], + runtime_lock)) { if (hash_bucket_index >= NUM_SLOTS_TO_LOCK && hash_bucket_lock_offset <= CLUSTER_SIZE) - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1]); + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1]); return false; } - if (!qf_spin_lock(cf, &cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1], - flag)) { - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); + if (!qf_spin_lock(qf, &qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1], + runtime_lock)) { + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); if (hash_bucket_index >= NUM_SLOTS_TO_LOCK && hash_bucket_lock_offset <= CLUSTER_SIZE) - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1]); + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1]); return false; } #else if (hash_bucket_index >= NUM_SLOTS_TO_LOCK && hash_bucket_lock_offset <= CLUSTER_SIZE) { if - (!qf_spin_lock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1], - flag)) + (!qf_spin_lock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1], + runtime_lock)) return false; } - if (!qf_spin_lock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK], - flag)) { + if (!qf_spin_lock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK], + runtime_lock)) { if (hash_bucket_index >= NUM_SLOTS_TO_LOCK && hash_bucket_lock_offset <= CLUSTER_SIZE) - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1]); + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1]); return false; } - if (!qf_spin_lock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1], - flag)) { - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); + if (!qf_spin_lock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1], + runtime_lock)) { + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); if (hash_bucket_index >= NUM_SLOTS_TO_LOCK && hash_bucket_lock_offset <= CLUSTER_SIZE) - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1]); + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1]); return false; } #endif @@ -241,32 +230,33 @@ static bool qf_lock(QF *cf, uint64_t hash_bucket_index, enum lock flag, bool return true; } -static void qf_unlock(QF *cf, uint64_t hash_bucket_index, bool small) +static void qf_unlock(QF *qf, uint64_t hash_bucket_index, bool small) { uint64_t hash_bucket_lock_offset = hash_bucket_index % NUM_SLOTS_TO_LOCK; if (small) { if (NUM_SLOTS_TO_LOCK - hash_bucket_lock_offset <= CLUSTER_SIZE) { - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1]); + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1]); } - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); } else { - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1]); - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK+1]); + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK]); if (hash_bucket_index >= NUM_SLOTS_TO_LOCK && hash_bucket_lock_offset <= CLUSTER_SIZE) - qf_spin_unlock(&cf->mem->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1]); + qf_spin_unlock(&qf->runtimedata->locks[hash_bucket_index/NUM_SLOTS_TO_LOCK-1]); } } -static void modify_metadata(QF *cf, uint64_t *metadata, int cnt) +static void modify_metadata(QF *qf, uint64_t *metadata, int cnt) { #ifdef LOG_WAIT_TIME - qf_spin_lock(cf, &cf->mem->metadata_lock,cf->num_locks, LOCK_AND_SPIN); + qf_spin_lock(qf, &qf->runtimedata->metadata_lock, + qf->runtimedata->num_locks, QF_WAIT_FOR_LOCK); #else - qf_spin_lock(&cf->mem->metadata_lock, LOCK_AND_SPIN); + qf_spin_lock(&qf->runtimedata->metadata_lock, QF_WAIT_FOR_LOCK); #endif *metadata = *metadata + cnt; - qf_spin_unlock(&cf->mem->metadata_lock); + qf_spin_unlock(&qf->runtimedata->metadata_lock); return; } @@ -453,7 +443,7 @@ static inline uint64_t bitselectv(const uint64_t val, int ignore, int rank) return bitselect(val & ~BITMASK(ignore % 64), rank); } -#if BITS_PER_SLOT > 0 +#if QF_BITS_PER_SLOT > 0 static inline qfblock * get_block(const QF *qf, uint64_t block_index) { return &qf->blocks[block_index]; @@ -462,55 +452,38 @@ static inline qfblock * get_block(const QF *qf, uint64_t block_index) static inline qfblock * get_block(const QF *qf, uint64_t block_index) { return (qfblock *)(((char *)qf->blocks) + block_index * (sizeof(qfblock) + - SLOTS_PER_BLOCK * qf->metadata->bits_per_slot / 8)); -} -static inline qfblock * qfi_get_block(const QFi *qfi, uint64_t block_index) -{ - return (qfblock *)(((char *)qfi->qf->blocks) + block_index * - qfi->cache.sizeof_qfblock_SLOTS_PER_BLOCK_times_bits_per_slot_div_8); + QF_SLOTS_PER_BLOCK * qf->metadata->bits_per_slot / 8)); } #endif static inline int is_runend(const QF *qf, uint64_t index) { - return (METADATA_WORD(qf, runends, index) >> ((index % SLOTS_PER_BLOCK) % - 64)) & 1ULL; -} - -static inline int qfi_is_runend(const QFi *qfi, uint64_t index) -{ - return (QFI_METADATA_WORD(qfi, runends, index) >> ((index % SLOTS_PER_BLOCK) % + return (METADATA_WORD(qf, runends, index) >> ((index % QF_SLOTS_PER_BLOCK) % 64)) & 1ULL; } static inline int is_occupied(const QF *qf, uint64_t index) { - return (METADATA_WORD(qf, occupieds, index) >> ((index % SLOTS_PER_BLOCK) % + return (METADATA_WORD(qf, occupieds, index) >> ((index % QF_SLOTS_PER_BLOCK) % 64)) & 1ULL; } -#if BITS_PER_SLOT == 8 || BITS_PER_SLOT == 16 || BITS_PER_SLOT == 32 || BITS_PER_SLOT == 64 +#if QF_BITS_PER_SLOT == 8 || QF_BITS_PER_SLOT == 16 || QF_BITS_PER_SLOT == 32 || QF_BITS_PER_SLOT == 64 static inline uint64_t get_slot(const QF *qf, uint64_t index) { assert(index < qf->metadata->xnslots); - return get_block(qf, index / SLOTS_PER_BLOCK)->slots[index % SLOTS_PER_BLOCK]; -} - -static inline uint64_t qfi_get_slot(const QFi *qfi, uint64_t index) -{ - assert(index < qfi->qf->metadata->xnslots); - return qfi_get_block(qfi, index / SLOTS_PER_BLOCK)->slots[index % SLOTS_PER_BLOCK]; + return get_block(qf, index / QF_SLOTS_PER_BLOCK)->slots[index % QF_SLOTS_PER_BLOCK]; } static inline void set_slot(const QF *qf, uint64_t index, uint64_t value) { assert(index < qf->metadata->xnslots); - get_block(qf, index / SLOTS_PER_BLOCK)->slots[index % SLOTS_PER_BLOCK] = + get_block(qf, index / QF_SLOTS_PER_BLOCK)->slots[index % QF_SLOTS_PER_BLOCK] = value & BITMASK(qf->metadata->bits_per_slot); } -#elif BITS_PER_SLOT > 0 +#elif QF_BITS_PER_SLOT > 0 /* Little-endian code .... Big-endian is TODO */ @@ -520,24 +493,11 @@ static inline uint64_t get_slot(const QF *qf, uint64_t index) * to generate buggy code. :/ */ assert(index < qf->metadata->xnslots); uint64_t *p = (uint64_t *)&get_block(qf, index / - SLOTS_PER_BLOCK)->slots[(index % - SLOTS_PER_BLOCK) - * BITS_PER_SLOT / 8]; - return (uint64_t)(((*p) >> (((index % SLOTS_PER_BLOCK) * BITS_PER_SLOT) % - 8)) & BITMASK(BITS_PER_SLOT)); -} - -static inline uint64_t qfi_get_slot(const QFi *qfi, uint64_t index) -{ - /* Should use __uint128_t to support up to 64-bit remainders, but gcc seems - * to generate buggy code. :/ */ - assert(index < qfi->qf->metadata->xnslots); - uint64_t *p = (uint64_t *)&qfi_get_block(qfi, index / - SLOTS_PER_BLOCK)->slots[(index % - SLOTS_PER_BLOCK) - * BITS_PER_SLOT / 8]; - return (uint64_t)(((*p) >> (((index % SLOTS_PER_BLOCK) * BITS_PER_SLOT) % - 8)) & BITMASK(BITS_PER_SLOT)); + QF_SLOTS_PER_BLOCK)->slots[(index % + QF_SLOTS_PER_BLOCK) + * QF_BITS_PER_SLOT / 8]; + return (uint64_t)(((*p) >> (((index % QF_SLOTS_PER_BLOCK) * QF_BITS_PER_SLOT) % + 8)) & BITMASK(QF_BITS_PER_SLOT)); } static inline void set_slot(const QF *qf, uint64_t index, uint64_t value) @@ -546,13 +506,13 @@ static inline void set_slot(const QF *qf, uint64_t index, uint64_t value) * to generate buggy code. :/ */ assert(index < qf->metadata->xnslots); uint64_t *p = (uint64_t *)&get_block(qf, index / - SLOTS_PER_BLOCK)->slots[(index % - SLOTS_PER_BLOCK) - * BITS_PER_SLOT / 8]; + QF_SLOTS_PER_BLOCK)->slots[(index % + QF_SLOTS_PER_BLOCK) + * QF_BITS_PER_SLOT / 8]; uint64_t t = *p; - uint64_t mask = BITMASK(BITS_PER_SLOT); + uint64_t mask = BITMASK(QF_BITS_PER_SLOT); uint64_t v = value; - int shift = ((index % SLOTS_PER_BLOCK) * BITS_PER_SLOT) % 8; + int shift = ((index % QF_SLOTS_PER_BLOCK) * QF_BITS_PER_SLOT) % 8; mask <<= shift; v <<= shift; t &= ~mask; @@ -570,41 +530,27 @@ static inline uint64_t get_slot(const QF *qf, uint64_t index) /* Should use __uint128_t to support up to 64-bit remainders, but gcc seems * to generate buggy code. :/ */ uint64_t *p = (uint64_t *)&get_block(qf, index / - SLOTS_PER_BLOCK)->slots[(index % - SLOTS_PER_BLOCK) + QF_SLOTS_PER_BLOCK)->slots[(index % + QF_SLOTS_PER_BLOCK) * qf->metadata->bits_per_slot / 8]; - return (uint64_t)(((*p) >> (((index % SLOTS_PER_BLOCK) * + return (uint64_t)(((*p) >> (((index % QF_SLOTS_PER_BLOCK) * qf->metadata->bits_per_slot) % 8)) & BITMASK(qf->metadata->bits_per_slot)); } -static inline uint64_t qfi_get_slot(const QFi *qfi, uint64_t index) -{ - assert(index < qfi->qf->metadata->xnslots); - /* Should use __uint128_t to support up to 64-bit remainders, but gcc seems - * to generate buggy code. :/ */ - uint64_t *p = (uint64_t *)&qfi_get_block(qfi, index / - SLOTS_PER_BLOCK)->slots[(index % - SLOTS_PER_BLOCK) - * qfi->cache.bits_per_slot / 8]; - return (uint64_t)(((*p) >> (((index % SLOTS_PER_BLOCK) * - qfi->cache.bits_per_slot) % 8)) & - BITMASK(qfi->cache.bits_per_slot)); -} - static inline void set_slot(const QF *qf, uint64_t index, uint64_t value) { assert(index < qf->metadata->xnslots); /* Should use __uint128_t to support up to 64-bit remainders, but gcc seems * to generate buggy code. :/ */ uint64_t *p = (uint64_t *)&get_block(qf, index / - SLOTS_PER_BLOCK)->slots[(index % - SLOTS_PER_BLOCK) + QF_SLOTS_PER_BLOCK)->slots[(index % + QF_SLOTS_PER_BLOCK) * qf->metadata->bits_per_slot / 8]; uint64_t t = *p; uint64_t mask = BITMASK(qf->metadata->bits_per_slot); uint64_t v = value; - int shift = ((index % SLOTS_PER_BLOCK) * qf->metadata->bits_per_slot) % 8; + int shift = ((index % QF_SLOTS_PER_BLOCK) * qf->metadata->bits_per_slot) % 8; mask <<= shift; v <<= shift; t &= ~mask; @@ -625,14 +571,14 @@ static inline uint64_t block_offset(const QF *qf, uint64_t blockidx) get_block(qf, blockidx)->offset < BITMASK(8*sizeof(qf->blocks[0].offset))) return get_block(qf, blockidx)->offset; - return run_end(qf, SLOTS_PER_BLOCK * blockidx - 1) - SLOTS_PER_BLOCK * + return run_end(qf, QF_SLOTS_PER_BLOCK * blockidx - 1) - QF_SLOTS_PER_BLOCK * blockidx + 1; } static inline uint64_t run_end(const QF *qf, uint64_t hash_bucket_index) { - uint64_t bucket_block_index = hash_bucket_index / SLOTS_PER_BLOCK; - uint64_t bucket_intrablock_offset = hash_bucket_index % SLOTS_PER_BLOCK; + uint64_t bucket_block_index = hash_bucket_index / QF_SLOTS_PER_BLOCK; + uint64_t bucket_intrablock_offset = hash_bucket_index % QF_SLOTS_PER_BLOCK; uint64_t bucket_blocks_offset = block_offset(qf, bucket_block_index); uint64_t bucket_intrablock_rank = bitrank(get_block(qf, @@ -643,17 +589,17 @@ static inline uint64_t run_end(const QF *qf, uint64_t hash_bucket_index) if (bucket_blocks_offset <= bucket_intrablock_offset) return hash_bucket_index; else - return SLOTS_PER_BLOCK * bucket_block_index + bucket_blocks_offset - 1; + return QF_SLOTS_PER_BLOCK * bucket_block_index + bucket_blocks_offset - 1; } uint64_t runend_block_index = bucket_block_index + bucket_blocks_offset / - SLOTS_PER_BLOCK; - uint64_t runend_ignore_bits = bucket_blocks_offset % SLOTS_PER_BLOCK; + QF_SLOTS_PER_BLOCK; + uint64_t runend_ignore_bits = bucket_blocks_offset % QF_SLOTS_PER_BLOCK; uint64_t runend_rank = bucket_intrablock_rank - 1; uint64_t runend_block_offset = bitselectv(get_block(qf, runend_block_index)->runends[0], runend_ignore_bits, runend_rank); - if (runend_block_offset == SLOTS_PER_BLOCK) { + if (runend_block_offset == QF_SLOTS_PER_BLOCK) { if (bucket_blocks_offset == 0 && bucket_intrablock_rank == 0) { /* The block begins in empty space, and this bucket is in that region of * empty space */ @@ -668,11 +614,11 @@ static inline uint64_t run_end(const QF *qf, uint64_t hash_bucket_index) runend_block_offset = bitselectv(get_block(qf, runend_block_index)->runends[0], runend_ignore_bits, runend_rank); - } while (runend_block_offset == SLOTS_PER_BLOCK); + } while (runend_block_offset == QF_SLOTS_PER_BLOCK); } } - uint64_t runend_index = SLOTS_PER_BLOCK * runend_block_index + + uint64_t runend_index = QF_SLOTS_PER_BLOCK * runend_block_index + runend_block_offset; if (runend_index < hash_bucket_index) return hash_bucket_index; @@ -682,11 +628,11 @@ static inline uint64_t run_end(const QF *qf, uint64_t hash_bucket_index) static inline int offset_lower_bound(const QF *qf, uint64_t slot_index) { - const qfblock * b = get_block(qf, slot_index / SLOTS_PER_BLOCK); - const uint64_t slot_offset = slot_index % SLOTS_PER_BLOCK; + const qfblock * b = get_block(qf, slot_index / QF_SLOTS_PER_BLOCK); + const uint64_t slot_offset = slot_index % QF_SLOTS_PER_BLOCK; const uint64_t boffset = b->offset; const uint64_t occupieds = b->occupieds[0] & BITMASK(slot_offset+1); - assert(SLOTS_PER_BLOCK == 64); + assert(QF_SLOTS_PER_BLOCK == 64); if (boffset <= slot_offset) { const uint64_t runends = (b->runends[0] & BITMASK(slot_offset)) >> boffset; return popcnt(occupieds) - popcnt(runends); @@ -735,15 +681,15 @@ static inline uint64_t shift_into_b(const uint64_t a, const uint64_t b, return a_component | b_shifted | (b & b_mask); } -#if BITS_PER_SLOT == 8 || BITS_PER_SLOT == 16 || BITS_PER_SLOT == 32 || BITS_PER_SLOT == 64 +#if QF_BITS_PER_SLOT == 8 || QF_BITS_PER_SLOT == 16 || QF_BITS_PER_SLOT == 32 || QF_BITS_PER_SLOT == 64 static inline void shift_remainders(QF *qf, uint64_t start_index, uint64_t empty_index) { - uint64_t start_block = start_index / SLOTS_PER_BLOCK; - uint64_t start_offset = start_index % SLOTS_PER_BLOCK; - uint64_t empty_block = empty_index / SLOTS_PER_BLOCK; - uint64_t empty_offset = empty_index % SLOTS_PER_BLOCK; + uint64_t start_block = start_index / QF_SLOTS_PER_BLOCK; + uint64_t start_offset = start_index % QF_SLOTS_PER_BLOCK; + uint64_t empty_block = empty_index / QF_SLOTS_PER_BLOCK; + uint64_t empty_offset = empty_index % QF_SLOTS_PER_BLOCK; assert (start_index <= empty_index && empty_index < qf->metadata->xnslots); @@ -752,9 +698,9 @@ static inline void shift_remainders(QF *qf, uint64_t start_index, uint64_t &get_block(qf, empty_block)->slots[0], empty_offset * sizeof(qf->blocks[0].slots[0])); get_block(qf, empty_block)->slots[0] = get_block(qf, - empty_block-1)->slots[SLOTS_PER_BLOCK-1]; + empty_block-1)->slots[QF_SLOTS_PER_BLOCK-1]; empty_block--; - empty_offset = SLOTS_PER_BLOCK-1; + empty_offset = QF_SLOTS_PER_BLOCK-1; } memmove(&get_block(qf, empty_block)->slots[start_offset+1], @@ -796,26 +742,26 @@ static inline void qf_dump_block(const QF *qf, uint64_t i) printf("%-192d", get_block(qf, i)->offset); printf("\n"); - for (j = 0; j < SLOTS_PER_BLOCK; j++) + for (j = 0; j < QF_SLOTS_PER_BLOCK; j++) printf("%02lx ", j); printf("\n"); - for (j = 0; j < SLOTS_PER_BLOCK; j++) + for (j = 0; j < QF_SLOTS_PER_BLOCK; j++) printf(" %d ", (get_block(qf, i)->occupieds[j/64] & (1ULL << (j%64))) ? 1 : 0); printf("\n"); - for (j = 0; j < SLOTS_PER_BLOCK; j++) + for (j = 0; j < QF_SLOTS_PER_BLOCK; j++) printf(" %d ", (get_block(qf, i)->runends[j/64] & (1ULL << (j%64))) ? 1 : 0); printf("\n"); -#if BITS_PER_SLOT == 8 || BITS_PER_SLOT == 16 || BITS_PER_SLOT == 32 - for (j = 0; j < SLOTS_PER_BLOCK; j++) +#if QF_BITS_PER_SLOT == 8 || QF_BITS_PER_SLOT == 16 || QF_BITS_PER_SLOT == 32 + for (j = 0; j < QF_SLOTS_PER_BLOCK; j++) printf("%02x ", get_block(qf, i)->slots[j]); -#elif BITS_PER_SLOT == 64 - for (j = 0; j < SLOTS_PER_BLOCK; j++) +#elif QF_BITS_PER_SLOT == 64 + for (j = 0; j < QF_SLOTS_PER_BLOCK; j++) printf("%02lx ", get_block(qf, i)->slots[j]); #else - for (j = 0; j < SLOTS_PER_BLOCK * qf->metadata->bits_per_slot / 8; j++) + for (j = 0; j < QF_SLOTS_PER_BLOCK * qf->metadata->bits_per_slot / 8; j++) printf("%02x ", get_block(qf, i)->slots[j]); #endif @@ -825,7 +771,6 @@ static inline void qf_dump_block(const QF *qf, uint64_t i) } void qf_dump_metadata(const QF *qf) { - printf("Seed: %u\n", qf->metadata->seed); printf("Slots: %lu Occupied: %lu Elements: %lu Distinct: %lu\n", qf->metadata->nslots, qf->metadata->noccupied_slots, @@ -902,7 +847,7 @@ static inline void shift_runends(QF *qf, int64_t first, uint64_t last, } -static inline void insert_replace_slots_and_shift_remainders_and_runends_and_offsets(QF *qf, +static inline bool insert_replace_slots_and_shift_remainders_and_runends_and_offsets(QF *qf, int operation, uint64_t bucket_index, uint64_t overwrite_index, @@ -912,27 +857,29 @@ static inline void insert_replace_slots_and_shift_remainders_and_runends_and_off { uint64_t empties[67]; uint64_t i; + int64_t j; int64_t ninserts = total_remainders - noverwrites; uint64_t insert_index = overwrite_index + noverwrites; if (ninserts > 0) { /* First, shift things to create n empty spaces where we need them. */ find_next_n_empty_slots(qf, insert_index, ninserts, empties); - - for (i = 0; i < ninserts - 1; i++) - shift_slots(qf, empties[i+1] + 1, empties[i] - 1, i + 1); + if (empties[0] >= qf->metadata->xnslots) { + return false; + } + for (j = 0; j < ninserts - 1; j++) + shift_slots(qf, empties[j+1] + 1, empties[j] - 1, j + 1); shift_slots(qf, insert_index, empties[ninserts - 1] - 1, ninserts); - for (i = 0; i < ninserts - 1; i++) - shift_runends(qf, empties[i+1] + 1, empties[i] - 1, i + 1); + for (j = 0; j < ninserts - 1; j++) + shift_runends(qf, empties[j+1] + 1, empties[j] - 1, j + 1); shift_runends(qf, insert_index, empties[ninserts - 1] - 1, ninserts); - for (i = noverwrites; i < total_remainders - 1; i++) METADATA_WORD(qf, runends, overwrite_index + i) &= ~(1ULL << (((overwrite_index + i) % - SLOTS_PER_BLOCK) + QF_SLOTS_PER_BLOCK) % 64)); switch (operation) { @@ -940,20 +887,20 @@ static inline void insert_replace_slots_and_shift_remainders_and_runends_and_off assert (noverwrites == 0); METADATA_WORD(qf, runends, overwrite_index + total_remainders - 1) |= 1ULL << (((overwrite_index + total_remainders - 1) % - SLOTS_PER_BLOCK) % 64); + QF_SLOTS_PER_BLOCK) % 64); break; case 1: /* append to bucket */ METADATA_WORD(qf, runends, overwrite_index + noverwrites - 1) &= - ~(1ULL << (((overwrite_index + noverwrites - 1) % SLOTS_PER_BLOCK) % + ~(1ULL << (((overwrite_index + noverwrites - 1) % QF_SLOTS_PER_BLOCK) % 64)); METADATA_WORD(qf, runends, overwrite_index + total_remainders - 1) |= 1ULL << (((overwrite_index + total_remainders - 1) % - SLOTS_PER_BLOCK) % 64); + QF_SLOTS_PER_BLOCK) % 64); break; case 2: /* insert into bucket */ METADATA_WORD(qf, runends, overwrite_index + total_remainders - 1) &= ~(1ULL << (((overwrite_index + total_remainders - 1) % - SLOTS_PER_BLOCK) % 64)); + QF_SLOTS_PER_BLOCK) % 64)); break; default: fprintf(stderr, "Invalid operation %d\n", operation); @@ -961,11 +908,11 @@ static inline void insert_replace_slots_and_shift_remainders_and_runends_and_off } uint64_t npreceding_empties = 0; - for (i = bucket_index / SLOTS_PER_BLOCK + 1; i <= empties[0]/SLOTS_PER_BLOCK; i++) { - while (npreceding_empties < ninserts && - empties[ninserts - 1 - npreceding_empties] / SLOTS_PER_BLOCK < i) + for (i = bucket_index / QF_SLOTS_PER_BLOCK + 1; i <= empties[0]/QF_SLOTS_PER_BLOCK; i++) { + while ((int64_t)npreceding_empties < ninserts && + empties[ninserts - 1 - npreceding_empties] / QF_SLOTS_PER_BLOCK < i) npreceding_empties++; - + if (get_block(qf, i)->offset + ninserts - npreceding_empties < BITMASK(8*sizeof(qf->blocks[0].offset))) get_block(qf, i)->offset += ninserts - npreceding_empties; else @@ -978,19 +925,10 @@ static inline void insert_replace_slots_and_shift_remainders_and_runends_and_off modify_metadata(qf, &qf->metadata->noccupied_slots, ninserts); - static uint64_t counter = 0; - static uint64_t last_cnt = 0; - counter++; - if (counter % 10000000 == 0 && - counter != last_cnt) { - last_cnt = counter; - fprintf(stdout, "Home slot: %ld Insertion slot: %ld Difference: %ld Fraction done: %lf\n", - bucket_index, overwrite_index, overwrite_index - bucket_index, - bucket_index / (float)qf->metadata->nslots); - } + return true; } -static inline void remove_replace_slots_and_shift_remainders_and_runends_and_offsets(QF *qf, +static inline int remove_replace_slots_and_shift_remainders_and_runends_and_offsets(QF *qf, int operation, uint64_t bucket_index, uint64_t overwrite_index, @@ -1022,6 +960,7 @@ static inline void remove_replace_slots_and_shift_remainders_and_runends_and_off uint64_t current_bucket = bucket_index; uint64_t current_slot = overwrite_index + total_remainders; uint64_t current_distance = old_length - total_remainders; + int ret_current_distance = current_distance; while (current_distance > 0) { if (is_runend(qf, current_slot + current_distance - 1)) { @@ -1062,7 +1001,7 @@ static inline void remove_replace_slots_and_shift_remainders_and_runends_and_off // Then find the runend slot corresponding to the last run in the // original_bucket block. // Update the offset of the block to which it belongs. - uint64_t original_block = original_bucket / SLOTS_PER_BLOCK; + uint64_t original_block = original_bucket / QF_SLOTS_PER_BLOCK; while (1 && old_length > total_remainders) { // we only update offsets if we shift/delete anything int32_t last_occupieds_bit = bitscanreverse(get_block(qf, original_block)->occupieds[0]); // there is nothing in the block @@ -1071,25 +1010,25 @@ static inline void remove_replace_slots_and_shift_remainders_and_runends_and_off break; get_block(qf, original_block + 1)->offset = 0; } else { - uint64_t last_occupieds_hash_index = SLOTS_PER_BLOCK * original_block + last_occupieds_bit; + uint64_t last_occupieds_hash_index = QF_SLOTS_PER_BLOCK * original_block + last_occupieds_bit; uint64_t runend_index = run_end(qf, last_occupieds_hash_index); // runend spans across the block // update the offset of the next block - if (runend_index / SLOTS_PER_BLOCK == original_block) { // if the run ends in the same block + if (runend_index / QF_SLOTS_PER_BLOCK == original_block) { // if the run ends in the same block if (get_block(qf, original_block + 1)->offset == 0) break; get_block(qf, original_block + 1)->offset = 0; - } else if (runend_index / SLOTS_PER_BLOCK == original_block + 1) { // if the last run spans across one block - if (get_block(qf, original_block + 1)->offset == (runend_index % SLOTS_PER_BLOCK) + 1) + } else if (runend_index / QF_SLOTS_PER_BLOCK == original_block + 1) { // if the last run spans across one block + if (get_block(qf, original_block + 1)->offset == (runend_index % QF_SLOTS_PER_BLOCK) + 1) break; - get_block(qf, original_block + 1)->offset = (runend_index % SLOTS_PER_BLOCK) + 1; + get_block(qf, original_block + 1)->offset = (runend_index % QF_SLOTS_PER_BLOCK) + 1; } else { // if the last run spans across multiple blocks uint64_t i; - for (i = original_block + 1; i < runend_index / SLOTS_PER_BLOCK - 1; i++) - get_block(qf, i)->offset = SLOTS_PER_BLOCK; - if (get_block(qf, runend_index / SLOTS_PER_BLOCK)->offset == (runend_index % SLOTS_PER_BLOCK) + 1) + for (i = original_block + 1; i < runend_index / QF_SLOTS_PER_BLOCK - 1; i++) + get_block(qf, i)->offset = QF_SLOTS_PER_BLOCK; + if (get_block(qf, runend_index / QF_SLOTS_PER_BLOCK)->offset == (runend_index % QF_SLOTS_PER_BLOCK) + 1) break; - get_block(qf, runend_index / SLOTS_PER_BLOCK)->offset = (runend_index % SLOTS_PER_BLOCK) + 1; + get_block(qf, runend_index / QF_SLOTS_PER_BLOCK)->offset = (runend_index % QF_SLOTS_PER_BLOCK) + 1; } } original_block++; @@ -1102,6 +1041,8 @@ static inline void remove_replace_slots_and_shift_remainders_and_runends_and_off modify_metadata(qf, &qf->metadata->ndistinct_elts, -1); /*qf->metadata->ndistinct_elts--;*/ } + + return ret_current_distance; } /***************************************************************************** @@ -1249,80 +1190,6 @@ static inline uint64_t decode_counter(const QF *qf, uint64_t index, uint64_t return end + 1; } -/* Returns the length of the encoding. -REQUIRES: index points to first slot of a counter. */ -static inline uint64_t qfi_decode_counter(const QFi *qfi, uint64_t index, uint64_t - *remainder, uint64_t *count) -{ - uint64_t base; - uint64_t rem; - uint64_t cnt; - uint64_t digit; - uint64_t end; - - *remainder = rem = qfi_get_slot(qfi, index); - - if (qfi_is_runend(qfi, index)) { /* Entire run is "0" */ - *count = 1; - return index; - } - - digit = qfi_get_slot(qfi, index + 1); - - if (qfi_is_runend(qfi, index + 1)) { - *count = digit == rem ? 2 : 1; - return index + (digit == rem ? 1 : 0); - } - - if (rem > 0 && digit >= rem) { - *count = digit == rem ? 2 : 1; - return index + (digit == rem ? 1 : 0); - } - - if (rem > 0 && digit == 0 && qfi_get_slot(qfi, index + 2) == rem) { - *count = 3; - return index + 2; - } - - if (rem == 0 && digit == 0) { - if (qfi_get_slot(qfi, index + 2) == 0) { - *count = 3; - return index + 2; - } else { - *count = 2; - return index + 1; - } - } - - cnt = 0; - base = (1ULL << qfi->qf->metadata->bits_per_slot) - (rem ? 2 : 1); - - end = index + 1; - while (digit != rem && !qfi_is_runend(qfi, end)) { - if (digit > rem) - digit--; - if (digit && rem) - digit--; - cnt = cnt * base + digit; - - end++; - digit = qfi_get_slot(qfi, end); - } - - if (rem) { - *count = cnt + 3; - return end; - } - - if (qfi_is_runend(qfi, end) || qfi_get_slot(qfi, end + 1) != 0) { - *count = 1; - return index; - } - - *count = cnt + 4; - return end + 1; -} - /* return the next slot which corresponds to a * different element * */ @@ -1337,15 +1204,16 @@ static inline uint64_t next_slot(QF *qf, uint64_t current) return current; } -static inline bool insert1(QF *qf, __uint128_t hash, enum lock flag) +static inline int insert1(QF *qf, __uint128_t hash, uint8_t runtime_lock) { + int ret_distance = 0; uint64_t hash_remainder = hash & BITMASK(qf->metadata->bits_per_slot); uint64_t hash_bucket_index = hash >> qf->metadata->bits_per_slot; - uint64_t hash_bucket_block_offset = hash_bucket_index % SLOTS_PER_BLOCK; + uint64_t hash_bucket_block_offset = hash_bucket_index % QF_SLOTS_PER_BLOCK; - if (flag != NO_LOCK) { - if (!qf_lock(qf, hash_bucket_index, flag, /*small*/ true)) - return false; + if (GET_NO_LOCK(runtime_lock) != QF_NO_LOCK) { + if (!qf_lock(qf, hash_bucket_index, /*small*/ true, runtime_lock)) + return QF_COULDNT_LOCK; } if (is_empty(qf, hash_bucket_index) /* might_be_empty(qf, hash_bucket_index) && runend_index == hash_bucket_index */) { METADATA_WORD(qf, runends, hash_bucket_index) |= 1ULL << @@ -1353,7 +1221,8 @@ static inline bool insert1(QF *qf, __uint128_t hash, enum lock flag) set_slot(qf, hash_bucket_index, hash_remainder); METADATA_WORD(qf, occupieds, hash_bucket_index) |= 1ULL << (hash_bucket_block_offset % 64); - + + ret_distance = 0; modify_metadata(qf, &qf->metadata->ndistinct_elts, 1); modify_metadata(qf, &qf->metadata->noccupied_slots, 1); modify_metadata(qf, &qf->metadata->nelts, 1); @@ -1523,44 +1392,36 @@ static inline bool insert1(QF *qf, __uint128_t hash, enum lock flag) if (operation >= 0) { uint64_t empty_slot_index = find_first_empty_slot(qf, runend_index+1); - + if (empty_slot_index >= qf->metadata->xnslots) { + return QF_NO_SPACE; + } shift_remainders(qf, insert_index, empty_slot_index); set_slot(qf, insert_index, new_value); - - static uint64_t counter = 0; - static uint64_t last_cnt = 0; - counter++; - if (counter % 10000000 == 0 && - counter != last_cnt) { - last_cnt = counter; - fprintf(stdout, "Home slot: %ld Insertion slot: %ld Difference: %ld Fraction done: %lf\n", - hash_bucket_index, insert_index, insert_index - hash_bucket_index, - hash_bucket_index / (float)qf->metadata->nslots); - } + ret_distance = insert_index - hash_bucket_index; shift_runends(qf, insert_index, empty_slot_index-1, 1); switch (operation) { case 0: METADATA_WORD(qf, runends, insert_index) |= 1ULL << ((insert_index % - SLOTS_PER_BLOCK) + QF_SLOTS_PER_BLOCK) % 64); break; case 1: METADATA_WORD(qf, runends, insert_index-1) &= ~(1ULL << (((insert_index-1) % - SLOTS_PER_BLOCK) % + QF_SLOTS_PER_BLOCK) % 64)); METADATA_WORD(qf, runends, insert_index) |= 1ULL << ((insert_index % - SLOTS_PER_BLOCK) + QF_SLOTS_PER_BLOCK) % 64); break; case 2: METADATA_WORD(qf, runends, insert_index) &= ~(1ULL << ((insert_index % - SLOTS_PER_BLOCK) % + QF_SLOTS_PER_BLOCK) % 64)); break; default: @@ -1572,8 +1433,8 @@ static inline bool insert1(QF *qf, __uint128_t hash, enum lock flag) * and block of the empty slot * */ uint64_t i; - for (i = hash_bucket_index / SLOTS_PER_BLOCK + 1; i <= - empty_slot_index/SLOTS_PER_BLOCK; i++) { + for (i = hash_bucket_index / QF_SLOTS_PER_BLOCK + 1; i <= + empty_slot_index/QF_SLOTS_PER_BLOCK; i++) { if (get_block(qf, i)->offset < BITMASK(8*sizeof(qf->blocks[0].offset))) get_block(qf, i)->offset++; assert(get_block(qf, i)->offset != 0); @@ -1585,24 +1446,25 @@ static inline bool insert1(QF *qf, __uint128_t hash, enum lock flag) (hash_bucket_block_offset % 64); } - if (flag != NO_LOCK) { + if (GET_NO_LOCK(runtime_lock) != QF_NO_LOCK) { qf_unlock(qf, hash_bucket_index, /*small*/ true); } - return true; + return ret_distance; } -static inline bool insert(QF *qf, __uint128_t hash, uint64_t count, enum lock - flag) +static inline int insert(QF *qf, __uint128_t hash, uint64_t count, uint8_t + runtime_lock) { + int ret_distance = 0; uint64_t hash_remainder = hash & BITMASK(qf->metadata->bits_per_slot); uint64_t hash_bucket_index = hash >> qf->metadata->bits_per_slot; - uint64_t hash_bucket_block_offset = hash_bucket_index % SLOTS_PER_BLOCK; + uint64_t hash_bucket_block_offset = hash_bucket_index % QF_SLOTS_PER_BLOCK; /*uint64_t hash_bucket_lock_offset = hash_bucket_index % NUM_SLOTS_TO_LOCK;*/ - if (flag != NO_LOCK) { - if (!qf_lock(qf, hash_bucket_index, flag, /*small*/ false)) - return false; + if (GET_NO_LOCK(runtime_lock) != QF_NO_LOCK) { + if (!qf_lock(qf, hash_bucket_index, /*small*/ false, runtime_lock)) + return QF_COULDNT_LOCK; } uint64_t runend_index = run_end(qf, hash_bucket_index); @@ -1621,7 +1483,7 @@ static inline bool insert(QF *qf, __uint128_t hash, uint64_t count, enum lock modify_metadata(qf, &qf->metadata->nelts, 1); /* This trick will, I hope, keep the fast case fast. */ if (count > 1) { - insert(qf, hash, count - 1, NO_LOCK); + insert(qf, hash, count - 1, QF_NO_LOCK); } } else { /* Non-empty slot */ uint64_t new_values[67]; @@ -1629,16 +1491,20 @@ static inline bool insert(QF *qf, __uint128_t hash, uint64_t count, enum lock hash_bucket_index - 1) + 1; + bool ret; if (!is_occupied(qf, hash_bucket_index)) { /* Empty bucket, but its slot is occupied. */ uint64_t *p = encode_counter(qf, hash_remainder, count, &new_values[67]); - insert_replace_slots_and_shift_remainders_and_runends_and_offsets(qf, - 0, - hash_bucket_index, - runstart_index, - p, - &new_values[67] - p, - 0); + ret = insert_replace_slots_and_shift_remainders_and_runends_and_offsets(qf, + 0, + hash_bucket_index, + runstart_index, + p, + &new_values[67] - p, + 0); + if (!ret) + return QF_NO_SPACE; modify_metadata(qf, &qf->metadata->ndistinct_elts, 1); + ret_distance = runstart_index - hash_bucket_index; } else { /* Non-empty bucket */ uint64_t current_remainder, current_count, current_end; @@ -1656,36 +1522,45 @@ static inline bool insert(QF *qf, __uint128_t hash, uint64_t count, enum lock then append a counter for this remainder to the run. */ if (current_remainder < hash_remainder) { uint64_t *p = encode_counter(qf, hash_remainder, count, &new_values[67]); - insert_replace_slots_and_shift_remainders_and_runends_and_offsets(qf, - 1, /* Append to bucket */ - hash_bucket_index, - current_end + 1, - p, - &new_values[67] - p, - 0); + ret = insert_replace_slots_and_shift_remainders_and_runends_and_offsets(qf, + 1, /* Append to bucket */ + hash_bucket_index, + current_end + 1, + p, + &new_values[67] - p, + 0); + if (!ret) + return QF_NO_SPACE; modify_metadata(qf, &qf->metadata->ndistinct_elts, 1); + ret_distance = (current_end + 1) - hash_bucket_index; /* Found a counter for this remainder. Add in the new count. */ } else if (current_remainder == hash_remainder) { uint64_t *p = encode_counter(qf, hash_remainder, current_count + count, &new_values[67]); - insert_replace_slots_and_shift_remainders_and_runends_and_offsets(qf, + ret = insert_replace_slots_and_shift_remainders_and_runends_and_offsets(qf, is_runend(qf, current_end) ? 1 : 2, hash_bucket_index, runstart_index, p, &new_values[67] - p, current_end - runstart_index + 1); + if (!ret) + return QF_NO_SPACE; + ret_distance = runstart_index - hash_bucket_index; /* No counter for this remainder, but there are larger remainders, so we're not appending to the bucket. */ } else { uint64_t *p = encode_counter(qf, hash_remainder, count, &new_values[67]); - insert_replace_slots_and_shift_remainders_and_runends_and_offsets(qf, - 2, /* Insert to bucket */ - hash_bucket_index, - runstart_index, - p, - &new_values[67] - p, - 0); + ret = insert_replace_slots_and_shift_remainders_and_runends_and_offsets(qf, + 2, /* Insert to bucket */ + hash_bucket_index, + runstart_index, + p, + &new_values[67] - p, + 0); + if (!ret) + return QF_NO_SPACE; modify_metadata(qf, &qf->metadata->ndistinct_elts, 1); + ret_distance = runstart_index - hash_bucket_index; } } METADATA_WORD(qf, occupieds, hash_bucket_index) |= 1ULL << (hash_bucket_block_offset % 64); @@ -1693,29 +1568,30 @@ static inline bool insert(QF *qf, __uint128_t hash, uint64_t count, enum lock modify_metadata(qf, &qf->metadata->nelts, count); } - if (flag != NO_LOCK) { + if (GET_NO_LOCK(runtime_lock) != QF_NO_LOCK) { qf_unlock(qf, hash_bucket_index, /*small*/ false); } - return true; + return ret_distance; } -inline static bool _remove(QF *qf, __uint128_t hash, uint64_t count, enum lock - flag) +inline static int _remove(QF *qf, __uint128_t hash, uint64_t count, uint8_t + runtime_lock) { + int ret_numfreedslots = 0; uint64_t hash_remainder = hash & BITMASK(qf->metadata->bits_per_slot); uint64_t hash_bucket_index = hash >> qf->metadata->bits_per_slot; uint64_t current_remainder, current_count, current_end; uint64_t new_values[67]; - if (flag != NO_LOCK) { - if (!qf_lock(qf, hash_bucket_index, flag, /*small*/ false)) - return false; + if (GET_NO_LOCK(runtime_lock) != QF_NO_LOCK) { + if (!qf_lock(qf, hash_bucket_index, /*small*/ false, runtime_lock)) + return -2; } /* Empty bucket */ if (!is_occupied(qf, hash_bucket_index)) - return false; + return -1; uint64_t runstart_index = hash_bucket_index == 0 ? 0 : run_end(qf, hash_bucket_index - 1) + 1; uint64_t original_runstart_index = runstart_index; @@ -1729,7 +1605,7 @@ inline static bool _remove(QF *qf, __uint128_t hash, uint64_t count, enum lock } /* remainder not found in the given run */ if (current_remainder != hash_remainder) - return false; + return -1; if (original_runstart_index == runstart_index && is_runend(qf, current_end)) only_item_in_the_run = 1; @@ -1738,7 +1614,7 @@ inline static bool _remove(QF *qf, __uint128_t hash, uint64_t count, enum lock uint64_t *p = encode_counter(qf, hash_remainder, count > current_count ? 0 : current_count - count, &new_values[67]); - remove_replace_slots_and_shift_remainders_and_runends_and_offsets(qf, + ret_numfreedslots = remove_replace_slots_and_shift_remainders_and_runends_and_offsets(qf, only_item_in_the_run, hash_bucket_index, runstart_index, @@ -1750,28 +1626,30 @@ inline static bool _remove(QF *qf, __uint128_t hash, uint64_t count, enum lock modify_metadata(qf, &qf->metadata->nelts, -count); /*qf->metadata->nelts -= count;*/ - if (flag != NO_LOCK) { + if (GET_NO_LOCK(runtime_lock) != QF_NO_LOCK) { qf_unlock(qf, hash_bucket_index, /*small*/ false); } - return true; + return ret_numfreedslots; } /*********************************************************************** * Code that uses the above to implement key-value-counter operations. * ***********************************************************************/ -void qf_init(QF *qf, uint64_t nslots, uint64_t key_bits, uint64_t value_bits, - bool mem, const char * path, uint32_t seed) +uint64_t qf_init(QF *qf, uint64_t nslots, uint64_t key_bits, uint64_t value_bits, + enum qf_hashmode hash, uint32_t seed, void* buffer, uint64_t + buffer_len) { uint64_t num_slots, xnslots, nblocks; uint64_t key_remainder_bits, bits_per_slot; uint64_t size; + uint64_t total_num_bytes; assert(popcnt(nslots) == 1); /* nslots must be a power of 2 */ num_slots = nslots; xnslots = nslots + 10*sqrt((double)nslots); - nblocks = (xnslots + SLOTS_PER_BLOCK - 1) / SLOTS_PER_BLOCK; + nblocks = (xnslots + QF_SLOTS_PER_BLOCK - 1) / QF_SLOTS_PER_BLOCK; key_remainder_bits = key_bits; while (nslots > 1) { assert(key_remainder_bits > 0); @@ -1780,52 +1658,26 @@ void qf_init(QF *qf, uint64_t nslots, uint64_t key_bits, uint64_t value_bits, } bits_per_slot = key_remainder_bits + value_bits; - assert (BITS_PER_SLOT == 0 || BITS_PER_SLOT == qf->metadata->bits_per_slot); + assert (QF_BITS_PER_SLOT == 0 || QF_BITS_PER_SLOT == qf->metadata->bits_per_slot); assert(bits_per_slot > 1); -#if BITS_PER_SLOT == 8 || BITS_PER_SLOT == 16 || BITS_PER_SLOT == 32 || BITS_PER_SLOT == 64 +#if QF_BITS_PER_SLOT == 8 || QF_BITS_PER_SLOT == 16 || QF_BITS_PER_SLOT == 32 || QF_BITS_PER_SLOT == 64 size = nblocks * sizeof(qfblock); #else - size = nblocks * (sizeof(qfblock) + SLOTS_PER_BLOCK * bits_per_slot / 8); + size = nblocks * (sizeof(qfblock) + QF_SLOTS_PER_BLOCK * bits_per_slot / 8); #endif - qf->mem = (qfmem *)calloc(sizeof(qfmem), 1); + total_num_bytes = sizeof(qfmetadata) + size; + if (buffer == NULL || total_num_bytes > buffer_len) + return total_num_bytes; - if (mem) { - qf->metadata = (qfmetadata *)calloc(sizeof(qfmetadata), 1); - if (qf->metadata == NULL) { - perror("Can't allocate qf metadata"); - exit(EXIT_FAILURE); - } - qf->blocks = (qfblock *)calloc(size, 1); - if (qf->blocks == NULL) { - perror("Can't allocate qf blocks"); - exit(EXIT_FAILURE); - } - } else { - int ret; - uint64_t mmap_size = size + sizeof(qfmetadata); - - qf->mem->fd = open(path, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU); - if (qf->mem->fd < 0) { - perror("Couldn't open file:\n"); - exit(EXIT_FAILURE); - } - ret = fallocate(qf->mem->fd, 0, 0, mmap_size); - if (ret < 0) { - perror("Couldn't fallocate file:\n"); - exit(EXIT_FAILURE); - } - qf->metadata = (qfmetadata *)mmap(NULL, mmap_size, PROT_READ | - PROT_WRITE, MAP_SHARED, qf->mem->fd, 0); - ret = madvise(qf->metadata, mmap_size, MADV_RANDOM); - if (ret < 0) { - perror("Couldn't fallocate file:\n"); - exit(EXIT_FAILURE); - } - qf->blocks = (qfblock *)(qf->metadata + 1); - } + memset(buffer, 0, total_num_bytes); + qf->metadata = (qfmetadata *)(buffer); + qf->blocks = (qfblock *)(qf->metadata + 1); - qf->metadata->size = size; + qf->metadata->magic_endian_number = MAGIC_NUMBER; + qf->metadata->auto_resize = 0; + qf->metadata->hash_mode = hash; + qf->metadata->total_size_in_bytes = size; qf->metadata->seed = seed; qf->metadata->nslots = num_slots; qf->metadata->xnslots = xnslots; @@ -1836,120 +1688,126 @@ void qf_init(QF *qf, uint64_t nslots, uint64_t key_bits, uint64_t value_bits, qf->metadata->range = qf->metadata->nslots; qf->metadata->range <<= qf->metadata->key_remainder_bits; - qf->metadata->nblocks = (qf->metadata->xnslots + SLOTS_PER_BLOCK - 1) / - SLOTS_PER_BLOCK; + qf->metadata->nblocks = (qf->metadata->xnslots + QF_SLOTS_PER_BLOCK - 1) / + QF_SLOTS_PER_BLOCK; qf->metadata->nelts = 0; qf->metadata->ndistinct_elts = 0; qf->metadata->noccupied_slots = 0; - qf->metadata->num_locks = (qf->metadata->xnslots/NUM_SLOTS_TO_LOCK)+2; + + qf->runtimedata->num_locks = (qf->metadata->xnslots/NUM_SLOTS_TO_LOCK)+2; + qf->runtimedata->f_info.filepath = NULL; /* initialize all the locks to 0 */ - qf->mem->metadata_lock = 0; - qf->mem->locks = (volatile int *)calloc(qf->metadata->num_locks, + qf->runtimedata->metadata_lock = 0; + qf->runtimedata->locks = (volatile int *)calloc(qf->runtimedata->num_locks, sizeof(volatile int)); + if (qf->runtimedata->locks == NULL) { + perror("Couldn't allocate memory for runtime locks."); + exit(EXIT_FAILURE); + } #ifdef LOG_WAIT_TIME - qf->mem->wait_times = (wait_time_data* )calloc(qf->metadata->num_locks+1, - sizeof(wait_time_data)); + qf->runtimedata->wait_times = (wait_time_data* + )calloc(qf->runtimedata->num_locks+1, + sizeof(wait_time_data)); + if (qf->runtimedata->wait_times == NULL) { + perror("Couldn't allocate memory for runtime wait_times."); + exit(EXIT_FAILURE); + } #endif -} -/* The caller should call qf_init on the dest QF before calling this function. - */ -void qf_copy(QF *dest, QF *src) -{ - DEBUG_CQF("%s\n","Source CQF"); - DEBUG_DUMP(src); - memcpy(dest->mem, src->mem, sizeof(qfmem)); - memcpy(dest->metadata, src->metadata, sizeof(qfmetadata)); - memcpy(dest->blocks, src->blocks, src->metadata->size); - DEBUG_CQF("%s\n","Destination CQF after copy."); - DEBUG_DUMP(dest); + return total_num_bytes; } -/* free up the memory if the QF is in memory. - * else unmap the mapped memory from pagecache. - * - * It does not delete the file on disk for on-disk QF. - */ -void qf_destroy(QF *qf, bool mem) +uint64_t qf_use(QF* qf, void* buffer, uint64_t buffer_len) { - assert(qf->blocks != NULL); - if (mem) { - free(qf->mem); - free(qf->metadata); - free(qf->blocks); - } else { - munmap(qf->metadata, qf->metadata->size + sizeof(qfmetadata)); - close(qf->mem->fd); + qf->metadata = (qfmetadata *)(buffer); + if (qf->metadata->total_size_in_bytes + sizeof(qfmetadata) > buffer_len) { + return qf->metadata->total_size_in_bytes + sizeof(qfmetadata); } -} + qf->blocks = (qfblock *)(qf->metadata + 1); -void qf_close(QF *qf) -{ - assert(qf->blocks != NULL); - munmap(qf->metadata, qf->metadata->size + sizeof(qfmetadata)); - close(qf->mem->fd); + qf->runtimedata = (qfruntime *)calloc(sizeof(qfruntime), 1); + if (qf->runtimedata == NULL) { + perror("Couldn't allocate memory for runtime data."); + exit(EXIT_FAILURE); + } + /* initialize all the locks to 0 */ + qf->runtimedata->metadata_lock = 0; + qf->runtimedata->locks = (volatile int *)calloc(qf->runtimedata->num_locks, + sizeof(volatile int)); + if (qf->runtimedata->locks == NULL) { + perror("Couldn't allocate memory for runtime locks."); + exit(EXIT_FAILURE); + } +#ifdef LOG_WAIT_TIME + qf->runtimedata->wait_times = (wait_time_data* + )calloc(qf->runtimedata->num_locks+1, + sizeof(wait_time_data)); + if (qf->runtimedata->wait_times == NULL) { + perror("Couldn't allocate memory for runtime wait_times."); + exit(EXIT_FAILURE); + } +#endif + + return sizeof(qfmetadata) + qf->metadata->total_size_in_bytes; } -/* - * Will read the on-disk QF using mmap. - * Data won't be copied in memory. - * - */ -void qf_read(QF *qf, const char *path) +void *qf_destroy(QF *qf) { - struct stat sb; - int ret; + assert(qf->runtimedata != NULL); + free(qf->runtimedata); - qf->mem = (qfmem *)calloc(sizeof(qfmem), 1); - qf->mem->fd = open(path, O_RDWR, S_IRWXU); - if (qf->mem->fd < 0) { - fprintf(stderr, "Couldn't open file: %s\n", path); - exit(EXIT_FAILURE); - } + return (void*)qf->metadata; +} - ret = fstat (qf->mem->fd, &sb); - if ( ret < 0) { - perror ("fstat"); - exit(EXIT_FAILURE); - } +bool qf_malloc(QF *qf, uint64_t nslots, uint64_t key_bits, uint64_t + value_bits, enum qf_hashmode hash, uint32_t seed) +{ + uint64_t total_num_bytes = qf_init(qf, nslots, key_bits, value_bits, + hash, seed, NULL, 0); - if (!S_ISREG (sb.st_mode)) { - fprintf (stderr, "%s is not a file.\n", path); + void *buffer = malloc(total_num_bytes); + if (buffer == NULL) { + perror("Couldn't allocate memory for the CQF."); exit(EXIT_FAILURE); } - qf->metadata = (qfmetadata *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, - qf->mem->fd, 0); - - //ret = madvise(qf->metadata, sb.st_size, MADV_SEQUENTIAL); - if (ret < 0) { - perror("Couldn't madvice of memory:\n"); + qf->runtimedata = (qfruntime *)calloc(sizeof(qfruntime), 1); + if (qf->runtimedata == NULL) { + perror("Couldn't allocate memory for runtime data."); exit(EXIT_FAILURE); } - /*DEBUG_CQF("Mmaped %ld bytes at addr %lx\n", sb.st_size, (uint64_t)qf->metadata);*/ + uint64_t init_size = qf_init(qf, nslots, key_bits, value_bits, hash, seed, + buffer, total_num_bytes); - qf->blocks = (qfblock *)(qf->metadata + 1); + if (init_size == total_num_bytes) + return true; + else + return false; } -void qf_drop_pages(const QF *qf, uint64_t start_idx, uint64_t end_idx) { - int ret; - qfblock *start_addr = get_block(qf, start_idx / SLOTS_PER_BLOCK); - uint64_t start_page = PAGE_ALIGN((uint64_t)start_addr); - qfblock *end_addr = get_block(qf, end_idx / SLOTS_PER_BLOCK); - uint64_t end_page = PAGE_ALIGN((uint64_t)end_addr); - uint64_t len = (end_page - start_page); - /*DEBUG_CQF("Droping %ld bytes starting from from %lx\n", len, start_page);*/ - ret = madvise((void*)start_page, len, MADV_DONTNEED); - if (ret < 0) { - perror("Couldn't madvice of memory:\n"); - exit(EXIT_FAILURE); +bool qf_free(QF *qf) +{ + assert(qf->metadata != NULL); + void *buffer = qf_destroy(qf); + if (buffer != NULL) { + free(buffer); + return true; } + + return false; } -const unsigned char *qf_get_addr(const QF *qf, uint64_t idx) { - return (unsigned char*)PAGE_ALIGN((uint64_t)get_block(qf, idx / SLOTS_PER_BLOCK)); +void qf_copy(QF *dest, const QF *src) +{ + DEBUG_CQF("%s\n","Source CQF"); + DEBUG_DUMP(src); + memcpy(dest->runtimedata, src->runtimedata, sizeof(qfruntime)); + memcpy(dest->metadata, src->metadata, sizeof(qfmetadata)); + memcpy(dest->blocks, src->blocks, src->metadata->total_size_in_bytes); + DEBUG_CQF("%s\n","Destination CQF after copy."); + DEBUG_DUMP(dest); } void qf_reset(QF *qf) @@ -1959,84 +1817,221 @@ void qf_reset(QF *qf) qf->metadata->noccupied_slots = 0; #ifdef LOG_WAIT_TIME - memset(qf->wait_times, 0, (qf->metadata->num_locks+1)*sizeof(wait_time_data)); + memset(qf->wait_times, 0, + (qf->runtimedata->num_locks+1)*sizeof(wait_time_data)); #endif -#if BITS_PER_SLOT == 8 || BITS_PER_SLOT == 16 || BITS_PER_SLOT == 32 || BITS_PER_SLOT == 64 +#if QF_BITS_PER_SLOT == 8 || QF_BITS_PER_SLOT == 16 || QF_BITS_PER_SLOT == 32 || QF_BITS_PER_SLOT == 64 memset(qf->blocks, 0, qf->metadata->nblocks* sizeof(qfblock)); #else - memset(qf->blocks, 0, qf->metadata->nblocks*(sizeof(qfblock) + SLOTS_PER_BLOCK * + memset(qf->blocks, 0, qf->metadata->nblocks*(sizeof(qfblock) + QF_SLOTS_PER_BLOCK * qf->metadata->bits_per_slot / 8)); #endif } -void qf_serialize(const QF *qf, const char *filename) +int64_t qf_resize_malloc(QF *qf, uint64_t nslots) { - FILE *fout; - fout = fopen(filename, "wb+"); - if (fout == NULL) { - perror("Error opening file for serializing\n"); - exit(EXIT_FAILURE); - } + QF new_qf; + if (!qf_malloc(&new_qf, nslots, qf->metadata->key_bits, + qf->metadata->value_bits, qf->metadata->hash_mode, + qf->metadata->seed)) + return false; + if (qf->metadata->auto_resize) + qf_set_auto_resize(&new_qf, true); - fwrite(qf->metadata, sizeof(qfmetadata), 1, fout); + // copy keys from qf into new_qf + QFi qfi; + qf_iterator_from_position(qf, &qfi, 0); + int64_t ret_numkeys = 0; + do { + uint64_t key, value, count; + qfi_get_hash(&qfi, &key, &value, &count); + qfi_next(&qfi); + int ret = qf_insert(&new_qf, key, value, count, QF_NO_LOCK | QF_KEY_IS_HASH); + if (ret < 0) { + fprintf(stderr, "Failed to insert key: %ld into the new CQF.\n", key); + return ret; + } + ret_numkeys++; + } while(!qfi_end(&qfi)); - /* we don't serialize the locks */ - fwrite(qf->blocks, qf->metadata->size, 1, fout); + qf_free(qf); + memcpy(qf, &new_qf, sizeof(QF)); - fclose(fout); + return ret_numkeys; } -void qf_deserialize(QF *qf, const char *filename) +uint64_t qf_resize(QF* qf, uint64_t nslots, void* buffer, uint64_t buffer_len) { - FILE *fin; - fin = fopen(filename, "rb"); - if (fin == NULL) { - perror("Error opening file for deserializing\n"); + QF new_qf; + new_qf.runtimedata = (qfruntime *)calloc(sizeof(qfruntime), 1); + if (new_qf.runtimedata == NULL) { + perror("Couldn't allocate memory for runtime data.\n"); exit(EXIT_FAILURE); } - qf->mem = (qfmem *)calloc(sizeof(qfmem), 1); - qf->metadata = (qfmetadata *)calloc(sizeof(qfmetadata), 1); + uint64_t init_size = qf_init(&new_qf, nslots, qf->metadata->key_bits, + qf->metadata->value_bits, + qf->metadata->hash_mode, qf->metadata->seed, + buffer, buffer_len); - fread(qf->metadata, sizeof(qfmetadata), 1, fin); + if (init_size > buffer_len) + return init_size; - /* initlialize the locks in the QF */ - qf->metadata->num_locks = (qf->metadata->xnslots/NUM_SLOTS_TO_LOCK)+2; - qf->mem->metadata_lock = 0; - /* initialize all the locks to 0 */ - qf->mem->locks = (volatile int *)calloc(qf->metadata->num_locks, sizeof(volatile int)); + if (qf->metadata->auto_resize) + qf_set_auto_resize(&new_qf, true); - qf->blocks = (qfblock *)calloc(qf->metadata->size, 1); - fread(qf->blocks, qf->metadata->size, 1, fin); + // copy keys from qf into new_qf + QFi qfi; + qf_iterator_from_position(qf, &qfi, 0); + do { + uint64_t key, value, count; + qfi_get_hash(&qfi, &key, &value, &count); + qfi_next(&qfi); + int ret = qf_insert(&new_qf, key, value, count, QF_NO_LOCK | QF_KEY_IS_HASH); + if (ret < 0) { + fprintf(stderr, "Failed to insert key: %ld into the new CQF.\n", key); + abort(); + } + } while(!qfi_end(&qfi)); + + qf_free(qf); + memcpy(qf, &new_qf, sizeof(QF)); + + return init_size; +} - fclose(fin); +void qf_set_auto_resize(QF* qf, bool enabled) +{ + if (enabled) + qf->metadata->auto_resize = 1; + else + qf->metadata->auto_resize = 0; } -bool qf_insert(QF *qf, uint64_t key, uint64_t value, uint64_t count, enum lock - flag) +int qf_insert(QF *qf, uint64_t key, uint64_t value, uint64_t count, uint8_t + flags) { + // We fill up the CQF up to 95% load factor. + // This is a very conservative check. + if (qf->metadata->noccupied_slots >= qf->metadata->nslots * 0.95) { + if (qf->metadata->auto_resize) { + fprintf(stdout, "Resizing the CQF.\n"); + qf_resize_malloc(qf, qf->metadata->nslots * 2); + } else + return QF_NO_SPACE; + } + if (count == 0) + return 0; + + if (GET_KEY_HASH(flags) != QF_KEY_IS_HASH) { + if (qf->metadata->hash_mode == QF_HASH_DEFAULT) + key = MurmurHash64A(((void *)&key), sizeof(key), + qf->metadata->seed) % qf->metadata->range; + else if (qf->metadata->hash_mode == QF_HASH_INVERTIBLE) + key = hash_64(key, BITMASK(qf->metadata->key_bits)); + } uint64_t hash = (key << qf->metadata->value_bits) | (value & BITMASK(qf->metadata->value_bits)); + int ret; if (count == 1) - return insert1(qf, hash, flag); + ret = insert1(qf, hash, flags); + else + ret = insert(qf, hash, count, flags); + + // check for fullness based on the distance from the home slot to the slot + // in which the key is inserted + if (ret == QF_NO_SPACE || ret > DISTANCE_FROM_HOME_SLOT_CUTOFF) { + float load_factor = qf->metadata->noccupied_slots / + (float)qf->metadata->nslots; + fprintf(stdout, "Load factor: %lf\n", load_factor); + if (qf->metadata->auto_resize) { + fprintf(stdout, "Resizing the CQF.\n"); + if (qf_resize_malloc(qf, qf->metadata->nslots * 2) > 0) { + if (ret == QF_NO_SPACE) { + if (count == 1) + ret = insert1(qf, hash, flags); + else + ret = insert(qf, hash, count, flags); + } + fprintf(stderr, "Resize finished.\n"); + } else { + fprintf(stderr, "Resize failed\n"); + ret = QF_NO_SPACE; + } + } else { + fprintf(stderr, "The CQF is filling up.\n"); + ret = QF_NO_SPACE; + } + } + return ret; +} + +int qf_set_count(QF *qf, uint64_t key, uint64_t value, uint64_t count, uint8_t + flags) +{ + if (count == 0) + return 0; + + uint64_t cur_count = qf_count_key_value(qf, key, value, flags); + int64_t delta = count - cur_count; + + int ret; + if (delta == 0) + ret = 0; + else if (delta > 0) + ret = qf_insert(qf, key, value, delta, flags); else - return insert(qf, hash, count, flag); + ret = qf_remove(qf, key, value, labs(delta), flags); + + return ret; } -/* count = 0 would remove the key completely. */ -void qf_remove(QF *qf, uint64_t key, uint64_t value, uint64_t count, enum lock - flag) +int qf_remove(QF *qf, uint64_t key, uint64_t value, uint64_t count, uint8_t + flags) { if (count == 0) - count = qf_count_key_value(qf, key, value); + return true; + if (GET_KEY_HASH(flags) != QF_KEY_IS_HASH) { + if (qf->metadata->hash_mode == QF_HASH_DEFAULT) + key = MurmurHash64A(((void *)&key), sizeof(key), + qf->metadata->seed) % qf->metadata->range; + else if (qf->metadata->hash_mode == QF_HASH_INVERTIBLE) + key = hash_64(key, BITMASK(qf->metadata->key_bits)); + } uint64_t hash = (key << qf->metadata->value_bits) | (value & BITMASK(qf->metadata->value_bits)); - _remove(qf, hash, count, flag); + return _remove(qf, hash, count, flags); } -uint64_t qf_count_key_value(const QF *qf, uint64_t key, uint64_t value) +int qf_delete_key_value(QF *qf, uint64_t key, uint64_t value, uint8_t flags) { + uint64_t count = qf_count_key_value(qf, key, value, flags); + if (count == 0) + return true; + + if (GET_KEY_HASH(flags) != QF_KEY_IS_HASH) { + if (qf->metadata->hash_mode == QF_HASH_DEFAULT) + key = MurmurHash64A(((void *)&key), sizeof(key), + qf->metadata->seed) % qf->metadata->range; + else if (qf->metadata->hash_mode == QF_HASH_INVERTIBLE) + key = hash_64(key, BITMASK(qf->metadata->key_bits)); + } + uint64_t hash = (key << qf->metadata->value_bits) | (value & + BITMASK(qf->metadata->value_bits)); + return _remove(qf, hash, count, flags); +} + +uint64_t qf_count_key_value(const QF *qf, uint64_t key, uint64_t value, + uint8_t flags) +{ + if (GET_KEY_HASH(flags) != QF_KEY_IS_HASH) { + if (qf->metadata->hash_mode == QF_HASH_DEFAULT) + key = MurmurHash64A(((void *)&key), sizeof(key), + qf->metadata->seed) % qf->metadata->range; + else if (qf->metadata->hash_mode == QF_HASH_INVERTIBLE) + key = hash_64(key, BITMASK(qf->metadata->key_bits)); + } uint64_t hash = (key << qf->metadata->value_bits) | (value & BITMASK(qf->metadata->value_bits)); uint64_t hash_remainder = hash & BITMASK(qf->metadata->bits_per_slot); @@ -2065,19 +2060,142 @@ uint64_t qf_count_key_value(const QF *qf, uint64_t key, uint64_t value) return 0; } +uint64_t qf_query(const QF *qf, uint64_t key, uint64_t *value, uint8_t flags) +{ + if (GET_KEY_HASH(flags) != QF_KEY_IS_HASH) { + if (qf->metadata->hash_mode == QF_HASH_DEFAULT) + key = MurmurHash64A(((void *)&key), sizeof(key), + qf->metadata->seed) % qf->metadata->range; + else if (qf->metadata->hash_mode == QF_HASH_INVERTIBLE) + key = hash_64(key, BITMASK(qf->metadata->key_bits)); + } + uint64_t hash = key; + uint64_t hash_remainder = hash & BITMASK(qf->metadata->key_remainder_bits); + int64_t hash_bucket_index = hash >> qf->metadata->key_remainder_bits; + + if (!is_occupied(qf, hash_bucket_index)) + return 0; + + int64_t runstart_index = hash_bucket_index == 0 ? 0 : run_end(qf, + hash_bucket_index-1) + + 1; + if (runstart_index < hash_bucket_index) + runstart_index = hash_bucket_index; + + /* printf("MC RUNSTART: %02lx RUNEND: %02lx\n", runstart_index, runend_index); */ + + uint64_t current_remainder, current_count, current_end; + do { + current_end = decode_counter(qf, runstart_index, ¤t_remainder, + ¤t_count); + *value = current_remainder & BITMASK(qf->metadata->value_bits); + current_remainder = current_remainder >> qf->metadata->value_bits; + if (current_remainder == hash_remainder) { + return current_count; + } + runstart_index = current_end + 1; + } while (!is_runend(qf, current_end)); + + return 0; +} + +int64_t qf_get_unique_index(const QF *qf, uint64_t key, uint64_t value, + uint8_t flags) +{ + if (GET_KEY_HASH(flags) != QF_KEY_IS_HASH) { + if (qf->metadata->hash_mode == QF_HASH_DEFAULT) + key = MurmurHash64A(((void *)&key), sizeof(key), + qf->metadata->seed) % qf->metadata->range; + else if (qf->metadata->hash_mode == QF_HASH_INVERTIBLE) + key = hash_64(key, BITMASK(qf->metadata->key_bits)); + } + uint64_t hash = (key << qf->metadata->value_bits) | (value & + BITMASK(qf->metadata->value_bits)); + uint64_t hash_remainder = hash & BITMASK(qf->metadata->bits_per_slot); + int64_t hash_bucket_index = hash >> qf->metadata->bits_per_slot; + + if (!is_occupied(qf, hash_bucket_index)) + return QF_DOESNT_EXIST; + + int64_t runstart_index = hash_bucket_index == 0 ? 0 : run_end(qf, + hash_bucket_index-1) + + 1; + if (runstart_index < hash_bucket_index) + runstart_index = hash_bucket_index; + + /* printf("MC RUNSTART: %02lx RUNEND: %02lx\n", runstart_index, runend_index); */ + + uint64_t current_remainder, current_count, current_end; + do { + current_end = decode_counter(qf, runstart_index, ¤t_remainder, + ¤t_count); + if (current_remainder == hash_remainder) + return runstart_index; + + runstart_index = current_end + 1; + } while (!is_runend(qf, current_end)); + + return QF_DOESNT_EXIST; +} + +enum qf_hashmode qf_get_hashmode(const QF *qf) { + return qf->metadata->hash_mode; +} +uint64_t qf_get_hash_seed(const QF *qf) { + return qf->metadata->seed; +} +__uint128_t qf_get_hash_range(const QF *qf) { + return qf->metadata->range; +} + +bool qf_is_auto_resize_enabled(const QF *qf) { + if (qf->metadata->auto_resize == 1) + return true; + return false; +} +uint64_t qf_get_total_size_in_bytes(const QF *qf) { + return qf->metadata->total_size_in_bytes; +} +uint64_t qf_get_nslots(const QF *qf) { + return qf->metadata->nslots; +} +uint64_t qf_get_num_occupied_slots(const QF *qf) { + return qf->metadata->noccupied_slots; +} + +uint64_t qf_get_num_key_bits(const QF *qf) { + return qf->metadata->key_bits; +} +uint64_t qf_get_num_value_bits(const QF *qf) { + return qf->metadata->value_bits; +} +uint64_t qf_get_num_key_remainder_bits(const QF *qf) { + return qf->metadata->key_remainder_bits; +} +uint64_t qf_get_bits_per_slot(const QF *qf) { + return qf->metadata->bits_per_slot; +} + +uint64_t qf_get_sum_of_counts(const QF *qf) { + return qf->metadata->nelts; +} +uint64_t qf_get_num_distinct_key_value_pairs(const QF *qf) { + return qf->metadata->ndistinct_elts; +} + /* initialize the iterator at the run corresponding * to the position index */ -bool qf_iterator(const QF *qf, QFi *qfi, uint64_t position) +int64_t qf_iterator_from_position(const QF *qf, QFi *qfi, uint64_t position) { if (position == 0xffffffffffffffff) { qfi->current = 0xffffffffffffffff; qfi->qf = qf; - return false; + return QFI_INVALID; } assert(position < qf->metadata->nslots); if (!is_occupied(qf, position)) { - uint64_t block_index = position / SLOTS_PER_BLOCK; + uint64_t block_index = position; uint64_t idx = bitselect(get_block(qf, block_index)->occupieds[0], 0); if (idx == 64) { while(idx == 64 && block_index < qf->metadata->nblocks) { @@ -2085,7 +2203,7 @@ bool qf_iterator(const QF *qf, QFi *qfi, uint64_t position) idx = bitselect(get_block(qf, block_index)->occupieds[0], 0); } } - position = block_index * SLOTS_PER_BLOCK + idx; + position = block_index * QF_SLOTS_PER_BLOCK + idx; } qfi->qf = qf; @@ -2095,38 +2213,44 @@ bool qf_iterator(const QF *qf, QFi *qfi, uint64_t position) if (qfi->current < position) qfi->current = position; - qfi->cache.nslots = qf->metadata->nslots; - qfi->cache.xnslots = qf->metadata->xnslots; - qfi->cache.sizeof_qfblock_SLOTS_PER_BLOCK_times_bits_per_slot_div_8 = - (sizeof(qfblock) + SLOTS_PER_BLOCK * qf->metadata->bits_per_slot / 8); - qfi->cache.bits_per_slot = qf->metadata->bits_per_slot; - qfi->cache.value_bits = qf->metadata->value_bits; - qfi->cache.key_remainder_bits = qf->metadata->key_remainder_bits; - qfi->cache.nblocks = qf->metadata->nblocks; - #ifdef LOG_CLUSTER_LENGTH qfi->c_info = (cluster_data* )calloc(qf->metadata->nslots/32, sizeof(cluster_data)); + if (qfi->c_info == NULL) { + perror("Couldn't allocate memory for c_info."); + exit(EXIT_FAILURE); + } qfi->cur_start_index = position; qfi->cur_length = 1; #endif if (qfi->current >= qf->metadata->nslots) - return false; - return true; + return QFI_INVALID; + return qfi->current; } -bool qf_iterator_hash(const QF *qf, QFi *qfi, uint64_t hash) +int64_t qf_iterator_key_value(const QF *qf, QFi *qfi, uint64_t key, uint64_t + value, uint8_t flags) { - if (hash >= qf->metadata->range) { + if (key >= qf->metadata->range) { qfi->current = 0xffffffffffffffff; qfi->qf = qf; - return false; + return QFI_INVALID; } qfi->qf = qf; qfi->num_clusters = 0; + if (GET_KEY_HASH(flags) != QF_KEY_IS_HASH) { + if (qf->metadata->hash_mode == QF_HASH_DEFAULT) + key = MurmurHash64A(((void *)&key), sizeof(key), + qf->metadata->seed) % qf->metadata->range; + else if (qf->metadata->hash_mode == QF_HASH_INVERTIBLE) + key = hash_64(key, BITMASK(qf->metadata->key_bits)); + } + uint64_t hash = (key << qf->metadata->value_bits) | (value & + BITMASK(qf->metadata->value_bits)); + uint64_t hash_remainder = hash & BITMASK(qf->metadata->bits_per_slot); uint64_t hash_bucket_index = hash >> qf->metadata->bits_per_slot; bool flag = false; @@ -2134,8 +2258,8 @@ bool qf_iterator_hash(const QF *qf, QFi *qfi, uint64_t hash) // If a run starts at "position" move the iterator to point it to the // smallest key greater than or equal to "hash". if (is_occupied(qf, hash_bucket_index)) { - int64_t runstart_index = hash_bucket_index == 0 ? 0 : run_end(qf, - hash_bucket_index-1) + uint64_t runstart_index = hash_bucket_index == 0 ? 0 : run_end(qf, + hash_bucket_index-1) + 1; if (runstart_index < hash_bucket_index) runstart_index = hash_bucket_index; @@ -2161,7 +2285,7 @@ bool qf_iterator_hash(const QF *qf, QFi *qfi, uint64_t hash) if (!is_occupied(qf, hash_bucket_index) || !flag) { uint64_t position = hash_bucket_index; assert(position < qf->metadata->nslots); - uint64_t block_index = position / SLOTS_PER_BLOCK; + uint64_t block_index = position / QF_SLOTS_PER_BLOCK; uint64_t idx = bitselect(get_block(qf, block_index)->occupieds[0], 0); if (idx == 64) { while(idx == 64 && block_index < qf->metadata->nblocks) { @@ -2169,7 +2293,7 @@ bool qf_iterator_hash(const QF *qf, QFi *qfi, uint64_t hash) idx = bitselect(get_block(qf, block_index)->occupieds[0], 0); } } - position = block_index * SLOTS_PER_BLOCK + idx; + position = block_index * QF_SLOTS_PER_BLOCK + idx; qfi->run = position; qfi->current = position == 0 ? 0 : run_end(qfi->qf, position-1) + 1; if (qfi->current < position) @@ -2177,77 +2301,93 @@ bool qf_iterator_hash(const QF *qf, QFi *qfi, uint64_t hash) } if (qfi->current >= qf->metadata->nslots) - return false; - return true; + return QFI_INVALID; + return qfi->current; } -int qfi_get(const QFi *qfi, uint64_t *key, uint64_t *value, uint64_t *count) +static int qfi_get(const QFi *qfi, uint64_t *key, uint64_t *value, uint64_t + *count) { - assert(qfi->current < qfi->qf->metadata->nslots); + if (qfi_end(qfi)) + return QFI_INVALID; uint64_t current_remainder, current_count; - qfi_decode_counter(qfi, qfi->current, ¤t_remainder, ¤t_count); + decode_counter(qfi->qf, qfi->current, ¤t_remainder, ¤t_count); - *value = current_remainder & BITMASK(qfi->cache.value_bits); - current_remainder = current_remainder >> qfi->cache.value_bits; - *key = (qfi->run << qfi->cache.key_remainder_bits) | current_remainder; - *count = current_count; + *value = current_remainder & BITMASK(qfi->qf->metadata->value_bits); + current_remainder = current_remainder >> qfi->qf->metadata->value_bits; + *key = (qfi->run << qfi->qf->metadata->key_remainder_bits) | current_remainder; + *count = current_count; - /*qfi->current = end_index;*/ //get should not change the current index - //of the iterator return 0; } -int qfi_next(QFi *qfi) { - return qfi_nextx(qfi, NULL); +int qfi_get_key(const QFi *qfi, uint64_t *key, uint64_t *value, uint64_t + *count) +{ + *key = *value = *count = 0; + int ret = qfi_get(qfi, key, value, count); + if (ret == 0) { + if (qfi->qf->metadata->hash_mode == QF_HASH_DEFAULT) { + *key = 0; *value = 0; *count = 0; + return QF_INVALID; + } else if (qfi->qf->metadata->hash_mode == QF_HASH_INVERTIBLE) + *key = hash_64i(*key, BITMASK(qfi->qf->metadata->key_bits)); + } + + return ret; } -int qfi_nextx(QFi *qfi, uint64_t* read_offset) + +int qfi_get_hash(const QFi *qfi, uint64_t *key, uint64_t *value, uint64_t + *count) { - uint64_t block_index = qfi->run / SLOTS_PER_BLOCK; - qfblock* addr = qfi_get_block(qfi, block_index); - if (read_offset) *read_offset = (char*)addr - (char*)(qfi->qf->metadata); + *key = *value = *count = 0; + return qfi_get(qfi, key, value, count); +} +int qfi_next(QFi *qfi) +{ if (qfi_end(qfi)) - return 1; + return QFI_INVALID; else { /* move to the end of the current counter*/ uint64_t current_remainder, current_count; - qfi->current = qfi_decode_counter(qfi, qfi->current, ¤t_remainder, + qfi->current = decode_counter(qfi->qf, qfi->current, ¤t_remainder, ¤t_count); - - if (!qfi_is_runend(qfi, qfi->current)) { + + if (!is_runend(qfi->qf, qfi->current)) { qfi->current++; #ifdef LOG_CLUSTER_LENGTH qfi->cur_length++; #endif - if (qfi->current > qfi->cache.nslots) - return 1; + if (qfi_end(qfi)) + return QFI_INVALID; return 0; - } - else { + } else { #ifdef LOG_CLUSTER_LENGTH /* save to check if the new current is the new cluster. */ uint64_t old_current = qfi->current; #endif - uint64_t rank = bitrank(addr->occupieds[0], - qfi->run % SLOTS_PER_BLOCK); - uint64_t next_run = bitselect(qfi_get_block(qfi, + uint64_t block_index = qfi->run / QF_SLOTS_PER_BLOCK; + uint64_t rank = bitrank(get_block(qfi->qf, block_index)->occupieds[0], + qfi->run % QF_SLOTS_PER_BLOCK); + uint64_t next_run = bitselect(get_block(qfi->qf, block_index)->occupieds[0], rank); if (next_run == 64) { rank = 0; - while (next_run == 64 && block_index < qfi->cache.nblocks) { + while (next_run == 64 && block_index < qfi->qf->metadata->nblocks) { block_index++; - next_run = bitselect(qfi_get_block(qfi, block_index)->occupieds[0], + next_run = bitselect(get_block(qfi->qf, block_index)->occupieds[0], rank); } } - if (block_index == qfi->cache.nblocks) { + if (block_index == qfi->qf->metadata->nblocks) { /* set the index values to max. */ - qfi->run = qfi->current = qfi->cache.xnslots; - return 1; + qfi->run = qfi->current = qfi->qf->metadata->xnslots; + return QFI_INVALID; } - qfi->run = block_index * SLOTS_PER_BLOCK + next_run; + qfi->run = block_index * QF_SLOTS_PER_BLOCK + next_run; qfi->current++; if (qfi->current < qfi->run) qfi->current = qfi->run; @@ -2269,12 +2409,11 @@ int qfi_nextx(QFi *qfi, uint64_t* read_offset) } } -inline int qfi_end(const QFi *qfi) +bool qfi_end(const QFi *qfi) { - if (qfi->current >= qfi->cache.xnslots /*&& is_runend(qfi->qf, qfi->current)*/) - return 1; - else - return 0; + if (qfi->current >= qfi->qf->metadata->xnslots /*&& is_runend(qfi->qf, qfi->current)*/) + return true; + return false; } /* @@ -2288,55 +2427,66 @@ inline int qfi_end(const QFi *qfi) * insert(min, ic) * increment either ia or ib, whichever is minimum. */ -void qf_merge(QF *qfa, QF *qfb, QF *qfc, enum lock flag) +void qf_merge(const QF *qfa, const QF *qfb, QF *qfc) { QFi qfia, qfib; - qf_iterator(qfa, &qfia, 0); - qf_iterator(qfb, &qfib, 0); + qf_iterator_from_position(qfa, &qfia, 0); + qf_iterator_from_position(qfb, &qfib, 0); + + if (qfa->metadata->hash_mode != qfc->metadata->hash_mode && + qfa->metadata->seed != qfc->metadata->seed && + qfb->metadata->hash_mode != qfc->metadata->hash_mode && + qfb->metadata->seed != qfc->metadata->seed) { + fprintf(stderr, "Output QF and input QFs do not have the same hash mode or seed.\n"); + exit(1); + } uint64_t keya, valuea, counta, keyb, valueb, countb; - qfi_get(&qfia, &keya, &valuea, &counta); - qfi_get(&qfib, &keyb, &valueb, &countb); + qfi_get_hash(&qfia, &keya, &valuea, &counta); + qfi_get_hash(&qfib, &keyb, &valueb, &countb); do { if (keya < keyb) { - qf_insert(qfc, keya, valuea, counta, flag); + qf_insert(qfc, keya, valuea, counta, QF_NO_LOCK | QF_KEY_IS_HASH); qfi_next(&qfia); - qfi_get(&qfia, &keya, &valuea, &counta); + qfi_get_hash(&qfia, &keya, &valuea, &counta); } else { - qf_insert(qfc, keyb, valueb, countb, flag); + qf_insert(qfc, keyb, valueb, countb, QF_NO_LOCK | QF_KEY_IS_HASH); qfi_next(&qfib); - qfi_get(&qfib, &keyb, &valueb, &countb); + qfi_get_hash(&qfib, &keyb, &valueb, &countb); } } while(!qfi_end(&qfia) && !qfi_end(&qfib)); if (!qfi_end(&qfia)) { do { - qfi_get(&qfia, &keya, &valuea, &counta); - qf_insert(qfc, keya, valuea, counta, flag); + qfi_get_hash(&qfia, &keya, &valuea, &counta); + qf_insert(qfc, keya, valuea, counta, QF_NO_LOCK | QF_KEY_IS_HASH); } while(!qfi_next(&qfia)); } if (!qfi_end(&qfib)) { do { - qfi_get(&qfib, &keyb, &valueb, &countb); - qf_insert(qfc, keyb, valueb, countb, flag); + qfi_get_hash(&qfib, &keyb, &valueb, &countb); + qf_insert(qfc, keyb, valueb, countb, QF_NO_LOCK | QF_KEY_IS_HASH); } while(!qfi_next(&qfib)); } - - return; } /* * Merge an array of qfs into the resultant QF */ -void qf_multi_merge(QF *qf_arr[], int nqf, QF *qfr, enum lock flag) +void qf_multi_merge(const QF *qf_arr[], int nqf, QF *qfr) { int i; QFi qfi_arr[nqf]; int smallest_idx = 0; uint64_t smallest_key = UINT64_MAX; for (i=0; imetadata->hash_mode != qfr->metadata->hash_mode && + qf_arr[i]->metadata->seed != qfr->metadata->seed) { + fprintf(stderr, "Output QF and input QFs do not have the same hash mode or seed.\n"); + exit(1); + } + qf_iterator_from_position(qf_arr[i], &qfi_arr[i], 0); } DEBUG_CQF("Merging %d CQFs\n", nqf); @@ -2350,7 +2500,7 @@ void qf_multi_merge(QF *qf_arr[], int nqf, QF *qfr, enum lock flag) uint64_t values[nqf]; uint64_t counts[nqf]; for (i=0; imetadata->hash_mode != qfb->metadata->hash_mode && + qfa->metadata->seed != qfb->metadata->seed) { + fprintf(stderr, "Input QFs do not have the same hash mode or seed.\n"); + exit(1); + } // create the iterator on the larger QF. - if (qfa->metadata->size > qfb->metadata->size) { + if (qfa->metadata->total_size_in_bytes > qfb->metadata->total_size_in_bytes) + { qf_mem = qfb; qf_disk = qfa; } else { @@ -2406,12 +2564,12 @@ uint64_t qf_inner_product(QF *qfa, QF *qfb) qf_disk = qfb; } - qf_iterator(qf_disk, &qfi, 0); + qf_iterator_from_position(qf_disk, &qfi, 0); do { uint64_t key = 0, value = 0, count = 0; uint64_t count_mem; - qfi_get(&qfi, &key, &value, &count); - if ((count_mem = qf_count_key_value(qf_mem, key, 0)) > 0) { + qfi_get_hash(&qfi, &key, &value, &count); + if ((count_mem = qf_count_key_value(qf_mem, key, 0, QF_KEY_IS_HASH)) > 0) { acc += count*count_mem; } } while (!qfi_next(&qfi)); @@ -2420,13 +2578,22 @@ uint64_t qf_inner_product(QF *qfa, QF *qfb) } /* find cosine similarity between two QFs. */ -void qf_intersect(QF *qfa, QF *qfb, QF *qfr) +void qf_intersect(const QF *qfa, const QF *qfb, QF *qfr) { QFi qfi; - QF *qf_mem, *qf_disk; + const QF *qf_mem, *qf_disk; + + if (qfa->metadata->hash_mode != qfr->metadata->hash_mode && + qfa->metadata->seed != qfr->metadata->seed && + qfb->metadata->hash_mode != qfr->metadata->hash_mode && + qfb->metadata->seed != qfr->metadata->seed) { + fprintf(stderr, "Output QF and input QFs do not have the same hash mode or seed.\n"); + exit(1); + } // create the iterator on the larger QF. - if (qfa->metadata->size > qfb->metadata->size) { + if (qfa->metadata->total_size_in_bytes > qfb->metadata->total_size_in_bytes) + { qf_mem = qfb; qf_disk = qfa; } else { @@ -2434,17 +2601,17 @@ void qf_intersect(QF *qfa, QF *qfb, QF *qfr) qf_disk = qfb; } - qf_iterator(qf_disk, &qfi, 0); + qf_iterator_from_position(qf_disk, &qfi, 0); do { uint64_t key = 0, value = 0, count = 0; - qfi_get(&qfi, &key, &value, &count); - if (qf_count_key_value(qf_mem, key, 0) > 0) - qf_insert(qfr, key, value, count, NO_LOCK); + qfi_get_hash(&qfi, &key, &value, &count); + if (qf_count_key_value(qf_mem, key, 0, QF_KEY_IS_HASH) > 0) + qf_insert(qfr, key, value, count, QF_NO_LOCK | QF_KEY_IS_HASH); } while (!qfi_next(&qfi)); } /* magnitude of a QF. */ -uint64_t qf_magnitude(QF *qf) +uint64_t qf_magnitude(const QF *qf) { return sqrt(qf_inner_product(qf, qf)); } diff --git a/src/gqf/gqf_file.c b/src/gqf/gqf_file.c new file mode 100644 index 0000000..2d6ad3b --- /dev/null +++ b/src/gqf/gqf_file.c @@ -0,0 +1,254 @@ +/* + * ============================================================================ + * + * Authors: Prashant Pandey + * Rob Johnson + * + * ============================================================================ + */ + +#include +#if 0 +# include +#else +# define assert(x) +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gqf/hashutil.h" +#include "gqf/gqf.h" +#include "gqf/gqf_int.h" +#include "gqf/gqf_file.h" + +#define NUM_SLOTS_TO_LOCK (1ULL<<16) + +bool qf_initfile(QF *qf, uint64_t nslots, uint64_t key_bits, uint64_t + value_bits, enum qf_hashmode hash, uint32_t seed, char* + filename, int prot) +{ + uint64_t total_num_bytes = qf_init(qf, nslots, key_bits, value_bits, hash, + seed, NULL, 0); + + int ret; + qf->runtimedata = (qfruntime *)calloc(sizeof(qfruntime), 1); + if (qf->runtimedata == NULL) { + perror("Couldn't allocate memory for runtime data."); + exit(EXIT_FAILURE); + } + qf->runtimedata->f_info.fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU); + if (qf->runtimedata->f_info.fd < 0) { + perror("Couldn't open file."); + exit(EXIT_FAILURE); + } + ret = posix_fallocate(qf->runtimedata->f_info.fd, 0, total_num_bytes); + if (ret < 0) { + perror("Couldn't fallocate file:\n"); + exit(EXIT_FAILURE); + } + qf->metadata = (qfmetadata *)mmap(NULL, total_num_bytes, prot, MAP_SHARED, + qf->runtimedata->f_info.fd, 0); + if (qf->metadata == MAP_FAILED) { + perror("Couldn't mmap metadata."); + exit(EXIT_FAILURE); + } + ret = madvise(qf->metadata, total_num_bytes, MADV_RANDOM); + if (ret < 0) { + perror("Couldn't fallocate file."); + exit(EXIT_FAILURE); + } + qf->blocks = (qfblock *)(qf->metadata + 1); + + uint64_t init_size = qf_init(qf, nslots, key_bits, value_bits, hash, seed, + qf->metadata, total_num_bytes); + qf->runtimedata->f_info.filepath = (char *)malloc(strlen(filename)); + if (qf->runtimedata->f_info.filepath == NULL) { + perror("Couldn't allocate memory for runtime f_info filepath."); + exit(EXIT_FAILURE); + } + strcpy(qf->runtimedata->f_info.filepath, filename); + + if (init_size == total_num_bytes) + return true; + else + return false; +} + +uint64_t qf_usefile(QF* qf, const char* filename, int prot) +{ + struct stat sb; + int ret; + + qf->runtimedata = (qfruntime *)calloc(sizeof(qfruntime), 1); + if (qf->runtimedata == NULL) { + perror("Couldn't allocate memory for runtime data."); + exit(EXIT_FAILURE); + } + qf->runtimedata->f_info.fd = open(filename, O_RDWR, S_IRWXU); + if (qf->runtimedata->f_info.fd < 0) { + perror("Couldn't open file."); + exit(EXIT_FAILURE); + } + + ret = fstat (qf->runtimedata->f_info.fd, &sb); + if ( ret < 0) { + perror ("fstat"); + exit(EXIT_FAILURE); + } + + if (!S_ISREG (sb.st_mode)) { + fprintf (stderr, "%s is not a file.\n", filename); + exit(EXIT_FAILURE); + } + + qf->runtimedata->f_info.filepath = (char *)malloc(strlen(filename)); + if (qf->runtimedata->f_info.filepath == NULL) { + perror("Couldn't allocate memory for runtime f_info filepath."); + exit(EXIT_FAILURE); + } + strcpy(qf->runtimedata->f_info.filepath, filename); + /* initialize all the locks to 0 */ + qf->runtimedata->metadata_lock = 0; + qf->runtimedata->locks = (volatile int *)calloc(qf->runtimedata->num_locks, + sizeof(volatile int)); + if (qf->runtimedata->locks == NULL) { + perror("Couldn't allocate memory for runtime locks."); + exit(EXIT_FAILURE); + } +#ifdef LOG_WAIT_TIME + qf->runtimedata->wait_times = (wait_time_data* )calloc(qf->runtimedata->num_locks+1, + sizeof(wait_time_data)); + if (qf->runtimedata->wait_times == NULL) { + perror("Couldn't allocate memory for runtime wait_times."); + exit(EXIT_FAILURE); + } +#endif + qf->metadata = (qfmetadata *)mmap(NULL, sb.st_size, prot, MAP_SHARED, + qf->runtimedata->f_info.fd, 0); + if (qf->metadata == MAP_FAILED) { + perror("Couldn't mmap metadata."); + exit(EXIT_FAILURE); + } + if (qf->metadata->magic_endian_number != MAGIC_NUMBER) { + fprintf(stderr, "Can't read the CQF. It was written on a different endian machine."); + exit(EXIT_FAILURE); + } + qf->blocks = (qfblock *)(qf->metadata + 1); + + return sizeof(qfmetadata) + qf->metadata->total_size_in_bytes; +} + +bool qf_closefile(QF* qf) +{ + assert(qf->metadata != NULL); + int fd = qf->runtimedata->f_info.fd; + uint64_t size = qf->metadata->total_size_in_bytes + sizeof(qfmetadata); + void *buffer = qf_destroy(qf); + if (buffer != NULL) { + munmap(buffer, size); + close(fd); + return true; + } + + return false; +} + +bool qf_deletefile(QF* qf) +{ + assert(qf->metadata != NULL); + char *path = (char *)malloc(strlen(qf->runtimedata->f_info.filepath)); + if (qf->runtimedata->f_info.filepath == NULL) { + perror("Couldn't allocate memory for runtime f_info filepath."); + exit(EXIT_FAILURE); + } + strcpy(path, qf->runtimedata->f_info.filepath); + if (qf_closefile(qf)) { + remove(path); + return true; + } + + return false; +} + +uint64_t qf_serialize(const QF *qf, const char *filename) +{ + FILE *fout; + fout = fopen(filename, "wb+"); + if (fout == NULL) { + perror("Error opening file for serializing."); + exit(EXIT_FAILURE); + } + fwrite(qf->metadata, sizeof(qfmetadata), 1, fout); + fwrite(qf->blocks, qf->metadata->total_size_in_bytes, 1, fout); + fclose(fout); + + return sizeof(qfmetadata) + qf->metadata->total_size_in_bytes; +} + +uint64_t qf_deserialize(QF *qf, const char *filename) +{ + FILE *fin; + fin = fopen(filename, "rb"); + if (fin == NULL) { + perror("Error opening file for deserializing."); + exit(EXIT_FAILURE); + } + + qf->runtimedata = (qfruntime *)calloc(sizeof(qfruntime), 1); + if (qf->runtimedata == NULL) { + perror("Couldn't allocate memory for runtime data."); + exit(EXIT_FAILURE); + } + qf->metadata = (qfmetadata *)calloc(sizeof(qfmetadata), 1); + if (qf->metadata == NULL) { + perror("Couldn't allocate memory for metadata."); + exit(EXIT_FAILURE); + } + int ret = fread(qf->metadata, sizeof(qfmetadata), 1, fin); + if (ret < 1) { + perror("Couldn't read metadata from file."); + exit(EXIT_FAILURE); + } + if (qf->metadata->magic_endian_number != MAGIC_NUMBER) { + fprintf(stderr, "Can't read the CQF. It was written on a different endian machine."); + exit(EXIT_FAILURE); + } + + qf->runtimedata->f_info.filepath = (char *)malloc(strlen(filename)); + if (qf->runtimedata->f_info.filepath == NULL) { + perror("Couldn't allocate memory for runtime f_info filepath."); + exit(EXIT_FAILURE); + } + strcpy(qf->runtimedata->f_info.filepath, filename); + /* initlialize the locks in the QF */ + qf->runtimedata->num_locks = (qf->metadata->xnslots/NUM_SLOTS_TO_LOCK)+2; + qf->runtimedata->metadata_lock = 0; + /* initialize all the locks to 0 */ + qf->runtimedata->locks = (volatile int *)calloc(qf->runtimedata->num_locks, + sizeof(volatile int)); + if (qf->runtimedata->locks == NULL) { + perror("Couldn't allocate memory for runtime locks."); + exit(EXIT_FAILURE); + } + qf->blocks = (qfblock *)calloc(qf->metadata->total_size_in_bytes, 1); + if (qf->blocks == NULL) { + perror("Couldn't allocate memory for blocks."); + exit(EXIT_FAILURE); + } + ret = fread(qf->blocks, qf->metadata->total_size_in_bytes, 1, fin); + if (ret < 1) { + perror("Couldn't read metadata from file."); + exit(EXIT_FAILURE); + } + fclose(fin); + + return sizeof(qfmetadata) + qf->metadata->total_size_in_bytes; +} + diff --git a/src/hashutil.cc b/src/gqf/hashutil.c similarity index 89% rename from src/hashutil.cc rename to src/gqf/hashutil.c index 257b992..4fc4be5 100644 --- a/src/hashutil.cc +++ b/src/gqf/hashutil.c @@ -1,6 +1,6 @@ /* -*- Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil -*- */ // Pulled from lookup3.c by Bob Jenkins -#include "hashutil.h" +#include "gqf/hashutil.h" //----------------------------------------------------------------------------- // MurmurHash2, by Austin Appleby @@ -59,15 +59,10 @@ uint32_t MurmurHash(const void* buf, size_t len, uint32_t seed) return h; } -uint32_t MurmurHash(const std::string &s, uint32_t seed) -{ - return MurmurHash(s.data(), s.length(), seed); -} - // Thomas Wang's integer hash functions. See // for a snapshot. -uint64_t HashUtil::hash_64(uint64_t key, uint64_t mask) +uint64_t hash_64(uint64_t key, uint64_t mask) { key = (~key + (key << 21)) & mask; // key = (key << 21) - key - 1; key = key ^ key >> 24; @@ -81,7 +76,7 @@ uint64_t HashUtil::hash_64(uint64_t key, uint64_t mask) // The inversion of hash_64(). Modified from // -uint64_t HashUtil::hash_64i(uint64_t key, uint64_t mask) +uint64_t hash_64i(uint64_t key, uint64_t mask) { uint64_t tmp; @@ -118,7 +113,7 @@ uint64_t HashUtil::hash_64i(uint64_t key, uint64_t mask) return key; } -__uint128_t HashUtil::MurmurHash128A ( const void * key, int len, +__uint128_t MurmurHash128A ( const void * key, int len, unsigned int seed1, unsigned int seed2 ) { __uint128_t ret_hash; @@ -138,7 +133,7 @@ __uint128_t HashUtil::MurmurHash128A ( const void * key, int len, // 64-bit hash for 64-bit platforms -uint64_t HashUtil::MurmurHash64A ( const void * key, int len, unsigned int seed ) +uint64_t MurmurHash64A ( const void * key, int len, unsigned int seed ) { const uint64_t m = 0xc6a4a7935bd1e995; const int r = 47; @@ -164,13 +159,13 @@ uint64_t HashUtil::MurmurHash64A ( const void * key, int len, unsigned int seed switch(len & 7) { - case 7: h ^= uint64_t(data2[6]) << 48; - case 6: h ^= uint64_t(data2[5]) << 40; - case 5: h ^= uint64_t(data2[4]) << 32; - case 4: h ^= uint64_t(data2[3]) << 24; - case 3: h ^= uint64_t(data2[2]) << 16; - case 2: h ^= uint64_t(data2[1]) << 8; - case 1: h ^= uint64_t(data2[0]); + case 7: h ^= (uint64_t)data2[6] << 48; + case 6: h ^= (uint64_t)data2[5] << 40; + case 5: h ^= (uint64_t)data2[4] << 32; + case 4: h ^= (uint64_t)data2[3] << 24; + case 3: h ^= (uint64_t)data2[2] << 16; + case 2: h ^= (uint64_t)data2[1] << 8; + case 1: h ^= (uint64_t)data2[0]; h *= m; }; @@ -184,7 +179,7 @@ uint64_t HashUtil::MurmurHash64A ( const void * key, int len, unsigned int seed // 64-bit hash for 32-bit platforms -uint64_t HashUtil::MurmurHash64B ( const void * key, int len, unsigned int seed ) +uint64_t MurmurHash64B ( const void * key, int len, unsigned int seed ) { const unsigned int m = 0x5bd1e995; const int r = 24; @@ -235,7 +230,7 @@ uint64_t HashUtil::MurmurHash64B ( const void * key, int len, unsigned int seed return h; } -uint64_t HashUtil::AES_HASH(uint64_t x) +uint64_t AES_HASH(uint64_t x) { const uint64_t round_keys[32] = { // These were generated by hashing some randomly chosen files on my laptop diff --git a/src/kmer.cc b/src/kmer.cc index 07974a8..d65a7d1 100644 --- a/src/kmer.cc +++ b/src/kmer.cc @@ -66,11 +66,11 @@ string int_to_str(uint64_t kmer, uint64_t kmer_size) inline int Kmer::reverse_complement_base(int x) { return 3 - x; } /* Calculate the revsese complement of a kmer */ -uint64_t Kmer::reverse_complement(uint64_t kmer, uint64_t kmer_size) +__int128_t Kmer::reverse_complement(__int128_t kmer, uint64_t kmer_size) { - uint64_t rc = 0; + __int128_t rc = 0; uint8_t base = 0; - for (int i=0; i>= 2; @@ -85,62 +85,12 @@ uint64_t Kmer::reverse_complement(uint64_t kmer, uint64_t kmer_size) * Return true if the kmer is greater than or equal to its * reverse complement. * */ -inline bool Kmer::compare_kmers(uint64_t kmer, uint64_t kmer_rev) +bool Kmer::compare_kmers(__int128_t kmer, __int128_t kmer_rev) { return kmer >= kmer_rev; } -/* This code is taken from Jellyfish 2.0 - * git@github.com:gmarcais/Jellyfish.git - * */ - -// Checkered mask. cmask is every other bit on -// (0x55). cmask is two bits one, two bits off (0x33). Etc. -template -struct cmask { - static const U v = - (cmask::v << (2 * len)) | (((U)1 << len) - 1); -}; -template -struct cmask { - static const U v = 0; -}; - -// Fast reverse complement of one word through bit tweedling. -inline uint32_t Kmer::word_reverse_complement(uint32_t w) { - typedef uint64_t U; - w = ((w >> 2) & cmask::v) | ((w & cmask::v) << 2); - w = ((w >> 4) & cmask::v) | ((w & cmask::v) << 4); - w = ((w >> 8) & cmask::v) | ((w & cmask::v) << 8); - w = ( w >> 16 ) | ( w << 16); - return ((U)-1) - w; -} - -inline int64_t Kmer::word_reverse_complement(uint64_t w) { - typedef uint64_t U; - w = ((w >> 2) & cmask::v) | ((w & cmask::v) << 2); - w = ((w >> 4) & cmask::v) | ((w & cmask::v) << 4); - w = ((w >> 8) & cmask::v) | ((w & cmask::v) << 8); - w = ((w >> 16) & cmask::v) | ((w & cmask::v) << 16); - w = ( w >> 32 ) | ( w << 32); - return ((U)-1) - w; -} - -#ifdef HAVE_INT128 -inline static unsigned __int128 Kmer::word_reverse_complement(unsigned __int128 w) { - typedef unsigned __int128 U; - w = ((w >> 2) & cmask::v) | ((w & cmask::v) << 2); - w = ((w >> 4) & cmask::v) | ((w & cmask::v) << 4); - w = ((w >> 8) & cmask::v) | ((w & cmask::v) << 8); - w = ((w >> 16) & cmask::v) | ((w & cmask::v) << 16); - w = ((w >> 32) & cmask::v) | ((w & cmask::v) << 32); - w = ( w >> 64 ) | ( w << 64); - return ((U)-1) - w; -} -#endif - -mantis::QuerySets Kmer::parse_kmers(const char *filename, uint32_t seed, - uint64_t range, uint64_t kmer_size, +mantis::QuerySets Kmer::parse_kmers(const char *filename, uint64_t kmer_size, uint64_t& total_kmers) { mantis::QuerySets multi_kmers; total_kmers = 0; @@ -179,10 +129,7 @@ mantis::QuerySets Kmer::parse_kmers(const char *filename, uint32_t seed, else item = first_rev; - // hash the kmer using murmurhash/xxHash before adding to the list - //item = HashUtil::MurmurHash64A(((void*)&item), sizeof(item), seed); - item = HashUtil::hash_64(item, BITMASK(2*kmer_size)); - kmers_set.insert(item % range); + kmers_set.insert(item); uint64_t next = (first << 2) & BITMASK(2*kmer_size); uint64_t next_rev = first_rev >> 2; @@ -206,10 +153,7 @@ mantis::QuerySets Kmer::parse_kmers(const char *filename, uint32_t seed, else item = next_rev; - // hash the kmer using murmurhash/xxHash before adding to the list - //item = HashUtil::MurmurHash64A(((void*)&item), sizeof(item), seed); - item = HashUtil::hash_64(item, BITMASK(2*kmer_size)); - kmers_set.insert(item % range); + kmers_set.insert(item); next = (next << 2) & BITMASK(2*kmer_size); next_rev = next_rev >> 2; diff --git a/src/query.cc b/src/query.cc index f90ff5b..ce0d790 100644 --- a/src/query.cc +++ b/src/query.cc @@ -1,15 +1,6 @@ /* * ============================================================================ * - * Filename: query.cc - * - * Description: - * - * Version: 1.0 - * Created: 2017-10-27 12:56:50 AM - * Revision: none - * Compiler: gcc - * * Author: Prashant Pandey (), ppandey@cs.stonybrook.edu * Organization: Stony Brook University * @@ -150,7 +141,7 @@ int query_main (QueryOpts& opt) sample_file); uint64_t kmer_size = cdbg.get_cqf()->keybits() / 2; console->info("Read colored dbg with {} k-mers and {} color classes", - cdbg.get_cqf()->size(), cdbg.get_num_bitvectors()); + cdbg.get_cqf()->dist_elts(), cdbg.get_num_bitvectors()); //cdbg.get_cqf()->dump_metadata(); //CQF cqf(query_file, false); @@ -169,8 +160,6 @@ int query_main (QueryOpts& opt) uint32_t seed = 2038074743; uint64_t total_kmers = 0; mantis::QuerySets multi_kmers = Kmer::parse_kmers(query_file.c_str(), - seed, - cdbg.range(), kmer_size, total_kmers); console->info("Total k-mers to query: {}", total_kmers); diff --git a/src/validatemantis.cc b/src/validatemantis.cc index dc47321..de64a04 100644 --- a/src/validatemantis.cc +++ b/src/validatemantis.cc @@ -1,15 +1,6 @@ /* * ===================================================================================== * - * Filename: validate_mantis.cc - * - * Description: - * - * Version: 1.0 - * Created: 2017-10-31 12:13:49 PM - * Revision: none - * Compiler: gcc - * * Author: Prashant Pandey (), ppandey@cs.stonybrook.edu * Organization: Stony Brook University * @@ -90,7 +81,7 @@ validate_main ( ValidateOpts& opt ) console->error("Squeakr file {} does not exist.", cqf_file); exit(1); } - cqfs[nqf] = CQF(cqf_file, false); + cqfs[nqf] = CQF(cqf_file, CQF_FREAD); std::string sample_id = first_part(first_part(last_part(cqf_file, '/'), '.'), '_'); console->info("Reading CQF {} Seed {}", nqf, cqfs[nqf].seed()); @@ -123,15 +114,13 @@ validate_main ( ValidateOpts& opt ) uint64_t kmer_size = cdbg.get_cqf()->keybits() / 2; console->info("Read colored dbg with {} k-mers and {} color classes", - cdbg.get_cqf()->size(), cdbg.get_num_bitvectors()); + cdbg.get_cqf()->dist_elts(), cdbg.get_num_bitvectors()); std::string query_file = opt.query_file; console->info("Reading query kmers from disk."); uint32_t seed = 2038074743; uint64_t total_kmers = 0; mantis::QuerySets multi_kmers = Kmer::parse_kmers(query_file.c_str(), - seed, - cdbg.range(), kmer_size, total_kmers); console->info("Total k-mers to query: {}", total_kmers); @@ -146,7 +135,7 @@ validate_main ( ValidateOpts& opt ) for (uint64_t i = 0; i < nqf; i++) { for (auto kmer : kmers) { KeyObject k(kmer, 0, 0); - uint64_t count = cqfs[i].query(k); + uint64_t count = cqfs[i].query(k, 0); fraction_present[inobjects[i].id] += 1; } } From fcaf74d12ee75e8c74c9a4114411f4b04f360f63 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Sat, 18 Aug 2018 17:01:55 -0400 Subject: [PATCH 087/122] A compilable version of deltaManager class + a base for DeltaEncoder class --- include/ColorEncoder.h | 41 +++++++++++ include/DeltaManager.h | 159 +++++++++++++++++++++++++++++++++++++++++ src/CMakeLists.txt | 7 ++ src/ColorEncoder.cc | 142 ++++++++++++++++++++++++++++++++++++ 4 files changed, 349 insertions(+) create mode 100644 include/ColorEncoder.h create mode 100644 include/DeltaManager.h create mode 100644 src/ColorEncoder.cc diff --git a/include/ColorEncoder.h b/include/ColorEncoder.h new file mode 100644 index 0000000..0aa42b9 --- /dev/null +++ b/include/ColorEncoder.h @@ -0,0 +1,41 @@ +// +// Created by Fatemeh Almodaresi on 8/17/18. +// + +#ifndef MANTIS_COLORENCODER_H +#define MANTIS_COLORENCODER_H + +#include +#include "lru/lru.hpp" +#include "DeltaManager.h" + +using LRUCacheMap = LRU::Cache>; + +class ColorEncoder { +public: + ColorEncoder(uint64_t numSamplesIn) : numSamples(numSamplesIn), + deltaM(numSamplesIn, 222500000, 8) {} + void addEdge(uint64_t i, uint64_t j, uint32_t w); + bool hasEdge(uint64_t i, uint64_t j); + std::vector buildColor(uint64_t eqid); + DeltaManager deltaM; + +private: + uint64_t numSamples; + + struct pair_hash { + template + std::size_t operator () (const std::pair &p) const { + auto h1 = std::hash{}(p.first); + auto h2 = std::hash{}(p.second); + + // Mainly for demonstration purposes, i.e. works but is overly simple + // In the real world, use sth. like boost.hash_combine + return h1 ^ h2; + } + }; + std::unordered_map, uint32_t, pair_hash> edges; + LRU::Cache > lru_cache; + //DeltaManager deltas; +}; +#endif //MANTIS_COLORENCODER_H diff --git a/include/DeltaManager.h b/include/DeltaManager.h new file mode 100644 index 0000000..d386462 --- /dev/null +++ b/include/DeltaManager.h @@ -0,0 +1,159 @@ +// +// Created by Fatemeh Almodaresi on 8/17/18. +// + +#ifndef MANTIS_DELTAMANAGER_H +#define MANTIS_DELTAMANAGER_H + +#include +#include +#include + +class DeltaManager { +public: + + DeltaManager(uint64_t numSamples, + uint64_t approximateColorClsCnt, + uint64_t approximateAvgDeltaPerColorCls) { + slotWidth = ceil(log2(numSamples)); + if (slotWidth * numSamples < 64) { + slotsPerColorCls = numSamples + 1; + } else if (slotWidth * approximateAvgDeltaPerColorCls < 64) { + slotsPerColorCls = (64 / slotWidth + 1) + 1; + } else { + slotsPerColorCls = approximateAvgDeltaPerColorCls + 1; // 1 for storing count of deltas per index + } + deltas.reserve(approximateColorClsCnt*slotsPerColorCls); + // assumption: count of slots * their width is greater than 64 bits + slotsPerColorClsWithPtrs = ((slotsPerColorCls - 1) * slotWidth - 64)/ slotWidth + 1; + colorCnt = 0; + mask = (((uint64_t)1) << slotWidth) - 1; + } + + void insertDeltas(uint64_t colorId, const std::vector & dlta) { + // see if we need to split deltas between main DS and heap + assert(colorId <= colorCnt); + uint64_t mainDSDeltaCnt = dlta.size() < slotsPerColorCls - 1? dlta.size() : slotsPerColorClsWithPtrs-1; + uint64_t startBit = colorId*slotWidth*slotsPerColorCls; // index for next color + insertValIntoDeltaV(startBit, dlta.size(), slotWidth); // insert count of deltas + startBit+=slotWidth; + for (uint64_t i = 0; i < mainDSDeltaCnt; i++) { // insert values into main DS + insertValIntoDeltaV(slotWidth, dlta[i], slotWidth); + startBit+=slotWidth; + } + if (mainDSDeltaCnt < dlta.size()) { // in case count of deltas exceeds the reserved count + // store the rest in an array in heap + uint64_t *theRest = new uint64_t[(dlta.size() - mainDSDeltaCnt)*slotWidth/64+1]; + insertValIntoDeltaV(startBit, reinterpret_cast(theRest), 64); // store the pointer to the heap in the main DS + insertValIntoHeap(dlta, mainDSDeltaCnt, theRest, slotWidth); + } + colorCnt++; + } + + std::vector getDeltas(uint64_t colorId) { + assert(colorId < colorCnt); + std::vector res; + uint64_t startBit = colorId*slotWidth*slotsPerColorCls; // index for next color + uint64_t deltaCnt = getValFromMDeltaV(startBit, slotWidth); + uint64_t mainDSDeltaCnt = deltaCnt < slotsPerColorCls - 1? deltaCnt : slotsPerColorClsWithPtrs-1; + startBit+=slotWidth; + for (uint64_t i = 0; i < mainDSDeltaCnt; i++) { // insert values into main DS + uint64_t delta = getValFromMDeltaV(slotWidth, slotWidth); + res.push_back(delta); + startBit+=slotWidth; + } + if (mainDSDeltaCnt < deltaCnt) { // in case count of deltas exceeds the reserved count + // fetch the pointer to the heap in the main DS + uint64_t *theRestV = reinterpret_cast(getValFromMDeltaV(startBit, 64)); + getValFromHeap(res, deltaCnt-mainDSDeltaCnt, theRestV, slotWidth); + } + } + void swapDeltas(uint64_t colorId1, uint64_t colorId2) { + std::vector c1deltas = getDeltas(colorId1); + std::vector c2deltas = getDeltas(colorId2); + insertDeltas(colorId1, c2deltas); + insertDeltas(colorId2, c1deltas); + } +private: + uint64_t slotWidth; + uint64_t slotsPerColorCls; + uint64_t slotsPerColorClsWithPtrs; + std::vector deltas; + uint64_t colorCnt; + uint64_t mask; + + // width is limited to 64 (word size) + bool insertValIntoDeltaV(uint64_t startBit, uint64_t val, uint64_t width) { + assert(startBit <= deltas.size()*64); + if (startBit == deltas.size()*64) deltas.push_back(0); + uint64_t wrd = deltas[startBit/64]; + uint64_t startBitInwrd = startBit % 64; + wrd |= (val << startBitInwrd); + uint64_t shiftRight = 64 - startBitInwrd; + if (shiftRight < width) { + deltas.push_back(0); + wrd = deltas.back(); + wrd |= (val >> shiftRight); + } + return true; + } + + // width is limited to 64 (word size) + uint64_t getValFromMDeltaV(uint64_t startBit, uint64_t width) { + assert(startBit < deltas.size()*64); + uint64_t res{0}; + uint64_t wrd = deltas[startBit/64]; + uint64_t startBitInwrd = startBit % 64; + res |= (mask & (wrd >> startBitInwrd)); + uint64_t shiftLeft = 64 - startBitInwrd; + if (shiftLeft < width) { + assert(startBit < (deltas.size()-1)*64); + wrd = deltas[startBit/64+1]; + res |= (mask & (wrd << shiftLeft)); + } + return res; + } + + bool insertValIntoHeap(const std::vector &dlta, + uint64_t startIdx, + uint64_t *vecPtr, + uint64_t width) { + uint64_t startBit{0}; + for (uint64_t i = startIdx; i < dlta.size(); i++) { + uint64_t val = dlta[i]; + uint64_t &wrd = vecPtr[startBit/64]; + uint64_t startBitInwrd = startBit % 64; + wrd |= (val << startBitInwrd); + uint64_t shiftRight = 64 - startBitInwrd; + if (shiftRight < width) { + wrd = vecPtr[startBit/64+1]; + wrd |= (val >> shiftRight); + } + startBit+=width; + } + return true; + } + + bool getValFromHeap(std::vector &dlta, + uint64_t cnt, + uint64_t *vecPtr, + uint64_t width) { + uint64_t startBit{0}; + for (uint64_t i = 0; i < cnt; i++) { + uint64_t res{0}; + uint64_t &wrd = vecPtr[startBit/64]; + uint64_t startBitInwrd = startBit % 64; + res |= (mask & (wrd >> startBitInwrd)); + uint64_t shiftRight = 64 - startBitInwrd; + if (shiftRight < width) { + wrd = vecPtr[startBit/64+1]; + res |= (mask & (wrd << shiftRight)); + } + startBit+=width; + dlta.push_back(res); + } + return true; + } + +}; +#endif //MANTIS_DELTAMANAGER_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 51dbbd6..dcd0f1e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -100,6 +100,13 @@ target_link_libraries(walkCqf mantis_core) set_property(TARGET walkCqf PROPERTY INTERPROCEDURAL_OPTIMIZATION True) +add_executable(colorEncoder ColorEncoder.cc) +target_include_directories(colorEncoder PUBLIC + $) +target_link_libraries(colorEncoder + mantis_core) +set_property(TARGET colorEncoder PROPERTY INTERPROCEDURAL_OPTIMIZATION True) + #add_executable(walkEqcls walkEqcls.cc) #target_include_directories(walkEqcls PUBLIC # $ $) diff --git a/src/ColorEncoder.cc b/src/ColorEncoder.cc new file mode 100644 index 0000000..fb2a7d8 --- /dev/null +++ b/src/ColorEncoder.cc @@ -0,0 +1,142 @@ +// +// Created by Fatemeh Almodaresi on 8/17/18. +// + +#include "ColorEncoder.h" +#include + +std::vector ColorEncoder::buildColor(uint64_t eqid) { + std::vector flips(numSamples); + std::vector xorflips(numSamples, 0); +/* + uint64_t i{eqid}, from{0}, to{0}; + bool foundCache = false; + uint32_t iparent = parentbv[i]; + while (iparent != i) { + if (lru_cache.contains(i)) { + const auto &vs = lru_cache[i]; + for (auto v : vs) { + xorflips[v] = 1; + } + foundCache = true; + break; + } + from = (i > 0) ? (sbbv(i) + 1) : 0; + froms.push_back(from); + + i = iparent; + iparent = parentbv[i]; + } + if (!foundCache and i != zero) { + from = (i > 0) ? (sbbv(i) + 1) : 0; + froms.push_back(from); + } + + uint64_t pctr{0}; + for (auto f : froms) { + bool found = false; + uint64_t wrd{0}; + //auto j = f; + uint64_t offset{0}; + auto start = f; + do { + wrd = bbv.get_int(start, 64); + for (uint64_t j = 0; j < 64; j++) { + flips[deltabv[start + j]] ^= 0x01; + if ((wrd >> j) & 0x01) { + found = true; + break; + } + } + start += 64; + } while (!found); + } +*/ + + // return the indices of set bits + std::vector eq; + uint64_t numWrds = numSamples/64+1; + eq.reserve(numWrds); + uint64_t one = 1; + for (auto i = 0; i < numSamples; i++) { + if (flips[i] ^ xorflips[i]) { + eq.push_back(i); + } + } + return eq; +} + +void ColorEncoder::addEdge(uint64_t i, uint64_t j, uint32_t w) { + if (i == j) return; + if (i > j) { + std::swap(i,j); + } + if (edges.find(std::make_pair(i, j)) == edges.end()) { + edges[std::make_pair(i, j)] = w; + } +} + +bool ColorEncoder::hasEdge(uint64_t i, uint64_t j) { + if (i > j) { + std::swap(i, j); + } + return i == j or edges.find(std::make_pair(i, j)) != edges.end(); +} + +int main(int argc, char* argv[]) { + DeltaManager deltaManager(3000, 10, 6); + std::vector d; + d.push_back(0); + d.push_back(300); + d.push_back(700); + deltaManager.insertDeltas(0, d); + + d.clear(); + d.push_back(0); + deltaManager.insertDeltas(1, d); + + d.clear(); + d.push_back(100); + d.push_back(200); + d.push_back(300); + d.push_back(400); + d.push_back(500); + d.push_back(600); + d.push_back(700); + d.push_back(800); + d.push_back(900); + deltaManager.insertDeltas(2, d); + + auto vec = deltaManager.getDeltas(2); + std::cerr << "\n\ndeltas for 2\n"; + for (auto v : vec) { + std::cerr << v << " "; + } + + vec = deltaManager.getDeltas(0); + std::cerr << "\n\ndeltas for 0\n"; + for (auto v : vec) { + std::cerr << v << " "; + } + + vec = deltaManager.getDeltas(1); + std::cerr << "\n\ndeltas for 1\n"; + for (auto v : vec) { + std::cerr << v << " "; + } + + deltaManager.swapDeltas(0,2); + std::cerr << "After swap:\n"; + vec = deltaManager.getDeltas(0); + std::cerr << "\n\ndeltas for 0\n"; + for (auto v : vec) { + std::cerr << v << " "; + } + + vec = deltaManager.getDeltas(2); + std::cerr << "\n\ndeltas for 2\n"; + for (auto v : vec) { + std::cerr << v << " "; + } + +} \ No newline at end of file From b76c5965e122880cef8cfa15db74c89afbafe546 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Sat, 18 Aug 2018 18:34:08 -0400 Subject: [PATCH 088/122] works when no need for heap. swap doesn't work yet. --- include/DeltaManager.h | 96 +++++++++++++++++++++++++++--------------- src/ColorEncoder.cc | 11 +++-- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/include/DeltaManager.h b/include/DeltaManager.h index d386462..06d8761 100644 --- a/include/DeltaManager.h +++ b/include/DeltaManager.h @@ -8,6 +8,7 @@ #include #include #include +#include class DeltaManager { public: @@ -22,58 +23,80 @@ class DeltaManager { slotsPerColorCls = (64 / slotWidth + 1) + 1; } else { slotsPerColorCls = approximateAvgDeltaPerColorCls + 1; // 1 for storing count of deltas per index + std::cerr << "here: " << slotWidth << " " << slotsPerColorCls << "\n"; } - deltas.reserve(approximateColorClsCnt*slotsPerColorCls); + deltas.reserve(approximateColorClsCnt * slotsPerColorCls); // assumption: count of slots * their width is greater than 64 bits - slotsPerColorClsWithPtrs = ((slotsPerColorCls - 1) * slotWidth - 64)/ slotWidth + 1; + slotsPerColorClsWithPtrs = ((slotsPerColorCls - 1) * slotWidth - 64) / slotWidth + 1; colorCnt = 0; - mask = (((uint64_t)1) << slotWidth) - 1; + mask = (((uint64_t) 1) << slotWidth) - 1; } - void insertDeltas(uint64_t colorId, const std::vector & dlta) { + void insertDeltas(uint64_t colorId, const std::vector &dlta) { // see if we need to split deltas between main DS and heap + //std::cerr << colorId << " currentCnt: " << colorCnt << "\n"; assert(colorId <= colorCnt); - uint64_t mainDSDeltaCnt = dlta.size() < slotsPerColorCls - 1? dlta.size() : slotsPerColorClsWithPtrs-1; - uint64_t startBit = colorId*slotWidth*slotsPerColorCls; // index for next color + // We always want to assign slotsPerColorCls slots to each index + // even if deltas in that index are fewer than the avg num of deltas + if (colorId == colorCnt) { + while (deltas.size() < ((colorCnt+1)*slotsPerColorCls*slotWidth)/64+1) { + deltas.push_back(0); + } + } + // now assuming we've already reserved the space for the new colorId, insert deltas + uint64_t mainDSDeltaCnt = dlta.size() < slotsPerColorCls ? dlta.size() : slotsPerColorClsWithPtrs - 1; + uint64_t startBit = colorId * slotWidth * slotsPerColorCls; // index for next color + //std::cerr << mainDSDeltaCnt << " " << dlta.size() << "\nstartBits:\n"; + //std::cerr << "for cnt: " << startBit << "\n"; insertValIntoDeltaV(startBit, dlta.size(), slotWidth); // insert count of deltas - startBit+=slotWidth; + startBit += slotWidth; for (uint64_t i = 0; i < mainDSDeltaCnt; i++) { // insert values into main DS - insertValIntoDeltaV(slotWidth, dlta[i], slotWidth); - startBit+=slotWidth; + insertValIntoDeltaV(startBit, dlta[i], slotWidth); + //std::cerr << startBit << ":" << dlta[i] << "\n"; + startBit += slotWidth; } if (mainDSDeltaCnt < dlta.size()) { // in case count of deltas exceeds the reserved count // store the rest in an array in heap - uint64_t *theRest = new uint64_t[(dlta.size() - mainDSDeltaCnt)*slotWidth/64+1]; - insertValIntoDeltaV(startBit, reinterpret_cast(theRest), 64); // store the pointer to the heap in the main DS + uint64_t *theRest = new uint64_t[(dlta.size() - mainDSDeltaCnt) * slotWidth / 64 + 1]; + insertValIntoDeltaV(startBit, reinterpret_cast(theRest), + 64); // store the pointer to the heap in the main DS insertValIntoHeap(dlta, mainDSDeltaCnt, theRest, slotWidth); } colorCnt++; } std::vector getDeltas(uint64_t colorId) { + //std::cerr << "\n\ngetDeltas:"; +// std::cerr << colorId << " cnt: " << colorCnt << "\n"; assert(colorId < colorCnt); std::vector res; - uint64_t startBit = colorId*slotWidth*slotsPerColorCls; // index for next color + uint64_t startBit = colorId * slotWidth * slotsPerColorCls; // index for next color uint64_t deltaCnt = getValFromMDeltaV(startBit, slotWidth); - uint64_t mainDSDeltaCnt = deltaCnt < slotsPerColorCls - 1? deltaCnt : slotsPerColorClsWithPtrs-1; - startBit+=slotWidth; + uint64_t mainDSDeltaCnt = deltaCnt < slotsPerColorCls ? deltaCnt : slotsPerColorClsWithPtrs - 1; +// std::cerr << "deltaCnt: " << mainDSDeltaCnt << "\nstartBits:\n"; +// std::cerr << "for cnt: " << startBit << "\n"; + startBit += slotWidth; for (uint64_t i = 0; i < mainDSDeltaCnt; i++) { // insert values into main DS - uint64_t delta = getValFromMDeltaV(slotWidth, slotWidth); + uint64_t delta = getValFromMDeltaV(startBit, slotWidth); +// std::cerr << startBit << ":" << delta << "\n"; res.push_back(delta); - startBit+=slotWidth; + startBit += slotWidth; } if (mainDSDeltaCnt < deltaCnt) { // in case count of deltas exceeds the reserved count // fetch the pointer to the heap in the main DS uint64_t *theRestV = reinterpret_cast(getValFromMDeltaV(startBit, 64)); - getValFromHeap(res, deltaCnt-mainDSDeltaCnt, theRestV, slotWidth); + getValFromHeap(res, deltaCnt - mainDSDeltaCnt, theRestV, slotWidth); } + return res; } + void swapDeltas(uint64_t colorId1, uint64_t colorId2) { std::vector c1deltas = getDeltas(colorId1); std::vector c2deltas = getDeltas(colorId2); insertDeltas(colorId1, c2deltas); insertDeltas(colorId2, c1deltas); } + private: uint64_t slotWidth; uint64_t slotsPerColorCls; @@ -84,31 +107,33 @@ class DeltaManager { // width is limited to 64 (word size) bool insertValIntoDeltaV(uint64_t startBit, uint64_t val, uint64_t width) { - assert(startBit <= deltas.size()*64); - if (startBit == deltas.size()*64) deltas.push_back(0); - uint64_t wrd = deltas[startBit/64]; + assert(startBit < deltas.size() * 64); + uint64_t &wrd = deltas[startBit / 64]; uint64_t startBitInwrd = startBit % 64; + std::cerr << "gstart: " << startBit << " with wrdIdx = " << startBit/64 <<" start = " << startBitInwrd << " curval =" << wrd << " val =" << val << " -> "; wrd |= (val << startBitInwrd); + std::cerr << wrd << "\n"; uint64_t shiftRight = 64 - startBitInwrd; if (shiftRight < width) { - deltas.push_back(0); - wrd = deltas.back(); + assert(startBit < (deltas.size()-1)*64); + wrd = deltas[startBit/64+1]; wrd |= (val >> shiftRight); } + return true; } // width is limited to 64 (word size) uint64_t getValFromMDeltaV(uint64_t startBit, uint64_t width) { - assert(startBit < deltas.size()*64); + assert(startBit < deltas.size() * 64); uint64_t res{0}; - uint64_t wrd = deltas[startBit/64]; + uint64_t wrd = deltas[startBit / 64]; uint64_t startBitInwrd = startBit % 64; res |= (mask & (wrd >> startBitInwrd)); uint64_t shiftLeft = 64 - startBitInwrd; if (shiftLeft < width) { - assert(startBit < (deltas.size()-1)*64); - wrd = deltas[startBit/64+1]; + assert(startBit < (deltas.size() - 1) * 64); + wrd = deltas[startBit / 64 + 1]; res |= (mask & (wrd << shiftLeft)); } return res; @@ -121,39 +146,40 @@ class DeltaManager { uint64_t startBit{0}; for (uint64_t i = startIdx; i < dlta.size(); i++) { uint64_t val = dlta[i]; - uint64_t &wrd = vecPtr[startBit/64]; + uint64_t &wrd = vecPtr[startBit / 64]; uint64_t startBitInwrd = startBit % 64; wrd |= (val << startBitInwrd); uint64_t shiftRight = 64 - startBitInwrd; if (shiftRight < width) { - wrd = vecPtr[startBit/64+1]; + wrd = vecPtr[startBit / 64 + 1]; wrd |= (val >> shiftRight); } - startBit+=width; + startBit += width; } return true; } bool getValFromHeap(std::vector &dlta, - uint64_t cnt, - uint64_t *vecPtr, - uint64_t width) { + uint64_t cnt, + uint64_t *vecPtr, + uint64_t width) { uint64_t startBit{0}; for (uint64_t i = 0; i < cnt; i++) { uint64_t res{0}; - uint64_t &wrd = vecPtr[startBit/64]; + uint64_t wrd = vecPtr[startBit / 64]; uint64_t startBitInwrd = startBit % 64; res |= (mask & (wrd >> startBitInwrd)); uint64_t shiftRight = 64 - startBitInwrd; if (shiftRight < width) { - wrd = vecPtr[startBit/64+1]; + wrd = vecPtr[startBit / 64 + 1]; res |= (mask & (wrd << shiftRight)); } - startBit+=width; + startBit += width; dlta.push_back(res); } return true; } }; + #endif //MANTIS_DELTAMANAGER_H diff --git a/src/ColorEncoder.cc b/src/ColorEncoder.cc index fb2a7d8..7b55201 100644 --- a/src/ColorEncoder.cc +++ b/src/ColorEncoder.cc @@ -92,7 +92,7 @@ int main(int argc, char* argv[]) { deltaManager.insertDeltas(0, d); d.clear(); - d.push_back(0); + d.push_back(2999); deltaManager.insertDeltas(1, d); d.clear(); @@ -102,9 +102,11 @@ int main(int argc, char* argv[]) { d.push_back(400); d.push_back(500); d.push_back(600); - d.push_back(700); + /*d.push_back(700); d.push_back(800); d.push_back(900); + d.push_back(1000); + d.push_back(1100);*/ deltaManager.insertDeltas(2, d); auto vec = deltaManager.getDeltas(2); @@ -113,6 +115,7 @@ int main(int argc, char* argv[]) { std::cerr << v << " "; } + std::cerr << "\n"; vec = deltaManager.getDeltas(0); std::cerr << "\n\ndeltas for 0\n"; for (auto v : vec) { @@ -124,7 +127,7 @@ int main(int argc, char* argv[]) { for (auto v : vec) { std::cerr << v << " "; } - +/* deltaManager.swapDeltas(0,2); std::cerr << "After swap:\n"; vec = deltaManager.getDeltas(0); @@ -138,5 +141,5 @@ int main(int argc, char* argv[]) { for (auto v : vec) { std::cerr << v << " "; } - + std::cerr << "\n";*/ } \ No newline at end of file From ba87c2e08f783492a0c7342e03e995a49c868247 Mon Sep 17 00:00:00 2001 From: Prashant Pandey Date: Sat, 18 Aug 2018 20:35:54 -0700 Subject: [PATCH 089/122] Adding Squeakr config file to mantis. Stop building if Squeakr file is not readable. --- include/squeakrconfig.h | 33 +++++++++++++++++++++ src/CMakeLists.txt | 1 + src/coloreddbg.cc | 65 +++++++++++++++++++++++------------------ src/squeakrconfig.cc | 31 ++++++++++++++++++++ 4 files changed, 101 insertions(+), 29 deletions(-) create mode 100644 include/squeakrconfig.h create mode 100644 src/squeakrconfig.cc diff --git a/include/squeakrconfig.h b/include/squeakrconfig.h new file mode 100644 index 0000000..b830e15 --- /dev/null +++ b/include/squeakrconfig.h @@ -0,0 +1,33 @@ +/* + * ============================================================================ + * + * Authors: Prashant Pandey + * Rob Johnson + * Rob Patro (rob.patro@cs.stonybrook.edu) + * + * ============================================================================ + */ + +#ifndef _SQUEAKR_CONFIG_H_ +#define _SQUEAKR_CONFIG_H_ + +#include +#include + +namespace squeakr { + constexpr const uint32_t SQUEAKR_INVALID_VERSION{1}; + constexpr const uint32_t SQUEAKR_INVALID_ENDIAN{2}; + constexpr const uint32_t INDEX_VERSION{2}; + constexpr const uint64_t ENDIANNESS{0x0102030405060708ULL}; + + typedef struct __attribute__ ((__packed__)) squeakrconfig { + uint64_t kmer_size; + uint64_t cutoff; + uint64_t contains_counts; + uint64_t endianness; + uint32_t version; + } squeakrconfig; + + int read_config(std::string squeakr_file, squeakrconfig *config); +} +#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 399a0c1..ee50c6f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(mantis_core STATIC validatemantis.cc coloreddbg.cc MantisFS.cc + squeakrconfig.cc gqf/gqf.c gqf/gqf_file.c gqf/hashutil.c) diff --git a/src/coloreddbg.cc b/src/coloreddbg.cc index 4bab65b..c2deb54 100644 --- a/src/coloreddbg.cc +++ b/src/coloreddbg.cc @@ -32,26 +32,18 @@ #include #include #include -#include "MantisFS.h" + #include "sparsepp/spp.h" #include "tsl/sparse_map.h" + +#include "MantisFS.h" #include "ProgOpts.h" #include "coloreddbg.h" +#include "squeakrconfig.h" #include "json.hpp" #include "mantis_utils.hpp" #include "mantisconfig.hpp" -// This function read one byte from each page in the iterator buffer. -//uint64_t tmp_sum; -//void handler_function(union sigval sv) { - //CQF::Iterator& it(*((CQF::Iterator*)sv.sival_ptr)); - //unsigned char *start = (unsigned char*)(it.iter.qf->metadata) + it.last_prefetch_offset; - //unsigned char *counter = (unsigned char*)(it.iter.qf->metadata) + it.last_prefetch_offset; - //for (;counter < start + it.buffer_size; counter += 4096) { - //tmp_sum += *counter; - //} -//} - /* * === FUNCTION ============================================================= * Name: main @@ -74,16 +66,6 @@ build_main ( BuildOpts& opt ) console->error("Input file {} does not exist or could not be opened.", opt.inlist); std::exit(1); } - //struct timeval start1, end1; - //struct timezone tzp; - - // C++-izing - // This is C++ ... not C. Why do we have raw pointers - // and calloc them. We use vectors instead. - //SampleObject*> *inobjects; - //CQF *cqfs; - std::vector*>> inobjects; - std::vector> cqfs; /** try and create the output directory * and write a file to it. Complain to the user @@ -120,28 +102,53 @@ build_main ( BuildOpts& opt ) jfile.close(); } + std::vector*>> inobjects; + std::vector> cqfs; + // reserve QF structs for input CQFs inobjects.reserve(num_samples); cqfs.reserve(num_samples); // mmap all the input cqfs - std::string cqf_file; + std::string squeakr_file; uint32_t nqf = 0; + uint32_t kmer_size{0}; console->info("Reading input Squeakr files."); - while (infile >> cqf_file) { - if (!mantis::fs::FileExists(cqf_file.c_str())) { - console->error("Squeakr file {} does not exist.", cqf_file); + while (infile >> squeakr_file) { + if (!mantis::fs::FileExists(squeakr_file.c_str())) { + console->error("Squeakr file {} does not exist.", squeakr_file); + exit(1); + } + squeakr::squeakrconfig config; + int ret = squeakr::read_config(squeakr_file, &config); + if (ret == squeakr::SQUEAKR_INVALID_ENDIAN) { + console->error("Can't read Squeakr file. It was written on a different endian machine."); exit(1); } - cqfs.emplace_back(cqf_file, CQF_MMAP); - std::string sample_id = first_part(first_part(last_part(cqf_file, '/'), + if (ret == squeakr::SQUEAKR_INVALID_VERSION) { + console->error("Squeakr index version is invalid. Expected: {} Available: {}", + squeakr::INDEX_VERSION, config.version); + exit(1); + } + if (cqfs.size() == 0) + kmer_size = config.kmer_size; + else { + if (kmer_size != config.kmer_size) { + console->error("Squeakr file {} has a different k-mer size. Expected: {} Available: {}", + squeakr_file, kmer_size, config.kmer_size); + exit(1); + } + } + + cqfs.emplace_back(squeakr_file, CQF_MMAP); + std::string sample_id = first_part(first_part(last_part(squeakr_file, '/'), '.'), '_'); console->info("Reading CQF {} Seed {}",nqf, cqfs[nqf].seed()); console->info("Sample id {} cut off {}", sample_id); cqfs.back().dump_metadata(); inobjects.emplace_back(&cqfs[nqf], sample_id, nqf); if (!cqfs.front().check_similarity(&cqfs.back())) { - console->error("Passed Squeakr files are not similar.", cqf_file); + console->error("Passed Squeakr files are not similar.", squeakr_file); exit(1); } nqf++; diff --git a/src/squeakrconfig.cc b/src/squeakrconfig.cc new file mode 100644 index 0000000..5f7e9c9 --- /dev/null +++ b/src/squeakrconfig.cc @@ -0,0 +1,31 @@ +/* + * ===================================================================================== + * + * Author: Prashant Pandey (), ppandey@cs.stonybrook.edu + * Organization: Stony Brook University + * + * ===================================================================================== + */ + +#include // std::ifstream + +#include "squeakrconfig.h" + +namespace squeakr { + + int read_config(std::string file, squeakrconfig *config) { + // seek to the end of the file and read the k-mer size + std::ifstream squeakr_file(file, std::ofstream::in); + uint64_t file_size = squeakr_file.tellg(); + squeakr_file.seekg(file_size - sizeof(squeakrconfig)); + squeakr_file.read((char*)config, sizeof(config)); + squeakr_file.close(); + if (config->endianness != ENDIANNESS) { + return SQUEAKR_INVALID_ENDIAN; + } + if (config->version != INDEX_VERSION) { + return SQUEAKR_INVALID_VERSION; + } + return 0; + } +} From c8b9f8f159c241799a0a96c44c4bd0ab65cef261 Mon Sep 17 00:00:00 2001 From: Prashant Pandey Date: Sun, 19 Aug 2018 23:26:18 -0700 Subject: [PATCH 090/122] Minor fixes. Tested OK on sample datasets. --- raw/incqfs.lst | 3 +-- src/coloreddbg.cc | 5 +++-- src/squeakrconfig.cc | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/raw/incqfs.lst b/raw/incqfs.lst index 03ea24c..a28cf6b 100644 --- a/raw/incqfs.lst +++ b/raw/incqfs.lst @@ -1,2 +1 @@ -data/SRR191411_exact.ser 1 -data/SRR191403_exact.ser 1 +data/Squeakr1.sqr 1 diff --git a/src/coloreddbg.cc b/src/coloreddbg.cc index c2deb54..e094f34 100644 --- a/src/coloreddbg.cc +++ b/src/coloreddbg.cc @@ -111,10 +111,11 @@ build_main ( BuildOpts& opt ) // mmap all the input cqfs std::string squeakr_file; + uint32_t cutoff; uint32_t nqf = 0; uint32_t kmer_size{0}; console->info("Reading input Squeakr files."); - while (infile >> squeakr_file) { + while (infile >> squeakr_file >> cutoff) { if (!mantis::fs::FileExists(squeakr_file.c_str())) { console->error("Squeakr file {} does not exist.", squeakr_file); exit(1); @@ -144,7 +145,7 @@ build_main ( BuildOpts& opt ) std::string sample_id = first_part(first_part(last_part(squeakr_file, '/'), '.'), '_'); console->info("Reading CQF {} Seed {}",nqf, cqfs[nqf].seed()); - console->info("Sample id {} cut off {}", sample_id); + console->info("Sample id {} cut off {}", sample_id, cutoff); cqfs.back().dump_metadata(); inobjects.emplace_back(&cqfs[nqf], sample_id, nqf); if (!cqfs.front().check_similarity(&cqfs.back())) { diff --git a/src/squeakrconfig.cc b/src/squeakrconfig.cc index 5f7e9c9..3be2693 100644 --- a/src/squeakrconfig.cc +++ b/src/squeakrconfig.cc @@ -16,9 +16,10 @@ namespace squeakr { int read_config(std::string file, squeakrconfig *config) { // seek to the end of the file and read the k-mer size std::ifstream squeakr_file(file, std::ofstream::in); + squeakr_file.seekg(0, squeakr_file.end); uint64_t file_size = squeakr_file.tellg(); squeakr_file.seekg(file_size - sizeof(squeakrconfig)); - squeakr_file.read((char*)config, sizeof(config)); + squeakr_file.read((char*)config, sizeof(squeakrconfig)); squeakr_file.close(); if (config->endianness != ENDIANNESS) { return SQUEAKR_INVALID_ENDIAN; From a470e42c4eb33719aaa644e0caf85a4a3ef9849c Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Mon, 20 Aug 2018 12:34:00 -0400 Subject: [PATCH 091/122] swap works now --- include/DeltaManager.h | 33 +++++++++++++-------------------- src/ColorEncoder.cc | 24 ++++++++++++------------ 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/include/DeltaManager.h b/include/DeltaManager.h index 06d8761..af12cd8 100644 --- a/include/DeltaManager.h +++ b/include/DeltaManager.h @@ -9,7 +9,7 @@ #include #include #include - +//#include class DeltaManager { public: @@ -23,7 +23,6 @@ class DeltaManager { slotsPerColorCls = (64 / slotWidth + 1) + 1; } else { slotsPerColorCls = approximateAvgDeltaPerColorCls + 1; // 1 for storing count of deltas per index - std::cerr << "here: " << slotWidth << " " << slotsPerColorCls << "\n"; } deltas.reserve(approximateColorClsCnt * slotsPerColorCls); // assumption: count of slots * their width is greater than 64 bits @@ -34,25 +33,22 @@ class DeltaManager { void insertDeltas(uint64_t colorId, const std::vector &dlta) { // see if we need to split deltas between main DS and heap - //std::cerr << colorId << " currentCnt: " << colorCnt << "\n"; assert(colorId <= colorCnt); + auto startBit = colorId*slotsPerColorCls*slotWidth; + auto nextStartBit = (colorId+1)*slotsPerColorCls*slotWidth; // We always want to assign slotsPerColorCls slots to each index // even if deltas in that index are fewer than the avg num of deltas if (colorId == colorCnt) { - while (deltas.size() < ((colorCnt+1)*slotsPerColorCls*slotWidth)/64+1) { + while (deltas.size() < nextStartBit/64+1) { deltas.push_back(0); } } // now assuming we've already reserved the space for the new colorId, insert deltas uint64_t mainDSDeltaCnt = dlta.size() < slotsPerColorCls ? dlta.size() : slotsPerColorClsWithPtrs - 1; - uint64_t startBit = colorId * slotWidth * slotsPerColorCls; // index for next color - //std::cerr << mainDSDeltaCnt << " " << dlta.size() << "\nstartBits:\n"; - //std::cerr << "for cnt: " << startBit << "\n"; insertValIntoDeltaV(startBit, dlta.size(), slotWidth); // insert count of deltas startBit += slotWidth; for (uint64_t i = 0; i < mainDSDeltaCnt; i++) { // insert values into main DS insertValIntoDeltaV(startBit, dlta[i], slotWidth); - //std::cerr << startBit << ":" << dlta[i] << "\n"; startBit += slotWidth; } if (mainDSDeltaCnt < dlta.size()) { // in case count of deltas exceeds the reserved count @@ -62,23 +58,19 @@ class DeltaManager { 64); // store the pointer to the heap in the main DS insertValIntoHeap(dlta, mainDSDeltaCnt, theRest, slotWidth); } - colorCnt++; + if (colorId == colorCnt) + colorCnt++; } std::vector getDeltas(uint64_t colorId) { - //std::cerr << "\n\ngetDeltas:"; -// std::cerr << colorId << " cnt: " << colorCnt << "\n"; assert(colorId < colorCnt); std::vector res; uint64_t startBit = colorId * slotWidth * slotsPerColorCls; // index for next color uint64_t deltaCnt = getValFromMDeltaV(startBit, slotWidth); uint64_t mainDSDeltaCnt = deltaCnt < slotsPerColorCls ? deltaCnt : slotsPerColorClsWithPtrs - 1; -// std::cerr << "deltaCnt: " << mainDSDeltaCnt << "\nstartBits:\n"; -// std::cerr << "for cnt: " << startBit << "\n"; startBit += slotWidth; for (uint64_t i = 0; i < mainDSDeltaCnt; i++) { // insert values into main DS uint64_t delta = getValFromMDeltaV(startBit, slotWidth); -// std::cerr << startBit << ":" << delta << "\n"; res.push_back(delta); startBit += slotWidth; } @@ -107,17 +99,18 @@ class DeltaManager { // width is limited to 64 (word size) bool insertValIntoDeltaV(uint64_t startBit, uint64_t val, uint64_t width) { + uint64_t localMask = (((uint64_t)1 << width) - 1); assert(startBit < deltas.size() * 64); uint64_t &wrd = deltas[startBit / 64]; uint64_t startBitInwrd = startBit % 64; - std::cerr << "gstart: " << startBit << " with wrdIdx = " << startBit/64 <<" start = " << startBitInwrd << " curval =" << wrd << " val =" << val << " -> "; + wrd &= ~(localMask << startBitInwrd); wrd |= (val << startBitInwrd); - std::cerr << wrd << "\n"; uint64_t shiftRight = 64 - startBitInwrd; if (shiftRight < width) { assert(startBit < (deltas.size()-1)*64); - wrd = deltas[startBit/64+1]; - wrd |= (val >> shiftRight); + auto &nextWrd = deltas[startBit/64+1]; + nextWrd &= ~(localMask >> shiftRight); + nextWrd |= (val >> shiftRight); } return true; @@ -151,8 +144,8 @@ class DeltaManager { wrd |= (val << startBitInwrd); uint64_t shiftRight = 64 - startBitInwrd; if (shiftRight < width) { - wrd = vecPtr[startBit / 64 + 1]; - wrd |= (val >> shiftRight); + auto &nextWrd = vecPtr[startBit / 64 + 1]; + nextWrd |= (val >> shiftRight); } startBit += width; } diff --git a/src/ColorEncoder.cc b/src/ColorEncoder.cc index 7b55201..c2a6bdd 100644 --- a/src/ColorEncoder.cc +++ b/src/ColorEncoder.cc @@ -86,9 +86,9 @@ bool ColorEncoder::hasEdge(uint64_t i, uint64_t j) { int main(int argc, char* argv[]) { DeltaManager deltaManager(3000, 10, 6); std::vector d; - d.push_back(0); - d.push_back(300); - d.push_back(700); + d.push_back(1100); + d.push_back(1200); + d.push_back(1300); deltaManager.insertDeltas(0, d); d.clear(); @@ -109,27 +109,27 @@ int main(int argc, char* argv[]) { d.push_back(1100);*/ deltaManager.insertDeltas(2, d); - auto vec = deltaManager.getDeltas(2); - std::cerr << "\n\ndeltas for 2\n"; + auto vec = deltaManager.getDeltas(0); + std::cerr << "\n\ndeltas for 0\n"; for (auto v : vec) { std::cerr << v << " "; } std::cerr << "\n"; - vec = deltaManager.getDeltas(0); - std::cerr << "\n\ndeltas for 0\n"; + vec = deltaManager.getDeltas(1); + std::cerr << "\n\ndeltas for 1\n"; for (auto v : vec) { std::cerr << v << " "; } - vec = deltaManager.getDeltas(1); - std::cerr << "\n\ndeltas for 1\n"; + vec = deltaManager.getDeltas(2); + std::cerr << "\n\ndeltas for 2\n"; for (auto v : vec) { std::cerr << v << " "; } -/* + deltaManager.swapDeltas(0,2); - std::cerr << "After swap:\n"; + std::cerr << "\nAfter swap:\n"; vec = deltaManager.getDeltas(0); std::cerr << "\n\ndeltas for 0\n"; for (auto v : vec) { @@ -141,5 +141,5 @@ int main(int argc, char* argv[]) { for (auto v : vec) { std::cerr << v << " "; } - std::cerr << "\n";*/ + std::cerr << "\n"; } \ No newline at end of file From 327fb95f80bf4ba99e1abbb8131a8002317efa85 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Mon, 20 Aug 2018 14:25:05 -0400 Subject: [PATCH 092/122] deltaManager works beautifully! --- include/DeltaManager.h | 66 ++++++++++++++++++++++++++++++------------ src/ColorEncoder.cc | 31 ++++++++++---------- 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/include/DeltaManager.h b/include/DeltaManager.h index af12cd8..cafd1ba 100644 --- a/include/DeltaManager.h +++ b/include/DeltaManager.h @@ -7,33 +7,45 @@ #include #include -#include #include //#include +#include +#include + +struct DeltaManagerException : public std::exception { +private: + std::string message_; +public: + + DeltaManagerException(const std::string& message) : message_(message) {} + const char * what () const throw () { + return message_.c_str(); + } +}; + class DeltaManager { public: - DeltaManager(uint64_t numSamples, + DeltaManager(uint64_t numSamplesIn, uint64_t approximateColorClsCnt, - uint64_t approximateAvgDeltaPerColorCls) { + uint64_t approximateAvgDeltaCntPerColorCls) : numSamples(numSamplesIn) { slotWidth = ceil(log2(numSamples)); - if (slotWidth * numSamples < 64) { + if (slotWidth * (numSamples+1) < 64) { slotsPerColorCls = numSamples + 1; - } else if (slotWidth * approximateAvgDeltaPerColorCls < 64) { + } else if (slotWidth * (approximateAvgDeltaCntPerColorCls+1) < 64) { slotsPerColorCls = (64 / slotWidth + 1) + 1; } else { - slotsPerColorCls = approximateAvgDeltaPerColorCls + 1; // 1 for storing count of deltas per index + slotsPerColorCls = approximateAvgDeltaCntPerColorCls + 1; // 1 for storing count of deltas per index } deltas.reserve(approximateColorClsCnt * slotsPerColorCls); // assumption: count of slots * their width is greater than 64 bits slotsPerColorClsWithPtrs = ((slotsPerColorCls - 1) * slotWidth - 64) / slotWidth + 1; colorCnt = 0; - mask = (((uint64_t) 1) << slotWidth) - 1; } void insertDeltas(uint64_t colorId, const std::vector &dlta) { // see if we need to split deltas between main DS and heap - assert(colorId <= colorCnt); + if (colorId > colorCnt) throw DeltaManagerException("colorId > colorCnt"); auto startBit = colorId*slotsPerColorCls*slotWidth; auto nextStartBit = (colorId+1)*slotsPerColorCls*slotWidth; // We always want to assign slotsPerColorCls slots to each index @@ -53,7 +65,7 @@ class DeltaManager { } if (mainDSDeltaCnt < dlta.size()) { // in case count of deltas exceeds the reserved count // store the rest in an array in heap - uint64_t *theRest = new uint64_t[(dlta.size() - mainDSDeltaCnt) * slotWidth / 64 + 1]; + uint64_t *theRest = new uint64_t[(uint64_t)std::ceil((double)((dlta.size() - mainDSDeltaCnt) * slotWidth) / 64.0)](); insertValIntoDeltaV(startBit, reinterpret_cast(theRest), 64); // store the pointer to the heap in the main DS insertValIntoHeap(dlta, mainDSDeltaCnt, theRest, slotWidth); @@ -63,7 +75,7 @@ class DeltaManager { } std::vector getDeltas(uint64_t colorId) { - assert(colorId < colorCnt); + if (colorId > colorCnt) throw DeltaManagerException("colorId > colorCnt"); std::vector res; uint64_t startBit = colorId * slotWidth * slotsPerColorCls; // index for next color uint64_t deltaCnt = getValFromMDeltaV(startBit, slotWidth); @@ -90,26 +102,33 @@ class DeltaManager { } private: + uint64_t numSamples; uint64_t slotWidth; uint64_t slotsPerColorCls; uint64_t slotsPerColorClsWithPtrs; std::vector deltas; uint64_t colorCnt; - uint64_t mask; // width is limited to 64 (word size) bool insertValIntoDeltaV(uint64_t startBit, uint64_t val, uint64_t width) { - uint64_t localMask = (((uint64_t)1 << width) - 1); - assert(startBit < deltas.size() * 64); + uint64_t mask = width < 64? (((uint64_t)1 << width) - 1) : -1; + if (startBit >= deltas.size() * 64) throw DeltaManagerException("startBit exceeds bit_size"); + if (width == slotWidth and val >= numSamples) { + std::string msg = "delta index is larger than num_samples. val:"+ + std::to_string(val)+ + " num_samples:" +std::to_string(numSamples); + throw DeltaManagerException(msg); + } + uint64_t &wrd = deltas[startBit / 64]; uint64_t startBitInwrd = startBit % 64; - wrd &= ~(localMask << startBitInwrd); + wrd &= ~(mask << startBitInwrd); wrd |= (val << startBitInwrd); uint64_t shiftRight = 64 - startBitInwrd; if (shiftRight < width) { - assert(startBit < (deltas.size()-1)*64); + if (startBit >= (deltas.size() - 1) * 64) throw DeltaManagerException("startBit exceeds bit_size"); auto &nextWrd = deltas[startBit/64+1]; - nextWrd &= ~(localMask >> shiftRight); + nextWrd &= ~(mask >> shiftRight); nextWrd |= (val >> shiftRight); } @@ -118,14 +137,16 @@ class DeltaManager { // width is limited to 64 (word size) uint64_t getValFromMDeltaV(uint64_t startBit, uint64_t width) { - assert(startBit < deltas.size() * 64); + if (startBit >= deltas.size() * 64) throw DeltaManagerException("startBit exceeds bit_size"); + uint64_t mask = width < 64? (((uint64_t)1 << width) - 1) : -1; uint64_t res{0}; uint64_t wrd = deltas[startBit / 64]; uint64_t startBitInwrd = startBit % 64; res |= (mask & (wrd >> startBitInwrd)); + uint64_t shiftLeft = 64 - startBitInwrd; if (shiftLeft < width) { - assert(startBit < (deltas.size() - 1) * 64); + if (startBit >= (deltas.size() - 1) * 64) throw DeltaManagerException("startBit exceeds bit_size"); wrd = deltas[startBit / 64 + 1]; res |= (mask & (wrd << shiftLeft)); } @@ -139,10 +160,18 @@ class DeltaManager { uint64_t startBit{0}; for (uint64_t i = startIdx; i < dlta.size(); i++) { uint64_t val = dlta[i]; + if (width == slotWidth and val >= numSamples) { + std::string msg = "delta index is larger than num_samples. val:"+ + std::to_string(val)+ + " num_samples:" +std::to_string(numSamples); + throw DeltaManagerException(msg); + } + uint64_t &wrd = vecPtr[startBit / 64]; uint64_t startBitInwrd = startBit % 64; wrd |= (val << startBitInwrd); uint64_t shiftRight = 64 - startBitInwrd; + if (shiftRight < width) { auto &nextWrd = vecPtr[startBit / 64 + 1]; nextWrd |= (val >> shiftRight); @@ -157,6 +186,7 @@ class DeltaManager { uint64_t *vecPtr, uint64_t width) { uint64_t startBit{0}; + uint64_t mask = width < 64? (((uint64_t)1 << width) - 1) : -1; for (uint64_t i = 0; i < cnt; i++) { uint64_t res{0}; uint64_t wrd = vecPtr[startBit / 64]; diff --git a/src/ColorEncoder.cc b/src/ColorEncoder.cc index c2a6bdd..ea4c523 100644 --- a/src/ColorEncoder.cc +++ b/src/ColorEncoder.cc @@ -85,28 +85,29 @@ bool ColorEncoder::hasEdge(uint64_t i, uint64_t j) { int main(int argc, char* argv[]) { DeltaManager deltaManager(3000, 10, 6); + //DeltaManager deltaManager(20, 10, 6); std::vector d; - d.push_back(1100); - d.push_back(1200); - d.push_back(1300); + d.push_back(11); + d.push_back(2999); + d.push_back(13); deltaManager.insertDeltas(0, d); d.clear(); - d.push_back(2999); + d.push_back(2); deltaManager.insertDeltas(1, d); d.clear(); - d.push_back(100); - d.push_back(200); - d.push_back(300); - d.push_back(400); - d.push_back(500); - d.push_back(600); - /*d.push_back(700); - d.push_back(800); - d.push_back(900); - d.push_back(1000); - d.push_back(1100);*/ + d.push_back(1); + d.push_back(2); + d.push_back(3); + d.push_back(4); + d.push_back(5); + d.push_back(6); + d.push_back(7); + d.push_back(8); + d.push_back(9); + d.push_back(10); + d.push_back(11); deltaManager.insertDeltas(2, d); auto vec = deltaManager.getDeltas(0); From 2be1538d06e9a73bc557b7e33f37ca5028183d9f Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 21 Aug 2018 18:55:42 -0400 Subject: [PATCH 093/122] lowercasing name of deltaManager file --- include/ColorEncoder.h | 41 ------ include/{DeltaManager.h => deltaManager.h} | 0 src/CMakeLists.txt | 2 +- src/ColorEncoder.cc | 146 --------------------- 4 files changed, 1 insertion(+), 188 deletions(-) delete mode 100644 include/ColorEncoder.h rename include/{DeltaManager.h => deltaManager.h} (100%) delete mode 100644 src/ColorEncoder.cc diff --git a/include/ColorEncoder.h b/include/ColorEncoder.h deleted file mode 100644 index 0aa42b9..0000000 --- a/include/ColorEncoder.h +++ /dev/null @@ -1,41 +0,0 @@ -// -// Created by Fatemeh Almodaresi on 8/17/18. -// - -#ifndef MANTIS_COLORENCODER_H -#define MANTIS_COLORENCODER_H - -#include -#include "lru/lru.hpp" -#include "DeltaManager.h" - -using LRUCacheMap = LRU::Cache>; - -class ColorEncoder { -public: - ColorEncoder(uint64_t numSamplesIn) : numSamples(numSamplesIn), - deltaM(numSamplesIn, 222500000, 8) {} - void addEdge(uint64_t i, uint64_t j, uint32_t w); - bool hasEdge(uint64_t i, uint64_t j); - std::vector buildColor(uint64_t eqid); - DeltaManager deltaM; - -private: - uint64_t numSamples; - - struct pair_hash { - template - std::size_t operator () (const std::pair &p) const { - auto h1 = std::hash{}(p.first); - auto h2 = std::hash{}(p.second); - - // Mainly for demonstration purposes, i.e. works but is overly simple - // In the real world, use sth. like boost.hash_combine - return h1 ^ h2; - } - }; - std::unordered_map, uint32_t, pair_hash> edges; - LRU::Cache > lru_cache; - //DeltaManager deltas; -}; -#endif //MANTIS_COLORENCODER_H diff --git a/include/DeltaManager.h b/include/deltaManager.h similarity index 100% rename from include/DeltaManager.h rename to include/deltaManager.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dcd0f1e..557c756 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -100,7 +100,7 @@ target_link_libraries(walkCqf mantis_core) set_property(TARGET walkCqf PROPERTY INTERPROCEDURAL_OPTIMIZATION True) -add_executable(colorEncoder ColorEncoder.cc) +add_executable(colorEncoder colorEncoder.cc) target_include_directories(colorEncoder PUBLIC $) target_link_libraries(colorEncoder diff --git a/src/ColorEncoder.cc b/src/ColorEncoder.cc deleted file mode 100644 index ea4c523..0000000 --- a/src/ColorEncoder.cc +++ /dev/null @@ -1,146 +0,0 @@ -// -// Created by Fatemeh Almodaresi on 8/17/18. -// - -#include "ColorEncoder.h" -#include - -std::vector ColorEncoder::buildColor(uint64_t eqid) { - std::vector flips(numSamples); - std::vector xorflips(numSamples, 0); -/* - uint64_t i{eqid}, from{0}, to{0}; - bool foundCache = false; - uint32_t iparent = parentbv[i]; - while (iparent != i) { - if (lru_cache.contains(i)) { - const auto &vs = lru_cache[i]; - for (auto v : vs) { - xorflips[v] = 1; - } - foundCache = true; - break; - } - from = (i > 0) ? (sbbv(i) + 1) : 0; - froms.push_back(from); - - i = iparent; - iparent = parentbv[i]; - } - if (!foundCache and i != zero) { - from = (i > 0) ? (sbbv(i) + 1) : 0; - froms.push_back(from); - } - - uint64_t pctr{0}; - for (auto f : froms) { - bool found = false; - uint64_t wrd{0}; - //auto j = f; - uint64_t offset{0}; - auto start = f; - do { - wrd = bbv.get_int(start, 64); - for (uint64_t j = 0; j < 64; j++) { - flips[deltabv[start + j]] ^= 0x01; - if ((wrd >> j) & 0x01) { - found = true; - break; - } - } - start += 64; - } while (!found); - } -*/ - - // return the indices of set bits - std::vector eq; - uint64_t numWrds = numSamples/64+1; - eq.reserve(numWrds); - uint64_t one = 1; - for (auto i = 0; i < numSamples; i++) { - if (flips[i] ^ xorflips[i]) { - eq.push_back(i); - } - } - return eq; -} - -void ColorEncoder::addEdge(uint64_t i, uint64_t j, uint32_t w) { - if (i == j) return; - if (i > j) { - std::swap(i,j); - } - if (edges.find(std::make_pair(i, j)) == edges.end()) { - edges[std::make_pair(i, j)] = w; - } -} - -bool ColorEncoder::hasEdge(uint64_t i, uint64_t j) { - if (i > j) { - std::swap(i, j); - } - return i == j or edges.find(std::make_pair(i, j)) != edges.end(); -} - -int main(int argc, char* argv[]) { - DeltaManager deltaManager(3000, 10, 6); - //DeltaManager deltaManager(20, 10, 6); - std::vector d; - d.push_back(11); - d.push_back(2999); - d.push_back(13); - deltaManager.insertDeltas(0, d); - - d.clear(); - d.push_back(2); - deltaManager.insertDeltas(1, d); - - d.clear(); - d.push_back(1); - d.push_back(2); - d.push_back(3); - d.push_back(4); - d.push_back(5); - d.push_back(6); - d.push_back(7); - d.push_back(8); - d.push_back(9); - d.push_back(10); - d.push_back(11); - deltaManager.insertDeltas(2, d); - - auto vec = deltaManager.getDeltas(0); - std::cerr << "\n\ndeltas for 0\n"; - for (auto v : vec) { - std::cerr << v << " "; - } - - std::cerr << "\n"; - vec = deltaManager.getDeltas(1); - std::cerr << "\n\ndeltas for 1\n"; - for (auto v : vec) { - std::cerr << v << " "; - } - - vec = deltaManager.getDeltas(2); - std::cerr << "\n\ndeltas for 2\n"; - for (auto v : vec) { - std::cerr << v << " "; - } - - deltaManager.swapDeltas(0,2); - std::cerr << "\nAfter swap:\n"; - vec = deltaManager.getDeltas(0); - std::cerr << "\n\ndeltas for 0\n"; - for (auto v : vec) { - std::cerr << v << " "; - } - - vec = deltaManager.getDeltas(2); - std::cerr << "\n\ndeltas for 2\n"; - for (auto v : vec) { - std::cerr << v << " "; - } - std::cerr << "\n"; -} \ No newline at end of file From b48477e4b7bff12c7834cc21d78c582f29210419 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 21 Aug 2018 18:56:54 -0400 Subject: [PATCH 094/122] Adding the initial skeleton for colorEncoding dynamically. --- include/canonKmer.h | 256 +++++++++++++++++++++++++++++++++++++++++ include/colorEncoder.h | 78 +++++++++++++ src/colorEncoder.cc | 203 ++++++++++++++++++++++++++++++++ 3 files changed, 537 insertions(+) create mode 100644 include/canonKmer.h create mode 100644 include/colorEncoder.h create mode 100644 src/colorEncoder.cc diff --git a/include/canonKmer.h b/include/canonKmer.h new file mode 100644 index 0000000..6f4d5ce --- /dev/null +++ b/include/canonKmer.h @@ -0,0 +1,256 @@ +// +// Created by Fatemeh Almodaresi on 8/21/18. +// + +#ifndef MANTIS_CANONKMER_H +#define MANTIS_CANONKMER_H +namespace dna { + +/////////////// bases ///////////////// + enum base { + C = 0, A = 1, T = 2, G = 3 + }; + + base operator-(base b); // return the complementary base + extern const base bases[4]; + extern const std::map base_from_char; + extern const std::map base_to_char; + +///////////// kmers ///////////////////// + class kmer { + public: + int len; + uint64_t val; + + kmer(void); + + kmer(base b); + + kmer(int l, uint64_t v); + + kmer(std::string s); + + // Convert to string + operator std::string() const; + }; + + bool operator<(kmer a, kmer b); + + bool operator==(kmer a, kmer b); + + bool operator!=(kmer a, kmer b); + +// Return the reverse complement of k + kmer operator-(kmer k); + + kmer canonicalize(kmer k); + +// Return the kmer of length |a| that results from shifting b into a +// from the right + kmer operator<<(kmer a, kmer b); + +// Return the kmer of length |b| that results from shifting a into b +// from the left + kmer operator>>(kmer a, kmer b); + +// Append two kmers + kmer operator+(kmer a, kmer b); + + kmer suffix(kmer k, int len); + + kmer prefix(kmer k, int len); + +// The purpose of this class is to enable us to declare containers +// as holding canonical kmers, e.g. set. Then all +// inserts/queries/etc will automatically canonicalize their +// arguments. + class canonical_kmer : public kmer { + public: + canonical_kmer(void); + + canonical_kmer(base b); + + canonical_kmer(int l, uint64_t v); + + canonical_kmer(std::string s); + + canonical_kmer(kmer k); + }; + + /////////////// bases ///////////////// + base operator-(base b) { + return (base) ((~((uint64_t) b)) & 0x3ULL); + } + + const base bases[4] = {C, A, T, G}; + const std::map base_from_char = {{'A', A}, + {'C', C}, + {'G', G}, + {'T', T}, + {'N', A}}; + const std::map base_to_char = {{A, 'A'}, + {C, 'C'}, + {G, 'G'}, + {T, 'T'}}; + + ///////////// kmers ///////////////////// + kmer::kmer(void) : len(0), val(0) {} + + kmer::kmer(base b) : len(1), val((uint64_t) b) {} + + kmer::kmer(int l, uint64_t v) : len(l), val(v & BITMASK(2 * l)) { + assert(l <= 32); + } + + static uint64_t string_to_kmer_val(std::string s) { + uint64_t val = 0; + for (auto c : s) + val = (val << 2) | ((uint64_t)(base_from_char.at(c))); + return val; + } + + kmer::kmer(std::string s) : len(s.size()), val(string_to_kmer_val(s)) { + assert(s.size() <= 32); + } + + // Convert to string + kmer::operator std::string() const { + std::string s; + for (auto i = 1; i < len + 1; i++) + s = s + base_to_char.at((base) ((val >> (2 * (len - i))) & BITMASK(2))); + return s; + } + + bool operator<(kmer a, kmer b) { + return a.len != b.len ? a.len < b.len : a.val < b.val; + } + + bool operator==(kmer a, kmer b) { + return a.len == b.len && a.val == b.val; + } + + bool operator!=(kmer a, kmer b) { + return !operator==(a, b); + } + + // Return the reverse complement of k + kmer operator-(kmer k) { + uint64_t val = k.val; + val = + (val >> 32) | + (val << 32); + val = + ((val >> 16) & 0x0000ffff0000ffff) | + ((val << 16) & 0xffff0000ffff0000); + val = + ((val >> 8) & 0x00ff00ff00ff00ff) | + ((val << 8) & 0xff00ff00ff00ff00); + val = + ((val >> 4) & 0x0f0f0f0f0f0f0f0f) | + ((val << 4) & 0xf0f0f0f0f0f0f0f0); + val = + ((val >> 2) & 0x3333333333333333) | + ((val << 2) & 0xcccccccccccccccc); + val = ~val; + val >>= 64 - 2 * k.len; + return kmer(k.len, val); + } + + // backwards from standard definition to match kmer.h definition + kmer canonicalize(kmer k) { + return -k < k ? k : -k; + } + + // Return the kmer of length |a| that results from shifting b into a + // from the right + kmer operator<<(kmer a, kmer b) { + uint64_t val = ((a.val << (2 * b.len)) | b.val) & BITMASK(2 * a.len); + return kmer(a.len, val); + } + + // Return the kmer of length |b| that results from shifting b into a + // from the left + kmer operator>>(kmer a, kmer b) { + uint64_t val + = ((b.val >> (2 * a.len)) | (a.val << (2 * (b.len - a.len)))) + & BITMASK(2 * b.len); + return kmer(b.len, val); + } + + // Append two kmers + kmer operator+(kmer a, kmer b) { + int len = a.len + b.len; + assert(len <= 32); + uint64_t val = (a.val << (2 * b.len)) | b.val; + return kmer(len, val); + } + + kmer prefix(kmer k, int len) { return kmer(len, k.val >> (2 * (k.len - len))); } + + kmer suffix(kmer k, int len) { return kmer(len, k.val & BITMASK(2 * len)); } + + bool period_divides(kmer k, uint64_t periodicity) { + static const uint64_t multipliers[33] = + { + 0, + 0x5555555555555555, // 1 + 0x1111111111111111, // 2 + 0x1041041041041041, // 3 + 0x0101010101010101, // 4 + 0x1004010040100401, // 5 + 0x1001001001001001, // 6 + 0x0100040010004001, // 7 + 0x0001000100010001, // 8 + 0x0040001000040001, // 9 + 0x1000010000100001, // 10 + 0x0000100000400001, // 11 + 0x0001000001000001, // 12 + 0x0010000004000001, // 13 + 0x0100000010000001, // 14 + 0x1000000040000001, // 15 + 0x0000000100000001, // 16 + 0x0000000400000001, // 17 + 0x0000001000000001, // 18 + 0x0000004000000001, // 19 + 0x0000010000000001, // 20 + 0x0000040000000001, // 21 + 0x0000100000000001, // 22 + 0x0000400000000001, // 23 + 0x0001000000000001, // 24 + 0x0004000000000001, // 25 + 0x0010000000000001, // 26 + 0x0040000000000001, // 27 + 0x0100000000000001, // 28 + 0x0400000000000001, // 29 + 0x1000000000000001, // 30 + 0x4000000000000001, // 31 + 0x0000000000000001, // 32 + }; + uint64_t piece = k.val & BITMASK(2 * periodicity); + piece = piece * multipliers[periodicity]; + piece = piece & BITMASK(2 * k.len); + return piece == k.val; + } + + uint64_t period(kmer k) { + for (int i = 1; i <= k.len; i++) { + if (period_divides(k, i)) + return i; + } + abort(); + } + + canonical_kmer::canonical_kmer(void) : kmer() {} + + canonical_kmer::canonical_kmer(base b) : kmer(canonicalize(kmer(b))) {} + + canonical_kmer::canonical_kmer(int l, uint64_t v) + : kmer(canonicalize(kmer(l, v))) {} + + canonical_kmer::canonical_kmer(std::string s) : kmer(canonicalize(kmer(s))) {} + + canonical_kmer::canonical_kmer(kmer k) : kmer(canonicalize(k)) {} + +} + +#endif //MANTIS_CANONKMER_H diff --git a/include/colorEncoder.h b/include/colorEncoder.h new file mode 100644 index 0000000..907cf0c --- /dev/null +++ b/include/colorEncoder.h @@ -0,0 +1,78 @@ +// +// Created by Fatemeh Almodaresi on 8/17/18. +// + +#ifndef MANTIS_COLORENCODER_H +#define MANTIS_COLORENCODER_H + +#include +#include +#include "lru/lru.hpp" +#include "deltaManager.h" +#include "bitvector.h" +#include "cqf.h" +#include "canonKmer.h" + +using LRUCacheMap = LRU::Cache>; +typedef std::pair node; +constexpr uint64_t zero = 0; + +struct pair_hash { + template + std::size_t operator()(const std::pair &p) const { + auto h1 = std::hash{}(p.first); + auto h2 = std::hash{}(p.second); + + // Mainly for demonstration purposes, i.e. works but is overly simple + // In the real world, use sth. like boost.hash_combine + return h1 ^ h2; + } +}; + +class ColorEncoder { +public: + ColorEncoder(uint64_t numSamplesIn, + CQF &cqfIn, + uint64_t approximateClrClsesIn, + uint64_t approximateDeltaCntPerClrCls = 8) : + numSamples(numSamplesIn), + cqf(cqfIn), + bvSize(approximateClrClsesIn), + parentbv(bvSize, 0, log2((double)bvSize)+5),//TODO take care of this constant!! + deltaM(numSamplesIn, bvSize, approximateDeltaCntPerClrCls), + colorClsCnt(0) {} + + + bool addColorClass(uint64_t kmer, uint64_t eqId, const std::vector bv); + +private: + uint64_t numSamples; + uint64_t bvSize; + uint64_t colorClsCnt; + sdsl::int_vector<> parentbv; + DeltaManager deltaM; + CQF &cqf; + int k = 20; + std::unordered_map, uint32_t, pair_hash> edges; + LRU::Cache> lru_cache; + + void addEdge(uint64_t i, uint64_t j, uint32_t w); + + bool hasEdge(uint64_t i, uint64_t j); + + std::vector buildColor(uint64_t eqid); + + bool updateMST(uint64_t n1, uint64_t n2, std::vector deltas); + + std::vector hammingDist(uint64_t i, uint64_t j); + + std::unordered_set neighbors(dna::canonical_kmer n); + + bool exists(dna::canonical_kmer e, uint64_t &eqid); + + void maxWeightsTillLCA(uint64_t n1, uint64_t n2, + uint64_t &w1, uint64_t &w2, + uint64_t &p1, uint64_t &p2); +}; + +#endif //MANTIS_COLORENCODER_H diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc new file mode 100644 index 0000000..293b1a6 --- /dev/null +++ b/src/colorEncoder.cc @@ -0,0 +1,203 @@ +// +// Created by Fatemeh Almodaresi on 8/17/18. +// + +#include "colorEncoder.h" +#include + +bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const std::vector bv) { + // create list of edges to be processed + // 1. list of neighbors + // 2. zero to node + // calc. the distance between the node and any of the neighbors + // that exist and the edge hasn't been seen + dna::canonical_kmer cur(k, kmer); + std::unordered_set, pair_hash> newEdges; + // case 1. edges between the node and its neighbors + for (auto nei_eqId : neighbors(cur)) { + uint64_t cur_eqId{eqId}; + if (nei_eqId != cur_eqId) { + if (nei_eqId < cur_eqId) { + std::swap(nei_eqId, cur_eqId); + } + if (!hasEdge(cur_eqId, nei_eqId)) { + newEdges.insert(std::make_pair(cur_eqId, nei_eqId)); + } + } + } + // case 2. edge from zero to the node + if (!hasEdge(zero, eqId)) { + newEdges.insert(std::make_pair(zero, eqId)); + } + for (auto &newEdge : newEdges) { + auto deltas = hammingDist(newEdge.first, newEdge.second); + updateMST(newEdge.first, newEdge.second, deltas); + addEdge(newEdge.first, newEdge.second, deltas.size()); + } + if (newEdges.size()) + return true; + return false; +} + +// deltas should *NOT* be passed by reference +bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector deltas) { // n2 > n1 + // If n2 is a new color Id (next color Id) + if (n2 == colorClsCnt + 1) { + parentbv[n2] = n1; + deltaM.insertDeltas(n2, deltas); + colorClsCnt++; + if (parentbv.size() == colorClsCnt) { + parentbv.resize(parentbv.size()+parentbv.size()/2); + } + return true; + } + // find the max weight edge from each of the ends to their lca, called w1, w2 + uint64_t w1, w2, p1, p2; + uint64_t w = deltas.size(); + maxWeightsTillLCA(n1, n2, w1, w2, p1, p2); + // if w >= w1 and w >= w2, happily do nothing + if (w >= w1 and w >= w2) + return false; + if (w1 > w2) { + std::swap(w1, w2); + std::swap(p1, p2); + } + // Now we know that we're gonna add the edge n1 -> n2 + // and remove edge with weight w2 along the path from n2 to the LCA + // update parentbv and deltas for n2 + // change the parent/child relationship + // starting from node n2 toward the c2, child node of the edge with weight w2 + auto parent = n1; + auto child = n2; + while (child != p2) { + auto tmp = parentbv[child]; + parentbv[child] = parent; + auto prevDeltas = deltaM.getDeltas(child); + deltaM.insertDeltas(child, deltas); + deltas = prevDeltas; + parent = n2; + child = tmp; + } + return true; +} + +std::vector ColorEncoder::buildColor(uint64_t eqid) { + std::vector flips(numSamples); + std::vector xorflips(numSamples, 0); +/* + uint64_t i{eqid}, from{0}, to{0}; + bool foundCache = false; + uint32_t iparent = parentbv[i]; + while (iparent != i) { + if (lru_cache.contains(i)) { + const auto &vs = lru_cache[i]; + for (auto v : vs) { + xorflips[v] = 1; + } + foundCache = true; + break; + } + from = (i > 0) ? (sbbv(i) + 1) : 0; + froms.push_back(from); + + i = iparent; + iparent = parentbv[i]; + } + if (!foundCache and i != zero) { + from = (i > 0) ? (sbbv(i) + 1) : 0; + froms.push_back(from); + } + + uint64_t pctr{0}; + for (auto f : froms) { + bool found = false; + uint64_t wrd{0}; + //auto j = f; + uint64_t offset{0}; + auto start = f; + do { + wrd = bbv.get_int(start, 64); + for (uint64_t j = 0; j < 64; j++) { + flips[deltabv[start + j]] ^= 0x01; + if ((wrd >> j) & 0x01) { + found = true; + break; + } + } + start += 64; + } while (!found); + } +*/ + + // return the indices of set bits + std::vector eq; + uint64_t numWrds = numSamples/64+1; + eq.reserve(numWrds); + uint64_t one = 1; + for (auto i = 0; i < numSamples; i++) { + if (flips[i] ^ xorflips[i]) { + eq.push_back(i); + } + } + return eq; +} + +std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { + std::vector res{0}; + // If i is the dummy node zero + // ow + return res; +} + +void ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2, + uint64_t &w1, uint64_t &w2, + uint64_t &p1, uint64_t &p2) { +} + +std::unordered_set ColorEncoder::neighbors(dna::canonical_kmer n) { + std::unordered_set result; + for (const auto b : dna::bases) { + uint64_t eqid, idx; + if (exists(b >> n, eqid)) + result.insert(eqid); + if (exists(n << b, eqid)) + result.insert(eqid); + } + return result; +} + +bool ColorEncoder::exists(dna::canonical_kmer e, uint64_t &eqid) { + uint64_t tmp = e.val; + KeyObject key(HashUtil::hash_64(tmp, BITMASK(cqf.keybits())), 0, 0); + auto eq_idx = cqf.query(key); + return eq_idx != 0; + // commenting this line, eqIds start from 1 (rather than 0) +/* + if (eq_idx) { + eqid = eq_idx - 1; //note be careful about this -1 here. It'll change many things + return true; + } + return false; +*/ +} + +void ColorEncoder::addEdge(uint64_t i, uint64_t j, uint32_t w) { + if (i == j) return; + if (i > j) { + std::swap(i,j); + } + if (edges.find(std::make_pair(i, j)) == edges.end()) { + edges[std::make_pair(i, j)] = w; + } +} + +bool ColorEncoder::hasEdge(uint64_t i, uint64_t j) { + if (i > j) { + std::swap(i, j); + } + return i == j or edges.find(std::make_pair(i, j)) != edges.end(); +} + +int main(int argc, char* argv[]) { + +} \ No newline at end of file From 320fae76b11326b08a90e4be02e78d388fc50a6b Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 21 Aug 2018 21:04:21 -0400 Subject: [PATCH 095/122] Adding hammingDistance implementation --- include/colorEncoder.h | 3 ++- src/colorEncoder.cc | 40 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/include/colorEncoder.h b/include/colorEncoder.h index 907cf0c..e840646 100644 --- a/include/colorEncoder.h +++ b/include/colorEncoder.h @@ -40,7 +40,8 @@ class ColorEncoder { bvSize(approximateClrClsesIn), parentbv(bvSize, 0, log2((double)bvSize)+5),//TODO take care of this constant!! deltaM(numSamplesIn, bvSize, approximateDeltaCntPerClrCls), - colorClsCnt(0) {} + colorClsCnt(1) // start with the dummy node + {} bool addColorClass(uint64_t kmer, uint64_t eqId, const std::vector bv); diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index 293b1a6..a1bfd7f 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -4,6 +4,8 @@ #include "colorEncoder.h" #include +#include +#include bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const std::vector bv) { // create list of edges to be processed @@ -42,7 +44,7 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const std::vector // deltas should *NOT* be passed by reference bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector deltas) { // n2 > n1 // If n2 is a new color Id (next color Id) - if (n2 == colorClsCnt + 1) { + if (n2 == colorClsCnt) { parentbv[n2] = n1; deltaM.insertDeltas(n2, deltas); colorClsCnt++; @@ -81,7 +83,13 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del return true; } +// returns list of set bits std::vector ColorEncoder::buildColor(uint64_t eqid) { + std::vector eq; + if (eqid == zero) { // if dummy node, return an empty list (no set bits) + return eq; + } + std::vector flips(numSamples); std::vector xorflips(numSamples, 0); /* @@ -130,7 +138,6 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { */ // return the indices of set bits - std::vector eq; uint64_t numWrds = numSamples/64+1; eq.reserve(numWrds); uint64_t one = 1; @@ -144,8 +151,33 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { std::vector res{0}; - // If i is the dummy node zero - // ow + auto n1 = buildColor(i); + auto n2 = buildColor(j); + // merge + // with slight difference of not inserting values that appear in both vectors + uint64_t i1{0}, i2{0}; + while (i1 < n1.size() or i2 < n2.size()) { + if (i1 == n1.size()) { + copy(n2.begin()+i2, n2.end(), back_inserter(res)); + i2 = n2.size(); + } + else if (i2 == n2.size()) { + copy(n1.begin()+i1, n1.end(), back_inserter(res)); + i1 = n1.size(); + } + else { + if (n1[i1] < n2[i2]) { + res.push_back(n1[i1]); + i1++; + } else if (n2[i2] < n1[i1]) { + res.push_back(n2[i2]); + i2++; + } else { //n1[i1] == n2[i2] both have a set bit at the same index + i1++; + i2++; + } + } + } return res; } From d4632a95d9be90bdaefafd24590aa796ee816dd5 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 21 Aug 2018 22:14:03 -0400 Subject: [PATCH 096/122] Adding buildColor implementation --- src/colorEncoder.cc | 44 +++++++++++++------------------------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index a1bfd7f..d92e18d 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -86,17 +86,20 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del // returns list of set bits std::vector ColorEncoder::buildColor(uint64_t eqid) { std::vector eq; - if (eqid == zero) { // if dummy node, return an empty list (no set bits) + if (eqid == zero) { // if dummy node "zero", return an empty list (no set bits) return eq; } + uint64_t numWrds = numSamples/64+1; + eq.reserve(numWrds); std::vector flips(numSamples); std::vector xorflips(numSamples, 0); -/* - uint64_t i{eqid}, from{0}, to{0}; + uint64_t i{eqid}; + std::vector deltaIndices; + deltaIndices.reserve(numWrds); bool foundCache = false; uint32_t iparent = parentbv[i]; - while (iparent != i) { + while (i != zero) { if (lru_cache.contains(i)) { const auto &vs = lru_cache[i]; for (auto v : vs) { @@ -105,41 +108,20 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { foundCache = true; break; } - from = (i > 0) ? (sbbv(i) + 1) : 0; - froms.push_back(from); - + deltaIndices.push_back(i); i = iparent; iparent = parentbv[i]; } - if (!foundCache and i != zero) { - from = (i > 0) ? (sbbv(i) + 1) : 0; - froms.push_back(from); - } uint64_t pctr{0}; - for (auto f : froms) { - bool found = false; - uint64_t wrd{0}; - //auto j = f; - uint64_t offset{0}; - auto start = f; - do { - wrd = bbv.get_int(start, 64); - for (uint64_t j = 0; j < 64; j++) { - flips[deltabv[start + j]] ^= 0x01; - if ((wrd >> j) & 0x01) { - found = true; - break; - } - } - start += 64; - } while (!found); + for (auto index : deltaIndices) { + auto deltas = deltaM.getDeltas(index); + for (auto d : deltas) { + flips[d] ^= 0x01; + } } -*/ // return the indices of set bits - uint64_t numWrds = numSamples/64+1; - eq.reserve(numWrds); uint64_t one = 1; for (auto i = 0; i < numSamples; i++) { if (flips[i] ^ xorflips[i]) { From 2c68c42da6f73ae8b469fceaec239543c2cef3d9 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Tue, 21 Aug 2018 22:54:15 -0400 Subject: [PATCH 097/122] Adding maxWeightsTillLCA implementation --- include/colorEncoder.h | 26 +++++++++++----- src/colorEncoder.cc | 69 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/include/colorEncoder.h b/include/colorEncoder.h index e840646..7913046 100644 --- a/include/colorEncoder.h +++ b/include/colorEncoder.h @@ -17,6 +17,18 @@ using LRUCacheMap = LRU::Cache>; typedef std::pair node; constexpr uint64_t zero = 0; +struct Edge { + uint64_t parent; + uint64_t child; + uint64_t weight; + + Edge() { + parent = 0; child = 0; weight = 0; + } + Edge(uint64_t inParent, uint64_t inChild, uint64_t inWeight) : + parent(inParent), child(inChild), weight(inWeight) {} +}; + struct pair_hash { template std::size_t operator()(const std::pair &p) const { @@ -57,10 +69,6 @@ class ColorEncoder { std::unordered_map, uint32_t, pair_hash> edges; LRU::Cache> lru_cache; - void addEdge(uint64_t i, uint64_t j, uint32_t w); - - bool hasEdge(uint64_t i, uint64_t j); - std::vector buildColor(uint64_t eqid); bool updateMST(uint64_t n1, uint64_t n2, std::vector deltas); @@ -71,9 +79,13 @@ class ColorEncoder { bool exists(dna::canonical_kmer e, uint64_t &eqid); - void maxWeightsTillLCA(uint64_t n1, uint64_t n2, - uint64_t &w1, uint64_t &w2, - uint64_t &p1, uint64_t &p2); + std::pair maxWeightsTillLCA(uint64_t n1, uint64_t n2); + + void addEdge(uint64_t i, uint64_t j, uint32_t w); + + bool hasEdge(uint64_t i, uint64_t j); + + uint32_t getEdge(uint64_t i, uint64_t j); }; #endif //MANTIS_COLORENCODER_H diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index d92e18d..03a4873 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -56,7 +56,11 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del // find the max weight edge from each of the ends to their lca, called w1, w2 uint64_t w1, w2, p1, p2; uint64_t w = deltas.size(); - maxWeightsTillLCA(n1, n2, w1, w2, p1, p2); + std::pair lr = maxWeightsTillLCA(n1, n2); + w1 = lr.first.weight; + w2 = lr.second.weight; + p1 = lr.first.parent; + p2 = lr.second.parent; // if w >= w1 and w >= w2, happily do nothing if (w >= w1 and w >= w2) return false; @@ -163,9 +167,59 @@ std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { return res; } -void ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2, - uint64_t &w1, uint64_t &w2, - uint64_t &p1, uint64_t &p2) { +//TODO we definitely need to discuss this. This function in its current way is super inefficient +// time/space tradeoff +std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) { + std::vector nodes1; + std::vector nodes2; + auto n = n1; + while (n != zero) { + nodes1.push_back(n); + n = parentbv[n]; + } + n = n2; + while (n != zero) { + nodes2.push_back(n); + n = parentbv[n]; + } + + auto &n1ref = nodes1; + auto &n2ref = nodes2; + if (n1ref.size() < n2ref.size()) { + std::swap(n1ref, n2ref); + } + + // find lca + uint64_t lca = 0; + for (uint64_t j = 0, i = n1ref.size()-n2ref.size(); j < n2ref.size(); i++, j++) { + if (n1ref[i] == n2ref[j]) { + lca = n1ref[i]; + break; + } + } + + // walk from n1 to lca and find the edge with maximum weight + Edge e1; + uint64_t i = 0; + while (n1ref[i] != lca) { + auto curW = getEdge(n1ref[i], n1ref[i+1]); + if (e1.weight < curW) { + e1 = Edge(n1ref[i+1], n1ref[i], curW); + } + i++; + } + + // walk from n2 to lca and find the edge with maximum weight + Edge e2; + i = 0; + while (n2ref[i] != lca) { + auto curW = getEdge(n2ref[i], n2ref[i+1]); + if (e2.weight < curW) { + e2 = Edge(n2ref[i+1], n2ref[i], curW); + } + i++; + } + return std::make_pair(e1, e2); } std::unordered_set ColorEncoder::neighbors(dna::canonical_kmer n) { @@ -212,6 +266,13 @@ bool ColorEncoder::hasEdge(uint64_t i, uint64_t j) { return i == j or edges.find(std::make_pair(i, j)) != edges.end(); } +uint32_t ColorEncoder::getEdge(uint64_t i, uint64_t j) { + if (i > j) { + std::swap(i, j); + } + return edges[std::make_pair(i, j)]; +} + int main(int argc, char* argv[]) { } \ No newline at end of file From 4e6b7a411e4c97e4d038916a0c620c5a4032daf4 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Wed, 22 Aug 2018 02:43:28 -0400 Subject: [PATCH 098/122] Calling colorEncoder from inside add_kmer in coloreddbg --- include/canonKmer.h | 186 +++-------------------------------------- include/colorEncoder.h | 14 ++-- include/coloreddbg.h | 49 ++++++++--- src/CMakeLists.txt | 11 +-- src/canonKmer.cc | 181 +++++++++++++++++++++++++++++++++++++++ src/colorEncoder.cc | 52 ++++++++---- 6 files changed, 276 insertions(+), 217 deletions(-) create mode 100644 src/canonKmer.cc diff --git a/include/canonKmer.h b/include/canonKmer.h index 6f4d5ce..661d567 100644 --- a/include/canonKmer.h +++ b/include/canonKmer.h @@ -4,7 +4,16 @@ #ifndef MANTIS_CANONKMER_H #define MANTIS_CANONKMER_H -namespace dna { + +#include +#include +#include +#include + +#define BITMASK(nbits) ((nbits) == 64 ? 0xffffffffffffffff : (1ULL << (nbits)) \ + - 1ULL) + +namespace duplicated_dna { /////////////// bases ///////////////// enum base { @@ -76,181 +85,6 @@ namespace dna { canonical_kmer(kmer k); }; - - /////////////// bases ///////////////// - base operator-(base b) { - return (base) ((~((uint64_t) b)) & 0x3ULL); - } - - const base bases[4] = {C, A, T, G}; - const std::map base_from_char = {{'A', A}, - {'C', C}, - {'G', G}, - {'T', T}, - {'N', A}}; - const std::map base_to_char = {{A, 'A'}, - {C, 'C'}, - {G, 'G'}, - {T, 'T'}}; - - ///////////// kmers ///////////////////// - kmer::kmer(void) : len(0), val(0) {} - - kmer::kmer(base b) : len(1), val((uint64_t) b) {} - - kmer::kmer(int l, uint64_t v) : len(l), val(v & BITMASK(2 * l)) { - assert(l <= 32); - } - - static uint64_t string_to_kmer_val(std::string s) { - uint64_t val = 0; - for (auto c : s) - val = (val << 2) | ((uint64_t)(base_from_char.at(c))); - return val; - } - - kmer::kmer(std::string s) : len(s.size()), val(string_to_kmer_val(s)) { - assert(s.size() <= 32); - } - - // Convert to string - kmer::operator std::string() const { - std::string s; - for (auto i = 1; i < len + 1; i++) - s = s + base_to_char.at((base) ((val >> (2 * (len - i))) & BITMASK(2))); - return s; - } - - bool operator<(kmer a, kmer b) { - return a.len != b.len ? a.len < b.len : a.val < b.val; - } - - bool operator==(kmer a, kmer b) { - return a.len == b.len && a.val == b.val; - } - - bool operator!=(kmer a, kmer b) { - return !operator==(a, b); - } - - // Return the reverse complement of k - kmer operator-(kmer k) { - uint64_t val = k.val; - val = - (val >> 32) | - (val << 32); - val = - ((val >> 16) & 0x0000ffff0000ffff) | - ((val << 16) & 0xffff0000ffff0000); - val = - ((val >> 8) & 0x00ff00ff00ff00ff) | - ((val << 8) & 0xff00ff00ff00ff00); - val = - ((val >> 4) & 0x0f0f0f0f0f0f0f0f) | - ((val << 4) & 0xf0f0f0f0f0f0f0f0); - val = - ((val >> 2) & 0x3333333333333333) | - ((val << 2) & 0xcccccccccccccccc); - val = ~val; - val >>= 64 - 2 * k.len; - return kmer(k.len, val); - } - - // backwards from standard definition to match kmer.h definition - kmer canonicalize(kmer k) { - return -k < k ? k : -k; - } - - // Return the kmer of length |a| that results from shifting b into a - // from the right - kmer operator<<(kmer a, kmer b) { - uint64_t val = ((a.val << (2 * b.len)) | b.val) & BITMASK(2 * a.len); - return kmer(a.len, val); - } - - // Return the kmer of length |b| that results from shifting b into a - // from the left - kmer operator>>(kmer a, kmer b) { - uint64_t val - = ((b.val >> (2 * a.len)) | (a.val << (2 * (b.len - a.len)))) - & BITMASK(2 * b.len); - return kmer(b.len, val); - } - - // Append two kmers - kmer operator+(kmer a, kmer b) { - int len = a.len + b.len; - assert(len <= 32); - uint64_t val = (a.val << (2 * b.len)) | b.val; - return kmer(len, val); - } - - kmer prefix(kmer k, int len) { return kmer(len, k.val >> (2 * (k.len - len))); } - - kmer suffix(kmer k, int len) { return kmer(len, k.val & BITMASK(2 * len)); } - - bool period_divides(kmer k, uint64_t periodicity) { - static const uint64_t multipliers[33] = - { - 0, - 0x5555555555555555, // 1 - 0x1111111111111111, // 2 - 0x1041041041041041, // 3 - 0x0101010101010101, // 4 - 0x1004010040100401, // 5 - 0x1001001001001001, // 6 - 0x0100040010004001, // 7 - 0x0001000100010001, // 8 - 0x0040001000040001, // 9 - 0x1000010000100001, // 10 - 0x0000100000400001, // 11 - 0x0001000001000001, // 12 - 0x0010000004000001, // 13 - 0x0100000010000001, // 14 - 0x1000000040000001, // 15 - 0x0000000100000001, // 16 - 0x0000000400000001, // 17 - 0x0000001000000001, // 18 - 0x0000004000000001, // 19 - 0x0000010000000001, // 20 - 0x0000040000000001, // 21 - 0x0000100000000001, // 22 - 0x0000400000000001, // 23 - 0x0001000000000001, // 24 - 0x0004000000000001, // 25 - 0x0010000000000001, // 26 - 0x0040000000000001, // 27 - 0x0100000000000001, // 28 - 0x0400000000000001, // 29 - 0x1000000000000001, // 30 - 0x4000000000000001, // 31 - 0x0000000000000001, // 32 - }; - uint64_t piece = k.val & BITMASK(2 * periodicity); - piece = piece * multipliers[periodicity]; - piece = piece & BITMASK(2 * k.len); - return piece == k.val; - } - - uint64_t period(kmer k) { - for (int i = 1; i <= k.len; i++) { - if (period_divides(k, i)) - return i; - } - abort(); - } - - canonical_kmer::canonical_kmer(void) : kmer() {} - - canonical_kmer::canonical_kmer(base b) : kmer(canonicalize(kmer(b))) {} - - canonical_kmer::canonical_kmer(int l, uint64_t v) - : kmer(canonicalize(kmer(l, v))) {} - - canonical_kmer::canonical_kmer(std::string s) : kmer(canonicalize(kmer(s))) {} - - canonical_kmer::canonical_kmer(kmer k) : kmer(canonicalize(k)) {} - } #endif //MANTIS_CANONKMER_H diff --git a/include/colorEncoder.h b/include/colorEncoder.h index 7913046..79e196f 100644 --- a/include/colorEncoder.h +++ b/include/colorEncoder.h @@ -9,12 +9,13 @@ #include #include "lru/lru.hpp" #include "deltaManager.h" -#include "bitvector.h" +#include "sdsl/bit_vectors.hpp" #include "cqf.h" #include "canonKmer.h" +#include "hashutil.h" using LRUCacheMap = LRU::Cache>; -typedef std::pair node; +typedef std::pair node; constexpr uint64_t zero = 0; struct Edge { @@ -43,6 +44,7 @@ struct pair_hash { class ColorEncoder { public: + ColorEncoder(uint64_t numSamplesIn, CQF &cqfIn, uint64_t approximateClrClsesIn, @@ -56,7 +58,7 @@ class ColorEncoder { {} - bool addColorClass(uint64_t kmer, uint64_t eqId, const std::vector bv); + bool addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_vector &bv); private: uint64_t numSamples; @@ -71,13 +73,15 @@ class ColorEncoder { std::vector buildColor(uint64_t eqid); + std::vector buildColor(const sdsl::bit_vector &bv); + bool updateMST(uint64_t n1, uint64_t n2, std::vector deltas); std::vector hammingDist(uint64_t i, uint64_t j); - std::unordered_set neighbors(dna::canonical_kmer n); + std::unordered_set neighbors(duplicated_dna::canonical_kmer n); - bool exists(dna::canonical_kmer e, uint64_t &eqid); + bool exists(duplicated_dna::canonical_kmer e, uint64_t &eqid); std::pair maxWeightsTillLCA(uint64_t n1, uint64_t n2); diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 9a35a2c..55bfe09 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -35,6 +35,7 @@ #include "hashutil.h" #include "common_types.h" #include "mantisconfig.hpp" +#include "colorEncoder.h" typedef sdsl::bit_vector BitVector; typedef sdsl::rrr_vector<63> BitVectorRRR; @@ -87,7 +88,7 @@ class ColoredDbg { private: // returns true if adding this k-mer increased the number of equivalence classes // and false otherwise. - bool add_kmer(const typename key_obj::kmer_t& hash, const BitVector& vector); + bool add_kmer(const typename key_obj::kmer_t& hash, const BitVector& vector, bool isSampling); void add_bitvector(const BitVector& vector, uint64_t eq_id); void add_eq_class(BitVector vector, uint64_t id); uint64_t get_next_available_id(void); @@ -106,8 +107,9 @@ class ColoredDbg { uint64_t num_serializations; bool flush_eqclass_dis{false}; - std::time_t start_time_; + std::time_t start_time_; spdlog::logger* console; + ColorEncoder* colorEncoder; }; template @@ -195,8 +197,9 @@ void ColoredDbg::reinit(cdbg_bv_map_t<__uint128_t, } template -bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, const BitVector& - vector) { +bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, + const BitVector& vector, + bool isSampling) { // A kmer (hash) is seen only once during the merge process. // So we insert every kmer in the dbg uint64_t eq_id; @@ -205,7 +208,29 @@ bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, 2038074751); auto it = eqclass_map.find(vec_hash); - bool added_eq_class{false}; + bool added_eq_class{false}; + // Find if the eqclass of the kmer is already there. + // If it is there then increment the abundance. + // Else create a new eq class. + if (it == eqclass_map.end()) { + // eq class is seen for the first time. + eq_id = get_next_available_id(); + eqclass_map.emplace(std::piecewise_construct, + std::forward_as_tuple(vec_hash), + std::forward_as_tuple(eq_id, 1)); + added_eq_class = true; + } else { // eq class is seen before so increment the abundance. + eq_id = it->second.first; + // with standard map + it->second.second += 1; // update the abundance. + } + + if (!isSampling) { + colorEncoder->addColorClass(key, eq_id, vector); + } + +/* auto it = eqclass_map.find(vec_hash); + bool added_eq_class{false}; // Find if the eqclass of the kmer is already there. // If it is there then increment the abundance. // Else create a new eq class. @@ -216,15 +241,15 @@ bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, std::forward_as_tuple(vec_hash), std::forward_as_tuple(eq_id, 1)); add_bitvector(vector, eq_id - 1); - added_eq_class = true; + added_eq_class = true; } else { // eq class is seen before so increment the abundance. eq_id = it->second.first; - // with standard map - it->second.second += 1; // update the abundance. - } + // with standard map + it->second.second += 1; // update the abundance. + }*/ dbg.insert(KeyObject(key,0,eq_id)); // we use the count to store the eqclass ids - return added_eq_class; + return added_eq_class; } template @@ -380,7 +405,7 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg::ColoredDbg(std::string& cqf_file, num_samples++; } sampleid.close(); + + colorEncoder = new ColorEncoder(num_samples, dbg, num_samples*100000, ceil(log2(num_samples))-3); } #endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 557c756..fb1bd7e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,7 +10,9 @@ add_library(mantis_core STATIC validatemantis.cc coloreddbg.cc MantisFS.cc - cqf/gqf.c) + cqf/gqf.c + colorEncoder.cc + canonKmer.cc) set(MANTIS_DEBUG_CFLAGS "${MANTIS_C_FLAGS};-g") set(MANTIS_DEBUG_CXXFLAGS "${MANTIS_CXX_FLAGS};-g") @@ -100,13 +102,6 @@ target_link_libraries(walkCqf mantis_core) set_property(TARGET walkCqf PROPERTY INTERPROCEDURAL_OPTIMIZATION True) -add_executable(colorEncoder colorEncoder.cc) -target_include_directories(colorEncoder PUBLIC - $) -target_link_libraries(colorEncoder - mantis_core) -set_property(TARGET colorEncoder PROPERTY INTERPROCEDURAL_OPTIMIZATION True) - #add_executable(walkEqcls walkEqcls.cc) #target_include_directories(walkEqcls PUBLIC # $ $) diff --git a/src/canonKmer.cc b/src/canonKmer.cc new file mode 100644 index 0000000..160ceb8 --- /dev/null +++ b/src/canonKmer.cc @@ -0,0 +1,181 @@ +// +// Created by Fatemeh Almodaresi on 8/22/18. +// + +#include "canonKmer.h" +namespace duplicated_dna { + +/////////////// bases ///////////////// + base operator-(base b) { + return (base) ((~((uint64_t) b)) & 0x3ULL); + } + + const base bases[4] = {C, A, T, G}; + const std::map base_from_char = {{'A', A}, + {'C', C}, + {'G', G}, + {'T', T}, + {'N', A}}; + const std::map base_to_char = {{A, 'A'}, + {C, 'C'}, + {G, 'G'}, + {T, 'T'}}; + +///////////// kmers ///////////////////// + kmer::kmer(void) : len(0), val(0) {} + + kmer::kmer(base b) : len(1), val((uint64_t) b) {} + + kmer::kmer(int l, uint64_t v) : len(l), val(v & BITMASK(2 * l)) { + assert(l <= 32); + } + + static uint64_t string_to_kmer_val(std::string s) { + uint64_t val = 0; + for (auto c : s) + val = (val << 2) | ((uint64_t)(base_from_char.at(c))); + return val; + } + + kmer::kmer(std::string s) : len(s.size()), val(string_to_kmer_val(s)) { + assert(s.size() <= 32); + } + +// Convert to string + kmer::operator std::string() const { + std::string s; + for (auto i = 1; i < len + 1; i++) + s = s + base_to_char.at((base) ((val >> (2 * (len - i))) & BITMASK(2))); + return s; + } + + bool operator<(kmer a, kmer b) { + return a.len != b.len ? a.len < b.len : a.val < b.val; + } + + bool operator==(kmer a, kmer b) { + return a.len == b.len && a.val == b.val; + } + + bool operator!=(kmer a, kmer b) { + return !operator==(a, b); + } + +// Return the reverse complement of k + kmer operator-(kmer k) { + uint64_t val = k.val; + val = + (val >> 32) | + (val << 32); + val = + ((val >> 16) & 0x0000ffff0000ffff) | + ((val << 16) & 0xffff0000ffff0000); + val = + ((val >> 8) & 0x00ff00ff00ff00ff) | + ((val << 8) & 0xff00ff00ff00ff00); + val = + ((val >> 4) & 0x0f0f0f0f0f0f0f0f) | + ((val << 4) & 0xf0f0f0f0f0f0f0f0); + val = + ((val >> 2) & 0x3333333333333333) | + ((val << 2) & 0xcccccccccccccccc); + val = ~val; + val >>= 64 - 2 * k.len; + return kmer(k.len, val); + } + +// backwards from standard definition to match kmer.h definition + kmer canonicalize(kmer k) { + return -k < k ? k : -k; + } + +// Return the kmer of length |a| that results from shifting b into a +// from the right + kmer operator<<(kmer a, kmer b) { + uint64_t val = ((a.val << (2 * b.len)) | b.val) & BITMASK(2 * a.len); + return kmer(a.len, val); + } + +// Return the kmer of length |b| that results from shifting b into a +// from the left + kmer operator>>(kmer a, kmer b) { + uint64_t val + = ((b.val >> (2 * a.len)) | (a.val << (2 * (b.len - a.len)))) + & BITMASK(2 * b.len); + return kmer(b.len, val); + } + +// Append two kmers + kmer operator+(kmer a, kmer b) { + int len = a.len + b.len; + assert(len <= 32); + uint64_t val = (a.val << (2 * b.len)) | b.val; + return kmer(len, val); + } + + kmer prefix(kmer k, int len) { return kmer(len, k.val >> (2 * (k.len - len))); } + + kmer suffix(kmer k, int len) { return kmer(len, k.val & BITMASK(2 * len)); } + + bool period_divides(kmer k, uint64_t periodicity) { + static const uint64_t multipliers[33] = + { + 0, + 0x5555555555555555, // 1 + 0x1111111111111111, // 2 + 0x1041041041041041, // 3 + 0x0101010101010101, // 4 + 0x1004010040100401, // 5 + 0x1001001001001001, // 6 + 0x0100040010004001, // 7 + 0x0001000100010001, // 8 + 0x0040001000040001, // 9 + 0x1000010000100001, // 10 + 0x0000100000400001, // 11 + 0x0001000001000001, // 12 + 0x0010000004000001, // 13 + 0x0100000010000001, // 14 + 0x1000000040000001, // 15 + 0x0000000100000001, // 16 + 0x0000000400000001, // 17 + 0x0000001000000001, // 18 + 0x0000004000000001, // 19 + 0x0000010000000001, // 20 + 0x0000040000000001, // 21 + 0x0000100000000001, // 22 + 0x0000400000000001, // 23 + 0x0001000000000001, // 24 + 0x0004000000000001, // 25 + 0x0010000000000001, // 26 + 0x0040000000000001, // 27 + 0x0100000000000001, // 28 + 0x0400000000000001, // 29 + 0x1000000000000001, // 30 + 0x4000000000000001, // 31 + 0x0000000000000001, // 32 + }; + uint64_t piece = k.val & BITMASK(2 * periodicity); + piece = piece * multipliers[periodicity]; + piece = piece & BITMASK(2 * k.len); + return piece == k.val; + } + + uint64_t period(kmer k) { + for (int i = 1; i <= k.len; i++) { + if (period_divides(k, i)) + return i; + } + abort(); + } + + canonical_kmer::canonical_kmer(void) : kmer() {} + + canonical_kmer::canonical_kmer(base b) : kmer(canonicalize(kmer(b))) {} + + canonical_kmer::canonical_kmer(int l, uint64_t v) + : kmer(canonicalize(kmer(l, v))) {} + + canonical_kmer::canonical_kmer(std::string s) : kmer(canonicalize(kmer(s))) {} + + canonical_kmer::canonical_kmer(kmer k) : kmer(canonicalize(k)) {} +} \ No newline at end of file diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index 03a4873..5117e65 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -7,15 +7,22 @@ #include #include -bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const std::vector bv) { +bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_vector &bv) { // create list of edges to be processed - // 1. list of neighbors - // 2. zero to node + // 1. zero to node + // 2. list of neighbors // calc. the distance between the node and any of the neighbors // that exist and the edge hasn't been seen - dna::canonical_kmer cur(k, kmer); + duplicated_dna::canonical_kmer cur(k, kmer); std::unordered_set, pair_hash> newEdges; - // case 1. edges between the node and its neighbors + // case 1. edge from zero to the node + if (!hasEdge(zero, eqId)) { + auto deltas = buildColor(bv); + updateMST(zero, eqId, deltas); + addEdge(zero, eqId, deltas.size()); + } + + // case 2. edges between the node and its neighbors for (auto nei_eqId : neighbors(cur)) { uint64_t cur_eqId{eqId}; if (nei_eqId != cur_eqId) { @@ -27,10 +34,6 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const std::vector } } } - // case 2. edge from zero to the node - if (!hasEdge(zero, eqId)) { - newEdges.insert(std::make_pair(zero, eqId)); - } for (auto &newEdge : newEdges) { auto deltas = hammingDist(newEdge.first, newEdge.second); updateMST(newEdge.first, newEdge.second, deltas); @@ -44,6 +47,9 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const std::vector // deltas should *NOT* be passed by reference bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector deltas) { // n2 > n1 // If n2 is a new color Id (next color Id) + if (n1 == colorClsCnt) { + std::swap(n1, n2); + } if (n2 == colorClsCnt) { parentbv[n2] = n1; deltaM.insertDeltas(n2, deltas); @@ -81,7 +87,7 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del auto prevDeltas = deltaM.getDeltas(child); deltaM.insertDeltas(child, deltas); deltas = prevDeltas; - parent = n2; + parent = child; child = tmp; } return true; @@ -135,6 +141,22 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { return eq; } +std::vector ColorEncoder::buildColor(const sdsl::bit_vector &bv) { + std::vector setBits; + setBits.reserve(numSamples); + uint64_t i = 0; + while (i < bv.bit_size()) { + auto wrd = bv.get_int(i, 64); + for (uint64_t c=0; c < 64; c++) { + if ( (wrd >> c) & 0x01) { + setBits.push_back(i+c); + } + } + i+=64; + } + return setBits; +} + std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { std::vector res{0}; auto n1 = buildColor(i); @@ -222,9 +244,9 @@ std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) return std::make_pair(e1, e2); } -std::unordered_set ColorEncoder::neighbors(dna::canonical_kmer n) { +std::unordered_set ColorEncoder::neighbors(duplicated_dna::canonical_kmer n) { std::unordered_set result; - for (const auto b : dna::bases) { + for (const auto b : duplicated_dna::bases) { uint64_t eqid, idx; if (exists(b >> n, eqid)) result.insert(eqid); @@ -234,7 +256,7 @@ std::unordered_set ColorEncoder::neighbors(dna::canonical_kmer n) { return result; } -bool ColorEncoder::exists(dna::canonical_kmer e, uint64_t &eqid) { +bool ColorEncoder::exists(duplicated_dna::canonical_kmer e, uint64_t &eqid) { uint64_t tmp = e.val; KeyObject key(HashUtil::hash_64(tmp, BITMASK(cqf.keybits())), 0, 0); auto eq_idx = cqf.query(key); @@ -272,7 +294,3 @@ uint32_t ColorEncoder::getEdge(uint64_t i, uint64_t j) { } return edges[std::make_pair(i, j)]; } - -int main(int argc, char* argv[]) { - -} \ No newline at end of file From 0e18b13db0012b364596d88173797cd8dfe9285a Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Wed, 22 Aug 2018 02:45:24 -0400 Subject: [PATCH 099/122] Adding the very important method for taking care of dangling pointers --- include/deltaManager.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/include/deltaManager.h b/include/deltaManager.h index cafd1ba..0bc4260 100644 --- a/include/deltaManager.h +++ b/include/deltaManager.h @@ -44,6 +44,7 @@ class DeltaManager { } void insertDeltas(uint64_t colorId, const std::vector &dlta) { + // see if we need to split deltas between main DS and heap if (colorId > colorCnt) throw DeltaManagerException("colorId > colorCnt"); auto startBit = colorId*slotsPerColorCls*slotWidth; @@ -54,6 +55,9 @@ class DeltaManager { while (deltas.size() < nextStartBit/64+1) { deltas.push_back(0); } + } else { + // take care of deleting the pointer in case of previously creating one here: + deletePtr(colorId); } // now assuming we've already reserved the space for the new colorId, insert deltas uint64_t mainDSDeltaCnt = dlta.size() < slotsPerColorCls ? dlta.size() : slotsPerColorClsWithPtrs - 1; @@ -109,6 +113,17 @@ class DeltaManager { std::vector deltas; uint64_t colorCnt; + void deletePtr(uint64_t colorId) { + uint64_t startBit = colorId * slotWidth * slotsPerColorCls; // index for next color + uint64_t deltaCnt = getValFromMDeltaV(startBit, slotWidth); + uint64_t mainDSDeltaCnt = deltaCnt < slotsPerColorCls ? deltaCnt : slotsPerColorClsWithPtrs - 1; + if (mainDSDeltaCnt < deltaCnt) { // in case count of deltas exceeds the reserved count + // fetch the pointer to the heap in the main DS and DELETE it + startBit += slotWidth * (mainDSDeltaCnt + 1); + uint64_t *theRestV = reinterpret_cast(getValFromMDeltaV(startBit, 64)); + delete theRestV; + } + } // width is limited to 64 (word size) bool insertValIntoDeltaV(uint64_t startBit, uint64_t val, uint64_t width) { uint64_t mask = width < 64? (((uint64_t)1 << width) - 1) : -1; From e04d34fcdc08fe1d93de4879fdab083367e26391 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Wed, 22 Aug 2018 15:45:39 -0400 Subject: [PATCH 100/122] Add serialization to the colorEncoder --- include/colorEncoder.h | 2 ++ include/deltaManager.h | 37 ++++++++++++++++++++++++++++++++----- src/colorEncoder.cc | 23 ++++++++++++++++------- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/include/colorEncoder.h b/include/colorEncoder.h index 79e196f..86bc1a7 100644 --- a/include/colorEncoder.h +++ b/include/colorEncoder.h @@ -60,6 +60,8 @@ class ColorEncoder { bool addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_vector &bv); + bool serialize(std::string prefix); + private: uint64_t numSamples; uint64_t bvSize; diff --git a/include/deltaManager.h b/include/deltaManager.h index 0bc4260..62ed054 100644 --- a/include/deltaManager.h +++ b/include/deltaManager.h @@ -11,6 +11,7 @@ //#include #include #include +#include "sdsl/bit_vectors.hpp" struct DeltaManagerException : public std::exception { private: @@ -46,22 +47,27 @@ class DeltaManager { void insertDeltas(uint64_t colorId, const std::vector &dlta) { // see if we need to split deltas between main DS and heap - if (colorId > colorCnt) throw DeltaManagerException("colorId > colorCnt"); + // TODO not an expected behaviour, but we have no choice + //if (colorId > colorCnt) throw DeltaManagerException("colorId > colorCnt"); + auto startBit = colorId*slotsPerColorCls*slotWidth; auto nextStartBit = (colorId+1)*slotsPerColorCls*slotWidth; // We always want to assign slotsPerColorCls slots to each index // even if deltas in that index are fewer than the avg num of deltas - if (colorId == colorCnt) { + if (colorId >= colorCnt) { while (deltas.size() < nextStartBit/64+1) { deltas.push_back(0); } + colorCnt = colorId+1; } else { // take care of deleting the pointer in case of previously creating one here: deletePtr(colorId); } + // now assuming we've already reserved the space for the new colorId, insert deltas uint64_t mainDSDeltaCnt = dlta.size() < slotsPerColorCls ? dlta.size() : slotsPerColorClsWithPtrs - 1; insertValIntoDeltaV(startBit, dlta.size(), slotWidth); // insert count of deltas + totDeltaCnt += dlta.size(); startBit += slotWidth; for (uint64_t i = 0; i < mainDSDeltaCnt; i++) { // insert values into main DS insertValIntoDeltaV(startBit, dlta[i], slotWidth); @@ -74,8 +80,6 @@ class DeltaManager { 64); // store the pointer to the heap in the main DS insertValIntoHeap(dlta, mainDSDeltaCnt, theRest, slotWidth); } - if (colorId == colorCnt) - colorCnt++; } std::vector getDeltas(uint64_t colorId) { @@ -105,6 +109,27 @@ class DeltaManager { insertDeltas(colorId2, c1deltas); } + bool serialize(std::string prefix) { + std::string deltabv_file = prefix + "/delta.bv"; + std::string boundarybv_file = prefix + "/boundary.bv"; + + sdsl::int_vector<> deltabv(totDeltaCnt, 0, slotWidth); + sdsl::bit_vector boundarybv(totDeltaCnt, 0); + uint64_t j = 0; + for (uint64_t i = 0; i < colorCnt; i++) { + auto dltas = getDeltas(i); + for (auto dlt : dltas) { + deltabv[j] = dlt; + j++; + } + boundarybv[j-1] = 1; + } + bool deltabvSuccessfullyStored = sdsl::store_to_file(deltabv, deltabv_file); + bool boundarybvSuccessfullyStored = sdsl::store_to_file(boundarybv, boundarybv_file); + + return deltabvSuccessfullyStored and boundarybvSuccessfullyStored; + } + private: uint64_t numSamples; uint64_t slotWidth; @@ -112,10 +137,12 @@ class DeltaManager { uint64_t slotsPerColorClsWithPtrs; std::vector deltas; uint64_t colorCnt; + uint64_t totDeltaCnt{0}; void deletePtr(uint64_t colorId) { uint64_t startBit = colorId * slotWidth * slotsPerColorCls; // index for next color - uint64_t deltaCnt = getValFromMDeltaV(startBit, slotWidth); + uint64_t deltaCnt = getValFromMDeltaV(startBit, slotWidth); // get num of deltas in this slot + totDeltaCnt -= deltaCnt; uint64_t mainDSDeltaCnt = deltaCnt < slotsPerColorCls ? deltaCnt : slotsPerColorClsWithPtrs - 1; if (mainDSDeltaCnt < deltaCnt) { // in case count of deltas exceeds the reserved count // fetch the pointer to the heap in the main DS and DELETE it diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index 5117e65..82a58d8 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -44,19 +44,28 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v return false; } +bool ColorEncoder::serialize(std::string prefix) { + std::string parentbv_file = prefix + "/parent.bv"; + parentbv.resize(colorClsCnt); + bool parentSuccessfullyStored = sdsl::store_to_file(parentbv, parentbv_file); + parentbv.resize(0); + + return deltaM.serialize(prefix) and parentSuccessfullyStored; +} + // deltas should *NOT* be passed by reference bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector deltas) { // n2 > n1 - // If n2 is a new color Id (next color Id) - if (n1 == colorClsCnt) { + if (n1 > n2) { std::swap(n1, n2); } - if (n2 == colorClsCnt) { + while (parentbv.size() < colorClsCnt) { + parentbv.resize(parentbv.size()+parentbv.size()/2); + } + // The only time that we will see the edge zero -> n2 is when n2 is observed for the first time + if (n1 == zero) { parentbv[n2] = n1; deltaM.insertDeltas(n2, deltas); - colorClsCnt++; - if (parentbv.size() == colorClsCnt) { - parentbv.resize(parentbv.size()+parentbv.size()/2); - } + colorClsCnt = n1+1; // n1 i the index return true; } // find the max weight edge from each of the ends to their lca, called w1, w2 From 45e7b2e86e3af75dd51f9eff292bdc5d1c0d1908 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Wed, 22 Aug 2018 22:00:22 -0400 Subject: [PATCH 101/122] still not fully working --- include/colorEncoder.h | 6 +++++- include/coloreddbg.h | 24 +++++++++++++++-------- include/deltaManager.h | 19 +++++++++++------- src/CMakeLists.txt | 22 ++++++++++++--------- src/colorEncoder.cc | 44 ++++++++++++++++++++++++++++++++++++------ src/coloreddbg.cc | 1 + 6 files changed, 85 insertions(+), 31 deletions(-) diff --git a/include/colorEncoder.h b/include/colorEncoder.h index 86bc1a7..bd91388 100644 --- a/include/colorEncoder.h +++ b/include/colorEncoder.h @@ -55,7 +55,11 @@ class ColorEncoder { parentbv(bvSize, 0, log2((double)bvSize)+5),//TODO take care of this constant!! deltaM(numSamplesIn, bvSize, approximateDeltaCntPerClrCls), colorClsCnt(1) // start with the dummy node - {} + { + std::cerr << "\nColorEncoder Constructor: bvSize: " + << bvSize << " parent size: " << parentbv.size() + << " colorClsCnt: " << colorClsCnt << "\n"; + } bool addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_vector &bv); diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 55bfe09..17e3b0e 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -184,7 +184,8 @@ template void ColoredDbg::reinit(cdbg_bv_map_t<__uint128_t, std::pair>& map) { dbg.reset(); - reshuffle_bit_vectors(map); + //todo fatemeh + /*reshuffle_bit_vectors(map); // Check if the current bit vector buffer is full and needs to be serialized. // This happens when the sampling phase fills up the bv buffer. if (get_num_eqclasses() % mantis::NUM_BV_BUFFER == 0) { @@ -192,7 +193,7 @@ void ColoredDbg::reinit(cdbg_bv_map_t<__uint128_t, console->info("Serializing bit vector with {} eq classes.", get_num_eqclasses()); bv_buffer_serialize(); - } + }*/ eqclass_map = map; } @@ -200,6 +201,7 @@ template bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, const BitVector& vector, bool isSampling) { + //todo fatemeh // A kmer (hash) is seen only once during the merge process. // So we insert every kmer in the dbg uint64_t eq_id; @@ -282,9 +284,11 @@ void ColoredDbg::serialize() { // serialize the CQF dbg.serialize(prefix + mantis::CQF_FILE); + //todo fatemeh + colorEncoder->serialize(prefix); // serialize the bv buffer last time if needed - if (get_num_eqclasses() % mantis::NUM_BV_BUFFER > 1) - bv_buffer_serialize(); + /*if (get_num_eqclasses() % mantis::NUM_BV_BUFFER > 1) + bv_buffer_serialize();*/ //serialize the eq class id map std::ofstream opfile(prefix + mantis::SAMPLEID_FILE); @@ -422,12 +426,14 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbginfo("Serializing bit vector with {} eq classes.", get_num_eqclasses()); bv_buffer_serialize(); - } + }*/ } else if (counter > num_kmers) { // Check if the sampling phase is finished based on the number of k-mers. break; @@ -452,7 +458,10 @@ ColoredDbg::ColoredDbg(uint64_t qbits, uint64_t key_bits, uint32_t seed, std::string& prefix, uint64_t nqf) : dbg(qbits, key_bits, seed), bv_buffer(mantis::NUM_BV_BUFFER * nqf), - prefix(prefix), num_samples(nqf), num_serializations(0), start_time_(std::time(nullptr)) {} + prefix(prefix), num_samples(nqf), num_serializations(0), start_time_(std::time(nullptr)) { + colorEncoder = new ColorEncoder(num_samples, dbg, num_samples*100000, ceil(log2(num_samples))-3); + +} template ColoredDbg::ColoredDbg(std::string& cqf_file, @@ -487,7 +496,6 @@ ColoredDbg::ColoredDbg(std::string& cqf_file, } sampleid.close(); - colorEncoder = new ColorEncoder(num_samples, dbg, num_samples*100000, ceil(log2(num_samples))-3); } #endif diff --git a/include/deltaManager.h b/include/deltaManager.h index 62ed054..2a88c91 100644 --- a/include/deltaManager.h +++ b/include/deltaManager.h @@ -66,10 +66,22 @@ class DeltaManager { // now assuming we've already reserved the space for the new colorId, insert deltas uint64_t mainDSDeltaCnt = dlta.size() < slotsPerColorCls ? dlta.size() : slotsPerColorClsWithPtrs - 1; + if (dlta.size() > numSamples) { + std::string msg = "number of deltas is greater than num_samples. val:"+ + std::to_string(dlta.size())+ + " num_samples:" +std::to_string(numSamples); + throw DeltaManagerException(msg); + } insertValIntoDeltaV(startBit, dlta.size(), slotWidth); // insert count of deltas totDeltaCnt += dlta.size(); startBit += slotWidth; for (uint64_t i = 0; i < mainDSDeltaCnt; i++) { // insert values into main DS + if (dlta[i] >= numSamples) { + std::string msg = "delta index is larger than num_samples. val:"+ + std::to_string(dlta[i])+ + " num_samples:" +std::to_string(numSamples); + throw DeltaManagerException(msg); + } insertValIntoDeltaV(startBit, dlta[i], slotWidth); startBit += slotWidth; } @@ -155,13 +167,6 @@ class DeltaManager { bool insertValIntoDeltaV(uint64_t startBit, uint64_t val, uint64_t width) { uint64_t mask = width < 64? (((uint64_t)1 << width) - 1) : -1; if (startBit >= deltas.size() * 64) throw DeltaManagerException("startBit exceeds bit_size"); - if (width == slotWidth and val >= numSamples) { - std::string msg = "delta index is larger than num_samples. val:"+ - std::to_string(val)+ - " num_samples:" +std::to_string(numSamples); - throw DeltaManagerException(msg); - } - uint64_t &wrd = deltas[startBit / 64]; uint64_t startBitInwrd = startBit % 64; wrd &= ~(mask << startBitInwrd); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fb1bd7e..4e7c35d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -66,15 +66,7 @@ endif() install(TARGETS mantis RUNTIME DESTINATION bin) -#add_executable(monochromatic_component_iterator -# monochromatic_component_iterator.cc) -#target_include_directories(monochromatic_component_iterator PUBLIC -# $) -#target_link_libraries(monochromatic_component_iterator -# mantis_core) -#set_property(TARGET monochromatic_component_iterator PROPERTY INTERPROCEDURAL_OPTIMIZATION True) - - +#[[ add_executable(msf MSF.cc) target_include_directories(msf PUBLIC $) @@ -101,6 +93,9 @@ target_include_directories(walkCqf PUBLIC target_link_libraries(walkCqf mantis_core) set_property(TARGET walkCqf PROPERTY INTERPROCEDURAL_OPTIMIZATION True) +]] + + #add_executable(walkEqcls walkEqcls.cc) #target_include_directories(walkEqcls PUBLIC @@ -108,3 +103,12 @@ set_property(TARGET walkCqf PROPERTY INTERPROCEDURAL_OPTIMIZATION True) #find_library(compression_library NAMES SIMDCompressionAndIntersection HINTS "${CMAKE_SOURCE_DIR}/lib") #target_link_libraries(walkEqcls # mantis_core ${compression_library}) + + +#add_executable(monochromatic_component_iterator +# monochromatic_component_iterator.cc) +#target_include_directories(monochromatic_component_iterator PUBLIC +# $) +#target_link_libraries(monochromatic_component_iterator +# mantis_core) +#set_property(TARGET monochromatic_component_iterator PROPERTY INTERPROCEDURAL_OPTIMIZATION True) diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index 82a58d8..6859766 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -13,10 +13,11 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v // 2. list of neighbors // calc. the distance between the node and any of the neighbors // that exist and the edge hasn't been seen - duplicated_dna::canonical_kmer cur(k, kmer); + duplicated_dna::canonical_kmer cur(k, HashUtil::hash_64i(kmer, BITMASK(cqf.keybits()))); std::unordered_set, pair_hash> newEdges; // case 1. edge from zero to the node if (!hasEdge(zero, eqId)) { + std::cerr << "case1: " << eqId << " " << colorClsCnt << " " << parentbv.size() << " "; auto deltas = buildColor(bv); updateMST(zero, eqId, deltas); addEdge(zero, eqId, deltas.size()); @@ -27,13 +28,14 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v uint64_t cur_eqId{eqId}; if (nei_eqId != cur_eqId) { if (nei_eqId < cur_eqId) { - std::swap(nei_eqId, cur_eqId); + std::swap(cur_eqId, nei_eqId); } if (!hasEdge(cur_eqId, nei_eqId)) { newEdges.insert(std::make_pair(cur_eqId, nei_eqId)); } } } + //std::cerr << "case2 " << eqId << " " << colorClsCnt << " " << newEdges.size() << "\n"; for (auto &newEdge : newEdges) { auto deltas = hammingDist(newEdge.first, newEdge.second); updateMST(newEdge.first, newEdge.second, deltas); @@ -55,6 +57,7 @@ bool ColorEncoder::serialize(std::string prefix) { // deltas should *NOT* be passed by reference bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector deltas) { // n2 > n1 + std::cerr << "updateMST: n1= " << n1 << " , n2=" << n2 << " "; if (n1 > n2) { std::swap(n1, n2); } @@ -64,6 +67,11 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del // The only time that we will see the edge zero -> n2 is when n2 is observed for the first time if (n1 == zero) { parentbv[n2] = n1; + for (auto d : deltas) { + std::cerr << d << " "; + } + std::cerr << "\n"; + std::cerr << "insertDeltas 1\n"; deltaM.insertDeltas(n2, deltas); colorClsCnt = n1+1; // n1 i the index return true; @@ -94,6 +102,7 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del auto tmp = parentbv[child]; parentbv[child] = parent; auto prevDeltas = deltaM.getDeltas(child); + std::cerr << "insertDeltas 2\n"; deltaM.insertDeltas(child, deltas); deltas = prevDeltas; parent = child; @@ -118,6 +127,7 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { deltaIndices.reserve(numWrds); bool foundCache = false; uint32_t iparent = parentbv[i]; + std::cerr << "\nwalking mst\n"; while (i != zero) { if (lru_cache.contains(i)) { const auto &vs = lru_cache[i]; @@ -127,13 +137,16 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { foundCache = true; break; } + std::cerr << " " << i; deltaIndices.push_back(i); i = iparent; iparent = parentbv[i]; } + std::cerr << "\n"; uint64_t pctr{0}; for (auto index : deltaIndices) { + std::cerr << "getDeltas 1 " << index << "\n"; auto deltas = deltaM.getDeltas(index); for (auto d : deltas) { flips[d] ^= 0x01; @@ -154,31 +167,42 @@ std::vector ColorEncoder::buildColor(const sdsl::bit_vector &bv) { std::vector setBits; setBits.reserve(numSamples); uint64_t i = 0; + std::cerr << " bv bitsize: " << bv.bit_size() << " "; while (i < bv.bit_size()) { - auto wrd = bv.get_int(i, 64); - for (uint64_t c=0; c < 64; c++) { + uint64_t bitcnt = numSamples - i >= 64?64:(numSamples - i); + auto wrd = bv.get_int(i, bitcnt); + for (uint64_t c=0; c < bitcnt; c++) { if ( (wrd >> c) & 0x01) { setBits.push_back(i+c); } } i+=64; } + std::cerr << " setBits: "; + for (auto s : setBits) { + std::cerr << s << " "; + } + std::cerr << "\n"; return setBits; } std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { + std::cerr << "\nhamming dist between " << i << "," << j << "\n"; std::vector res{0}; auto n1 = buildColor(i); auto n2 = buildColor(j); + std::cerr << " merge, "; // merge // with slight difference of not inserting values that appear in both vectors uint64_t i1{0}, i2{0}; while (i1 < n1.size() or i2 < n2.size()) { if (i1 == n1.size()) { + std::cerr << " i1=n1: " << n1.size() << " " << n2.size() << " "; copy(n2.begin()+i2, n2.end(), back_inserter(res)); i2 = n2.size(); } else if (i2 == n2.size()) { + std::cerr << " i2=n2: " << n2.size() << " " << n1.size() << " "; copy(n1.begin()+i1, n1.end(), back_inserter(res)); i1 = n1.size(); } @@ -195,6 +219,10 @@ std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { } } } + for (auto r : res) { + std::cerr << r << " "; + } + std::cerr << "\n"; return res; } @@ -257,10 +285,14 @@ std::unordered_set ColorEncoder::neighbors(duplicated_dna::canonical_k std::unordered_set result; for (const auto b : duplicated_dna::bases) { uint64_t eqid, idx; - if (exists(b >> n, eqid)) + if (exists(b >> n, eqid)) { + std::cerr << std::string(n) << "\n" << std::string(b >> n) << "\n"; result.insert(eqid); - if (exists(n << b, eqid)) + } + if (exists(n << b, eqid)) { + std::cerr << std::string(n) << "\n" << std::string(b >> n) << "\n"; result.insert(eqid); + } } return result; } diff --git a/src/coloreddbg.cc b/src/coloreddbg.cc index c759581..aff9fe6 100644 --- a/src/coloreddbg.cc +++ b/src/coloreddbg.cc @@ -204,6 +204,7 @@ build_main ( BuildOpts& opt ) console->info("Constructing the colored dBG."); + console->info("\n\n\n\n\n\nDONE WITH SAMPLING PHASE\n\n\n\n\n"); // Reconstruct the colored dbg using the new set of equivalence classes. cdbg.construct(inobjects.data(), std::numeric_limits::max()); From e1ff70c740f65c7e4835361444f8321629f009c5 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Wed, 22 Aug 2018 22:26:26 -0400 Subject: [PATCH 102/122] fixed a bug. went to the next (cut in a loop for breaking a loop) --- src/colorEncoder.cc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index 6859766..f5b6a2f 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -98,16 +98,18 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del // starting from node n2 toward the c2, child node of the edge with weight w2 auto parent = n1; auto child = n2; + std::cerr << "here: " << p2 << " "; while (child != p2) { auto tmp = parentbv[child]; parentbv[child] = parent; auto prevDeltas = deltaM.getDeltas(child); - std::cerr << "insertDeltas 2\n"; + std::cerr << "insertDeltas 2 " << parent << "->" << child << " "; deltaM.insertDeltas(child, deltas); deltas = prevDeltas; parent = child; child = tmp; } + std::cerr << "\n"; return true; } @@ -284,7 +286,7 @@ std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) std::unordered_set ColorEncoder::neighbors(duplicated_dna::canonical_kmer n) { std::unordered_set result; for (const auto b : duplicated_dna::bases) { - uint64_t eqid, idx; + uint64_t eqid{0}, idx; if (exists(b >> n, eqid)) { std::cerr << std::string(n) << "\n" << std::string(b >> n) << "\n"; result.insert(eqid); @@ -300,8 +302,8 @@ std::unordered_set ColorEncoder::neighbors(duplicated_dna::canonical_k bool ColorEncoder::exists(duplicated_dna::canonical_kmer e, uint64_t &eqid) { uint64_t tmp = e.val; KeyObject key(HashUtil::hash_64(tmp, BITMASK(cqf.keybits())), 0, 0); - auto eq_idx = cqf.query(key); - return eq_idx != 0; + eqid = cqf.query(key); + return eqid != 0; // commenting this line, eqIds start from 1 (rather than 0) /* if (eq_idx) { From 95045e0e98bd9b2c6eee039d100f07d4d44cd0d8 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Wed, 22 Aug 2018 22:46:43 -0400 Subject: [PATCH 103/122] fixed the loop. got into another --- src/colorEncoder.cc | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index f5b6a2f..82595a6 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -17,7 +17,7 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v std::unordered_set, pair_hash> newEdges; // case 1. edge from zero to the node if (!hasEdge(zero, eqId)) { - std::cerr << "case1: " << eqId << " " << colorClsCnt << " " << parentbv.size() << " "; + std::cerr << "case1: " << eqId << " " << colorClsCnt << "\n"; auto deltas = buildColor(bv); updateMST(zero, eqId, deltas); addEdge(zero, eqId, deltas.size()); @@ -38,7 +38,9 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v //std::cerr << "case2 " << eqId << " " << colorClsCnt << " " << newEdges.size() << "\n"; for (auto &newEdge : newEdges) { auto deltas = hammingDist(newEdge.first, newEdge.second); - updateMST(newEdge.first, newEdge.second, deltas); + if (updateMST(newEdge.first, newEdge.second, deltas)) { + std::cerr << "case2 " << eqId << " " << colorClsCnt << "\n"; + } addEdge(newEdge.first, newEdge.second, deltas.size()); } if (newEdges.size()) @@ -57,7 +59,7 @@ bool ColorEncoder::serialize(std::string prefix) { // deltas should *NOT* be passed by reference bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector deltas) { // n2 > n1 - std::cerr << "updateMST: n1= " << n1 << " , n2=" << n2 << " "; + //std::cerr << "updateMST: n1= " << n1 << " , n2=" << n2 << " "; if (n1 > n2) { std::swap(n1, n2); } @@ -67,11 +69,11 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del // The only time that we will see the edge zero -> n2 is when n2 is observed for the first time if (n1 == zero) { parentbv[n2] = n1; - for (auto d : deltas) { + /*for (auto d : deltas) { std::cerr << d << " "; } std::cerr << "\n"; - std::cerr << "insertDeltas 1\n"; + std::cerr << "insertDeltas 1\n";*/ deltaM.insertDeltas(n2, deltas); colorClsCnt = n1+1; // n1 i the index return true; @@ -98,18 +100,19 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del // starting from node n2 toward the c2, child node of the edge with weight w2 auto parent = n1; auto child = n2; - std::cerr << "here: " << p2 << " "; + //std::cerr << "here: " << n1 << " " << n2 << " " << parentbv[n2] << " " << p1 << " " << p2 << " "; while (child != p2) { - auto tmp = parentbv[child]; + uint64_t tmp = parentbv[child]; + //std::cerr << " first: " << tmp << " then "; parentbv[child] = parent; auto prevDeltas = deltaM.getDeltas(child); - std::cerr << "insertDeltas 2 " << parent << "->" << child << " "; + //std::cerr << "insertDeltas 2 " << parent << "->" << child << " " << tmp << "\n"; deltaM.insertDeltas(child, deltas); deltas = prevDeltas; parent = child; child = tmp; } - std::cerr << "\n"; + //std::cerr << "\n"; return true; } @@ -189,22 +192,22 @@ std::vector ColorEncoder::buildColor(const sdsl::bit_vector &bv) { } std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { - std::cerr << "\nhamming dist between " << i << "," << j << "\n"; + //std::cerr << "\nhamming dist between " << i << "," << j << "\n"; std::vector res{0}; auto n1 = buildColor(i); auto n2 = buildColor(j); - std::cerr << " merge, "; + //std::cerr << " merge, "; // merge // with slight difference of not inserting values that appear in both vectors uint64_t i1{0}, i2{0}; while (i1 < n1.size() or i2 < n2.size()) { if (i1 == n1.size()) { - std::cerr << " i1=n1: " << n1.size() << " " << n2.size() << " "; + //std::cerr << " i1=n1: " << n1.size() << " " << n2.size() << " "; copy(n2.begin()+i2, n2.end(), back_inserter(res)); i2 = n2.size(); } else if (i2 == n2.size()) { - std::cerr << " i2=n2: " << n2.size() << " " << n1.size() << " "; + //std::cerr << " i2=n2: " << n2.size() << " " << n1.size() << " "; copy(n1.begin()+i1, n1.end(), back_inserter(res)); i1 = n1.size(); } @@ -221,10 +224,10 @@ std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { } } } - for (auto r : res) { + /*for (auto r : res) { std::cerr << r << " "; } - std::cerr << "\n"; + std::cerr << "\n";*/ return res; } @@ -288,11 +291,11 @@ std::unordered_set ColorEncoder::neighbors(duplicated_dna::canonical_k for (const auto b : duplicated_dna::bases) { uint64_t eqid{0}, idx; if (exists(b >> n, eqid)) { - std::cerr << std::string(n) << "\n" << std::string(b >> n) << "\n"; + //std::cerr << std::string(n) << "\n" << std::string(b >> n) << "\n"; result.insert(eqid); } if (exists(n << b, eqid)) { - std::cerr << std::string(n) << "\n" << std::string(b >> n) << "\n"; + //std::cerr << std::string(n) << "\n" << std::string(b >> n) << "\n"; result.insert(eqid); } } From 80aeff93bdd79b6892c24e7aff4333e222c3cbaf Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Thu, 23 Aug 2018 00:42:16 -0400 Subject: [PATCH 104/122] works (except for serialization) :happy: --- include/coloreddbg.h | 8 +++++--- include/deltaManager.h | 4 ++++ src/colorEncoder.cc | 46 ++++++++++++++++++++++++++++++------------ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 17e3b0e..c94b601 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -281,14 +281,16 @@ void ColoredDbg::bv_buffer_serialize() { template void ColoredDbg::serialize() { - // serialize the CQF - dbg.serialize(prefix + mantis::CQF_FILE); //todo fatemeh - colorEncoder->serialize(prefix); // serialize the bv buffer last time if needed /*if (get_num_eqclasses() % mantis::NUM_BV_BUFFER > 1) bv_buffer_serialize();*/ + colorEncoder->serialize(prefix); + + // serialize the CQF + dbg.serialize(prefix + mantis::CQF_FILE); + //serialize the eq class id map std::ofstream opfile(prefix + mantis::SAMPLEID_FILE); diff --git a/include/deltaManager.h b/include/deltaManager.h index 2a88c91..1276e62 100644 --- a/include/deltaManager.h +++ b/include/deltaManager.h @@ -122,6 +122,9 @@ class DeltaManager { } bool serialize(std::string prefix) { + std::cerr << "\nSerializing Deltas:\nTotal delta count:" << + totDeltaCnt << " Total color count:" << colorCnt << "\n"; + std::string deltabv_file = prefix + "/delta.bv"; std::string boundarybv_file = prefix + "/boundary.bv"; @@ -129,6 +132,7 @@ class DeltaManager { sdsl::bit_vector boundarybv(totDeltaCnt, 0); uint64_t j = 0; for (uint64_t i = 0; i < colorCnt; i++) { + std::cerr << i << " "; auto dltas = getDeltas(i); for (auto dlt : dltas) { deltabv[j] = dlt; diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index 82595a6..a005437 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -17,7 +17,7 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v std::unordered_set, pair_hash> newEdges; // case 1. edge from zero to the node if (!hasEdge(zero, eqId)) { - std::cerr << "case1: " << eqId << " " << colorClsCnt << "\n"; + //std::cerr << "case1: " << eqId << " " << colorClsCnt << "\n"; auto deltas = buildColor(bv); updateMST(zero, eqId, deltas); addEdge(zero, eqId, deltas.size()); @@ -39,7 +39,7 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v for (auto &newEdge : newEdges) { auto deltas = hammingDist(newEdge.first, newEdge.second); if (updateMST(newEdge.first, newEdge.second, deltas)) { - std::cerr << "case2 " << eqId << " " << colorClsCnt << "\n"; + //std::cerr << "case2 " << eqId << " " << colorClsCnt << "\n"; } addEdge(newEdge.first, newEdge.second, deltas.size()); } @@ -75,7 +75,8 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del std::cerr << "\n"; std::cerr << "insertDeltas 1\n";*/ deltaM.insertDeltas(n2, deltas); - colorClsCnt = n1+1; // n1 i the index + if (colorClsCnt < n2) + colorClsCnt = n2+1; // n2 is the index return true; } // find the max weight edge from each of the ends to their lca, called w1, w2 @@ -100,13 +101,15 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del // starting from node n2 toward the c2, child node of the edge with weight w2 auto parent = n1; auto child = n2; + //6 8 0 0 4 //std::cerr << "here: " << n1 << " " << n2 << " " << parentbv[n2] << " " << p1 << " " << p2 << " "; while (child != p2) { uint64_t tmp = parentbv[child]; //std::cerr << " first: " << tmp << " then "; parentbv[child] = parent; auto prevDeltas = deltaM.getDeltas(child); - //std::cerr << "insertDeltas 2 " << parent << "->" << child << " " << tmp << "\n"; + /*if (n1 == 6 and n2 == 8) + std::cerr << parent << "->" << child << " " << tmp << ", ";*/ deltaM.insertDeltas(child, deltas); deltas = prevDeltas; parent = child; @@ -132,7 +135,7 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { deltaIndices.reserve(numWrds); bool foundCache = false; uint32_t iparent = parentbv[i]; - std::cerr << "\nwalking mst\n"; + //std::cerr << "\nwalking mst\n"; while (i != zero) { if (lru_cache.contains(i)) { const auto &vs = lru_cache[i]; @@ -142,16 +145,16 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { foundCache = true; break; } - std::cerr << " " << i; + //std::cerr << " " << i; deltaIndices.push_back(i); i = iparent; iparent = parentbv[i]; } - std::cerr << "\n"; + //std::cerr << "\n"; uint64_t pctr{0}; for (auto index : deltaIndices) { - std::cerr << "getDeltas 1 " << index << "\n"; + //std::cerr << "getDeltas " << index << "\n"; auto deltas = deltaM.getDeltas(index); for (auto d : deltas) { flips[d] ^= 0x01; @@ -172,7 +175,7 @@ std::vector ColorEncoder::buildColor(const sdsl::bit_vector &bv) { std::vector setBits; setBits.reserve(numSamples); uint64_t i = 0; - std::cerr << " bv bitsize: " << bv.bit_size() << " "; + //std::cerr << " bv bitsize: " << bv.bit_size() << " "; while (i < bv.bit_size()) { uint64_t bitcnt = numSamples - i >= 64?64:(numSamples - i); auto wrd = bv.get_int(i, bitcnt); @@ -183,11 +186,11 @@ std::vector ColorEncoder::buildColor(const sdsl::bit_vector &bv) { } i+=64; } - std::cerr << " setBits: "; + /*std::cerr << " setBits: "; for (auto s : setBits) { std::cerr << s << " "; } - std::cerr << "\n"; + std::cerr << "\n";*/ return setBits; } @@ -236,17 +239,28 @@ std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) { std::vector nodes1; std::vector nodes2; - auto n = n1; + uint64_t n = n1; +// if (n1 == 6 and n2 == 8) +// std::cerr << "1 "; while (n != zero) { +// if (n1 == 6 and n2 == 8) +// std::cerr << n << " "; nodes1.push_back(n); n = parentbv[n]; } + nodes1.push_back(zero); +// if (n1 == 6 and n2 == 8) +// std::cerr << "\n2 "; n = n2; while (n != zero) { +// if (n1 == 6 and n2 == 8) +// std::cerr << n << " "; nodes2.push_back(n); n = parentbv[n]; } - + nodes2.push_back(zero); +// if (n1 == 6 and n2 == 8) +// std::cerr << "\n3 lca="; auto &n1ref = nodes1; auto &n2ref = nodes2; if (n1ref.size() < n2ref.size()) { @@ -258,6 +272,8 @@ std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) for (uint64_t j = 0, i = n1ref.size()-n2ref.size(); j < n2ref.size(); i++, j++) { if (n1ref[i] == n2ref[j]) { lca = n1ref[i]; +// if (n1 == 6 and n2 == 8) +// std::cerr << lca; break; } } @@ -265,9 +281,13 @@ std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) // walk from n1 to lca and find the edge with maximum weight Edge e1; uint64_t i = 0; +// if (n1 == 6 and n2 == 8) +// std::cerr << "\n4 "; while (n1ref[i] != lca) { auto curW = getEdge(n1ref[i], n1ref[i+1]); if (e1.weight < curW) { +// if (n1 == 6 and n2 == 8) +// std::cerr << n1ref[i+1] << " " << n1ref[i] << " " << curW << "\n"; e1 = Edge(n1ref[i+1], n1ref[i], curW); } i++; From 7febf966553c023322ad59ff59c471d16b3b72ea Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Thu, 23 Aug 2018 01:13:03 -0400 Subject: [PATCH 105/122] works!! :dance: --- include/colorEncoder.h | 5 +++-- include/deltaManager.h | 3 ++- src/colorEncoder.cc | 11 ++++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/include/colorEncoder.h b/include/colorEncoder.h index bd91388..08f6e2f 100644 --- a/include/colorEncoder.h +++ b/include/colorEncoder.h @@ -54,7 +54,8 @@ class ColorEncoder { bvSize(approximateClrClsesIn), parentbv(bvSize, 0, log2((double)bvSize)+5),//TODO take care of this constant!! deltaM(numSamplesIn, bvSize, approximateDeltaCntPerClrCls), - colorClsCnt(1) // start with the dummy node + colorClsCnt(1), // start with the dummy node + lru_cache(100000) { std::cerr << "\nColorEncoder Constructor: bvSize: " << bvSize << " parent size: " << parentbv.size() @@ -75,7 +76,7 @@ class ColorEncoder { CQF &cqf; int k = 20; std::unordered_map, uint32_t, pair_hash> edges; - LRU::Cache> lru_cache; + LRUCacheMap lru_cache; std::vector buildColor(uint64_t eqid); diff --git a/include/deltaManager.h b/include/deltaManager.h index 1276e62..ee1d047 100644 --- a/include/deltaManager.h +++ b/include/deltaManager.h @@ -131,7 +131,8 @@ class DeltaManager { sdsl::int_vector<> deltabv(totDeltaCnt, 0, slotWidth); sdsl::bit_vector boundarybv(totDeltaCnt, 0); uint64_t j = 0; - for (uint64_t i = 0; i < colorCnt; i++) { + boundarybvbv[0] = 1; // TODO careful to add an if in case we're gonna change zero to something other than 0 + for (uint64_t i = 1; i < colorCnt; i++) { std::cerr << i << " "; auto dltas = getDeltas(i); for (auto dlt : dltas) { diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index a005437..cb8f46e 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -15,12 +15,17 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v // that exist and the edge hasn't been seen duplicated_dna::canonical_kmer cur(k, HashUtil::hash_64i(kmer, BITMASK(cqf.keybits()))); std::unordered_set, pair_hash> newEdges; + + std::vector setBits; + if (!lru_cache.contains(eqId)) { + setBits = buildColor(bv); + lru_cache.emplace(eqId, setBits); + } // case 1. edge from zero to the node if (!hasEdge(zero, eqId)) { //std::cerr << "case1: " << eqId << " " << colorClsCnt << "\n"; - auto deltas = buildColor(bv); - updateMST(zero, eqId, deltas); - addEdge(zero, eqId, deltas.size()); + updateMST(zero, eqId, setBits); + addEdge(zero, eqId, setBits.size()); } // case 2. edges between the node and its neighbors From 5138bd02e09af8aa14a50d3b59a172c4fbe91f3c Mon Sep 17 00:00:00 2001 From: Prashant Pandey Date: Thu, 23 Aug 2018 01:12:27 -0700 Subject: [PATCH 106/122] Adding auto resize. --- include/coloreddbg.h | 144 +++++++++++++++++++++++-------------------- src/query.cc | 1 - 2 files changed, 77 insertions(+), 68 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 2ed8358..0f668c9 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -1,16 +1,8 @@ /* * ============================================================================ * - * Filename: coloreddbg.h - * - * Description: - * - * Version: 1.0 - * Created: 2017-10-24 08:49:22 PM - * Revision: none - * Compiler: gcc - * * Author: Prashant Pandey (), ppandey@cs.stonybrook.edu + * Mike Ferdman (), mferdman@cs.stonybrook.edu * Organization: Stony Brook University * * ============================================================================ @@ -45,27 +37,28 @@ struct hash128 { __uint128_t val = val128; // Using the same seed as we use in k-mer hashing. return MurmurHash64A((void*)&val, sizeof(__uint128_t), - 2038074743); + 2038074743); } }; template - using cdbg_bv_map_t = spp::sparse_hash_map; +using cdbg_bv_map_t = spp::sparse_hash_map; -using default_cdbg_bv_map_t = cdbg_bv_map_t<__uint128_t, std::pair>; +using default_cdbg_bv_map_t = cdbg_bv_map_t<__uint128_t, + std::pair>; template class ColoredDbg { - public: + public: ColoredDbg(std::string& cqf_file, std::vector& eqclass_files, std::string& sample_file); ColoredDbg(uint64_t qbits, uint64_t key_bits, enum qf_hashmode hashmode, uint32_t seed, std::string& prefix, uint64_t nqf); - + void build_sampleid_map(qf_obj *incqfs); - default_cdbg_bv_map_t& + default_cdbg_bv_map_t& construct(qf_obj *incqfs, uint64_t num_kmers); void set_console(spdlog::logger* c) { console = c; } @@ -85,9 +78,11 @@ class ColoredDbg { void set_flush_eqclass_dist(void) { flush_eqclass_dis = true; } private: - // returns true if adding this k-mer increased the number of equivalence classes - // and false otherwise. - bool add_kmer(const typename key_obj::kmer_t& hash, const BitVector& vector); + // returns true if adding this k-mer increased the number of equivalence + // classes + // and false otherwise. + bool add_kmer(const typename key_obj::kmer_t& hash, const BitVector& + vector); void add_bitvector(const BitVector& vector, uint64_t eq_id); void add_eq_class(BitVector vector, uint64_t id); uint64_t get_next_available_id(void); @@ -105,7 +100,7 @@ class ColoredDbg { uint64_t num_samples; uint64_t num_serializations; bool flush_eqclass_dis{false}; - std::time_t start_time_; + std::time_t start_time_; spdlog::logger* console; }; @@ -158,28 +153,28 @@ template void ColoredDbg::reshuffle_bit_vectors(cdbg_bv_map_t<__uint128_t, std::pair>& map) { - BitVector new_bv_buffer(mantis::NUM_BV_BUFFER * num_samples); - for (auto& it_input : map) { - auto it_local = eqclass_map.find(it_input.first); - if (it_local == eqclass_map.end()) { - console->error("Can't find the vector hash during shuffling"); - exit(1); - } else { - assert(it_local->second.first <= mantis::NUM_BV_BUFFER && it_input.second.first - <= mantis::NUM_BV_BUFFER); - uint64_t src_idx = ((it_local->second.first - 1) * num_samples); - uint64_t dest_idx = ((it_input.second.first - 1) * num_samples); - for (uint32_t i = 0; i < num_samples; i++, src_idx++, dest_idx++) - if (bv_buffer[src_idx]) - new_bv_buffer[dest_idx] = 1; - } - } - bv_buffer = new_bv_buffer; -} + BitVector new_bv_buffer(mantis::NUM_BV_BUFFER * num_samples); + for (auto& it_input : map) { + auto it_local = eqclass_map.find(it_input.first); + if (it_local == eqclass_map.end()) { + console->error("Can't find the vector hash during shuffling"); + exit(1); + } else { + assert(it_local->second.first <= mantis::NUM_BV_BUFFER && + it_input.second.first <= mantis::NUM_BV_BUFFER); + uint64_t src_idx = ((it_local->second.first - 1) * num_samples); + uint64_t dest_idx = ((it_input.second.first - 1) * num_samples); + for (uint32_t i = 0; i < num_samples; i++, src_idx++, dest_idx++) + if (bv_buffer[src_idx]) + new_bv_buffer[dest_idx] = 1; + } + } + bv_buffer = new_bv_buffer; + } template void ColoredDbg::reinit(cdbg_bv_map_t<__uint128_t, - std::pair>& map) { + std::pair>& map) { dbg.reset(); reshuffle_bit_vectors(map); // Check if the current bit vector buffer is full and needs to be serialized. @@ -194,8 +189,8 @@ void ColoredDbg::reinit(cdbg_bv_map_t<__uint128_t, } template -bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, const BitVector& - vector) { +bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& + key, const BitVector& vector) { // A kmer (hash) is seen only once during the merge process. // So we insert every kmer in the dbg uint64_t eq_id; @@ -204,7 +199,7 @@ bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, 2038074751); auto it = eqclass_map.find(vec_hash); - bool added_eq_class{false}; + bool added_eq_class{false}; // Find if the eqclass of the kmer is already there. // If it is there then increment the abundance. // Else create a new eq class. @@ -212,40 +207,50 @@ bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& key, // eq class is seen for the first time. eq_id = get_next_available_id(); eqclass_map.emplace(std::piecewise_construct, - std::forward_as_tuple(vec_hash), - std::forward_as_tuple(eq_id, 1)); + std::forward_as_tuple(vec_hash), + std::forward_as_tuple(eq_id, 1)); add_bitvector(vector, eq_id - 1); - added_eq_class = true; + added_eq_class = true; } else { // eq class is seen before so increment the abundance. eq_id = it->second.first; - // with standard map - it->second.second += 1; // update the abundance. + // with standard map + it->second.second += 1; // update the abundance. + } + + // we use the count to store the eqclass ids + int ret = dbg.insert(KeyObject(key,0,eq_id), QF_KEY_IS_HASH); + if (ret == QF_NO_SPACE) { + // This means that auto_resize failed. + console->error("The CQF is full and auto resize failed. Please rerun build with a bigger size."); + exit(1); } - dbg.insert(KeyObject(key,0,eq_id), QF_KEY_IS_HASH); // we use the count to store the eqclass ids - return added_eq_class; + return added_eq_class; } template -void ColoredDbg::add_bitvector(const BitVector& vector, uint64_t - eq_id) { +void ColoredDbg::add_bitvector(const BitVector& vector, + uint64_t eq_id) { uint64_t start_idx = (eq_id % mantis::NUM_BV_BUFFER) * num_samples; for (uint32_t i = 0; i < num_samples/64*64; i+=64) bv_buffer.set_int(start_idx+i, vector.get_int(i, 64), 64); if (num_samples%64) - bv_buffer.set_int(start_idx+num_samples/64*64, vector.get_int(num_samples/64*64, num_samples%64), num_samples%64); + bv_buffer.set_int(start_idx+num_samples/64*64, + vector.get_int(num_samples/64*64, num_samples%64), + num_samples%64); } template void ColoredDbg::bv_buffer_serialize() { BitVector bv_temp(bv_buffer); if (get_num_eqclasses() % mantis::NUM_BV_BUFFER > 0) { - bv_temp.resize((get_num_eqclasses() % mantis::NUM_BV_BUFFER) * num_samples); + bv_temp.resize((get_num_eqclasses() % mantis::NUM_BV_BUFFER) * + num_samples); } BitVectorRRR final_com_bv(bv_temp); std::string bv_file(prefix + std::to_string(num_serializations) + "_" + - mantis::EQCLASS_FILE); + mantis::EQCLASS_FILE); sdsl::store_to_file(final_com_bv, bv_file); bv_buffer = BitVector(bv_buffer.bit_size()); num_serializations++; @@ -270,7 +275,8 @@ void ColoredDbg::serialize() { // dump eq class abundance dist for further analysis. std::ofstream tmpfile(prefix + "eqclass_dist.lst"); for (auto sample : eqclass_map) - tmpfile << sample.second.first << " " << sample.second.second << std::endl; + tmpfile << sample.second.first << " " << sample.second.second << + std::endl; tmpfile.close(); } } @@ -301,8 +307,8 @@ ColoredDbg::find_samples(const mantis::QuerySet& kmers) { uint64_t len = std::min((uint64_t)64, num_samples - w * 64); uint64_t wrd = eqclasses[bucket_idx].get_int(bucket_offset, len); for (uint32_t i = 0, sCntr = w * 64; i < len; i++, sCntr++) - if ((wrd >> i) & 0x01) - sample_map[sCntr] += count; + if ((wrd >> i) & 0x01) + sample_map[sCntr] += count; bucket_offset += len; } } @@ -315,7 +321,7 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg::max()); + bool is_sampling = (num_kmers < std::numeric_limits::max()); struct Iterator { QFi qfi; @@ -335,14 +341,14 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg rhs.key(); } const typename key_obj::kmer_t& key() const { return kmer; } - private: + private: void get_key() { uint64_t value, count; qfi_get_hash(&qfi, &kmer, &value, &count); } }; - struct Minheap_PQ { + struct Minheap_PQ { void push(const Iterator& obj) { c.emplace_back(obj); std::push_heap(c.begin(), c.end(), std::greater()); @@ -357,7 +363,7 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg c; }; Minheap_PQ minheap; @@ -366,7 +372,7 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbgget_cqf()); if (qfi.end()) continue; minheap.push(qfi); - } + } while (!minheap.empty()) { BitVector eq_class(num_samples); @@ -393,20 +399,21 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbginfo("Serializing bit vector with {} eq classes.", get_num_eqclasses()); bv_buffer_serialize(); } } else if (counter > num_kmers) { - // Check if the sampling phase is finished based on the number of k-mers. + // Check if the sampling phase is finished based on the number of k-mers. break; - } + } while(!minheap.empty() && minheap.top().end()) minheap.pop(); } @@ -428,14 +435,17 @@ ColoredDbg::ColoredDbg(uint64_t qbits, uint64_t key_bits, uint32_t seed, std::string& prefix, uint64_t nqf) : dbg(qbits, key_bits, hashmode, seed), bv_buffer(mantis::NUM_BV_BUFFER * nqf), - prefix(prefix), num_samples(nqf), num_serializations(0), start_time_(std::time(nullptr)) {} + prefix(prefix), num_samples(nqf), num_serializations(0), + start_time_(std::time(nullptr)) { + dbg.set_auto_resize(); + } template ColoredDbg::ColoredDbg(std::string& cqf_file, std::vector& eqclass_files, std::string& sample_file) - : dbg(cqf_file, CQF_FREAD), bv_buffer(), start_time_(std::time(nullptr)) { +: dbg(cqf_file, CQF_FREAD), bv_buffer(), start_time_(std::time(nullptr)) { num_samples = 0; num_serializations = 0; diff --git a/src/query.cc b/src/query.cc index ce0d790..bb526f3 100644 --- a/src/query.cc +++ b/src/query.cc @@ -66,7 +66,6 @@ void output_results(mantis::QuerySets& multi_kmers, } } - void output_results_json(mantis::QuerySets& multi_kmers, ColoredDbg*>, KeyObject>& cdbg, std::ofstream& opfile) { From e96e251cd4efb16531b102ae253ad733d567fcf7 Mon Sep 17 00:00:00 2001 From: Prashant Pandey Date: Thu, 23 Aug 2018 01:33:52 -0700 Subject: [PATCH 107/122] Fixing a minor corner condition in find_samples code. --- include/coloreddbg.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 0f668c9..aeaec85 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -262,7 +262,7 @@ void ColoredDbg::serialize() { dbg.serialize(prefix + mantis::CQF_FILE); // serialize the bv buffer last time if needed - if (get_num_eqclasses() % mantis::NUM_BV_BUFFER > 1) + if (get_num_eqclasses() % mantis::NUM_BV_BUFFER > 0) bv_buffer_serialize(); //serialize the eq class id map From 542ea3ccda7d7e0645bf13b77b5c2947713bff72 Mon Sep 17 00:00:00 2001 From: Prashant Pandey Date: Thu, 23 Aug 2018 02:16:23 -0700 Subject: [PATCH 108/122] Fixing a minor bug in the itertor in coloreddbg.h --- include/coloreddbg.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index aeaec85..31fba2d 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -218,7 +218,7 @@ bool ColoredDbg::add_kmer(const typename key_obj::kmer_t& } // we use the count to store the eqclass ids - int ret = dbg.insert(KeyObject(key,0,eq_id), QF_KEY_IS_HASH); + int ret = dbg.insert(KeyObject(key,0,eq_id), QF_NO_LOCK | QF_KEY_IS_HASH); if (ret == QF_NO_SPACE) { // This means that auto_resize failed. console->error("The CQF is full and auto resize failed. Please rerun build with a bigger size."); @@ -328,7 +328,8 @@ cdbg_bv_map_t<__uint128_t, std::pair>& ColoredDbg Date: Fri, 24 Aug 2018 15:53:43 -0400 Subject: [PATCH 109/122] This works correctly ;p --- include/deltaManager.h | 10 +++++---- src/CMakeLists.txt | 2 -- src/colorEncoder.cc | 47 ++++++++++++++++++++++++++++++++++-------- src/walkMSF.cc | 26 +++++++++++++++++++---- 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/include/deltaManager.h b/include/deltaManager.h index ee1d047..e37a6a4 100644 --- a/include/deltaManager.h +++ b/include/deltaManager.h @@ -125,22 +125,24 @@ class DeltaManager { std::cerr << "\nSerializing Deltas:\nTotal delta count:" << totDeltaCnt << " Total color count:" << colorCnt << "\n"; - std::string deltabv_file = prefix + "/delta.bv"; + std::string deltabv_file = prefix + "/deltas.bv"; std::string boundarybv_file = prefix + "/boundary.bv"; sdsl::int_vector<> deltabv(totDeltaCnt, 0, slotWidth); sdsl::bit_vector boundarybv(totDeltaCnt, 0); - uint64_t j = 0; - boundarybvbv[0] = 1; // TODO careful to add an if in case we're gonna change zero to something other than 0 + uint64_t j = 1; + boundarybv[0] = 1; // TODO careful to add an if in case we're gonna change zero to something other than 0 for (uint64_t i = 1; i < colorCnt; i++) { - std::cerr << i << " "; + //std::cerr << i << " "; auto dltas = getDeltas(i); for (auto dlt : dltas) { + if (dlt == 0) {std::cerr << i << " ";} deltabv[j] = dlt; j++; } boundarybv[j-1] = 1; } + std::cerr << "\n"; bool deltabvSuccessfullyStored = sdsl::store_to_file(deltabv, deltabv_file); bool boundarybvSuccessfullyStored = sdsl::store_to_file(boundarybv, boundarybv_file); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4e7c35d..cdde09c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -66,7 +66,6 @@ endif() install(TARGETS mantis RUNTIME DESTINATION bin) -#[[ add_executable(msf MSF.cc) target_include_directories(msf PUBLIC $) @@ -93,7 +92,6 @@ target_include_directories(walkCqf PUBLIC target_link_libraries(walkCqf mantis_core) set_property(TARGET walkCqf PROPERTY INTERPROCEDURAL_OPTIMIZATION True) -]] diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index cb8f46e..e0f7f05 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -54,7 +54,7 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v } bool ColorEncoder::serialize(std::string prefix) { - std::string parentbv_file = prefix + "/parent.bv"; + std::string parentbv_file = prefix + "/parents.bv"; parentbv.resize(colorClsCnt); bool parentSuccessfullyStored = sdsl::store_to_file(parentbv, parentbv_file); parentbv.resize(0); @@ -133,7 +133,7 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { uint64_t numWrds = numSamples/64+1; eq.reserve(numWrds); - std::vector flips(numSamples); + std::vector flips(numSamples, 0); std::vector xorflips(numSamples, 0); uint64_t i{eqid}; std::vector deltaIndices; @@ -142,14 +142,14 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { uint32_t iparent = parentbv[i]; //std::cerr << "\nwalking mst\n"; while (i != zero) { - if (lru_cache.contains(i)) { + /*if (lru_cache.contains(i)) { const auto &vs = lru_cache[i]; for (auto v : vs) { xorflips[v] = 1; } foundCache = true; break; - } + }*/ //std::cerr << " " << i; deltaIndices.push_back(i); i = iparent; @@ -169,6 +169,9 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { // return the indices of set bits uint64_t one = 1; for (auto i = 0; i < numSamples; i++) { + /*if (i == 0 and (flips[i] ^ xorflips[i])) { + std::cerr << eqid << " " << flips[i] << " " << xorflips[i] << "\n"; + }*/ if (flips[i] ^ xorflips[i]) { eq.push_back(i); } @@ -201,17 +204,36 @@ std::vector ColorEncoder::buildColor(const sdsl::bit_vector &bv) { std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { //std::cerr << "\nhamming dist between " << i << "," << j << "\n"; - std::vector res{0}; + std::vector res; + //std::cerr << "\n"; auto n1 = buildColor(i); auto n2 = buildColor(j); + /*std::cerr << "\nLook\n"; + for (auto cc : n1) { + std::cerr << cc << " "; + } + std::cerr << "\n"; + for (auto cc : n2) { + std::cerr << cc << " "; + } + std::cerr <<"\n";*/ //std::cerr << " merge, "; // merge // with slight difference of not inserting values that appear in both vectors uint64_t i1{0}, i2{0}; while (i1 < n1.size() or i2 < n2.size()) { if (i1 == n1.size()) { - //std::cerr << " i1=n1: " << n1.size() << " " << n2.size() << " "; +// std::cerr << " i1=n1: " << i1 << " " << i2 << ", "; +// for (auto kk : n2) { +// std::cerr << kk << " "; +// } +// std::cerr <<"\n"; copy(n2.begin()+i2, n2.end(), back_inserter(res)); +// std::cerr << res.size() << ", "; +// for (auto kk : res) { +// std::cerr << kk << " "; +// } +// std::cerr <<"\n"; i2 = n2.size(); } else if (i2 == n2.size()) { @@ -221,21 +243,28 @@ std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { } else { if (n1[i1] < n2[i2]) { + //std::cerr << "pushing n1[i1] " << n1[i1] << ", "; res.push_back(n1[i1]); i1++; } else if (n2[i2] < n1[i1]) { + //std::cerr << "pushing n2[i2] " << n2[i2] << ", "; res.push_back(n2[i2]); i2++; } else { //n1[i1] == n2[i2] both have a set bit at the same index + //std::cerr << i1 << " " << i2 << " to "; i1++; i2++; + //std::cerr << i1 << " " << i2 << ", "; } } } /*for (auto r : res) { - std::cerr << r << " "; - } - std::cerr << "\n";*/ + if (r == 0) { + std::cerr << "haming: " << i << " " << j << " " << n1.size() << " " << n2.size() << "\n"; + exit(1); + } + }*/ + //std::cerr << "\n"; return res; } diff --git a/src/walkMSF.cc b/src/walkMSF.cc index 4fb5509..fdf9b94 100644 --- a/src/walkMSF.cc +++ b/src/walkMSF.cc @@ -16,6 +16,7 @@ #include "lru/lru.hpp" #include "tsl/hopscotch_map.h" #include "nonstd/optional.hpp" +#include struct QueryStats { uint32_t cnt = 0, cacheCntr = 0, noCacheCntr{0}; @@ -93,7 +94,7 @@ class MSFQuery { sdsl::load_from_file(deltabv, indexDir + "/deltas.bv"); sdsl::load_from_file(bbv, indexDir + "/boundary.bv"); sbbv = sdsl::bit_vector::select_1_type(&bbv); - zero = parentbv.size() - 1; // maximum color id which + zero = 0; //parentbv.size() - 1; // maximum color id which std::cerr << "Loaded the new color class index\n"; std::cerr << "--> parent size: " << parentbv.size() << "\n" << "--> delta size: " << deltabv.size() << "\n" @@ -105,6 +106,7 @@ class MSFQuery { RankScores* rs, nonstd::optional& toDecode, // output param. Also decode these bool all = true) { + eqid++; (void)rs; std::vector flips(numSamples); std::vector xorflips(numSamples, 0); @@ -139,6 +141,7 @@ class MSFQuery { toDecode = iparent; } } + //std::cerr << i << "->" << iparent << ", "; i = iparent; iparent = parentbv[i]; ++queryStats.totSel; @@ -159,6 +162,7 @@ class MSFQuery { } } */ + //std::cerr << "\n"; uint64_t pctr{0}; for (auto f : froms) { bool found = false; @@ -166,9 +170,11 @@ class MSFQuery { //auto j = f; uint64_t offset{0}; auto start = f; + //std::cerr << "\n" << f << "\n"; do { wrd = bbv.get_int(start, 64); for (uint64_t j = 0; j < 64; j++) { + //std::cerr << deltabv[start+j] << ","; flips[deltabv[start + j]] ^= 0x01; if ((wrd >> j) & 0x01) { found = true; @@ -177,7 +183,9 @@ class MSFQuery { } start += 64; } while (!found); + //std::cerr << " -- "; } + //std::cerr << "\n"; if (!all) { // return the indices of set bits std::vector eq; @@ -406,13 +414,23 @@ int main(int argc, char *argv[]) { if (newEq != oldEq) { std::cerr << "AAAAA! LOOOSER!!\n"; std::cerr << cntr << ": index=" << idx << "\n"; - std::cerr << "new size: " << newEq.size() << " old size: " << oldEq.size() << "\n"; + //std::cerr << "new size: " << newEq.size() << " old size: " << oldEq.size() << "\n"; + std::cerr << "n "; for (auto k = 0; k < newEq.size(); k++) { - std::cerr << newEq[k] << " " << oldEq[k] << "\n"; + std::cerr << std::bitset<64>(newEq[k]); } - std::exit(1); + std::cerr << "\no "; + for (auto k = 0; k < oldEq.size(); k++) { + std::cerr << std::bitset<64>(oldEq[k]); + } + std::cerr << "\n"; + //std::exit(1); } + //std::cerr << "\n"; cntr++; + /*if (cntr == 20) { + std::exit(1); + }*/ if (cntr % 10000000 == 0) { std::cerr << cntr << " eqs were the same\n"; } From 8f3a64e324b28aa47b109dee2ce1901e734674ad Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 24 Aug 2018 16:10:27 -0400 Subject: [PATCH 110/122] cleaning the code --- include/coloreddbg.h | 3 +- include/deltaManager.h | 2 - src/colorEncoder.cc | 87 ++---------------------------------------- src/walkMSF.cc | 23 +---------- 4 files changed, 6 insertions(+), 109 deletions(-) diff --git a/include/coloreddbg.h b/include/coloreddbg.h index c94b601..15384fd 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -287,8 +287,9 @@ void ColoredDbg::serialize() { /*if (get_num_eqclasses() % mantis::NUM_BV_BUFFER > 1) bv_buffer_serialize();*/ colorEncoder->serialize(prefix); - + console->info("Done serializing the color class info"); // serialize the CQF + console->info("Serializing the CQF .. "); dbg.serialize(prefix + mantis::CQF_FILE); diff --git a/include/deltaManager.h b/include/deltaManager.h index e37a6a4..3629ec2 100644 --- a/include/deltaManager.h +++ b/include/deltaManager.h @@ -133,10 +133,8 @@ class DeltaManager { uint64_t j = 1; boundarybv[0] = 1; // TODO careful to add an if in case we're gonna change zero to something other than 0 for (uint64_t i = 1; i < colorCnt; i++) { - //std::cerr << i << " "; auto dltas = getDeltas(i); for (auto dlt : dltas) { - if (dlt == 0) {std::cerr << i << " ";} deltabv[j] = dlt; j++; } diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index e0f7f05..2face41 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -23,7 +23,6 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v } // case 1. edge from zero to the node if (!hasEdge(zero, eqId)) { - //std::cerr << "case1: " << eqId << " " << colorClsCnt << "\n"; updateMST(zero, eqId, setBits); addEdge(zero, eqId, setBits.size()); } @@ -40,12 +39,9 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v } } } - //std::cerr << "case2 " << eqId << " " << colorClsCnt << " " << newEdges.size() << "\n"; for (auto &newEdge : newEdges) { auto deltas = hammingDist(newEdge.first, newEdge.second); - if (updateMST(newEdge.first, newEdge.second, deltas)) { - //std::cerr << "case2 " << eqId << " " << colorClsCnt << "\n"; - } + updateMST(newEdge.first, newEdge.second, deltas); addEdge(newEdge.first, newEdge.second, deltas.size()); } if (newEdges.size()) @@ -64,7 +60,6 @@ bool ColorEncoder::serialize(std::string prefix) { // deltas should *NOT* be passed by reference bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector deltas) { // n2 > n1 - //std::cerr << "updateMST: n1= " << n1 << " , n2=" << n2 << " "; if (n1 > n2) { std::swap(n1, n2); } @@ -74,11 +69,6 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del // The only time that we will see the edge zero -> n2 is when n2 is observed for the first time if (n1 == zero) { parentbv[n2] = n1; - /*for (auto d : deltas) { - std::cerr << d << " "; - } - std::cerr << "\n"; - std::cerr << "insertDeltas 1\n";*/ deltaM.insertDeltas(n2, deltas); if (colorClsCnt < n2) colorClsCnt = n2+1; // n2 is the index @@ -106,21 +96,15 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del // starting from node n2 toward the c2, child node of the edge with weight w2 auto parent = n1; auto child = n2; - //6 8 0 0 4 - //std::cerr << "here: " << n1 << " " << n2 << " " << parentbv[n2] << " " << p1 << " " << p2 << " "; while (child != p2) { uint64_t tmp = parentbv[child]; - //std::cerr << " first: " << tmp << " then "; parentbv[child] = parent; auto prevDeltas = deltaM.getDeltas(child); - /*if (n1 == 6 and n2 == 8) - std::cerr << parent << "->" << child << " " << tmp << ", ";*/ deltaM.insertDeltas(child, deltas); deltas = prevDeltas; parent = child; child = tmp; } - //std::cerr << "\n"; return true; } @@ -140,26 +124,22 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { deltaIndices.reserve(numWrds); bool foundCache = false; uint32_t iparent = parentbv[i]; - //std::cerr << "\nwalking mst\n"; while (i != zero) { - /*if (lru_cache.contains(i)) { + if (lru_cache.contains(i)) { const auto &vs = lru_cache[i]; for (auto v : vs) { xorflips[v] = 1; } foundCache = true; break; - }*/ - //std::cerr << " " << i; + } deltaIndices.push_back(i); i = iparent; iparent = parentbv[i]; } - //std::cerr << "\n"; uint64_t pctr{0}; for (auto index : deltaIndices) { - //std::cerr << "getDeltas " << index << "\n"; auto deltas = deltaM.getDeltas(index); for (auto d : deltas) { flips[d] ^= 0x01; @@ -169,9 +149,6 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { // return the indices of set bits uint64_t one = 1; for (auto i = 0; i < numSamples; i++) { - /*if (i == 0 and (flips[i] ^ xorflips[i])) { - std::cerr << eqid << " " << flips[i] << " " << xorflips[i] << "\n"; - }*/ if (flips[i] ^ xorflips[i]) { eq.push_back(i); } @@ -183,7 +160,6 @@ std::vector ColorEncoder::buildColor(const sdsl::bit_vector &bv) { std::vector setBits; setBits.reserve(numSamples); uint64_t i = 0; - //std::cerr << " bv bitsize: " << bv.bit_size() << " "; while (i < bv.bit_size()) { uint64_t bitcnt = numSamples - i >= 64?64:(numSamples - i); auto wrd = bv.get_int(i, bitcnt); @@ -194,77 +170,38 @@ std::vector ColorEncoder::buildColor(const sdsl::bit_vector &bv) { } i+=64; } - /*std::cerr << " setBits: "; - for (auto s : setBits) { - std::cerr << s << " "; - } - std::cerr << "\n";*/ return setBits; } std::vector ColorEncoder::hammingDist(uint64_t i, uint64_t j) { - //std::cerr << "\nhamming dist between " << i << "," << j << "\n"; std::vector res; - //std::cerr << "\n"; auto n1 = buildColor(i); auto n2 = buildColor(j); - /*std::cerr << "\nLook\n"; - for (auto cc : n1) { - std::cerr << cc << " "; - } - std::cerr << "\n"; - for (auto cc : n2) { - std::cerr << cc << " "; - } - std::cerr <<"\n";*/ - //std::cerr << " merge, "; // merge // with slight difference of not inserting values that appear in both vectors uint64_t i1{0}, i2{0}; while (i1 < n1.size() or i2 < n2.size()) { if (i1 == n1.size()) { -// std::cerr << " i1=n1: " << i1 << " " << i2 << ", "; -// for (auto kk : n2) { -// std::cerr << kk << " "; -// } -// std::cerr <<"\n"; copy(n2.begin()+i2, n2.end(), back_inserter(res)); -// std::cerr << res.size() << ", "; -// for (auto kk : res) { -// std::cerr << kk << " "; -// } -// std::cerr <<"\n"; i2 = n2.size(); } else if (i2 == n2.size()) { - //std::cerr << " i2=n2: " << n2.size() << " " << n1.size() << " "; copy(n1.begin()+i1, n1.end(), back_inserter(res)); i1 = n1.size(); } else { if (n1[i1] < n2[i2]) { - //std::cerr << "pushing n1[i1] " << n1[i1] << ", "; res.push_back(n1[i1]); i1++; } else if (n2[i2] < n1[i1]) { - //std::cerr << "pushing n2[i2] " << n2[i2] << ", "; res.push_back(n2[i2]); i2++; } else { //n1[i1] == n2[i2] both have a set bit at the same index - //std::cerr << i1 << " " << i2 << " to "; i1++; i2++; - //std::cerr << i1 << " " << i2 << ", "; } } } - /*for (auto r : res) { - if (r == 0) { - std::cerr << "haming: " << i << " " << j << " " << n1.size() << " " << n2.size() << "\n"; - exit(1); - } - }*/ - //std::cerr << "\n"; return res; } @@ -274,27 +211,17 @@ std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) std::vector nodes1; std::vector nodes2; uint64_t n = n1; -// if (n1 == 6 and n2 == 8) -// std::cerr << "1 "; while (n != zero) { -// if (n1 == 6 and n2 == 8) -// std::cerr << n << " "; nodes1.push_back(n); n = parentbv[n]; } nodes1.push_back(zero); -// if (n1 == 6 and n2 == 8) -// std::cerr << "\n2 "; n = n2; while (n != zero) { -// if (n1 == 6 and n2 == 8) -// std::cerr << n << " "; nodes2.push_back(n); n = parentbv[n]; } nodes2.push_back(zero); -// if (n1 == 6 and n2 == 8) -// std::cerr << "\n3 lca="; auto &n1ref = nodes1; auto &n2ref = nodes2; if (n1ref.size() < n2ref.size()) { @@ -306,8 +233,6 @@ std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) for (uint64_t j = 0, i = n1ref.size()-n2ref.size(); j < n2ref.size(); i++, j++) { if (n1ref[i] == n2ref[j]) { lca = n1ref[i]; -// if (n1 == 6 and n2 == 8) -// std::cerr << lca; break; } } @@ -315,13 +240,9 @@ std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) // walk from n1 to lca and find the edge with maximum weight Edge e1; uint64_t i = 0; -// if (n1 == 6 and n2 == 8) -// std::cerr << "\n4 "; while (n1ref[i] != lca) { auto curW = getEdge(n1ref[i], n1ref[i+1]); if (e1.weight < curW) { -// if (n1 == 6 and n2 == 8) -// std::cerr << n1ref[i+1] << " " << n1ref[i] << " " << curW << "\n"; e1 = Edge(n1ref[i+1], n1ref[i], curW); } i++; @@ -345,11 +266,9 @@ std::unordered_set ColorEncoder::neighbors(duplicated_dna::canonical_k for (const auto b : duplicated_dna::bases) { uint64_t eqid{0}, idx; if (exists(b >> n, eqid)) { - //std::cerr << std::string(n) << "\n" << std::string(b >> n) << "\n"; result.insert(eqid); } if (exists(n << b, eqid)) { - //std::cerr << std::string(n) << "\n" << std::string(b >> n) << "\n"; result.insert(eqid); } } diff --git a/src/walkMSF.cc b/src/walkMSF.cc index fdf9b94..46a1ff7 100644 --- a/src/walkMSF.cc +++ b/src/walkMSF.cc @@ -31,7 +31,6 @@ struct QueryStats { uint64_t numSamples{0}; tsl::hopscotch_map numOcc; bool trySample{false}; - //std::unordered_map numOcc; }; @@ -141,7 +140,6 @@ class MSFQuery { toDecode = iparent; } } - //std::cerr << i << "->" << iparent << ", "; i = iparent; iparent = parentbv[i]; ++queryStats.totSel; @@ -154,27 +152,15 @@ class MSFQuery { queryStats.rootedNonZero++; ++height; } - // update ranks - /* - if (rs) { - for (size_t idx = 0; idx < froms.size(); ++idx) { - (*rs)[(height - idx)][parents[idx]]++; - } - } - */ - //std::cerr << "\n"; uint64_t pctr{0}; for (auto f : froms) { bool found = false; uint64_t wrd{0}; - //auto j = f; uint64_t offset{0}; auto start = f; - //std::cerr << "\n" << f << "\n"; do { wrd = bbv.get_int(start, 64); for (uint64_t j = 0; j < 64; j++) { - //std::cerr << deltabv[start+j] << ","; flips[deltabv[start + j]] ^= 0x01; if ((wrd >> j) & 0x01) { found = true; @@ -183,9 +169,7 @@ class MSFQuery { } start += 64; } while (!found); - //std::cerr << " -- "; } - //std::cerr << "\n"; if (!all) { // return the indices of set bits std::vector eq; @@ -414,7 +398,6 @@ int main(int argc, char *argv[]) { if (newEq != oldEq) { std::cerr << "AAAAA! LOOOSER!!\n"; std::cerr << cntr << ": index=" << idx << "\n"; - //std::cerr << "new size: " << newEq.size() << " old size: " << oldEq.size() << "\n"; std::cerr << "n "; for (auto k = 0; k < newEq.size(); k++) { std::cerr << std::bitset<64>(newEq[k]); @@ -424,13 +407,9 @@ int main(int argc, char *argv[]) { std::cerr << std::bitset<64>(oldEq[k]); } std::cerr << "\n"; - //std::exit(1); + std::exit(1); } - //std::cerr << "\n"; cntr++; - /*if (cntr == 20) { - std::exit(1); - }*/ if (cntr % 10000000 == 0) { std::cerr << cntr << " eqs were the same\n"; } From 66dcea9955bb95b69923fd25f03e4d4c4251d858 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Fri, 24 Aug 2018 17:36:02 -0400 Subject: [PATCH 111/122] resizing parentbv --- include/colorEncoder.h | 3 ++- src/colorEncoder.cc | 25 ++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/include/colorEncoder.h b/include/colorEncoder.h index 08f6e2f..7649b05 100644 --- a/include/colorEncoder.h +++ b/include/colorEncoder.h @@ -52,7 +52,7 @@ class ColorEncoder { numSamples(numSamplesIn), cqf(cqfIn), bvSize(approximateClrClsesIn), - parentbv(bvSize, 0, log2((double)bvSize)+5),//TODO take care of this constant!! + parentbv(bvSize, 0, ceil(log2((double)bvSize))),//TODO take care of this constant!! deltaM(numSamplesIn, bvSize, approximateDeltaCntPerClrCls), colorClsCnt(1), // start with the dummy node lru_cache(100000) @@ -60,6 +60,7 @@ class ColorEncoder { std::cerr << "\nColorEncoder Constructor: bvSize: " << bvSize << " parent size: " << parentbv.size() << " colorClsCnt: " << colorClsCnt << "\n"; + lru_cache.monitor(); } diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index 2face41..2ec3c50 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -50,9 +50,22 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v } bool ColorEncoder::serialize(std::string prefix) { + std::cerr << "\n\nCACHE STATS:\n"; + std::cerr << "total hits: " << lru_cache.stats().total_hits(); // Hits for any key + std::cerr << " total misses: " << lru_cache.stats().total_misses(); // Misses for any key + std::cerr << " total hit rate: " << lru_cache.stats().hit_rate() << "\n"; // Hit rate in [0, 1] std::string parentbv_file = prefix + "/parents.bv"; - parentbv.resize(colorClsCnt); + // resize parentbv if it is larger than required size + if (colorClsCnt < parentbv.size()) { + uint64_t newSize = colorClsCnt; + sdsl::int_vector<> parentTmp(newSize, 0, ceil(log2((double)newSize))); + for (uint64_t i = 0; i < newSize; i++) { + parentTmp[i] = parentbv[i]; + } + parentbv = parentTmp; + } bool parentSuccessfullyStored = sdsl::store_to_file(parentbv, parentbv_file); + std::cerr << " parentbv final size: " << parentbv.size() << " , bits: " << parentbv.bit_size() << "\n"; parentbv.resize(0); return deltaM.serialize(prefix) and parentSuccessfullyStored; @@ -63,8 +76,14 @@ bool ColorEncoder::updateMST(uint64_t n1, uint64_t n2, std::vector del if (n1 > n2) { std::swap(n1, n2); } - while (parentbv.size() < colorClsCnt) { - parentbv.resize(parentbv.size()+parentbv.size()/2); + if (parentbv.size() < colorClsCnt) { + uint64_t newSize = parentbv.size()+parentbv.size()/2 > colorClsCnt? + parentbv.size()+parentbv.size()/2 : colorClsCnt + 1; + sdsl::int_vector<> parentTmp(newSize, 0, ceil(log2((double)newSize))); + for (uint64_t i = 0; i < parentbv.size(); i++) { + parentTmp[i] = parentbv[i]; + } + parentbv = parentTmp; } // The only time that we will see the edge zero -> n2 is when n2 is observed for the first time if (n1 == zero) { From 328577ef63b8cfac485bafcdc313c7c5a00f595a Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Sat, 25 Aug 2018 16:07:39 -0400 Subject: [PATCH 112/122] haven't had bitvector.cc from the beginning --- src/bitvector.cc | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/bitvector.cc diff --git a/src/bitvector.cc b/src/bitvector.cc new file mode 100644 index 0000000..22c10d5 --- /dev/null +++ b/src/bitvector.cc @@ -0,0 +1,43 @@ +#include "bitvector.h" + +BitVector::BitVector(uint64_t size) : size(size) { + bits = sdsl::bit_vector(size); +} + +void BitVector::reset() { + bits = sdsl::bit_vector(size); +} + +bool BitVector::operator[](uint64_t idx) { + assert(idx < size); + return bits[idx]; +} + +void BitVector::set(uint64_t idx) { + assert(idx < size); + bits[idx] = 1; +} + +void BitVector::resize(const uint64_t len) { + bits.bit_resize(len); + size = len; +} + +BitVectorRRR::BitVectorRRR(std::string& filename) { + sdsl::load_from_file(rrr_bits, filename); + size = rrr_bits.size(); + DEBUG_CDBG("Read rrr bit vector of size " << size << " from file " << + filename); +} + +bool BitVectorRRR::operator[](uint64_t idx) { + assert(idx < size); + return rrr_bits[idx]; +} + +bool BitVectorRRR::serialize(std::string& filename) { + DEBUG_CDBG("Serializing rrr bit vector of size " << size << " to file " << + filename); + return sdsl::store_to_file(rrr_bits, filename); +} + From c8e6dd33b3d95ae0bf0a1af5d3bbbb6d3f3d4731 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Sat, 25 Aug 2018 16:11:02 -0400 Subject: [PATCH 113/122] and also haven't had build_eq_graph.cc from the beginning --- include/build_eq_graph.cc | 166 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 include/build_eq_graph.cc diff --git a/include/build_eq_graph.cc b/include/build_eq_graph.cc new file mode 100644 index 0000000..4bb85ec --- /dev/null +++ b/include/build_eq_graph.cc @@ -0,0 +1,166 @@ +#include +#include +#include +#include +#include + +#include "bitvector.h" +#include "sdsl/rrr_vector.hpp" +#include "hashutil.h" +#include "clipp.h" + +uint32_t hamming_dist(BitVectorRRR& v, uint64_t vec_length, std::vector& blengths, + std::vector& boffs, uint32_t i, uint32_t j, uint32_t thresh) { + auto d = [](uint64_t x, uint64_t y) -> int { + uint64_t res = x ^ y; + return __builtin_popcountll (res); + }; + + auto idx_i = i * vec_length; + auto idx_j = j * vec_length; + + uint64_t dist{0}; + uint64_t eqwords{0}; + for (size_t idx = 0; idx < blengths.size(); ++idx) { + auto wx = v.get_int(idx_i + boffs[idx], blengths[idx]); + auto wy = v.get_int(idx_j + boffs[idx], blengths[idx]); + dist += d(wx, wy); + eqwords += (wx == wy) ? 1 : 0; + if (dist > thresh) { return dist; } + } + if (dist == 0 ) { + if (eqwords < blengths.size()) { + std::cerr << "dist = " << dist << ", but eqwords = " << eqwords << " / " << blengths.size() << "\n"; + } + std::cerr << "for " << i << ", " << j << " eqwords = " << eqwords << "\n"; + } + return dist; +} + +int main(int argc, char* argv[]){ + using namespace clipp; using std::cout; using std::string; + uint32_t thresh{5}; + std::string fname;//{argv[1]}; + uint64_t vec_length{0};//= std::stoul(argv[2]); + uint64_t num_to_process{std::numeric_limits::max()};// = std::stoul(argv[3]); + bool help{false}; + auto cli = ( + value("input file", fname), + required("-l", "--length") & value("vec_length", vec_length) % "vector length", + option("-n") & value("nproc", num_to_process) % "maximum vectors to process (default = all)", + option("-t") & value("thresh", thresh) % "threshold of distance to report edges (default = 5)", + option("-h", "--help").set(help) + ); + + if (!parse(argc, argv, cli)) { + std::cerr << make_man_page(cli, argv[0]); + std::exit(1); + } else if (help) { + std::cerr << make_man_page(cli, argv[0]); + std::exit(1); + } + + BitVectorRRR v(fname); + + std::vector>> pattern_map; + auto num_buckets = std::ceil(vec_length/64.0); + pattern_map.resize(num_buckets); + + std::vector blengths; + std::vector boffs; + size_t offset{0}; + std::cerr << "["; + for (size_t i = 0; i < num_buckets; ++i) { + auto s = std::min(vec_length - offset, static_cast(64u)); + boffs.push_back(offset); + blengths.push_back(s); + std::cerr << " (" << offset << ", " << s << ")"; + offset += s; + } + std::cerr << " ]\n"; + std::cerr << "pattern map has " << num_buckets << " buckets\n"; + + auto tot_bits = v.bit_size(); + std::cerr << "length = " << tot_bits << "\n"; + + offset = 0; + uint64_t vec_idx{0}; + size_t max_num_vec = num_to_process; + while (offset < tot_bits and vec_idx < max_num_vec) { + for (size_t idx = 0; idx < blengths.size(); ++idx) { + auto w = v.get_int(offset + boffs[idx], blengths[idx]); + pattern_map[idx][w].push_back(vec_idx); + } + ++vec_idx; + offset = vec_idx * vec_length; + if ((vec_idx > 0) and (vec_idx % 500000 == 0)) { + std::cerr << "processed " << vec_idx << " vectors\n"; + } + } + + uint64_t s{0}; + std::vector means; + for (size_t b = 0; b < num_buckets; ++b) { + std::cerr << "bucket " << b << " has " << pattern_map[b].size() << " patterns (" << (static_cast(vec_idx) / pattern_map[b].size()) << ")\n"; + s += std::ceil((static_cast(vec_idx) / pattern_map[b].size())); + means.push_back((static_cast(vec_idx) / pattern_map[b].size())); + } + + std::unordered_map candidates; + std::unordered_set skip; + std::vector cd; + cd.reserve(s); + size_t nn{0}; + size_t nc{0}; + size_t m{std::numeric_limits::max()}; + std::cout << max_num_vec << '\t' << vec_length << '\n'; + for (size_t x = 0; x < max_num_vec; ++x) { + offset = vec_length * x; + for (size_t idx = 0; idx < blengths.size(); ++idx) { + auto w = v.get_int(offset + boffs[idx], blengths[idx]); + auto& c1 = pattern_map[idx][w]; + //cd.insert(cd.end(), c1.begin(), c1.end()); + if (c1.size() < 2*means[idx]) { + for(auto c : c1) { + if (c > x) { candidates[c] += 1; } + } + } + } + //std::sort(cd.begin(), cd.end()); + //auto last = std::unique(cd.begin(), cd.end()); + //nc += std::distance(cd.begin(), last); + + //std::cerr << "x = " << x << ", num candidates = " << candidates.size() << '\n'; + + for (auto& kv : candidates) { + //if (kv.second >= 35) { + nn++; + auto d = hamming_dist(v, vec_length, blengths, boffs, x, kv.first, thresh); + if (d==0) { + std::cerr << "WHAT?! " << x << " = " << kv.first << ", num matching blocks = " << kv.second << "\n"; + for(size_t j = 0; j < vec_length; ++j) { + std::cerr << v[x*vec_length + j]; + } + std::cerr << "\n\n"; + for(size_t j = 0; j < vec_length; ++j) { + std::cerr << v[kv.first*vec_length + j]; + } + std::cerr << "\n\n"; + } + bool within_bound = d <= thresh; + nc += (within_bound); + m = (d < m) ? d : m; + if (within_bound) { std::cout << x << '\t' << kv.first << '\t' << d << '\n'; } + //} + } + if (x > 0 and x % 10000 == 0) { + std::cerr << "x = " << x << " ( " << (nc / static_cast(x)) << ", " << (nn / static_cast(x)) << " : " << m << ")\n"; + } + candidates.clear(); + //cd.clear(); + } + + + + return 0; +} From 30458d97e620a66a0509dcff9c647d2d7796ce31 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Sat, 25 Aug 2018 16:12:14 -0400 Subject: [PATCH 114/122] the file ended up in the wrong place --- {include => src}/build_eq_graph.cc | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {include => src}/build_eq_graph.cc (100%) diff --git a/include/build_eq_graph.cc b/src/build_eq_graph.cc similarity index 100% rename from include/build_eq_graph.cc rename to src/build_eq_graph.cc From a6b6d987f238bc64a4820fbfbd3f28a88a0103e8 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Mon, 27 Aug 2018 15:31:05 -0400 Subject: [PATCH 115/122] adding the cache log, removing find() in addEdge. --- include/colorEncoder.h | 6 +++++- include/coloreddbg.h | 4 ++++ src/colorEncoder.cc | 10 +++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/include/colorEncoder.h b/include/colorEncoder.h index 7649b05..a13fcfa 100644 --- a/include/colorEncoder.h +++ b/include/colorEncoder.h @@ -44,7 +44,11 @@ struct pair_hash { class ColorEncoder { public: - + struct Stats { + uint64_t cache_hits{0}; + uint64_t tot_hits{0}; + }; + Stats stats; ColorEncoder(uint64_t numSamplesIn, CQF &cqfIn, uint64_t approximateClrClsesIn, diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 15384fd..677d481 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -286,6 +286,10 @@ void ColoredDbg::serialize() { // serialize the bv buffer last time if needed /*if (get_num_eqclasses() % mantis::NUM_BV_BUFFER > 1) bv_buffer_serialize();*/ + std::cerr << "cache hits: " << colorEncoder->stats.cache_hits << " " + << colorEncoder->stats.tot_hits << " " + << (colorEncoder->stats.cache_hits*100)/colorEncoder->stats.tot_hits << "%\n"; + colorEncoder->serialize(prefix); console->info("Done serializing the color class info"); // serialize the CQF diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index 2ec3c50..a703a8f 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -17,10 +17,11 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v std::unordered_set, pair_hash> newEdges; std::vector setBits; + stats.tot_hits++; if (!lru_cache.contains(eqId)) { setBits = buildColor(bv); lru_cache.emplace(eqId, setBits); - } + } else stats.cache_hits++; // case 1. edge from zero to the node if (!hasEdge(zero, eqId)) { updateMST(zero, eqId, setBits); @@ -144,8 +145,10 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { bool foundCache = false; uint32_t iparent = parentbv[i]; while (i != zero) { + stats.tot_hits++; if (lru_cache.contains(i)) { const auto &vs = lru_cache[i]; + stats.cache_hits++; for (auto v : vs) { xorflips[v] = 1; } @@ -156,7 +159,6 @@ std::vector ColorEncoder::buildColor(uint64_t eqid) { i = iparent; iparent = parentbv[i]; } - uint64_t pctr{0}; for (auto index : deltaIndices) { auto deltas = deltaM.getDeltas(index); @@ -314,9 +316,7 @@ void ColorEncoder::addEdge(uint64_t i, uint64_t j, uint32_t w) { if (i > j) { std::swap(i,j); } - if (edges.find(std::make_pair(i, j)) == edges.end()) { - edges[std::make_pair(i, j)] = w; - } + edges[std::make_pair(i, j)] = w; } bool ColorEncoder::hasEdge(uint64_t i, uint64_t j) { From 7429cb0b99373905fd1fc3bea2cb4b15298530b0 Mon Sep 17 00:00:00 2001 From: Fatemeh Almodaresi Date: Wed, 5 Sep 2018 12:51:33 -0400 Subject: [PATCH 116/122] Adding a new stats about how mst weight grows by adding new kmers --- include/colorEncoder.h | 18 +++++++++++++++--- include/coloreddbg.h | 4 ++-- include/deltaManager.h | 2 ++ src/colorEncoder.cc | 38 +++++++++++++++++++++++++++++++------- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/include/colorEncoder.h b/include/colorEncoder.h index a13fcfa..dde0fd9 100644 --- a/include/colorEncoder.h +++ b/include/colorEncoder.h @@ -47,12 +47,19 @@ class ColorEncoder { struct Stats { uint64_t cache_hits{0}; uint64_t tot_hits{0}; + uint64_t tot_edge_access{0}; + uint64_t tot_edge_access_request{0}; + uint64_t edge_access_for_updateMST{0}; + uint64_t add_edge{0}; + uint64_t parentbv_access_for_updateMST{0}; }; Stats stats; - ColorEncoder(uint64_t numSamplesIn, + ColorEncoder(std::string prefixIn, + uint64_t numSamplesIn, CQF &cqfIn, uint64_t approximateClrClsesIn, uint64_t approximateDeltaCntPerClrCls = 8) : + prefix(prefixIn), numSamples(numSamplesIn), cqf(cqfIn), bvSize(approximateClrClsesIn), @@ -61,7 +68,9 @@ class ColorEncoder { colorClsCnt(1), // start with the dummy node lru_cache(100000) { - std::cerr << "\nColorEncoder Constructor: bvSize: " + std::string f = prefix+"/weight.lst"; + weightDistFile = new std::ofstream(f); + std::cerr << "\nColorEncoder Constructor: bvSize: " << bvSize << " parent size: " << parentbv.size() << " colorClsCnt: " << colorClsCnt << "\n"; lru_cache.monitor(); @@ -70,16 +79,19 @@ class ColorEncoder { bool addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_vector &bv); - bool serialize(std::string prefix); + bool serialize(); private: uint64_t numSamples; uint64_t bvSize; uint64_t colorClsCnt; + uint64_t kmerCntr{0}; + std::ofstream* weightDistFile; sdsl::int_vector<> parentbv; DeltaManager deltaM; CQF &cqf; int k = 20; + std::string prefix; std::unordered_map, uint32_t, pair_hash> edges; LRUCacheMap lru_cache; diff --git a/include/coloreddbg.h b/include/coloreddbg.h index 677d481..674bd85 100644 --- a/include/coloreddbg.h +++ b/include/coloreddbg.h @@ -290,7 +290,7 @@ void ColoredDbg::serialize() { << colorEncoder->stats.tot_hits << " " << (colorEncoder->stats.cache_hits*100)/colorEncoder->stats.tot_hits << "%\n"; - colorEncoder->serialize(prefix); + colorEncoder->serialize(); console->info("Done serializing the color class info"); // serialize the CQF console->info("Serializing the CQF .. "); @@ -466,7 +466,7 @@ ColoredDbg::ColoredDbg(uint64_t qbits, uint64_t key_bits, uint64_t nqf) : dbg(qbits, key_bits, seed), bv_buffer(mantis::NUM_BV_BUFFER * nqf), prefix(prefix), num_samples(nqf), num_serializations(0), start_time_(std::time(nullptr)) { - colorEncoder = new ColorEncoder(num_samples, dbg, num_samples*100000, ceil(log2(num_samples))-3); + colorEncoder = new ColorEncoder(prefix, num_samples, dbg, num_samples*100000, ceil(log2(num_samples))-3); } diff --git a/include/deltaManager.h b/include/deltaManager.h index 3629ec2..91ebbf2 100644 --- a/include/deltaManager.h +++ b/include/deltaManager.h @@ -147,6 +147,8 @@ class DeltaManager { return deltabvSuccessfullyStored and boundarybvSuccessfullyStored; } + uint64_t getDeltaCnt() {return totDeltaCnt;} + private: uint64_t numSamples; uint64_t slotWidth; diff --git a/src/colorEncoder.cc b/src/colorEncoder.cc index a703a8f..80b5315 100644 --- a/src/colorEncoder.cc +++ b/src/colorEncoder.cc @@ -8,6 +8,10 @@ #include bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_vector &bv) { + kmerCntr++; + if (kmerCntr % 10000 == 0) { + (*weightDistFile) << deltaM.getDeltaCnt() << "\n"; + } // create list of edges to be processed // 1. zero to node // 2. list of neighbors @@ -17,11 +21,11 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v std::unordered_set, pair_hash> newEdges; std::vector setBits; - stats.tot_hits++; + //stats.tot_hits++; if (!lru_cache.contains(eqId)) { setBits = buildColor(bv); lru_cache.emplace(eqId, setBits); - } else stats.cache_hits++; + } //else stats.cache_hits++; // case 1. edge from zero to the node if (!hasEdge(zero, eqId)) { updateMST(zero, eqId, setBits); @@ -50,11 +54,23 @@ bool ColorEncoder::addColorClass(uint64_t kmer, uint64_t eqId, const sdsl::bit_v return false; } -bool ColorEncoder::serialize(std::string prefix) { - std::cerr << "\n\nCACHE STATS:\n"; - std::cerr << "total hits: " << lru_cache.stats().total_hits(); // Hits for any key - std::cerr << " total misses: " << lru_cache.stats().total_misses(); // Misses for any key - std::cerr << " total hit rate: " << lru_cache.stats().hit_rate() << "\n"; // Hit rate in [0, 1] +bool ColorEncoder::serialize() { + (*weightDistFile) << deltaM.getDeltaCnt() << "\n"; + weightDistFile->close(); + std::string statsf = prefix + "/stats.txt"; + std::ofstream stats_out(statsf); + stats_out << "\n\nCACHE STATS:"; + stats_out << "\n\ttotal hits: " << lru_cache.stats().total_hits(); // Hits for any key + stats_out << "\n\ttotal misses: " << lru_cache.stats().total_misses(); // Misses for any key + stats_out << "\n\ttotal hit rate: " << lru_cache.stats().hit_rate() << "\n"; // Hit rate in [0, 1] + stats_out << "\nEDGE STATS:"; + stats_out << "\n\t# of times searching for an edge: " << stats.tot_edge_access_request; + stats_out << "\n\t# of times edge not found: " << stats.add_edge; + stats_out << "\n\t# of times calling operator [] on edge map: " << stats.tot_edge_access; + stats_out << "\n\t# of times calling operator [] on edge map in updateMST: " << stats.edge_access_for_updateMST; + stats_out << "\n\t# of times accessing parentbv in updateMST: " << stats.parentbv_access_for_updateMST; + stats_out << "\n"; + stats_out.close(); std::string parentbv_file = prefix + "/parents.bv"; // resize parentbv if it is larger than required size if (colorClsCnt < parentbv.size()) { @@ -233,12 +249,14 @@ std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) std::vector nodes2; uint64_t n = n1; while (n != zero) { + stats.parentbv_access_for_updateMST++; nodes1.push_back(n); n = parentbv[n]; } nodes1.push_back(zero); n = n2; while (n != zero) { + stats.parentbv_access_for_updateMST++; nodes2.push_back(n); n = parentbv[n]; } @@ -262,6 +280,7 @@ std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) Edge e1; uint64_t i = 0; while (n1ref[i] != lca) { + stats.edge_access_for_updateMST++; auto curW = getEdge(n1ref[i], n1ref[i+1]); if (e1.weight < curW) { e1 = Edge(n1ref[i+1], n1ref[i], curW); @@ -273,6 +292,7 @@ std::pair ColorEncoder::maxWeightsTillLCA(uint64_t n1, uint64_t n2) Edge e2; i = 0; while (n2ref[i] != lca) { + stats.edge_access_for_updateMST++; auto curW = getEdge(n2ref[i], n2ref[i+1]); if (e2.weight < curW) { e2 = Edge(n2ref[i+1], n2ref[i], curW); @@ -312,6 +332,8 @@ bool ColorEncoder::exists(duplicated_dna::canonical_kmer e, uint64_t &eqid) { } void ColorEncoder::addEdge(uint64_t i, uint64_t j, uint32_t w) { + stats.tot_edge_access++; + stats.add_edge++; if (i == j) return; if (i > j) { std::swap(i,j); @@ -320,6 +342,7 @@ void ColorEncoder::addEdge(uint64_t i, uint64_t j, uint32_t w) { } bool ColorEncoder::hasEdge(uint64_t i, uint64_t j) { + stats.tot_edge_access_request++; if (i > j) { std::swap(i, j); } @@ -327,6 +350,7 @@ bool ColorEncoder::hasEdge(uint64_t i, uint64_t j) { } uint32_t ColorEncoder::getEdge(uint64_t i, uint64_t j) { + stats.tot_edge_access++; if (i > j) { std::swap(i, j); } From dd3ce40d695753ee359e7d9899b5792697bcb756 Mon Sep 17 00:00:00 2001 From: Prashant Pandey Date: Sun, 9 Sep 2018 12:56:15 -0700 Subject: [PATCH 117/122] Removing cutoffs and adding non-filtered squeakr files warning. --- data/SRR191403-k20-Cut1.squeakr | Bin 0 -> 1536774 bytes data/SRR191403_exact.ser | Bin 1536738 -> 0 bytes data/SRR191411-k20-Cut1.squeakr | Bin 0 -> 1536774 bytes data/SRR191411_exact.ser | Bin 1536738 -> 0 bytes raw/incqfs.lst | 3 ++- src/coloreddbg.cc | 16 +++++++++------- 6 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 data/SRR191403-k20-Cut1.squeakr delete mode 100644 data/SRR191403_exact.ser create mode 100644 data/SRR191411-k20-Cut1.squeakr delete mode 100644 data/SRR191411_exact.ser diff --git a/data/SRR191403-k20-Cut1.squeakr b/data/SRR191403-k20-Cut1.squeakr new file mode 100644 index 0000000000000000000000000000000000000000..c922c2f6a77402f723eccb926307c79519f1b43f GIT binary patch literal 1536774 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

9#BuvKp}s+KpYv5 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

yJKg0p;Af6mUfN0&tYnLqRoc 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

#2M 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

$ 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 r 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

QWP{Fm(m0W1JzS(N|-lr05 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

I~8nj6&xeC%sMsLzo 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%0AZdq)#ysCr((_{ob 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

_E7BeYFzL3 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%0A%3<$u*7X_0c; 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

HC!}c`}rH*@Igi!tmD42`Dt#tL#HL@G1!G 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­#|?x<@?_I3d6EGl`17^bt?Yptm`SfN6t3^ z|9*l!t;QS{g40vGKSI}(?9kjz6c~L7?>{2t*R&<^C#0FRlqXs7pH8!BDY3yRLd$M@TLfx1r{{2)<=SS zIIoB~Cz}2C_Bzlb43I8f>Rp8dp&e(>I(XiwGS@xde|Bk#`|C5N7gUx?f=!-($(e{^t*?e;CexbI68I%tMc-O~G?@;~X-jx2Xc**z0UDW}4 z8qwc#wocP$XLDvEeG};Cl}AQTS})by{-i`ic}lnkc;{m%`!DlrSO)^R5_D|~g$v`# zY^z(g!%8H2XDRIxyOOtMtSnY#W1RPmojdfQ0YiRC3v~q^Vn{hD+yZ`7t_RZ6iv$=n z{s+wm9$r*Ol^+GJJz(s~BMVv&IvC)E7K`jcQZ21_YVLUvRHMZLcpL>M?;9Xcavnz4vZ=q7SZv#fOs(fsaSFwQakehr5C zRC>3n91Mwegk&};Xl-cNB4$=DWNm2DA%+n$Taj_57PCu)Hvc(d!8>lAPH|~=83Pvn z-KWcaj^R!W3Ctme=M~=Rfxi6$hSdvyB!aMT0CP{RUjaLF&&>pqb{iB@$r7-cOW&4% z6`wcrU#X})t}x(ji@evP=h^R~uDCWWxq&lzRm!>7Cd6z=oiS4B*}5t`)^U1%()udA zj~fRwfg(T&YZG+;r1H|@#HR2K8S zmPl$x`Bv!nSf2fJdP=Fh+zLTekCgIQ!t(L<*H|2$-O;m<+|Zxp7@lGaPHyNis2+fm zBR<8*lcDLe{{b<20o>$ws>|>ISnNx3S)cRjBW~Y5n?HWZqbH3uladBCMjG+BWRU9^ zb_#QxP^VJlg<<^9MXH2@Y{&C56H5ir=lB^lv(M*$&>-9a(JpJ;Z^yp%V4MxRKD`B% zqLKuOs{c)G(SHI3n6z4Fb=2R=5S9@k5^&_v*o2ei+YhnEl=aZloPV@rQd>M`$e?$D z%dS>U`;3-r&`a|HMH!8p^*^}}xQ#ez{WxZHleJ8@R^=;_RjgrFYcY;wjotHcIMpSN zexZW;H1FjI@Gr=ZH$bub)_Dz!zr`VJ=s;)_x6lMuMd&B8m_T_DnDxP#$U$XeclNe@ zR4EW(R@;M(UocE6@8d1+d^a?XKCGF;B<{+c@PJF?8R&Oq{5GRLb1+U*GUaEntjxV= z>59!YNtdHpF7I2-erOrF_DjGz_Crc!uFJ^=%D=W4dh54a2y&n#a6 zR;G3eT=w_39oG7%oOIJw=EWGIrONF8KOxVZ4JP{ylknIxT~qVih&7&5c6a&~hmJGC zkj5SA*tvMGYX~@^reX#~1|uPF{RYye9^tRwR_uD30?`QvSuJqcK|}6Xu9S-FIB4n~ zGbRFw^X2LRGXmW!>5-|U#8P~dmoFQCaAu8PZOT_gCNgDO=&8;CNV6IqEkuS+y*LqZ zVmh&^4{J_)`58mYstg5;75kbq=%oHgWZB|tyU$C;AwjCeyYCDq;ChK6CVzUqn|p`K z{mBlAyw^j4guy^v$@pJpkMokI*rt?GgM|;fG?gFp8*@C}^Sg2A3Z~8s1;Y*#aIfn0u;#vx+xdqr3ZO&=Ss(+I6X*)G)SjTowK_?1ABZ+Vdr=1mA9dd+Mt0zz z-V6veD(`tnPoFm2m(}%*d+KUlEvs7?_dAhbQOWtQa9kGIlsag7?l#svP^4S6BWeY& z)9*`MDpc1v8Ep7^W>f`pIh07xS7N(tw=Vkg>DWP`KNe*NE4;PWL5ro+lb+x+)Hm(G zM#Lz?!$~gsrtm!2Bnx8%(`b28ReR+RDyYRu!7HQhmwS3?74UB0YMf_3y*n&O;mr}3 zcOE>>9eiTf$IENafb_GzrR$`}c7=Pq>OI57c6viasm}VOr!F<%omYIojj=Om?<=>F zjZ@*Tj*TzNVxtrp96zt7BBr5!wq&|u59{rNXY**2S3ZnP6x#YcXsSE6n&r}K7pe-XVEF|l;OQ;2cu$=swi-+{(#z;AJ|97g`@+xn zvj5;%N2+a6ZMNi3AlJWDH9Z3<2rG|#=M%BqAq73ny6_Q9PTdeVP7P`N{I#)pJU*>N zaRg*D(~XUhzpSV&8Rv#zuf9m-|j0hx9 zKaJDwCAl?8o&#))+$V=xyW%dIk_Ij2qD1X^a8L&}qzoX!m|XdR58m34Fr>8Q#oO zi#|L9t4-VKbYsw53lA(yVqLBS8+zG$&H?iTzi;I6J~_c{zeQ0`H}-pHZ#TX=Ty*_1 z^J^w0_7=Dx6GF6%O$&E2$)=s)M=caPt1ExG#)`b>zq+^0vI}|PJ zm`(m~LhHkS1t!WVp|9W|Y&c|J=C`C{;JB60X~>?7al*S zXyB(|kQMbu__xErcRPbjkflPu1wh9!psIZW%pZMF9`=1=c?ixyvY^7OZj zBZt3y*?8D?fz*Uk21r?eb;n;E?_$J-)9>7>Z+vIOeM@bQuz|{m2ToP7AC<6rDrNAS z;A-G-0P5+i*Fh&~VWj7q#hz@%I>#=4x~xivrB*l-Q=@y~9EN$bKb;63Wj%Xh-+AeJ z->(Smx zjAHR|Pc(4l_A+4Iz*+F{lZzy=%B;n?uR?GoCG8G~*-<=_s4$W_?DxR)w6icPeeK(Q z6SlA_20$*oGQ0>(0JBxRiOrX09q~b+(Hv+-dQzFt2wk&__~0=%6F zg#UtjV*t0-s?r7WfM&ST!;=|HTO?DvP*vco(xJGEElv zoB+L3oCu{NX!=*)d`lARn)T4|-XKdjsh#aAm#IoaCYstq3f?(WYe#<%bpkUJN^ai; zA@1Z8UtY@-bpA=lrtpn}=}%A^m7_$T$VNQw5xBI@+G(MoEJNQrjo@M(3NZ~MDjB=$ zD_U?i#biUTS0f24Kv)ZXgrJ5R670MF>EglrEd5xcUT~CxcjX><7{&4k_Q)lLZi$8A zQ5TA0{i~qgs3L&+nR12!8EVwF$YC+1H{akZk9XJTOzc!P!cauVanq1@gEbKQ%*eO) zk_u#{xAnrCYInj60~P*iU-tXH+xXUnzTCPOr)NaBU$`8nZE>8t+)H3}l-es}cO#c!1h8K%&I3i_TmKt(e{=et0s{Tq1L z3k7yKv4oSX16Jj3w!BG^5mx1OPP}|)C+1BTI(g=^Q`(Nk+bLtbnAiTmrt>q+85S>< z7h=N5(gDE(sC*iN4VkqZF&miokFb3`H2Y#fmP zq78J#bYj~^fEk^eF(|P9$cF;A_5_1*_k-gfWq6wwuR7&E{Id zo%*1wm$giBVR7+O{{z-bUFRz4&EQoqK=zph^L?xvkY(n{k8!74qNt1sPF{td<=8$0 zdnvcxvr%S-X;K~|x!XwB+l~-o#oE#GW3e>ZdKbL_B1&=90(D9@QmmQ=)Pk7*Jr5Ys zi|3e)Sju+ZBZ2;8OlXyS1Vz~LggFM%m0{c=AL{LWBh<;|ft6ov-Q}Y;$1%i2BZ$(l zal)R5!?;Wpo&bgy;kT^ms>BqT(uog~o;47)ahX`8C z6nUza(|zfbO}fh(O{=UxXU^cZ{G+g(C)>D*Z~gUT84OdF=ZK7K8TL{&d{BX`+Agv# z5|1LsxWEskk@85f`$Ws7Zbe>)v`MxURaUB5+!8ZzY6SG^fYt->io5Be?$)-+Zy{J zj%6JjQ*rx8C!w;$$TuBvdAA2dQA6?HRgQW0l+BYreUa~{snd#K)a|CrPrNmM0lU(?p*lgw$x-AVs zGB}@K8V)l42S(zc*}iVtdO4w$w8vAVSBhMEJj2Ve>~#8Wyx%UR2OVE*3o2S zK3o5g>o8Hcu%ggr-U%y3VvXHJpSMPYOpI6k3_AoqSMk)(%a5g%PnXUluRi+ijL=>?Urar) zFAw{9COC+F!g){6hx=pfcX#dY`2fE5zy_#QKAIW%Mo8Ou6{D;V6d#9MJa4r>Cw+PM z_J{l0dsxwEXdqF^Ov&FLMO+F23e+&y$5ADg-lMMNQ703wdW9Y6%cqIg09xcq7^}mi zzG=aSUA^$F6|kRXZjCTZ{m721owtF)`+b2^M$)(a9S*O6)*S`V$;}&Kae30>o787+ z>i|+-twoWU$}w8rG0UP<6?ULYX-hfZYK#q|@H`x8(jPm+Y+N7+5bPHD85GLNE|#e5ReBM3HwwclNZC=q;)1a#)1ruQheB6KI$j^Gn4F@yk=iVtYAe zp7vew>w%+ll`4kZCqDiY%qe3HKj82uo^@^FZ}&vac-AY4A3Rg-i6+|Hpi>OSdV4@g zjk}2&A{{TGd@R5+)QeFcB+7rjP+f#CQeUcj*Z9H&Fc|rI7p7v0etqw){OdwtQz7Z< zX}famM7NmhL)Mb8D+Ne2cW+!un6;N&$Yssj+whYq8jBr<0c=Men(2Nj?Y|YpgJRN8 zs2-zaEp?@zuwsqOT8c`N0B-PGh@;E&jl?!zwh?$Z4`IX;!=Y5IgG%{F*zB_86eC|C z&FKCRAOY89f=c;M(cQnWoexn$DrxwjH9ea{Z}y%8>Gi((3b*ulP?3*oTFh}lb-;u}{-^Y1)Xbh7TKAp_N;0 zLc5TA=$FrxeeMZxEZ+o$lTUa}IOKbOefQYHJptX*>kqc^$f%UsU1}M&n}j}c#P)kyfWP^>R@m~$bVx^6d8T=*fChA=$`NE!M9>iy@( z2HY|3AoOQ+jtn+*TLPNfzyh;z_?x~#KCxOGLZ5XG=ub=+6oRZ)zXLn`wTJK0ZJr@r z)-yNSn>L-;6b=U&$dB3o;{bH+c3zE6LPluAe<9!376OJ6>qlLse=96sy1Q8)V$ZUC zY3wWXjw`E~}fJ)Wy^uhE75g+Nbc1eSp8~gI&PZ(8au;lKW(^%oW-NpvOg`#q<2 zt(0cj5Z1E0Z5ipBQ0YMbPFSBSyWi^gKH}%%Yc;^=%=2=1C&PV-I zAl|XAi`+7b?4T8ti%F~-Y+i$QQ2QYJA!2UCsjtv~So`1@p&6WZrgqDGGU z1b(z{)>{y%qhXyjO)4b~9zP(+rSulkVL>v;gHpN=ll7QTcsu8ri)J47-UQu$(hDUp zCW&8oC1B;bKYO;fWfL1gM zHjCr~vr=VtfUGY|E^1Fp$L+D3j2e;d{_rb&@J!JnIULDPZ)u|qedukL{?b?ni%0L~ z#zv+*IzIWeL;QtNco8-mtQvWE7aJB5SO+BAP4?u8%<(ac|Bb06zMcf+HKL`i zol+la_vC&)RQ%23!gh34jaNZ`=76jd1v;tN6W^N0O4l*k%J2!sDtRv&Y;n!t9z4BG z?ek#*U6hZyN>r0OlC91v<4t~if1dGidK0q|SM+aq+@V2V1f)q?{ zb;cume8E!tG@#JCKjRwzq2hf{EiOZhU^wDNgtet+JcHgAn&ls~*uk0pDlV$&6iL(L zVW|pP?>xhB<~Egl>^#v)mp{UJJLtr@@$^>*)stF*vrY&uq~A|tqgEKX8~VTgx<`w? zbSXvsfyCyYhd7kX9=1RfJGy(zWrOYiqIpXX7MS) zUdSv#1QUQg@@B1Xr-j4r#lG;-p~Beda6AmawNr|;)R^_|De1a2Nc&E$Y)ON;hM>RZ z^@|o{bj^{#d(bS5cLR!*%c!6Rh5=h-J* zgxLwpHSV`u{)JIh63S_awG|v$i>3c5HL*)Jlp5Szg0JzVsNyA=X)$hNa`pins#Q^MQv#I;4+}!+>APxPpmk=3-MP-MeeYUgD3^@dJtm*_yER!=Z%@ zU6%Bm-QTu6GZb8nvrGE@9iMg*YZZBZ4H#3wz6TAkEVEdBydiz9_qO112o{Vr=Z6Ho z=!`7>`1r%2dGo*8gGbKaGRLRTPl0HQ5CH@FA;*Jy1x??<`bPK_|3vpl$JKfgqoJh0 zCJ#yA+Q`RuiQE1wN1tvdtD3tHpRc6WcOCg{E4n4MMq0}sICUsxGQ zR9}Vew`2KTF8A!w1pjTv# zw!+A<9Qw@Vov5(j`&a}ZgXrI;!YQP+P1!R4!+g0*mq3-f)%|O-dCNDKqW1Fjzt-Lk z^X}asj+_!$2V{cRAQc>HPBJ0L^pMb5#79W4sVa;k=1cT2+&{h{$A{+_IIqaJqjkU^&aFDPrx#Q0F@*G*tJO< zevYm4>9fAprLp4##0CsB{v@6l8MYy9y7>8o{L~?W#6RhFnQF%3 zNWRqd>=uF#xx0{Qq%Hx6lD4D zvu1_FCxWckpHZo(Ji%PVMFJCe2O?@qC0vYY!=i{J%c%KeMLX_hh(A`vA&uhbyu@bkm@vP9?HKbh9xy7(lvqUw`pVb>Oft2lPCOuD@Zd;s%KP z`R^x`$G?erfgO4+<1*U>MoO>t!6u+srHZ9sBG)KEt)QvN-#n_Vg!gYaqF~3YfyD0y zJh4w(rf~3H0h%)}<0*Y`8)Gh|!h3EVfaJd_>ks-ypPBm2*aJ#Mi{OG;osDPtLzO?W z8Yic%5dCUPBQ4UyJi6Ptg%-I$1PsffZTap{TK{(C=o4@AVklm`Md6{hlWuEuJ%C1; zm$yBb!e#@R!uxQ9LIAD&Zj7l2oJp#0V98TY63`EN;rgk%u$JfMRFrbRVx?$Tk?198 zIm0|N{3{>-OCAl2)`r~&U`0x+VzEDVZN()#!IavfCW&aK- zZ}orq-rBce(NFFK36hT00^L#?2zuJ4og)Ms$+X`>j7rP#zYo*S>7hRPAIRAveAeur zlC?$gW_Eiz_b;ls%Br(gYa{=PgmaI9k9MKs0>t79Z_p3_7xq73{+aLwV^w%;MN#yX zQ-)ZyPi5+BrRH%Q&8^(Qr=5d7GR*|q3I@gyirRtXyv4Dj9$$v`YvS$@_6*f4^U$w{ zB->hJ==PUE>I}&WiL^j0KbGY}qvZwlrArPAd~6H0B8XzCrN1CNVj#KVg~iQdYgVnO z6{!Smc+S_caz@?A{n5~Kr>*9n-?Kaq-u-Fa&jZ_S-nDW{ezg}YJ$@OcWS<7*s{7;wdwi#)#He+vg0;$n|;NPyYd)pC)v{bpJ8Ab zE@#PQtRH_0c1(JC?p(t@#JW(kV}p>>ch=it-V9i51aF_`VqOVeaobj!4eKCAu&Tok zSmPVBR-AurY~Xr?BUk{DyG$h@A3!Z$K+dL}=Qf&Qe`AYQvBPJuT8NInQ} zHO&L|_#bwGDGJFvuQo&J;yCGmz>{t5toIKE6P}MT-~>|q)3hq;ozziTp=$6G>W7F$ z0c4(QEeizsGQmLdT!r%@Z8ig$yyh_X=P*G4uVy)BHBIIl_?{o*uHzMhnHE^#&@tj* ziE=bk+uQ}LK{&syNgaNDbKB+4{G~NfBxTJKO}2=#uc!2n6`A;?MJ!u&OTp63!riv7 zNN6oKe@X)9q+r4IL!DVP`NA@Vdgy4Z3%am5v(RR!|AC0y;uw9SScYc*JF~j4%<;@6 z&rcqdDlmV*|NLZ`)OtG?0h%3VE=WseK%pY6r*dn~J^c*mZk%EKiq{wOGF#d&H|E+m zVfn#I#S=VWHGOj5uW;}6{XM{#>Dg6b8~))067u(hRHM~mnmyOwtn7_j3{`^-O4qJZ=5I)>FqZyhk z!sEH^Lp*IIiO^0nH+|L2+AyKiTYV#Tk!WoN7Zy(xdEcqv0vX6N;y zHMf0nCr2RM?iH><;~IS6vrmiV7$#WSjy{jmAB?HttbI@(Erhk8RxW}GbfMqx-F-!m z#Wmzec0qOX^Tc(|*5TU;-LrqXlEjlpE|qnE86_;L2dUF7n930j|Fb?#s{)qnB@nS5 zB^DYz|NP||geZXI9otThZ;MaGRSRC=>f&ldKcfKbKZrjILXI($xdDowrMTe19*7Mp z3Ai`5k4CYWP`1n&ySve#dC>*Nib300n3_U#v@D3Bq@HiLzN}pPV}47cl{(sO-x|+4 zf@|=r+(B|{X0^z97X^Po%W9G55hwZgLMrXGx8hEW#P^g%+UN(Nd;ENalEE#o%VN=u zR_*dk6+9sI`0Q&X%s%HOR7Nf6%~SLj!>F$)yDT=BXRl#;ewOQ!9>`$25~K}C!?IHg(1&CGFSJ+V z%evIv-~sH>zdjhzsV;akFIeo&#l6&_9MDGz4O|fIk}-du(v}cjV;)tmchujk5LQN9 zaW|#s@#Xf$l{Qv0tDqy^b+OH%-QLU~=8TlzdpS8>dx~^r3fLxN%B$R_Ci;Q2Xx$46 z>dA1-QW@E@1|CQdJc@xg?Gz; z=z_@JuZsJJ?)A+|sKzpZHgbbSw#5AT)=x&(_^Aa^^Zy~;ZjCFMTl*q^L(z9k_HSh+ z1+jmXkA<{}(0*v zH`s72Fg|wnr2MYAU;((~<={>Kg|#0`|3)@mzeo-u?9lJR3fE;AhbT8A7KPCjWjL*Ca-Hl?I_{fioUpZ|clTYIw zdB-e-gx+~X;m_dG>(yF&>l2=*+ig1aHSX(WLH8^2f@(@tnwKz4n`>V#SI#q)RObVv zS1^liJzV$a$dx~X(+$6e;yo&&i+dQ%PW}eh?3IYbLrMYg%|%f04t- zJDT47cUN!!)lbC=7wdyjijL<-eF^{cnBe?5a|ovxkU7U6+}tlI`u4v;C)2-Ghss}q z%c0p>vDO10{m4N!5zT>HIu??R5}!57%*_tTx9tBQ$)tg-Vy|gZcRI{%D4|gQl62|= zm${Vckf3rWcdnHBUZG!~_g!>ZK-sCA$n7O}yawbHba=5y)xo?GXx|;p>3$%J?pR>z zEBJYz1wKIA*M#5!BUJ;YII?`9+7A)=yolv@Ip0)>jsi0-`xCPv^AgUDF@D&Y>Ise3 za3@8nF^5N8m?_hFEA;_{=yOePf7R!5npa4N3jM~a>7wY800(b{C?VS7i$2$n9~YuY zT=Z#Yo;ix5|1PFzcH2`hnFkK+XiI8gM8XAN9%X?FxtSsgEkW%;a$)|A>wV}Opco`O z2!9D0y!iw$Aw>Pc^#-q+f2D{PzP&Si zN&}vZLPXeb9ml#kb650p33fMo;bv(HPI&rYc8Xg1)RwUllJD4VS_`9;bQD9JyqQMn zPy4SUXWRKL_kE_jVm=JrxB8Ywd2wc+?5#SZ%2oZua0t3(lb?nM14-tP-vR$6ir5Zd zv!a-;FerefHUYF4x{Ip!bvMm(D?YFH?W1G*L$~1KQJ06sLC7G1_fMMf8vHnWTw}>Q z@fNnG=&iRkP(S3go3)3Z#-His&gvkD2rOqdL3m%no$$L?M$8ZRuGmpJ9g0<&UD0`n z{y`%2l(`Wr`R79G4ahvg!LgFf1KreRo+4*1{j7XQc^NiU>c*^Dmi^O;gtQH?Fz!@D zfjh*Y>N;V+bn5EBd{xkHx91P#(!VLyw5VMU zMG>nvlJVd1a9;dowxT?eyuSN6tI4^Qc~!Knk#;spH@?$Ou>2L>Y+|1EE-riF(f37g zJduvSl}dK`@H{l&P6S=}*TwfQIPUQJ*yb4uT{(K;=gYVXVE+|gB2*zxlCQI)D=Hy^ zY@=&KAb9*7>(PRHPk(Rn=qHVpy8k&(ii7w32N2cAqqj^&2SSXg=xVM8QzGPMFhyYb zr_1wV0@jH9T9-wp)b_sahGIS0j)41-8`|}bO~GOO%RQ19f~E=Kh#AHB_eCh9tAAX! z7&0%OcnWSoWd-Xs1Fodow~88%=XO&uwPv(v!KzR-!hFDGX^j%_-t9E60&si|kgu*4 zt@~Keg9-p1rO#hB#W^!~lJJAA5am~6Ci=|Bp(Oj#M7eWzK*Q5zt~re}51rL9i{!l~ z!qPU7?z57SgU2D^SSS@gTeDy9Jtj1)F!#lkRnMI4r^c*LyN*hTnE9Jl$?>RKr) zp-U?JE3fk{TFFa!hUroA)F@UzR`TyC6*`N$?oMunc@jW7otM?K=YFfJ`6THu=S0<< zzyPCU`-FOo1c3^FkXr()kbJ>F5+&NcL)6Yxkixz*MXb{lPM7`#T?Nc#Lm{mS=R8&~ zuOlVpfO`hYwuWW!o-xukg<(=yhA@m|1w&A(ryVI8_jmFzX?7`{M3}dvI=0*QGcEX+ zrMBp2D}N#@DRb|7nhNSRf$HL0^r6uxKEK5Tu`y>!dWCSj*qDnh znr8eG#%h|5_jhk4kmjXU4+c+ueB9Ig{P2j`u;%qQCbDlYyyDIS`ju^;&lLjPNhXNlBZ}d1i z*9_6@f3f`~HpZZWhg*L>Q<)(KOf(Csz!lg*bABKyq6#=pUl$*M)Om9Kih(Is2tWo0 zCG#gh<}%+D;Tf7$F$4)LRkSoU`wHq@d7cO>G}lz3GYvzP=e57j9Lc)GVF1cX8dD|4 z=p+-v$?#tJI`rdy*~8*HdVVt=kFHuR^>S!EHSEW9!b&*a+N@u!?RU?w^T1Yiwd?5+ z$L7HRRUgX*WE|}DGI2RTYAkNa@+*;5$J3SSsJEJ7F!nLqQ21kd^3Q;LWEi`1zDTIU ziumwlGN@+yd%)mD|M=&RBQJ)m@CG@9sV%c9AiW2r|>@>G|dYnlEKmN<+_7h^5x5(V880!=;9ZKk>lh2rh#!BE)#v_2;Dx5r7^f}bB z&~)_iv1gSJPktV5IVedW{hyQ?tUU9e-b((Bvqp}7<>6PLX};*YNdydtc0OGr;7rMEl&-!ZMDs*4yBKg>5Ws zVX_1u4n|NI)`78#|BddL4zL@8EI;&Td$G#j9(NAMMLKThIKVdO16E?e>%qjJ$Y*x+ zzt_4By}Rc=wVqj6SA~%wC;ujWwTu%ab5wEPvRxoV=FsAE8(*Jw0iNH%l z$3ZqS?1UemhZd0Ul^lM;N8rIJV0YSCvVW16t&v%VZ%g|;B$8hA2c9L-euy4>!NMi% z2ce2GjoKp)4kC7OSwsZjph?^Ew`~&3iUzzDA1R zDt6Tm`Btz)I{hU<8B1lH4d?Ro#cLcR3H9SoUadcVu;$@h^}M^0Yg<{ggsr!| zW#5oB2@7rlM^!2ohiiHbZyxO_PrNQ4{!*a-qPTQ+j;W^ zaG2icGyX8|lbi0VEpy+ADJSLT6>I|3G~Q`T)UU(u;yeBm3$vU08H4_xU6;8xbuDos z2$h=4F?1w&g|KJM45I+a!f0X*@yjCrbuj;-#Vi{wla%w9SnzMMJi9G_iv|DmxmfXJ zP|>*RgQs6y*auq}fZwtNasEfIA^+(I49hMZtiU*~d}>|D({5}wURBRvYVShiAEw3f*Y|hWBtL!&k}uQSK`V) z1LvpBMmkckqf06xqPnE-lInRxEOiO=+|&s4d+#;-@>LI&7(ICScJ*^#R+%8silGVf zTkum2EwZeM4?F>qDox)1RLI}Q_>LvZ4R^=W(f5ApyzqE@Eg0gMN^LSW8_fbUVlZiEHeqbi`PVSMl6$&x*ZUZn2sEZ1S9@6{CwAtM zW31v|0+Se+{7qQi5kTd)CvG7TUT`|moR%vD=ROGRX^@+`?|PfRX>&Jc3g*dyz&+4E%0>;>rpb^}m4 zXqDA3_{o?-A&UZ`li0i?4LOS2brO0B-WltD2AYGD^ z$O0B>%_CwqC{`(-D6&0!zkv(HO&QRE zLf%H;%sq2BmeCLai&C%}iM$PIg#&n3FS{KZLcxBtG&M1DSK(U*voPSEFNJ-|fmesTaP5!{7}HR*47UN4#7t=aF|>!hTYJ6?k(I*6vAwToq&&d2O3#4`c&Aa za*Ul6Yh&keX^~yhp7lgNU(SO`jTYc*KyP7Y<|P(EN+k3bDEi9u)3HVCyhlX(qN>~O zH{Opu&bGa$+WL?9Ix-FIJs0KBu#NT`hvLqJ&o+Ty(m%Z8E>m7J`X%RwoopP)`mpP% zA-^FzSRDcc9SDIdbplxBj($%mU<5c`e_ZZG^*iLN_X)Zf z$L3Ytd8hY*|IW%ASeCzQP^9!dJatLa9r$5{Mu0umwgNA?MiR?Ti3%<5eq3-UGC{n@ zF&*O!dBmP|CPM1&y^pf)3vjlr6()?SoZo_l{X)se!fYiejteDz|7Dv?Wu-%{L8mD? z)LB?!iO0u1!TvWZF}l4|j*|3;EhS##ifD>tbi%fYy-YQ?*RpH>2lxlaXoDZ{Lgf>CQ*@*b_1AR7e zK|f=IZTdXcf^xs7dTJbF3Il9XaXfY%o(71VOUMK z${*7ToQw6;69 z%+9g!a+i2>_XiIlcsOePmd$ei$&h(+Yb*Ev&nZZ~Z&Tfd)U-2&O)H%?7SH1SSk zkf#q9HE%v}bYi2cGFwsX+Srm8M6aamt;Wr^k?v_%wkDf6vv|wLzS!$fPAJ65VHkbB z!SU=7ErHcgC^d}fI`S@qzyLb;7M)K*L!V7If6GaeBlxU_R%2qN)%~a9vW8x17U-l$ z-M4+eQGq3hp#nX6e4{_B5X`~kXZ&C%uYgx|MES=pKtX%ht@8WltOB_)LSDr!Im06Xn1%u>8JLlROO~V=ER^L9S#6vkx?R zenAwL6m=F8n_)KO>3ar6C2L=_IYH+8#s__sSOB8jM;{8hV^_Lo9cE?on$O)*7P01c zy51X54%jqTNZgG~)q~&lklt~%m%=iiuTWe2iQw#lfZavndh%Sr}0$IIh0jjuWj z`S38U^#wOHf(lIJuaIi)xmyY|UmhQKO*F1d;RDqu(ZxhhqTJjMo-bQiJ>>BU7O<8% zU|ax=ML1@E75C!2qyJRm0jjo7zR7HlxAH)FRYS*dAPg9UJ{F(*28 zP9c<&#%07TqzJP`OhE)8Tb%73YQ3w3?Tcx`Ckg$b=Hc@uXlyg|PITx9dAWJjce}?> zxJaL%Nnk+wOV-=vm~vmdzP^J{BG`QZTH-E^YN;U^{BG&3jR_rnnxd&q|XJw-=sqH zq3)Ub{^3It`Y`q36_WFjke?ZtZBw>;8;aDh<~A%D8$TZbvXRpc_i=?DKP6@B99$4|nQ`OwFaOQ3#B3!9$#p|s0gw=f(}DoT#9?4sQ(RMcg5 zk9;598fhQbcYC1p2yM8aW}YF9va|2`PRVz3c(qt#S+MWwn$M~(U{VM|A~;S=!86l( zQ}61!es(qX;r@C}aWJ)6Hm+7)+n!GrhrRhc&xPm!bRuY}UNLtRrML9$(?wpFs zdkdYrvJkrgK4f(XZGol?{$Fl~$Bb^oro4`s#AgUu?-ELgzbFzNO zF|le&b9?@)_BN$Wn6DrgXUmoo4e<2sKAttOJ4bFW!dx~j&cs%j-Rw)evXgRv4u&#r zg;_nUA@u@e7F#8r#8R6?Sd;;ny|Y#p#hV)Mlnze??(FxTC&A@+aBGC-!0T%e^KsGv$SiEC`{hE?r82E5j`ZZ-ans7K)(l5aX?#c4OKM|?P z?DPUsy9c&i+Mw5MfYHS+nu1%SRxT0af{-6|BCaNaH0|`Lk?mmc90ra4f%2cCDQ(_- zN73hTKm{HgQk0CI!z7)Or5^{A$?8*BNvm6cZbF`Jq=@?grdChbfgqK&uyPmfu}rLj zApw~0M5h>_LZPs4in$R$h15CW23Ey&1E#4Gd};Zvv4Pn~&#)ScrOt8%n_e(KAc7S! zFSH12f=hG7l+6W%7}7N6sUcuAq#9i`ntn61!myuz*`A|ovNK>f;h(@-LIwVN5=P9| zLh&=mq!oU&L_Ge2oRN|>(0I`oQj)rsihm>WV^J=elBQO(jYpjO9k27sfUpQ;(tqVW zesCnqjsg$vEy}6#RiEOTl){Gha(SS3OqS;3&Trt){{iw72QC#2tBAEojB~?_nYIx5Oy)1auBmjAvY`ryUKFr5uaj{8ywY zTW1D>AJ}3VcRRFWkH_CY(tlc?OBqLpGO%b&G|W2U;NBA8wVY{fL@}g@=j`d~ywZPf zqiy8UO60TaqOazrm8fJ#VYOG`@4se&`sX5cU;o$vT#5S52ZE#D_h@E?KKWymGj32~ z();+shA`K>4IIHK5h2W0$lCyg7k}}LA4rP;m0hxzoj{%C^?aB`NT-R#;T+jW+zH8y z;rlPFa&$9Dl~yd8Ll2Q*K39j8H*k-?b71-SmHpyjJ2vu{@9Z7HK5Ue`$8=u&T9IOW zjSZOkfz!}_lYhd1O%sd2&qKoDV9%j4)_Ml}cU_Cd7Y9fo;a`ZcF|?FHHZjHvsC%63{b zk7)$axs+DEgOwLBNwS+U%nMG4*!N@tTa9jfa-I2HPM1U{yVo*OCImXpw$w_XlR9WEo$l zhp`q3KeC||np>o7#>l2`JRlYnmmHYu^ zR=(j+Ast;_&U~ZkC1^@CJ%RqSngK-_9&R|(r@vlq*Lju+LJ?^1!vJrX`owb8ey*tl zTROYn@N8Io`{bo3OSn$T)U14Z5G*yT!I7Y4)T8}NJL}>8&5;4D<&~DWH%{H~kF01s zvs-h-V?t>DH-LcF&9K@8!dWn>KIA!WOttPE`Js>)Pt7e!SaIDFVcESrVp80Hb4?h# zS?6hwPM0bN7Nhe&1~(}xEwbCbzev7k-|k^7kxo8~Ffozu<7MnW?p9eVWboTwC@aUD z6T-P3e8<>XTBc7(!5g-9!V(9B&iMAe5_S&}I`=hAtTBOZF_5qIsL@(#VB@Fuw7nU} zU?B*=^?VbozZZ}&Su=YejbOS+W1O(ykHhgAdosY8IX) z`l4k?FHnPhNVwju{^Kl^gVBqmUIv)yK4o7mH5y=+h<}xVK!$f~U^v}Mgjo3ns&#Er zgOJ?T`xuJ+8iM_hrLW$h=<<21Kv-~caBptKQTj&0(=k*s2$NgsFG>)H7ALQ_S^8Z| ztE2}ATsx|c({eX50DB3y6Z7NwyqwTo5 z%>m7YDqSO;(-8Uq*z1j%6p;2rd1QzH2|AB)1Sa@leq#7yTWGg^fs2Zt~D10;s<^Yf#g_#<5k6*|lPnbC*o16MqFsldOBol=f)=Yep+H-eo(}`xA zif@-o$AgQW28zFZqKqzDJ+A@BhrPWof513TR@ItEdHyZZjUMFemdGBtaC+1l*|6cs zs!iP$wb&DO(lHa}wd=*Xq+`bIj%~K)`ucb80R|z-@-iKa&f6Z)%dt?^Iilr!36Oaw z=Klx@{hk+~dGn@|?pv$%{C`g8Yep$O?0s1GERhU`Ammji-)=xSBKomO)p2>{ojdi>nCl|- zRsZe&A3OPnlv_&ci#$q6T~cqw^0t-*tFg++oAYq|ei|w^Lq6}j?b4ch6Bp39g^!k) zGi^J+v_w*?FU~FBYVArVhfAK!hDeVasGiD8z$!WQ9-6%0iPazrX_9=ezj($}T%2Jg zytZH7lVRPJ5UT!jRcvk(eG&Eo*PCEd*pLxGy_G5A6d-bIUDKM;0&I1NEF1xGjBOwJ z^9+y%Y?ntXi%R?)y8YpCR5izLDN~Gnel9=Ec0QTTz^NSzIeMj8nZ z!&1@t4_}m{4@D3_e>F*Z&!IxnpU9>@U7$iT{Gjhj#}x?)-Yxz(*sgKAZbsmTEeR=6 zZnaWr^O_PQu$1vf_;2n!@BB|Cn)wtnQ_dxEh*?hK5yvReY>$xC8lRowrYdRkuD{E= z^6!OQ`6Ajvj2VDCV!GmbxyF4bo!_vYBtUP$DPJX&BL)%_?^06#Z}xQ*6E&88d}Z_f zswWeX((IcsY@#j;wi9Gz`CD|za_)n!Y!$dH^!YU`IiNzAA{HTmcWK`rbyA@G3RmRMm*&b&15o{3>azaHH=L=VF<(m6{xt z>s}H5oVFAmK0r?Qp1c5_iV0tHX!Lj+mVvMOwH66Bto02pILpAFS8B1XFu!%*CC1gb zQcH98N3xL^+TsHu#e8yy^(TC0>5ypg7Iqyb1X)*-JirB@U)e(nZh*jU7IH7Vj5*LbLik<%Vtj3ycfT?G*PUKX7Af+7UH;2WbEU~k zY+n68lzn+TR^RjZwXX>w3fal}WXW1RmSio2gz_YWY)K_amt-efLaK*0N}^Co^(drL zmNrRI2&JM_TIe_TUKhRJ@6Y%3`pqBr+;i@nIdkUB%$alU=I&6z@uceA^!ER&4@3z1 z7ZuT6gv6OG$2xBql*)d)?pVV<(*eL`{0f4By${C9a7;*!H(mCAdtEq1{BnnR+n8;{ z#>H=URnx~nGH-2AHrHMIi(c*HV|jK&5m$WLvV%Y&;>ITPP9UU?Jx$3{2OaBDI>)89 zzOkgCP8u{#XaVxpjzuVaMP?%5zwx-fox^JsXXvxW?RJeS)$K+F33 zgm~V;wRbMbO;w^QO&Y;gVJBxXr_rTM<4#Trm+$3FV`xMPTNf5>7#N#`%$;O22C<*ijzP60?w zvZ6T!W?G_EMYS(f!1K);W{oM7a6E+r>O}-blwUZHRG}|8E+d_FmQuxsochI4(X}n7 zFEHe!>-kUAN%?p8Q!|&bvp)jxD}g84z5#hoW0fZAYiR^7!%v&c0t$~+v~fw*K!=j{lc|bv>|-bDMV7dsMZ{3>x@$Bsn$Ff>xjsBJWy$J`uTpwGr|kI z@ZnmINaoz_8NaXlFQae7UISS(C;LbNG$MQzmksqLw&}^d@iTJku<^RtH;p4x_8PrV~Xio?Y!ZdF~w3_ zGKE{$f|gwx{;PYmsFzcDR9GKccKM?XjcosWq?=VS>nfig%;p!&ZL@Fb5L=-lxs-zWc+Y zVJ{D&y4A#*XGer9Ktf2FW^r*;)uP%mPo)w_A44~b(~F<|$O(h1li1@9s5vM)wM_|^ z1&ulK-HpmJ><%^wmAm0=H4`9787z`_Am(m6Y;Epr1#%p}pa!Qa#wJ(avRjqZc2{NJ zV}ln^;${XWr-VH*FyhQ?yB#FLPha=YtKZ-LX5VYO6{mMT{Hr%q0S)mNUX=w@_QxWV zir!zC_{Hfzlie%jrETc`$BX;r_1!?Hao|7NedomTsSG4Wfv3fm`bjXkFdy1GHp_N! zj0OI{c>3r&=;DG&XtCO4o{0)I-jglz{`QSelWT%*-qWtNzI@9!A4k_SR>9Mej>jH$ z5AP5D9=pu8n*Wu^_hhJRrV6fR+96?Lx1rcmA?6McQ$4)RCdT$mV<| zyvcnk)EIYQexJMeF`ri!(PgCLz($pflj%$y2l5Rw(5XXKcd(}L!oObwH_cS&_S92^cP2~!`lfLytDyTEBN4~wn zvnUxHquBe=9PGpCQqrqHZ+64zQIBFj&G~){X5-=Z9e>`FgnB}a$y30z3pqWHoSJD` z0h?90y8g3D%hVr*Cape8^gG#;q{Qacj{;( zm1QY?WmeIfIz^r*Pbv@nNcgp8vG|wJ0NZc(>chXT-I@D!ZOhA{lDtH?+xAZ=*3_LT zlKOq|f~NFLe%9I6rI_c#z%2AsyXb6}pf@yPw&XS%BN_#7Acg}^vn4nn3tgE-n)_qt#9_Z3%nPZ3V#E*IP_g2%oQHY- zIs_jGQnBErucA|Fpj6%Z7|8&?IPrHbb>+)eKI!Jm_bBEogs!q^O;S>UaKsw73Ldf= zk=h)0L@nC+j-@;gL+#r6jPO!biC*(mTq@KX&Z5nSQkN}4WhHJ5emHT|C&uh_!g$j4 zwfF1!x0pR}^0>a-=!yUFCv`GqFaM@ImFtK;NS1%;i}{IpQ|_<0=F*d3(->f+b_66o z7HrH9F|O3;@*T|IkTE=BAS)urL#5eYvpd#=v6wSJ3DDnmOhwXmS0pG^3rnQ!Mw}>> zi%dI~yH|*Ab=#_q&1J7(>a+OOU_1NWC#Ch^IQ{}kO#s6Uw>>zy4M ze@tg=9;k}fq4CYnN7RFq%G{f_lA4Pnw^l#De3|h08PqqlFVaEoZ}%`QTXF52);^u4 zv-eLNiu1`Y@jP1E&wAz_-c_txb`Afr=<|V-xsWpLh=Ro;fKke&^RLbv+kj`nm#@jV zgL@NHLWW89UfFLLi*f!I{33oBSwhT*cm{oZdv{>^pR+5kL0AV6(t1;;<)2+0F*&g8 z*jJsVI=aVa8`+{Q=K>9Tc2u$(YBZ&0IjAV`3YFz|PFmHxck>};0E+dwUH4+gPKxOB zpICaW@=){%=6fQy4+fUAQd^YwUkGHdQK`zyV%$Xd%N=RBYeCo=GpE6DL*sNsNx+Ld|=1=YgsT!7FRjp6#vHoEzrVRm8I_}5{}c`W{P~5 zF^bcxICAjYj@4w9$?WCw*hdxrf?AdX9g;%b-Peu8TdwjrEBwAN{(jxE)EE18TCmsW zX*JVNly+Q%ih5&VOTRr_ws!gCR;uR7%KP-`AM#3F46VwTrC$8O24q8SqttB&9;r*< z`948%!070?2e;P?23uAHB4L&m3cQJhvSIcX=1Pf$5GnzTDQm!N-}l3tgSlG(7+-z| zrak~0+2(+|*rr3)jL1;seYoB+cE7^A*b}!ZBgz*?XD*g`<40ollo!Rv#D~hpegxwI z=-rKbD)>YSIY~CXJw0dL%n%|$&VWJxOc(z?zsKp%wY_*Xn|yFz7*Ja|L&M)8bSjO@ zbEuDrju*}x81duN=75!s_zR8mi~r_s|0Md=ANS5kxGbTEQWJrWm9q|a z**H9G2)S<08R7pXbir^kP47_+Nan=j`qGs#m3P+JEc+Io|J2c`>NO`Q*Z&D|>@e}t z`$rK6K$>GAKi67Kv z%71%Q_zL0I=!z>o7MG+iXZ~QMA}D&VHEeXa;kTFVom~p{7xlOVRx8Ft_ij@>o>4n} zyT|VS^~z(Q_|PhUAxko#8L!nb7;liu6R*qp0MTKxS9e#}z@-OVolIF>m7Le%8 zGU3TTwtiM9p(hRD-H5bKi#?%jpurj&NrCw3iNCk#=Q6L6zHyFtT^_C!y9x&Pl3jES zj#$z7uw!pufq_UpE_8}DK06?KU(Xx1pI8!4Du#=kV6;0Z)w+ENiCB2@GnS-5bRYo> zGkK4j6%m0hrHdb_T#_B=7P)7%a#MC-WC!*l4+i#tFw!!VZm0;nuB&r;LIg-KA3n5n zTzKM!JlWz%2Q)+LBc_Y;6@ACp!5UQAWgcdLCVTeW%-aF%1T2el+wqCEGu_W7=4F4` z%`4OILC1;;?iG7CuWzzv?vB<5lX9`n0>h;~eo|LO;#RnOx?ijk;aI2@IB4wna8oC; zdCQ14`?DsmiXZ)PUMPM5MWTb2W{TV)@YZ7Low@@Z)A3Ghn)9}q|f0a?F=VGWdC zIbQKG9^^_l2vSVn&3$Io+5E%lgrI=p?!wKrEeEjo(+@UY7*cbadWaQN<$BKf+WUB$ z3CduUlpqT{!|$keYQen(OHxv~J?ry}Fr}KO@eU&$`^TzYV7u?{r=Mk4pDto4Mywt z>9JXADSw%FsFmA7hwBgf>#nlZG$rdjDPM=Y%fBDr^~qr4e%K#JWveZ@wpoTOogzfl z{Lai)a9=Iu?bpX|Usve9Ip5k`7}+C#$ZLVf*@j~R$xSCCF4&YWx^X*Ka}+|Ot6dUR z*!#dZaAkw9=;~MnS_1zqz`(aF3Qqwu&@je-(VVD{PDgHPQQwM;yjlI7$$MCjoQNnVwGMO(p|Ivz+tI4hFS!E{Vi9Tx~r5&h6J$9Ms>H2)f&hXX@H zmpsMOFKMinRk%JfR6meB#ylWsRPT_vnwgHlJ zNggdqqG4>ViuJ1RM`lSJ=3k6p z(S||rB&^cRyv2wdd_>`})*XJ`c#&_(_k<&}Cy1#Kzu1xk9V4Li6Y$uKtye3BlbEKL z;j9ec5nzu=+e^zAQ#!^epZiOh>?cTE>dWT5U;C9%mc!z8CxPMThY=$PVCWoXe7);s z9&FMHMhvoZ^o?CkavNlyb~W*h1$(Amho2Ioz5C+6a|ij!;f>&jVg+!f)SY{A`b~Vm zU=MFb>O)Z5o_A~pjUcM>&Qr{{Eh`&9;-L;Dp83NUbNaEce*9FNhU*Ml)Mo5P{}iBN zf(kJ~%fy%H>5&-YMceR;4kM0+PXi+A1(IL;+E~or8B#cVEpcMBY3(zAqY>HPO`D&U zN7&6!18sq(7kT(C{y8{J`(y3AZuok6-myfxI#MsHR8rKJxRJF`g1{JLeI(eJ)e=0&4@ z^nkcP$E6J0)?5c1aXd_nCl=Swo+?)Gy*RM1AYzn?x#|(vzTEGpx+MZi^6nmOwf7em zniN$Ix#W^o#`rj|?B3^7t)x!deOv;gwg>ywov{>IID)Kt_{nCW{#WDsuHQ_ZenrtJ zcth$kD&2==!svKrMDTcqx($65_(;1&;Rf7HDU5YTITK9LRK^BK_oG^8!BuyaT~D{d zLkOtEsG43R;v}GDYQUlzD`{u$rybH(e%2~KQ!$UvgMHvCM-O7X=64IibHC*fuN;n7 z1Wi?guT+zrpDMwUi~GpJa6u1&egh`&Sr=h08;-fvfSQ}@FdTICW!U;rJFIUT=Ck>Sk z$#t^m$w9Y8LSRYL0s3~usQQIPv#FxVBik;7?tE?)e}wE6F#OtKjYbu&)>2$cc{zDD zMpQ)yL-mQiWBG4e;|NcPkwM?bQVQMKh7^qgZWD8U1&fak7|9zICv3& zroG@*{t2xb?-AzM{9j5+;)Tqy%IB7+XUl+;ERK-I`B#P7R8S%kXjXTi5e^wxo>x(N zA3n3=1icJj!E@nf)}{9!JKVB|Z65Q$PaX#DgHgNyx5fdA zj~qreofXL9l-q3*eNG^Y-z*|#SqV930fN=0`>V`9^^Mi}1>c005S1Y4c-do+e9t~3 zNg~9C;&j9wjyODcet&5#ekS#~3vYx$1BE9*XsypWKC&-((J;$KJheKye$xaf)z6xm z)XiXFlcmCnrS)pbo>jP}QSCoYk${Au6l)yck8pBGiaus~c!*OwTdad=wLXc!`v%+t z3GW@=i1I%h@#fK0dtd+l<#dpWyf;^gU}K6~)#1*}ByPSq>^`LnpCr%#dAX&Ozj#!d zkg2aIKK>K@xK{;D`{9|s+0zNx64Dd!!s*!cD=HvyunNg@{zA6%TrS-VsP(tv`?Til zBE2&3c2~AWZZHf*;O?LQ_qk z($2Hgx#*OdzZzVjdw`O4KD*xjy+>sUjyaZ#hd8K6>YDoA=p@o&X}^O>JH*jkB)XMK zxK9a9Zq(}DN7M9zR{uC+VvAAa4n5hd!x5v#9V&Mc|C^5o+f2)VQ9g2Yy=(vAp^)}B zMq8jUW)5Ebw~kgD9_b61!>zCWxj0AW*aMu z?Xe*$Yg5cYj^U+*-ZcFE_fe=*w$rDAdE>!7DR=Qj4?6tyu9``89uebaH5mVC$Sc~l zW&m3DVj@&!iXBMoiZ7Fy+*Q+2F+Ana-p<+!UM2e{rFA>BUVaXyJ-hGU<}%eCl0}x; z@a+0wC1u@g%eQy^8Y%0Io8Q}+9kcWPC6_}#I+iTHLH|#W_U%xRbWfkrqXJy=h{!Q$ z>0qBRC41tRqr~hzoO~ad`KLH9QJe$zdc4U7usBvxj0umCTTiXC zJDp+`yo8Y@4NJ>LD$*wt6UxqNx2jJjww;|7ZO7@ARuYoe9Nz6cP`BY36t0Dr90*`1 z7jy_UU0{{0|5Py~RDKl@|1;uI3jVSR&-V%Zd=GUxp`RxWg~#FIKfyQYKfE=LWGB_i z{~=#Ko$vOeO+q*7->llqEpXzI(l-5lU!PpM+11LFmnLH%Dzsqjku(!mQ6c5ETs2p( zVo%ju{VYR>J&-HBR?zY?u}eRAj+&9fn(kBqYG!LeauVAyXBqbD6fk~N7p4`9SA5An z4!Mn|LI`EUuAr6Qht;@d@=&BV0#kL*st4F*0?6ZnA9 zT7zL|bEudMSI%W}3bfhw#eN}kLy*n3wRg^fu!U4k^rke$!2*}MKN3tiv?DNMTmSl~ z9ba2E!%N2mT(3Rni-q8S!2%Isn7=YNgZVde`1ifjp1+Pmov&=jsA+{udvi5-zL*6y z8sq{Ad8~ahyc8#Zepd&=&FTVEJH(GZMxJ-t8kuWI->ezlVxgpZ>g7Oe;xQZ9$@Qe9LquMI=!{>rjV)W> zu3a0X?lC6QZm}`QZ>hKhAM`?cwOQb;ajvEznfbX_$Sys{w!#@se(G*1U%ugm;W1C~ zwEh>^Hj*FJ(Sb&w-ghu({BQ7k4t|HVu!)wpXp3RdVXy4>LisNc#K$q&NzzLe@yW(g z^MBVPN@Tx(ZKmRK51<7zlb@iaJ`-u-f#MC~Qe;rJVcIdzF@T7YS9 zp!o>rsyDQ{)jM*^NufYZArHcC8XPDpa+I_(x9#p4cPOG|Cz6=w-Q!L06yf~3C1p-} zkqD>rK>ydmG9j~Vh8#3RV_f7)IQA=3fXCK~qI*@Z;&c!@j?d}g@xidVpgig-_X0)u zCp72RI||}*RN8(5B<&?e3f0*~ce*z z7}Zm|f3;W)Xge*jWzlh|Pej~XC{eyxDxtMh`~I23A8sC!fBPdmQff{?&Iy87oX~k? z#jR2SS~s<24rB6^aIv#FnLZaBv^!($Sl#FFKQjG(lA`x53xf)$=Ty6ZQANvaL z(B1=X+`!2;EcVFfUgm^(yo4QiuMjR|&~Fz`qgV>&7;1T3*&Z#o;l+0?=M{ic3V%6k z{_1!o)UQ>c(}Sbq2gYxlbrES>#n@AkY730-LRtVp2t(dZGjmzf<)ikEB)0IZ5F6qB zWBkh)B)@iOR=uQYHfgJ%38#|BE~p?S@)c1QRA{Y5FMyTqW| zB?Y7nEhfpp9n4MAGa2m6giX$zb*+$`37e8R=UO>AFeEkzlSx2rv4C2f2}*>{*psC* zsY|w6{{nCi4stNYI8aZ7_-)Z7Gm(*GZ+N5ct!o2~Bcc34#Wvyw2}KdaPJMWTypsM& z16-z1A>||}DhVC)mgA>q_f1Vvt?Uwsw0QBQ=^PT=b!pnpBqDUcj00v)o1|9}f97<- z->CL&hV|SmXfXvHr*7iP*kZLRlPu@x9av)my|l!>{)L0ypl=oSnj{Gu9~5z;Ux(LR zeQ4$)XDY_@u$%0S3(|!nHKoLF^(@LqJ$`FXRie4|ZNQcqw08;O7X(drV}d<(*VArS zB7cN@ucuvAs#FBk8ARljL`(s#%lUz~`<5G*pTCQ}fMU#C)bubdS!<^dO&I(bO@@#H zM*gU4=oi8F<0iJ+5kOJAlkzHl+ujeieEEw7|**)y6qB_*6yy57t@s|}dmYnp3@zP_QH&HZYo%{wB(R=j)|shl zcjpnF(Odh&(#n#yfBo67{z;z8KzQg51FX+~1H;pIcV`}1cIGoL?2Fg0iTt|I>Z;?F zfe@xTp;?xSqoJjmuB?Osc+La688iizk*Zoux>}N!@zZ^`jOWZ%g{1}S(7gLomzme< zp9odx7hqm{R&$5>DNIv4ISH>tR)o|vHocFiAPo0H7>GASUi(c?t;0Ta<J?WR8M{+*95{6dM1+?%SA3vs18>yU61(W zfVZM>KWQ2EB!)bfAb98lgbfQiDVgrzVjk(d6D`Xx_e%j5Z?VKC6IRDT!p^ghlISS9 zh~_D+nUM>`4`)XDc3t*sep?4)ecU6Qt7?~uYmUt5o?wxi-1JtwrN0g#1M5x>c(utB zU3-s^{k9wbo+btCT$`;#x9q^WdUARFXqghc_`orKj_YRY;S|+miOr@qYOg8rY{L~A z;abW+^N9-IXoh?5=ojup)*58_8wI~w>Ni^T8|Aiwn74(X2w6;@-h?^sJGri`Juaau z)r;jSD7w%Zk{$l%jox{W_U3@d9@Bes27|G0Q7+X2od<|pPRyikS5=PXd)rz5u}upk zcqy2$4yx^uPFu*49`OBXb?^80C$4`bX+o=ujifq5fPyab0_?eyo<{%AiV>l@q+Jgrvn*MXWbAq%hi%E{Vq6NS!QxwAbXT&1apFKP&H{PL zq>(K#)CN26!3;t|>`4|8;)nM%_I{9xrOl<(Q=nAomjEO20w_RAtObb-a}6cs8CPhc z|roqN;XEr>P_cy3SgF&bk)_n1rS7_fIoM^Rog%=XR4_)mr;Zxf! zVR^YPG4BHQgVy99p1W3W5y0d*1y?-61Nr|{9fCF_m;U&%6eMozI{JEvpH|G;7?7Oy z4?rxnHp0;#w1c}cTa|LPr9X1d+H>;!{gn#^SUwzx4~5S zqHU$?A&CK?%Ztr+HFxq%hJl{QBh^mvr9v-_GQnpWvOsi*@W|RVfCCX>yLfU`T7J!6Q zv90TqLWFvo&v|mRm-+mxeq$AK^BkCMcZ9wzF3Duw_BQl1RtA8EA2MNEnW+oy^kizy z-G^%~y3(j$nA0l;S_;<(DWLhpYo@J+Z$Ue}*^+;cKvm=LdHUfv5%O@Z3-%)slMBX8 z*4;hPz2VJKkWI{XxPNb7DtHyUuiEz(^*-5QnO6gL^K&7tt38u86_v|%bqN}`KPU%# z;iq1WPEmIl)i;>R53?HAQPRUJ4s#mU(b5Oo=Hspkkf_vdSxL+Tq%yCqKD{_s@H<3w z=x6Mky|5v|PDr-t3*Ui$Tx2*|L?()GWke2P{J`EwF4!wHl_wHgW2YA#GbOb5ymh1c zgFG;bGhpXP!!!H|?o);6f!pd;W{89AwY|R=L?DY9Qd(3CheevGv^UklbJ6@S*F3OS zsl*FH0#3?pkenekbB5;q3YUI%ivcD_!nZJDmV2=M%=SCB&i1gL^Wx|_blq<^DF+f0 z(p0BenSz)2go|#zi(~d2_<~v1btIF&NgLZ^9}zQk~S3XDId?(AK{<1_i#X%6fI z(7>zYp2%Y=UCNmuACr4PO_#Gyp0zIC@d)^ezN_53I$`7-a-bLKtG3QBc*t9Q@BB81 z8s2@jeuru7G@w@DWUv8ckCgeRuF!ZReFdC8G*0x7uf`sHW7UYnujJbjz5=t<&BE+N zA%3Na$*$`MVX@W&Y<$0zLp8_Y&NB)75MNi}=P6yPqPyP((0|Wwv&jR4PYnZ; zk!4)+=G)i+)XR5>z7daWRi{5lM=awkF55q{WU`F2dzJCjl4;*@ajYG5eIhPL##jQ* z7R_7MXA~nj1x;+^bb@Z!8e-%RbB)<3kgjI$3#v5gOj11K;_=L#VH^?T_iUoqoa{86 z*B?gBv&djY#a)MYY=0KJfd8StVuI>l$5Iw2{mI777;VCKl)*nd!QK=~+4diHg79j# z$*i>CQ4-aDgQawwv4YBI(`CQ(it!9_n9s<=LV5mVqr-%*3k4D%$Rdm=u|Od*nz|V2`%4@O9&OizQmlJZF}h%1a1^g81#Pt z`s8e{-RXjlM}4FoOI<@!nPMYuQ5eFIhTevBNgVJ}pm+0a4oN)R%A6h7ynhhFvot!1 z23dL?Fy3>q7T7sX)oi<6%JU-e)abH8t5Xwm3}^6ApYO~eVKdZ56hy|JNX_JDohct% z^|b~qfMH12&;$up8cdTps`@!!L@SzCN6q27xO24VmJTpIqk%(iD_sN)xpDxT|4mV5 z1L%+F=$t%Ph^GwRYfq7;@g2j@C(*5yh{Z8)G!Cvv7mI^XzR>|!fT;!$+VV)x#p8i?=WKp9Cw6?~hpIQg^B@K0 ze>qOHaQSF9t5#)jYh|0|ap|Vmbx$2F?BI^-J*A^q;S!WG$$&8B+YJMXo}4 z?A%5Z_RAGdt66-WSiUh_1ildc5(la$Ws-J?f8J%$qF0h}*o0#D%*m&A&v#bWU94rFZ!1ZzYC6j?p^`E5LD8zELpJH{LLMV}a_xX;_@p*GIc~@T_Oq6|mueoJR`u;_3D#uAgqyWE@YNUUvWMcXpq_ zyoC#}Fe`E2EEDtOmfePZtsCiMVi&Xb)sZ~O#=a!8RYx7K+LOK)Pf<(FffMx61?_`t zS5jj?8{vgBho1NPdpW5yc{pqD07C+Ze%-dZ_U9rZyc?Y#moJ1%oK~_67@r$nSRIU` zfy9$r?+BI}E+2Y+X<*}Vix&{hF~q^iQ?Kr=GE|*HV%UovaMuW!zmt;AoIJ%vlw6E> z-2u*>pA{fY;)lqXR=7^oI+63kS?fRj=bkRB5uG2jFNlNv@o~8Qv!+K;pw+CWlhf$0WBsP|FBi>b%nV>C8)>uMbUCKrN6;4}G#a zn8xFJfz^=^a73B!ex#)`ln`Oh6m*2!xH7eE_|X`TNynkK%2VFSBbO&_m^77zL=l}G zZ4o;of7TR>G!5HNT#BmPkdxB$`F{Hjr=hc#lVuIN76Rjx_hhsU&3fO^aFdwTn@0v@ z#=W3u(Yr;KE^NOz_G{NAtr`3dp-y<_@smSGE)!2EmXE${qLW%-7ybao=JB4JOL3J& zlvEk_Vsswan=<$Mi6qrtU@{wE5u)|c_&06g5Tz@5^NW>%=PQViV4d4A|K@P7Yl}Rv zb7;-=r{_bHcHshfb1Go4na8rmU-!B0OGk~}Q&O6h^tt9d_34e=I;&{r)Rmep*u3!V zHM}IHM7yONTo<~8Y8r;gat6?wlv&aCz&V@pHy>*4@axC6m@wv^GaVZIs_?yJh2a)3 z<5kv;Yb4pu-LcCDKmOZ>_Fq{o6-)90!O8eaSSMf{+p13+;X`?I(*GVQg|K-oV=@7YOUFUlV+n-dfmi)@(PiZ~Hw6N5)_I#8V(eKWwZ_ zs?TN2(10^^Bf(gJ7(~UsxAUY1mR*a;%2ydO`g4_Xq6g8SLIBgo0Hx}?n;H#Y$%B=k zs4|l|bEpujm@<c&<_P1d*NX$!&R~#dG@U|RsrVxXs<}rjgYXPoypk&l zejruZ+~0Gnq;2GRL`R0BTqxee(%XciRWzB|RY-~x)*KJ~0jrC{MJhBJd(R4np=Axp zaC?D?ssV72XQe5yY|$u0Gye%A0^aq}1%=tY_J;U%sz<=Q^L&5UNitQ4-WzTw(Tz40 zAV(aOjy6CS?cYpgvBY??^_GglwhQkPLlp}j8jB=DjzduIFBa&R#7Hz%+h z<^UIkN=$7N&LS{)7rs%YTLE2X#DNY_C45QYPSGM0#xKf(Z)e3~Ni87C*_S8YJNG4= zuxZs+!h7SFhV}6o56ykDEqR&A`Xvx9bu2>`>{+6IX&$;kyfV;vSR6eH*0pBY@H(1Y z^|?P$WXnpuO`MIl>VsSs^$N4@c(mc%{&%+7U;2~0r3aqx)zR-$*v(HD6|}-Rp&I`D zu(wZX(VHjf3Hk93*dtxb1-SAz+#7#llQHz$c7Msup7s7u_CME`JsUsj(5#>KzI>2u zi0s=;%4nJYi$wt9as6k7p$iIbfLNZ&I&SVl6r+oCMfK$puvE!Fj+1_J>u~x4-gDRxYJT#T}{nu-l`T*#;Z^@cb+g9)vef&dMMM3MR zKlZOdWtI`~#=#U75()gzHy1n;dLB2g=?hla4;yem5jQ6Ow|eO#tV|36A3r<|=Yj6w z5K>k`kY2Wm+R+kzx_?^^{%Ju7vgv~)cS8LZ>$svo_;JDFP7?2#Z!x>*LaE_ zLe^`FDZW;6)0qgv^W`gvNs`3qGay?hOa7fw&6uAmEkWqoNK&5X=MRwb9`$_lH=Unl z;J3r8)HT=sH2qJu zvDy6)&weXsoAGIid+5aU9gb~+C(ApX4wjb;; zoDCd;lx(qhy50&U5Bp_jHc$iy7hb&M=4ADK4}l5CM&(s$x9L(6Hd;PujdUqB;B`N( z=X8YFBJk(X;uhtBXCF2EH-=(w?xk4?s|D}=K@8`mivEm`FAwQFn19RYbxw?XT0L|O zZ2Rr@^sybfq`(oE=St^dHt@79iieOH2S&AcrrV#X43Wf!>%nJ4KJ5Rikk6B7=qUC- z;IW~3HRY>ccjk&sZ3oL~x(IwP3gJ|O^jAy;&MoqIq3}HHMNH~??I}{UJ>vKXhOiwP z^97GL3k-)ocoaZh=?bhDbj>DB?sN15YY&@oQIqcVkc;e9V_aF=;S%5U z4b>}4sF}YaSa(jQ5_SVa+a!Il{msE6F8<{t8JC08(tx5!T3xA_(LJR|MqP8Ek*6h& zWpg>~DA!QwAx|bVY87?I_*Z-v;|8txn1AG( zNcVG>u2`ZsyZic#s9!Xd0-{*<*W;La7q7a*NX^Il8SX6)e;WSlW=YHv&tB~{Oo|9GYkFK{S3{0r7S-77_r8nu>t=P0^ zLv6>O;JH2-kJEaQP2B8s$sYQFP29Y6Nvdpo;s=PrNU#g0Z*IbQlG48o94s1~H4}p! zikaAaOE%j{a!pnv)2?<&h(t1vLq5$iZ$Mf@-pBd>_$nQeI_N3)YCJQp zc+Er;hJioB2)3qMPW$yiwbrHMB8P9it-8~1ur?jWTDx{Au0_e)|Ewb(!BVxbm!WnU zGUXH^LKK73L42MmAY;0D$ccrQ?XJ^ed>ewTqOC4nD% zpA4NdBo$=<)hmLZL^_@CT#ASVKj|0jo$l(HBn~I8*Ad-SDZ=ZyqK|j0uiKXsHsLY( zLq#y{MZYjqZHGTG;Qf+ahm(E*y;H2+wDB%B)v9lB^0iBO3#*0 z)fU@dz!c&XagzWknEjt+85W#zg4>u?EjI!))o4g#|9e_#jO6G_yRnu2x6E(r-{VP9 zQ}g8xX%b>uaZ&i3?>64lgk{_^x=e)^ixD$ZW)0a(O$etm^D_2>!g;H4i37=?A+UUU z^zEZPZowEZ!LhV3__5PVSf$q65f|SOFW1i^aCXHa{tG^Do$r^3>M~UOVZboxoA1f}xm&iU zPx2P`*raR^UPyl}h^GM;3j~fHs@+~dsTpSQMD7l|dhARADO^yhOVb}N5FGIPk$+He zEhy1TSdpXt`hTtR>4&*Ll!H;yVm2r-K3K4%i9BFSRa|g5%hObPjo*@M5}!DILnTCp z<6nF_J4%YpU@OD{|Af*Mp61bBTLjTJ_I0rtHsv)iYjR0(StI(3>$af>@Wxsy?;(5v zd(;Ua-d4d+(N(~nq|$nNMkP`D5RCcqgM+)F*M^FIx$<0wIng6K5e1BAgrt| zC&Fm}$Bf)KKZ?F9Y+$hX=wpGUKkXhoezHihhO?3*2%kw&r3sQ?vjESJAIkKgX|dVK z7|l7QYJoz!vZ3ArB@w|q7YO%)UVM5V>?X(m&F?w5vpA3p=g_~Sc zDZ6s3I?5^F2dmdg@!GwcmrpH@aY|BqS4>ev@9*DzdSni4=S)LHPoeJY{VAt4>*hRa zsgeaJ|DD@LmwDt-!gZ&;F1aZQ!Znmagj5m7iZaa+uC!+)&nwpb6+QrYi@CoMPR-R` ztc!bo@q0RZu`2gW4-LI`w?rjb6>w`JaDIAvEvXaDA;ilaXvc@zJ4NPmD(60i{wl$P zk_5=zT!WwFX4V(pCuuYZ225Qh63<0R*NvdYVNDnbKF3+3NTT_k-DVf)9r8Yk$#DHn zuG+?m#r1a&JUgO37+2(W9P#CxuNzBEYbu^f$yy6rz`CTiMz9XQvLyXDs?$X%DUNhE zCzm-Rb*?ffmHX7Y6!7KzHn(TuZ?8dc+9P_<%n$aZNYH+@n>>YH-Xu0qXO(0f!22zw zyym_~;SnCPzaw)GP9dSoZ{R^Usz93`sf_HGqtwKNFF6S5-Tk>*5~*e}E3m!DSs)C& z0jR!_!PCwK0(d%eXyr|L{SuZg09^HicV1EaQ`O@5OAWqF-5aES^$u=)J%bKs9f~lC z;eaZ0Ggnm=aJjep@+p4mZ}@F8fVa*Nr*iJn!ciojo^|Ld4Xru%IOdu)Uc$dbpShrc zKR6i?Nq4=&A3TpqfnK@ulTb$QAr6&;*TjOy)O8LZEB2i@5aINBoZX&=*8+Jh-pH!@ zekucrU3Ls9AmNIKYu$>Ev)#ged!0k4(lCZ^UtMwz6dO?!?dN`qSL=u=h~guN6+z43hihIGa1zOVNXy+t_Sw+c z&%;?YFh(O6INeXUWwr1{h)pvveWFwcPKP0)HWt*WtLqAhrJIHaomOuNB5utJ02b1;roTW3TbmnOfmoy-hcd*@wc$kdM3P@{q!YERRcCKeR_BQ(OJ& z+cRb0rGo4;e+~OTQXqK*)#G~p=Nb46fYN4&3+!&p3YBKirq6t$!`jXrq+SV@Ki+j6B6&nYn$SGe4pFs{beHmIA$-AoExi)+P% zQ6e&!u$noErzE?W%MDVquY|Uko`I3FX;*|17=sOdhO>nD%ai4Tg)y<0-o+^>X{P+1 zY7uSy1}$;EVcG8`G18_>Y@eh3?4$yen@Tm-^TwZa6IC|f>zerxMpJhLABG-_w!gH2 zAlM1tNAAO@w%YgY>Rkp0vz&|930Sf^i@AiJ zXO6WjX0Yg-iHo;vVz7BD#y3($PJXEg@Fg0+jx;x0?j3|jxcmrLd^Fv2<}z}Q0s43K z4zI0~S{SJXRhnfDkYT^U=MshiN^ zj$^#R|J?avCnM=)qhxHT%S?6tlUtFZT<-N(kI*Y_iSqbeeZ;7MA<_6lq6oGq$DO$o zolNGH6de})zy-m$BDyo`+%oK?$9;mc*e&QJ){jA&6^)50C7Bl^33SEFKtt3kDIvXsuOJMb$~SFX58lZrsr8Jznu zxqD-T$T8tMzYKkyV9Q{ab%*!6qnw&PcgO!nky5PspK~`;A^DzXuj-Z3Syc*gPTLyQ*ii5I@YEtxEha)JeZY|3!u+K!~0} z-2I=Tij1kOghB-)yT(-8RX%=`Om?G9?O$9t{@7O}J%aR?1REh-=wl2ffJ3ct5G)~F z4UBO?W0!hIheM&Bulh~a{n!qKH9M@xL1^khRQuJv7v>zt@8%)26ip~p$Xd5!aj%v@ z$TnOU^lyR*sdkSk7ldD7bujQhvX*TWyZ~PS498G(cihC?fa%!@$B;dJ_3{p-lk7(r zmf|`)8VgN50yY&h39HX5aJwGODxofNr8o5~E6d->MXPX!t+O`gF7^!oWM@&Y?8S~ow`g}cC~Pr(K> z#(yjEWPIlxV4p@s54$-OgAxItHXEjj(ID(K(7ulesZ-x%gay>ob^X}>CJrtW?2D$C zf5^K9CX1$=Q-f-DuDl)#a@QuiTmjO)7>J=Q(9w(gaMfeL*ZZ9bo?ZAe{ntVxKXb%F zSa9bKlzWWi;n+s(?6`O~60O0H`X=lmerK-MgQsw{&Rm&`dNN;E-i}{1|Hs{u&|B|W zE_tbH;1@I2-rb)@TFVrz|B<*VpK_8v^-(@l`BdJ@Q^Sfd0Zn2LbGWMx{d%!Z#$drq zyzf2c(Li8PA?V?iDI@q@3W^^|*^3hn&|e1Y)8^G(-=J&m)#xcZg(I-`LJL=a`JAk@ z6@tsLXkzwBF}YSvhkg(Af@$f-Um?aW)MQxjnr=VbmaEk8Y3lc<@HIOlQ4skJN_ zlEj3z7A=G~^P|Z{K+j=2T(?_gj{}bj9drCsq2ITVzZ9P8+N7oPJJY!|jc74|5`7!{ zY8*6lC!g}&3B16^01kT|7xeVyHnPbPZLDCzPzn6_Z2mJ>069UlHVFU9!*I1rM zAhWYb9-a8vaNfG`&C`AT_f2tp6E`(HUHT5#m;tI5 zA7zN}CSBT2*XiH@BBj=^)*cA{GKGYCis2HWy47N=eN&*Bp!`U0wK^>Z(tdE3PHy|1 zpu@B)%>|yh&B4|SP9H4a)G$^#DRTsSC}wa8De;+R5u0}$@glf#(wA>R^V5&giYAmrVX zaNFz-?RA9}^iUL=tHB5*xE)r&h^5C$;rS+~20SGX)%#S*)0y;Vk=-2UB90Frgm=$V zGZ|jH$7BAGdbCRh#t0NZ=~RB*@hky2b!j#?HgY!$iQUi^d&h74wa^vrEtd{#V2l37 zNNvrD4+fMRvy#9!>|f0-y_pm;CtWzR&9v1rCqp=M&9sLDM@v45UQ)Qa16nBJ2DgAi zPc<>E*c~FCqOfE0gz|Y_HHN1H4)!lQ5zl_iYnU zE2_bZ67p6YO8e7Hm`b`mbP*uHYs%xn-+2htg~Gm|0C4J0M&}w%?Ho{$apqXFs(Ca> zS>rB}(#DFEB`8H_9roiY+yIruqn1kM!>;cyWBbsw1PHTtOT*?US-Z5%| z7fg&`#s2|l_754#?cm{?cdYd+iR%Kzx9?4l#a~sr2E#-Rp`Tan51hmHqTBDEezfQF zsn4snhRsbXHa;RtmYtc&!>OkrEr8{O3M(tGbfs1PRm#@MHG&}J4mu{0`8UNH=ZEWN??m2YPIGa z*#M~0GgzV-q~AVon%e3;x{tUS{q@%ieF^SO`CCNDoJI)JMXYH>?yTn!}d`hX6fwU`e) z#+t{oDDcRo;U-5>m5X|2^E%w|?Sj1rf5`UWg$&gcXtO-4tap~Tff55I!}+UWlagPK zX%0N%jQW*BzuOqzcaDQ=&{~s<{IADdsLlFr6D3RnT5Du?79!>IoZQTJ79r(Nr9Olt zgLJ@|b6>y`3_c$8>fZW$ojh#CY%yq-?0p;pGe{TDA^#KC=SQ2@FH?a5K>e}%a}F0h znOd&CCk&|LCR5@?2O4l!g{l!;aJ=pnl9Lr`5KT11A^{Q$D&s5O3{&AJB== zkxdGq&25{J>{vQV6&C0?8OuPJ=1be*wf-MrZyt!n_dNhV_9bLX5_;@=2-$g*vL8(lsWMUU&XOYfn%u^C& z)X>C+KAnW9plA8tL^L3`X$#&0d=XLsZe1~;ay+RRSEpUzN#gSGkUERFk9~(m#n|cF zo^KoU(x~dWkgc<`Xl+&LuCiS`8{X+buu3^94KtXK`cmlC%O6EkEBAcLH+kf=stHO$ zkM_Oyc2o8}H5oW-6qUOzv;9kvPm1)ew3-^XUS`eHiV;{-C84BUw(M7zOQBz>%KOQ7 z6#b1*N}}%VC24!#Oeuve-I$1g%cuNWHP}mZotm}y??%j72E{jt-mg3ymY8{ZyPaQ( zu0HnvqM%$i0g1o>BaJ&ZrT)jo^5Xsk$ukVK7obnr0=S&&t$EBx2+prdLW zkD6$7sT6dB8+SMO-1vw^R@L#2;JAhKk+Jf|#=Az>sQg4X`VMT~ZqKs9|DkZDtsId# zreG{%QGl$m>l@|7|098wP}VY#tv+WZ4-S?{sA+Mm8*@5Imvm6(bg_e}YT7}E)83+H z9tG+UPo-w)DYQJj)sNMhVVCWX!5N~P_xk44h{O-2kH9)ZH&W-6<=Ls6d;T`VTSc8O zpbvxuZH9iMyHQsw6QiO!yuD0?oQhA~K29v#M5r`*Gye`UiJT>z=Q?JFR;-+YJ5eR7v_J7k4au^j)aEs*xg3udkhFnVaguBihpUW^Am z*QbJ&`oAzR@e9eoZ9;Ta+R3Gah)SOU?eZ%2#%fHimB-R!A4P%V<|>INCb-hrYm2t8 zD|Tc1OHr8fE}5OZi7(t>=nc~97jo~I{Um5&nX>FrC(LiXmVmiw>CI_6OXqRd zNj=ps+WPXSvwdkmEcF_9&>Xx1i$jl|=b0xio$vizM@~IRLSJQ4xqaZyw8*Zr_l@q^ zhFKT{1IO8>P1c-*xyf?C0%53mn+F1-8FvxK-N!qh5}Ckp34et9_*+RCV9K$5ppiRvLVgWN}86eJX449bRh6CslYd$Y4Y@r zyaHb;^JM1wTJW4-?(?Y&V|9iuX~P^}{X-C_%>|`Q&+v140L)(iZij*uhQBoYG~4>` zCNWiiK?)YdQv{kUB3Tvz)oR#69G2#Qx2~f3M(RBSu zHu?ck93MHbxhzQIm(W)|LXJ9A3VAi$9=aRbVbhA7CdW1Ls)W5#TjH?y^~xjM2J$m; z0{WNZUV6eTI%^QshJG7W#NVuoqTe`vsh-)!Zu@e~{vcPWd-+kR;5_;{)|u8Sp4@83 zV-~O~or?1%m!pET8V}qE&W{T6Z47^I|5`Gg#m9GBD2CvQZfu*;F2ybo1@e3?cp2#a z!o`3|wYSg_mTg4epv0x;RKD3OrR3<9SSEGC7qlVe5-!G{tbT;R*?{a?S&4=c@%>3h zsyKyWuRyhUw!yc$4TZ@p=pGxN<(5_~qO@=QdF_A)8LdV6b!s;ixjr!6q*L4|o4aCV zJkKV3S#dQ;$;)6}#du158Ud|xOxj*!k4Xg*#^13dUr?^{h9S^DsIZ`NB)%Nzo7P`G zY#`NZ%?maV;%%?C3Njl6z5OrbqMssk?`}7!Un(k%VcJFaHK6;Q5*4Bww=WtpRtV}l z3>%FB3XiRO{be~VGly~E(W}`q=7}^`=Rinrsj;~P7O#hqfd#n!8tWS+)(bS%y%2gJ zcop1ps}y`5fdH~w@seM-#UrwGNHRBfLyctpAj;cqO>C`I%iU0>JZm(_pivG z23QlK#v3ONV38QI^4LFmNG%DvI(pT8hlYL&rtyV4eX)ceKTp z5p7^Ose9Isu|n@hzQNEJ!}<5kGXuOGX=v&$F_Jbt|E^_xMLUMr^U`DgK8!2KlO9l$ zc&%&)m&~6KhXTw=O$>j;Tl#qK%~OcL2*~lEC-02QUsB$JlOgiQo(H~)Bfn74w8-X1 zcQeCiw%8U%;N&o*V7;y2Fcfkl_gr%p%)MqTkqu0N3EO=81wFdB4qXUwAO#r&t@!Dy z%@kxXAT;jhgv|_E2D)Q#L166D8uW`wAS~R+qCT|gB(E&zWy!*AC7O-H%!3URCTk`% z_T2AdYO3&U2R)ox)})|Blmo3Hbg?Cx^X<8g`)*kRn-A7f)wb$sqOU`wNkdo`HY4Y& z4(M#}F*AnLOf4rJ_W?UL%EqOV9 zrle33V+C5S-giwA54K1)`2+YfHfqB7{o^3101Z44vA15_qdA)vDZ1G~?9X3=dmcwu ztz^$%!EzR4+0>U{1qCi~plnc`Cy(Y(c=S`GANYx=X2;Vo5T>cJKd0JP1Xb1Ic8Hcw z`CEJ;_vz_3sJ-yuvGMrHb?#O!Pj2p1W=kt;zrdAfyYLZl_^P(1> ziP{VlkPn(!{XgsEc;xHEIB@HNQFiHW# zar)7i%a%LUTTS+2=x{jcpLkGXmd~iY$2F@No9a~YbiE~qV=Ur>*8!RHm)0wTH_Bmg zB8l+`QO2X$4UKr@DMKgOmGpKl3U|C#KTkxzik)^6UdfA~@0=Kf`krw6XU;Lq9JiVOR3jM(5X@-pVK?cJgm;u4Tmv~; zwTfAN8&*lYg$Szf*1EK?cpNTbyrmo-6=tn0_8OE>dBcA4(rC9I`v%QhUXnHhm0E` z`+F!kWae3>MpptvusDb?<$0!Jr#;>RneycCrN|2O--gjT6wk)XX@X`S9=#`q#vd7J?S& z|6K`awpzWcb$4B{TK4t0M0#$-%1x&(+t#}yW;wwjz+B1vYo#$(BfyAI{hZCD-~EzI z5pbP!qoFtZdCO|jZVuJul9ef8yE$x@n;yH+P~Qn6m^yJRDH`sJV}g(Fj3$WAtVV7+ zLB|Ow;m4;F!sKoJ8tm<#&|N6&KW*r}_S_wA%*dM#5jC+Ii~|{eg+RuInAGCZVKnt^ zSTsjaqndD^I9NV)YO_dOz3&cK2YF0Ww{bHX3c@k7W82EF$|kFto+$FraUffLKU1UB zIU%j6hG8kL>fDCEMnlm3Z<=~K*!Y9}7XQi^Iq;l`Q?Q`;qHqy&C(v(~On>-z z8y@80FcKlacI$8#{lmt;o;EoJ2?ww9{zFtKlffhp%L)IwWTNuguL*42?vzK{&^u|t zD?7nRy`(Sj#r1?_=PP9NF5MxE@2R}eJE5H-8sf`e@4%)gCjj<5Q;xsx8IfFXgNYuZ zQ4cIjsE_K+f2Rx`B5PHH(8yikXBpmCt&KGr>iti55CJ^abFbK}QH%?I^F0^J*lXVa z5JQ<85Y?P1&L+Lb`*=%ZbCH_WBXPE)U~10&xn1&B@EtJY^BBwxnexMeqm1zd)=JD* z8M+kSvKTw}GVKihOKjVyL)wLJ(oUVc-bpUjm;a4B2da=2G~yFHeclh!m#C?s0r@2b z8={I|$Kyv?GomVEmwIJzUvp0#hKK>>tde4c$iZ*o-L`Mzx1;wVl+VKd*a|?i6g?f5 z(Hj8a%)sGqST*jiCh2Nlehzw*AAdj>Fn~jc&fT)8Q8c1)M*0EiWb!`tc&!JdqU17m zG|Wl&Kj>U?>xR>aHrrYij0%U{vCXIg#l5pAKOj~Qc@=}_<02UpkTEO@bM-7(TtqtT@Z0z7RSczKH+vcZ)&f-pZ!bdp5k+u2G=aY zbNxyUGfyp&PhYdElVHl)KQWlVz+?h~3Z))<5-fxI0>bGWSfo$#?U83N!W|FH$<=oI z;0Y#%h_*zmrq=hUNF~Bn;dJbYG9F9JHBp&JV?sDJU$_h; zF3ez#;CGk-@V-xuK&DojfcaJenMi-IHgbRAp~i-t7`5Uv>X6KWCxghj5~3KnPU@H= zE=r9kZxwCY@aM-wCizzeaZ04qkI+_6I9JpaBvZwi_RrKw>m8FSMPHdh@J&QGX3r-5 zhXLQ{pF!W=8~W@ww!3sNkbBXzri#jy?q(RHbJ>&RqX$*YSy|sG&>Eq<2r%(I2*&79 zs{W^qSSGt1I>_}azUwrc+-M`yee**3?ptl{FnblhOnf#kr`5y~Y9&W&tJT%XeDg=D z`XDL*itSohN=jAllKdWZlde#`&$#DA**rV#cddFSxHJRILW4dY09sc<_!Zx4=TL}8 zO;CT1Kg9B^fyEBi{m?gAg}?giF`}ik&!H!Y4ZiYOq)^^tb=T`Z49f3MUDnC)(;P|y z_G5t>-j(=rIt!<9%RiW@p?tdK)v+3Q$iGc#gAk7J&#_Wx@)9erYvkuWd{C zj@2V@DB=Of;<^38V|rPH^EwQNN{4&;X22E`Fi>XNY6No>^qBonvUq+&+ymFI8;rk} z_b&qn_oqFB#UpbXA2;+N%9t?jQ2lMJabI~#-e!UlmI|h2mUBS|3Z%f}F`F1bJk|W= z-Z9P+2@|HvLp;yk4VxHbjPRH62GOgxs@_W#&m`%_ue~QIQ9+7u2>WWtJVy?%>wfMR z#t0n$2XfXo!*wyQ1#|!5sjZxn`(vR6_oW-t2BG%=N*f^5fj-vfcadggQSVGPAfq_| z$c%E@UA&_M87~J8&6Rj!Dgs56C|oZwyqe#=8pw0)RlECPYPlouS;R^fAFkMPv&Q@J zv0kwyvyxK;#O&I;XN$!=Kq>&V>cJd3w&(-pQ1BQ4Gt1zkuGAa&x*V1R2bKC+`7WC1 z(egI@SujQf8l=eU(8Dgk49XwoF_USfB_?SF?P|wEvP8;@{U}G z$)#Rh&yjOSerw#GwZ$wMB9#tLRk}KGvqnm-8lNLwOF~N5O?NO9=OIKZKQESE+49pA zL@+F-4l*lR7et8KvbXNsvdOz!E9Hhg_a{w(u-FtJWSj-T&S!yfm1VX<`oPQ5Ws2ATb+f< zF~-rP6Q^62?K;aU9G%}G`V>-6@5el?8R4K71jP7^?Kl6m?L(55hl1Yu2p-_D?4&R>(17Lgr@H)$0NEj^d*^^=;M(l z()GWi8lrQ3B~e{aqn_j&JkdI3SgZy;|kSop!lY%s#@G zJ1u_!H@=mkF1P}!jJkeZy~s8+gh?+TQNF!Qt&{c{4v9TA;kUPKL!dy$@J=?V&^XR2 zy%ZO<9bTq{7=xU=V7~B6G=E2FR5lD?0g#Kw<}YmUctDPB?+A{QlOyh|!7imr(#Knk zMi;2bk7`I!hmSMcGG|kw3{G>{LyD0oe6h&yxmWE`<2gA)eX`_vvv26%T7lWgc01&+ z!qh6i6E7pyMvw3iLNt)|61ulv@a?L@uS{?)q4j8hC7Z7$7^LCH8JLFU_!l)Oh3~!qNb4I!Rx0($WAjq>HG)>p99Y zv}?*=wwvRi`K41EFnWT#pOxD!|EfOu6(XRDV5QsIE7LNC|zKGcKBq;;M$e+bExF;Sy(Z&)w@Va&_9wDjZuc z;<}ADaLnXM6fPYzeZX|t$Spvd@zCRY-^INinuXJiGiEW&%skJXVk~E9-RsuK{OWKC zGh6@X<`Pi$HO)mEbbqgb)dK+XmK``r+EEPs+jvdvJsHJO)|unmV5qX?8!wJl-2aEi z|5PF6(4C9_oH90Sf9`n7AmQChC%NR3u4{9D?k#lbyx>!<9HWV-59;oMsOF{EQody; z`>ZY()*TnSO9vdPF-pum`rDj~#Ir&DqI%8g6g*8!Kixz`ht`xVLIyriAoB7OLl=Tk zpfXnz${KG6XV&XGTIh)G=B#FEqCQnq)DB9(kSYGhu{5~J5@fupZb!Hhc8whx-W(z> z5;p3L(n~#Z*{)vU+_jWpq9hqBDfETdmpZC)`}{iHPxtiE#VCkEs*jLfEv1n5RrU*` zX%+Kt2MoK}{cY3lC|y`0A$gwAXtF4N7=^orUXSx&>@gvMd>QxS|~D%sG*@-8sBVdc8n3y zGx;{h7%AqUy8ngAR6&Ee(8@=irvH+7|tGpH1r z8rKU1Kd9xT@+-GbGuQ8Jp8{sGkp!Lw^gRxQWgkev`}vLPL>II)?_sAoUx|*>N~Dhu zOQzpyR72C-w;I9Xkr~)8lI5`CtKt$^>hIE{F~2~9({UN7JvCvNPDu8hZdr@+2n6~D zfZWB+!P-J18q7(eyLigQP-!*y7wyZSloTP#m7y~N=nJZVE+QQg2GU>}6bn%_p?NAu#b&u=l)MQ0;GqgJGG2w#Y{>b znr2_Pk-ENLYH-i;Y3cXXmgXUfv__CM^3{!cX7|+AzlS-{_;`5Ro&#;xo(yN7M0G14 zGS=6U#1){RQWv8Cr8RWyjGyd_+)1xF89(t~qmsTpEEjaVaq3|={S+9@e;_`ZaHDwk zbo=K=_{*aMp%+7$-+h@GeI$<+2{u9)R+RyHqp)DYKS=WkMv)V{hrSP#;9brq^2(-Ro0L|Cxr4bM9CT z>-4hD`$;qTS{Cna+ekM+hXEdbvb`+mcIrihJ4lJ|J9{whp0W7~(B~e5=N^x4D*J66 zT%?EGRgPFS;vhsU%M`dq-selm%)5Fqa?Q1b3J^-9<-y?x6}$a_9HG}`FES4=gWH%hc)Zx41}-09 z+6xXJ#yt9?6}jZ?O%p;Z?qCp6(H{o$1l1o1dsUOd(Chd|;jZyUYW#J<-SVUN^IT z7H@2x!{&xH_4UA;{Vzff^n2&073wP<%>3sD3fX7s=ro|e=jw zSaBnOzIq9Atl{yjGd%W+ZAjVbcBAF(y%mXP_ixt53^8(rukBT0OrprzGcb?=^F^fo z@2P38@MkTrJLz(4nqLYr3jjERZNwSrN)2{Dhg8ifpF_2wi~jQ)r>2a?b-%X>E@^T- z-d&Pwa;1Xh;7SlGM2AA!8&H|eb>g&u!Tt9p>kMn{MC?*fS5=0742h(8L)C+hT4<~| zX_re!Z7{~Vqv++r`xV;T@&^x>e`#Jh)yZyosNvcx=4Tu=0Zt9GJb{K?48|&R zivP^b)&br6R=OxC7_n}PyBqA#RQy>GBm4h#piW)B7b-_!qz(hy?ETt1ig^1!W0~rQ zHqi>H+$oH3I#{*L#!h`NH=6EH-%E+%2a`>}A%_bO&FQvkf*J7sNMu6#rzusQ$K@}G zU}5Z0D4dx8sLfX{0xjq+C?68s4c9(R%GdDEcai@beWvwIX^cP$o3;V1nAyLQ54Tuy zto*d1nXPw(cn%%7gZ6PA$QR`Mq)z3adq_y|t&jHyc%5HB`t*Ug3+^eQ#awiT^Uy)W ztKv>#zWU;mSC$)z%NuE6>6KhSouPXAa*#)Hy;NO}8P$fLsAjHH^wpEcRD<01&~-Mzor_l| zn<0X8@Apw|59VVb@ST9VlvRw|m8H9sZhP>GMjnwBPDP@j{D?nrwwWxk}F6 zCRiLq*%*g8GoUeWeMf=L(>~n%V^oPlkm1A8lLn3IHD6i;N-XRP-u(i@S-K?@8vu$V zOR+*ev!0KaMSQT7sOYr=I3866YVv=98dt*o5~hJdfBbgOb~#d;H1JKh$O}G5jK(9sW3;6}B;QG?V z!#@#X+#KtW^VjPSB{v!5UxYodB!)>fyE$`Z1HLjmzB1_IbfTkoaApfS<``Rbz?r=? z64JkS$~tG&Ze3(7j-Pt^!1zSsbBrE!?+6%>!H^AkmC%>cKA^XIs%Sk#hluHoMJ&Vm zh#geegyVjFWsokCb&-~Dh-;spW3<$??i+z=X0e6?eXgWJ#u_5$Z3Pu!omcXT;}Ffb zFaA*;^|Sp%y#oXa z-Np~-E-&icGVuTG4U#9Z`);%=!WsCTU*&_RKX$aW;dAt4bCf_JI5p{*&}x}bhW*nU zS?xWe=U@DXYW#0*rayt+U*p1UnkdhraXx!vQflg;r5k%Nud_RYiDp{kD37&0TUkvz zDE-==@od&{{wAIWA3vW+j>~aBpeJ8_iI4N*EZU_-K&8ILu5uXHKeA_j#eP$r*J6C4 zybuY^^0v`KrxiF=4SR{f#@0q9i%D9h`PN4jh?!c#fCo4n;&!*L{c6taHGVhbZi20t zdGl5OR_0@$j}TPsze-Sxu#4dFP$CohWIlG_h0jle$_t=at%nK|V!_gzuA27JBIXKk zr=Q3!)QEY1rFa+knya=Nfv&`8eErAmPGjOs)$|73)94>x0C-n@ynGIuiSMr^=uef# zJnxqtok>wSH99AKrBeU9zr<#@kPY92j4#PxSQOd6SUv23s70-g1U|AxGJZ6|h$Bl| z*=glky#?*v`^K+-6W=G5^La(q(3ZWMr`ErP>h9Bpg=EoIlXb&>gAQx;TSd~nK+@I; z=`rDMz|>U)qmdnC()ieb_NO z*mLyug0?g2vAll|cD>uS_sCjDLix$78^F6gfE`Ew1Fruv0p?b|u6lA&;~O!(uNd>W zKIDbL-ysNaSKSKJt7o~c>_J9Rp9uC123lFcdEFxgPT58iQiiX5p`p>zn2+Ued-pYO z9}33x1v2X>Kue&OXkg}X2FaaeKZio(OrARznwH8DO<`#l?`03WwVo;+l=uTSXt9xIZ34ncdNHzn^O_j4Q4Q(KC6VVea zhr$~~D}jXiM6NaKAopcQT>7U%`u*kuh^SLE4ty6udlXbwu?Mp!_Fbc@N8 zf&{jky80asw%xUlfIw)2#bhN$Z!BZcYQaJa|2?CoFuwXqL>y2^^`QmSE6$YHp<&(s z2Fl}VtT4yl?st^MrnrGidwB2cjQ#sXJ0v8vBVug1qART;H6sI^O0mQhR7KA9)na@W z6s2Y6)fZ`>GQhB8FTrKjq#oUt{${BPH%!{ckx)zsnMN5)?ZWB+|j zfuT`%^h?<~KVJh&OTxFV@G^n;QKJ>Gq7$0nnn!xW1tJRaa|xmHWc7+ZTiTj`ybcpo zLO;;(YY|yln(Z;@3N$I{VX%0F++~)=2iqLvsU|oB3;y9z@naj&s3mL?s?uh zcx{=>$jjfAm#t;3%)hTaxvw^qO=TO^f)Z?d|f zVyJFA9@TntXOge&)f4d!dulL}4UzJNyC2j>ymt(uBx7=j+(k z^3$yeW)OoA3_!3k3`@t#A=tn{z%|G9nf>;=*&)QVia&t4{x2vSYJNY94b`1J^4i3r zK95}~ri6`YEX)0dy!px^vGL`@v~T!=&>|Wy7zpZ+d0Rqrp~%Y%Y$C-~7w&-C8qsXZ zfVRNuG+{pq`QY-kX~NA5sMr~m`zK*5h!P6RbFmr6Fi$ex6zmRX?o*cvp$1;PPg#N3ne2j+A`vV*QCna)MZbm;>42xLz($!jT>7efWh6-a#aNn zs*0yVu+(@}z8oL$F?417okn-{O$X7>E-Zk1fU5n4H-8rY(HSe8oW9k{378c6V}|ic z;o^;UWK(c+$?vbgr%VT2DhA?q{)z+cut#AvkQ1ZJ^aBqMslaVfCY`Apuq?G}7e*@_ z9cN*^esVCU+zEu|W;VJT)p{2z=v@c`dJ|Ml%xYeUJXYhx18CT|SI_eekh#yN2)AzV zISv&Vy8*a8!9vPk+w{Tz1g-{eGqkJ)QkK#{Mks~i?pvO56(H#^o1(+}r{=72MSd|#ix3Y@jX zGGTT~w|(rE6Sx@syN_7~hG;!ShLT@iv|>Er{r&rvYVInxYMCtwB=NuY<8Ue*WD9Uh0Ik-%jF z`p?5v#0D&TRc&&V)ZBM1^VStXKdlBZTx-;_4qc@2+lgynyGP=Fy@>Kg5p0jXF3_snfDM?(1wTwAkKVQ47aJ4IeU(;j($7ShreQCC}c zKtKedLXjOvR}JK>xe?9QqZto_LSf@dS}l@$wJnxI98#vVuv3N7chcxqIRgBDVfsQ` zx?kO`ye*z(a{t}a7gL79r?7l$^zWkxXGV11eK0Y@y34A;?hv4L7^aYNA0B}q)?MWd z6Cde+#jLu0KATRUNv+D%2ay)B2*lE#hs=UV;Y-?O)D3TbCpi8itDOn8H*K*ngFxqr z<@O@3zbki-7?o7Cl&joY?aCeQ{WK6402l00JFVd=RaX2LnQ)DWnqYH{Y;MHJxC_SW z*5k9^cqZ_r$^SH=wM|P*-4`aVeK59uI`PZGMy=X8WLFs-EppH zc2XbsNy(zxk@}%zJXl~a%wA6J{2R0n=O2bHP$!YzY5hFIJRn*3POC4D**LNS!xS~KYqSbTmvv-(LjfDEGmz)0 zah(v6nXoy)eB`)~*K={Z|DH?kk34&QPx|29B_dEzyBjOzt|uZ~YDUUS2cibmBjnHW zQ}p&^R<9E^>%>#Y+)*cMBE?q?x7T(az2OSV9-jh?(Bnh= zrO5JoA{y%KSxZABRoqv?Pgmc&7V=Wh>4M&SrO+ZnrwbrZSDd{Yw#cv_{!39qjV~IZRG}LB0aPR-tg5h z+y1I^xXKYD#^{zCF_B1a>~ILz1bQ#XTR`!+{DkT z*xoa)aDc&a>uOnspg0b*$uv?svgUvtQ6V1YWqfJCG#Y=euy&^^U{lB zU{-P_c) zbH8+K63?$@YWJ|v0=WUjax+($P{viV+jU2Zx#&D(Dc9p!ZF?@8BDcBOY*V9dZ-_$| zZ!j3IayyueWX~^m?6X4UMwL3dXxlywl|*U2k@^51VrtMp?49qCB2L@%U~fsC4}k&! za3@|fUf(Jie~}4^QvOF4$t|MTY_^(5LA637>;fmy>-OkJF9^YhDW%FvIwi)w-A}2q z$T>#&ug@S|?oak$;eFT6y$bvRMea5tGWuy6t7y7?5fLnh@~|0}c``#&bVWMSSHF8u zxF?B2SDjb;iPN~&tt@AtrLw%{VSPoG%a-`P}`tm+DSV?Y%f@lb8(3bRrZbY+0s;p>d|Do-`>|gN`FlxFB?zzun zD%$ol-XeMQ!}nzNNuLvpMDXyKdIkJ;3rwb7E36kbKbKaO6t9 zi8kHxf`cLMA>k&m$DK`x$gma2^EcIi|GE-qVsUXu+-bq7_bpDv^@-HIZ}jL(*+pFHQl10zqTd#S5+MT0eVE}@Qx$iRNA&= ztZ~`_8Mz2r7V8%y?E^!7umM-&L=BImQ0EjbC-#Lo#w!R>#a%Z7W@L$#VhN#YAJgzf z^LthMBn`093|u-L)S@_$fU$3&_?idT-J@2T-90ai%y-2wIC{`4no&q*+#L4GW(bxR znW(DGEPrX^D+fJbz9N9N_Wc=dYTSk}U7;8*S{|Dp#NAJlo;mYZi%*(pxBhLl*xXQ^ zFaI+d#59u_09K1J>uu&;V8%b-Wupq$<`AS)B*f5?AVD*8rwv?mIdbNZo2=;_$E$gC3i{7RxTraG>^2H($F=ut`P9V;H@Z$3U)Y&wLOfDp0ZAG;uJ(GB0!HGOo^&`PaorklXU z(lC`Z)TQSc4SL+GDED(2=Y7@G$G*qwt4`WqXKudud!dGPEt^+;Et!nndy#bUeY7f; z>|1o1?;CoYOkcpn&ZtO9d+mD14{==&55H{@yKT61Xty4H#J0nKr9J zSjtWNQ-}5W|FWC%+$bur&R%-$Ybn9QG{e@Xv?1hhjHS~@r42scSkI<`Jv(R!EIYB> zJxhS@uPO5_$-gW!b-*3!>(mWjVC+_i64sv4IpDlv`+i#UR0q_DF7;+>=xjO*hNE@) z2KT}wrdwZTB~2Alf-S_u?OvT2KjO5rReN}}&ozTqjg&`^O%z-XBydG6**!X@rk)wS z4HGI#<62R>PE8+tAc`HfA+UUa{Q32UM%2Ev0O$3HYr1 zLKv40$W(Rt4+bK-9|YfPC9d@UDTGV+Ww`x;7QeR1O2ej39He$Nst93y&Y5-U>$Ue! z-S)BU{p|5e$`9zhtMdTO9VLuIu|AfTF82a5uOG<=UL!48ek;gIF~%29;|ujfpU&8T z(R55*Bv^++Sw6DoqTfUqI9pD%>~!>zGS2&%-sJ`rz~l-yaNq<`PIa3Yoqp1f_wC)@ z1Ck@F3qavv>ZZ~#TLGtvNk=!Mcc}u+^L<1y5KsFrzcsLi#@eo-IRHox`mRiT<4i8c zYM&9(diQvNCY@iW(A3WVaGLhG$;Ejq1?bc6#7ttf;gCS`|Mjm2rM!b}r2sL{9z(a} zdR{-;1)@?hA39Z^Nc@3-wmnrVxMB&)8F3(Thw_>w*E;?J?SfkAb!ksne;-5tWhBA( zR4yMdV;^@2G6acC-iUv(%+BI=!dTzzO~dHS{{B|17qU(Y@YM{~QIFlLO-Z?|JF9oE zj>iiAdlM)`sxQRc8-`Kp;q5r)}O_X{(EyJ?@XSMt_VI>Q~+Y11z}%v(D5T zZ6+MQ5@`GJ?|y)mb(A}Tk})BroZ2e*)pu9;=D^}}0fceGs3HYo5*%R12^h%4X9XVB z?^-Y3FskddAFTG!VP^V>=DctW&mIyL+@jxI|G&jg!Zq zy|Jq}Y&)sQB{sI%rR=<1RzhsE@U8PgBU~sUs7jAPsM)%x8#JpU{ ztbMZHotu7Lch8i@Nd9v$j$?81AO=8`JcN586h;rNU;Ao$M@Ya~sn=o9#-F1eJMUp7 zjJ>$0^{ejUn>Y8+YLM%zeKOzJV0laM^8peg$9ms8$ahCb3-6GA_+nL4S9@dWKZio* zf@OO!`N6ZRSbEIwE?^+pi{dHdTP|PT`-wDM%pI;Gh zydut!6!V+$81L8)*_hYN$9N0R%F&P!hj?6_K+61?_}o^({E)SKUViveGJ9;*pYCO% z+XI)a9K0?yLKPDb?&G-RE#M#=K42zNoZEh{G`l6A^EGa} zjGM`Kwjn^LY&|zTuG2G;5+j1)Z|erleW*`!;)1tzqCujrg0lTqfJ2MAT`(H&crXr`NF9l7aQ zra?pd)9yE}i z4zD{GkjO>ZU?Y=u$f@sL$m9+>95k!=9yclj{IHh@*+q}*_R)hs*2IK*{1d!V@B9f9 zKiF#gLUcU~D;ICS(cRl7xraw@==A*%%caW>?`_0QO~0MdCZ6E0oFwyo3-6urVJi^GAGkJ5jI^ix&a-<3BN$zUxgCJ(H`ncB2)E%vB=oWbR5Ln-x28 z-+%IB)i2q9nx$l{Tb0cz&--eo^^dzQ=dFbOM_?Mx@pc$&!|*0QlCjjBw&CkVQ@P&i zDl+}~PMqaTumu1q>BIaYHJoQzE%oZ79+rBAO;z@`*>B*UiE?P~e=Yt$9Uv|&?}D8` z;elKv(}NSj-U^zLOpjuP^U1eK163dm@Bi@Hnp+hV1RBRXDCC!!d=06#{O~$}+AX{_ zlUPqc&f=TNa7tjM!(bH!>fY`JSGWlJH>w}0v^((kNapehGv|$H$x+3F2JD4?32~9z zrwB~egcR&F_=ekT`WfZ<;PRpl^i`Hg8!NoXva{mFk{{tDy2o2M{gV{QbaN@3To=^I z+G>Q35cIH=Y*E3%oMA6laDCLJMf$mk&20#L26|Z(93ccR{ptez3=7UDzfk1^$@Nlx z@ho6b(BTtJC-8*s`tvsi5Kah(AK&?_YJ*Eo79I-INfOdeTVIbQAxH)YCB_<4taz!mY`Tz*ovJVIHwch~;i@ zOlNtVlM_+1&<-wej-d@Xj}4W2-5*ze@xsg`RF0y&#?C4BGHUrH*0U0;D>0Aw)R1 zrR2NW<>7sIg6dCw)Nw0+G)R{ft@V0lo#N{q@gDZd7aI-EyLd@Nyu$hdhbVWrcL&%d zCZ@?I0AI7xkQD7YAFhmL3b7^_XD=8O%C8pQL$1@uR@I}oMHrALQhaaY7Jv6|S{})L zM8U73%g2sRU3Bs)T{J~JO-FH*E*cSkkq$x|_Hsb>dgkk-+S`>C31*h&P^o-HmE;?Y>2_V;`jp)M#J-L;|ZjiyS*HD2=$kJeZgBZ_cw?$3cOvhNAzM6` zgfPE<rJHXXv@KIJVNd4UPeFn=`q2MkmJ}sF= zm8p9I%Z&Fy`^LPN`$^rsLr?tUc-U-DpM{*LB*XSVv0_!8jRilwbCpk@`M}h&>BzR9 z8?FvWJVxE0_FH4NL&=@@he7c!A3!H6zC>!Zv=Vn)PKlJ2^d{~$j6QatR`2zF0#29p zq8eDbb&lH)35yq^HMW3`_`bWb2K~gLmH^JOL!&EbH4zoFXG$lRS%Gyq+q#~Tg^$r`=((Q+?E(2nFkkJl4_KCx8k|Zb zOC9&p#bj8XEDdTSq#BXgh0EXHny#Ba?G2_h;V~p)I-bLs4pqDEXZm~wCjYg{H*WQ< znr@M6v<_ZXtJ!7x&R?Nk?(j}b{YeQD&Y8#toBOmOK|;;lD&_!U8#C@j%S2LiG&?8E z_XiCfgq(Okq64ja26CmBY6x&;w52TmRo^@2x= zPuqJh6I582Eojkv&6fRi@uM88Rcg`ToV@JZIoe{9&^^)X6j)I6xQSK&9^pA(g%p2W zs{e-#SBuo#E%}~RAFXAXOu5b}{v6KS?I3&ZvJ@WfFQAijS~)+T$oLH5Qv*v)iX+jV zQYkxUa5ai5aRrp?RWos0vn?@Bl?1%%l;={cYdn@RLgl_ z)9r&5x+A#vUSoP!_q<`?11vL2=-JP2gmorpL{&$C>Sc12hZP*UonhqV^UU+KU2Xct zfF#T=Q*XWmj;%fj_YMHb5;3>dC%b~+&OA6uH|qS%q$ibK?w%W-s+awFV>YI)>aev* zlzY|TjGCX<3&^5}ABLLWUFF?5~dgdel%E=#kIt3)_c{6QR2kB{y~b#!ZHu z_sfW&fW$8fZ*16vYwC^>Z|i)Sw<`qOV{zx!aY4B3@twPD}f%1Mmcq!xNCs2s zjmNs6soxL+6YfgqY^)CBm`pOV=1HJogProGGSGUmySRQkNmu82f>wbghrja=NF4~wVP^gm!AhC1qorNREo$9FJs8OpJ4 zZYS5NDnLy`Tprw|Jh35uKR$mm*GTvGv$^+mNm}rxLWRvl=vaR};?E_120uG|t zrnoirA@f22=zjj(yWJzwiXT`NiQy9R;DfFb9&W_p`A~j$n-FCc@^H zuKroG`@pPB(uD?)Ts1qXJ?lUkDB{tN5-)@^B%s2DLAUxHvl5>RC=T!IX(Ks;mUTLF3zlaUk@HSwcPiZ&VBE} z=RZJxa^$YBGYAWF4m0Rcea-&&)~d5!a%0=is%@OprFCzB6W?PRUn=xAaQyJD${i)| z(XCq~kz@K-L2fV@lP!SsacJ$9-xX$ikAU|e>H1@Z1I~9t++naP<95)M0&b-Xe_+Dc z4xtoKs46%|UgOo9M`wO%g!QI0ubJJY1~v~RUJoVtWp1Ltl}l#UF2~{i@Dw;%0P3&# zUmZkllZVBYYq)TP^Q`@YPDkr^#u6E)rA+zXaKUeuCu{{&R;>m>z@XID-91ln=+C}S z{H+HMP09pbwfPtimT%G!k0U|y^nYWNXHxLpGEnV}72E*g*0c+|f)Bn=OVnMA41gZs+| zu*zt}3AV-BMG2h6YXoSrK2QF>>gbw}!QS_7{~111KP>S3N9of$?5x}lD_-$+W3^42 zq4&rel>K>szw+jSZok;E^6ctq6}Hm4uC;@`<(XD!Tz~^v@a8$@`q>nU`^`qqE{_Q! zA4~6S^?8bZ_mBoi&aCWK$>Oa6?LsiE*32+U9G)7J-ipJtu$+y*{K;hJu=Mr7vWQq; zS^cz?7akszFPF6G;R(rZ8JAyVnWFg5NJZ_mQPE*(Ukythi0%D-zg5HSz`9_d(!=_w z*CQ#vx7)vW)89+n&Gi*5I$6pZJxrT(IO;w{lhV08*s9Mk4wi9yvaQc&&4uPm&|MjC z(JJrX-l@L;B zpuf&fk8}JYA6UR{ja6&Z)_SImK&;W4bpxZU_&d*WhOh>Z6Oyy&!ngRN?@^m)&U7|4 z9akq#Y)?#%>b*L5qOUqAql^DVs{`-j^e;a1*dS zhey10+vyPWiB<>Qvzws4$bD82m2*2GC$$7_N>uod2Zh!q+<&+8$6*!TIJQ-j-9=N5 zWxGW(z>TspEK&v1Sr@vy;=IVx-QQ){7>-Yhg!Cx0dGCH=u7hDgFVk~sFW8qIcDIe2dB=!lQn}b$su=5M zV{}{IdDc`2QAm2EBFY|>^lW9zmdaYn5+xBu zB|V~K4~0m|lBA*}mGnFJ-ghmZ@AvyVf84nH3}l- z_Z#>YiJlsH%t!gl_Nh|mf}0*rCN2jAVD@GCo8Q3%mpg-GsFiTaFg3iSh*XYGdRx5s@0RK9AwR|LeO0NdCM{*|9+w_G) zkc@`2e~xfNa@rf5rI4uml&DRQ3qCHiqS{{UbpYT)9xqyjWDFnzsg8_k1o;IL-8Hpikn!C}oN`I-G5 ziV#a#ztMV&N%^T>2`%0<$#|Qmxeu=&z0q(we^Iq4W+n17b)uCWq_>S>UX3Tqx1U32 z*DmNZAQD>+0E3ye!`Cv#H)MMBq5J5@W_AvtUz@EA{hCt~8uoAHW|yrfkOq$9 zu`>GgV4$H(Q^-F4S`Sg!~j8x!{JL+yQlyt0kR!a&GD@zHW<(99*B8x9iT= z%t{8dTFT6KEqTkXDWF*YI2-tA*5@HBeX`-harWB@GFv6CWL2zHSkg7pDLcG+2+I$` zSPKnfVb)L5u5Vbm<^WsGn)+VtH;33AE}mX`E5^Nibh|{Q@VM1e-T-F zD_+sb5tLoz=4GfldmTC+aPe74ko-UuL8xrJHX(}swpE$!vxG$Yn^4509I*AAxbn&s zjaP&jqeUUPSWz1Kypwq0z-F*?7)M4QEg z3=-F#@Rf<8dte`uqQLOzMQ>V&fHFhR&V+*K*YNrbW!GbOdKtmKXqL3+$Xhm4QDlS3 zZpkk6%W03fB@4|OjoFPy?CaL~!UGL}1bgv;&1bK!xO3Y?^BSbcb<3eh zl|Sk1J^$OrP)!gjk>Cvf`b#5;Tg=(ypMEX8E?ShPEP|FH)?bFP2`-HO5kR+W*HFki zb&3_U{b2n(#V0NQ6V=n^%hM!AcL@abHw$(#j z{q*r$n{{c{YQFSD;W{;Tw}CEv;2fjxMT}?~OzeAx8_0`m1IQclUSBi;%ftn0eBy(- zC8uX)m0dcb~(-&Zy41RI*kIwx(D~%^Zy!sa5>p5PLI6a2_U#7)PHBa@&PpB@YE>})g%_G zVR`u7p-2+dus_o1z&SJ*E;j$_Znp~rX>lXJaQhFC7_5HSVS^X0PNHR!BI3cm8NI6> zF}gt@r~N%9vrBJqN9ygQDi0K+aJH8D18!Yr4n7hQ1dazHo4N@^nJ?{we_wk%GoI)u z89a5Dx54n@;LV{5__4s#-})-&&R3`&dowi2YygG@1=s8Wg7QQIZxCHjs#%-4Oc0$z z`aKGeitGwsoem9tRb(~8iB2oD5`>AB36vhy$1Kd(^nYh^O=rID@>1tPyGG#^tlAaQ zKLUn0xknzH$14Xi`*1r1#a*46!hFc~$?JEG-zEV~n>d{2e?yky9rT|!Ou8Q0*1{^S zY^uJq;^!&&^pdhw`k&QZ6q)GZMqm6-N%x_-^#{9f8l^>#Hj>;slE;)62HC8f} zVQXc(gG6>JgVlIvhD2`7X{`D@pSnqSLIz-~tMP5aeL~kF#|SOj`tlNw3NWIu;J_4k zAe6z^xWFs*rr-0r`im|*_`#%vsty&Vc!mgSwjSt>vleU!N!DLE`3|Mw!8lQz=jFHy zA{hh=**Jwuu*rl&w+%L6qo3D5S(|ZO5=}QnOeQfZflv`;r~W)fLkbC(U$K(HOEK2H zIsbfl6xiC7Ssve`m=1CCLgKJkZhmb?w4=YS0 z%>R&=J#;;i4Lq<8Ibak`kr>E9LV^*Nid>7U0`7gO=+rvrf=y&i8*RamM2&B|qAG4~ za!>fuiT}>bYFjc_$AUTRzKWF?lCQmI29&M`03QWHJZZG4J5HjbH}GX2q?})aGb4X) zf`+jus1T8}JGZg5Xym0knrijx$TRfgL{jPSL2oWeM#?=IPfID}L)N7*lgL>8@!oV! z*6!!t;2x9jz3?3dgF>me@x>Twto(=3OG!R@tH4OGNqv3)bDL`04S}CQ;^<}n$bN-T zvI%dSTGm=}lWXR$&98-K-sq@upv3 zKCc#HE1KomoC!#x|x$pO@$^~*1x~{ zsA{&4r6@}oM6H>7Q|7oyT#ecdg7Vr0x+%9D7>^I~oLopDrsO-zc>ZLOE%Y-rhPDhy z2^^7wMPA^VH(iP!@0$P9zs+!QKvAFjIJ!qcF_znq=LmYEz0NaBm7V@>+$EG2@lX8* ziMddhO5-%benT$$5bqZ5!{JHk@Aecp;Fh2nf8c;ETk z(SGhAEX+{IGJ1=$4kP}OY0alVS`HR81>%@^GR3_|Y`-WiX7RI4DEq0}@tZCYz>L?8 zo&sGfcc3PlYUy#A&lLpUjTaTJgcPSgiLd}bAoQT}g=RIC21bAL3pVO1ptD4k!8hG5 zST1tq)IR>#^To+^eF8f2SD(df|985ErO{Rfm@e?phi;Q z4lpE)*mta>Q(`NQ1DR24)i<)sga_f)ZKmX>oz)zwu@Ifuloh%e1`q@H&yB)5!U!kC zqR?3wl1UoHAf0T^Xm2PnQ`L_;Qv z%izVjnPA2wr*{q#@Mb~Iww0yyUk8*@^pFtPJd{oV&4)AGQ~%8(Ag|5;2TfTSP<V6REXu{wUfZ5xbKdhMrclQ3Ir4Pdyl+u z{%L~EIr5)8DEn{EeA6B@(xNAPq!A#DUcx3%&Ttfs62d+|0XHY1NaWT0>GELKaxR-Y zT?FwXXg%7Zow=DoTI)!>_T|kCayY58gK@uwU5sJQ0cdI9hm|<1(0A4plikv~4z`a{ zW0QVJ9v`_{#d~+C5v`1R?rHb%fvPA>^Y0dwO=f65RYZ#`oId!7#3Z<1zT!^RGc<*o z4a}t||Ck`&2#;|2>Z3;c8_&9L>c1;cawyuOi}fj%IOG$-WGf{swTxhC!FbNwWt8A# z!5LRli>9{mM;Q1#=Tde^$-HN~Nqb)V77>DBkA;aGg{;PWmXHTB*vYmyj>VA{Xxl+S zTr2#n&YjxPv2i;tDy89j`9D9~(6I;IG)77lui`Rd1{Fbpvigi|;DzuKL%$3(Zrz0G zi`a^%#$8pq8icfnb|T+{l^i#g?UdkqEf4cw)`Msmr$0Om8^uW7>w#XqnuoL?Ea+e9 z(TGBjRYzQ1cze?|bVSKF;XOT)>5(Wa&6`8m0t3aY+^_--4~l|U)5&K5dbmFwZ#d+v zfs=tKKAAfv_Xu5n9&Gw$b9(wKu$89mj3*yDyYX> z1mwb-k*TSWu%Xs4Ej}8)X_NYLKz3UvJ@D-O77+AAL|=G%C=5#>fq(2x&Pl-g2}d;4^)bLLgcoZ zW7U+c3&@;#?P!E_8`F4*$db?$YFVW8k2Eko40SNphYi4fL*yNHJQMYg$bxoJ-Zy%R ze0toe?8Mjcqx1Ej>T~T3)y;2jjWM1tATD7tg}=RF(-)Ndl8XhkDY*kvrrC4s@O*df zEdLFJjm`PR8<|!nSv1$TL6~nr;RJfNnX>yZD?}I-zt!-MMv9_>obyAvD>XW|_9l6O z{y?xt>60p*kNK~=ZftoD~CE(5-c@fK`PO149E;?9t zLQe8G-n%z1t~an6h4CJ!!5v=6&Z;kk>o90NEU|BvOIxe~GB>v9&{Ol8#Vrq#+!_m-2(CJ^9TZ~SpU4+}c`_ZT`h#f(_>`QA10A}`Yjz}&yc7@?3= zq_OGK+?u%MQmTu|P}Y9G%#|dYU6g*~%&t~6M@dB8c>9Alha1vGdi&2lY=nNRxeV_< zK`TZj)%H2vCxmE(={?yy{X4{p!Jsbl-qGKon4w=FuJ-#1L>#A4eiqlAe^roiL=T!H zv5vs}4s9@wE0@k)m^_^%yeP2)+3}g16!H^4x$^ z@0`kh4a&b#bv-RB4h^5&_}|g9NL1nyZE8wyZ26saq|w$-EwR7;RK_O-oD3RD%79fx zfjDXeWKuf4~!}{EE9T2#L-*ZEECf7h@%oAuWbA7H`M+1-70h_YhYz0 zuVt0tyI)#IbwNGsLlCt9PhK7_%yzh$v=ICPOg?iPmkJbzD0l(m{73QkYQ@^7P;+|1 zp)DI>Cb2T>)OpK6`?Woa3>RGyz*quVg)kC};QSdNm=+eRO2;E*NRY7&5fZRSW>=^W z6;iX{)s}IW2tM`Z!8fgb;A6)8DMDy@N|B)6b@9U)4Prr;@jvDYa-VN=ym&fv85zR3< z6`toCdd%*`as1nW6(8TQJ=WQQv&v(meJs7IL_eCUc~l~3H{4}~onEo`h%JZ6MN_c} zV3q8YQbop`7B-@mwL>a1stRRWmjz3#gUl_@@R}EltD-`ddYWiTPZt-I4t48EURb{L&hIO@tp$Yv*5- zq&1k}>MWoCuj|I&;>LiY8yqXvrwt>zZn`t;3=17|xTy_=c{!a3Ho$(yqYSj&rxPSV z`o^_!ETeA|*m1Q4ctAqh7xrH>Vba8yyJgK><_jSM@l9N37G-N4m$x&<1mNEM$Fe|k z^dio2h~Us=_py3|O?%N+U-HGJrq-GQHWCN5C&Kg_o=Y)lOIj3{@BuEIkmP2W8zwfa@KOlO(5 z7Ri|4RiNQeCu)o+tCSuDls!s5-70crk&=gL+XP}{!$P)QT;(9 zg#PW2!)ecDaub%apuU%oQ`9!nZTaRh_tpJS!~UQ+3=g@I*0R={1V4ZF&Ej51K5egN zn|7#5Lim1GD>n!kpMhx}7(~7;x8|n%H3BX0?FDYdm!W}y!qpCI`Hpm}tqoOOAm2R4gfC0>xl$O680r(ci{lLZd@-g}3f?TA8xUJ>;5 zSc1imTv`eiMAx~SuE2&>Y&g7e?+y2sBkO%uMAhpqL4yeW(VVBt7b>Idi`5?O!;#Lu zF5Ge8S%-q#O3{83-t<@X=GIb5XAT=Hg#gSxWI@}~#x|0vHomu~+Sw?9c|mswFYBQs%W z?dBBSf5}eZ6Yv5d{EnK4oL$yLFS_ITGm|IBF0Mc_s6s4h%<=SYVm8Z+B_h9mg3wfw z<+Mxk^xn>)VBx<}j=HNxYHF@< ze-#5a;qVCM22AHfzbo;o*1g7^fgX zA|-qcM;1v#5<;;olB;BDr!{g+FA525ONNp1^J!tvb5y$m)h?kJB9eEM$2gYFpCQ4V;4yvEpd*qRVd%FfF5o*A^wAw;QxjWukKKcyn-j z$v16dBSXfu6!afwx2S5YIi`BCXc()RCqQOaxir0o_G zDK|AM+azdlqQ)>ZT%?J8As@Y)=)+91b1@i{KvPVUy}X7~*JI4tbtvXts*ozr;$oEc z`DgDuU+N%UMYGgR%f z5US5m6a~UGuioB&3^TK65sMc{Hy?zf9Z1-XJz5b+1&)%I)1y*Bs}2eB5-|w|bsk5i z0TePz!Niihhf^YhGyXD5^3#&B;40gt+@!8G$1{7~>mDx20RvK6z@zqdLs&jZ4_xKR zD{dE-3m*$&B^C^9qqid*A#{6jc{>tjkkD`J+ozCM;w%v=mcs#`!LB`)Trv|cqO4*% zanqh%JV3#0pZdklv7gPumd~9Q<^PXc%26+k3#yPjx8`%%_JwTf<~0B7l(+f7nGHdl zwDh*HXdGj1EJ62ozo;Kf&1;zC##sEHB33|w+(s;++9ZJj))BPo=4%3)fbPB-H$?dHXm7*X8rK_f@d6yugkt2mo z^+yS>jUqZR!KYyih@L=4Zvy%EJPmjcF`lr9Gw88!XAH7nH^ZhCN^inr7ehIZohw_8 zBi<9a^`zGweFMJ9l^`J9x*tZA@#2N?OZKB6nLHTlB7cfC{Lkhq{vc8)1SHxs6#{*aD zwS#CmctGB83X0Q!q&yCHdl{gi7E`w1d0hwb14sydL&LxW%B}Cv#xKoyFCbn2A~=Ez zKoMb+p)#Yzv*bqz3Wf9`dh*@Z70T#C^|sdQ0OH$P;L6DtFuZe?i!20V(PqK@?jQ5b z_>~}^$p@rkJZyn3cIUsBQyf_rFMw?p&y!}9!+07gS_W=OEZA60>;)5^awj=39Cwm{ z`jIMY`(;WmM_wM;v_0V&)s*+q^2cVpVe5nfc8_nVGVpHN%y3jAjPdN%GfrRZqgY}> z_@A&vf3N=@vnzWjR?tf?vR&pQzpDgn3;cWgWn;@=KV?(QNLj4KPz-QqUdSzMXdY5-F;emn9Mo7j|I&KcQ&FUCuC9o*w#~~+%zT}vH~i1 zCVs!>^MYQ@<_C(KQV@9Z?LBVng$GDUR@gc=%i#Tu5EG;{!KajTH>Og{Lfkg#m5-5^ zunpKAHhP=^CW93yiBY#S-_^rKB3b*o&gJS9+(8pvWDAbHW^2k6xCQgp5+K4elI)C1 zp3UXU{fAcNTR*$tTuR67cz6lYY#osyQ+1o?-cvQZT+y5XD)yoT@rM^0H!188XXpMn zc*X&?MAVw>NNe}DS^3NSCWyS?m6ra+{5Y0tt+e$0!{AQ=5Z{8Yde$q;{Hge@?K-O0 z8B}qmX06@`BU$6!ThyUK&{Ya4))@EUrslso*-b=CqL)4*CK`ofi1DL=*LKYHF209Z z`p4MZN--CYkJwA+68z8h*OSW^99~$geHf8t((uS{O_R;l$Pv(&j)~jrdDY|p*o~We zxXSar7r1jI>;uDw6>4#PF*oDyagdenH{XhncD>{t$B-B|Xtu5T^uFBAn<5n)k9WN} zy3y!pRkaNPw`+KF(2%NPH+rc@I}B|uxgq%yoW5iXe4CNhrp!y<;W#iCR^~=pb*z2=86#G8N z{KBPEQH_C**A1^VUv-+0(@vnj%XVee0EEN5zgD4nW!qyK-L3c&R=ICxeBdhS2~0*# zMMc2+Jm__pEO8%b=>H0oND8X?i!x8X0uMwDd6}b_H%!*}=Ux96ePr7PT$(QvC)w-z zbMX=)pV(jQR$47w9+J{CVBi{e@8i?~*N;D=_Uz*E06W2%?LJR-XSkux^iQ*k4&jEb zA6#`GKU;Z~r#;zp<0VIFjz?fLzeVE~A!lAepz!Dk#4^Cs#xXJ)!bNKS{|6S6d=b3U zI}#Zc1-NTk*u)y1Y1f1vTC*BV?P?#f?dh){oqX4jdv0iL-tnS3+v%e0XDYmc^5k9q&B*3-bVmRMuHqa`iB4<4NMp$gm>s0ZSHRpVfQYUkJm^bcon&9+u zJa6@zCoIG#vXkS_+hj`4v5%{^P)+x}c{$fFl9=EI`?)yy<*@HBrn{?sHyaYEEBy$f z6HGhbiV;GT2bg!2A0dQP$EtlqZ z75^m|HI3nM>-A2ahB;=VEDYEjava3urmJg*gEovN6Br7g#O?|8c)I+h7Mo6vZEwO@ zu+R8a4^<7L9|khG8CyaSBG+@#Rpg@Nft|+)^@jBFB$F357Dh<;?f@;D5~uQx-x{2( z{u4Xqb`_XIH+dgn(g6bR{0o&Iq8vQoSPFc4sQsqcY=U9owoqA`i^V8+PvbHTv=q*u z3Qx^>2n`=zj$2i7w}yWH#N23CLjR9duqf;E*bB&F1`@~Cm7soKS3>pmA&s{^9}@K5 zW*tJcC&gncWJG$EM1)v`L^ihw(dVK+!^DqHtSTr%i5ZAXy2qNw~M-AJws-XwfM$Y zM2o-P7tqjA+jr0Jt&T1>rnc0ZuOg$}wOlRn!$3BlbT7TbIiZZh+8oxl`RO$fz!FyHv{C zj*dB2?!_d_2=X5JeYF+-QiWZA222e@CYF#qc;)M+Sk}_f{0pcK6MDBixts)%1K zz-Uy2!5CHFbwlNdliPl`_$?f5+ z+JAqgX!!@UwpUD+elLq5WHFs7m=G*6&SHvHPux^uj8UasZy-O;ptesz^q}RwwEt=< z!I(A7ZLaM$u2uPWo)}|05CoCc4#WTUAk>KK8}lN;)S#f&w*STt;gdp-#$Po@Fo*F( z9hcp6Z`=35le36-2PeLMMFk%2I2jGj$-$Dq>pt`^%S`DV^1ccN%;-BIXWHbsW!m%g z&$KB>%3H}B09OC7!B4zAppufb@ggAj9%isx>w4=~@0U&k(z6H<5xybN!%#^g4T(H8 zh>D2Uo-ayno3O+lFr2dwbDOmQ(}V%n*sQ)H6m#IlSzymoa+O))=5we&Kv`D|?0tq7F!F7JKwoe3141I}G|_NXb0xkCgnV&)KBC=Il8 zxF?xJvg3k3pjHLP{r`quZoLdAO1SA!5z1=EcE9s0#1uooRdfiKMGH%deVcuWVi39K zAD7xTwR>w9E5~sU@BA)aft(=RX7edGLang$!;M%%B|*$>A|gM#kY4BTL{xDe)(8%F zGbZaxWTiW66P$kKHhftVbPxMVb7>pBQ+kXKt7>i-HrHN*Zi^Cb{j2xi2#>jzr$X0U ze-DLn;u?yU%-Uf*eb2E;;3Hq@&27-oiPy`X2;<5`^|qvur#!Yjk^8el6nr%DtJ}Xh z>MM9IH%g+Jz8$w(XgZw3-(Ma_9JE2>+cyD%yu=D3sweR4hp^+o-~r1Cyet z;rkk=N$}7Gv*oaDJsOHSdSMzJME=A_tA0+63p0SsIQc|^ei zdE7Wq8>O7rtzH7FH5BBS@=qK^RgUsw`}-bjcO<0+s~Ir9eFQ!lZfo&to?6w5me4WM z2mWR2TvDe^J{TIhFICu%!j6lI0S8xw%0fD z-i%t7p!r%AMBXkC5$Q_c777$5;lhVRS_BIkPw53(;c}EyyT}0GXq)q;`E_A9GLR<) z#K)p|Iu^DPIHO4|-fw;rIAckD-X(MHUWs)_6<3vM9nTiqURWf!rlf6hX_5aCD|W=* z!$&iSPCuL8t-IGq8&Lu$De6B3sG_v(t!#ShWDaj{vCv~+yWWiGS29x@u$d05!IsL% z{5c84wR1;)?vn5{Chhhm-wTb6wfm=^4v#SGv^S&AT~Tm5IMAFvmoI99zx%oUC7W)? zI^7R9%H_yLLGGC7{8JEEo0pXOTi?6F&TBXxWc()gbnPqQ8P_BxR352=;izztX5Rg$ zp#=F+SQqnZd86qhNg43VH9EdF6_C1FD?9`(l48E#B^+pGuN5(rW0xbe*j+|aKVLUK zVDcqr@6R=}iBo*>p9-tbRo`Kpb?swj6E)I(81x3h2;HBN(xrStTwP3@eNmsh1S5XD zk2UREWA^Td`m*CL->x2rid}A@c9788k>pkga+-5p-o@mM8uAYB`XjUnZ1yo z)#ZII-`{Z^f20`?Gf0LSY97!66zwi_%qeUmiF+!JlCinfYbBHS`8MI%Gb%7(k}QUl zRSVM!R^RuvL!{KRyZ(9qh7y;X8|Kd3ufcTN;gVPfIh?Co7$UipNy|8u1WRu%0!*Sj9QV@8D3 zq1X3S#Ud6s-V$DnOx;@*)j2pOX{7N&U0CSJ(Vo5UepIiJ7|J~KTByp=>!mZ|Od_>TVc%)(NZro|R zakGdMG1U}AZwDwatXk*pj)-Mkh|9Td{y7a4sk4ELhQ}#3$%XufaN^I+M{iKW2^noV zjo>k~U%umWX?YGdM~HZ#3n|hN7g`bdy^nYI^jqexF4j|QO#=DYSkY)wUx|WDf`qxd z650rVh81{Et!yLw_^e>~1E0kpcj7ZJk*Zh1So4-vZfWrMhl0lbS7(M|#BC1UYYk!) zEck13?!n!E*(%-dL?(HFWurOQ+_Vy>Vf4r^0-#sl8$d)vx)%9rYM=EWb7oDUZEFNF zsTmyxA3vo}Iq%oV)KNO(_72Nqp2LJ37zVkdp+h^+R?}KxyeFD-i$DX>_*gXOIg4#T zhnnTM&E+WG$cKoZs(;VF!FsH*N=yAs#!WgF?3Psfy;) zSTu`xFw`EcMhO3lQ{&c8B{A;nQj=yZrEuy{|M}Plv5DLxuuE_HavK-6RP+fNgO1WB8}{N_u^JBxf(3~5uJ|)Gz}>cB)jsf`=<|PX%0bnX@~1sF!_?4?Ono?X z2d=?y!NdfsfjcI~D_-EggBh&^&!gzFPSNEO*zf=8<{4And*0^(t``*RTd9@f$r8{q zftQz9>ekWSQ7apc`sB>*A8}M3FAr<%Na(=85WM_F^@TjP1+4$F05Rj+>}R7Y6kww;DAm(N-lwLkok_Te1wk^*Ns zld^xhyq&U+Nn3pI0;zia0$Q=~KIp(Hq^2@Gu;YgQ7>%G)EglZ%8uRidj%_5Uw zfT(OcYJIFqBlhpe=U2LSqpCSmSkUW3zq#vKP^5L-;`sGN1AeUn$3LLU1Duz*``af3n&h)CV#fAf2Yav=hE|*>_Cuy;=iwGc3bXk z2xU1Bmcm3%eD)RX4N2FFPfQEV=MtD_o88SBhB3Wp&KIlE9JwR@F!97yqmlYI8|a?h zBmQiRX%Lg;bwrzniK|#b~+!*CO`0{L&}}`%9}FVafwo_UVXQEeOI!_|yj=>w!sY z+q$FaPbk4_jX!8Bkkoq4BE~JyW8E<9A1wu~lGSI7&7B@DNxk#n&}inh#%46JUg8uO zl+(sh;(v7okqY+TB&_5rIFp%olc2zrn3j2+6!!Z=?IU&+yNQ|-uG8*d-5||*`6fJH zd3*p^b&&*OOe7^OsRWPkneH6t|9Wy)wP1P{*D_a0G@W<;a5)Pxcnb~#(Jst+6}dV< zpjwhRBKaPiB^MSOPoMX#QTE#3)Yyk&2FA(6AP!34S5COdDlt<2XLUSl{af=6?B% z>L0ZgIU*|4!=z4`y9!evpyRYB^r3wb^=mi6N)>5MXZNj^nAkr00uvI;s0UhE(!)g> z>k^2$h|w>X&)VD>gPCq%>vbo-n~%e=(FNP_$?i#k((B~6b1up2aIZMuspM|px^v6d zJv7#L5Km4D51(O=w6)TtJ9qD^oE2keL;Nd8#Tp1LDxUjo-G1@>zchRq!jc8Hh`dp{ z3_XclPe#az<$KGpL1OKT*!D{CW=?C-#_&o}1IE}(N=KQ`RM_ZdLcb>tLd_#ixLyp< zx z0k?0AWk~1^h68}uumuelE?FyIedKTgt8#AV3RRdix{q(r^|tOq5X^jHFsYzyfW!J7d+#6T=bE9_9h#3vCRRsjXPO@mu0qORN_^ z!clZD=}fy5m3cxIS$gy>tShMsEt1dQvaX~krb%s8Z%}!E@_b2P_QWa!kTQz?4JpY{ z=*qG*sCRB7Sa=*d=%Gd9CoXu;KYGG*7D5WORa&ZXW_0ZjM}Xd=(tn>1>66SGoN&f9CbmJ3f)~bn0}c%qnJX5FN!(90?7HAJxIxFf>a0 z6_D3K$Y@l_e3<)HR4Zq)HzID&5==A_StH0S`E`%naZ`iO+_;QSe56x!0~MM0a2PJk zQ5fdvA`f9Pp*1WXXH6NL^4GBhGMh2Dg=hlypU@Dt?DbE)5;P2If;JTY8DAv73;8LzSrS*!;%FMh$r2lD z2g66qQ)p7z2jXF+%0oB!Oe!eh`@*2yOqk6GK6+l=Weln!LFpa937omHNWBnB3n})g!5) zSoXioD6=V3>SFuDuM$fZy^FoX$$gen2Fv-}fLtF_j12E7oBv)|m-23=?@!!AE5?eZq z)h;}FQDfUb+4S}D-hU>cll|^3XfdslHw^RtWT25E6wf>dJ*npdjWv6MIz=S@y{E4Y zef5B$8TxRnk>AG5bm;O;RPYg(nhw{ zduQXiU_nOujr=Vk)C(0(tXd0G=y6sBxOV_v?9y23{FRvUVOq4pU*OxCdT5ry57fRv zuJkl$Swa}k$(>o(KP}RME5~7omq6^1&0xBd;bovj5IHF=nYpMH1V2_&uf+Vzysj%^ zy%N=~&`h(D&cnPegR`Rx z>s9Wjc03xGkcWLk1bG7nt!Q&bv3Keqg)(@R>wd8QZ7@nE9hx~Yx8AzmZ$KRIdQS!45s}nr5&;KeP-)X3cUMNX_N-6d!%;?ag@u7!I)Eqcv11A zIukP=e^xsze$o(aV{tS`RIKXX>Pm%|GWth0@WXhs9kbY&8fpxjW|@Me8w!+g8Dta_ zW(cZRjRh&3zX%(z&Yug0V6^c&pontKyLy*nJ!QD|%gseG-s`s0dq!Z<{{nRJy50AC zsv@t3&-mpXW0gTsigtmx;ZS=5nn8U32rjJ*V?z;It!NH45;?GA^IlLQ7>3gbeQcWEws7-(lT{b33SI|4oF;LTTSX-Ai&9Hc6b@O635cI<<4?` zJ2x74Q6HIr4juw&aG%Eb$Jd3TRIERMhNJejz>Mr)3$&<`FELX@OVa8u-~0;(j6gfO z(oy{Ix~D=j;HCiRwDH|v@=l*IU!Nbv+Ly}|4Lc;D<^t|cP4O+tsN|A9Am865&H}wk zEm7#m*iu)t_Llgu5Fuw!HY)QdS3cM+fGI^Tn8Pi1QDsf{mEp>X=HQolUS0H-)Vl+w zfxYltfU<84>ez0;*I$74dm!j7x^Iq4Ja0#RxTTTs%MzQw)Z|Nz*Oq(!J^mNw*(^WIdg!S;K*b`{shS7v_pI({E(LL#nsqUKd)7G8{=rz@wBh#R6#KL7xU>a{ghen z{F6a)^J$wfyx$YGl^@^q96WqJscavVWk5Q$S?WU@E>3zAcP(TH`;S~&b?^31Vm{!L|X;BS$X6;IME z%M=SQdG(``Mt;s25@n2%HGXlYBL24UI9;$FU4+^PCAsghj1d;sRpf$d&Ax_w_*?TV zN!hV6Gig_eM9_zpY^RXHg0^BmlL53aH9S?}Pnxv+*`1^GnvRQ_38#pW!M&S3sJ4)p zFucgS@!B_|?RB`V5_IBv%jb)sJuA1Nm>I?D%N+btpMAP@EoSdo=l48p&ho?CWfw}Q9V|%H;Me@LkU|uS zS%_eF)SbIJd3Q5Ezgn-8mkL@>?|~>;=N--zCh{ti|86{S;&p`y4A%HypygbtCG~J&mfE^aVcRkiOT++UM*eA~?e_vr z%hlO9H$sCi^P%EQ;^1O+G|y#!b*dUc1fcAG&?m5H;-UPFHIV*SN?s11rf!*5ddPfb z<8kCYe1KnTAQ1=8oVepR43n?+z3U9EyU4c&PY1jHHUG^2dh|bm9M^!NnMCCaeHF_) z1Zp=FDs|yeUOlE2grR|xLg(MHx{Z>9g>#*Fo}<_37m982hQ`})uXIl#x9Ik0@4q>(=D0x`l3v$6h4#B;@VwH4Y(dm;(1+5J=p`@ zeb#tI+;p9%g`QU8hO{LfoNc@&S>06&~IvgJKFGsecSS1Ce_>jJ-G8m)t^0u zZwEZ6qD}lU0ao8Cihch)K--p(uod(ttOjrB{}GZB6c)ygB(GXvA;01gX4udY6)Z(= zr}H>P;LTE&h-{ZxIO~{&>3VGn=8yiD=zjj(A#8aCIW5+bi}NYpJ}erOlQXO(R)dch ziEno%T@dlsr|hI&XgENA1aQ7$a0?nCj=Q5_f2*fytBM99i?GbxKPVpZ_sXhvVXBOq zPj1PL?ZIz99y?v?Db70^DC2}a0fIL=TrESd`m!b&G+TyN`*J7!q8nmDX1U;;4z1?6 zNPA-Qv9+o}+q#}4OuCwm+=iSPyW#BJw_hgyDZ-oQ<>m>7linV+R>Tu6fV2Sl^&h|2 zFG{g}bM_JEU5=G;w*Fi9q3ISr!AAxycMqcdCm#xfvd&wB;$jy#Pks}OViuc#g$Ce! zsscaBW#i20m5q!LRpT^yD;qf)1&YJ#zeog4__|JdO}kvL_^SB+>xB>B7w7?rLctYz zj}1J{POV)s43IP6MVPSKLbGQ%!5ro z{srug=d8o4&{DB)78y>ERs$i-j)TW7PbTo4dlKQ2+@L9VE-WS|r@?yepAZ<3sT*sr zwj7$qYt5U2)>@}~h0P!NOLh=W_Szgge>Nl3%(v0pWIRz!aCBrhld5|tSW^`8S7Obe zY!yS}TC#E^1^E;oG}5#aG&kXqe)SkkAS^7`p?Objpe_~ci)rc?SA)N=QB0N}`+*sy z-V;4VTP6{iKkulz=ZlhYPm|e2e ziPH;+#fQSLAKFZdQh|>+C5UP}vUsrpZVKoXsvbkwE+P&@#|WaP)Q9`Gu4AiH9nTI0f1J|;JCJJrHy|Zzqa264*Cwhc zhGLFl^Q`gys2l&#DCO_U6YS#GF)z07*8KTI^oj@_T6wE6cuq+z1*Kw6=Jq#P2zW;o)1& zU3xmGOp-|Rk`}qn;oX-Wk}qI}@*}J`|L_p~Q8qWy`C~umHAKBhUB`6fW>E4JeXM%} ztGDXMg*|8S&&#;wQ}QU$ecNVX^X!Kkj!EBFEK&Qrv0~?j^21+a?-`W-VOkAilw^(d zb@7#KU@IieqjknlJ?PhSqxiO~!F@k?9783Z@g6+#u+#%K&oOXbgDs#;t1WG6P129G zUtAb`oF3)S8pNu6T0L}QvoLydI3edUTQ!cbdinJ+o=yKFh|MtDc zKPx5~TysD!AGlWQI^k4ITWDio6!u+u{2Pcf+4K!KSKJ0bp=m$_*VId9vJC=aW)KSQ9ijUNJZ%Px&98Bz-Xd`J@(MNYj~{h zhqeh##x6sXV*2YH_vsX251=8Dyf0uLDMQr)4(U4?V*A~K5C$PyX0} zD@B5yDW9-?R5WyJ@`7u{#ccvTcGsMPwXbw6R8jowBhYK~$2+o+19uj;o7WCp+3{jY zCaH!MvP75d$o1XLfY}nX(CK%pJrL+*W&d(ihdD-RSe z)qG6GF=8D{311Iw=}3i+U?A69Y3oR}&M|TCx9x)vPxs6K)$Qc5aG3)O zu)$XTfFkr}hCWzCaLV!hjZVRHTCy|h!d7Fq4*v9XQhN5{Zgb4IySz~uO{AN*XXsIEYWyHw2*pet6vIm2*BD0whbu~LvXvt8@E4^ zm#``7K@mt=3ZaOwUxigdmD@NcTSZ!;PtrJtV{Pb1LtIVhO{}JDOdouJVPIvdo1<80 zfIX{f#g`#uYrEvJe!99l7PcHyuxbHN@4%E5jj+*S^S=|HQ>qi4&|tvD8tdv#LZ0!p zwan0~0bvS?MtJjpu^68@i684I!?+euByV6CV8SP`EstVfsDPSsn&53wdBRwfkHOnw z=7g0SA44_r0S^iZ<-y^z>xZx{;Q8_{$uL;Kfq3&G)!0rdH=ggY{bl;I$Js9z6sYzh zU}OjFv^%As|GEzOKpey_`R_B&iahhts!qs!g)HKxH-ng0480cI_*LdXp^erMvV#@( z4#c_8%oG|L-hkKn<40By`rZMafQEue0%FWX_Ij4dCSuIy=^-bkfF3~d2Mwz)vwy{E zccX(fuvz^~mNTf@hW%)E)UQYJLt&IBs=+1%j38?M9Y$P zdXB1C6F6%)m$5zskn!NFfIprudQ5=R18*g+oYVe0mOYI3nW8n6^Yiz~WZsRd zzm7TFu7?$ChKQiK3p+FUT)MVP>*c4>JI$k%^I6`g?cOkxXq6odQx*;+Wa}fr_zoTi z!!ZOYl&=Xy;aM~>Yf34=R=!W`LL9lo@c-!g z@<1w|@8N699*Jx%SN5$$*+SP=NV-{yN?npY5k*3eB}o!O5<*CeP!cI!Dx?yHv|p8_ zQc2RLde1Y@ebDFod(R)wJTr6V%$YN1nR!;q3bl(TGZ(zN!mD#tF?T`Zmm0r6H><{O zsIlo|uTBoDJIVRI*aX2IGbo!cX0?hBTnoUcwZ)9eAQihc+e$f=I97pIp}VZLTow_~ z^F2KrVZ_LSa(e%@sd@k0{B4b*b+K*lSFjTRzM0PWx{9}6Qs7nt=y>T~B~%WFm_OuM z%3!72c$0H>?a1NpuwlZ|PZe6{r+;2I|M|+x_LT3=I*N8Houvb1jTP+z#OC-@7Z55>98J?1w^C(;oF;JhzRC(~s-B#2|69qtWbCp8F@ zL!G|^m(~S5J*I)=Nw{7K%Q6J{3o_s)VBSjP+{x;SH7k5}Qq&XH1V!j)9}i94dx&}G zClUf})-quDu|J6k>GZwx;Q_6_1`21AuI$~8Bo|2M1a^so#2L@LrXWt9YP^b_jXy|_ zASJOI{LY|*ra8ErVO0t7CxqhPvw4y+sB`o8<)vHhCrq}5Gu!YuNvi8e$Gpb<+izBe zaoBih{L~kC3(h^o>^amgtJXQQ9JTO1KYq$|y!kGgF#STFT|pvwx!#&l4dCW zz-tt%*2eJ#Sfwdeg~ajoSc@sCf^>2=+9DFm%}h{=4k?|m^`|_q8{ef5ca)Pt!0$Zs zwoqGMvQGEcXR*gKgWi&Jo^HNmvKe3GZZea~LcEY!7Dw;mf(7RN+sk)|JvaThe$Kh% z6_&*8S>pGTVB%imSD>IHcUW!%(7l{c%6KZop~AED%kErJEma=luW#>B8Qd0PsIJxp zw4#Rn?^WAr;Q9O+NQqD_erVHhzqOu0duhmM8pwI%NAP+s`i%J=?{lERqVXs03huZ^ z6&7?d8X&AIp;x|Z>4gUmPk&ZcU#WWz0xje4&@=lDBMTDgsg0aXcaBaFr?hjLEZPVX z`UhyvQh7ncsq^s=EjKOhiTZrU4pT^-%p6k>@}f=%e4)-P6CtFJti-b3UCaKVf-;Ob z_LGGzS(czbCe^Uk^;pEqFaSGjn!%xOQWPYAdzsqs9?v@u-p!EJ6P5f8LIJJ^705Zt zlFaxnNh&^{Fu2zCbkITULJsga;7d*v{rea2X$m(jGjDO+oarrX&lF{K$OPgkLugnE z>ORhYF50}K3nKCFbv(yi9PbJ-K8t;^yZ`Ol`-wX{Y=*I#(cTZu4D7@?O)#j0Lj*4| zhi-bVoE#F>M-^oP&mIDH-uFz_kY~)N9hOm{WDa%@7xt6vP8PzMn6jd?kifkljuoR@ zGem79oYr-o%AnfJcDk!vQ`##jZ9YsuJ%+0zyBz{_0 z9iT1#yhUZb_N-2Z!^thrNye9pfiQ$(H9-9wJpf7RHD04ecg{I?XuwR3-kx(VdB6;2 zaDOgHWq1snCUpg&@RL0U;!mQ{TCR{p0dwTdjC^|Qa&d$CW7Y}?Q%L-z<;Yo$CWNFF zZjB0!er~*g2Ov*g;K1sEmV}JTOuF(>ZLP@_Yt-kg#ofHm{jNl!7FP4(mc~{OfjzR^ zb0<4wF89D^RbGu+lcL2fE3ZwZ6s1#7sMkGL4S3d*nIhE!;jmJ+B zagkj^B(pztM^qu-i}ILNOS0jb?GX*(UpTh>&iXCNMjnb^Y^}h^VBet!IQ)I{WRS zdF3p}?osHk?Ec*3)EY~9l|0%E>15Mke>afysZY-O8K6J*6J)#9d`Fh;aQg{`qSG%u zzfyXFh)R)PINb1Qeeh=uE$B+P{CMK5B^@g~e)PY6IepY@&ox^bn>Nkbam3;q@OoW! zQUNCWCC-M8<6MVz*roZ0`&q%@8%5Vep4{`rJL2zcCPnjvcZ{f_d<~>=qWk1)9^s#l zy-AA;oC}k~olG>{-MO=bcU*D*k!@N}*qwb)XA{L{WkZX^dk$H&b1HAf51{up*?54c$pZ1o&qVf51J+7Qzi zIb`HyuGvWzBBI!jzWz$(cWtV{X2wo7n0s;HYfs$osW9uRaJtRR(P;Q ziTJ`K&2I)da~1xuU}&ccsrm37W1h*9a^syMG69oCa1l`Y z$@(rT#`Xl>IGc)2+s&M~h}RaewZVY7ycVe$Wv3JLR>8L(G;Ukje+&Rqae&d~Tpr&t z>ice(Vr~&g>9kTcG`C=I++1UAXrGz6bc$Vc+YzLZpzXIstfVOCBS?dS0t<$2-tqjiVJ^Fea~Z9q$de?PUj2jXlK_^ z#P7qjoGU3lGx2vvRMURCoNP|ynS`v0@ZWdfz=PSU8f;z0!-F}hx)d+t>DUiF8|@>E zok01&j&kqDRPvQ5Y6KEa~6%g6+_AzJEOh8`!*Ug9ba5vf?8x1WMnEx3bqDGK78Ar?S$>oJ(GV#M3KSY zUqc`k>uU|m2m<HrWxhpucejPbHrN zQ#=WDGYJmvn*tlmrUwf1+!WB|5~*19!wr@v?f7TF{tM68mJj8*Pp+=F0gYUk20i7k zCtMxEcMVV0R6g-sxYetnw#+heg`#|K!uncWTSMaGH3)|$uwi$7!P~m^V%axY`mXqk zfBy^A4q@X%qmMb`B(62t%mLn=a^>gUZZ+dNE=3?$Q)rl^k%4J91;f@?8AO9U=OsE; zf!o0jEz^p0*5|wFx#NH7{%;9$qjTZ4nEx$r+5YwEv)4Tz^c`f5zLc#iH`Y7wPJ@(R z&fK)(c^21BpIx|UPES@cV^J>hv)GrYp4Yx9Wh#i=p}(M1lR%T?lAX1S;!Cw%du_i` z^edX}+Dk{3fN|zCSg=se8B1+lIUuzp%9?WbUy)oi&`frU85m0<>{K;U218&crJo3@m`h7Fwa`dZ!9tDNVg@-jt-6ZJy;j7{R zRO?wVi@%*x`b_XF#omKp+#^5|+yUURv=VH+#BcuiTbiz3u@u~4kGb+>2jp4Kz%fsmIDs}a8f-aC{uJ=A zQY3P9F|p#_*3*>hVh*Ty^YbdNj4^43&&p$0M>@-R%lu3PO*Q=snCAjol^9bu;?&5 zLKW!E(9;ySKhJnM{>bm{FXEO@HwyDFmQ}015aO!GMSn@#IdH=};|k1mjXl{SX_~*u zEW)LcMN?Cj`VpQ!*fWSOxhobi-d4id>D100Hpbzcvr(7wYhe4Yl`gFZjXw`Uo1F(q ztC%2geXGFH**E6`5xyxv0fpcm<-wNXOXT1&8|MG>j zmVCN^o7vUCguO2xbs2AY^O8a@op-%vXZZl}rXu#!Y0blLoKG%2d;%VeDrZg?5&x(= zZId$Hfv3@QLEetIV2+n&t2`W)76pE?T;<^n>64y4j??%~4f0=e+@SvB{ogtHU8Te!Uvc6LwF3PCYoi`dBTCH!a zyWcG4po4S(lDZ&rrtoqyF3jiBs+Bb?Y9a#p9%9kPRNo0g4)^5m-v8uSyJ1nu zEOc;42d{wsP^~6xC)Ul`Gn3on90Nxza67P5Y zG1;89e#-OX`MdI)FL_2t%pbCu%-8${tR)+YO_f) z_*p80D#lb;Lz~e+q`$o(z9OLBDnyJ|{9HQ}3Ku0v*d@{;y#AEXgu~|MWz*hu1-a$1gOaY-t-@s{y#;MND$l0J+A;?q0(-ZHBvdi z4IEzznC5iu_0gzhjk*;{q1%&B?IYt1&~S8SunTFMg9_#TZhKBdClYfqwTdj7Yg=t{ z>AC9DE2hMN;NCF*a)c3O%r=b_A$2uJZ4s4xNYl0FfR2;EFu8qy$N=2zVS*hj?C2kv zDUz2w+N4(#T4StKK0 zd7=?`6JN~pxt`u=FW!9L`IybJ{c#}!-7)GzOs|U)4dE+!0;4vqS>cS9(MAiQXr20K@qu3$w|%$Qp9m(wnsCIam5)Qm&^(N8=`Di-Lb~ z1t9)45*Ox{!jkCaYJ{C$g5U;j8&yN^WZXzmb#V z2c`a2|6Th`R%7dPMf33JGzt1NjVYNj@V8T-LQO$v7O-ja0S4`_HT`HTRGJ7T$!zia z@NMr7kjVdl>@9J6m*#`{vX&C`z9kP7dgEjhjo=1J{z%i2OcJ{l|FascPn^u>r_QCY zhD~96IeZ38uzfRlk^h2LUo>1KDBJ_3V1%GRhT$C3p7Ex z$s3W-KQJT2B%6Xi<>GV_^~4(34>cLupkR~0D*3spPYLC3#=0hXmAt%n|5}hJ{DBbw zyUYoGyz;c*ForND4@`ySWXM8=!y9YIHbEP!I*3Zt$bpcHwN7t6A4+X475TCP*$+a) z&ee3kUQD|?_gEKRc#8YsqSl*=D$U$ZI;|GUT7H{8Q-zOi+KOAm*7g^Q55&NrfY}i?)f=mkL7!WE-EGV7b6`eCQSnjq3wtb=JD$;OG6Qo z0(FAs&(|XDe)MbG!{%#>2B6AaF~%q*53-_5GlEupVmm%%xi4KQU$yv@<#sgE zd~0EvwPq~`+ ztQ<0fZ6x>h-b`syLoHz3-ENcO2QMsMq7+oIc)ylXl~Pd3k||XcH|jFBUH`V%;#%tc zY^kJE^Xh=%e4&@hv7{ha!|OO(@Bh@TWX-g02WwEFuZMlduVbD+wNYmbC&IbEQ71Kw z_K|1slxunin-_M{_&my(xmf!4cF*KI@Y39(5SpkpHl7;@aXr!s?dMArUN_n3`(Yyv z;hiVM`JJ9j6X|=QV(N5;_k7ZQJR#iw2>2fcv4&vCeFxLnIrq4`g`C`3F@-hN+8MWf7_Ry{zLSjOXPhT!7OS{q zn4EPOq_df@%*leL|HAyXO71_JzxOwmRwX*DbAn2#7jQxCU-+_p=v)sf>clS+AYeAU z13T=*KQ!#`CX&Nm&qy_bHjL6Pi?T?c!A6IK+1U72kM6id;b-rExnLvF-VVxSDN}SV z+>vX{6BE>NTrJP}NlGwuQ?)PgRCs$*Qq4iqbu(ja$}fe(^%*PKtyw&8>Ep_g>e96$ z2Iu2P_Z%R0`ErqKLNx}OpVJaaHmWp0gAuSKJf^TwV%^DGHgof_qRxzkIsU4gcbf~3 zdY#$qpZ*pYx9;9eGn3@ovx0D(&^$Q7WA|RraW(wsWE0>xBkg9Llbo*ejKZ7OoD^D7 z#rLH$iQAJm!4i~NQwi9a8%h7gjMJIqAm4#_JBoIRfA%JbAc5F*)I6t}jOxra`!n?m zW(n_gyHAyE%31v5v6LRZ;-H+3+Pxc=C;Ap!U@`P%HPEcb+dN*_o6*tIL2QdQc2f~K zQ20G!pBpxy#*zOI^KQy)W>yR>zNeLd&=`L#LtC6g438|`#UzFX9f zpAUsY{_u6uXc=(BgiQ_T#XkXk-0b(zW(PQklhY9r^TjWpXs5Ch2TgMpPSbjN*Gc?xjt6rANR7CxLc2Qr(zd6ai9whcZ+Xb!+dq7>4tg42(BSZW_es9^aV%7@AgU@cg=mny>B^AHL=;`eQvj zGl03s-QRS8B}Kn#j2NdL=bD*$J+NqdSkXPz51po>N9qu*jS^~*+B964Nv?Tmiv!~G zT(4|PFBS&Za6{&HKB=IXlv;JF{)cxe#}_WlZ{)ZVqr3WmOViUk0)DFxNZ)z-e$I>) z=0NOPglOW{8f%wsVIz~*wDmDQ4FR)j!=gY+2!{3N=W~iMo?ZK~Z&mfGxm#bjKIfGL z^&VSh^WcE_o-`}`Ur?Z=TUB5hMw-EaK*q-j6~gIYSWN@*m+~nxWU!CSW?Ly zeV_M6JAR9~A%w6JLo7mmlE8`SGkb3>SK5(Uqd~1 zX#A4dZB@;TSF*Dee;)69WGH8uL3%=qL6|@KCU^n>EBpP{oQ3m(1K%&C<3y&ljC9@1 zbb)8{mOpCglh~7fiRZTGLjE_B)aOq^ZzVdr#ohSznZTboV&9^T;~$+@kc5!mt?#mh zs7PZJT|NoAxgPuQfRQ}dXSepraAT>ROaF6raWkg5#90R7?`OLJL!`IL)pmgegAxO$VYe<|oC ze*^cEmJ0u`#$Je%;YSX}#o#7I=?{;Epvzi-U9s(?!mK|H=Rhy9O`y(F`uK7Nfh8Bn z;poHSZrD|jK>J(^h3!pTy@f#2e{qKkrDKgIyOzp5h&p14*FuXYTS08&p|>~V&pIG@ zccgVa`uXIJt1tFH;KO1cTz!$9#*f8KNLvAnN>ry46k8PR>< z?6Eyofi+j=jfkM42gRhtqU?VUtSIi5%9gb!{wZ(aOQp1OEMh*?pW|lBmCAhRtXlwq zyst7X@~!kK`n8*A zj50Yr8N%;ljKU~`;#^_RZ_J7tR^ZACZJrfJ()-=IJ1TdjYrNORKHWE{ka5!p0#4d2 zQ&&}#-{?J~n$RDz&6ISxX5!W~y_3^*8Lx#epPn7x`&PjB`1fsfrip%!G`wSL4@i~` z^(;$IIXJPmK5v-b_6_F7`cwTJPl$AvLZ^^}D^BH`C-bGxzfoG_Mp9SYdDWD-Z=_(w z9Sc|Rs5gGXbUtTqXp8++SMMrc3CkR#R;VO_VOR}HmAM_|wfK*kw7oe`8;=aXfdAhe z0d2om{Za4$R;@K-eO>pu(*qb}4Q)LL_uLovZFv0R9UzLIoDLJnggNn0T@bke6@^ff zlyr7V-|;&#?{rI*MEQOl{}5c2&+Viy^X3?)S~RXzRu%qCv#{7;peo%s@wJ;ky(7nh zmh@lSqWzHFBatlLfqf7oo%<+@^byQQm8hOoD8b+0z-~eF`tW)Cxj6%&V_CN(O&@m% zZeI?bZ)X`8k)F(pteLt$)-yvs+3rnwL`dF`{Ym5b_?7lEnudtXs~qr|&L$d}pEehR zOxbXGpZ)#<{(D@H4!CDF2y}5>*SuS5@Qz~-tZg4!csr%ey*(hhYhgXOSME<~$LQcV z5LB#&)nNY1^(cdRRCyQbti_-3`1M}2*wObcOZ{c>2)tJXkP>zM&aWwJw9%u{vgea; zzVC4t+Qom5k{7=Njo+J|^+*vxsH&QC*uL=@GLmZ4w8?{I>fbS6ih!!i~%_FQ| z*FoabpK-4x!>9cn9|`yq7E6gc%+G63pl8kPD>5BY8$@5_$1<`XFq1W+b?LVni_NMk z4hQim9&dkP{AkrZHRHQ8e@>5gTKjJHOmYDOZHBW=kKUPa{BP%P!r37*9}N&=7iPTxU2brlZP^>Uxg#%)-@16K)i9|xd&i&?mlQp*y_W$ zaFW_*FmA*h)e-8Q!D!;X?2Aw-9_uRcKU)*PTm^7H4aBq{H5Fa zVjkAN28mc@ZmoY+8tTL~@9KT;--F9b8uY6l;H+`1>veETEOF;(?#*-)IOVago1ANs z9;AbLzFls?>ENI$INg~?E^OFwqW0vP`7)CzKUujK+yF z6~8;3QM!Y7>J|e12SWjKy*@rhGupmf4_w#D4|KJY|4<+^-v`bvTb;MtK|GgsO_Z2pKDW|@!4sKlG{*S)U<9+}eT}0dm0&j)}wS*ma z!81y=ake}{t z4XL0sJ^$v@hTKx4UCKv0P-MaO(-2+O!&2zP+yKml^S3r`;wOY}feLyA`GYGFB^Ch) zlRN}o&wuAdi0bu&PsL#MnC?L`476-gJX$?x%t2$YW$W3^&d2_lD?~x`11LH($dwGl z&|JO;`(iS)vSwzZ)l6rOCK=5+9Q5qem9m<1IO%6n+mC+gxZl_zE+&IihBjMhsfhRb z<`7|dpr`t4tZ9hk%!~_EPfPk%)NUAJz#n~7{s{k?Xu_wVBY(eTjFOlNqZclppQ0UP zIXk88xdFnhBLWda9)DR+rY5ZJ*~jakDpF0ax^2kl+&-X#+F+j#QwoaBWM!uviY_R& z(2*55&0(Yj^Fl(4gE6Q`YIqp_eo)Qs4X8Xh4-%_QAZbRnVp<}(_MuJlfw}&^@3lG@ zIOUhjwD+67oxw*77czRJRQiFPA7X0zx1thsU|}r7i-~EYFpDJge}%JQ5yf)BB(>XD z-hp{ne9n4$ehNznv*P10hrQ4AK8j{>`pMIHdM>@m`Y$)}bSi89yY;I|<$^ixytCba z<`{ds8^UUSzuR}lj>^@*xIgc_b9r+gqPZdZZnXzY=2JraQU0|T17tL=lWhHy>ZLknSRlzh0^p{4jAA9(+f!6mE zK&IU!qGAB1bPAkSa~McN`K(($Ac^#!_j5?&WTRcnbWHs2zMu1+?6JhKlSep{>mW!P z9T1MoKP&$@?}Mz!Xh4%K|LRxO{@wu+90x6#Pf$ZEH-`_W=Yh(2Zq8Wx;Zh{H*OINH zy5C_M`kG?f0gd?$e7p(*#+|mKp@Gm52W~VFg-^xl@l9R5&e~WMf(DPp|q#b~qvaaN6 zG}s~YVz^$)%tvR^wogds_>6lZ=9UXNGGUnqp!JKe0*hELkfYUeRG2jng-J@!uHtAA3FkV1 zF?BXhkrV{=42Bo~^}_GqQX*O27h+-CA?vBGm_}Gc)|mjy#bF{Lv=nP9sFZagJSXHY00K zA^&gdN3mimP#B&-64!o;xW?XuR6M~)HwpO*2Do!XjMahAtInRw&fy2q#9?poM@ZiU zzxxF~k#dj%Q~SR_qZPaV+>6RDw7<@H@k1hoB0#Y?8tueK6QINy#BSuH!Ptx20Y%zj zikJFdQe+8L%z}MNGu14%sxaKZyKs&~t?7#!E*GXRZUq|cwaa6!=(qg(XBx0gmUoU> zcu73E{)7HDiaP_eBC2Xqjj>+we+EtVhEiG|xRT$sf_xx^W6R=?xw6bX5CQTpRHGN$oWWxf)ePfkRr?pRDqRN*!X+ zSn(`bVB-g&tz)yJHLSK>ZHox)>mG}8{`#<2L*iD)!{44# z2#=yLalOz;Jb#bj?rp$q9(>&Rm+SYdO-n18lcSa`7m=~K5yoibCm!-QNi6pp=v0tdfxpoXJev>x zR}3hny0y%zRoABPY5RHU4b>;jBx8A!t;|ze;Uj;) zAwg6ruxj<*ANYL*{SQskj-TZ_uWD8yV+D)VOCRUey1{aOyWfg+&#(U_wg-;HhjyU( zvtDaNB=Za5{%#+mzE6K}^3?n8%I)*$&*!YSMpnT3wWTL~>|+)%KK>R~i6jgK&00Js zIiV$5eNG$xBoBoaH(!XKerG=|PC<&F-t}LLq%9~U+%W8%uk0eJz=ffC8qoHR^<(W# zsL-E(iZ`Ck&spDh`sl>-3TWaBLmaTIEThWvknVEun3E5w)~WW5DjBVz zn?7<{Z8 zSi`^Vz+pNcn~Z30r_dgV{{Tr%a5<_qDW^#Y}v~i5Pq5tahXK#=JArqn z{(O+He>)>GhW5Dio>cK zI3tTPJ?iWqCz$4b)Cnlzw=6r1hN_tL zsl`KSnWI6|<$Q`}B)Y>UkA(Eii|uQN6r;x@|5!!aJrr%$muo!vamj9hSA9>E&0a2l z%bA~ISbt0CMT(Sbi)82(i+}~*Q<~F1f;S)`#r-|Sic_@US?F00Jl*)|st3Cc)6zn2 z0$XqjmC!s1_eM7TC)J8SLFNnaAAOLpfC}D3Yh3}#Z``^+f8@t|xQ<0TGe@>*_jP*K zgO(*VQ6E&y*9gRo%8Q-lyU&7O6uRV(JYP3(`tIo7pHhn8jQ`r8=5M%&31h}remjXz z{?Mzn%ma)C)S*}FnFpu~sR*mnaO0~+EO-?Su@n!qBCSe{d5xk=qhF_cZ2xMte&BYG+tCH=wauCt}`E-$Uc z`0w8SEOux<$Z1ob>OV**v%Y}63@h8`_R@jAj#9m^;m?tjO_XHuW_NA-{eNDrYuDd7 z{{^UV3-gUY0dOrkk2ZzQ!p9vSr)&k&cz6#-21t!9n&)VH=WInm^|r?~7nLlr4hIhG zg!;?1dx(cPSid2qeU|VUxjIRUcK8h#PxF_W{BQ5;mGjQVd)5!GRa*$FQ)Z1k8AT@1 z*=H?koEKK4#%s4|a~Fos*iUK|Hk18_!=yCX!IywL^Ef<)E&KMtA+5_oV4Mh8z7(NO zdv=CQ7~YZSl(BNdcV43~dZEG{t?QcG?#$wpzW6}0sm@z+%V%Af%_IJVmjUPGMO>a3 z^z#1pX=Z1q>vK9u3{>TI5oG**P)U2{wX^1GM5QRNcTdWuSFc5ROWRWeUQKvFsI>_o zP~f2r;k3%CGtI<=lZEUQ>p3q3d(RKcKYY_?zZecRj?bcZhB9Ssg?%qw4xAxIUNaRn zBP1Pm#Sc7FKxx5=3(T%R*Qi$=^D2lB4P!4R;&VV~!IsRg@1-@h<-WD&<_C6Z!6b-g zUcPy#-6r=!L+@5>sITTrQrEIPYy^n+4x7&5*G($Ym$lU7 zHxjoDlFM5fE8BL-^#aBFc_4^Bj;0ky?$*+A}W^*%bA+BUT`oeA7b#!tQ4ei_3>FHd>W(CwZ!);vs zI*4J;`73$GgL4neeBImzJ_9=y_H-=UxsB-g_*YAva?aT;eAMikd#heBef&_Yp`OZU zZ%}!P9!`hEZ633a}p=y7$UuiGkX)$3gz9u+W0->dv7UsC!7^v}x#j-?b;>D$S+ z&ooF>8gI4U3IjhB|FhVOBBE`05+5)DjG!avgf;-HLdJsJolV1@!JY^8Rv4*2PWh$Z zEB!gdof%%vI+M+6y!h+Rmv0XQvTNCG;d{qkcWJ@3q^VFFS`PQd7<7PQ&KDCc*syKP z<*4(eqqx7$&TBpfyQ%*|K5Hb_d#2ifw8(AnO}1m5E@C+{l>XD5E@nA4|HWnJskHK6 zFF+K5T{C`fHF@_wG$g;;7tbCV(&SAuh+s;5YZBFJ*O zrEJB;CCFNP%P575XG_Vx2>cy_&T8oS|AdOCe&R3N)l3Ie2VQqwz%L)uO>80LFJ^#o z_8a3K;47)0Si#u2zfqfyV836wfAv4$U`g=VV4h$WPTL zMdw^m(a>SnBku7^GSvwtZ0!&6Y|--h{kE%r1Y6c6$gjnJ(l&#R+!%IT0U}*U4_mXM zOC$t!d4$eFD(pE;=#COR?({Ecsci&BIDWaT)dW>U{e=LAm3-Y`2*S?411^gZK9}TG zdZf$sknPM)LE>jiBqipp_bU3Z?#bWY`Jm|B&C4)1r*4=lUMrQhBT*((zLIjNl_x7wzHK>0XJWWk7 zkrXGeLQ9>sA3f6$wF!&-+x3{>&aVTFlXd^;!gN#UBkKXm(=-!0LPJkA{$PRZsVfLo z?aA^)jjTAqZ!p|*SK+VSF*izZP-`;XwLAn0W0e04|} z=-~iqBxpT^9=l{@%C^8=gAET50<#Bnn^~4cw$WC;ywj#tmqq5mrc<@QwFLxTN16x4 zQW#Ol%8ccG8#P5UHBgIu{? zL5rA2nn}8oJ;IsBT!>l!09B-f#GJlE9#&e9W~#O$6~wAf$b0+1JALFk#C_$qui6Xr!bAcL-=J zH*-kMFYw_|5b$0!zaWSc(rAB}3J~VV*^z90pGRnwfaj-A{FzTYf|x{~Ut}fJ2iyR& zWo7-CM@N?@u)!YHR;NX_Qnmb`m)?g40&YS2zrfeK9Tghl-=0U1KadJ~6O8|A?Z2=? z7HG5!te~LTu$y_hsKa@aga6D*tv>HjkQ?(|e@osAUU>oiijo!1g(Wb9Li5&f3?n)f zb7M(*k6J*OFeDkBtVo4z=RmXdjSWy`$QL=IrZU^F20EJ1Q`@D)*>h(5zUJErB9x{m ziEKeLg`Ha;TAo& zEB!H6D7r360TmuMK^FtDeY|q=2DujvVGLPd60BJG@Eq2;h9TgjWqxLMw%GQ@7wWtM zpL4g``1m-f?;F=H*RgFg}r%H1V#-@yhfQ!-^uD*Uk9 zLk$TjWzjxl#&~r?CI5(imWF&v2mb}1QVnymO?$V->w4`JG79W%F^e#ee z86kX7R)cU}>(foE!m3A-J#_D;rI?QttV;PhY?hUjhEVPjcB0*>-<%e4(o40m0_YpM=EVE?p`9@ryef3Owoe!>x-O8vGfn zTbK7#Ub@h;Whi2d{>MsY{Z7rj&A{WVLJ&mTtKhEamJ{m2f52i_!mbSf6Z=RIOQS$x zM;Bx^-g1Q&FBK!~R6j{=;`npc=HF;MU-##oBk#kJ+7fMR8am>azg`F2()O%&{W|ie z5qNdyX%2BhVkeM5hP7NKq2F|oxcK+U@!t_?H$dr}b{EHe&^@rj@pp=7i(R2GVsIr% z0G{+br~QtL^bAnkI7*mQ0sAwSmsANUp%(J3CX(5(f$9@O>Q6i6R3i}F(^8Q*D{AGg zxi^}he4ZpcaV87qMzgCioz3?^7Ul-vYLc2!=(ET99Qd>N6je&rVmT#mH&qHtAJT>^ z4zQ#E#t(f4fvh(tyb50O|Ai>UijcxrEkAaj5g1&7u&h4jb~7v=P4@^QtnJ}G_QD9S z9o11~lVV77^^0#pnXKJpAyKDi3oSevcAawsE~Gi8gwggVYo>*|!*>b!V)0=p+Ft*}F9B@p0i_yZR5jxd z4NL;&;5V6JHY_U(r%^aE-K3*w)L9%#x1>2tsk0r&Cp^z2E<`1ns!!$4unIO5I(%b3 zEbco(B8rUCAI0_hAhf|Uney|evV`9m5<=DyXbWm4^|L|mUmb_$s@wP{-ql6~XhR88 z`@_#Qu4}N1nEs-_k7)R=7;N4*f#k+&-K_oUIadK~^>yv}cNWjn#32Nn- zPL{w~g!#?k>A$4q1*I2qe7wTQN-soK!PH8;*zYsL_uHaFoR|Gluj?1`UPvBXwEI9Xz!Q2w)AqV#OCB&33XKcTN?vt7Slxf2cEnlPTCY7) z+uxl!Vm!?;;L;uetnYTFfaebTOB*;Fha^vOHPEG-H6oV_H>5~^XfVF_`;o>rHgQaZ z|HI~5lo{lob^bEgV-A1&k%0DJ#Rg7$>^#DbDu={VtrpKM5xkGTWS#yv|E9vW8!o8D zRe@{w&fZs|tzJF6z{fjcwa)Y@7CR?>iuJ>kj&5C3IAvlmVc5nnpUEWU#XmJA!e86i zU;ajN7AZ%Fv$wK;26nrJ)oK*SUumRCdg@ z`<2VGvr;~Ad?^ozZ1}p*H}~M5;GlPrzN@7W&- z;l2^5Isp3_t9whwiBDo-PI}-Y`W>1MKVED9S39(`oWA!S;oQkyQEIf?23EyN0~3r_ zd5h++qR=X8yhW+2Aw^hk?y1!Z>wP>MOv7Ew(*`yP%$*9ct6t>Gv?-r9gt~l#Lu`zH z6BhmpQmtgYITBgV67~NG6i!D5&a6sB%Jnbu|CuxUfqT9QXji>5^8xsqVT}UHGXrLj zlEZq|#9;yH<-N)%?Y!j zUL*W&RNA$fdfv)wmadBS$oQwDxRewL-(Xrf=P}6xQVFFUW_b$=r1(yE#OV}Eb-iF(3TY7Cz^os7SU#N>vX$Oy1G zyWcDhV`nB3#Y$4rj8e(@9msuQR;yqZ>G+b+nOe{lj+XP#Ld!rCwj378FZ`f4+L__- z?WEb!l6+L7x54KK9c{&UQtbmFRp3vvXZM0_*`*umEx0&W><6Ko6TSa?@&O?cf@zd-oX?(Vp{E20$(ELsu!48%CZIFtxHf*4L|6*V{+>1x+%*NwFtjmc}Smz z9}>X~yO9IShpQH-&9EB&ag7kuDLh$fg-EPoSD@Sz7K5F#E z&7SfY;FLPnMhl=P?1J=>dh~1Lv)8W}D7rJX%U8>DwA=-~M38?Po?{3XFaFz~YMA2E zvBmMs#y<@u28`EXQArxkJY~E(Zz`YKR7C;umD5O%toq+ThN|7+up3eE@ADHn{LN<< zr7nk+p{QE!}95D$DImTgwu$~bz|9_;u za(2I-G#*>7?nqSrDueC$ca7Wg++c1FxWKg^^A#Y}Gog}rJ=PjZ%#`GvcZ!)BhE?Ad z9rfzY<56K+-Px)RE$&E^?+UZwffkIH&$hPzDuoFvO?*j_t3TORae5JlT++9Z3N>3U z?8y}OYGenDBVfr7pNk4TF84BVv+bd+`^GwFes@{Wm6rZ$X3aSxNx`+NmTfbST8aNh zAz#MUa;zmjLps83uSo07r*7~;hzSt&aA4knC3nsJ1Fy_)wta?*Iw?^6^W(60a+Z#` z9ra+IS0?x0q_WKxBAc$H4!k?#^*VQO_VNm(KNED|WN(Fk_Bm^+!qZBBnKEmtN_>bP z=b3&wYNUWIoR$8MM9!^uU)#kDr38vcqv(hAhM>{n*NbQ<#d6CfV6CaReueh9)eIDt%yOHBY>w&@>%ZT#`h`zE~=n)%^_lSi3qjaCXJk+3v;f@ z{>T(&{g(w|bwu&u@#n`Zt~yja-oa%~-RQHDYnsdt zP!d&mnh9g9of3E{LK}U~$69lw2!-{YpSFrK1rg~P9XSw;tZR}cd=x3}PhL~f;|7Jf z!vY|+_4xmR0R$KKy1o13bz#WkoX8>AEc5LAY|(RX6r?pfW&z~Ivh-zAERvHCAooWQ zq+LGoHuuqjnB?ln#y^ByQ|^K>#yGoMm?5Ji@cwh8ciN#6Y%5r*gQK@bZG}E%i(`dG z?;?Z$pc2_R1xiK4n^(;8hXL(Z4-SCuMIfFK0KA1=hR!TupHBj|c6Q&^I?3P@JEI<& zyho)i%VgfJfbm-OFGTqgA!6Le-(rP{*WJ-k@SqLSy{nqlw+mg)`5 z4p)WHKjfTLs6Vn}0VWOey1y6jpht>v}*?pD99GPNX(X?B|usLa^6&>1uS+=cN|rcj6&re>gyy?D&(i_*J4rio|~^Y39qL4$Ifp07oa()SCAt z#HTlUlkOm4DSfHu`o0$)zlhNPPurgN%h-9=?TwSF!Ymn_^)TksbZppFHS#bP+MVA8 z@!4g?$=}B~Wa8dTE4%D_TPLB2x3)X@cGvbF!Eo1&BGKVs_VVK$Qm8<-)PSe`85!l; zdz?^Dfr;iQeP)=^w@&ij_(#D{&rTIbPBCMsuE&2;m2^nLORetY(Y|A_j?|jDM&g=$ znX6tCPfMo$WATG+T{X!<_g6ejKBdeFL-p=BnxeWjLwMtQV5RUmVIAD;XVB%29xP~~ z@Pw>CtJQj^l_w=77?NoxmcqCz8og+WN@!?0m< zv=3~6Ffa+&kERDh928x94o>qb?&+EC9GsRleBdcD!*8`+a6?^GcX6rGiiH<_Z{LJ? zXnGqpW$rGEn6X{=24ueZf(8FCV43&^;vWz{C37>p=MBVa1k921gv8-$>hWnpPk!4* zyPJZm`ECUr0XT95@;Ohr^Z`cMeEjBe(U?$SR8JhFXW47nPfK%Con=qA7fN^U%q2`N za7@#e*X5ZW8#P~U_S~xxpb!G@CL1%}TtpnSj~BxdG~0Qy2F_@uJH9S`Cbt1;0^^Dx znw#Nc1wfYIc=sQz}ipdF|O02lC`g+&Wx6sq7=GU=Y&qe_@>B@uMvhIDi#iMl)x7UnXWwEu!^J6 zx(RJzc|wc+BjjjAzmT(onTkRo=c1(|zPJGlz1|YTX|sXz+}X}pZkuqn&cZ(rkAnlz z7ya<|wyW(|uKhQ~8}js@QCffl{g<;cXJZHNGc=YvI*nBaV6r@3mWwthExCDU(W-Gj zLo>qK*sN2ixz=z>-!py-)=~ceRptrg2g(S~bsPT|rpWw)4`Jsdy>H41zY6q;$Zb+U zY}34P7yB=m2YU_tO>S(r-0|W4wG$Sp^*4IYo5m}zBeAr;K$G~=owFO)%N?vqH_bg> zc>T!n!lD`L{N@?``WQXByy%{Jc5~4QJ<~^WBR~DqZxt7OpC4WG3D}5`t69?;I#*x( z?)6V=-aCJFt5kXNAF2Q^ni%IS~<;%O^ov9 zaKn>LQ=GugOK4hYbMQMIZ3-wn6S%Yf00FuMERC6B7ezX@pi)j3xJmF|u)*KAU|i@D z+Df=V0SsEY=YPI>1Yyz=suyNQe%x*P&JAeQDN{HY9CPPNo{!f#UA%CI?$!8P)(`W3 zyYkPt_w?7PO)D>Z?h0JY?t&Vj*sD8x#bHOSeydNR;l{utm!Hi*bNp;p5RZ^~)1lKO zJFLaLN!u3;y$*ByRX82|SXMvmXYvI+IE36M*>$F9s2(?yHpm_f0T)~GtR!j6(k}}m z_vlv;67j=-1`%h?HF9~8u6F(lzt9&aukc@8x1}w&wvNl}<^Ln>%j2ndzsJWeRF)Ky zuI!RTWbcxl3dvq%DN02sR2uu5r4ZV#vXc~*O1gv;3ZasUQc*}siI(4)nLAPM@8|V9 zf6SR@JI{H}bIy6r^W2%akFTCWHpSBt&m7`%+fLxRmueTEzuQv)0#I7zP@-pwpk^HN?&7BW%+O9pZV6h(*2*qdgK~ruddqaB`wwL*EZbXV!w#T|l%!$0b zrk!)M%pBH)J?`IaK%SQJ#$Px6?t;a_vXyn-t95O4`?L;UT*~Wa_kAgh3V>rD#8Wtw zyF0=I1G|HBHNqJ=Xl@qCczN)Wcb&rT3-pW|lk(e&=+$|tZas(V97TI8c07I~6NI<8 zp+ba;)!xKfQxgX){0-PUq9j;ab}$`P;eT>)9bay;$^x$=5&XHQ_C0U+yl_+Cf2gRAkxr8I zUK)SkyMI%S>od^gE1suyLrGT$MrkLzTCU={dN`G!<(meFNwc5{`*U;vu|QCwCkty4 zf$v?h=|65O2o%5MJb7w5@VEQq9YLJlSSX5Dg-d2RW$jwXEMcRcv2&Bi%M7S8)aPfD zu+)6+sowch5Bq8YdPe4>_G7Yz;v+H~GZ9>_>R~t}TsSS8E9n%Jjn|;1e8X|3+)&u1 z-^@s0c~5zkXiu#t*P5l<9HNEYHWa>!ZkXQ7zAVsaHPv~lf6!Iu_l;lTx}B>i79RFk zSi7k*04Y>a(v~RN%v7ktn-^T#EHQ=86ICwM;E}S4@#Lo^l>9KhLr?j7cFiTW$AAC!L5_pKV$UE9-0GCjk zkvq4s%->ZJTK4I@W{$jfntoZXI-oxWRc}Y`jkVb;PPi&chFv+&bU%fc*Glp+=Oa25 z@H@^u?Uh!V@Ixi{exb@Gu#mF`wMk`G?Amcv9fXHw;p7^{8c75&PkNW(m7}V>^!E2hxPWVz3_h3CX;IrAP{AOB zwq7L(Vpw&Uk54&8ZzEuSx0EnDy9ls%QA1m3`rOXt8?o!f8ys@w@yP&}E&ZYA!l?4= zmy>H3F$nQ4#g=It>e>g3&f{`Sd4fHj%>Xo=&`>l_wCt5>%_#1z$wJq)Rd`KQxaV#) zOzjNNEfqRN%YSLYUOp;eAmdB=4j(4i0C>rr-T$6`D#lji>RSPLFOqdA`AEd-w;Q{9wO;q6 zYxCqO=C>-bB_7%U4^$zDX??d{yasEEY2?J|_$=O(k_KYaZOV%ir62pVVpb}V4vGF$ z_4I4{qRQv#icdz5ad8UdPQ85l=#9_*G>;J8!o?bwiucwnmBAnBPCG68W(}-%6fD+# zI!Mq4d8CA&QXwX!&L?nR&~5>?(kBvQ$@q|lKXDl!cI2of%1J3;nASy1GHu&a`&kK*jj{gm z2=H5M8A|d(YfAp^k}LYQu<0-BB8zd1hGV;3PJLp7_>y=roZ8}9IQg}i8_%R#udrWZ z*S-Dl#f!Uc@)w7uE|FC^NqMysnzhMz6fWQU&i^lpctUi9Pi`f#Hmu3R%r2Os!%an- zPliz1=TnjAuiqFOdBHC`=(kiZ(J3+0GjD7Ea&pjOT#jg;@qiQFI9kicchz52IbnSs z_xb#vk+R#(Ok7H)FQqax_`e*$oVytRU)k{PzF`Edr!zT{Q>rOx8}=s<==|2A(wNR< zYuZjo#CP)F%h!A_E7tSS=_3Ha!B}w<4W0Jw9a})dT9DwmEjq2Oq0$1~Dc$T{7 zhCuvNm2o8YR=3sCO}D+koxOG3{ z0ZWdq<|j;+loWB~#wuzbg(i+Ts!;p>lEr>4GL9m%Q~U-}RI$)8|Dv%tICT;x@BY_o zl4Tf{v;+J5y0wM({^Z*Y4TVxZ{QL7-3b5k|VBGA-dpfwD{{r23mLU_xnIQ%Kn=9a8 z|6vNJEOl{p{^AV4rZvji7p@BA+~z=ebLP0KfFVU~r^iPMPw{OHlxG32>>5wBS1Ro% z`zg_-dIKquR7!UUlolsB!iu3xt+|BD^=4OrS2W@b^Tg!YE3OnPck+>eZIG3#>} z4TTp<-Kdp+D>0??d(Lva+so^rcH*QTp0K48dh}!CC&dWJd4hcm83(!-ibLHsKs z2<<{A$Wuk2jsM%)SF00lZ>qLoR6|$J=iu|tc)VXfI(Wgx(DHPet~o-1qp~`8>g#JS zhyaFB7elwc)1!F7av4AmRKVo%>5TyWuKMWFReKlHAJ|`BbMgXVdZ0v`-Sd7hhnoAx zo}(~NaBodrwr|xButzTUpLAsINJzqjcF)<(N8)~jW%&G`6ODCbpC|EX#53U4l@e!` zO=1oBkkKIClI)8NHjrOgk7a8nw8eH4r(>=$Cu*Cb##p8E-L@T&*KJ$&J)*yJ63`x3)-2|IeoA+_)Bq`T=YD>C5GY#T>?O$~nU>=g8@k8bU%DV?TZ* zwlP)LFJ4~s5#~BzQPFm*r@6CK$N#gNALst_n?@yA4=Q{~e8oL)tW**ojq~Y4=g%f8}fy0Y1l60gI|-ojxqYP$wDPrroby=%d17rNEr807w!}837^0$3aJx3NVWRSD5foQMudE4gT4cN~}}y?lK+8=Ii+yci~k+8`)>4p5WM1q&oqnE}GDO`R=sxe6f4FT=}6dLb0@LecQ=yFzU z-BHaZ=k7-=JevswH6|_lRAhUH=qGMjSN!NUj~D3C##{Q{WwnZUdkZGsa?wdV2ft96 ziN9qix4eyyKS*HRVMEib>&5bpGnKHrhuO@(7L-*keU{B!WB8!cZoWT<4`Mz8E&K6u=xuZ691DKjU5+Wv|S-IDWtz z_tI#t;7;dUaLBg}uOB$_VDZ0BUR`GpS`J{pq^n?04$lBx>Wz=wnpnnzT4I#H##^p% zaxBEvt3$yyI&6_Hx7(&trMp|q8wVg(;h3i-?sqzx?5J;-gnfWeQ6)BR8HER;hx7+2 z#tIMYd-c7<%{K_9-0Cddh1tdel^&qLl0LXbI=xE0(0cLXP}8>;IY3Jv2oVST z8g-s7rGH5Y=xG0fxwkL{#!}97KsWkrJ`cYRJ*wUJy1Mp?70djILpSc7IE$S8EZmlH zFiTg`Lh2*$R6(y4RS{=GpfxwaWYk^`Pkb#=#xZLZAcZL~*D>!NyhHUWd%Q$6x#O9t z;?}@6FF6wkkDZXl9RJIl_oC-VH$GY>5Ela>H+}7abB%wN+HiwMb}<*2kJ4CNgowj< z`ilDr$aDqE&81SS?%+hrB~^ckqk8@6R9Q%8e8+uATj1Udv5u(8>-=Ye>+L_nK{U$Z z#HlDwj{j^cvEYs^#RD0tPIj-!6R)AA@*-2%;|V0lN(2r(ao)MwU_kX%^Y{wNb$149 zUnHBPnQ9GuE9GL&oZWWrWGu>4`-(k`oR-T(bw=S?SgHLBI;=F zUm*~0K;!4kHB{^=NW|UInsT&NKisg{hGNwf5sCgbb8PpjbHke^$(#Prmbk7#4H#1Q z0H2%B$(i7x`|5<`hYty>F&$SDaaY&n!{RXqWshy#0~hOF_Y(ToK!EKmYXmDBcFYP#v;*Z(YQ0Ei4uo z&g5I(xry)!tl;OeJSD^{pbB`l86cyUdyLg6| zKjdDhEu#ivyDBX&7x9Otl1Y79f23#?fjuzeU;sMavvJ8HGKFE(*Ux)R2RjPP5N9N5B8O&G728a*t7HQ*1Nd9!ZQW4gj-JcBYcYpaE}C*hge-Maf% z${sw~O1-rJ`}PzA(3a!uVan?=AgQizgvi3F%4^f5Coz1-Vd;$eUIYMGBbn@1L1Y`FbaP+d%9_m$0=sdc** z_IKp=R(z~n0@L7_yuH0o=_?$GlOQYWLEip@sdwUHx{N=!fNd|-G~cB0V$y+81Kp*5 zZCU(LBH|@fT_ge#!!6miB_@YFV8Lk0K4ZjJZD|D(V&lryYZUkinbP0h zcLkxzS7vC=z6&pmP?Wx>;1FKOq9uJVF$X4%Uxb`iyyDnYE29Cr6njS}e>3Qfy7b9= zyt^y%@d1$Zp18=dzwy#}^!3oH?H8H{w^|O6`qO6=a6T*uuNU-^@Za9VPjP5d5$-eT z_Uyglm+%_p4L8NGy+U@TU|P$(ny>tGa5191_E%WAZu^z7c&Ch{F|R(v6yHWcWz?#f zxN5~w<2jW4dhWN$Fe&9fB27Lju$1-IJ3hy{iOBj%Q=qs#P$W z@)ZPoa71loYN1LjUDyv94L;fL9XxJ}fABAyu_xr$LYdh#X5D*T&KKIb#QQ-o0W|mUN>YEc7ZG9I^>4%(EJymI;5=x>S&i!= z^&wapIwrf@Z`Sfi*{%yUOxQ>zWAuU?GVvpC37l3en3TvXXPs6oRhr1V${NoNl7raC z^2HjQ1KJzdbI>P;K)v5{g6I^CVB%v9vR7NddRXPM1I{WQnzr76BLN)fp3G@=J=wa$ z5==?&AhvzNQHSTNCtEFUC5Pn8c!(D-Q5pqHB;Z6VaU1q1%eFcA@E?O${e%y-d;}<< zJEk!*4*KJ-5K01z)w;TO2&IE%_i{m;#=AVB1vjo`^Y+K->|LlYTRukp<5v;tFL=-v zfG&^Xb(Lh@9ew8yzUFt5Il8*}fQ$v>`JBw2&T|5{Vn_wY>;B)D-UMQy`1vSZ@x#8x zbvwtWS;vI=j=HS77OpkqQo}s^O%Q^_svMNX)b`_(Rdp!K9J}c!JDG@Sg{7~HoyUm~ zk$-@yWh=>eoxgCxYI9bv|FWR&5c_t(Ipz>qldR!ULY#!k5HFcgcfa0NRe!*G@vGHZ zxtJH|ehfA3zJbD=tqFNvad0CiBEr6_xJBp4)=%R~jSMpAi@eLszk#pS?!)sxqCR1wF~@9OAao=s2h^%ed~66jmOnEV6^2r|?IE5bnnqrT-VQAESU# z?&rU2cgJcksU_ilcC?SmPtrwcQ(|6u_f|0)QrGv|Y;3!PLbX$S_#yMwRoDS_VrMhc z<}hcpw!oP#l)Tm=rA+$jW)58$RZJ#*@f~qriPC$*awqd|l!YW~fT4W77mb}g{}<2; zx|9Q|SwHXr(9wb!Nu%c{5K(h6PszaxN=&8DZGSa1x6#G--1PmCeukgFe96)2f^jL` zdAD}o$E43&1@Ch9EnW>Z+SJPucrY;#PLESuYr(DaagyvsZ~du;Uuo1;>u1*gt2V^M>p%;xg>R1)XLKl{N1D z`WUswCLWYZdkeTKdiPUOp5R*C;jJ+APVSzGn++b9et!jjw0PrygZhmXovf?Y?oNCy zCxiD9y8ouJn%U;p;#_iB=`+py77`Ze8gIO?&hm5l4foKnS~U;P?}vz0acXbOiv99z z;mi~pnK`O_3Y(YNtl(4Sw@?4+Xdj0;5F8RkW^H-f+TjdstQ6bjM`M;M!t)~dd&}uJ zk=Q3H8lx$A3ziFnZTGw^x%rx8N3IJrIcN@y9(|~Hv5la4cZgk@*ejLG|Ly5qwb>PS z1>W*IxNUy!6u8BF?BVj*1rUbPu-EF)ch^4!enQ$>x%*MGLsEci!x9h45gcPzn&Vxl zW$BaR{k#-AJ4BYG+Fd4!vzWN{1?Eq@wK)X5ZCX1^*Y#(AOgimqmN|c}XLs$3zoKKH zLCM(ym)OO}_8;)VJ~;re?o0l;7x-NpyOcwBX5131^9v7Qk7=yJ-U4W|ob&4>_{t~D z5anRtb@jY)k2TXVU$qPCF-+tl=s69~!NpPTFGoJ&bm_e&p;_jz%FQc0LQ?|)a39|r zU*l0cvE^3JdJu>Yb}UHE__?ou7Qeje?j-vqoDkK;79SP8Y%lBtM0zx;Kns^A92TOisPzjR|J(jTHJE4lo^#wmtX&=lu#r_ zeFAwY6UVGyxC#H#xkL&tsF4$e-HI^9McTR!UlQP>z50cwBGySUC`?+rzr44e+pcea?EEaIV2f=`$FGTM?+;pgmNg zxt;zGV-F(Z%UU!OJvT(9RSyPa_CwQ!AMWi_SKoj~N$7*;J)P=4N5BqwX*gNpBD>ZS z=$%WIOjyjlu>JfQT%FOcl_1BE&^Sr^= zntVVFlheeDE$gkkMTSyaw)3thO@ew;JbZo0oiRM!=C*!B1)@3U)+HMP>e*M`(JeBh z3yaQ${{yl|zlYZbVE7(K!`KXS6c6H{8*)+}FP+K%=MG&F8y4sdo!ndiQu;5nO+;8O z!sUgoEidiXuH$uIU@djky>P8?r_vFZH8U+zN;cl;nl9eedW3NTfuLI?UG6=^Yb4TR zB&}nY{`~yt_sG4Nm3I);K;?XnPj45fT~#jT=wGqu#~Z67ZR0Iq`E9D>4>e(q0Upq3 zHY4;{5W7-GLFKmz>6Sw5vF^U?7@VV&w3GvW;&@HSWbuBd!~4AzT0S@jd3{^oC(v&k zc^mn8Iim)F28%$`2w>H1Q>zRuxp_zH_2X;D-kF47Jt_&!(TLQ8t+iJ1OIEJY?*Ehk z>P_o9OSVDS%z2e8l+y#|tOtBfuv;J*c-9t=Eju&ISEN)PEc;;;G}1-zK4FQF0>JNv3>YK zuS=APIU*T8gsm+eJ)r9|Kuvx@3Ppj{ z1J7^Emjs4A%`DnriLUQ=n5m54QFx-?m>bMwK9Pz8Z#u+g&6?$d!1rFQSyv6sl>V=~ z6h&eMiF`nWP7)CKAR^zV#`keNHeF;Laz=2;p2i&HFJ5W$O>uJQz$1u2RCkirP(6O= zuWsfif-pcyO4h&nZqe=!gD%fKC%^@x>2vscT43q-qVWxyA6{^r{fQW)c=cp$7t#kj zUX(9H3@e4_k1;9?{zci6UW;o*3gypgH+C2XQs<6*4#7+M_HT!tjXvg77PP8VWJx>R zT|7RHlS5+{sYWf&7uSX`zSIRYmV>p=Z5d8}`_*XHyw9&e8*o5n6A+)-M=A?xa~AJm z6w+UaV-vQqlwUYfYWFUuQhvEex!omEl(W*Dz8L}e<{OLQu|c|{D5=hpHm`2mfnBN{Y;fH;yHD&Lnu9(l=utsHO>DMP&e& zBE;Hi&7oHnHdiMtMEUGxA_N1|cOC8+!qF)_SlgwKVN zRk^5mKwHDDy2`;b=fgw$n3s5Te?FE)1ed#didORkU5LJ|veHkvrGn5jd^E0@y-@1o zOX2^T2%taH#wU;{MiDS7u)Q>FBTj%=yX0VW?%m7{?Xrb21|hIgQ*M-e9#YuO!jq%) zaqrIb<-p@-7zY6}!SNcvi}@=#7y&qx!*9^)C%SDjU_LU$}AShUbz_6W2E41pxGE zs`KyO-PY1@a*+NKK`#BO3_HYRja)i(Eo_*T^brgO7)a5U)R*#huVOO^@FsR}p~!Cd zRykZMXCrz2=A64(WsXNIg9bMF^KxmgfGulZ{M}+um7tYvyv_e^(+5uy<{9urHs{KT z2>z-?0=_4O_F8I9NC%8syj%Bo_ekHCW?bUD<$JX)C7vOxmc*PVvsO|!E%;)E8ZNGmEPJ*TUc;#o%-1g zI|+4rWy5;2858ITizIhB+y1~JZww!LJM0gPwp^}UYnvEi`XYAN=`%VSB7gm|gS<|u z@tWR^OUsUD?dL*C6*3{7ph7mRf*>6rHGhZtUhv};cDVTQyjhp>g6z%`3KlfHeEu71 zq)5RM|2KH0b#%Er*2|8X`^@T-BDSA>sY1KwyAgY~)-skQd({6?yP{M+a++!`$D0$T zbd~BZN687ZvoB0NxQ>s8Xt zAY>dYDISbF48;r7PI)oX)N7CP_ANTE=<7U*cS2U9Gv2te_jdtaS@}L5+QDu0SI-ZCW_p5msSkpWsl%n0WzoF+{JVhi+(?%epf=^r2`b1D2?$Au-& zBCsDpF}SIXiY^xal=#^hRZv_u&Qr80liL6kpz|i}QR3m=v(vOQY!HV7j1@W${k(Fy zEMMKZgYRE<@EYaGbvQP8-f+r-zx+_Al?0xC_so%6=P{o|PAP8WenAM6))p>MKa%ab=0cAvIb?--W!XNt#%e=ye+F`*a^ zxXFYJyOw%{Hu9!l-350W~WyVIf2S7};~F$UM|%47$44xA|>xs~hq z^!db7>pKDSixVzt>~IKPVV;P=x%aziQg}kUY8mV~w@@DBH_bB&g{unsl|t6sS~p8vV~VTs{4LXj_JSPV z?Z9(njd_^#tzR~GXD=u-wEploW3ga&zw6NV^?@=68KkJixkQh<#&M458#X}Cj@UoT z@}!=8)R@CjaO--ZA0NB`RIrM#nd_SpQZ5p_Wk8T=Xj2RLv(#USJKaFy*97mJkpMUN z;3Q<8nKCGx+)h7nVGs23YLCOT8&12gVvlG&M4Bdp`cTu`p1}f!1nDkiEn4eU?Ls!A zbA=;9yZMn$x`;QWhb0xToL&#xGQ4(XGVF583t`(yKb{(4{d5)y?3RA->$H{`2%zRA z^r+e}FXRU)=W#yV1)(gX;5&xr2?z;EF78uJ{^RuF?F{yo6CAiOP;Wz^5%;0^^bNq5 zzB~OeV{O=#Q)zG(7@q~B8>D?U6Ji@{jMammK74fom~xBetYT(LXv({c;f41fZ-GvD zZ*RjJ8cBdtr_Hsov5EWo5;~(@yg9Q-Yu6YFw{Sp+KAsqNjR6!gs>BqoRIu zD>C};bIzMp3D4AD&c-O5g@&%l#WfolHk_q>a--t;XqkyudRZ!XLUg1KUzO>s5j$U% zB2rH{8;@G7-zlBRm6!~8{8i*u!)twnGo9oC9%sR7@ms8p3(a>cPyR)!Y>Swk;blw zSQC#LQio0{WmSM9dEN+gak%QGrrYnr5zTYYTs-#jhGP{!eJ_ElK|L)%J2qT6T<*zp zREAL(kQcwQ!9~^M*z2eNg2n|rQ;GcybYL9Ty9&w zgb2u#c3k>HLpBHPT;Xu8P4*mWLJe}t19IR*)TG1q)q9uBmIoJ}CoqH|UmZ9na~)S| zK2Jz$S<1#M^mvu7>L)8tl5xp*n+_xnT7r;gfk|3%ZNu-(d$Y3sd+X;mZzi*BE*wv! zyx%jE6KfH^xP zDD^*UV9vK#lRQFs{jJAOim~PhuR6uyJ74j8b?)k+p9D!f-&XKM!0nTZpDLTySnh)6 z&Q~gpL(H=u&MhwT;qN}8RRoGK3-(1Vn@ORzrS>YHUcV8cdNw7=D61ZC3Y2j>{Hgi) z$bf&uVHY*ol#j;r6}owztUJ`4n-(-y{5q%xCkV1N)5x05;X5P3+1uB6GLK{U=dPyg zor^ES2~ykT2cnEmm^)x`Q|Cu5(Eh=#+w4Y+?_zrpg!W*9oK~C{-?62wY+i*y8}+E_ zanYCl`ZqHi!=-5(YhNoq^(@?SE~fh#*Ty%5rkzNKaWii} zo<;XquaK}g<_Mg>(LZjh=_4H{P(|-+t6yQQeye7sKePpwD9)o&5SyOoL|*ctJM-8- z^Pd$!b~P;cyrAQ0K!V@T;-A;y zn0x&Az9n zy^h2H)F0M&0t#b54{2dV;CugqBu>zNr8MhSPU0*KFVtbiJs-+^&Nt9)raEPznDJNH z6JcbI`b`(mf-3$Z;lm{_x|-fnJh|O;{YI+4RXB zNwYZ0gZRNalJO>tL)3-y2rYpC)BQb38pjI#N;uZqo@yBRaZSfB>^n6u7wpa}+LS^> zUVj2djMgaP=8JYaj8}Q#)!|a(n;Fc$X8MbN$pq!%^0n?gOJ;;Zf3bsAj_5MT)gNyW zucgaqNl{x06Iis8sGjO2;1+?L5O`qUzk0y;jDVxxE5OW8j@KkCx}g14e|-%RCR1OS zh;O>l@)d8@rbzrWgH!C&nbUmfyhr-(FR%}%uPjlNHU5}Q1Rvsl%3&wJr>-rKhaQ`+ zDF~&Lb5r*DyM%7hvbQ*e+{g_uaZ16=O-mU(0vg!sjVIhvc&>N_ADiW3s27!Qzuu9e zfe9rhESg7fzNL6fc(kO{e5!=1v-ELsuWv+jh_|mgm_C|X=8hlO;ThP)o*n!Rx?DU@ zV9eP|fFY^E~6bJ@8$yt9Wb+2ErX=Xtq*BfJs8#*kYx!V6zK|=wJAJ zy-@ZP&}TW2vdLuCGr7OYs}fgKI_wF5zjC}SOq=JGN4}S|7oa8L9adLT)3v|I|=Dc}l`R@*#W^r&vNug4q!xyLB;&;-0 zFM{<`Ct559O9yH;PZJWx$3% zwh$wUKG7y!w%5-o7>yqmiUKgt(nCn{yaLSy$TXG!rylqmM#21Lj*amoA309moc*O- z@UxA*W5RWB>a(@K%h$s0Vr65$04No}(9(vY*Z5tgCFSJ3H%{BXA^Y$&T)z8ZO0nA{ zC!eP?h~8=vR>32Akk07@9?x=4O&k?W{OxA`XIsv7-XxnieZ?IJ%Kiq-<3u{i1{Cv5 zAEkIEFWuJ*6^z6Q4bj8sNwRkY8We;p7w3-h}~pYdd{3{0YMSGW*#DjtXeu znz#cl(}W!s%Er$$hr7B&hzl?X8OZaqGtG}2ufNdmlp_uLLA6PSHT$WIb!;j+divt@ zyX)q7P3n0f%*p)nxcg#b1dp=&Rq4{PjZ@3+L$TOKkcOqWd3*=4@Iud;p*6eCUU_kT zi8|-+r9Xw^l;;i{w%Nx4tCLeHbR+764;Ae^IpDQ_LNP#dvG8?>ylA662XzTW|JoxB zj>La|ei!g+@MROu1%8P&4`CWu*u?ZMzgmB2p$W@qWp%j2OQ;oljQPS2(8v3cZv4*` zbHO`PLn#W~j^Koh#%9{%cW5K_zYT!2z@4}XEtc1GFsBaq6YDqB-3a!o3Q*BnZ@T4;hdt+WR1LJqllA6@}6sMGDh_HoqM@7*I0EeKVN ztyBn%j7@!g%FiBRlj5(dmXrENe;0rne>XqU*yFg$Ng${XH)otGTwVobm1Y(zWgB(DPXa2ia0 zQ#lH<`_x};dQ^8~rLQFOq`vy8O&O7h7VB!uWNj|7tE}}H_IdZ6UF^%DEus-il;%8% zy0D=uU$%wt(!L5ra5cj<-1&NA1`x6PNqyfI;fd&VGlldY@cbcStpOTWC-C~D((0cH z8LEus8u4-gzYWkUteCRkVIvZIKKNweSJq(-@i5!&!4LwWfNX-18cF92g{v z6|Sc6#&{fNr(xI&FkV$CX%9%D#Q9;j45R5RbZm;)yUt|M*|N#G(Hqw{;rP%%u!Yy4 z_1WOKWpns5GjK`@)uF9nkh6XYl!&6O;lBBSr)t_9v$kq1bVB;>wN+<;`u#6Rbb!qn z`u-3yB9za~G>K#fn*ttVDjt6g#y;>fv3Ia&j_;DEIk(M!1;(ik;43E*a-sc;v9i`Z zDHIn7Rrx*NNrEk|1`YU`D`R`i)u#qE(CLI2NScJCZ~q1hWC_LtTE~|s!n}`u zW5%;oGv)Xl)$$wQx^3Mtoc-m%xL1-5axBBs#(W%|;6LGQV_qLI0a6$U#@TWefa4B; zK_Aoa1zTjZcWnffL$$qdQc8V-@;b0I)Q80&)8H!xMpsSTC8t+?ttKoh?BFBlLnR>B zJ=q`iuuoUfS#W}MUPIgfD8`X&OQ@jR1v|CRJM0WqlfSpU`QZCVk__HaO89QkUNKOf zLa^VqJB^8-;(p5?DJy3o_$B<7uy3!CoDHQQMYe&yut=;)WM~dGy*={-(v{SH?u_ zw@fsS=_@`UD0X-aLpmkp>^XOPq`}o=z%U7s+rZ_d?7vN>5m#yiFPb~w%9T2Y50zY+ z`lkT9LA%8pu@kX>fbNHdo`Uz;FQ}>IZ!r4vX{gxa>vzWUGAfVRd;HUER7c6WJwGWTupBVFr_1ro!*8BQTZ%PSJG9L zsQihkI&@V=Wi)&Dm~@`V4OvF|Z{#ctL?f27Ub&0s(^QQ9D%Vc#bAFEdb#(sRTDt?k z>hPY?WGexoNND-=mqOCukrV3v$K&p(bXNu@1)a_X<@zsB-G4uym+KY;_A;=R0+y>F zy{3}zamd^QbOq^tfg(Y`@_H-f>4Koi>c3x2F&n7ykz=0Dw4t6%Uk>iol1X)EwkQYV z6OpsKqTcM;%Cj2JQEGd=5qlan*BbN(2fFg-;FiVRRlot=2gm@#(x4*}0)j2Ybj9@I zzUAC{aX2gLsi0|r=Xcj=utR{ZgAvb4!ACw3hG{t@EeX+Z=qaY_I`dOwoBQwI(h*CG zJ^r?3{DSlQ9ci(fSsHYcWkbaw=Ap}8c#iKR1CCx-i)cJ{Fp`*t&ke=%U+jH&N_~#8 zfDt)FR(7s=>n@t_h5nSv@ot->zu*`yzQl1o+y#AO8$VW!e^S2i(L}^OIG;*cq*0Z? zos1M*U8-e@w>a~hnWO!cgr-$asMLQP1rDv=SE`+Qx^OAPbBhjbthq25<5sfeOYR*k4e#oNPCP6@}}!oy{38 z!E;!*fV==QDwZ2C|70`76Ruhdp2@mo$5!hsfO+Ojhk^K!^ccKN2& zd+J$O2QIGp3zu-N$5Z7Ujyu_x3k{#U&)+xjdku^eAaS!cj{mxd z?x-)m7G}2{-7xWU8P1#U0WE?@m1TMsP*-WYG2TMo6xB=Ti}BIQ7tMB zA0I(iX6*ZY8x&y9!pOX2GkL--yzAa-3KLInC*Sl&;QQ=jxnvv=GL;I z5Qgf#1LD31my}T zSaz?|-E!<)T=u-h<`p}>aqg`<=GJ>zIi`TpqngdNV^enL-rK5XM|K=|8IkMr;>6pz z-;d-!zeo?0l&#t#{k-G$s#m*~a0O55wI1mGwt)SgWr2BnJ}*~RUKneD5g*T`yfEO& z_{to&z!~1~JJfaK7}D(RQ{cYtv4&UVIy{rM-T2@hJN(wG|Czvut;*$NE6CLjHJlav zyN=yaKd0n&fuWyR6H{rzUY-|!ufMhy`!^Z-+j-mfgEnwV5Xy5d5Mni2YgPE)uZlz4 za^^#bTO!juuVQv}H(9oMUiY~mE<^%N z*@{OHf1L4|3w`H%)3eX?RcFmHqYzz~2bs}p5tBCDMEXqRKr2pt25%s%e1XzY^OX*f zl15Id5ige<=^|zbBQ6}v*M4_5jflC8u@qUS#e80q<`P*~!YrjjtHCQyuAk-HsxUK0 zN2xO;P?inf64P3|a=OYvOM35Exot&6P7lq=&hmoQEw`Ff9kRu<^A^+3LaW^Hv|EBY ztlia|mXjyUl47erOY@vquqA=S+lI)&9meJAH1 zxm2`QM!L_#>hAlbny-q=MGt+?>~8nQ{zess`mnTG)PeTfd`<9-3Zdd}@Ip!u^G_GT z;7#mc+@~xda8Z&Pe7{~2Ch$xF-tpM<3Q*AH+y8JOrzB_1n-O;gunmTrSz(n2D0v7L z!Xxvqs`(}>Xv7j;`K>y!JMFs((m{8U?w{&2p!&`SqM9{Dz+=_HKRsbL^d;A3(p2xW zx*X0B2^=A!VN}njJ`*Q{qQp1Mpf0E1RiOUdf9QeGt2&ttL`1LjQv*pw_>)ogmuZ0C z04@(=CEbEj5z(n-KV_z~zwW4v>%c)+AkNZEZFAcToHia|yh^xC6-NRcAnD9*=I3^nRz!#!Js%$jU z!D+=U=<>n7lhbORRQE*(7#_=q)=y?INuMz1^`LXG=meuf^yA1ZyW23z$H=E*KPb_y zu^$Y$)E}>y@4`|n*Eh@a72E~w_tArCptRYweIj-j%8wLQ_MX1eTVylD_0CRj(IeWO z2vJt8TkD?dyo*Y#2Va?(FM7O@Z%2P&2Hpa_-aIxB{1Phk40B4W6g`@*n`K^DrJ37w z!>q9ExzHj8JADx&K3_oFaG+5%ZYi&-e_i~!ao&AxMkb&y^9eKdaX4VPUT&21ji|38 zZb;OVa+fb&kGn;0u>*;m@m1K{(EAv6r?Hb1klSh#I5LwH#+Oz4Ckn<61KJ-rU!+Z9 zk83uKy{?kq@ALTg#$}o}Kr*(jP>q-K1@gfE*tu7%;+*r&3Y>RiTZYN$9RFeaoQx8D z6+lDyD}FuRl30D~5YGp*wca(ohxFZ%*mC;A;~R!vh)LmUFveGaW8W!-WHFOBH^}Z+ zV-I&Uv1fu+?KL6iDzi1HfqJdwuwl89=@N+6A1w zDoqg-r${bcBSV#2Mv+YY;2xqhx$);Mmsd^JI6F<38`Jh>1tk5*S`YeDe|htDAU4^% z?BR`cBfS?&wm9kGwWMjx^n}<}GB;ztff<|!4!ru=^718q?7Jtv60Z{CWAa^}RR8VW zq$ix!TvmxJhRmj2gf+bS0@Hv&{rv87Wm%(5_49jEj+;aiZ1f#lLzE&^|7BI5Ruv}I z729#NhyzN@oDg86Y>q6cWPk8w5 z?eAl^OftR^MH(ggWDwRtj|uZVRXCe{o0`@|h?9MI8YZYd-Zo@>MenVEBTPu`R=7wx z!NP%vSo5DlzoxDdv0io#{gJ8+LY+7V`gkPnE|2ldh^f~f<8$3c!Pat(%XkUBl6VEd z^^rE$MOj8?^sQJphC}flaFlH}Zy3p0h>pEifqwd6C3x?t)6szaB23Ec;t{LQH+%y9 z!69YGF&ulvHa4aej9|e6I8I?z2X0^y1GY=Oj7B7IGwu*kKvxzGX1o7>x{an%KKJP!(F7eVR2$tLO#!$ld zv;4lTjGhqW?w{`-a&Aq>n*;aFYjHD1-k$HW?S-IfPar;SUZ&GPv+vhFx0X%eL-XiR zotq}YmZC4Q`hj#4`Up`qx8_5HJj_&WZJ3y4J>p&W0v%^56oxg!N3K~rzzqh%QUri1ca`%>AiJWdtfxd5 zD)}c8c>hJoj0EUptitAs*)}PjhnbN0?aH=s7=3h;Lm6fm2NQj3#vO^!b;N_;ke1B* z+gIRKI*+~XT>i>06;Dh$5EE#!jZ?lwR{A5Z2zUuRDKzgG7y$2)19N4DU%oF8Ryyr%a{1LnjX z-9Rd#%(}3=oVvJ0)W~THq_au)l5dtmlwWf(|;OEvUGk{o%%49FWfQ83#vk^5*IG&-Z3!9^0#l){PUlz zKqFdy{`40Htyu^?bgqbVBJk1e(M&S;t**($TsAr7x-h?nTQ#0_LZ@1NsGzEcw`wZ- z_dNN&;=r-4C?4!p^5Sd0f#qOKlAFbEgd)~q*_)GWiqW(%5xXQC^)PUUr_X*l0%IN9 ztODEB-89yaR|flMW}WEJ^}rsO6K;6Y2<1M`-=2<_TWV(L%>Sac#WxX>ltoveIi0bN zKWmS9ao@d`6df96kJI_$y$&{F1)$D!XL!g2fchlgYqfF1xs}wV!OIn3Z*=Lj-EvTZ zzd$H;CGu&dkEzrE#pshxX`I3!B^vV*1h`h6yliv)wA#h6@!q;yO=(dJUJK_pPrm+o zW+7Px`l;23TLVg2ifE;9M736MY#^fIHRS}y%RZ0oT=^^NTpahC*DLNR!i1R_i_A2a ze2U0nZ>5IpjgIySl5w9YfxE^?!cjsmZ&;MH8M=+o=>hcSW`v6yQL|S-w()Z#+P;a% zHV{>|B5-oJC0H2dg#bg-r{)mjqLlxgi1!VPx zJL-#=kc2Z$Yu#FbJ%un2<&j&FowKX|xmR-#{d130dzoAi!X_^Gi6D|_xS1{3ei4rM zW#FPhS5|5l5AmJ6>X1Zs0+r>=xNA-UuDMlwhAnR(-0a1o4H7Wd`-qOd9i2F!=oLm=}PEeXiLM|3j9Oak=32N#IqsO=IBTEZOZlEi;k_ zmJ0mLi+bWVkcL>OM?Eh5g3VbiI~li?6^Gp&wZ`qls@kT;!z6{pN{%tpYcv>3g5J~iwV|4)U@qVh@_plOsQnu zFDaY@a3HsoEw9y@U9T+t0>9%;EVP zK!50-khyeZ{K^qzDVGT+zq)~qBew}JKP}3BSe89myMWcR{LNW&uD0O_<*2#)O~b8h z*1JLbM-liW;gtSdeh{C5LqF@nkV!BcukE-+CICwiPj{zSPR`22Z@2Tg_cO#ui*uRQH8$6hZ1uW>jtUA1<>f>AcV6Mp_= zeH;sPtScK?s~C`f6s)5DizJ?@TMR?YRW%Ge51IcPgzAq1ZQrI%j(YNdOH-n zze>iqB4^SRFY_pc<$Pzrzmd?Vf+y&|Dh%lNbx>jd&1dC(Iy_JDEqC)4sZ^=fgrymg z&;%>6+2+e5JMi*MBZ=C&ij~AK97GGXZP1C# z%CFRqbXz&vFPVuX(39Zt%eCQBvwvty+I;=T;@$+F;n_^9bcL>Y~5piQY zYUqDOy#+!=fO5Ia9VgUa22K@p=Hh|x3{9~2qy-+Nl3G@pfB|W|gSj?0p7b4o=Et*G zCJ+B$ZtN5z9p~z0OAFvTWCT=wfIU3G7@f+r0mxXNnD8?}Lee0dmlRG_0zpUmhS&?HseWC+rI2uwR#A(3s{5)We zxQmy=qn8b9V3tFVCPKIxk9YLq2PE4F6-N%;X1eWUq%fG2L1BDF$^fnK9rtW(S>Rj zKnaGSy39_XMkm8}<+4JvUat({4?-Yio*zs2MvWeHXCmNIV1V%Fo1a&R?B~&WGn)bd zvNz&;5a_wJ&F8eKpKXWFi4RE6EQ5hsp#zT0d7d5N&G+f%B@fxjEPws9*RF}21jN_+x78?9!s*eG?dhn1P{#^uF~5?(e4kqo|m(sR9|usqa@`?c{&W-YAr)hWzJ6SdjyezB-{?kt|=_fjWB^?gQ((b z@w2`N$7wL4yJFYE^Vpn>INfHrq1C^bVD=zq80);f^E`~?IL;YIbVaDPgJhb*qxFE* zC)0wDFIJqd4PRFIzl=WVgEQ9%{mS*$kMoc7@D5C)k_?*f7Rc%RQc|&pFt;o zWhquUc9}%~P;9+!0E_6K*b3siRoj`0i>+B_5jXn64fso>z}6L(UViI_2Qf@w|K@?U ziaR$S0hSR!&G5!<7YVKeOSR)_jqR!T=%WB&>b1Q#22zv5z^Tvys-FviT=f-KA(+fEXz6<(^7a7kM!eO>5Xra7 zxi1;f?d36z9B3j*ckr1;raU9b4xf=qyXdJqon|#PlKXw=v18n;W~;VW%W^w$&9^7y z53ensb30M(7A<0|2hm?15-=#md@lUcNFv$-wY~%26qj!vDc=2JQdwG6*NL?$yG!#@ z?X!|!A^z9S0#6(&DFba_v%K1@Uab{k~z%*H;K3nK%%Y>maEZ(Prhu%(uQUMqaC z#dAr>GD-ez=XOftb2}aF(oCFrYijMP8+T_)tJM#0rB58!jgIQ8tI&TZsQ2Zq|4SE{ z<7YH{n$FnP{W9<2(Xib{73Ji=cIvki-fyr=_Z*jvHQ#@A_412t{G+l=OkUf690ony zxcoKKOHzGSUIw_enVU@HwJzTL)Fz#0#WB!`H|F|ZZ|qmy=UvZKO;zkVDR7c$RAR-8 z8GqJ8SS4h|p=`e}NAPD~0o`Uc&dUPf*i$(YIsfaUYX^7mYA5HlkE_F&uZ)}RCn<-$ zxsHEP#>g)#Os&a}ay(UrO|rlbsM)GSAkBvr#hZY3RB4;jFY%(o^(*Fc;7)Bo2D}eb z#vA+xz1?BJ#y06_sytAdm11+QSh}2=+(h%Gi4Yc1flg_mKU6LJVhlwer28P@9*j%rc{J+B(KgsgQg1FtR`=IL5TW2cr@ zyW6@Y4Sy|EF1}H_Z$Ke?Gcnc#6;MKXFDx=u43Dn~K(e-dyCY+~b0fL0nhEBbzW`}r zo5x&`lR1SCM`j^33t_gvvW#_cZJfuOLcgRzUff5m4|3ndk6d9Xi;Njhnm{;@Mchen zPeB`bYCS?8L>=01AfmxmsJl1!$OlM_`b1cO5^9@kNf{l-K9fBmyLOH1l=AgQ&m_Mn z?Tp)XEot%B3m>q`W=YCcqBYNf#Kr8F2f%-(U95gEd-$4f8IIvg2mRyj^zr`CL%>a!wke(-V@fGLRKq;yB-`VX^s%1 zv*YjwYn8x47KeZH{0(lyVet_qKEqo-7`$lZE;NhsG|IVqx%2JywQ}|&(C?9hg&yZ3 z-eVIGGNp4RHWA#C&s%OCFwh_a=YgxOd&k3`F zcs%oCH}@1UWp7kXfyC4&%VO3aq2rT6JIs0K3(nt>*?Mh~Lv7RkBvfQw^;3CryJvAt z>Q9Ho_XOKUKhAK@=?Ms>jb83aj`L3lr$Zut>nW)r6J^?M+^d_X33oNhm4kbE3KdSt zL@yE@s(OoMayo^AYS1;THt?wDWr8Gcvzup_j5iB;o9*c52)6-`gBHmI2I)IG59A6q zo&*k)ey)YyIBfEsrBCK+MvJ1LAFycJsM9mN{fK9dOlUL%TbgV6iPJ9!EVWPeZyQ?F zz-W}|`}BgB1C@70VDisbe`zj7V!S?GauW^PpElMm=4Bw+USG7x9!bwovw^tNHc zRorhz5gekf4;VjPn&=Ss@wKsoI1CmBNYfGa3=VJ%wpM|FNi{<259w&~+F-*97__=@ z7!xOW(2ZCv)90^WeqObAo80H0m*A`XF~%`^Te`L}+1@|wV|IQPXF=ev9D(U}i+C_;Q%tN2Ic_N5 zJ|^}qnXSHnOY2;^B`L$&>QHX`ZxCl&GW2<-=5du(=0!|ev zxr@+_qig~bKJVfp2}~jkx(YNc@tp|7kOx!*gYk~tZIh~s(OBCcMM5K-H&?l6^ZRxf zZ`)xV3@Kd)oCFDrGo-{l`?yKJrD79DA>1%Ra2$}h?O-nnAoe9l7eG2Yqp_X5AHuQb zYHj`&kzK#wqiP(;0fBsH)vPzpGZoU+duP>u7U|#M!zn<&>CRYQZ?J?EVl7d4 z{VN*ImO_e{B`#@skyl~y<6qvy{@k3q+T6B&Kg(--`OaFJStrPNdDv#zt&yp1d~drV zYzmx$(ndPQb}JtWADW2Yx#CTJIZKD*Y0)@hlEg_jv8vd&aZ7X)B} zAQ997MFrJFzdr&|Gs2Gmk+|^;EkmmKGj*x4uaZPvIly22FdG~-r0diIObaxIgthz`ttWV(TE^|_v&|qYClJkmlzU=C z*Mx4F-HN@oQmiXw`_vF2L>67+v&D!2Nu3l7W+EA#YiLM}8Y+gcg&Li!%(m_?TT8p1 z33RFJpr#5Yb9U{LM067s*2m|;6$O|YAsk>*&EuyN;0FN6Jc*vfmp`O+2}eAWu|(`r zLCQEl52k0zi6@%Mj9k@boKpx`seI7S&L4b*uLS`;+~6EfY-#-cANpQ#@Nqq2yahPp zv|0)#BH9Kzcwv0hlpdkwtY;r(Wk6`DH^2oYph2HMa&w=Qf#(eca($Dx+@o7yDZ;O{ z@Q-7yA9Q>T^xn;#=tdM%|AfyIl>XtYu^claEZv__@X3_&6iDt!Y1Ag28`HV<>^Ohs@5j(t z9l$DEGbo!Xf~m%UW*crNaRG_n{t6l9ij9}Wb>A)MKELc-j27@#>1i1_CR>wlz+ zfOS~?{=-w`>dvPE4sZLFVO8Kb4E6^hbCJv|R`I;2P?VxO^(Eg^sOvzWD_n)D2tlX0 z*-snpul(g_9~VE3Bq(Ab`TCcS>V%RKIka1rW=@Wuff*$AL1^uBbhFMgdVO3s zY7U!u=##@70s{CHDnPGFc)<3_r>8VA*WhuuDxC*@f}{nFMAn2V?mTOF1f{`xE%NeDb#necIJB^LH|jF~n0uMnvG~ND z-iwVKLLU?q5QSm#7vK{S78!i;WY65+l>ABW?l*%)qKwg4BYx&|bNyJ_bZFawizJ`1 zlx?T)eVng|yG`Yl--4l@&q1tRpu)mF^FpNYzy)qKEs%87Yvx@P`I zjy3jX@4GOAj98&>BFDz)fnvGA)*t!lHJAqIi_CI*3EGyz7ZlZ>q4OZwDekxKZK^8$ zm9RXoeexqaZh&c^T@P=Y7DT~y6)2p&v-o2RXN$t$OG-d@ z?u@4&Y}O2XUM746%cOOmRpL$~i8_j1Ti;V4rRA;v%C*pWB~~PT;JV4)2ANwS$n3Te ziBgHqUCjm!4Z2reeqKBc^mcZJmsn@C>o#N*9Q#?Oh#S!D)7O!)s~wQ7K6qx;ZyBG8 z{_Ub|kM&Eg!g{V$oN5ueDRecd9+$WWzsfEdg8Jq#`L9h3dcP%Y{N?#wEK3`<-VBgP z-yq84|GA1*bz48wU&P66kJpnpm+GE9A|^13D5y;2SovBkJ#96aTu9lVSS| zwM*7K{%VgvPQ_HI_tA}!-N!-sr}Pg8xrJ})dTvO_3t)~_qO5!EgB6dw7#=&r>j7sH zB73p_#lo*LHaniPAqxWywO4IA{8+;=T+;r95BGt90X-`QKFuYA~fMLsuLB?D(%bWw4y+FVT z7_`rkjKwYKfaINHYX^+rsasC3XtVWH22ry%!qhhzl>Di;QF=V*7R>}?U+`9UY%ir%~TCh{Q z-!l zq9gdehvGWWKcRdJm=ly?Uj+w!hYcL1{eK=@uYVm;O(eqCcKkg`Mw0ea6PX?rx`;Ci zsKV>~9;$o~ip$;(=3bXKAc{;I^i@jEF{GISoLzGJkT`A@paZ5WK2!w&neQ$zdZn2M zVb`7{(eqTR#^f#n17;%&%mX+(KzOoP6p`nPc<$M^``N+wSR5{;NBwjbv&4=vJS;W2 zXD9&LPcGO3>vuyFk#kw3{fgZ#$;?6m`;#(UlR1UJR3Vl4!oKrz{0WPW_B}A>sfRJo z1Ua?CIQeDJkN(^CjUT>5-Gph|O+=KDPsq0s>Y=Vrbk40YH19-TKgXb5<@~bSSVSTd z_Gkd89+OzO`n@(p7`epcq*J&+`~LytlW#61-X@|8&-mOAKD@C$RYN_?$Y6~xE<}kT-9*tOS8nhOQZ{1r zJMZGvyyL~KG!_fGU^%jf$NksnJ;3~*pgsof0|sRE&pr=7<(KzP^b$GbR+G4aL+q`Eok@&m^Y<87l@FPP0~(lAFOe0wnNkawF} z`fK2%HcE>W)zI(JE@&FTUuKoKd)VP%%jxkS$Kl;bfT@m$Fb_7@*vaD>tgBR!^70d>X3cfW-!I{eU023XZtxDNg6b> zOrBiEkU-mE$^Uv41L4VGB}lB5G561;oVro{!8z%H(4`)(Y=KE%kg~)wsKnA0Qz+!8 zDVSM+PqRPEX}wr23d5kD1TR4Wu+rPA+CJ2VS4j{f^fm1Es-73SObo{%%X)f>6mro* z_3O>3P|T?!DLZWPmqttd2dsBIpS}b>J_hQ~qe!QhN5n`i(?~w;6qRABufcn26*NOe zZ7pb7@Hq@iVf(U#OXllBW_<4D{t_#h`KDSz-`W7!5>OG%21_ z)Ofk&`5xs+EN9#sHK7XqJoZ*te7Ga;hazTw$Z?v?^ft@y@Tw}zti=a#pq!ZUDIOlR4TF*qejpa`)pKbr4B2UHlZ+ zGAsu!>EW8E-(O4kRm2>KCDEJDtQ%C;p)!yc$hdbsh=PSMuo<~ls^IwE1$}DSqG#38 z9d_j&2uU=Yy@ebY^Blhh80vO!Nwkg}U#eTxSZCiau}Dy3n(jU*3_Lsn#sRS?DO-%1 zC>TZ6!=%&u3LBNA!_RvBMi0t|I?@ERvAQi$1QVQrE0iMbq~0A zY%#J$4}v!V>#UgWXV+t0lrZ{%mLXZ_vwl4aHBarzdYWxhL4CAOsaiYW-o*JuBv+QY z=<|{n^E1LOdY|Pq8r^^UiGB4!y_;0MRjp^1>E-aLG%MLrtXBFySgLX$yXvXqkHfmA zUp-IB#rfNk+IO+jv0taeS$yN8XP=}%p8$%yIUn(;RnhBiWmuG=-MQSo)rx~{{LSv+ zueEPNoUQyLlt^m=a+}y+fbfvC63y2i!$c1v3isX1KcFQ321@H$=uO(3Q;z3q2>>%T z=G%BZ_X;v(h!tKvHy>m9M}Nvu{GrQT;~3jW3UlA*e?JgTcetWVNMFp66kKstNMBW$ zPGkIFwgyxp?P~Dr8($hrbtrIr9tI0NDCm|Ot9<;foTMjBE>bnOE6!|tvNuEuyv{O{ zSxkieM`OU>qu&`@q`)0C@PRsezRKVsaA2m+RBtshkak(*vzn+QPzx&bkzD3#0-|yP z`PFx}+!8wYACRO_+K?jtNCoc?>8Ze_l7mU&lo*b_cg;tk;F*oeM*)?N&{mEH+Mkco_OpbQ-%uM-L*c^K! z?r1zxG;`(k_^Xsx93PkqhlPvi+58IpkSCJik6l&jsa~;eiO|p@FX)Z+hgXAquoziw zOn3M&OnFo7d6FT24HP+JH%zB3^<6PvfV$i-kU+2|VXdJd$ZHZn|PnJr{*@imLt^-uZ0e4$;wsiOn|$ zU*#w1b9%#si!ktF15Y3=2)}3lH4oxBli?BPq^-nd)<)wh@lp!3LEV&oobl0y;G<<( z+9Jmi17Vzdm+)|wIOhDWsimhzc{N2!FjAyX?u%xYWTf=+u=URVU>6SZKtB@k%san~ z^tk?7)2Q3zdu$$r$`aF(XtOJr^MyQZWzqT<`wDPGb6-`I2 zBLNPv2e8`&>_^B;l!#s(8B4=xny1O3a=a<#1fJG=RahB`WRqtgA#JOCyRy{B) za8;;oKQ?5~{xY8?kIx5yRnH{D9$s8lvZ?3U? zn@;*%@zW0VlBS?n%)L-aY5u@Bd=~P*d&DXVs;_8c6;_gyj^gs_)=3AOSsl_C+@cW72Et~rn{Z(?%OOP~Oxdma@ zH+Gn=+=?g!n8Lac5kO#GFcB0g3zQ1pyHTy#KN3WxdKFfbvOlO!4Qf#LkiHjK-VhuU z2a2SAlpm%(%?6?u^KFAW@;Q(_%8se0w)WHCL{4o9byb|tf5RSdA%yLlo49e#irGhy zBl<&!iRlkHcX1w<^x4SKCk5hK<7lF|jPd5NqV=6SAyle8PJN}d!zMh7l*zEdHCFBW z_lDR$qOSpR2Qey|%92rkqPsEp(iwajqt3kAq`SNB#>+SFprNqb$pDSQ5{_o zl^!vXNsagLWZ19InA0%mvh3cbgSXoTVNMw2&PlJaHGK%iJ>@Yj_@Djjf6PrlFvJ^O zvqC~2dgmQ}K+j^&NM{zu8K&mJN*@(R3G=u*`bv3Si%_f;gLdiFQ&LzPGlN-jzjP@8 ztH>seG7r98dO$R|(>+UW7=)C7VAhh|aEkih@Zt>!*&8WhkAiHjGW6mm+w5O?{7j%X z7^uHEg@x4P$CryF?<&hlIvQyN{f@su2+=GeNx^hI>EfvV3tsNaWnFFtSH-wBf&!(I z!Y@LfpKO=7{&Hnb!P3D!x^J)oM+0$R#alb=mCJz^ij<2uD0}{Hmiq>G6=*Ve^}Q+W zwH9fdRx$OQzw5qrud6umapiHfFVjra-i`c6uf>eX5S7n|i9K__%(CtydPmteR*fxD zuQ5v5dIrY#+^?rMO8LC2*vN&!)-_#|&Q`-|^U2q2kpiyEwpm{{h&+t`Phqtl_{zvT zba~Kgx!xm9sCwBXE}7h$V%&HR#E0Wd^nh$0da8*(U{h&XyGQ z^BF#B<36$Noo|OSZ^_U1PG4`yd_-O68ENNoCO5Kz^O)07HZ&|$w=q{a$>Jm4yg|#I z51MB;2IEBe5x3r~k@3jhZ@-62NOEpw+>7R`-~kE{KZUpjQt+!QrMw z<<^9-0m2E9OW4Hu?#|S^a7@Z;zaZz zHz+2Fe%q0LNZb;OWfB*m9lj(ELy|Qk4Pg;5BvBZVFqV|_$Gy2CuT50n`4vzxU&-L1 z+L$=KwQsKT$vJZil%oO7x-gb7NRvEp^3xNn1s28$ zAYpfJK}3%3+9u7Zlgq?-2nQGmS68o^WlJe5wH^YOy1Rr;M1f3JM=ghS%b9;G-7{#d$(t%%`$pWY>s*Ct3rDm5OQ5QL?%zrBo z9j95C+LQS6g$x$qz5GX~3;X~&UuQa3B0tII_1=NL3+>YTV;W4*=1^TBL&lFVI^Jmbf$;^#}(z^etpcltgw9<@A| zJA6rZJpO0dO_g{~o^l;u)(L@(^B47tW-?n2{H9tdI{akTp+}@h$95lEXG1Sdn%Ps9 zZIgCQiXZ<1iD)2sCOEZenr@O{F8LYk9{gW$rflq;-&zmS=+B&({+r#0o7V0-u^EJ` zS#tD_SLF>Df$FQa7U(O0@*6$XPTl&^8#L9styAdF?!2CqytF?l`(|{K9dmD%8j7Gc zP429}S`O6p@g7!tg|!Bi+m&=qpZy}nhPhn5Q!T9#CN zAlrbjZk8-RtO?kL=l`;eRPXg7HMn+dB;zykza4VbW6#+VEzMsXY+{%FPHi1`|E;jY z3QKO>ZVH)O8w@geIlPsI-Z7QT7f<9v8=T=4ud*?+X7z7$)4-&e&RGh>XvtTm+b%$= z{bWj)7`TMgSFo0j+P~~8Rb|J{2Yvflhj;EhEdL`?@g}u!)lSf(@)S_9QQ0ikBjl-tIxYePW8i- zo!VI)PXRM&+dl5mGj(9q(=onXCxQLDeXb;8Uk@)Nzq1Bx?qhnY@8;`Xtvy2pa&b;4 zyDEa)PCZA5_kY3h2iTecxu^@);6~$a%X?6(F89P9tZPjFvM~noXUZX{Yn62(T*p|2cAJkW~DuL=ScVWu(Aa-N2SteyW{|ru+_T z&S12{t;p=INvPRV7@rBS%ENxHV1{GyyX#{XRb6m%jYu*t8uha}aiYCS0B4k9RTAMQ~ADyUUpt zHxcDABKT!EBUkez#(pf0NXm=G2man|f3J{GG zk{2Q2jNdiiN-aqGf8UbLH3^0(6|3k2@}738QlG;Tvhp z$5Iz86AsU1c6p~oDxM|v)j4o0*R#Y=hVA24gHTQ5F)12GlJWp?@u!(>q-Q?caNSfK zR_gOW+qkab%z)h>hz5XA13BP>IFAQCy2&yRgG_gcNOwVN>Bo=UJ0DZQLg|*`&Vjwj z7PAFMUC_ehAC77Ic897ixhC&Ql$;Hz$$MezC~OldA}A>693U+viKU2f*BdI5#L`r` zP5e~ca8Z%!;%x4Yr3Y#Fzd%V-5O&7m%SENndYmVaCJHGgPtNy(X!vU##}a#0k(58M zI_0lp51JX@bpHsGEP*^5c9LgJ5q!dVniV4%~ky2mLz@2G-3l*Gk@VGJ`&=hMVjX!z9|1 zt_jQ3f4eoaZ59y?mehPWj7~5AfP`kNN1PYz?Qx#hdJc0c<+ZFL3oiKoU?SaHLp%d}opb***{_9f7eANixuzYB_BYu<7jbcinV!ChRe#?G%3o6L#SbR;EM7If(*2 z|JMBAbG4KEf*P3c2fl+7Eik)pT6o5_aW2~KcIv@y!Y2lraICdo zLLV%0_H17jW_cAo{@o(<)Av=h57Vy&)scNlcSEkhjF&6fgKygxLtLZ^wCpP}fc*_vuF&?OX@cI&1y6yAsFT_pB`1j(5jHUFb zR5&Domjqo4&fv9eW-sIbcW-)p>2iD|?a*UBOEVL+omcn!)jl3I)T+(Y-c}eOf z@x~|ReAoQ!H(2Ve!tgX-O+Ji07Pl56XS=Q`;+{v-8rROHTXz}|l(10G5;9G9`tYP+ zGEVkArn4kDolfiMt&2Gn3}fBwStGo^yj9SH&S4~&@KGdhd_{_V_qc2;->Jk{Ux8qI zzO#u~5kRr$Bs9WPGys^)-kkSfS=bXe-(d(f8uX%bB zeC=2cLN_;Eckad)Vt-C!4X0i91?wXHfLNW82lWVADO$L*{_X{C`CwL0HX<@%)R=R@ z(HP^S-$tuzhEXKIja8r5TZ3EQ$gX~ansG|dp8>*wOCL@okv~NmC9;$SkhjJ}CW>}| zRQv@r)=6ki^@b|BKlu_tkfks^%0e$wx}jxd(z??w4TsD&EWqzvb<$Ou>(}TjLY&Dj zCgb>b&tq^mqGJUMQICf!a{Qa)VSihjAz;?Ko_NAs?*qfep|UAKlO0?Ub#mRFXGHo;J%2@pu$fay5A{FX$OSTUcFuS*GP*$A0yu(k@ z9+xnp1gr)QN<@=(A)VV3o9P;z)82%Wj8`{1*BoM?i}iQ6y~yU@6=hCfdoYfLTJc|i z)dd07ZSI;|G3!J&QVW<0C{7Y*_PVng6k7eSWU8RZkKyhd ztOP-W6ijM~{htmTImrF{s7UJ>P)aaYABQFMwi1Pyt`AWE_ zLn9~E>-|#Lp#y4NLeqVJ_ZtekoDVS8vSt;(dn>>=A3`Xara?5n$KE8g_QR#O6?XAT zoT5xePdEAn?rh2eq|PfZv6?CAU^~cVHM?X^Zlr*iL!^d&L4WDySixv!43?_x?fFK- z=i7eluE&aFmrn6KC}uI|XaJ{|;O~OsIO{~uikyQJaBEp;(d=`ynOi*5q;%-4W9|U6vBBAMn%=dNx7`UJn}bI%?Z*oKeNpFMuD!HE{ScZugIX0AR(}Z-lIK-7 z2+hHELz4cj673@>0?GK<*GYI`kN`v?8fzO7C&Er93RiJiG`vLhH@s_A!xAp*Zb#qFXiL77I`>0;P>PUbt` za5=@E_T99ktFqg?q_C)>Y<1~nqVF_+kOm1(Ua0BU*k5g0qnD9SOQ&Z_r3;TOK0JAoTfPHpBRBhYEG!yt5%|@9*r~~{bI;dPxmFBzV1Ihy zrG1M2&f*I^?BR!DY+fb~YwWpbW$PZhwW3{|t7?6wW@z!SPE(x1nFBNBS|7hmkz6mz zGRVRj>wr7Al#gVzLEAJIq1i}+b5B@@ei0>Z4{gYTctd=E><(D4BOV)Z} zb(>T#b37De^5i9g?41o4=T~JP|^^e~n z2lz2LqUt4QDjkxmeR;XV{pY&vzsl;xj9*w5$LM^t;E(v}3B{-^XT#d=uNgEl!e>q( z6a9ynFvz|_x7zzJloDD1nGgTRqaq|{beC+CDe+V+=xv<@>3#f=-Ao-B!a7vHts;Ug zplz*IGgV{G+nk=R$w(;{KOVaDYT-LY%io8_CsdAI6gmAd1l5r_Kn_P8uEJG=NV~Tw zd=h_agh*Ks`X9Yv=>K~IZ}}}qbP164((!$L7sM{G2!^&)ns7^m+|Bbu7R;zFR8uws zGv+e7CCBe57fP`_IwyQbZy3Vcof*cD%x{DQdbRm~4!)^&*G|so(UXsCkzO9=RayJW z4-H_IOE|kQKG=MAOZEk>@)y51Rk_4o;2BRx6#aum$Bf>5&X834*>duM;H)P{8a(8{ z`$y^VFSD2z4Z%;VB_yx(fM4}(?qtO8w*@h>LE3)n-%t+wFX$k8JJ{WfrI4?l0xzWh zTDi0n$tH_T=jXCs#3qZx5bCla1%_3d9UHWYts&6&oJhg~bn3?&v2g9dZHI&4f`P@W zDR2usc2~t#Sa1-J&tD5_&7y`U20mV93%d1tl~5~b_B{)qiv&(PLri=LSghBRhDQU6 zmS68e|78g5pWX_i8pLzSbO_d89`=ZCtR>gFZ$_0vR?tkz_;D_hA@U$BK31D_lOTnK zqt`cZ&{RFXcF=r}K2iQbG51`u?D+j7Laot)x|{oga2E>0e5`G`Bj?lct6;h&_Ch5Z z#J|c(6-s|*X(EQ`0XWEqrn~At(@vKxKXF!Qq`t1AL%MkVGb_S9`>TE;f)qPAzwRuG zS{2h(k|&aVnu+5UX+uXTXHbQ;;9}4&-XUlx4qQmY}YJH?PQ@mE%!k`IY#f*PVlk!V$Qj(AnfnqjL8m)@iPY zB+nFrC1MwvMi9s${c_P2ik-RUsnP2nPx8FwE`eu=WYzS@-)&^A`m%ilIy+z38`K^7 z2go%KDRN~NISZ17R~vveEmZy>L|DFxTktIUZ#XMQ|C`nCjhdiyMr7gNS!iqu{Y99Z zJ2&|$ABAKNVZai~f3dI>q+!$g6m)Kfz6flVHw7D6(g(W$7QMb$;`QOXTTWwl#D!wF zjff%8xmF_P(JFfGcs%`QBS(pw8^|a!8w)r6`h?ic#HoSl89m< zvJ3qPAyZ6ptL!4z7bMeb7Rfgpw0uqNEmNO&rjB7au>C$;203m49k6N(a_8RhOjuE? zQqU!QWFlH=X({QEQUtSxX`&+3@H`*RzNSNkq! zV6Flx6$=4L^c||H!cr<+9f=%&)~im|!BT{$fMhYu8x-=877Ro9eY5w6jC{i0vvCoH z;0>OpH`x~i8h*Z%uUk2SB@4;j2Ih9nK4(#~-M&&8OCQn)TdB49!vQv>+DTK!k< z71uoXOL}>o_o+dUI$jOumaiChVzuTxS8zQ!w(^eUnBOaS^t)&PUp(^emwbPG;OQNQ@Vv+jo+z2;IF$6iH@G>{bd}Y+NWa#JC7)rHM&o?HXnn*r%?~9zPTshZ znw?+sh>=6+gt3gv32II=OXmR@Xf(olU3DD@Ee#V!&Hl3YQ+9 z4}9$~Jr_pyT{nmLbO{(ti}iY3U29*wzIV(+ke{tpu)>Y-T>u00hV>HGU+c?<-`XpD zeg;XE+vv}`LZVvnzBar9p()&_#YzyD)vjHn!jzkGV#@qH(b+e(bXE$1oKW8Ln9tTQ zT=d|}Bi{bHx>Fq715PzMNq&aQaJx;}=8RF%Fa2_r*Q3Faei){{)rMqf&SKV(+^)4B z86;z8&f!YRe--p5)iW_{B<2lQ{yhb(WQUB%izW9qJFCQsoI0{y0JBJV!z%o$1%E3w zUdLl#u{B_5F;MReB@#*G$YOrFy0vL!M&-?DIG2In(3Ix>{aDct!$*KnoMGeHRKYlS zzH*Qrg!TDJAYK$CmDvSdKkCSpxo*Ezu2clWe)Kqq_SU~RjnnX%J%*W}DC$lh?CUdT zzBVRI73Nk9XlZiTm{4QJ5Lrdg+{gHCZF-s$sgR3XgQ@PqVDw@vB!iS)&k8Qp#R^W{ zVQ>ptUlq?vXMg#P^C20-j=#hgrw%?!XcR^t;wNWc)dAT~*zhO`S9F_rFw8X!vIIUB zPKwS=H)x!kJuGrDpw#kOmEcUI6o%1L2*y_k{dL3ws5Y_|<5a%0m_F?}Q#xlRUECUoZBp z&xRX5J9IDl{SRIskzdeh~J=KoT$uUC;a;$R*PRP$n7x8;Ymy+-tH;LfQr}LHWbck9^`I>O`vh@t zgw|JLk(Tu^gZvAz`y<3gpYL+65yuVXd_FnJ5+Vu}U}p)~w$u7f5`Co176jTgb!nE| zV7;S1V{(iFb*&GLDK`dJ9HNMmnUX!}aGqvXct~ye)KkiAI(fy>5|UkZ>2zisjKzS9 zxsPf^q&TgPD29boYt7@0IKMXD)tR{44kl*DC{dh1LtNXjTmQy$#;0Ldj5~(8XYsOA zt82MojwN8q_Soz#vg#Q0CP8Ob0jd|tB7^ba>JXB0(&(Krtk^|hEG7KnKlp)=3ZdH* zgS&V4l^;t)KNcZi`+m54`W?4c3CETb>HmZ^R~*z1V(S?^qeSe^MTHtpUe%(8*MiTL ztE6@*#UjLlO^Z>-w&T4Z3wGoD84gSR~HaeZu< z+GgP2SgAwG(nKHanLNJ6%KiN=F24e65<1Xxv)6@9z6v?a_$=&}*Yii(-j~)Woq+^azOgc)eLky3dh#c{4-M!_ImTnIE%;Q_EzR z7m(rJCrkQmVp|8d38!bWi`h}migsqQU!4GzeX4a^=7ZE4F&qW{>)O};u&L19 z7MeH63UjvpF;uS?!nB9NI^@qTzUX+1^2303J%Fjw6c7Zyb>6-UHL6NzP<=QyJnWzQ z@Mg9&@-%SYryHLX!rXp1KLov`B{#u>5t(>Y=Z0ZWqE3L+I+0<*(7#4Fa%j$?XxAgL zB{|0DhD4l&2|sI-hX@KB8vf9Nlbr z@*{Q-JzzV7zs~I~=lr0GvGK2)_7iG6iv|JWYY#?#9S0%X6>Km-v?f2s$G%_eiJ5=j+JDO#Wy?xg{0c#uSbN4Fk2l6*WiJpb3K zZ$4A0XJ^%dTLn(A!t%cE^T3&^T%e4Yim$0t?Y4E4W3$ z#HPP}hS8Y6&}7lmDp(6ZP_1r_(8hgt2QYFsR+ZPJx|2dA9bb#qO=^codLLL7mi8hJ zJ%v_lJInN=6E|HfH`rvNCon|s!#xy?1ej;pd6&gN z;-R>3pOmvMcdon^m9jgPC8896(VJt%>1(g=y-dSLEz6A%DvT=4zkB=Qm{);cr?zf# zyxY}d+ic8ap!V~X+@0?`5BH595`6ebo3(76w9(nr$Jd{qd`W8KmS$wB;!!!y17WG4 z!v{aLGoZvsfD}fLEZ9@~&Zu$=VYfT07zh=;nQz&7nut&e*32`i2)=x~%%jKw)T=f$%1Zb+r43$9Gh_Jz37*JpCX&*Gg-jV_wqJxPpQ~U`7$X zw?&po$#45q{3_~RmAoA#uVe4voF3j|of}%lwJV3fjaRRcbpKniXtvne+9W%goAHsg zX~fNVZpKs_i%n}T#`FX$V2dX(!cEnuDorQcNG-7 zhEFfB7@!aX0A1_!b3=PL2P*asbNBcSlCZY-)BzwfaRYQa4)W9(QegHJumm}1Q;y;i%Pl^2~~`k zRU!3jH?89t@7ti|a;@0^5$hpQ^3KFIU-DecHeK-P z*rFFEKWW=XFJ7_kHMKR)TQK)R$ybqepCt(zpsD{KV{aah)$;|8Kh}`!p+feZ6qRf} z_BA1ut+FpgQIb$SWDhASNqT6LD20+#k1e!HWN9J$7Ah63zjNn4*Yf^;e&5&k{Bh^b z%$YMYXU;iuX70Un=|5~!JF_18eN|L(bgX$Fs@+vHpt57l&|V|w1&6S`(hPVi=$p*B zD@^CfmcV4r-F6QWoe4S%xT#8NA96TWQ$T9WQegxIylx)9Nr+JBTQCOzWnE*j=jJ%A zd;0`yGcCPNJG(;S!*`b2(+G`v1^fQMMXIK@!-j9G(%|-Y9L=|Q^&fJRr_LviGjGa2 zdg#JcV}l;xr(-JEC+AhDBFbK8>eS2ir*(A6r}E8eGxFcS%ue28g=MD250p(A>2s#B zeO)(grq6lx(^nyU*S9EX1*$Y7yBvjZv2r5XC~slWr*9t-^1_d4yVCcvm3>8(m-v}= z_}utGE&=Yby=Jr*@L30fuynWY6O>3%f14A2-?*4sr)wmYh|n&z2tD;cB20ULE!BDr z=czTtr;M15rC(7DWakR|Xh_}ELVfXwq$8(?l?=r55WNUKJ3h=XOLMs&GdJ;W7LgOG zo!)it+WX~MkxN)ttI5=Y=w+44tq2-vv0B~zBR>?TmCwDrJ}sudpYTb52V3bd?tMh2 z`yS)av(UBY)4v25u$tam_#|0}i^*CW)uBCpbGbOwK?hoi6)uxR0{G+Cmdr-XRCE*q z7M_v+8(1e;seku2Bgvd3VN;qQ$Ye{BB(yavMap4+dUo z~6^#v`Zso0Qj%jUNnsQRYa?7<9 zWZU!@!*yNW>e3wuS@|UxC9{7PK{mi1>2gW1c(j5m24GsL&3_PvuaPYZ1)9ZCxMGEa zHS2$QN!qVdoU!I4kJkQB2ru#M9h(MQkvGl-j&(-Qs}h?5dqT?C&fENanmt-EzM}EV zC!j-nE~HOI^r*S(eJTZjJD+c%n&2|XXDY}ET?R7e%7A5_0XG>Uz@@vNpHK+vSp?#x zvTT!~(D3#7zfXf9DXRQT&w)VvsXHo;r(E%c$ZPJV5XL%%hMN}Hc$cSbP%)1>y|cG*?#XQe}b($-d>q_VBO+)f1E=xxAG{g3NZ53k@Nf3Cp13%<} zvNaWi^xsCXR$af^9Rfhhk~Y_Fzr(ZpUfeJ;gOs(+Xe7uU6}OZ}p(1&?Ss90@T4X6S z_&|35j;3S1hoVPr7IU;1S=C?LS$abbcZFzM1JClloYd~!6-J^8YD);H7j@ADbtQz= z8!13w7JdqzT`F&ay%$aSbZBrBuq?G;=E=^I=^#;n*?NJym8!>^1hAigyLNOErT8Ch zQWQN-8+&O^sBy(>FzSs-{~TX{5&@Weyf-E%Xk2UA0KGBjl>w!hpbR_5WY1|UWgE8& z8|b#U{gc$qA`FXl8`7XRnTvY;6&S)OwnpSAqi0_!C!)zwX3yAvSZv%29RA6c_-Oq7 zP+7zQotEcBb7n&a0nLa(x=T+Y8+`$1W7CHCC0+Q@fa`8B`^ynEX;!Uhh7t=21qi?BYL!kunLdgk{0+hVv1gsCXo_W zSfx)(Vee=w+dG1aq~w6T9;idIP1_d<9J;4P%!jm`SKSx8n@>DCZrY$Zph`g;Ode=D+-u6s}p9XuW~-z{P5IsxT!$CI2)f zT4KNH?H#OQ^|R36Tagjc_@kN4`>o)Sx}6}~0HfF&YN0w&me@BTX}m@yCmr*mQsl6| zXvpH6SH+JHrbO%mjHTFt^8n%}VZelCT8*9mf1z+7$R`I=l-2XVWiHDRW^$NOr>kn8VauKqCLDGn%;=b~k{zp=ZCp@!VTM(=FfThtIxkZ$s&|(}OU|z9g~{ z^==sHa1te&_7f(syK8_kT30G`_q4tK=;k&@DrSooRSN5|4CSP|RTO7_e)i{YTr(%r z4ZJtNkO18+zX$^&{NcX@$Wi_kti^~{jg&8)CXL-V_(%V21#qW%;>F2 zFs(Vi?cd^aH}jt-uZXWJa3Yav_kYY;P_U2G&{riR^bGv^{{gnkVC1q!oZ9~2Gs%MR z_T)W3mn?;EFd4OBS#;#DV#~fYlU%L|?R*mX-=Nfe!(WX=1bJ}6O4 z6b7VAF{t@^a9re?+ANu6ww-140(j7C^;jGGh>l0l{HgDWarwTpub-O2zyVKhbA;EN zzpRYu5!1-pjaGHsmr(eE`_gCc9FIc?3#Wpv<|@pg*gK~0u=mG2&v%kngnN!C`YAkO zzV8bnM=|{If_^sDHua}J%yKUA{uxCIKsiNdH7T`BHDDJS6EZJ$6h>@Jzypi4FkYt& zq|W5HxOH)h_{_zYNv?dGbz7a#SfUk4?{JO!dk=DoN=D!VrfLxb{o>kHBtuaHgL0FR zqeHT8(8j9eXEuqnbRv->3eOKKgO{&{H;o7&pMSs3M1yfVlu$1J^<}u!cg<`jMFM}! zjw3&vfVTEm*Eqf-3WHMIdjP#jW3jHkX!48s>aRiFeepveZbgSUKV^`#7Rk+=FH8Az z&{0N&!F{Jp-s$HrBt1}=BGsn+zKukTb$YOjGXr7>tz|bPKESk0F?x9T9Sry8ze~VS zRk)+dyXJpGPuZ@8soVxYlb6HLglts~S;N@$u%q5&WV+bHA-^Pz?9q*ExGOo8a`dGJ z%tDE>ElU>-@uIPzL}}fg4t-x*{0}?31wnTzotFbagZ&qW?&u8!P3$c9yQ}9i@Y*Go z%;Ar)LL~pBecHb)=k0ZZ^Nm7f4c!IGGcF^b6`p;w-?E3(^jy}d{l_34;XLktEol!l z=Z?*VyfB+ZaMaHfh55RLdn7sepJSRF9H8jWz>AtjjY^u8-A*dwbv6v0w8dYO8B3zI3f08bX*fC;Kq$`{x@X zK+t!^B%LBel4D6%g( z<|oN4iX6`v2O(f(ES}XWzzCEvpjSc!WabT+D~|)kz8zX(ufYAm#Zc;E|BmKPcS0UP zrX>;>Iqcww`5dby3Ai~#rD!b=A(J%^FwfVrRNWDL{>XIT&`=>k^nZp8_-{FA9ZD!$ zDrYJ4&Ub1g;~Wv@;5_yJS>!;hqlSg`w?LHXAY7(}y|;Q`e2x_SwA2AlAHEZ?G5+y}bC|j0 z*uDB&1~@Mr#opWE+X$q<+$homK}Ja|d}zlgIZHgX@=VyLg}r8of7GTQL^HXtV*)bH z-2j#?n)B@?6dX*`oh?4+?w4x6bz9czIEDjlzgnf9fZ7VHtAQA2r7iGcp^fM6>mE&A zGb8}a0Q>Xl8&T0L1#*u*5piD&wPSGqY@UXVZXj=8^|d@3UE1PI#>v5PvEZ-ObrQ#t zl&5pdl@u)8t}yR8jm=$ktE;kKzp?J=PP;G}A-ffK($`@>rseqpE=Ac9umAvMVv$3h zZ`JPRgidi9-7oxu648aMyBHemvH-f`v(>Xh{+xm=F-J><`Ik9WhV1(xSGvJLVokXBV5is@DUVZOFyea0k zC#I;9irA60VL@DLrIB$5M2dUnq1Djv-B@3Ne3nBTH(uX z%D?U7zs}fQ_UnJP?`7(L8cKV=sx3ffHc@SRT`2#^!Na!KoO4Qf!%y~jwJZ}Hd!~0s zLd-DTq-T5u`pirfsOX>^J`l z)Hc_`VRH1GWhHIaD|s$-m^HL0Jb!gMZ0MLx*zV1G$cjG9d8kKfirnQa`qLEIzZKEZjpy4KyVj;hj; zD39-OJUh0cKh8BC<9J8&mS>Q|tyh^miZHq|=rbmKBkR(%ZIEbz2# z$=nXxxBo)INmM(rmbfEvih?L<@`Ggn5WvngK)`d<{!uHAH6YvU$cmUNet1&pI0zMW zBG9*j?q84s+n=KnWh+j|F|Fm;K_@_Z>_!MAq*w{3GymKaROsL<1%`70`HZaMdjb37 z_I?Y%4gn7nSiHl#NgLeMde{ua3^wPun(#AfGj77p%ns&D{PjgYW&*Ivn?Txj*AX>C z!JS(rUR+{0+kg?lfp&MKh<`r&brIoG_Vg%uinO^WJb`Z=@Sr!O4~cO%(I=L%`9@s zKqQHI$cAAP7WReVj-XV&LABPKmOX=tkALtZ($I@FgP%)HU<<%83Q}s!a80IXLeRKH z!ZuHCnGLjUvXaX5Ia`6dR}xt1q{69~xaUJS!fgAPQF7$^=Ywx|!bCI=GP-N##pgwu zv40xRg~y@^y>fwqWha-jKQ2`h0z*W8eIVku>MyuMh584TYZhu%^w?M-{3NkP2QB*A8Xbj)N@)jCos6R`z$rkGU3(B zx3&64IRqK7e5@?i8@pECUP$V_bXvEq%0+1-BnAY{VxT+9_oXpBK{N(gkXFlej7zCD zN5qdF)3eL5=@e%wQKvVw^xW7ce2orxPHM1YzC^90%Z$1L5V zWhY@cUPOZZiR@mo&?&7#^V^0HUXCMXq~V;a9LHxd;qFQ@E-X3AL;o55=JcPLlYt?k<8Za$XE4@L3p#mMRmquu_yNix z$+_nL&3UnS3uW-~(@VGBP6m{)nBe=l+FS|cn&yh@XDed!hJkZ6o_SA~uN>yUIdH1v zasX`H(0aHq6V#%1QlIqCO$)+E()9rs_MafJ6J;>J7PXWB)`Y}l58=H4dY8eLwI3zu z)Wln{opkWnz3*BYXkHcPp~w&PulIa90-I$UyiLiZWL@eVv>;gGwmH(4IIpl-X_G6zUH7< z%(m4W>02I$<8GshMvCwG?|=GOzoN>ho3AW0c)-qj?<>A{;riE-`h6KV1-ZB$A|$i2 z(~6bL;ULCKtDS9)^1y14n-7AhD1G(Ui!8a*f8gifkylcS!OcTJJB~@-fhc61>1kN- zECG^>7RRBaE>)0HO8x4>_rMX?Gu$maD(}&`4$Ks%%d!>`4$PFy%d&~@91u3y_rgH46o|mR28+>5WWP zn|D>J?4V4Q@E!;!;EQ+gu5W;+3Secu0)?P*)4lXNEYLYJ!lz7+QzDuMKG2}y1;T?e zE`uw6_D^PPQ4GTlROc!R?~_A9L`8o!BTDrC4nIq!M<58K;r;BY0-2`U z<2(zL(m(<0@&dp9{y&P-TqK87Nz#I5&6mDZX%bhuX+Wn^IB%)h~YoMv*I?S~ZY5!H2 zp-$hOzF+u!^BT3$zGm#DpvP7u^&+zpzG@jvDM_@5(HgT~4AiI18kdBW`*NP9uc;SO z?hi7Wqh0wT;4V|d-CEA>F2_!;D&2y6T!&XD8+o9bEAo%PWFZ6lRFp4u)sYYXq$W4~ zWA(CtwdYgT#IiZg7H76gq!uXt@Q&Ki{;|i}o2!1d@Li5kfC<<02pSed5H7*NUw<8i z+8*u$bbyUJY7bwNvsoULfSzt_t{4eHQ?~6?l-$UP^Q(hg(w{S92s=IB&M=!xZ(KErPc2ymnJ>p z*7Sa-afveq^EiI|8dHMl6amEq-&uu7b~bo3Y^uH}ee~CvRSkzYk_Tz|P2Sue1nx1S zfyWEky|B|*r`)~h>G-`w{F{IAlR1nA?zi?4^B2NXtEWBXvGr78(HmkEVwRv$f_NNu zFzeGOw|N|@{T@EN>iq9+;F5a<;(b&6TdI(BK)EgtLLc$UEYyioi**tKRauygI*u3Z0$!mTA$Y zSL`Csr`M5uy7zW}<9+u^aY^wt!jj=_77>q@Gw!lY>F7eup}t`KmTvvv^Q5`djSnDs z2*kJHul^Ekqco5fUyL;DJv^cDKn$5)3i~opmCwgKk`j}*l<0cD?6K4Ek#V2>n>4)h zui?rFpM2AS<7`vHHVRdj<5p9`d6MLMn4V+?z(xqnd;tiE4cIB|ApOhzC?Yxj9dcj^ zKLx{OB|B6GCHA)fAH&tjR#FLD(DI0K!jG5_g_o$5Uk7*0?07yUc8awc#=3t&0b?#! za6Y6y-pif&;&u=rasEF*59f%aXI}$m?a~w0@(98u}XaY`}6+n`SGtLL9h`O z3|1Y5&Up;3ZL_f_cb_mrzlzQlf^lTkKkth%T4* zzj=GjrtLL~!5 znc4G_sjczz-+9V8yysEd8s#(QA!S+2Q316Of$0T)GE@Bt3<205b!VY#rNwAC(Q4ZE zynv`m_up4-hwU0er(bmXEwt_nKfF8isNUqeY6)@1JKn;lz(#VU(AQ%gylq0w>;fA@ z`HFPy`Q^7B;C{R_AlLoCw$VMp{Oy6VA?rXIrZ|FhjF42&{G4ZN#_z-mW0#+KD>53o zJ+iu`?@?T-{ABxfZAs(yMtO7GgGG1+hDP8zU4s2$a>36?>YL&X+2ioK9p%`MXU|KC zu|b$qmCp>XkLj2hjKXRF;A!ecpFbs;8?!_!IzNIGe_DfSu6dY2Aaj3tTS9r18cV;6 zWAsN{SHfJ7v7^|;Qm7zmRCYFKxP|)jQtg-~^MzvV|G+aA?=C~VLF7p}moq{l@muT~ zxZctsG_kTyQ7s|99GsAX-aWIK=*~6BblYlt$H24hexanKL^Gr-M-{96zNNwV&dba! z{w&@cbj z%lPYdFvyhkD!@M@50IUmUELk zNAg=YTP~+d0x8#}tbXE-VQk?k0m<;LRR30|4J_$O9f`lChPds|6Se|KMZ~yjL`U}@ z>&S@r0cE76ox?j&w(7kq8C6x~`8@6;!?H?fV`S9XMITt&`FZ__-n7DlJw`rjdBd+n zN*AjmS%tj@9uHHr79A~W+rqaaXAR9OXFC6_GBe^BQ2m`w)~#KVwV$D^E5?AOw;*+0 zcZ>mV^!-$Md&F_ddj-v|1BtHUFy`_8mkfAZ&`u-P9cpv&vNmfhaX5-wXGwnM zozY#g`nRy}4|oi9>3HN`OH_;^W8|Hb?<~1Lpsp61N&SPgjt1ZY!C?Y|4`A?BX1PmZ z5fpcQXWAS9{pQT}HE8bJX232C6I1SAzmqZ0F%SAL?NJ$5E8Np2MVSdxS!tv-(Ka;k zMPvq8$dZNj^n4FSSm;7&n_-#ovi&M#Fk#bY=XB{vHow`)#YOkz;?FRO&lZC_z6=nN z#@E^^D{dOaP5-DCYo%8@4-1cwt77l%@N3;kpBLw2bVYP5le7wM8@4T0wW6=k`hvA| zm+&k#e%yB(LQiA2^t=bs1`F^}iMCu4lzgmp!NzTX9q4foroM{TV^eEZi@VF!gV;7= zUGiHe9;BRVwJ7miRkUmPc94>K_Tv+>R-BH5l`5zKpBwOgUYSzM(VWv|dg~~E@@ErW z*?1$mhA)oT*7?a9fBu6V18;l*YIhx!p`+6dW?@F&RlbcBcy|MbtGokqW46NfA0d2c4D0a{J~GnS0x{U1QW`3*(gf z_B&ZNohk$Ehe53ae}70(0WXH_w@rjPRPg&#_fP}w;zdWG(ypBn(2AE&wj|Gf$u}ZE zVlo%q+c+XkVzS-5x3Sfz;RVJO^bHnfJa6o4 zR}`ft5`PoVe7N5_4q?f@1wOW?}p_K%g`>QtipYg_BUq~YhenUBp(NVhWpuUjO^U3dg zDC6EoWmcxrP8=bse;llhvN+0%t%sLp+CxE_QhU5`j*%*GKH{({`@N2o0kJ5KM`EO+ zb9Wd*cltUUv_9$$D+0$u4FR)f zv!zX9_`cK81Cq`ANZj>pMr@T{J4NHCjMyUMt`D_M{ylJH={`cnl(==VI=OBB_Jl;RyL?^@OB`bkPF=>$d&#ev$OY z?E}hmX&*jrlCHW=$O)oThxRe}ZZJ^c@|9q^W^K^R6)4Zt#fKZa7j4kSLCeM+cWvMO z^(?mYSSR&&TN!E}iDHWFx`D(FRO1O)gt8TLp)9v{F|;Ua=ds=Msu5Y>wdQ>E5uJKi zK)vvG`fR_mW{x|LmBC}W*)!Rw?Az?pcyYfJn!&|@+J>Rbh14gkVhOS^Y4ET=6a&=*%wqi4v9zN; zMr{qz%3_7vo10fneiw~h1ER8l6KO@)KYv6nbxa>hye)u4ZTGfgOem2i%CK41>gvsN zU+Mz+VQc`Sw_WGa+$W-z*i6nbB5C;$%O(Y`2qq~6B%IU_S6FIZcp_c@kj2t{a^PP; zQT`O*4#gvqGZJDRuR$u<%zOHarSq9dDq ziX`)9i%*>RV-xt@;>dwbi^7 zJsoT~qPR9R>w7OL{(1XsI<w-43rcZf=KnR){yBlG|mnUoA zH5wK+-vItr;H`Sdr0)AzIs_*RZHkQP)*`dHZ;p(qY9}LXxcE#V#UY|($C+OW=$SH7 z10Zwhy}U!MY`>wc6B7=AXK)P^;0VvWVvwAAeVMcfRHqiVnIHyyFuVqlk7dNrhAbpE zHb-a(2MZm#l``>KHTwR-l+7HYePLog@5g3~p*z~9Y^aE&WdHtwKKrI8st)aUW7*S^ z&beAfEm+Ix;~Wf`<+g+bLqtuqX?VeIXZXB=rZ@#=L8Tg`u5AH}8RJl@s`b#R4WCMW zZ{AhD`A3A@&jDL@otmPfzpZyMggHkq%#Kg?j7=oEM6o=f;@egvjMfa40W(h29&}mt zK=)Wb!lUrUz_Ob8SKGUO(95=MQKJ3_)sQu%osukk(#ybPEL`spEA@H-9(sJfpsN3U z@mAaqCMaI}0n(woRhu5C6{-`gf|N_QYZzgJX+BtL;1~x=to;rXS>JY)r*iQ?f7BS^|?35zYQ z2M1tmKr}*J;krr~*A94Sw{8ea2A~waLo`V`!WmNMWZPDcD84LVXv)O9*0&+@a?G<8 zhI`ki7!69E2OF7Ee?fBLt!sZsW=+CY-vW13nKh?cwb=ft!pV^+v8-rZNa4>>&9vIL zi@HIa8}+cI@UB{)48qf(FzBm}|EnW8>3tQ+dWnh)JaUbYK*dN`VBrAY_JYN?M?0W; z&njz#Z!i{EZ?yLmrq@L(l{>bRp(!{wuUCGrXiIs>oly1QWS`wbk>rt=o`UB9?Iv2S z;rO(-uS_2wCk-7?xV|`gm-Oeu3dMra2aUYe;UZTkSYy`187DV&^@*o#h+=}p$~r^A z&a`*BwjY0pT@e65$kxQCl{;dTm&m2^W;Hsq97Jh9&Ez-2%9#AVz1WYQv&VtTpap+MtabVg>MC6 zz`L+%)YXp(8m#Hr)BcMpH9n*KenCg0MLD0#Quzq=%K@9B#t zHDR?Ay|~0$Rrhu>@hN}Wy>OjzAA$K)f!-?ZQ0KKwa$#7NLCc=gXD*9qvFO<~h;F1Q90%S=h>D^eq0z(H*px~Wke4yyRJ zM!_}HH_9&bzuuAeXIGwEAE~dlSghoEnGW-kC>)9290*dUy-;LA}^rl|IO5t zpzj0>?$CAK3#MOkPe_y;Au^B|vmBcb=0daZoUv}R`!355K9b1kBM2w^!nJE`U>>F& z^Mx2x{bUmhpp9iNdf*IYf>6x?|GuZ{XZ9GH89SyOY(?V^nRcVE^?0E;iAf>YHeR3W zG^;}T-c$y$tfoKRU3k|w`cR7nm zXKxbbtI@%A$dFL{LQkD1jcG6Nqzb5HYLi)$Zk;gFp2&anIfIB=n-qPnVugOS*#YTw8D5*^%-h z4hC+e^&BzO`4#-U;`GU}D`dZrf64hK^kQURwQp|wNSf&%?5{{acMHtxcQlb{ccz*pG6}mfaZRqP?8>2U5v~7Q#p@?LjK`e~v zU}kRfy_+ddfsb|g-&$x+CN#sHh<25trDbV(#|qBZs}e$CG0 z<1dYep?L7L^}YU_gjdu0_=E&+ehMuTLFYP5pX?5+?r~i_EqUcVgiF#G@TFTr`h_NT z_WhDt)kY)UK)Mt2%Yf-u&p>-YA6-P)eZ0IllHuijRil3=+zxmxKP-NYuV?qo z!+Xkf%x-;EK7Tmjt5QNi@Enb$7%a)Akx;HacC^7hxRZj|Z zB&QCG?&VsXeE)#!jQqq6+K$O`Mx-oI*mgrnID#u?VR?4Zn7=0~iY~(2lXRVN8Go>qX8PF6Kk&;Km7Di)oTHCjXchR&?yUiICiXjWeHM|dpM!@ocNlm4cABZ zywE)HK(`$xw_M3N?n#A${HjTrvl=ta!AQ998M63#Or7K7T$@^kh`PF*Rs0*wX#^Uk={l47}G~Px5 zt<>vPm)+-d>P#0<49gqnvxOf-_u<1m|BkjI{hXU7E9J;i!!mP&3efufxDN*Cng?HE$ zxwY(Li$gmd;__|f<)4;&@XU1Rfy7^iZB$3M2NGcm{c;@LZ`gs1i%4HfU@MXi5;+1- zO9H2P{w%_0chrKD^j~y>?zA`@f32`#Yht>5aOPUKc>R=&rjSh6dO45FnaPAB2xY96 zf}|xGN#*_PcaylhU$d&1oVrT)h#(?|E+iZ5@cp;db*? z^dD!#<0qBn)mm8Vx3o$ycO)zETKtM+?nqGqO{We_npB(nWh7lbNc(G}p3DEQZU(4bUi)TZ7lw* zkF59RaJBgJCJAMv)-0&EgW+seP5xdkaJTxF`es>2Q)CK-bkI*ly+8kIxM`JHb}GI1Az@E^c$}V1sZ7Z!&eVf9;pz zisxU~?>sa3Xy>K{JW|HHroa1a47qb$ypd&-Wa%)6J+A&h@7QkABzb$4I|Jk8^w_Yf zTxQ10sAlEAJ|H66T(`EL4p2I}^_I=kz6gc(Be>-Qj9qc&?$t1o=PL(HDX*_H96mc% zX~QTAh6LKUIe1nNNgUiu{_F>fuf2vyygd?D!|8TDGN<{rLfJCma8%*!g|_hj*ooqJ z402$#WE(qZn&_CMGp>7_~gd_iKOUfd=+_|Gup~C~3Uw zAnB6FamiJ|NPP)3A`~||#`dU-s&7Ht%4lY$vSPa=XZlGqRc$ORw%c#m0hN5$a;**q zr_DP=t@sALAcfflIjyuO*f*F9^`9P5&JGgc!#31(VsN{|IHIX{W#He%hUz4DKH*j{ zdI)ImQ}^t>0gO!SB}@W-tDWH!n2`#4YQ)36FmMZC1r0#? ztV7~#k9B8?n>xXHD?n4`|&bEHr1Ijl(zaSs&IJz3Fre z$_kGrJq^a&cbFZf91IVqxA{uz?PR;-ut4L*y|dVwczGQuQPGE-xx$*nXL+5RxeN2; z#gh|2k%mfbY*!#UG?U|el(ZlaMv5Z#VKl{Cfz=BbLcY&_>el_Fhc(V%`5cn@C4>1A z?B}%&Gnm*bn;>B|4#4RhW#uMt*aN`CmY1AVyU>sF&lnyn$?39Rs{Lv=i5quS8F6RX zhRy5G1wmC;>>MgKVg>$Bkk}ciu2Pvw(+^DAq@vGPneK#-Il`2@JZ@}?RF0ytkowo^ z3;$uih7A(kLdT1W&aBe@?zebfYqzEvF|29|pM3wdzJa9+K#`IkpOwCHHDn5Kz;e*n zPVVCX^xI{vEXse=4UsuuZn=LoYm>Xg=?AE*JeCnC(`=uuC-&>ZS)c?dJ}7zNVP=>Jt?qr_cI%hdWw(OJ z845K^dI9hM&TgWTXx!>GCEg`5N}iH0jH^=~m><>jU*hVo1caCVz`H2N@$R4zs)(#7 zIol%QPR-PmsXma0nzAL2-A9h2I7lMY{SBcFP}{Sk*w`paUglvm^eaN4aZnf54D8_0 zD=%R#6W%oI4`&HInzFSTF@jy}@XVVQQfPL2 zlYF)ay(Ko0g*kev^a#yi@!GM61rmfF(v$yp(CA?Lzcc2g9&lte_Wb;bZjO`)H@gUT+6BX$P>HR5XSfa@vZU3+60KAoNvAtd=-(#FHsk%R!hzVe5it@VTf0}!~;ct~jaEp9lu zsp1dHDqgj@6>q(;;2%#A&)-*k%hI!817|8sbh}FXTg;j=IANl~X}z*b@|56b2wkO4 zE&Ys$r3aPtxy~CU*R$&vOD>d?#uE)}U|5w{@v(tj18z>^1#=u1L<24kV~xT}I_k24 zl*2DK5i@6p^=g#wEd|Di^PUP!(f!L1SvD$oqU12Idt9U@7d!Dtq5z4dUa zTWl9=VMj7&`02pJmeOF(uyY#|Pi8~sD>TPYGDDFyA+KYH?wM_$0Ekds9@dmo{Zg~gk|S8_W^y0|XUO0<%X;qkh}BtOXv#YZ-9I>6iDPJ0kJ#?38b7CDn zqo(TLLY4F`@j+pRwmJ$s*UabahY~23ehD{&2m>x^%=yJZgKPpvMG*1?e>|h(jQOG? zQJfi-H+o2xp$>Gpl9hGyA18G!PP1*|^AoaqlTTV)`6n!ES#>}{{ZN*ppk0Xb# z;Ioo&3@K6fLOy5xenuzQBp|>4TfIfv**Q;Gp~nxu%=mob7d`llnhi-t5} z7=*s{(!OfMD1#5#8%XFKU<7t|cc-9^qXu9X+fSihky|T@;-#l$bV1hH&!Q}vHRTrr zxDqxFb$eXDWa?dK`)ocsK-lhthiE-B6&L9(8Vi^g#97NE|LU~ zfdJ~Z;2Y>QAxVE$cVSuST?YQFm*f@YEny{)T}u>oG7^${imVHiOSLFDc^HJ1GG=}v z3}H~K*SP*TOpv69UB|L?IKxpyIcp5g5uNcaqibrS66G7$H*E_8oZ)yUz+eF~Q77`)JYbl_(uu znE&-26bV^&z2~KbmMUU~xTa0|eRE)w*)?GQI_p{7nY&X>e8O#v+qrD06!KA%9ipyO zQY4eqopEIB@3=nTORy-3r*nMk>(N-t(&Du#fY`?&hf(0y8%}86%-g_bbICB%o|6->JK6BF3Xswr+*)!6_WSH^OzX()nu=bt)azBb)CX4|B zTFodX1Q91DBn>(qFNoj`k}}v^hjXK?$l|GE6X&0st)-EFLExXq!JY+zz@Av702$RB z!z`f22-HqJy+nol8w0zY3#CPD#f|Oq z7fPFXJV!GpWH#QP0LOq6TW-!ms+UBleH^F)L|gitL;7*yiVE2x$C$yoo*!}PPoPOTp-UJ~P9d%hMifp*VMt@$8> za7LjP>su=Ol8g#1j$Jvtd$D6<0WyD$d1EBz0>$n9iZM@=LX)%I>TekSQ+6yw$99Y4 z0>647+7=1ycipQ66eQLc9+%f#M2sFykh@b|+I)NtbMwO&_7=jB9xgxG&lns({4x2r z^46pj#5TNA>t9mvtm3@NSq|J@967aVOZiNbEHrO9|Lb$>w9>ro{AbLq5c;g*{9eS; z{w88Bc}R>fhcFB}*{ls5x~+Kiirq=|yKs&lc>IcA2UeCU0p7333L1c!9c_L(0-D8( z9f2`;xz)}6Jl}-eP%Gtq&WBEZgF?FsrtLT5>aUA7tUm_*^z;wfpA#JK){UVPAeeY6 z(HJy62Cy$buJ zQ~oZ@*If+rUAI(@-yoZzGRaS?A{k8MK6+lx{bcmsubrfSGF~B1BW4xDDHLkFNS!Z& zW_{H`)hxf)mo^^khuNLll#O^oVR=V1r-7dk2#5X0o&0c{$o>m7wIo!MK*B-_8RLjq z0W4A)Kcc3ST$o|1YLq^+VcG8ha@cYg(WP z&XJFt8t0k67>yXJ3gl%wSEsQqAFjp1q6)_v5f1)tpBzetEen$~TSvnS1 z;Wyc0xnwgUirzfLxWF>4(rUj2kZBczVO+o%ApvDZ4Z ziSeeY&<6ogg!ksVH6mX`jB_=*TIc3=yuQcC%is{<`;k#uj={nIRV9-$b>)4b#kL$0 z;1zm^75&2bR>wKpaXO)yfWr0X;bk>uREIY=n zJR`jFMO6?6@XN!1cil}#qC(SUOWndD*c$vEn~mrr@l!?$Y^eg3`Md{3TZ0~E32)+R6RRR`W8a~F!$$DK zPzzhjke_8+3g-g zi3ct)fOTu|C2+Ivr^60hMj)HBKuMY7AQr*4vATAXv(I16#yy}X;V#(Jyrl2!8c}|hjHY|LV z%A=s47Q_W7iq1ge7knfkVf#wZ{I1xNuX>%0Rhlo?fC>A-Ym+!fNTCb9uRsJ3?Pc-` zL?S{n+J}?c{bpDah9P2aUFgWP(vcI^$I3-P5XRrLK0UxOlKsjEA_O=lcKI8l;*_j! zFkmTnmk1HhOK1a^EJM$Oe_3>h>{In>#OWQSh7qDzAsi=>=pl7Rn!GA z=|EoC2CRFrJ*X{&NFu`BKg;}`fAQUay=bpqCn7fy^Do?x%r7lm;R*(f%MD@L9G4&o zFEYs_m)PaWFR{1{A-;%$+_>HgRmU5JVLkvzlmx4F>4yU67|ZSrh#pF&QZ3Shmc6w` z0zg`^3J(usn+~=AuI>9CCk49Rh`)A#3g8!thYu06kJSc=va%RBu^Lov6ierGKw`>M z^8#DP_2Kgw!hf3xHIKAKU-5h3mIn%3qOAIZw!Bk?G5@w9?)rY9mq-D> zG`k8cqL!eH0=}`0DA4MiQdc#>$Zeh0uP{3`35nl>PoKj43V^0Zq0OLoMck5GlM5~_ zTdkXXR0!8YXvw}uq_~HMTM+xnx%}(L1i)fVKq-o(4Ty}|_oM6DMC*GDyguiqe{@kE z^Id<;A(ZUzb;W$BQ>YryQ3n}WS4-b1H+wG{6za%&k*}A|X*eg3L4S?KOm=3H8gE#*u zd-N_n+NIsn)BeWZymW0`$4S%LP#tHt4=Pt5rMGJZci#5UHV2_Xkji;@tpvUj;16Jm zi3O(Wyq-7vcjZ3?&U?(f#y@BW(*X{*&eEqDJW5UtlCI%#7S%Xfry99@#B-e?7lQ&{ zd~%WdkN%6&@r!Qd9_R~s_n_Vt8g6uoN6O7<<%uK%woTno={AooeOqBzcZWauCq>Xl z;kY1(9)i#ZP3X8<3S~og0xt02Q2&v<5r(S@^nb_KyGar-w+!EdDV~-`aL6H_Pdh00 z_Ti;9_N(=Jqqf5|;4$)Xa3SxyMg@U)HRXn5tlS^{^}ntikMjN(5V&TEGJ}~Q+nYEp zx!2}`Y;DK6R4-N!Ab*sS&x))RIpz==ENW@Kv7ai%tI2Jvl`0Eh zHQ-;Wjwsqhb9CX46;hu|?C$-NTV0s&oZ*7})|IQZLNa!^ZC!aP^Yk_5U76gm9=YkK z$y0yYeTo(y|Gw}VlW`O-0R20!R;~S1HgZ1(dLz+BQPfP&1{ZXq5qw?Bk|9XcI&~#;oxv^=+tYl+aCQlth=khTc%mKOoiR99*Ww+FII?8>YUO^=IF^ zJwNR!;S?z*2v=SW+w%af*B?K;9Ztu<1Kl{KWz%~j zuaMqMN$NrF>DN#I7WcqN3MSHw%js=L0~OuZA*4eBK&{GXUqHCM2&HV zeK+hx8;`#803|P86L_S>3fr_gNn4QZsB{7*ZrT=1l76=)3prBRoVCsb_)ZHw`ul`+ z)w_ewiCaX zF7Y2wJX_M1R_Qd4;%tQWbq!krNCCiTsmIo($Qsb{{d6}e-NYV(UMl)6si{_Qc6rqL|ugtA`~jFl8BiuNTU}+!QZ{I{Gp>-&o_zCsS))`#KHb2H zBA_GqL^?4WCoc24ZutWwEJ1HS5~$TX6Nhqp=YdBQ$mUAh^3V|{B?mbM@mU#9l}1fQ zN~*cCvZ!n+-wBdizsv0>M2D2SU$S7k5`*MGS@bmUrT0^XvQOV5GL7D~r0<%4cuBZ_ z3!+p{c??)n>3)A66E|W_rElNSRHioTVpdol(xv6H<@0alut}d-iqae|&ZLA;qgn+T zanG{2F4rTp3&O=goL9st)}pG0mx+a4K_%C7zXr9viaxLw0rg7hjG4ihY(L!N1OTXt4! z%tAc=?h0yOFtkr6pcllk!;RRzQ!Eoba#Pl@_I{PfS?kHoKIOW%1JJIoMN0y=nBapC z;~=38G>2NATp2juNt4zqx_lo~QexAqzYCjhImi3~{OK=)qJxy*tQ;4E`}KiQr=c0h z6fgD@etz)Tve%jEkL`^rmj$bgP}2#`G4S)lWjbq_7aO5ww?NCEbYS>fsBSB6zB?rG zYQU%5q{brG2&#r_Chd zici@Xr(KtU_#Yr{8fAxM91AVe)f?RXj1Ov{fd(j&C~rjf&FI$3VQgm6I^32yBX`-i z?0Hf#oqhMo?VAAyBA%}ujde~qjydGAFgD^t|r%ra^VyzPuLdL&!=j@C}bb;danfE zT<{jYaG}`GdzPJG3UNWFfus$On~}JFGZubX?@g!m_M_May>{!`i`~PXu<|oH3l(WV zvO!)b1b5P`EZ3q#%cMNLiCRMJGEx%fapHn54ddVB;0o=?MIm}#pg_)N%n*A1M2lR^ z+JP|6SBKIgZ)SuwCyOvi>&8s~uk410w0J1ON36kS|=brOI)|uYu6L>!0FF zDpGpJvwBms8glTLcxx+Nz`{3d@YdF(fCF=Jt<`pO4*4u*DLYT}^5o1W!%$r_!K>`G zucu&6gG$AAB%nkgx|C+ufQTpfXF&BmTLB(KnpNwh&Jq-Lty=fQ-jEvSgt zRQAcDnQ?1j8fFv?Qr&L6Xyah9ZqUHuBDCFQPd#1UL6A&BK~k_m<1&-jwD^iUT!26GWDgZ=NBnuM*2r8;XJAAR@)q3 zvmcEENYS0SLQ0KI@xid*By1+tiy`X}6iPNqU6g~1G!{e9nf1((n}i>{hm1Vir_^^N_?0)Zj~AR_p4sX z0t1e1Tx>61H;hAREDJhpQ|!SK4Dz3L*jVoc&z5s&Q?5qkZ1CroY>B4&?p6vUx*zQc zSvDmCk(ci$t=v|!HZpOw!_z;jLw9Z7aNRQ^Iy6Q&Wjy4>m|B@bx8d%ny|ctJI%Z{7 z0g=&1+!b2_jGjCX3Hl;|n}b3R*p~trepG!g<9$L!UtZlX>Of=W~`#*OHF#m$xR-*U`tpvheC4KR|KT-(_dKvon*NjD<8dRQt-Xb)pnG5vv zT$UfndbLuln}>bIS>7m2{DlpG{X(c0p)cKj=02IBt5VbR?XX z*yL5}z_$!(9ZhG!0>=*zl&`-t1`Q=DBY!?Yfw5kjjs&!L)BM7VAx+6cT{D3{hk47` zrNi4s&f{H9bF}%SW1Y~Q(u_7uxKi#eY`*ZvZqWR#j8w!%kchzLi3I7@EKQz?!~i)R z5mT>3A}~(X^E+o2FMSvcl;Cb@TC-x4o1Sv4b$c;b^i=luIyAGNTgA55vFl>v_S>&t z{YO+tg((nZfaB?0l_}A=4f{v|MvAq^E*IH*p)>RlMivf=_P7HQI&=_2Sv+=41MjQf zfZ~TLyc{b8<6fOLvu8dc{u?1y`Pl}W+y;RJW%XY+g$)7&@_SaiOVvUfRj-b7Qvkv#fQ`TTrNAIhH0%Wj~UO-!n62>SpOjt>DwXOMQd+4ZJx(keS zsTi#U{-ebFk*H54*f_Aa=BG2G;F%ZXu7--$Xcb(%D&UWP z_60BaH?>&_!4X9ky>D(?P7-4mf3z#5il8rR5VQNpZ@BG&0%>y}FrLTWsfYy#-3eI= zYjb}TMbSfasQd;ZkpDuHqu_a*n-dW~32_p^=l@So+iTM9v%hRgw-Fp>torUt> zzo1T_rCL3h2MHB4W7(!tuuq`?P)F4=lViDLDbVNr=~(Ch9k8wyWY37+fmV}U&Ac;d zr=4Rb@?bGp%0B9?F0b(Y^h;gdAa?45FpB{GPe`Y-#3+y9lx>5)anY|)d-n3}1{P`w zA2ae$3^@1*bdn}DWCQdw237{PrW)^Ie`y=>bPd=1)bQGU-6b=dD+p;ePUb1g1RJDM zG7?gF1W%z=Jx##$6TNFKcZ&eSMh>6RM`Dm7?ulO@M3coe()UqJI8euLPrrva^2153 zC?XVYLf(8Rc!L$!gW)xm<=G|`NaO>;eh>wWbqVc!v=nn2l%iWNT;ezk#^`q9+B5D&&(k}mV~_N21Qp@FjLshE`5dIX3qtyk`^P9~*&iVmCk$Z&UX;1Ns?d)X01IUQM%Nw18V1>Wq_{}XD(#t zpMDAokh)RvhziI{g`||_0oRCI?U*^J8U$NQl+%gO%8+@X-Yk>e>ZrqQxAoeo;M&xV z1|FM+Gm@8#-$h_sNHn%6>w5247t}e91;)S#2}yPN-t1EZWkP|XUQ7+|`LQ4G5|T;h z;?RDZL%5LM-=UvvhpY11dHsvnm-DeYv?KNS%YZWh{^906Dji-QbcTThqZHD$>!35q zkH8=kx6yVjIsfpc>aFCxH=qSwd$1w+p#4ti)4y9>%q#v3Yuqip^&+!l@UzOOOYY?d z%0soqRb2-CUpzZVpE>yCcZXV*9C?3wSSwWDqi>nmN0#MdpdB-g+D72)QmpXf-EYJ2d5v zmqF9~V(*w$Fz=_0zQ__HZYaVM;l=?%vQUnpId~ft3gT1#l`;gD$?}w_=!dE10BHR;+ zBU`s@UwKa?0aD+U)!2_UNgECaP;I%XpqB9- zm~Cb5^!EC_HL)vfcHR6`dAcJ#WfSoFVc_jW9w`xKKG*S>>eMnYHTo*(gkjXQ8gZva zy}s@o-NG9;#PPl|t<2>Xk>4^KKZ2&>*A{H&QansLHr%*k?auE2J#5F-F@YK5bN zOL@{1^%W{pPrm51{(+HrWVyWvvdDw{LawOX5WJs3pBWxwYSzCV z6&}*mByKCjaSa*Tev@tjeCiSp7Rsla6DA(fs%A(q$YIC`yyC!y+nnmJZyJEFl{~wHJ*JToOrwGePYUpyEF6ClNhzl^f;bZ+!_9KjG$b7xj!!#$Y>7kr=>bL)V5>F{L`iS{^}uM!jMBd% z)nb`ok%Ox*iUEiByFoSJ#l+~^`ro=Slm2f9xhLMEg}FIkNCK}NR+<|qyPE2?7Oydb z83%%FXp(jqj5^m}j^b5c*A(3$BqpuW;CG$>|PkW6Z&HWXG#Xf)~kR`BQ2WE5! z+f=V}ReI2ap9VTG*N1i1EL3+^gU2MFVP}kp_SXVMnz{Xmh&*+JLdiak!pT*4KP3%KeRQ>ykMxsklkW(^iOzul1G*cwzo^9pGsX+pp z{;DpM?S$JB2CJ?)Y$t%qQ6KvP%Cm3DrJK)u0>%MeJoJ`bvO7dnLskNT{?H3@Wuts` zv-v?34Gxc4`~^7B5vP=jevvF)ORqE5BM`tRAY-+%b!*L&0QkTlCD;NTILdmf^f$&} z>7TbjjR53p4gV+9%jLQ2opQXSpW`<2j8A4w>3SQZ9dwBdq$snO+3e{^`bk{i9Lcid zbhi}gW_$@xH_E|O80!&j@`{1~3c&w1BC9$8F^kr}FU!s}M-`4@3a|Jbn`y-@K}D}00fo5^ZW^Y;_u^s%b8k+93lh7 zGMVhX1EK=OvZkMat*B89{|{T_2-`e2)8S=K7T|zLW47KWY;VYSFh}5MBWsVzve>L# zsd46EWslCAkRdsigAty;#m}{a+X*hrcNoXz2(8g!VT9L&DR#W%^={_n-|rLQB@!l@ z=;;F=rXW)*5xIB5=a8ivK=R{Y0td6LYLAVvfH1SIx}bxx07n@_?e^PN8%OVUjQ!g^ z0ySx!&kr}8B$}EI;VyyuUK8Z#yhrDnY;z>^y5o%C=#r?1z0asKh}Jmw^0Ug4r=G`SU4KobA+q0g75V9$1H_CNUR z6dAVxkIB7p)e2b86CT1`hp29Fo!;(N-*IIy_|%npCeuAa=ImL!Zc115yM=&ANOBW` zi>~E1Y#p7{!A2V8kz1-^jg-@s$Gg7WU7sITKn9BUIuVH*P2tewE88aHcV56zKkBys4GX@#?=HDtZaofyAvU&v(wk{DEng4HX?eU(iAh|?AD^2yWJv|* z6jspbI zFi&+-Ir5zm<^+7+>J4-{LxdK963-N zc7Ol9)~)x{Kc21h462~(uG;9%(!k%*N7Zv@i50L}`wST5Pp0xK>(Xxrx~~r;X_?TP zCzbz9i4$5gRml6qF5fV|I$m1ts7I*KKA9-~8_#jy?YS$sJG)qOO3V&!RFWbd0*xXo zGX22RL(CAHk?iu9y-9k^h?^H|n{ByTVG<6#h86Tele=QEG!&Yf>1wW+@=N6|_uf+P-gc)WFtuaM+cYo_0@*yWWFvU+Shd7LOC^v51Y*%&Z$CC3mLD zCZUf`l^Ai~fG7__ODu|59C4g1iN97XRPv^~<8RhHZZT4z(QGEpGQ40H)04X&&?0faq%RsAl@F79U#hU#<*S3vPZvN~8 zkfNk5-$sDNj#Bp_|w1Mn0{sgb^Q)M@QWAQ%87aS;M=zabo?tZ7+sUk%0=9 zAw%)ttp0+{37LV%U$Thr-{#C6GfwTBHjugbN94=iRvAXz}Be|GC!N^$769Hfpc402XXn{cm?}38+}N@AJ`;CU3HX5 z9$vn~%@XSf(0e~5Q0Nat!g8Rc@xPCzbl(jFw~~c2GHgVVuzc1RWmq(u11GzO+&`~+SFkvXqQd|eO8@(ViR)O`KZ*-7_!t@j7x z(+XXjy*VssJnS(;HH(n`ZEYy;K9Q-{Ie9j7^91Yu1AX2&X4+Pq;pF^T`&-cEsX=+P zt_S=Q@PI`zy_ zrY->uoIh++U)^S@@w2jLq-Vn%JTxnMjmh^X;B3 zWSb>_fTWg(Lv|u zFd=-7Ji9Dh9-V2HUeC34KFEZLhGLeQ#32@>+3L1+;ay=%Qz3Q0q&kI=t zM}m9OUdcQ(Rb_d)T|Gau<;ZW@ai)Fw#0nYkV`6W;?yLK3vU(@E7bRjUr;Da3*)*R) z;O6u9{g!?3EPvi6Cp!W7jZKyz-W-}fv&u3LmZimee@jexzNo;rn*D5ezSswrH#Ok} zEq>t(#bEik10ctGjVLX@qY5+4aOrag>^cfls?{_41!h89Gs|vG)>YK+ata1X%Ot(L zWq$tehYC^5WR1PaN0cIvf9<^d8uRBfk1njBlW%RajDql_WM3$aAn7CW<`vk9)_oM( zw<6%`Ci2hAP%_iOo$7u*grHNQ>v*5(&4i>(*+N=%=tSrd-D`)TWzWX(t{iFEPk?=7 zC&uoUa^+!caUh#}4{zjtj`Ce`6`u3@j6X&eP9RT00r>6>=~r<$$8;AF1Q_;cV|1oU zZki*P2rNj3e{NT<5`INOF(_PY28eJ-_anAq0)|*X163ZC_m9` zmp2PspmSF~#aP_b>Kmqfign6#`);Al&s=kb2szGIohR-k>1;Jc1aA>a_#z8vzhP`c zzS0WNb(5y&1hHS3kM7>AazYQ!#OvB5M8fUn3&dEJN8$qG-%ir;KK#ScnQ&ksb-m-Cr%p0AemutWL52}RkB?Da}Mm`gO7kN zXzDrS?};c#opW;_uh|M>?8&$f?+x(Ae{?W)1X7k2t;i0#=i5G7g(V4T^Xb~-{#~IY z=Re}e|6K#4CDGEHgFo0B_d@Mu6tn1h7B;qJL<>lH+5U*Z!USNw5t{s;2clq_U8=$U zPk!c4!2wxFEJv{lkaq?J8Fu`cUE~o8WKG-A;#_|40Vt5@xE18ucEnvZn{np=qx26@ z4UlJQ%Y}E@s4kMBc1=J-d0k5}xe4M@hljt!a1->Uj@8?VES61i{x=+6EJMx`r~Vok z_`u-foTo}0D07X4~2W$ljp3P+|7gihzS-w@4pOyJ;3Ro2sdTD>W{p!$o)vL$f zXh(;K=_Qn58#nTHX?l*TtQu0x{f7$y@_wNb~at_n9T~EpltfyqU<%A59aIxyTBaUh+C z_Qsbl!U%mCojgZ=^Z@0al$e6(Nrg%Q;p)`55Gx)wl~gL*UpdFQ=Gvb>ghKk!+wp4* zxeMw27UG{Q{?S_S8+6alln?yFv<4^;&*1;BWu}vWd5imvadp@D-q5i_d6#auh~^y4 z|B~cfEY16nQ4SBP2=czT*b)V$mibD}?|iZ8>&$E3)Uw_TDwXZXeyc?G*j;I-U#y~a zjL3uCfx`PqR6T*85;m37GpZ5Y;*r#W=$4EYv|9cgm^-AtQleA#FCrn(ZzoKH3dutW z1~sEU3Wk^W)Bo8UcEf*-xS2?+TN8(pFZBPGF5A=8tP)V;{{V>y%V`XxRyTOrs2qcx z2qLcrt_>hRy*DF=!QKK)ZR#ULi>q68N;|KB)EP;84(quS@PR?Glmk|Gti&e^ax$1` zmJ}%!3Ox>{ifadX9Mi*QxMxNPNn<{g?Q+%y%H2bN9mji^-2n@@3-pDOKjvox292E1 z`E}J6gbG1-$67R1LjL3puvu864rHBZI0e_S%M@po3vO6JP{6mi3o&*_pIb_5Q(uvt zA*30ff=;16hjhah>VF*65PC%5_4>xcSo{SYT(OoFMr?8#n|5<1TXG)?3tt>@QgksY zpJ>By@cfE}VfeM>XJ7Qx;KzDA3;J-_6uls~V38MkVR`W0v4o5n`-5|5H~Qw3S9W)v zqT>|t_P+QmIE8)U+eKx!o10V#JXdV*AF}85=N-1Yf5=a$>Pul%DE8JJH~QZ_S|h~o z{>fMDxeQ8Pk018>@gcC`ZkYQWhi6Ao#%-Jr0-6x=T12?|Zy&@pJ0jR42G}ACm_w~#9R=0@SC(xsrM73+7!#|%Jo}DqLHH{^n ziljX%J}sY&o?bWZ^sL@YXl>gWnZ)pvwY9xIQh>z*3#|jUZHvB*@=(2lua zRG>&zn+H7vC}b->c@4}U*@Dg=><_@_SjWNRMgzLLL~)+bY&eLiaPYGTf}%&I%KJX& zgzxkL#P4CA8J(cskf6xmcxd{v^CcUy10}Wzl})1E)j2_rTOUwmC7n5sOB{mx@Zw22 z$BQbbUcXqy;99DFN+N;R&7V`wuBIh*`&}NtJbTmgr_&GBH5PqzU3P?LNt$Q@gd!emI*@^SKiykeJ5XxKbVMjpk8I!&-OZjC0db3G|(;{Au4* z8R$E?`S~yF0!8xs6aOeDRF>w&MjD0!`H~yy68p77i0UCgx-0+uPTfB>!Yv@Mrb>1G zlc}I)14hXg*|h|=I@tW4igQPFi!M~&O4jyxc|GY^n7moy{`)DrsMmu6j{O{V3O?^E zh2RoOdw-b3hPJz*7k>{X3M&cgBI_v$3WWrf$asBd@fLr#{;QpnD6f=x)zAF=|hIdLHVNg5yiTC?Vl{(UdGrr!;C{y%BU{0j6s?59EXj&D0LyI$81g8vW9 z+#W)&8DXVTWesL!R_Lsr)X+e!tJ+9@6vqDcMBiK z{=LSqzL}}Q7dey_Ufn0!?h(<+qQoFKt?dj~XV@;WD$8V-*xZ$e%U2gjT!a=qGqV?) zozelLh67PDNGQwvr9qgmH1*WgJJcNIi=~~&@{&&#MM=&HxX1+}V8o>9@B4J)=~WcM zgbej6%_td{m2xJ>xAGX5m1`lX0J3Rp^mki~bn7AzdA?qTm(bVY0L_FnZ&VZ@Evx*l9zte#3uN4>o)4cA#XaC&JJaByE=VIzL=% zQqW9gA^u;r*ob^AAF=q+QviO+l+0TyiN6N4;lM8C6Z$mSV}x12h|$Q zN3qEQtS!p1L5Er2d2m`J%bnYGGJKspJj~I6S(AwrGn^8dK9ri+CEF#MKau{5^c}i% z=~7a}c_>k-4VHM)?1HNbic7hhjIz;TuHy6y_wpFEDuV|HUqAq}+F&B;g(eBYP{t$T zSP_9TQXSneOGnvHiPm^~EGV*;60IXqEo1!v9Qq8b&>ZB2_v*jeS_=%a@X`VE(F#C-^E&f@)PCwqdfyDG zsNrro)&nI{F@8aItXspvJX&3y{Uk*|oFimv;xT)UwY86{Pi#`A4+38&ettBoTL|nJ zfFLJkvS98vs|3p8b_+@Z8L^QF6f%~p1C{yyurcCw?Ic(26exf?pQ%5KaASJJw&dskH>uOs1Fd2c_Z3$U|7NdZWEwPSXi&0o8h4^rqmjG$jxN+4TJcE4}7gFxLBoKgRBl)cVnJ`7f z_bFfdE1*baQh!d~cRJtW>6_>NnMI`Rn2BuOp)>kYy;ibok8m1-<)l)!e&5wtHOV%# zF<0R7&-?BH%qJuHFIB2MH?qY(*hq3u{Hr7Id~+2@?tlcgBgdwx4 z%qZi>q+AR7aU*Kk-en_N)>}~3BW~9t`OusG!3AGvQk64bs2H(WG$3uKeTWMjua6a) z>8?mymnDSO=^s#DrM3)1O8LOG3oJ8vielSHwiNnjLTf9)bIE3+6IV9u!E@7GQE0h}gufihXGi zg-^pV?R9JH58i5_066QQJQ$(($sNLa$F}U)wDI~Q zFFHPbm7n#jd%g@Ac)e(3J@u`(Oz>erGUxT(1g#|b=$yk(4rhfob+;JVF1=sM{u-u( z_Y1CaQ|VkmRqyXMppq-LDdXS*c=N!>^}eEJoE~}JRhloQ#GWOwp(6{dk;=69-l#)) zgbRP#cV*D)a87K{4{ia$Z~xqAM{zyV#jJWvItVSc`JyiR=TVevS<-VtHFGI}pDtc~ zilv+&M8~QP-J4K2RaX1^ty|`sPbUIZQ+jspJron9XtLn~rXI8wA)!=0Dl$0=HP#4Z znShBAOTM;P_}{a%PgjY~(()5{e%37F-Tyz(_z)OJp_gN=JQ+5^0y_lei`1w0?8&F? ziW$}2iCkxCBW6VRBzm2tClSS_$YM4EcEhMK>7d1=1_Ar07wF!)NVf4yLm+5l zQ=nO|b?^72$+Z8Y0A#_i1PUJ-vn!!BX{8UjHDRk0SO$)N0aknA+v&`?c~e3htlVZ5DV}h7)UaZ$O)AN zsTdYzeEkbG-IT`jd3l~!H9^R@2iF~lM%9^eKaF)mV57Fh4P>WeEB4(E(uI>qgC<4v zddYXZc!gf_s8;?2Vej|NT6}&_MD`9J*Wzm`)&M)s+6pRslL&pL zw{~wC!ii`ltH5ZiuO=A1clJBvl36%M9sb1n+AFSU{kGxnrPS^Gi-SuF{FLU!**=4r z1?u|lAlUo^2pk8M_yY$xx6k+xqA0+u{+`0)if1IH$IApaIf>rEY6KO z#p?FsO8P;aa?0J{-{@Qb4I5cTV&}`ObY|6XvSa3hBjCH6`5y$xL+ zdG(4Wv8m+H!oeF=fjcnKxaEh%6*ti*_rY~~(-?t-lr2)a#R>-b&K>E(9w6F&mJ{m1j~g2*&_%(3PhiYc{rz?QM~FhzMUxl~<-IYyy+aAULiZ3B-2m)IK*c=g1%!`i-yaFpQHxBkEcca1K&7xm1>#WCk;r-^k zuSE&_n}5XK^_b9mz;z#@80uAI_TYv**V7C1Mmv4NnYsxpOr!ee6S(PCnMMUKK&rdo zF6-^4R5#gDq?-L-&K?zEnjuZa%&W(zf6Q#*=M-S41$m}s#=+jV>~d>~hG^tE=0_^K zelkB}VLfB6Y~07`)qAAKyFC|Z>mrftQ93-Fa9W8?HQK6;$sFFg67mGX|A1A~9tB<- zvF7tlDN0DHlna*BGEsLTb_4$#5L^LU<+%@dE$R!*5%u0*lY1xD02`$rTfdK27MA4| zD(!EZ+|T*;t@g#d1#%+H@qhp>l!V{#mXR{aJ8mh7|^B?|i3HHph3TzupmE^mZ>S1Ty?-8F`fW$d71-bus6 zk`rlOyxV80GI?Zw<$0pLLi5Q5_t!P+Kk#cdLd4B6?}PiB6$jGoDi&$esIaL%y=clF z&BSKEy(^xb0ebCu>K2sc4B#_}(c4ktX=V?`lV@B1SIb+-Z*_Fuj(+mhG}rq%e@#Lg z>{;})XHkA0*A+v(phnH_FoR6Z3rW*rE^bd~O%XPjqT@|sI6Gwb9L)?28$1_@jw6QC zbVz}cx7Z8Fw0X93b5{CZ<_n?jI2kPMpB@xWYWz zDQj+gev`_%S?24Ry#hMQw8qHpk5G1~4F;4q@-R3CFbD*`VKkF;c5(?%ijwk2cI`@d zd-`U=>NR8Q+TMX01AO<82KQ%r;Jy|FBA)ld>7G>av+HO-5&QM3xw+M=gG(7n_P-Bs+?cECew!o$C-r`yl@<1JTcqMCvC@xRI;*7<#Gbbv3tp`-ow#M64-AA%Fs@jI zPm&SDzwjAGIdLDFh+wp|?DwUYmGb){~v z$?g@!BXO%Iy^p^ADuh-O6aEI~&|xQAz=5Mx8b$>melXvu{1?Zb&T4vdE_t1pQvPS6 z%4TK|Sh^S_iE-lGB`cl~({?r}(^4WKzU}P7EEZpUFYc6h%Jtax_Z5&!qqlk$w**Cg zB=6)pcN2P*s-JM&`}Y+wiQu6XLzHcxq{CS~%J$Imk&BVr@nC=ug1w9n4N=Z#g7{^q z0455`w5L?d=h)cJ;|76xd*Zw@D87%P_3m2K?vnO=bU2%mIec@Cpk>4{rn9Ju00Hh} zngnj{WMQgSY!NL*RG9|c`{A!i{{_!!OyvH_4@|;BC(|~mizcXlBt;v&9_sqv>QJp} z{A+P?+x`F=1rkSEsHJivGiIrdA^%;rQS(|PU*c2VNPG1l>J13&P6fIUg4(;a^ih!w zf9UGBImbns?K>7@7vxBa-7HXh0uO6}0_E0S&rL#$q|md`Q$0_$!QxL8IofBVLPeC! zi}Wg3xi9ql(2r5Gxx*atCXRfwCZ?aBD_$HZ_BN<4sBcL5)iI+n`$M=Fl?ovAx@9*` zICSvX$#0bgdp}`slTt%7;H88|d7vsW@>sCIwxU0#K0uZUYw|*3VR%yDe z7?OgM@F0|Pxc;`u8p??bEIrfjrFos2b!5wKVuTae?cI;jajZJ`%b^et?+80HRje@^2B0WT!OF-!RM)XQK&;?d3(O3T|RK8G_loQ;I zeXzoBoh!jP(tv?4o~{8)=`+%tQ5h!*XBwrzbdqYrKm^|ra81SANuFy1J$xFvNSu<4 zXsS$4`C-&r;D|_S`*Zd9Wi*3q*tb{%%^@+y;WEy;=fxGL565-!YqT5=$J#A^YI^_q zx~phV+YKDX!n!P$0y;IbZOV)rVjVPXQzYs}%0h%iNVmi2d1yap6{b`CGoC-RZ z$ew>VQ!?-FW3Xp0rk@Uyl8~S3Cd|e(g)`gyO0Mqh_<8RGV#*O9uJ)NadQ2|DvuT3q z@bW9CP#*#LV!gK-{Wn=@u&?PbZemO-fWg>w8m)mHBrts-?!DQ1GnY`GY$#d5OV?db zHk5-j4aHe`51%el!Y_rx*j@)#Tlu@Mg$^u46z`K~Mx>YMERpUs$oTe+W@bq*<&yq< z1(B%!fz80s4UjakvmPM1Wxsw^0wJv-2$emJK%)RUw%OzI)A%>wM@yC*WD6BQ{fr>) zBY7Iw*2vMvMBO2UW;1BV#gvktN5?mrnY*l3}{nCeyeTb z877Y%dI5WW%E420Ee(qp_*vl0PueNzkLK2*s|OlBJxl${>H3klWT~a`9xiMg)IN)u zzxmiUu=Bxp-0nc#lk32I0aU9$@>P1)d}|-IXGb$3`KvzB)BALO^4Ca6OMpilI&?Op zrz8ugM`r$x#b=*du1z!=;L{yw3PB`~Gc`Y?N=3>gF$&J04q z{ADIL&JsdF@k0=3s-neg%(I9s%l0GE?TXw%g!$okO1W?&-Ri$=K*?xXy-C*8bFg^S z5ln~C=9e7SKZuR)Y7O6jW_AArc9cbY(TmrIpPlDsV6k0!ThfyHF(a-0TkjFOhf>!3 z%0qhW-gVF~v2>Q1Aiy;;+O~uuoZ20V7H^#hp}SbYwAQjGpJ^s%^W%rVO4qdVP3nMY zAviv>G{~b*1&&UunZZp|G3f_?vd%K_gv_>knuokPb;NLyo>L=MB&~J~85=<6% zk=v-PUCOV`Q(#HsC~pKGpd>QX(&^kjoICKB9)ZG*=q0$S{c7Z7P$+PV0c3`3tyYWV)@@>? z_uh!XeUO|D1g*U5my5VAss=U|_nB|by@t~*_$AfUykh&pO&RF^=#NM~yQw{3766Po zd2ftwn`i~mxDy-v-mMK;edJ8D1Fvn5J|cgq8#QMOF#X}~SUbCablreb&sII1f)fyA z)r&!wW>)rJvVY0#EdBF@fXRNt(P)?UkDbK4e}$j#5%j$2R_KSz%EA0n_G{bP`&x-k10O>|3if zTU^mQGQL{&#vbBoJ8u-Rx(zq5=q0J}!U~IUIzyNPfa@Ypx94j_yfsgCpL-qaSiJlD zg!)TsN3UqBWiJM7YM~?c?*06v>|tv$u;rm_iMT)%E-LX+k@zR> zPVQAZ=+zB52SXollFPTVzux+?>`I8Xh2Ugj1K@J(ud9r%)YiE?G$X-HC>~lSCM(Vz z+lr+6VzDT(+^)J{csrUPbtzwBJ&S!q4E9xjtfikJ1D5*!3gNa{s3MRaCN3Fl425>Q zB?&w(f%(TUboox8lmNZhR|T-*V86?OS8#KPW|1G;)}><_=E@4DL27U-hKd}?%bf^s zRGC>xz3&2#y0D26gbxHM3d8(#)ZVteTXmuKoXlhX#w#)*zH!_3KaKb%&3biQCwXpK zp|lO<*)wR__`|ct?zp@xQYHxXJ$Wu%=B>3PYw2m=g|LjDsO=DsOI=htUx*b6OuPxO@ciqxP^zU)P3M~FywXDaH!rf z2b4U&ZbOr-`}0>!QqQ^T5iA5|+*aEaIb2BF!)3tm$07XLLW0~iWnKKTt!Z~ z3a(`MBvTUbij85#Xod;P(G4%Pr>&hkJoHfFLqv@`Keg;WVmx}%$vjI)u;fF)FMD+P$>U$kIz1=XX2DE zZl#E#hxY8>O{ju7{-~h|UGsIwxrZqLx3c+fB2+tr-1QFoDzg9|6d-K-`=4AWSsnmW zJFj!w`kC(xb~8e&KjM4=wAzs%vVEI)+t343iJ97;#@IHA>55jD^V`M@_cklCt%$lz zr587#vyGbbk(D;0_oBbQOLloda9Mb8@m`eI(6hMQQHB_Q9odCuf>og$1onQl6uW!> zS4w7h@d;A?wS}D^F^K)ghDt#m>Rf~l!v_~8Puzy6n3U8bxI>95^o`=olsU9Z21LZ@ zhcSw&V{Fd@Oh{Lc52?T8>QvTHnvi zfSU$~8n$?RZq7%6s0)!gK!zQCiF7i^!U5 zpNr(DQ8MRR*pSv-*y@L^s&sa0WLO;G%}M8W6R3D4Kb>-GO%GvH8jMg()=BMMbs^jp zM*6FDO1X$MS_Io&_y|>P*pwaDIzR48>Ltv(aZJ+9z7Y~S6xGN4^tLGRcvK&I_T|%j zaKn<6ceb8o%Y>oZMD}MUIm7gkPX1K)My;SsBoq%>bUtaAV!sO4>+zfqO-Ur?C&>8S_|Mx3P1o89cGbp2^ zG40DcPJ*c^T<~}R{>d{*=Zj$|#RwKBf8i{Nk@08LP%4_n45bBI+*khaL?vHu0hg{q z>~DU{E+x>0dh4n2NvKTAZ7Q}8K{E?HPp~h+G%`dRmUapH9mtW@`o4k zW?l(K<}V4-M;ex!W(E@10EV{T>{@R~A7am<#b4e+FtbjwWMHZBGFmH*`u<-tqRRui_zA4?#elD_rgI^B4nG;uXv1P{g- z`nvZbf+|YLozSx0eL<1XG(+{RzHmp?jxohkyLZ01^x}NW^8cR)!716t39I^n2@5|n z)mR~u`W?-G9r`1j;lR^?Nx_8SybFi0xdHj&0sQObNpCJ2CLNzU`YNC4Pt zTaW4tR(}f;u3qz-VIs@rwMoURl>Cq3YkiFd=|qXE1}>+G&}ouZ^)95T(p_R$b-ZOP zSmcCHtJhhEZU42?C`F=m*RNN%YNucZdy}7SFgcRPy2uu_gH51gO!JkZdLV-%&jjb` zm>qss{e5!`Dvy53TyFpBO@nXoB|_r{loI8x1Vq=}+UP}wMJs55oehk<^@n4V1>)>z zP(XNp4K+$A&f-C(csswjt|Ez7@dF(+8#1jhAp@Wl_Uvq%zc z4_>MA6<9PU3224z0QtTX2-Sh}!49AW2yk18$;+r3yK-9*6*sAyxnjAKg5NQaZE1Fk(!4ROFm7)r{M zg+VGfPP5tL4o&F1yZ4q0t<4Izb8Wn(XJ2zK$T5=nRW4L*8PezhAM46W3y=t*goiZh zq%Ffdong)21jg01ql2I!BSg`MD@!lOE9p32|NP^~(cz}YYmsjX(tv1qxxTKC&?}d` zyvcL65KV~=pK*FZsmWcMphzB1GV7ZlQyCr-A+>&mA#72{d=gFwW{r;lotshmikNf0 zKfajHZzkDoGWWqZ(YkKsM>!qe%a(*52AAMF9R!sZ>@Lrd)aa$8RNzQHXoxbqBZ#wM z&!8NuQbXK{_j|$z1`7UVXBhd6Rqa^=liv_mY`>22VgPq`00PJ24L$X8Kb^2rY>&a` z#M4sbIP&Fg>N&4P#;NuMa&M$w7e-j|`7Q-d&*+;?8X;BNl`45wMG0eKW)8`cLoOjQD*oQj@9F@@&E z8^3;#NM2=WS1U5g=!9n47=qydxTxiJs*g9E8unrA!*`SB`}v_JCA9beEnj*dg=j!S z9!b2)Y3P)qWh7Z`Wcc`5WOzHb+}7gffRr<72U<3v0xMI>t9%^W?MWU%!vEUU6?K@l zp-9E4@818i+6>bIAk>bzJ{U~xcxVi|B-M&nH^bhCg-o$|m>Ytlk206kNUou8iRFCEk5=qJ{h`J-5t)zeLkC~8(mwNt~{e0IejjE z%_N<|6ZWJJ$?N&GdxR1u$y=p#Ho!Rc_80S?uAJCyoq%bRV4$R3=IQ5V?kfo+a1)vG z#YBa09^sCle)8#oY@MP>^a2js?N5>1ymL)#pG+vpuQVJ~t-0xY-?4bQs9;&~N0UJ4 zx5BYVb$e}N9=rz3Vq9|fHdUA~#v6Rw*)_*2Q~4#3M;_IuY{z%v>fDRCjuH#L3+~_x z5jbQlN#|h1G<4K>EnSw;vbUPIVJpgry;&+pdyVF@D`&rYK?PweFs9_qU_~%^$PUtR zaU&_Mc9=&W&H8_gy?G#3K^HLoSVNL6*(!UoWy@Y3Ye*roRXid|6q1zUS`w8kNfOzM zEGa@oJw#MOMO2cqq@t9vw0`H#b))xvf4}cLf84nxq zMOqbZ`o@xI;dPM|f1a#tu{`Ze2*t$aKq=YBS1>n5T0OnAlbRBq|z<+LItp}%?L~Ss6`9FYPz_=z>1L+M2H`Q3EV2rNXK3!C^3UwICoS9K2h%VB z<838#cIXBK#?4M(IJr(NP`am*Ra1C%ga)gNS)e-LkK(e$NpzG$KW&o8H)A^~6umfd zy&RJiHe2#P@1|bfeVm{@7X8ckupWDE8OFF-)F6i~C*^gBqJflnjzGWG$FX_53!f#w zY(5(7nfa;YXVBH{2|=3BcIqFIdxCjkyH)9g)26oWA9bc~I)vm$>xvv-7>Mfqy-p5> zhSTESN>CZ-i+OJ%=H2!%7blq#ZYCw(VFXjY?9y<^S^dckKO~g92{sY|Fb%^uKa>6c z?rzBSrh9k3R%W4SRX2IxNX{?9YU9eUo_&pv5F9Ys8xb+{>gK)KCfu~|2S*0~xa_v% zP%|IxZvH=FVEL4Y8st%nzS7ZQHn0`s~SO{0V;hayB70$z!d51cmNd z!+4o40v`F^()CAuf9*P&rrhOS;{cN8?ycV#F#ks&YCy2f+1LTFX`HM0wQe@uFTS04 zQzq0mTJLou??g$j{%WtaKpene4mkldK78vhM*+b35lD(Ws33;j84#WPW4h`bvt8)illFp=c< zi+A%5gHbzi_FbrEYFXyhA5-ZnFxK-J+$!RiF!dI`AT$t!A)>0Yk>av-LB(N^+?u_w zo}B!ykuK$rh#eYeU1L_db|bvKZYXlecZfk<%w3l2Yk=x1v0PcvuWwYVoxrYTiNb*D zv&~-U`qu)ItiaO)E<#nDVhwyr^`zKuYkK#{tpoS<5M*=!Z!UrAGf}FwReNdivJtaS zp))B&z+A5n;wj@6Ib~(&FQJD)t>}nUn5uXT*8h>8RJS;B4LYnT7WE0ne?N7PCQ;Os zHzj6%^YB$m-U|uy&F;bMj;U|AJUH2@fCdz{`wc3RC6+`Kt%B3{NXUA0?xcnSXn$Z= z-*-iDPr5v3vano*>iE|PQ}$x-J*oBDO%SOSnk`Gxph%x5`uADNgu|eI0H_LpS}@qi zOM8vRpBo)4EAYFhavdd)wLRuv1&Yb7(+ag(9N&UnaiCzw(#EhWxM%l&1v>jsM9~*$ zqNp5j5)nmA4qx`l8HJU0iEsU7y^uThxo;owUyicLqln-ZMmCFM;X6L9DzZ`FQ+Q(a z-?b<&tnODP}5 zE~hwKB~*&r0ERU8fdejD@-t*0=y-eVDE?)xMEz>3S_lE&yP|-+#N4p8e*K(DzD~4; zhpi*P`AmPqu7&xu4AqrjCK_B4*;HSyDcs}ZvhxS@rsGf0Nx{(0JJ(2k!w!|Pz!AO? zs9UHU&|a#S5%3tfE1C1?s|AdQ3KjJ)EA}3kl9Nk!U{*@8Q&9Ef?D%=tqaf!J=YWg1 ze9_m$zA(;r)&2H&hLu9R6s$#oGsVlj;;SKS+;ZrA`RL0DMo>Kfj1B{-&sdGSwr{G{<(Hy;Mv{2F1}O3jd2$`;94K-p}mM>@UYt7MMr!tSvP1Ut}K5yROja(Z-Wt zZY;m_sr-KQ%bqM}yc$_NtmF=&hdHbTkvUm*aFqpA&`2Z@;@zXUFV%-xkkVTJ*-UEm z54Wq&ViUA>-3bEC1%_AJgB-1^1yM;6A22yJo20lcx8VF;EU0M~=iTtk#js#FGT_{KPR@-YWGc#cSh zE3LNexEe7Ifo5X}FV}i41`x)%B+?T9MTJ9)bcRF@B(jcO-U=G`7Xls9#`m$D7Yp1f z?BF7zA&xRSZ1G8Bo3C{3>F@A>k|OkM>4CoN(Dzrru@02OdX9DWD^yTIGup{?}M5f`-Eh({C+xI2%7P(g! zT(6a2I(2$m|4*`SKxU1Nk{{D;Y3}ng&W%DhWc^TDCOk)P?u&x>aL|)VR(0h`gfr9g zR5N)K*P+Bi@`AppJ9E064toCTnI~L>5(o07QJ7aZ$JaAH0sbeZrdY}WjxT({50O!# zV`*{RgV*rUL7_}b%HO$p3iBF$sWb#FrSfEUzkO!90Xyx37`B!#LTGSN89+J-chudl zP-0AVx#PnvP0ld!IL;iJbuAOm+1amHBwePhy)$>Uwo<#f?v-W00f0IF2EgNnChQM{ zug<6GTp)}dyuYxi+&FfaL2^UM;hdy>8v`?SOoP>oHWD6P>-(G8u?aWTQipbQaGEU@ zgVuOuc)L|Ze~I|Ks~mrPacR>W`Jt-XvyO$e#VS2H`cuMB3$+>xjq&+e*}C%cPr6bC zpOU`tB|6lu+AHqq?y?co4+lvW#%Ed{g0ZSRj61ap1Y-?bN4ISq&lg#6Jx_l8{?A1S ziSZOn%UK)a=slZ!Y-hVQD~mE4L5P&16T~Q~IZ(vB+huQ+ZP7R%OYTlW@xxAj-ekl& zp+=zM*Xm}|>-b~)DUok&qYRas$sDI#M>E1_gOndBNHHmQ@GpA^ST{vsk-94WW@OuT z7Fj&ZY5dIzbPhcvREESup{V9*kKZ6IJy&EK@U$^>nh(XAFY*+-Bp8aO@9dm&(ToRK>#@U|&L7*aLf1e|yUih$@{rHQ@>SZty z^-L3l>)E9m7`sHRDGeH0vMR*|!3}9z$|@BFeu!QJ{{zaxDq(cVELYMrW#tSK1O-vo zjTwt4$0YHeEn)b1XaqcL2R04ipVSoP9-_A(yGFPD+0-{jg_UOJYt$JySV|wrvu_aP zV`*&_72Y7a$52i85iFi8whmqeSB!v^JuvDzti@-dWI$6|lJrjgy*w;|&FuHwe0bNp z^9)avYN6z=Tk+S?q7h3pT9cho$Zd4u%hxV5{rW+H8`mZ=D^2U8Mo#z2oZl|pF~p^@ zn**G260mnEd*0n1?Ls4p?s|aG5(*{Vt=dRaJ+YC*qZs5|46sY|yJ1jY`q<9kjZU!P zoE-?TiCSk6_jL8Y@5$~y|F+^Hi_3;f5<_MmoU+zl>VQe2a#|1Mx1a+`B%MU6SQgMF z{lKR6q-QN5N33N7RN8qNtSS3`pt-b*esa||yns5%jRHNQQXqZ&3s=@@cDyHRi zz)l9%=WVhv)I|At>0jhysM+$&$+Z@4Rj&y%NYaJQ$r@v6`!)K)BPkV%K5$csY;ZXn zm-?_&1}_`j|YSl>0DrH$?|OHcvI z`ZCfga36M-ZH9G-+2ip;AK01bP^wV}i|{EspVX^8R5m*1-&udD)!K-2n3Gto zo$xxiKWIN!WD9N>Lq6>&TfH2Ae}Pth7gSE*-M)Bn2pc74`-Un%sG%r+>LJ#jX!u?^ zI;e7czelt7#|!6TLibrKUrc8;+I_DbMg#N->$yc^twi#(PA#|l_HoXq zuslk2@275~vbde~?x%WCwYkBn1RI(l0_4SX8fH!em}i$>u7ZSq&_h+l?a0DnY&=W$ z5;e;*FyC%xc`JKHahJmo^!d+=&X-O?gDjXbJhDk;ZHX#57tXrXouoYzQe{Gs9%ViB zkh--4CH5tkvR{Pa{|UDD@RX(?ub6{ttm&RMBjNq*F^HBDa0c^ekG?OtGBd%uYRQ#v&aEido!P5a#Se4-VI81vyy-BpZ{dM zuV9~+62n1pxgzGKW10JmMK+`m?%e9eTby70|AWw$7(^HLq?aO?diGB<-iIrYbbq<})V zEjIZ-m;#@0JUSEymWsMH>itVi;B}!VWOvq+gQs9)%9a%Zx_d2#ZKRJ|i5|R~UHC>T zvV_hMu`P7v-J_01k@Z0#k54)pWl|Xy`~9PGW*<(0Rq@zLR_pkJ|6z!g>l+y7LY{tE zuKkiJw634gg-YvvOD>sx+M50Ipiud*gv%J4)YwohAE5x0AD={4zoOlc_Xi#~cV-xt zKIF;PbB%fFI$2JRAFug;^@2a5zz@Dk!I*A9t>-2zrRMAp&<~^Iq2}2aNOIR`Ma|m+ z;Ye9jLgjJ)Ud}z6NH%9enk%EyvmeI;ms*3~P!#4kc$ASA5H80)FkNibQf`N*PuY-V zdt}7P+nG=~-}Il91h4m<(&b9*ODWafhLXLVWahldvJ+>U=6gJ*GWX>ZiTraqtZW+} zMZ8GHtPAVgEMBf))iI(1L+&v>S6e z^^+g}QEBSGFW*tZiN{$~6%A-rshvX3qWVjq1J;cx&{4(aac#wj@IRM?>&Euah}8f} zzdycZu;u*ysUn@Tj_x4-u$wfg$T#W26#{s+4*UF83%+Xt-t6rQ^GCdOLu(SrBs(@BYnMqH$^|mjrv^y4zf3 zk9G|H2tA!*Vyo~HEgGrMS3=WM_LlC1y3Bs)KJV`&&_DV9jEiW|5M}z^@uM%^OMrgXHPqe{=Dhg#CniSFZ*?it=%# z^4^!cjYxALT6PY8ejazeXWofT<}x0p`<1z`>Q(J-u`BpmCG^tVMHweRN&ovhJ+Y`v7MYzhp{*{5x}*u+xyQ03mF*u>*o2q(`GEnLz~NF1>-dl5VHMrAtx zxNh^bG)P4be|^}(9|@7b@ri@tTAv+UFT$)ernVPs##<_6+1D#)tWq%Ax^w{H(H*uP z2=r4v3dOBB9d;S_Q~PtZ$f=*8gs%J8paFkDhLI*uPQk6$puN57b9K^?L!>gI+>&C*i-&elHfvtitkkF?AqYDw>jjwX zqEzaoc#`~KPuYPzkXW{U_fR;gOKU4$t8$3BJ^&{1RI&!9W@4b8y-*U2igcJ%TC zS41nJ?Vjr^xSQHUxC?zv6TNDQ@$j4l^SL%hsmO|nuv|<3YsC^6au8O~duY_Al5F8T0e#^ej(JG)p$n^^u_7upfK zGj6^-t8zidhmen3p%{QF9aD-Fy$$lSmfHamSB#v)I5WP*YM9tjC5{XBR-vpZ|NPU# zHRTWrG&Y0gU)-U5hb!Dwxm8Q}*WRo$-Kmi*#fn$1&{>i0{5A(Kw{JbOO<(=@iQ^Z%QkmCbqZBr6T zWWbHTnx1E?ZrINn^MWj~tp`Q{jg?8NdET853~uF7G|c1SVfWl4G_0Yu_UKO&rJtar z2y_DPtzrcwb+)Sgril8QT7A>NQUA;p`tV2NK45rYAz{Z5x>xhR3ay;NF5m!_{SSzx zqr{Pw9w_|2lx^1&+bZjIvL$tQhH~Ai@1ol1{HYrjUD@pmWIIkA`suWH^m=sdmFetV zJNhZ60*wi>!KMo#q|1(3g8j#We%D3)o&7!?BAD6COJfRv1`h@xl2BcL28?`5P14?A zE5;-3!Py|euZdC5%}>N>oXov~CQCHngG*z;zvx3t~^{x?kv z+pf$v=M#olDv8Avk8+4D;6vF#OQ|7SFF8!7%c%@|#p(#8AsJGHW)xeQB_C zaM0+bhN<$+U;+2RqVo!m=Si+MHTO%o^W*A(uJCH57aC!D+o}7x96k7d6dA7t4$R%7 zS#

o3Hom+Ts?hCthshbR*AAqw3z7k0&v8Pei>eS{}4Q|4nrrgaC`UvTao^*Y5Zt zA5@&8_U=xSB+|6LBH~||2nQ}y`2BV$f83VMoi@_DrQ^1o>r9e{)X%G~4c*o&?D|pG zlfGnsZdIz{4F(wR4NbmK$L z$b9bbn;*7CvHaW3s@Eahk~b_{n92!b+~WupDwJCVn7MoWSDVfoTyRbS*|f5%cz9`` z>X@?1kme2891(NEmv*3==*>^K36Em`WvIoF1L3r#7DEHm24Z|8EOaR_mPBFoT4)ss z)ZEkkNyKim#~N^GJBz#W^4NfqPWJfbN<3r5@~Rn3jBFXHo_>2U2O;tXR+aK z~@uZXwO8j_ysWO1=C2^j>@h{ng)b1Jn&$D%#h$56F{jH!@GFixRrUqe zX06pOTV-8-`SOvQegye<2BrJ=xJLi$y!$D5z)2sfz4le{H_h<-T?9hVxmO}Uvlnz@}X!Dab= zo^>BbVc?g!=B@WT>TZE34*Vz%3UwHjn>r5GK(P2KWt*te(c^py+<6L4>8JQSU>S+U zy3cyrKt{z#E`@9(n)yB;3$6flYa?ujbCV zxraw=$J`F1VzL*VD~ft~;2S?|YZLDv@d&_6CX;uGh$;(e0QD6{S}Ih6EzD3vjsE2$ zZ%UcQ0c2fJGG4`-PvkA-=_TI+1%9ckDQfW*j{L@NV6MKHKba*5^T4Vcpf_;BOLv)s z)%n8$P{}?jcT%R=ZFnYNUB0X^g5NJ33)79du0{EX1l_`73%ck08UxYT}&K4^GR zsk8trpX{OpR9TFd*23vbW6J8lmUVyay)hLg9Pa67|1fUt0fQAFwD09%{9zM#A=kH} zCW^fbI9%}CsLp~~M`5#Z6MV?;XTfG;AoNhG{TPmiL#r0Ku!qPKe&{&_ef&6u_Cuf* zF-u9kgcLuCp^hk4+xgV5gf$fu^ZfePJ!z`}bPp~s`@N{I=+y>U+>(gg*cVp_KNwBR ztw8bFOPO4YONZgkf#-!TpoV8~9Uo)Ib(?~D#0GOTQEdH^c3SHS9Ci0=*vaiGaC8fk z0t0?15OBxO?|E(c3UrUh5h!0h^C)CH-+q)z8**ohR{|U9rE(+VRmR&_jyGQaA@CO7 zf_8hhi5cUk49r3q`e&-`vwOQrs{0n%ZnJA~Toj$m5vI`S%Tta_4YhCQ$VnK+G@HMh zE!5(!*sNpHy?76U$UOwm&uU_P5jif z)L_}OzB*~zf~%rs`0tr|O8D`@n9|=jq=LUOG~ECd3BK}JyD@8vRmBtTxW}tnR^7Tj zGI1EO1NgE%#O&cAo+Xzfpel~jm;+QHx94jlB;Nj#-ppq6AZh}ajIlupj~y#}c$C3) z9uE%oaD;Bg0+S>HYJ~w`Xg6O8DMTWXwgR-xlofQmxk^C##`&=P=re#3qx|I_8Ur}i zSnR*HA`joZ@_VlvHd5wLipYqKesj3F%$^EU76}Rui3ub15GgLmQQrJd?T|8+#zOIL z)7^iU^#p^z(vRu8tei~yhRGsjHWv=(u9O@EgY^5Ocy^tw!pXSq=~Paa)XUZmNhy*1 z?=v?vODyX;e`qPKKd4QBnh&Ss@3)P6qjT(*yHxhcmUAt=k(*TVG(Es9JUCR8LMjcF zulC2$U`VW)sOgs`d7cd$D!PBlkz5p%hJ>x@D)c7%<}aF$W>P|Cm9d1+K3<;8PsS1` zW4PPg{R%M5-4>^H2kMKl`BnIF_~*ACfovYn#@AUD#%Rb+@A$TjH;SXb zcFH0$dQ1d4uqM64aogCM0+R7$@?^3nDch9g`WtQz3Aoeb`rB`v;=zIfxX(cY$}SBD z2W}m%74oFPAm=EkyoD!T<43$!v+}m(#LLQ%Qj}f3EmiLy10t5gtllcgbpr81TPp*K zFW&(XfgD1etvX9;GEpX6l=i8=;pPagNAWWKq5!id{wXNoaj?!Ipi;^7Mj!XvV_wtW zS(8}PfeSN!b4osUs(czV%V05t7VIp8XZao^3B5@?5Rbo2A1DoTO|!Z)Ngr&a-_O|y zy2Hxq%6U48)=<`TuKm8L3f4-W3&HXSR}s!CwEN&zz!(*NvzWX4M7!z8o9vbARr%n# zWS89G!8z^yoyQ0-K~;(WQ^&OyFg^5aLzI}r#Ub3x>!Ct9Cy=$HgKoksvN&s3YF#wj zN$bYNO;Gw;^n0!6h@Ie!N5bN@1xr2egFS2C3~tH)0IqbH?aUTS1X+LmkZ_XOE)+i$ zn09sLGGh{1OsPODbHa8l$k9u?daBO;BbtrKY)Z)9<=e+C9m}(_m$H$jfijx^{BaRN z=_}*RQ<*c2DO(D-xu}1fqJ5gVg{gmLPABwPZ<}GGdGGn~r7J_qU_eNS#D8rdW8EKp zJN|?!nFzyv+svLco>=wW7Ya6xlE^x;4)xG=YREXV*V&E`+rV^>L^q8dSGXqig z`^V@XR+Yo98|HjoYP;Nv25>eI&{{n1EhFcBs@7SkS&bQ)Y>M)vw{k5Eq#JiRMVK9> zNYf?uY#x)Fazj#SYE17Ve#45K0r&LR&zh$ZxxNG)(Wc!a5(M#T*5qe)gKWrHw_C_`-Mvk*q12PX zXY783Uv?>1{^DiX^;r1+z{k#LfuYyxxbF-#*5i7%#=hV67izNgvtgK?MW3v!!j~9t z{1OF?07=}@oBg!)MZCbjKwnn8FzM;eQCOt`+&RL@m1LlrcpH;{%a&06d$|->9NGDl zHIjSbeEqgZ45nk7K(1x*% ze;Eakb#cDQuY2ARC^GeeV@vMo9K7~xd#!@Q=P}m`9gnn*XYf2}OvR!WQg)m)#waY; zDE=>=L@@93C$vd|DV8Y&Su3%9GB+yi_+&^&hNf?h=qi8aMIi`tqT;`K!+1Ok1pgYA z8B7T1kF^TRh2VzUZZ^@nCqyY@jhdDLi$@qm@@J-i+Ws1y%4?5iG3MSaxxjKxYinQl zl1nV-wYFkufAAgFEy*E^kNSOem_-zeE8^~#mg5G4^t}{(l|GPSL909X&@uC(m6*$#X6z zyxG!h!pmdsZmpi$ui2Y_uY4%J6w#E0ueJ;G`1Q@yn7tr;!M20BKs^j)VIuJE6~)I* z9j1{|D@2XC1I9OiVKNVF#OA|P2tZd=0*(~m9RUKeyS~IGP@kVUy>#I(i7?;yL5vzA z4`%Wx9PeO90?=O^HLyRF`0L#JBmQ>+VSpc7c_G3y4&Tm4AS+2u`7;SK!6|&rsIy@0 zp8WCtC`#cqwy5)zZA-3PV`JXRSnp30MQ$^u^j7YCp*#iyi!CR^MITg$%@>4Ai)w#R z2ZY)$+mlq)pOJTDILt>3F(Mv0Ki7EW2^C(Kw_h&3w=7>vd+}cTx2b?;p4?OTlhuC$ zon2r*fb!-H} zkfpMr@P5`IEBu#z08s>5-vwV3g^QC?T!Fd8w%yV8`Qi-Y?IBm5E|Y)2kZ>VM;G*No z=)?;-0>`gd3AUIJHzt5~pqw9k%Im#(-C&AC*pRO#QEFlHTBasC|*d}@oAkcgi(e=V{3(7rg|RiPit+d>g>uP3!AY+zWc`5|>oZBPUx3~-$1lmWhl zq~-=Z8q$dg2<4+N(2Lfp3An2j1;PMe76pQ6$fy`}_zz%mp zQ2HZ_WwO}DO<+q1VTq}B7vx>~4-I(X;BKHWLx;cwYCr~z1VkH6n;uRtA?sm}}@ z6ZM6MNfF7F+RsQTE_Om@GWf!GzgW#1lGiq}^i%j6+Rxb}3H6CKv@f}nEJSxc>?RV$ z=6Bv>q4S!`e`<<}n`tYBs>K_6t9~}K>|`5=tVrpbaGUWES5zPKA8|KMmxH)sfIOR2 zm`-hd7K`MlJfZad;^4LH(pFq6MkN`YcZ2e1#sItUD0g?$mk5}Z0iK#Kctd>MjG}Wc zWnSw(V{twl41Ci%%e;csYZhyFhIkEoIPOe5PqSLm0FL|EVi80SO8GwGM`dNRc{T~E ztQmZ<**l|XId4yV@5|6}pGES%6P>l!1hzYv&NE!p7bM+gDU1mel!@PX=(BnQDKePq z-BN-3o@b--vuW@p@)Z>xs!Bu+CG{Z_s0u*yt6)GL8}43SG6}9R0eWk@4^s?xQBOb~J zOi2BkA@^TFo&}kN1(|%((Nx@GB{{MuGtWl9NP9h*1s<$trFn5~Yz8Zj)7It5&w0 zAyV;cU&V7!PR0JQ-lx9i;Cxsv5&%Q;YRD^o%#s@P&ckLE;PN_;?$K6 zgIzEK_zAzbXgqSX+e)07)&AYC|dW5UMgu9Nkfw3Wd9Ni4G99qiC~R!kLJtHTE^3mJg)Z16Sse=w@+^ zbFKCv^7=cCpEYI6lA~-)B7sOHM(B$MR8jA8+vgSFieuz;f^}L?7C-mW`Qu zJbLQveh5zj2_I*z#m;{bC}OOH{i^sABlt7sx&Mmx=3SdP&5RFN^mvXp+}?U)eHcsi zOKHJ1pBqejxD677gid@?3E5S3n2D)VHg?A9B66L^|7o1GayQIY^xt>XMg!2JOXFFQ z7q~9VayycY2ajk{{29}@6Td5O3}6(MNE}hwcK_9d8{g50=sP{H$jC4KFcby75wazR z$sGD^;oad48}5w0|9}$vey|VrIIJg1(9(}6mJ)VovYB*q>6OGw7|PL-K=VqW-9x=! zu?7iKc2fgsrP{0XO%9X1`MHg{F9#$9V)&y)fU)ZHdSCxUIN{P*ntG5fFz&{nBiJ9Y z*mfVz-R0f=6O!G(3lIJ})V>(UCjn!Ux>Zjck07>V*>V=%;FmB}o%?N^cHXJ2vQV^7 z&?B6Ytuu1AiCC^ZH%FU|04w%d8P_&!n>a~qLM7qLIiwZJ{Mz|6kG9(iqI(y7*fYHI zt4Yr-y>qs2pQ+Ub(m`x%qtRvyS#TyCofj$AJ8#p}w|PloE(S1IEOshq%b2i*QT%z7g*&mzD+SHBMhdjA8;PxO^=#;$nV ziNXG^Q}5l!2937dYYTt_K@(7wzB4$lDJ@9j{OC$qc=Y@3(4A%> zl11>cI>wPj7!Wwpu#2tJ2~5sE(B0CAxrm$PAyp|0h3ziv6#us0pdB>%%ijvMphga> ziSn)SN31c3P+aZa<*a_l4wmfDJvBsWTg^q@KfY#O5P9R&-Mg`?9~l0;syPnc^TxbA zfN=c?F}G6g-m9Z0adHkvue(D}=I0!SHh+IQTIf2%5tjB~#lUZOh0gg`fP_BrRx15u zfp*lwa)I|hIc7~4c9Zd@5V_u+DNUgU#lPzYV<+~=N%kAKOk%%pp)4)i3n-kb*<&da zm59IOIt!`hH?O=W2kC4w#^eEAs>i?T%!&Kqk0`PSl+BA3N(Ph@oz08wN(W{TG1kW8 zysyvZ5#>)^T!N=#n;VR~)yI8Kr1if%^yn3RqP57Ce%!w-B#@79viYzsw4#Ltd34yz zKEw;AjKArDKsi&8qXO=t*J^{a{u!4zyb;~>LNCOYGpVV4`$7B@pt7Faa1qrFQ&%cL ze`?Z!mh917gju}F7KLeV!&fO4jJ%`dFJ-0o7jf6e>T*(dyyvMOJq=cyw!L^U{=c&= zQ>|2>R~)Js$Rim*wV0kQBgt@Q^4|+_L>#tNpr4=`BCnxV71D zsN`jV`eBD zgS(7f&ezgeUCXo1KHNQW{T_oPAv5~Rd$Jxd zdm|!UbJb364i40WN9dh zD9~Xx;-1jt>ULZ&S`qMu(x>U1kWKyVP1g_L&ZSm_{Zsi5;)#1Ge6|? zle;l5NLGg2-Rmn90bgY&)8g1qp^q<>+?%_wp^RB&BU)a9fzA7`y-y(9eW4zf33Q>g z|5OBHTGJKjIA(U>MQC87r1Y(QPOe|uhjc^olrHTZzp=SpmEUPq)~${^60-PNoRmSv zlqV{M)>j)@(o)pi+}xOvuoe_wygKpp#$@(NBsYD%BRW4yI3D}*Bf|n z8I4m}R<4qa)~}yxd*y(#817`c@vz5a^jtWkNZ4&Z^%iYQLBz993^Ab>`!*gtyl3xC zVSyWtJ2#E<%`}J}%Qw!dI%SWdRO(J5{K&fV$F(hY#C{*9$gmrl9P#4fuwXZ2Ib9|y z0d2R^57{j3bP9&Ym)oXp3qZ?KatV4@kCSE=(ZPP2%v1vb#>NanUEBk5v-?Ckl~g+m zj!EB5l|4bH>|zGbFh~TvKqV4pzjXT=|1`8)e1({}@RvlGR%sKOfe|KKg)Qa9kuVUt zSOYvb!oqmU*4n?;oLREp)yQn`!^+t2>SQLkF~#o`{nVH86JfSsMw{SB{y-gs(N zj-v9R55uQ|O^A)P2l;lk`ehBmf0`-}-a(Z(j?P&qDSN{Zq`SFrIXxHCnPU+;R_d(G zD48dHk4gIZ5UD??w(jS*Gp5lL4N*V44PSnNTT;*{^(6(7#rRr@iV4GqhF?InQ)arIf5kA&2WrT_)OlJgIt7&!Rkgj(OHOb(4R?2i#O}d zADApma9?G(0qj|zT%-xdKf@Q;8u<=JuIvJ_FP?#F3;nOF zemg)3177RbfJ;rg2H6XBjzWTMPaV< zm#4!_O}RB2VR+CFF9<0+b3Q(*d?JZdeD3%;PFt4d99Icr3?PvL@zbrwD$@3ktH7iY z#S45$lW`I4UbsEe_Rz1kx3H>KZRet^`tEO(KT9E>RI zk47q^w5ru0ifb!!SH&Lm*u0&F+wmxs6?@}(c_pJ%RD1$FZeCZu$MuATJ?8L=;lLse_ zav$4`{eHHPg98|+CBoprP9&o;Vq1P9UI_wS4Jbk#=Oq)3BzhQkChQ?*f=UU|3tylW zlv0|dD4#)f&U5N|1%asQ98f{+^2Zr`8(h~1Bp2M^xaOm=DIAd`FM(+X@8ve#q<&fg zwv$${{5j#6P>t1bdI-EG53L#9mQ>Iyi=rnu>LuniaUTLd`6mA21Onr;X-9k(rW}}w z()q0q-6WI>nv@Y9>pAhbWEC95Q3942WsMOqsw^&A}?Ctq5b-!V54Wff!x!dr|7Qh(JQ| zy%K8=bdx*+C&qt&8Nt6o5)|OrYbZWUJkZDz0HXsiMcowj_XstVLSCrt=4IJ12huMY z0WD=dP&$f~C!_S@*uVngyNo7_V=D>}u17v_58$(Dg^@QJ>{xc!a&AW=Gd|$*mSxhy zAipsDtl59Sm>ZO;y>cB&|KD}mo+~|IZ$+Ff$!O` zk6x8b5vKbewq_${L$_V7OgvYP-KN%o-JwK;fX0CwGdHV7T*mMI3yfYJIp5?nsrB^OlJ^C@Oi$u_aWeMr7|wGi zM5cOvX3*nFh@?SMN3Xu@B2Zt;Y>Bv0fl9zKm_lNwuyq<93{H)G@_vK3uua8%(uzQt zS)iNoX!d>y7`Q`!t0w`{5j5TywD_+Op8L#Vl6PZQosd#le=6-hG4|pqnU2EYsc$oJ zj6EQCe(12x@)rzNQVt9(iZ;?3G=r*n8g9}$G=tiDqpJF?RStpwl)k=nr`(@&BMr;l z(;j%TgF(PSFaxv&0d+Nldu*r@eh`ILC8RVgl|t#26{N36DdrV?xcbldy!rW-6~u@o ziq2fw7Nqp{gBFp_l&6AJX>MqvD z)qG)v|Kt9J#Br!zj72b) zk$L`iv0E2EIfq;Qr9)BP#JUfEF4g=VW=S#KI+R0mcrc3rcY@|joVRdwrqK5MeW41> z^_#W0j+BMbo?>A|s?2r#Y{v$tRjN8b zuYnIPe!oIcfuGk1owoN^Xle#wgCKr(C4VdB|=3d8fT*UYPB7 z?(4yqoB>n21~6X6JpWD1x)70S26D;mudaBBN0_ePgHxCbey<{ibu;(*_=+s@Sx{{M zWWTc>%8n9-0UzVLoFUg}R|;E)amh0y-wN)$=xe(kS+!ZvPG)WL`*q9y*%3xH+dK8O zyn1{5_aj~j+Rtx*{`e2PSsvU|_fB*Y6hHXWNYiT6t$o{K7uBs&3>CE3?Oy_V)F++~^t^>%XsgnZpReE8BW5FchJoD20-#+TwpM*!shZ`s6FM!PcW@$wJ?=<%&%% z7O(5xui(s7GR*$l0lKr0*e^{elCiGkJtF>c3iJ-ZvGv4UNcCJs=t(HZv689 zaX%=jxTLXDVDb8Nh;wDQO|0mv2+jLe8y9k!?~Y`uw11q1aKZa>!o(7!mi3^Jdhp@A zgin;1r`G+9(qur<%#WtvOj`W=jqrTAtDjQB3Odx9`l;Bd{(;KvOq8AG;rJAM?6$e0r#I zTGMgYA{aU(u$neAPE{s60s|tDeW-6aO7|}NtI2pAt8j;|th5cS@W8^#VB+;TNpkV# zK2kdh`k7yArRr0??|4}6Mlfu3dQKLcte8IFdCM7PaH zZ$c#rTu6UV3N@9RP+1h?>vhJa5C>EifeivXm{*eQplm@S!Z)d@k7XF?-fl7?0E7h9 zpS#a2IK8MFBd#ONnA_S69j93L{E}Z3_v{MiQ1w=a3@Ksgxq{CHLMfFK`n!}}c z?PE&?w`)qe&`;V7x9X2}1nun59vSpaA2?8OqD8PfQuNX1U)&3Q+nauE&na6>UW+PFE)qt$eK3gPkG}?(9-9&BubZ^zVOwn_dxajJRVr8T3zh_-f54_vxfmEN~~(P{sG$dZxkbyxQPsc9;1 z@tvG!35i}~FZJ96)LtGkyG#E1C_rOxcIBng*V``ct*bPA0hYAy$#+zqdtA&ycEYuO z-dg>2J5*oYc45#0S5g18*5=QG>kIxIPg#F$^onu4T)(S=1 zg)!l3ETot5RtA>Wj7RO52;N*};pFMi{^OODhXPq=E9NH@bc$xBRd8h$+-L#k zS8qR8tFhxYS)i+O%R@06Gv8geHoA$D=HJic0HqG6`pR78QkAH!LV5%KNhJbFk1trq3 zb2BLv<3(-syXK0G(+j^3T0-Xi0`L|`o3?D+jxZBARX3iSL4PpHn3yt z<3K9cJnjV(wZtL!S&%*KJ0(dUbW>3Q)<38*_&3SPp6NFbR9Z z)_hi#uIg-lE~}V@@*97=7T1N6AH+yXD7-M-?r2Ji3&i|=V@~eW`Cu^aS`R!K*I9Kg z@=oKneEy_U$omqv<^6=s2NL-_#jcFqypm6`+wvvi=&ckP8M7NEU!qs6U=H@m_pQsl zewb@sYR)8BR5AR;ODq9@<667<6Z7?aL2wQV#xfr0elncH25l7o0!?HSG(_y_`W=fQ z0(xV80Losi+dO)KA^?`EdteJ<7+sVp?6W*WN;0$7`Q1q3^jrxTOcYky<+DdObX%~} z+Ro0ia$E;Vacj1mT#ZWq&x@4Tk!HNJL?f$v1I5=~-gN`$+!gEfe0WQ4sp(l_9tsEuU$x;(Nnb6dC?ZC;&vxc!0ZO z_8k~?eB(a+=F#TBVB4IeWo&YVA{SvM9Zn$paa2s~U6_on$|F#BHb%J+j&MXe3{ znM$i}%`+-SNXqKpm~Nt`QeS55yJhe)Nu_cDshc`?1+xXE&ea3vhLM6b_^QM7>D{9+ zVsYT0uUq`hhW{8R<*nYe?fl2U44Cc9Qcp|tAVf!C{d=Wvb}iI0UN)5+R44f4vW)Sj z*Bf+~gdRMxa>%`G{PH{Hpv!g_V+C3!_H`3WFiDm}G#9$49ZM;BY$ga+$L@-#4a;Hk&iD0no z6o(L0Z*ex+W>l_kszu}Ee~v?xL%aB!b5qVhD{3NUFb$f!yAL|PC+=>MTjaEpc$~a{ z(kjMlru)e?7= z;fgK4HB$i*SpJ%E1o0@Rt_PWUQ&Z?@0VQeWq7??2BFd?{0xJx3G;KiQi+06juO8Xk znih@d(NBFmpO$3zNbE)aL-vwa3O4!#!%QzZRJU{N@d4%Hg9`zl8c<~zV+qW19B}d( z4mDT*6*4#eQLsz1YLw#g82+%xrB-qkI)@(#r0KhUSn0Zk=|utU?2`T3isMOVRtP1C zJsaO8LJ)@`GO%Cy$?Y~={3|LrYIhB!-i{sPoPID+aNF7vO)r>LAY^hYB>oZFB3XyZ z4PUWgtvVL++jSw^t3A@K=AfJcvUf8rPu=32p$oxlW0-Jc^Ao-)jSEdor|&*XGU;IvH6nR`f@T!xXHbuOL3Hp>raHnPG|Rq&@d zEAW3LwCcmxr5@U>A}$i^sVVNaRABI4Ug2AY9h3z_J>`Sxufdl67szdh;}Mrsw=z`V$o9zSG=A$>}`c5sw* zvf8E6NF+o7UE2Q-V{aZ$)$>1&U;8df$a+b-vUHOW#U--Kz9kWfQr5Diu3e%?mI_@` zNs>L4bXAJ76_rRNL`mADQohe~j)Qu=KflNC`Nx?vGtYU>%yXXGoOMqOY?wSZ_6Q)G z6p-=oj^DLYc5o|q0Te!qeu+GS^R*II51?afo?hoplG#6P6VR?%S9E zz!XCIw?g#JIUU1;?A=6gPzRdM22jPy^<;42@9Ke!KL!<;2z5uNhw7)F!9 zrmRY)@ufbDIcQVkPI>SmO@#ypLZBx{s+ETqlGfW4;GLUA&uA7uSw~a&G2b&@ho#b9 zW6#|XH6hS0w?t`O4@Osm;OhTW=hhw+g%m7y9k~SeVg`+ssPZ2d(2C!RM}@>EUOQGi z-|6&}<1LI$U97a(9s=K^hJehKiFSbWk&U(8;d{|?(2rM{u=E=>sEl1xdCjHhku@Au zDwcu3RLX-@t$SIY`5*ZW+63^S0n9@2aE`lp3F~r+_rHDjl{(Fc(g{<4f1tqdcn6Ap zQk?Fdb(}4yzvm(?l%6X!f4{MPg%f|uYOs;bSz`XEt)TLSViQJNJ}{4MWRIXs+6eK~ z(Km(;=We|e0<)o+cByZ72Wucj=DRq%FnT@#a>1V0L$Nd<9Bm)ctBt0x*3GMNn=2D{ z+&cVZ}`D{NOWD`I^ZMs zoAkAJ{X}kv(GX!2kA7M$a#a*U&mHSTwF`)pTY!ttlHt5(9Ux`@+u&~v}aBT;v6ux;=)Ly ztpa+Ug6eLg^D;oW8Z`D}qh7OrZbSiLF*xvR<9O4RQZV4sB#`lcq1oPbRlyQ12tz1h zXT}aWXW37`jW<^)+^ut`|3jRS+w0iZqnza|xS>Ouhh#LoV2XYq*`BF?g)NS&Z2l1fqQg<1Howa=92-0KG}Dpnn0yC_mLSs-_Ig!g&6K_|byj4-5N%JCzFQ z?%E3kgLnm);n3n;cwxqR!+IZ;lpL^;E}pPgLV?<7j;1vqf{fbPpXXZci6K5+U}|8& z2+0fhfVJ8l9}VHZTS{{Mle?Udiw;gzl*mSVr9)hE zUWtC7_UNUwGuJU9vqwsw>wLbHhfBlsagARQPZ<;%`=zA*t)8>Q5rem7{@3O23( zApm*)l>E7m7S_h-{Rw27rw9`WKRmYe)aPSjD}4!}#hw$)tCkXKD?R;KRtXZW+Qiuz zUa#9F{>{7VZB2xlW_qd7HMB96%(X;v(;e?q2VsqA?l-}A3qO8Wbr`KolGxl)iW^R} zq~A|!rZ%^BqLj)uZ3#6Bbc2OsfOAUBD-gfxZBXgrhN*z{raYp`XxM5PWiPg^X@zVhAan1P;_ zcX_Zjid~Sv zE77z#n0r9?i~5#Wm*l~~L@A2_t@{pZn3w|Z>cvE!iQE;~pqCKU!eFx8PSuIiUAd3% zlCJy;9k178Fq|X9)T79^Q2~~49++C^Jp!GzjX&N0TOel@S`11WWT(KIg=RK5>&+Gt zR376-f2=8=8MzdNf}g@mL7{-RV(6H0T)r6dBZ5uk5-Fk)`;;DJ42WDQI#l5@?pfCX z?%*#VRZ1|NO9+#5XiwdHIVH@JB>zud&c9M9moA(}{D)i*K}g#)i-PZ~84IE$vf1ckcuPC8L2v@G#2K zIo&`e+)K(LHi%>0%yyRW2~`i;Z}a4sixCzx?nY>ZpUbsF$mRLUQe4I8Or)=gop@V| zG8~2R;oQrA@U~TA>`MmF z*`(Oh34AYFYzK762ly#2<;9=v7n>``Bel!ueH9_^#&gImE+eAd3dKE z3ja}d@M?@zS=vLW9QvfBy!Gh5u7;WHSKEPw2a#?1we6^c(MgFdLK{!A!7641RYWS%(f?V2`DydU&z!%^W(gna1~)(UPL+W0r{xW zS{w;UspgBCuON0hyR}5ddi!N`S*R9)M=zSeUAL@Odfr|Xox*M?x%s2|@tD9ytGU-9 zphi@c@S|u4QCiV~l5OCg*yP3nR^Z@HS=9rOk<4Ms}_LYa%JavjYi&CQAr$XxD zU|tmdq*QGB5GcCDYfOG7u}?WrH(!K86uN%$!$MU>^wKy!7mxA#=?rd|t)bl;YM0zP zo0(1cj`8bk&yG*wE9ckAy&{r2UrWDNHJ>3Wbn>;>wu_B(f09dr5W>&<)|wLBIrIy; z&M{*e6;8Y%JtG0E%22&~(afXcdz^=Q&l>jd3*BrJuz02M7MS88Uw-yf5#i66l@c&( z+wg?hnA&jzQK4Aw%e52OdjF2NjQpJjh4#)-G_`O%OyE#Axz3omgOjt=WRy|JU6=cv z&g1t$#CZ_6W3!DQq57Y>r}~M1SVPf+y?^-B^rG&?V6uO1LMK3dwjYF4J696Hrv?Q# zUf*6@Dn8hj$yW*m-pDThFW~ug_-O}UtsNBp*o%&lX6h8H@T+L<+Yzjyj7H+9icmka zL*Ul2Uq7z8R{PygTcksG+N8vXF{>?cc^AI@@bTQDJZ|339(D%nqbROjNHS?(TLtnb zFdP)>r9t#0d?1vuh-N#u5=@oAS*bliI{NQlzsl@>u_nGs^@U!_=Z^!w&Au8PiDw-- zzC#qECHMe;eU7mbr%wUf2%i*Y@l`(74ZMIgN@*fq+HWkQdRn{9++xWzD1e_~*J_P} zgKaRSb*2VM?Ae#A%C2H+Tyz~_!jZU3Zscdn)J~kdl;_$dp;dKJ;JZ0gG%hcb|A~&Z z^b#mxe{fdK`wU)(z>WiGVTjh{OV#KZb?qI{=mhdc;B&;MqS-HPp;c2QuNDgwE51TA z=)pqkm+a8ck+&!xXb%3q+TkwCHoT$sJ7+}-cApBGsg3W+ zKU^UkdOJ1n)aVN2---xG$(Jw`tzl4?QYv9k@MQ2ZID;EzsB}7yaXR;rC$DVUXTO80 z5VRiLsExLS*^HiT*JodK5?J=Owb2C0`l)~LZI-&<>4n@c6N}BSN5E6 ze$j53o0W!&99?XaZ9ApwSAAB~Y*T7x(tWp}y`{JZ?oTuW89)0TwV`yj+yqs__;kC| z%#F6bXp8nRtRa7bipT)0qLI;k6Ja&I7)6%B01vRrJiAfBpV(22c1RK~K=G%uI6{VQ zhN-~?+ABQ++cxc31WcY-e$%qLb`;)2q+69_aB>pPMayVx6JD8c9@71!_=K_2la@7= z%RNyNttFtLeo;HOKW=9AMu}r10cJCpp{HZ@z-a&{F`QMdwQSI1w|=NXwsbUR&!#T{ zr>9=Y{V94k=C@`0vq%EAkZ8-zZ!@>?ysZMg0jSo>*pV=BA60OI%J&RGqTtRUv<43P zHQ@SDaL^kgl*_3ZlCopHPsr~#BvrP7y8*ef!-)K* zC%a1pt>5^?AS$VO$%sgibSLkYoz0?Y7**s*{qBXD1A?!1pN-U9|W>a7oLSeDa=JdX%d zJ|Xsl>3LaF*m_005n$dsKc8tI0!7EcweA?}RtoLN=Mp<_!Xf3V1Y!4z zN9sIW_n7KY!t{H=r#O;h`!YEC7O#6yxeu|_$LK58b6F6%Gw3V)c_D?O>cc*Lwt=>j z@@{CWsbjTxSFAU3RyXt*8(G5H9SV_mB9wn3s~+4%5#wqK z?q-Mse*PEeA0l9lQs)9g9An=D-0?uk$@_{%cW_ZbFcP0iOVR|#z9iW$*CMWOhRqJ0 z^e!>UtI!k=TMDaQ{VssveXRxWMpIb{TJzUjAN#P$P(120J(` zU$!wX#`I9!cpxUMXbpboroq(sKJpwnYP3PA<$i^Bi`aG^QC+a6ICOrGAm0L$t<~fz zulZqrm{!qi{#MJcNa$m?-H718U0$Rez_>P0c9)DHi>}tN${sx$i|)nbfPwhcg*VUD z9vSIsHv!K9u(>+wBGr@?rqW)T`aq}BE;+Q-0w(axmpCje)l*vSEf9{4v47jWPcfTW zJ)5U^;n&**3XPc*SNh%+kWU@CCsui{UHv^2JUOIi(ac zwr6G;*Rz=1PJTLfCh4q$*uL=~yczWRS}b>Q)v@vQqV7$By|zzRXBE)2l@u^HXy|7> zkBOYAlS=wm*hR)TSX4tA6@%C6?3!^jv$U6GUc-VH!B^Iu9`xeBn>T8@Ton>Q<*K(=fuBf_mgIk>6besEUMYps3p;^W*%Og3G+2= zEaUBYCUf}$Pl|=hQiQFZEHMpnoYvLY? zmmMQ7%MYgGfap3HqQfDErN$W)^Sh0kgp9MPyCLQ3cG+E$8^XI;(SF!t;ljmVKw%pt@ggotDDmFXrR(Zl5_cVZZxMj- z{Gohl`dl~y`SoLm*m0vn-FOw1X=;`5%Qg2>lY= z$&Ii`k}@8xQ5_5LgQ{65tyW<(ZxvTaT&s^4!R`8K>%)Z@@s=5ke-%ud67?M-p6rV( zJY!$NQ2Vt$a+Hj=&~-raT;5Ma*F8fRl8XuE8P zIw-_T@O1&K>}D!QYrzwL!8;CzQP~Ex{#4|{Me|^>^aD^%s{a0EW~1)`w@S|F&SzDc z7h?0j9yBnzdY-OfVEyX|HXOKzcx~U>t>1Y!M(3iEHc#b0XEkZg+`9xGM6QTjXKd0~ zpt?>JT}^seP27u9b#3$w_dt!m9f4U9l%%gOK1U^SDaBluULHvT?di{de5mc?Zsh;4 zHWb-e+SKLS5B}VZW=@7t?`@_z=z3lhLGz}|MBB87O28(n|2hjUDo6^QqU-sV>`g0B zj85Ta6IcKU*^pB3f9#!670CB{|AG|5kl8#{ss(6h%|6*lb$By#5=QfFb|Zlyoz#)e z_}MP$k)IDEj}(oHz!z~wT(AXaM|?zC=`P@v;`DCaWrJx@BU`a ze8NGw(}l;`uAjB7Y}k2IWcHKs?Y<@`vL{EBcZc)hO2WBGQ-(N;D4}yNeHd6RqlCyrX*!W4vP)uAseI7piPAn_)6jj_c6)1+ z<6#9Ipa088lor*;PNoW{3Msjw{DA^)f?NyXVS2ja>|44cA7P5~P^x*&bkpWK^5#*n z=j|#TxcCu15o4UWY{^-caxpbr=gGod-IxG?U-*%x)ptU&JJWZE&FziSOv|jvj|V^? zNancU19^ofTJxO{`XE{YNFY5d`c`NCJoVJ^u{QPM$akhvT!@$NZouphij?~hLZ}cm zA;`p;ZaC?eWLPBm9j!qR6OYHO7FtUv6y@bUScF!#kMuIwbu3NNI+q->tz$*I%ehM< z``EYZp76{F-As8bB?_HAL>Q3b`ql0QDis!w5r-o_VqU0lNdQ8I#%j%~+w(&N@VDYP-QV4iY$H{GH9Cg!oT>pu{;)}fTn6snidkv-|?SmBVcN9vZ3RjKqj8F>AaaH8W_Y0lE(vc+e_Ej2}v zKg|&*20sl*Z0k|0i@po-=Ds8{9pK@aJ{0k8eEId6sY5rji_gxNe0M={Cl zlmHimg$R^V7IQv4Wz9dy7`W!`*$no?d*-{Celm5}ZdeZ&2`u}5Ztruwz!<^DBy)dr zFVkyF0wjk9Yt@6F$pAsd_$RG<3Wb|#OV#nUk);uN>BSJ z=SlKfnMZu1iWt18IT_t^>u49{5!ecVl%a@{dS5Zq*U$Omk&EW7+i@X9tMP+tQD6GW zp%8A&@=coX=CCi@xBRs`IyFFAwMZOC1)4PZ`)u4_JXtE*U4QSC#bj`#@@nx_`#KNS z@6rx*)J0Q$k6~6IoKwzT32RQ~ag%KxezBa)b7xia@Flx>qJ(ARj$}nN|40p{NnCkY zvKAW-S}5#L=!88X16gG~>zj`he}zN5_(aoj?Buo$fa#UDATUa4fB(rElX3UQ+{4#k z;Wk&zSO%c9Zd6X_*-W)GFTTTv8;&tJEzBF5ZG;5+x8`*^Ub+V#y2J(8r* zkO(BKjbgPD_bcTgqG`Jd5e$e)>R)>8nz0T#;aBBG?HWQC2YLWnqA7<~?Ieiz&VB$(NUye>|{dDb7am0 zdK0#SbnibLZG$Qq=(6@RB_?S@q)t_~neUG9DHyJy9Z$ugLum!HIbq~2CIIk>CVx<_ zIk@GHo+d?Wqv_T~q&ruPzf0{wa&>LA5v;P#emD#O=hEQ}?{wt3! zqsuUTRL*i@JtSXfrB3YIDqq>_^&L zsIJe!?&5Ik9YiY3?a!XCXWDk;3I01XM%q$FGCLzDGf{$>%+3rbl$G2Q_qXD${+rh5 zLm4^K=KHn%MsGK%pM9;i9eG)N4t|fk*cUvP`}>5*+e+n9`l~FI0iOz5?5Y`f_Q(Fe zFn~jmpge?p5P*zfBmi0nQ>!jO&od|Rxs7dUx9Z2m zkS~1|%NMkU;x@Xl6~T4n7_+T;>hmZB&03LzyVneY<*Rvr&8<32XigY8`J?kX(N2kO-_RB{&&6Ix=k=`j z8#w?9z#Iq%f|v7+r3^aYBw^Bfhz>H^TOZC*2>2+Jz8dV1mJh`nI_NU>r~7D+)&L(i zpFU?Gt>2)Xt$l2##M;!$UKi12p}Bi9hZoaclx$o?*vKCkdBU6h-U~=Atohsb)q`Bp zJ}sJX;4qF_`}&D`*WnY)>SJ&X2!)h=9!h|bp7@Xd|}aNKY-eEFrnIspQ4FLBX;jG;rohI?qfJvWUs1^y5~%V=$N z1DKk<(jOl33aeq4c<6CzLn4t5~)W(`Y@vT1b6t#tA z^^gIvj5^FYygxu*iRUuo3T2SkflH`AAf(=tI`0uaHR)@1Z`Cblwo9w8mp&K~Ynj~f z(dC}oY(MmOt^bZ*3;FQ}fM6zaF$b<1ZEeqUncB4m%>6(oTJqN+;e|Pqi!!HJ-zE>K zd#Yq_SwI^cehV!l}sCIO=sH_`kmRNtIS9(^iwj>RmP`ApgvduR@r%n zCIlVFme12KCFL5ntfF5y!E8XbF0tOCzrs z%tJ^fY%?iI*{53e`%ETyV=rW))ik*`_-syVXrM(>C1^NLP3tdqfrk<4Ea}FN{$rUR z$)pE;Biwm+rrXW;O#eO<76AbawbCC{?{3$HhxVUT`rP>n zipSFu7X@$_{1SBXdt-#a> zhD&?)sod(3vq#@!Q#;}dvs(WPI*c$uXVG7q^37;EI^#zy5(n{R!ht8%R&x|8ClUzX zYwp@;{<(=@o)j*0VZF=Fds3Kmg$q)Y-r#K#m@;lGgyLH|$9{+4u+w>1kMqZ@4v-t+ z9Z=v<{f3p?EA7%mCU3k}Jju3C==e-1#U?>ct1P9o?z7JC39<8l7%%=S`JgVy|qlOP1eEaaYi< z>g`5_TK0_l2*`VBYWvYnh<#Pil%BHtSaOFsco1rnw$BbnrS0Rxn7~7&jY!lA%^xL? zj;;}80f_@h6qk3!wqMlXq_rPw81x7$*K06hrbc`P-ZJF@CdTAR&+k4PHmpxx+8SK` zQ%>;4V0@|9)+ReI6JcknR0s9aW1#TW{3zP;CGrqs{H#c zPa<27dlw_KSA9x6M=sZ3U(MccJ)fqe)uwDkhti4`u`iBa;!$;R^%;AJlu)4BFTcNI zFPuK^S?R}U_X(XMbz~c7&Cnm2;KiiJ2R0KJ*)GIY#P=>^UKW>J5ibFTgZ8U0AoTAj z>6(-?Crd=&KuJ#T+s0YqNYcs{AY_T=DSC?oZv+f~s8+czXzn~-T$heZ8m(}Zeu@C^ z7`kwDzPp`mU}7!U{%A{Z?R`d%SU`;&V#dm5(51k%q{b$M)7~ChbopP%8R}}?=%rS= zNxG($?_$Mq?QY6LL{>HK$uX=eU1{c|C(p3XVzoIG^?Cvym$(edF3Fg3PMG_NHOUxT zV_@z+wk-!aj=U<>hy@alk!MI!Uavxj5c&Hrt-^-IQU$S!N(FdxziivKMY34w{IMa+ zSk_2SVC#&d{I{(2+KO#?9mp4v8W}NRVr*#S`e1eFNf>&u6Fz=Xjr1`l@O9yk6c&LwWe0lfxcLC(}@bvN>ECR){Qk{eXskWX=54Oyn=536r#kD zbGWvqb2#-}`ZMT+Z_Dx9k<~I|z9tnuzcH2Re4H}fR_Z=;-+5N}w0U2oTQ6!{~Qyun8dzGF) zz;Fsl8EFt5mJ$HJeXw`YPmm7M5STKW`JPyYFK({Su1mb$;(Rvq(kpAmss&aV?J-26 zkd+l3BlYlm(}SQVbai*yh4!>PASLSJ?yz8;5BFIAeE$}8`L3rIK{qHATi_~z_8}2V+-)ta@`2!dX_QeiDx%o#E129Jo4$#Mu%f3>zA1$Nj|iUUK=C z++}RV)1{(9^4GO~pHE$qj}v|vUr zKHCa4bc~eF%hKX^R0(F^DQ$N(t?x#*tk^wrIPRX$gAMb;3pwjJpDA5^Xf{LTB7tmw zM*h2oz_p_f+=SWRUVeM@MV>HU?bX|hZ?5t$Kj{J<)=_lfVO&qxVG`dt%&bIfVynyF>dT> zFsuMQx^84v>OutovR+ICFVhE7!BKr%6vCb@8(TZQCd}mQtpWREC%>&#q=N#Sl-dZM zEbbSY>}?Mv~ZU=i+t3?t9oybMs@KEv=jCo zje08{yK}(Dx3Tza77VBL85B~Fu?Fa(QbF5vf$A<>VqpDlEj~u+wed=FXvVcB1=5^% zz6E2s-vu~k)t}J}|PD7kc{73l%<)OI$(E&k0oV?U0<*R^#Cwaw4iU91S06G_C zT}lAfW%a0{dG;1I+Tf!hITsr*t3{vfj)wf8~C-&3RDBYl?k|_BD)L3nXmqN z_pFygYC3(*WXMHwW5v@sA;n)W&md(OSeSUACtk|4`>Y_ag|3%&G^Me1)juvsn|Y;X z{A6nFu4l^nfvf%3gW4gWM2@;gm)5sXA~WhoE;q7(qLh~hs0}NgfEg z%h5^|(oCL8wvpl=)?G_jr-l`7RBamvBi4hXFz z6)mSm0aMD7vE3Cg!*l|pgB+<9Q7Iv|Oane&gSVxIm>y?4xc%jlFL%zf`bMTRSN|*l zWlBUp#L4K&py#rDNiMT1LrjV6(p<27OO~r0@SG?0?-{-J*k@~C#HAoi} z*Ui(WUy(fi?bWzT$JYjQjmY<5LDp_yIn}(;T+nY0^@CS;P#}`>blZ2&@USm&!bnNAJVco)ZofUZtmr4MV?m2&HsxUwV#g zY$=7umvU7lb{>^dk-z>bVz;kgdbglP+3C*GYw0IIoTt1KxdUe{D}oSa<)>}na7IpJ z3+Q(4S(~fIur~ifXvggrf!pM!`mP4KkogF)iU(!hgOMZFeg#gyJR(rA?hS2NyRmmS zws0r5fwGp`Lg0^s-@(;{`+V;_vV+Y8@AK{6SrcpqW{*(&o~I6H88tYPt>Giy09?Mb z^?ZirhDe(ua(T3S(csSu3dBuqkDwEQS)H}IOX6FO>z{?C9r=TCjqk@1kZrGLMi%$i zn*6!DMg+#XiGMJzL6#q;SX%`LO6&*u8>qjcPGXn2=rIB|x6jyLd+~H^uJ@&j8JLd$ zags;U^)pV2&yZ`(3ERL7@O}7!cM8N!tx&r@!FK^~dc5y{$T$y!-Amp1W*BZBG2xp7 z;dw5{=J>P~c9moZCqmfl>@UBvZgPzH%CFKC}oKqZPC=MJHoh_vMI)RK=?+}y1o(iX*2GRHn=(ndkS&QGc z4lXz~PScju0$w+1FSKc93@*FHDjV1jxjd~4ZT8xJmV z;P|c{sBvG7npYgB|7|&(rZKaIkGc%^)!HNlE=Q|K=MF>aRR-4NSJUIIGtZwki9n2x zf#2CrrTuVvz6!KpL^2R)3j%HGrW>PgMg=ebs#FesAEBT9WH9Qnoh*cSXWJ$Bxa^wy zfa;Caq{;BQ;W36)>(VTHZLSHMth->j*YTQgMav?}J_t+fqQTFSZ=T>Pg&su%pp)qU zS8$T~r@fCLiU8|4{Ciiwd$Sz~{$4&RNde;Wrc5!9LLeOq2TKxYJC+5Osse#o(SfjbR4=y zfyz6!{66oT7_LjPYSL%YAg!aHfHZ(q?Fj9O>%dS@jasJKV9tL5RUq1fuHp0~kM63R&8iyaEF{5#fYW;$a09G`=mjlY%zK zi_w(YaU1qujs9;xtz)rK?7}n)!-zc$GzV&D7oDZm@9g!L&y!fX{1063-J9Wg3;4+$ z3*7n}E(Hr;C2!3;^*VbdUU&I*;u-ccb~tlW_#End{J$Ze+4U@y;VARy<&Rxmzpo|D zS^UdPZ-cbSL*{3)PrA)B*Yad&Mt<}zK)4)4Z5uugX^GUPg;@sEZ&EB84hqUj$ z5eNMoNG@Ib%HX-`uvYd{1*e=fXVHjj9#~lJEJLiNMN4rv@{@_yAPzn?yT!X_F{nS4 zb8g2iJ1c7u(NA*^h{zVn zfpTUIwygE*Yy+B?8i5b3S;J}%k!5Ie{gk2;byVqiUisop%$ep4FDEUid`r?cenO*n zidN3>=>a0PFUC4C-@MA^7{T|($ORs1BCIt`^@#HYwReX!#U4EITNW29iLxw3{R4(C zz>*X8Q9lVf#o-g?Er?we{INlJA-KI4jHeAPRFj28Zia8~@OX>X7!n^sYH&F;pZh%Q zj|daY3<+d}79YWNg5b6z9nyA&dXMyZI@C(!IpF`FAn-*j4SkMr4!`}Jn7N}k6 z@=ElVNpm&WiJ`-S{VVkz%eyGshIZg9=erk5+fbD9FsnyLVQHmY0F&mi0gUlG!bHC2NPE604ys;Siv0*XM7G(Rjki;j-tfiQ2rB?a{(0D zxN${#u2_W@`M%Ko{W>%xp7~5KbsX)4e&w>l)0SFBlGnt&f|>6v2V=qF6N42AGsUa^ zjv1)SOr0lPhXX^5ePL{=YcJEch#mk7C_XKi%#RDcUh4BMA6l*x13Uk)NM*)JRSy2K zRh<{IHpE^*V?C;a+5(1jP2w4{Pucu35RY1%cJw=Q0EYZWCSsIlthpcdT(=H_iE+JJ z9WJ+i?%7Ddn98&TCcx#)+d3gzh9p5XC>1#^qF@F5K1nsYD}fu`f{76M&laD8eZSpN zP6(%N0(OU7Y1sNPrE?_3oNW>C76HsJ`)NI7Gr1cEK{6DK-h4VwS4 zt(a8KIbjbZ?%c#NF5qt~U(aOB%CN8PD+K=lJRIC>MZ6OO@2?esO3QX}A4`5uQC5Uu8EtZ?H2H^^^Jfoh#T&g~- zginPqGWW#ea1XXyo25ZD+L-Z--%;Xk{edv{0h_=H2(teJ>6;c8PY|bF(>Li3mP&bq z?kMMkE1^}ug_I+RA%csY;^Yk9JU!$AIvkE;-+-a+Jc*W2l!D7ctVCYn!^@J%t*q&I z+FbR1dg9(FI}vXNQJW}3_l3IXPFmpJMY`bVadiA#M_rp@dL-RphL zK*~Ann@h&)fwa>thnxg_K2EkEcKl^Qh(#4an)LEsSITvQ3e_2XJ`iG#z8=JrWZ(EN}c6W$4)%8Kw9KT@JcZWiM)Hd#%N1gr$fb;$4Z0P^{^+8{Rrq0rs6 z?Eas|mc`Z({$p^V4`$JxwC37^+oDk8s#pfcTFm~?S>fqzLdv9@{ZmL&k)4KU@8*4j$Fe zU!Ov1{LYuHdh?Y*$nU(H+3*MxX2j7Zeu3II31$fd;CO%@!gfX29|Dl>gUM-ZdbPc7 zj`?M_{8TlVP1o68_;z4{1)Oa_wQL86K76~l$j#dtU3jQS8LFt%E??vg&GH5m{%HNx zb7SicyzK+`uP;ADXN*-Xm)NEed+)on5T8-LA8V+;W6QN1s~OCidiV4#cY@_;R?2;% z45NubralrW(1j^3Sg914e=e`IlP;K4r)*)r}$XcSB^iU)bj5?!3ajL(XMJjV<`j0Af_ zENe*00Yz_r&XXxIt~0d0c_j&ZxSDEjwfT#+c!_)-#AeiIm$;v>fiYgzyn@o|H@Lvm z+}Q+6SeUqxz+v1oUYrwoljFqGA6IgsG;g@0{*}7c5APZ6mQMZ+>Ey>8Y75X~sZaAh zBmU8fv4*$OIpO<>FrVf{OGi)jwjUjR>Xs(gAHZQnQum5)5vSI#3J2t&X*dePa0uGhM%tuM7MOy{3%yIk|ge=gX<# zw_uICo8{FviX-vpJWwxTPYBJI5V#(JodZZ81I(H`m;#_BA*7vX3WxkaEh+3g!S)f{ zgz%iZ=|T_|NR6lK3GxUFJgMyoVM&qCA^CJ~|wz?v(n5;KLQD#ytq1%QfIMH5PU&C|I18YJLv(th-1C%}*oxQNstbnW&%j z;-4-+FEg$L>9<=ow>4gdfg3!*o5$>eApm?G{>`h27fu|tMhkb`QFk9l!m|_0>isI* zWsaihU72K%zhQVK8v%1*00M8X3t}Hgql&X!%w@s{Ua=rZLeM%r`PQc)Cm2w8Z6>y^ z>lOj@=tpbgvBzj8EDWZIFeSstfc4*1(*$u^dT!_U`tVe4MVl7WF!8h62VNN9REzf@ zW6P!Y1mt7>D2xb46NWTj(e$IpF*|SGh%`wcvloN_UDMf*M$wKW=q7o5lhl{0aam08 zW!hp8kHX_PWyvCs{E+C{-g8KG`| z+GUYQur<4c$u7aLKU_qp(SW7^R>L|b^O6tDB)9@BNG%)V7kZF*OV-|h8xny$@XhjHp^YnDhlj|FY5+ zZK+8gSXC~OoWkkexbx)VH3tOF>TbF{`Eo`3?L4Chku{AZ4MStv z)jnm;sKwhXWzZGu;;C%OaJ>AEykkIh4{oxkk3P&&riTA`S~P}c)SaP_cJc`Cx)S??UDKG70lAgdN+Va7W)I{z(Oa&^j2&-8wi4Z{)q9bf8|td2s1JG zVCU0`DjPgw93ohB%O(T2J6LhFqJQyjLSY7Xtyy=XP+>OrOSA41xXYu4A`jZMzJ&b9 zfzP1aH_~;s43xcJq2rCUzOV~T$M}+6_u^WFch}FIbNh{eM*GUc?2ian-WVnzuYGa9 zaGTx6P=YaMMmH~j7EP>sry!y0TWWZEz}gsqYo>YyQSQ27Vkzv%g6oo9=TR<}DI zX!?wMZLFjIjXCG7tA~ZNOx~>TbbOc(92Yf$WhmBKsulX@IxvsARR)CXKsv;ckm1mf zZw1a!1nLhffqYQ>&tP=WV;%BLgT4TaR*ca7r*ff+oM%tkTk?>T={%osXr>7l%T?r8 zl?A^H(RN*nmqYp%+xo*9yV!MshjVjAQ2a=&@-1RvzmRi6!jW{Qn)K(Y1tDTp$CbFV z_q}&lS{KFbR=M-%#^#e8^gI`x^rx~}j!Jj$mhjFjJ<r7VFQB;^N)magbK2Wdn zJl^z_)-5oD{RORm2|P-gy6J15$|~TJdN_91Lr` z?Q4HXjp2EkE-DDriubsCA^K>8Y`NAv^WaFyrT+W7RWXV=&BxN4|4# zJsShjY`dB~hyzA~FjI5(a!5hmKsw0gU(jWBp1?{uP$&KK2p=nD@Vj((IYiU_9D&`X zntvhwlcBmfp(V5wiBMTtN5=BYwlU2f!G{<7xzTJBt81QI>Uw{KWrvzJ&%o(zYKCB? zsdrZ4W6S?CJ4<*p^%98TlO68;Ic4i}lo z_;Jh0wTBkOC}<7oGGWy-9!@I9L=b5Ijl8M+lr`7w8Y5XO_)K!aV$A}jt{)$Yu=yH@ zZ$#lLJ#}5ynl%{=?BYu}>B&KY9P5^FQjT{C8u~9mT2T*8FAx!n0a$Yi=d)g?FCWM6 ze8S=aZtW8K9h3q69ms=&v(SGPf_>`GEpWN({{V`(H(jDlidCeBD}Q}Z^EKS6PW601 zTg04Qa(DA8fpOzeH&$uh4TyH6$p`#&gf#lv9L>Gssym}LU0J?&TGcc~iSWkY(2n@gyw9$zUhSfEJY4C_ZZAi4e8sq_I}k_Bka*IY%E_@Ki%$P@BpSv2j%YA z0O+MU3HMXWAN`?~y`dy@&^DoX8luQmNdSLChTMyqh+cS6?;k3|{9QDrAQxb&AlB;GOWmp#AxTKCov7#?G? zpNL_85|~518#g|5c`Zr|_XC|js7zOp&ePw$Wy6NaeFmIgWsT0>ubZd7;H<#7Q?3>D zTX-RLh)Ydl&@DmvY28yoj+{MGneeoA6@}wLHoJWrn&P+vqqyizY~14iJ@svDS{))C zM!g_GAzY3POvU%A6*{zX!$q)Pn$j~4#hB)U8H@#YO1+*5ac02n|EbOt1!T`~5eXRq zs*f(vhUu|JZWe7f?79k&alfIIqJ^R*obuw%i#oCe&9u8t5i(cEoL|c*uYP?qq|A=$ z>|##uUB1;=8_OtF+YS(*2eMb^?r>$t19bs=(N^#LIDC;0rlJsY9*ZM8HDI6sSLHy~ zO(QY;oxes6W*H{+QDE3t0+l$TRotWyh6z*LyJ;hJ8`^B{U>p3<$yjlWcWKFD#tgAP1 z2?a@4AuKs1hMAnQv$)EU%UrV49~gY8Hp~93t*&u(*V1c@2Pei)Jlu!cGves5YYb|x zuz}S0tiw)_Bay{%_K;y!|S9QjJ@9#zqN;6 z$irt-)74&o9A`h?-Tz3VtiZcZAN7B1ahKIXm8UzDfyo&}#b+J2fosRes|=d+t=(#E z@7g!zVxCEf*pn=Zkb;{y12^s4vJ< zQRWmrSM?k2sZsK@sZ+%1=Cd8XC`L;A>ZxFXxSj>x1j@%lMi^$Dx;bBp74MV5F>^k? zbl%R1F__j8%Yt{>g{v^-ly}E_LR0FkhkxbK488XVDl+QZPIYTq+lqQ?2Ec-kXG5iN zQ)KulplNJ7oof*x_m?rEsr=DWfmn??1NRxI7xf!C-k{_P7HMhUpZ3jP+V`u_iKj#b zEi^r1SvC&Z;VdiARCii*1>woE3(48Gui5U1UPvysL%zt}UyD4k51zR2fG$17;q-=t zJ1QYZx#ge-xf8jYjc~CB%txBth1jwhPJzkpQjacj0^%u&rqv#}tr&^-cClz-YYk4bYnvLGXz3Kzo4N)V-L?2ihIe$MX; z>AN_gl_j%S)H^lUvQF`z(@mal3TGY-zn%{3O7=tKW>JsY47}Q65d0!l1~I%GWySY; z8~4;bvDVw_pB>JrFtRt87smsnKL(|2U;Vvnk%zl1{=5QFX|pM^{=nBZb0L0fTp-NC zYQm`(a|43AOCsdPS8QbGyD^~qO?F-7c$Zxa!yS1lX|YrP)7XtvlC)F5t;^OWqV5lK zy=FV1SY@9lHQBB3@~zD3@PF6L)%ImbZeCdChk~BP72yS!4!|&(;~Tmii{IhILd)TG7WZv4diKvZYZ_7856t= zEXkX`EK(6LV&iq(FX<>P~V=y03%f*I&3>%_|A^nUQ+0Ot%nr zWya_|eK=wAyrM0MbzIKx!K)1uPfF}xfbIY|+i%apo*E>q&Ob=h_RSl~jHppdF?tF; z+FUp1-GYeT|AP1l=WW)HT1}wNG~?xEr_2m+1O947OC*Z2L9WQZ70@yA#g!fXmN(?4 zqK}CPN{VjikEXR#-m|1BrCo>@*{te9xc{|w$7a=T!hMxr`LKe?1v*P~XR+wNkav}V zpG&sxy$ae@I(Wzt`<@yU(xfFxJ4h;Z5&5+%bsg7DT`d3^|DQlv;&(;u5MJf!`eBO% znhCM72dmQgF#OX|?u2RHI|GGUYNvun&ul3$-LZmw{p2D2m7(gO5)!jtgoitp**}v^ zz4?;}vGHU*1-7tfvGJ~ker#bcrQ|^=zy92acLFz@-juy95s_xM8!=c=81QXAV-dq@ z!C$_w19ukemPrJE)VI>F2%;(AdU&weB!|Acs;TaGnh6SR+Ie+Xzqc@(Cxf056Ut!$ zN!@*V>>rKyNA^Poc)7;f?D|2H-DPLQ)-fKPiZ3HvNq#EC?!YGB!c*~ro#d&wOBh1X z!PaD5MVNq%@WLKyjYZ8ShBq5cxWS7*ng($Lxk|l*uvLuC`NTFnj5-HN6lq5-|de6Pr zMg6|N_w&Bzk2`l}&YYP!bIzGFbMKuSPD8;_Xp{7Z!6TFT@}2t=2ByL`vfr>0#kxv)RxmLL-jR8| zz@K=0O2&)KUeAVE)&5%Kl}4EX%O5XQe~Et$Gn_p^El^37ZJxA27tI^-iKVv`Mr%5{ zl;oc%@{1Bv81A2_{#uHx!VuvME~(!aO+bCU=cGXEkpOJ4h-wo1S73@n9TYX!0=qVT zSvUL+qwtSSdzWG;jxNwFt2HfNml#y5RQNA>BN2kK zD4EK?InaCy=508p%<<(?ZS;ux5!nbCrNx)g1FPr3`+*hRDqS+|)W)#f0(~--e^XfQ ze*Y=wM<1%1ukaDVSu{85;*2%vrPCnOi0OWcT>iTwExxq6bxEWUf( z=h=%6I#`D&iF9LRVj{8>)^r=X08`$q#77`Q6xt>>NazVcqxn#t#@uP=koash)Ha@U zf~3FECxxHSnQFMvFGYY)Pj}M&qxN-~tKj=!fILcuHd?R1jQ|h6^rvy!Vb#;TSJa`O ziVdc@XGk>4JtMl7;_C|?`iyVErB{?2Fe>{-h987UEm2sNK*Gp-ihDzGhDGy;5hZe8e!9PCtCv&U}O&7|LNbe7Cn&vKK%zMz|FwNb}mKhR*$BhA0I0W(?@PkC@hHH}wcQWjIR(1*h^h9cth{3d2zL z8F!EESM!Iz>CQL)d^jxU2}Xh2kMfRN5!9L@0X3bD7m=bS`DV7Ig?`+2P*F*Ya31m6 z&c%&B80hs`cbyHC6L`0t0c$ArNi&vDah1L-8X1-v8td5@4E`h8QlDOrW&Ae6*LLR^ z%Q#&G-_tu5>09Dr9v8u|;*hdu>F5X3r2)e{Ja&IqMqt2L9}2xSUAazQy*IQnyA@%kMmBNE`+ro0Orpn5hdX>}Z`yan7hVv*dWz~PZj?(>PNsV5 zGlzBKmJc=%U47zXQ*K+)4#Eih2S`50*a%>mrpkWh#l-(4ajs zlaaQhyH1ODsHsuQuA#*rdbT*^W492l*NQ9dZ{PnhKmt})3%|_8Yks`%-Pcj%alNjay>9L%2W^w@OOVo5agTT>e!ykxZ_l@B8jmH>jT-?=#yfh~S9+xGk7{+F z;(DtopJ*gRqVpKpN(!avL1<0-O4G}l{@W~I&7+ex;{F3%Qb6BOGYXQ~G=e4L7q((( zc+!FQPq(tBmE0wr*|(XV&jSo)Fsv?+nAy$V1^BbKpI(Dqi#gn2u%cBzNV)INekQg@ z3q%FOqEfUOF1&(UhOl{~M18@8D(A}`Tikc{@a}%;>+{gxTD=hlUKjnd90t^xf&u&X zQSlY^BPv^=?Q+Z|LNy$c(Y#ah7|$nFmg8VT zSzny6?UtM6hf6ig z@0TOp^|Dj9m|Fsct?G|3d_MRh-b&i{kAuzt(m(wNm~Uh z@Gv2pb{IRS21WD?>o@xdT{4P=H4O%o4s(bLuf;k~tBdCqy++FA5gKn;uiBf-BU2XP z5IVT42bKI;uwTChNIusDaox@ZMFH0S&2f;QPQiT~=ga+<&6XhoO7DP~!<+1HQwiTK zh4bVk@#1JM;_=+OYKH7!+G>98+S{C?y-vudB`vycol7h0r>5QppEdI_%nr*jDJ(LO z+CN{6lz#nol@k}acFl?wrNwmVDoUqWG+gL1RFtyVwOr_hUGZ*8kehTSFpj6^?}N&c zg0b#$k4YJEVYZcMAcTq3*EdR}v4oU0=N4V?vidwB5>cRq6(5{2cyY=**KXq{&-s6+ zVz@b{FC5^BSWFh2nvpi;nx|)6jf<7k(^l(iKqI_RA#m=?t2s1gk}Er{%#MSDG{{bK z+@Sm|nIydP_cX$>MVloeSRQ?uW9-8MR{cYUs$5Q4n?Ij8 zD5PE~ze(Us+0R9VxA(>+0-GG`^9b1WL;zj?tPKiQWFK%>~@jsyN4MvW~3J_Gm z(g9E_kEp*2Ty%bYHR`y5U-}j|6j%f1c&wO-4B;|kjFg|!>{1$yIU-}w<}c_ofx;dv8!qHE z4>^2ajKa%G$V+{8c*LiVPWdLapbbY(uNcF}8bda=Dv9jb9Ls9-T@J{I{a%ia{v0$0 z!!18&vjQl3Z>|m0mJo9GvBqH$Uq{kHJitOS%J3A=bW~O^-IZ@-#&2urY2HBVooF;z<|zka}! zyTK?v{Z0Bduwfzrh9^WmpGWP7(%!R+SlRrfl-!qTbFjSvt`I=`?Nq-+g^2B*4B621 ziv-Md4)q$4AViPx7M&_P%ZV0@7c#NIC#TZ4WZZsWWa^52GEbY*bw+gIBMt&i> zyMNmqor?T%@(1HR+#qS;%k6~^B3e3_V%R2bW11c@o`;=>DEq? z9j88qkRIWLg_cp`^9A6}@w=rr^RJptMTX#=TV2K12a? z;W|*tWjhp`Dv6|jL;lPWX$r$78w+8;G(_mV*cbDfXeH4-(0X{8(2~R_r0NjzQMLK9 z)Lv{qLn%OCLQT))pweDzESyI$XOG>QjY}@j4d-Qgz8~~k?a0#0=L)yhZB*h8st!8g zce~`-6RR)lz-JLd-7pNOVW`T%ljU8gBQ*#4p;?lx^s~9BGLR=@jREn<5JtQWF1t?@ReF@#VqR zYLD=X=a=r#%3mOtOC&&!3RNZWivR2SBXn~N{4)(8=O`|`gJw=w%0Sjv;Xr1GN)UcHA8P@O zI4UoQ&EM|f2V2R}@~H2@0#^sJy@c*q#B?uCQK>tyOLZ0<8NWc#Oj3l_U_hPv42diU z?#7H0nWQjJDyI;4tP6zv5au`BA{Mx_g-GNX1?<*^VqrfG%*-pk82^m0Dg+V;Q;H)D zD_I6KHS%UT*>ZK^!k0QQJl|gDM_qF@&lOF)^My!^!FTVG(lA1Z%5?Ad;$1dDjJdncsPi5E>KZRu?mFvTciYCMIg8M^` zYzRih**xfnkpjD%H>2bS$2&_O{9bz%MCGT)m8RcTogO(g>l4129M@DE08A3Qrg&3+ zW><(Zgc4`EzU|6F`WBZH89r!r;R!DW^3~_xpo?q0Vmc3C93NZ) zQ&LU{Ditul$XbEIudqqTX${c(CJW~gAiqLVC|eKSI4*UHy;FenFp*K0E_L;(lcyN< z=+kWSp^UYgxy6w8X^c_SaX~+e4^_RyEJq>ZTbnXppQ7YR-a&Kr4~rkQ_0>U7%$Wu1 zVq0j&vXcpXFdP3332o!GUlSyuza51FSX&(v`SFIs(-zq#|lW8u|TQx*ZY22X-g(%>ABzX5saVa!Qx{rS+7 zb>KnMhzen)#`!IQAuG3kXgrP;IDE|7#W%BBa2uR0;%l984G2nWU%`&k|GE|xzueBF zgKD;}XMU+^M(+GN%8PqQ3o5f!J7bm9z8@dO9ezX(_YM)}w^HEM$ZL=iH?Ja7&64SI zHg6$QZPT!><9Oyt{6^Ut&^^~M81w4LEm>0+Ix%J4FJLPfM$*L9+s+l+JI!MvL4}#7%d7OJ zA8hS=i%$b+e@Qb27t?~jRZ!+Ju{$JDm*~~u(EwlyPbU~9B$JRZQ?YAXj?YHm6GH%6 z-4s@`%n?0vH(v_PWD&5)8WJO)V-c`aA2uLQ-1jk)l4B6ti6PEka`HO0*6am&R6)H) z45rDsT&IpZOPt~+PdXus!Zdrl|&vBzDY{s@-~X? zU<*!LT84$tce}j7ci<(K1;t^_B0aeTiILG~*D3~oj4rUi6X&VIFgWWT6RjqBxgn!~ zSyj}SW5go^MIC-VVLVxD6FJaW?d^v_?_1_J3!i~=h@%r?St`QHUbhm5(!ojlrGMG zj5l?O+iUMzFuR~Z(Z!Nw`%gFo$(|pYNX(y-&w55#RRx%US(3EZHLzXIODtU3ocqmo^5@(7~ z-cidGb5@p73aO5oIqHeJg|oSHfz7kO+BVMQzvJ8CmqojK!v9o!U-Z+sV8~$!Y3fCt z6pYcoa9J8SoEQ>$-8EQ?|4paQZ(}C$FV1-jbQmjvoKTrzG^$va756S|?g0?w*etaV z4_+WLdYhmmO&c6VCCBHa`(eu^sO znMd0C-o|ifY7rn*W%(ExbmjWAZoX}L-ticws_5=C&%*PqUky$_+C;}d zdUm%VZ<8f2^CW~PECmkTY%19ySSr7;M*MymReMz1VRR^$@xi%xUb%Z)K<|Z?o2uepgJx|wbcg{QS<2@}B}ujjH~D}Rf_z>bvRyYoCkz<#Lk+!WDYM*0J?TLVXxPi0m0 z@}h|%E*)7J!&eMMT+COUIsZILGR&af@uIWHw5wjdzF{N@Mb=n8 z-eC_2S^0Te4q&+@kG-*Qyq<8dsa3Y`vRN5zpuvjz+E^w}GA_Z=+zYhL%m5n>L~0rC zQlVZGB^7-cHl~>ZX6{c1Ru(Q!$3F6FVZ)GUGWS*P`FF;3nM#>?1vIdL_8$#ie0zC| z^6k4D)qc$CPAdAhI+fl1Rf#~3WMZ&R!F1bxb5jW8)AoaVLZjby{Y>zRAJ{xglb(BC zEW-#=hJ%0})>cQu8SCF}OT*5KUbi`ni{`!~+~E5eT{y!I#Vx%TsI?N0%%by{Z%zB` zu{dD|2m)o`v6d3p2)EZTV9f_I5_SXmfmK{_w;uihO#3hA)}kr%K-mjYLLKr(MTMTC z*__|bZ2)1+g)xc&uxMlBJnDbk9TH~8QcKrrG3Qsm2lL83#rzIE_H`bRxBXbZk?*W) z`ThtrfkS+F1MVvYq(imo+tX^67riPy?iSdR%4U}+!FIJ(#}dIkc8rYD)fEEf3*wY! znOX9* zx+$nvZe(K~s%K@po7TDt)8TT{HU{D9WuTOUDGz`s-pbJJ z3Y%Ryy_JdE4R$*iK@D)*P(AP z&_CpiB|p7~OUY~WZ#72iu`xnSG$7HjuEM+z61NHaHnkp)o#3lF@V#VwQtb88tMm~% zZeGI`Fe1+u&K*$u8?Xz=(}?SyYeC1Ej-#S-H9wJoF1Cu6cM(EX=_s+NQpxRD8*?@q zEH7-Hah3hDT6I+lS8LpF+?3cnQ2|nHeDfV})Yn8wkz_P9EyzqMCmH6musf;(VOket zOrr;zKyz~E7I`t8%=d*D55!=v0F(`$@;vynBj%~GGv%vBfR+7=m9vFuWilJt>*#J< z-k~Zu|d<5m+WBmdu-1N76$f=)I@!e9)o-zG*$Sh10*OlJq5224t@u0Z zkbzWj&%B?-CAIRjQz^ z=dP#YN$nuL=k^m2lNDtE50(IPdNJY_LhbZ`MHB6U^a6X{MTaQDSBJZ$`^(OLpZHWO z8GE5ZdX5n=y>7vpHHfUH^kB0&Dfb2pA>YtVAz_ix%t8S$G2?V2>vrG^Gf?AYm)sLF zcef8-aWan#J*GWB-bDS#Q&hH(MS{fTDLHzz;2#bn@$X(_oAebelvhkhSB!SSJC><&qK5zzhtYOpjCX8xsk^I)sG>BtRj zP7br`tA~hr49P$1dVFZ2AV^RwaHQevOY^UXyx(FPi}sE)TSaVh&YgF!s-xFKE!{dk zz&1trMx83u{$pxCJ$)`o&MG2Sm<>V~hLfsO)T?_B#p&H&d~<%v|Zs2w4i($u@3+^HW$I9DJ{Q3V|+aI9GLMvZ73z+ud?HH9BUK`W#yzI z-pkKz)|p4#>#RKCg!<|CCNJ4NTVg)zOB6Q^ay)WzX1d=x$nn(0nK^05vjSyF9vKj{ zjD^P&epQ^e=?QGN*=O)}EBX`$phz{Cxe2=?E|~%Td(;?*e_-*l&cgK*xOii6vY?BF zSI}qmKiRizz=|TdAAO$f$8tU}@5aJ>vxlG1_YFX36#Fn0f!j-YRm01mi$5NVb*fe^ zTa97PU20hq!|Y19#&y_p!j_eCjbj-j{6+dbkw@-lP(jL?o5d6l+K9vcJCe^f+ugb1 z*o~uY&iPksVQz8!v@HQ!2`tD@Ypqeo7y1ee}`41#<)<{G319T?(iz7`gWHFd9 z2{(ka5fbZDP^Wbr6lx3l833@#nn81hp2)-+XVti%jb%<&xk>C<&Ryr35@Rt*qiPsK z{;&N|Mn35fIHwg-Dc)&`EBZt~ZEfLKI;19dU3AZTiz0=_4YwyvdL2}yJVn|;qW=L} z4{X4biZ|lY8*>%SX0fC*xc4IcE*ft5Q+PKkwp7~l;KP0Y6s4r;VJVdSqY!4HYw%)3 zLb=mur07aXcMkg!e@DJO*+Nz7fFT;xF47c8N%htJ;TOy2^}Zm;4UMZbU@I z=aJKO`p=`^J-uK3@Q(+CAA4xKtzX;(-b6j)8o;P{%;8x2Vd9vI1!2Q z(!^-lXoKI3!i5NGaiplYn`|8EjEufiOKUucUcNe;NfC?-8?zw1DCv`+q5UPP%%gJi zu?r}xQRq#%hTqIx5ucB-dRZx5Ry{%GZ?bn3j5-zhUk(3oTwCrW$GxzfwY5_%%ulZL zN-L%SA6*ac5!u+K?!^Mx~({@VB8bQkcBK zx#|$9u|m1Og+#M$jJSb@8DI4L4(=kli7v=~dwR&GlxUedU_?lVpY>5!FpQq%d}xr~ zY_gs+N>R>kmG1{CE9+4MWt)?qQ&R_r;fYorhmTG(3`b;KsfX6sb9`1p)UdgM#M)BS zVuO?Wzh9|b%l9-nR4Bk)3-HDAdT16XdR5S~9|fdW1$@boFPXYsw(~>C+>4=btfOF| z!9J1q`(igUW$WzKbbP#J_weiC{?or>l8@$y7w$P*2p&l9Vo_LD?w}ANbNyDrcWl3g z)S)YfB~C~0#b~+QbHzgJ;_cf5vgkf%Qq4<3`tL^#B)uTJn(tc0{~yZJ?NVqHCdd60 zOoZY|a@G(jKFmRc#h;Tto-R~`tA`WU=m#mUR>PA(9Bkaq_H6}``3sE0C4$FGzAQJC zbQo!Inpj8adcT(k3XAOKfsOqoZb1Xp_whG}p;&}yv7^XlAwh$=pCUnG8v18+`x%ge zK@&t#kxl(#p2ycMU7*&~%>GOb+fV+YRU z+5Tx8RX)bSbHG$}KbXJb(ADb2_Qoq2>}xL15+n_OPsn$sf2~Nw!RI%-y4DIXC%?dj z7PnzG2kGy_?vU*%OU3g@j04N8sDgHK{!l{&9X?pWC>j35<&sAMZw+CuGbpjkBv+qxL&8qcWHf`+W;k8%2jwGx& zyFpplETTfgX)cdMYh?B_$Qt%OGgf*1jRi*yI=4D)u#r@*kEAyL=ZqpdJ?LM*c}^B_h+~j@4C=vnCiaBh)}uvfLFW_v zJz9j{eOG+Bo{MEIG}v6dai&Bz;E@GLX3kzs(F;8fxK2a6K^(Sw?n%HZqv~EVL;n^< zfT}-ui5#`7W`v!p?qdCh91hEpjnP|F#=f@t`5nfclrlZHbDdmYzNw%vLEX?5!HX7dIM3@4M}PUMp1n;yR( zX!=&Fysxk^M4&2|?aUX!q;9tQLtGFYm1 z$MT=zD>E`~524Iey|dG3;$Z)Sv}A=itOe$J2!_`DjaWb-KRwK?(;%@~S~qZ&=)Tt4 zumcTLSjX*kE4Y|xn-0bLZ6|}nyS(rT$EX!Ncz~!a&{YC9^FqX?Pl#SSAfApzfnMv# z;dnYuGkW*tCHLI5+&8F={yAWG=%I&D&L8kAfFT1;IkfvTPwKX+PK(qDV|z(x`W+`H zpHadOu*rB~^&Uee z3levO)$qpzI+! zm_5oG*o8q9^~>yUC(kUs8we(w&i~Mhj|A!YFo)HWgZstVgP@1@Zf%Y%>W$#j{|8Wg z?=)I|y7=mPO64}8A~;mC`pqTDSv;hyhI!4^^+WXURyLo|^7Yu)nZGOP*R=NLm|j9f zNI~lKUFKi!o*mbqH`w~M^(;~Jq>niduivSk@0yo#4$*NQ{AJR*ZWZYBRr$-a{s4&H z1TQ|*E^HU|o4iA&C!|8unrcv-tQzM zBuBn?XFJ+~iaTjvGm;p+is?3$(`(OKC#$#ck{_6je*hUZB6&yI34z@qfIJ}6brKpQ#02u9bPSc?#r8D{;gurU z0pGHjV8CoBX9#3ymMB}bA8Qz^4rgK^>EYlAGW zff+eVjDCOVv@{PC-x1gbz0mXU9J;`M*Y6x1>9Z=)viUHQ?P9}sOwqA4#5j*q=}AjH@trZlW-m!5_p%h!hSh`i-{j z9H=5R8H&5jeI!iKg4S!HFBTgxfj#S`f?G@4ikhQm>v$*7S1jh?0po84MX}(ls;92k zo;I}{{NTl19&_Xshzvl-MbUK;i#8XNYWWVoQOWBPye@nAO`gPVT!&uOs2cT6*B7O8 zd4{@;{`lQw2UFkm2d=AzPu6fqtp|UUfcRHoMVZ?vy25Sr1sCx#VrMX{$Cvus)~|_= z4tIy018P3Hse}4Ye%=X#&TX#ebs%w6z^4aq&bo2b^sG`i{3YgTGDe%b(wECNKQ+{x zb#Rq+olcaa7d|PffNpbh}MqRQ|z+5r`mmO>&z3 z8MhP*J{SC)13ucu6B%;{Q6RT|1UDT&QTnh<&L}2>WO^(hhIExT91E2d!ZDgNU!q^z zVbc}$%MGkql+g#bzWB~p*vvwOhVx#TYIFnEMW{>^y&JwWKj4V-+z~|H3iHYHNw zBSar4$7DB`uLq7bi`}hV8*+^)V4EL(-W9Hv#o$k2kkc&u7jp&`BuHAh%8rTj4@I?d zEvSnbwuwV?Leb$n5x7(Nli?|#2M~^aWK4kT1ZY?DjR(&1n5~6mTFSo1O^eR zoqA&3>+n9+?F0L~U*rNu^pf*Kc5k(P zX7QWo{q$jQ^-wTpc}$6FA&Hr@IE%+Zh^L}g%IHTJtd<*4WE<~_?HASdOU-9ucSu~$ znzPJ7QZweS@8A71W!1whx4Pn!eKmn5gPueJa9SAccXt9a3(-(&^deI_+#}ia0`Gs| zM>F3k4E=vz5M_MaA!lmeVm!ZBcQH%e!9-G7#A1QGgA}4;2!rvrm3iOBH79e)A>%*B z%46q77WlQY>M6_B))T};H0aYrRAyDxN}qj^U3w{Z*s~%c5m@)bl!&B1hRHHU=xfW| zM(X$z*A1_K&TJs+#xW(|GKPb3cWnrH7sgE#UJx3J^6HZc33}(O%OS=Z^6aR%v!*IA8IEsICJxoHy;i`8O4>T;&weR@#;J7P>YAfi&cByn z+jYJf3L9|Ql?w+uB=d`<9#svi&rY+*)alOsoG(WBP!Gzf{`Wo+z6|JHV*8Ng29l5P zvP+8#qx=h3EW5;0$OKkta2eOxm_#=o@vr6f^gDhUEZLPPJMe_8%ARJUJ-)Og{;wAJ z{h8q3VSjSLWQ82v0~jO8kYt}F2}=*f&glc<5Ak`Q3Wt3D4ahQSus?jSL2}EQFE!6V z#KzcT9>+CMCbq1p_2=HbH&eFDimFv>`?FiUbG2mT8DcC{(OaFoc8JkV9W?cXD-K}_g%s&vZAB3DV<4+f*qnpNDf8TW z((MK#hl7|5p>0Hpk$Tnl>1)qkCSAbn5N+ccv5WUQWayibZ#}I08y-A20{OXU_HEnk zaA9E^9^)!d_#ASp+j@dCIale34w$kmxn!uY`;lYBufZmZUn?C?HFM4{;mjjSjqg4o z6(&a17(e(yDkNF}lPfC~Dv^y_+O(~p?ADeaf3hJngzxJ`digMzc5U8C5(z^pVIA4 z!q?@t&8>OK`nB#q=>L`U13mDPyAAVykT>`zzbL|iLmPheQfwICu+O3+<>*h@`o6uX zqy(-y3LN`tKlz*!S$<@=X;0=)o}UY-cDtjJwo>te@(z`-8b!qtwP%)Loy!ITvik!M zt{5i6SRauhR%Ij(+Y_hw6OIEPmvu;ERMOrU-$)-=s!ordxXgz->EnC%*)z7kW;mS( zHG5~odQou)xV3guEWI;};jMjCI;M|@BXqL9_JWd9V)kJ+#ryDYn~9g62S6|Bya+iV z8tUD_b6p<{D8QhY@8_DF;iet8b(&P^48r)1Z{egT8-@ubc|aIV2jgLSVZ&@engV); z&}YGaEh*9+f?>R3EZ>vZe{F%=l~eExnxnrzQAZ!c$m!b1T%sa2bL=ObyH3wq^>f8z zF6Y4LneKqDa{MSP-at&i7&)D>)Il!hRfbxAJLHzMRj$8_-yR|I5YG1xD@qh6zC7+D(jo!!@kGq`niFCxVdk{-S{F zx==V>__hKcdOWMM?ld_qfKU0<$2xM_MSkV5Ho~CfJOz7AwLjVGcHxeqa+Ympm%mSi zLCv}S$lT*Zef=g2gbG-sVMBXr2dqtU*j(R*C1jl&zToNhJ&QTT~@ z&6!bP#NZCP;zFWC8ENxuSqO4|zv->>oR9x|2j>eWr-B}O1%Tw`Q;KU&oakKn7gFVD zFbtf>FdsbOB`J;Ck~pP{%fH`#xs+2nuHtD#v%`ox@Q;J_rNP2i3k@#kLlHMpdCidx)?5kV&rVY~PX$TwKq`{vbt#-g(2!Lz}=Zh1e zMGnkN*;mZ&w0lcQ68r?Y@wOCJ2*`UR#dD|VopHANdP~)k=3P%IT7{VyywROkykFj6jJ|7QWk+3JHp5reqx;lEKwzn zk?cwba{S+4;;O6}Kmxmp!RAm7uMAM`x*OzYpP?h+7dYnReDMzY%P5Wyc^1Ob!P-l& zYr;xkU*fB5P-A2A5mQ5+SLHxFai7g1(5eNH*}7fQsrVN1m32CxoFNyM1ytFc8I2qn zpFt`>exh_CeBtArHq69nlZEgekZg(CFay&mQnCZg*z_b==Ahy2fGTCg`W$hsPpmVp z*{_7xz$s*Pfqv6^EDIp$5?$`_ngSu~#uv1Bpib?C!U=CAuEtl}TdN8zqAobQB7 zJF6qy9@B*Za;q}!Nsn@npnD1)gS!Jb2R5cK}J`ao@j9^g4z5;y8&&jp@3p2-%{fUPZix-y2_E;Piwb?8?bnV{A zNw+bDoDp+LasX>I#4$Ero+0%vPWrnS?6lK zrfqPWDGGD8DeaPu1B2(Tb(2Gp0l?RzR$A}b%9Oc}=yrN`mS1|F?=b&zUVZg*$3R>+r(M=@(&cHLpYJIKg=ELYPYyo zGYGorGg?%*8O-2jGF+gc*^OXwg1SD@8(D0^n^|UYCvs)ndweOA&MKfE1Ln}!dgbr+ zSPAOUsnDv*Md@~@g$PC0XKOY1CBHl8*(g8p3Vrv7iTfRf z-aUH|ko=W(ATra;q>nWmV-7hPLl90TUnt7cDgc9g_;ioM6bPwa%rB0EN5u4*W-ygU z`g67x?&3WTT1I<%4Ws8567mIqjTVcdm$*gEekmfW6rcWC+#)w{vy^4Wg&qqj-Ls2S z9rf=ywB6iqe0=YzwcEEiD{W%*Q^xlw6477xagU!5a0Jdk>zPfI%v3DZK@Z74vdRy2 z-^;-G09i)w?KGDsnGVw*iFz7M={&afap_YQQH$uE+AJS_nQyrLy~;yc40Z# z#j%U3R4FhYpZNkB3o!5Hn2!emCrZmFOz8@~Y6|MC=N6quN47-=DgX})xfE7D+04kL z%P-7Wf~s+Apq?{XH@>>-i(Vc%j3WVNj1??kkWyTRJ5qt(C2ikJDytr49BzG(6mug* z&-3-ucR9N%40l4?Xue$pBcS0K_-l|<|3gHgjp@VWgUN5Yh-O4+GZMEXQlKd`XC9Ew z*A!D+rn{N8@4PkZk&ukn7}Njc%a6BPx-b9I-p_>V&yBSknq2i2i}kV5D+Aa2_3X{SRsqjr6Df5E26aSd>} zD;qHM!MotO{{@JJVxRk$0F6CsMM+{EK0Ykr-_`FEEQ6yP%Yx*C89y>To3l4i40?>>os?w<)y zOY#dU0d1nIBZdJ(!!C#>+oi|20a=i^>JeZ*j)obJ0Es_fjLXX8h&b$A9$imbZ6mRH zxg@gB#zv-6Cklz4%|&d(crv&iX>JpWUobC(WFcYa0mGn_;4oj!$u;PE7JiPvN(_+J ziTIy^Evl0oGo7yl!;0!Eegl(_O1dzpHCo7;jXboJ!0_G|f&KBGJw5G#%AHf;6HY|! zv!u` zt^j8(Y*3E`U#7ED5x;~=3$|jRs%|#{sS`1PzJcKm*j{kHmTT%^8H;%plrpuVgD%s3CM zW|B$W#A(Iff^?j|xd*OlpLu)FV0CxV(Uvck7qJo?{n^|>0~{fQkj;`ojLozeUOGD! z&`0jCw9S(g;Mhc9*f)y9beCPLV<2OF^O{xh%Oa&ui*Fs$t~mH7H3k6PD+`>ka=4J(nYxc)4S`H5w zgEqJ*dac@Y&=O970HCZ_b-K2{8xseKavPJeP50s?oNY~~;+~zrh*sA5Vc|Me!Y@P0 z;oUZXoOWMU4rS5aR6n`>>bw|I)-ld40-z0A8}OPh@_j>osySj2>R0@4(K+XCh`X+Q zXIgt8f8X6}%P$?vbQ&8ia!rP=Azv`Hrg^#!3}5DLX5XwLWoBCXqgPED8ZmBzoPqsZYK;bw}BRPxlXs@E~9tKsaG4<(%`?o`*MC7I&*Gr zZ%;@!jJta!vO>f>m`81Q9JJt#wbTyDXu7xk2K>XWbes>jR6g82jd21l{szFN2dB@= z;-8kdQQzr{MWKgVT4j zRMv%aoU^1t=AM0Bjl11-yoeG!Jy5}DKFl@K?sjMSZO2B}riF^r>SJ^on++^Zo-Dqh z_zQ5$_sKTMQ&xV4??lLJmIx*WLO8T!c@DlF#)x$^%o!Gl|1rd5oTz3#L<04&F5xNn zys=#)#6Q*lg4r3bvdHA9A)OCuf>>@FPr{>mqGXonV}{^lhgkIl3$giKZz7jOW8_k` z2Q>~_Y@A`=`M%k70G4pH;Ie_sLr=#stv6?_7S7+klZ1wM_Rle3j>6wJl+Y|t_MTnL z@V}@UY-T~&Y>9;lpne?a)infmhe$zP^M>3}^( zpW{O8V#CJB_=IO45Xz%~V;Qm+derW$cbY&S6aV3biAXJz#bQR>z3`m4`zf&K@BtIk z(uuHhmt(N-+BNPQM~UH0JYqC_L>a6;WAk0~2PK936tGI>FO(I)NhD!A@6E3H<_umf z#73Z_?o(StsP>lI^&2Ph37SONL?Pfjb-)f8e|Ux?_~N6M6lh@CdzcV%s9T>2bgW^M zw@T(G0mmJAx`{$hUh{U;V-@9Cbkp57S|cR!AznTr+Ez$rI$nOHMd$7?feM53q@z&E zR4>LyoOVdlY&IRSao&3EOeFr<3@N|bVf`cD2L%*#7znsfTA#nb35R5xFb0i>(52Op z;sg@x3~bGHUEAN@e1R|1KAzzduCvYG?DxX2!EkVGYrt+CkKZ~{1$P}+XN?EgKMJ+{ z$LC5ctc5WK5L$AaEa|R!S@bek6-UQ&SmOWx#XI9){}iY35k2W0U*TXop$7 zkV6qx1Iv!=nd~@Dnhd;GzpLX)D?@k7q=sHe5Y`bT!8^`jv})yVxruZGf)Y0+o85)0 z+puu?%dIxMKCG;Id);f={$9niQpM)A8){7I*1H5=UAaBjX#CM0_p+{!N^cAH;Ek|0 z$3mNR_~u`J_@nJ`$^de9$?tapTP~^t6PhB~YxrQVtJD?4T)rCj5b)yg*$gi*2ZlE9 z((@5e7Dq1~Xrs?l(4X|0v?>kg;=^A~lT||>74wT@@54UaEL=BRJ;kNZ6m@;G`8NSW zrYIsktK{&C$lyL8wc1NI;OwTLG&7x5TBDeV(H7ya&0x4lLM!)jhf}(Z0zB1>!mDUP z+IUl<6k#1dfMIH?9yG zPr}fV*os0vPV5#qRNJid>d9gO_8z zZew0zeUZg66ReZtpu!XD1(PKd=1~C_0vKg)TRP{F3)6GJ>a6JfI*GeF@QM%hiU_EU zh1RucF6oHqVxwyvEo!+RCnr)9$0(YV79Y!}>o6Msd69U>QVn(%Am^q7^xGgM<8~_+ zw@Ge=;b18a?blqYw876A%vdOPq?%It;ugB88x|18GHttMIZxM{^%8tDJs*c6J-fNn zz+nL>9EzwrNEo?s37GG3&>6-Uf;i(Fu~ez0VYeP;u03kJ;`Q1c1k!9sX}q=K>2fd% zi}{BEdp)5#Gw7w3-XMFmIAzZ~9j-~`Yc`vlT3 za|=d-b|4y>!5R#S)0V>xAuN}9$=^cF&ly)h=7V&mU5_b#EE}%udI<;Ok)AD+>_d&- zx8J(`j-_e1q@{)VXYg*n{B!IJ@Q>=G(XsNLatmV^o=M3A?FgP z1OrD_2k6bp&ncP-O=*435xZD*V6Uq$N%!uPG4w32Jkx)bD)A#BR8*Tnsk^6vmk+s? zFS*+mOHMO<@nCd+j^UGdc-v(}_w=p~I0JK>uCBf#bchA?oI5WuBL4`B-m4wrv(^9a z0di`umA3Ne4uQ$1aYZ$?M;6TgFb1cOwH&|Fc`u~ODg5iFyw2)Zx!GONrQQNHc83}+ zjeZU}|AXz>LU-KTGP3CFPtI2}s0ZVWj0y*D8rZK*S*^vAqvctvAsnpkaAVwYf2k2sIf(0yRWMk#-eTwe zeD#;{+_7#Zhwkw7(zRt=H!@;u`PVJsz??1G>Gj7`eEU2!s-lttA02ozDY1rill$Oi z5#E~^t=>C?@XV8*yOThF<(~MNX{9*KwcO}uQs&#v0LO<1{*ohZI9fR8RPXCQWzR{J zVqMoeF@vI%QeE#zR;Ry3qcT7fqiwIbWSO{kg{&*QbxWFl2x-03=^+ch)`TR+g851i zhCf{;kGGr%5PC)=-l-w%H7Q-Fk$b{J7Yqut?fC2?C1~&`{}m5GPzg{OORb|!#0u3; zlJ`6ZHO64@3dSaQ=zefBTSsKHID~wIk^It``(#|oiK4*i(d!^k=>_0e;!yhOE7_af z4&xp*pvCP|xFPy(GGJ=pTO-IQz2xWqZgu@AkcNL@@Yvdyh9^7A7|U_H``5V~#$Six zBE(L&%5NxmvodVa6joTNIcAEl?x4G>Jnlxh{G6SYED#y~t|aPbCqzaP!h`K2gmvcb zQ$P37#w8Hs@PC4KZ)h{1F~vA%}!(c4QE%WOs+lfQuKKMrvpSz^-TK6b}=Ec#Zwer_OLpq}~iR3A6a zPvPdv`F-6vo6WxexY(6ibhqAhWYMc9XAJh`+y`BuW~F1qF}g_3{akYFd=(VCD8P7y z8g(!vd-T5VW_+qUBn_)l94v!CXnPaE9Ya`-U`eYd%1W6;M(|^&kZ@7i=HkI^FmP3+ z4fgQx7r8$L#>r~&u9WW}X?ccy1EYYP>w&bxl8P1!C? z6`tA}Yy-h8+gNt%_|LmLa6yUChe>Tb3X=}1%mzK~ROrR>&YqBZ<6ubS$**GCsC3oCh+m+WrxKi|^=A^DltHKXL$;?*EoUXA%7>2~)*P%HC^kz~Pm`c3| z1McWG>J^Ur6(F$;TC1|W{=x(d8EKfvt$6ag?qihe^L$O z57_=saXc!nI&94d9%s1qmmctca!!wLwK)m~Q+i3sWi1@#s8xPhyn(T4sI|;h4#SxE zqWu<uF@yk=X?ATl0Iqqmio)xp zjC;*vG3yMpk(InhUC}OTd0;gOjW5~1pD3op&AciMO!lNH;c`>CER z?2oY9!`j)|=u!g`GaIlcT!E=e3V$^RQXUz&|wfAnM+MCH;N-SJ24*Fl2QWMLR` z`eE`K98Ts81&z%yyq8yWYiCkLk2Xd}?ID}51auJtp{%9B^ls5f%;)~QmPKRk55Tho z;FfVFQKxmY2Y0u&LFga$y!8Z@F@HhW_RQ-H;uZfJz}KYYD;AXMM`I6n3z*-4wmzDEd28CxMciBk5Zq9jzP#**wIgwQ1W9ulHPNu))z zSW8JMDxsp3-*fMs8@)d7@9+8Jo_o)E&U2pgY-gEs?})125@^$!2j2l;JfM60A=oAF z9PA$W-Ujy=)^7C{cc|0AWx{I9CwkLi;1bSOax*&clDV0y4BbvALp2Np_OpjFH;AV> zeO)va(%R{bTDzRL%6Uw@)ff-n1d_kc0lfcD;5Y+6h5}d_J^n_Bev(0V+e2#|4>^>= zwAWr+gd9fwbNfN@mHcBcW@BI)zo90ag`>%6=IiCd?^g62<-YB|E9O2~UG!dc+Mx8PiAByca(sj=BAD(&`YpWxL=rM5JxP zU0+HDY`A3i!AQLE_t#zCi6*ZaOy9Sa3(1WoSuUUQp3rwD$DpLU$Pv4D^^Uc+ctM<@ zNT}0)D7536&&RlL-@OfMUrh>4Z+C;kb0i{ui2zAlds8cn9+X=qtZD1eth^392*G2$ zkpY_d_p*6Sbb>fr7}KFOF=)ypMxO7RES#*|+5I+)tRs~4_eDZ)g?P)(WY51x(thb( ze>Mf9oJ%$CUA=Lz)kr?Ed^DN*`ZI14i8rxgRZXk|{r9kR3%1Qgc6o-4QZ$a7*rop+ z0p5^$Wx#&?IWc$s%eeI$cG-4e^EcLSq-EP3%yrYGXSsf&r6m4*Xj~UNX-!T71IIvX z>PuiOEAo?@Q`5tMbvipZvXd@ccUPqqlQTX;%L2!suJD7uHjWl1pJhc-()P!DkUyFs z<_EaTf7_KF$0lq*(s%5E8k-8=e7D{^sumP&ddW0Ck zKmdYy)?xm0QafMx?}&jdDBoz<)E(Y{NfgC_OA&V_e(Qo9A4b8BNp+mg{B0dPoZbvH zNlvycTFp&z>MvIz<>W^fGfIn4oy{I^b!=P-zo0*(R5xkyC`KPS(YSsnONs6_XR?JU zM^wDtYBWvje>=Xf8t)#EVEj|+Sl)I2dVROuSEj)+hl`VF%jku=B?%SC@J`Hrh!KB% z8p_q@z(uy#LGJ{-fZ^kn?37AW;^Y_aaN`=}n<$G}V#zpd7Gwhvh2Ce6_x47te{@u7 zU5GBX&pg_jD5GS;4jxLElzW8u4iVz(jV5Od4+|B$AiqQ`FTs=*ECMZWQWwBUW+eq( z5yKxAQ(JaW110Uoxeu6QL6=hrnU&vyp8Qg(T+1qPQ0m=5%Pm^Tw?s~ShA9sK-_;L7sl63cq>RBUp^(Vl(?AfwxT9LjGZZ+LaJRbo}2o4ver`^P1C*npfM`45neltGTP zv#!!mNsr5a3MmUJnKrW*wWXU1efpPul2}fkM4U?-F2b7Jx zU=WpssNX#^YfFuZkC&fF!-QH@7Z0;k79MKsn!LoqpTp-y!6-4vIJs2#Xd$g?8HeM7lE#s^F=`1S4PsJw~v2s73g+iP?m}SM^@!m5yb}Tx2s@7tg9K0 zpAUT4O4Yyq+ z#PQK=j5D;9H+;e<_t*x|O(G>>Kz8cS0i}8VO%|XV6sY~*s+b$EF340j-mQ`MERHSC zwdzOUX#S`>mCPUag* z<)I?P3t2DUH}``ehe8xz5ABY8knSHe($;}>Vda|4h!p;UQymFxoLKx|BhM#=GSN(s z*k}K#%+3GQOmB0d(0_sA&KNv!@kGDqqB1r7XBPgmM=SQ>AJ+|QP+;^)ANs)01I!|LK*xk_l| z|G;{Z{f0xrZ^jNlTC&CE1GvW%ABJ5$JRcsPJ*1PhKrs0qwQfp)NxjL!2J}At4ue*p z`%@Uy8sCPG^4vm|*WGaiQxus`hMI*F7kJpCUu@Vc zBkODCU^w}oz|A3r<~yYv{imGXQI}in9^xp`gkC(F-pc(ky9+Jf;R->kit*yi?%9%40~#m)>$; z>HK#3P3LOaRvTJIYI9wKG@1Fz=9AQo?@Grj_T9cw7@&tm>8D4_q>S?n$db3&Y3vp? zv(1q~5Q%Lr&{&`E>|2;}3!VIiywd_e=0AlG7Vxx7<+<)H7#s4OqqUfEC7^h4c`}Ns zlsZ)o3fo~5_^Jt&xUZ-aU{1%3-zf)jC#d8LfBOP%cFu%upnr(V+N)6IRsY6jSFi2r zbf3O-HXNKavz!kX(19vCs#Az#P_y2_!-gBq+tOeLLi+lnwpf)8c=dg`x;SvZ)yKYp zaq3m7Px*P1IhSKw8{E5oDgUuaPX02e>y#x)zCq4lN7*6XY*^uZve6K?2B0~10s4QO zv-bt^fyThRC|=S!PtXa#;2x4D*Ebiv`(|DIIo?v+5e63eduWmQe$qxdIfkh=<1}X` z1xUflHX}zi>FrRLqb{o(Eay`wmw(48D!&yvz*&rn%(qzi!FMocRUPsHYSjT(LnFnD zY~R#^&W8uMwi$$&qAlZ>FORSLtSt&wibm#Ot28iom0tVh5%L$3{{bQ~5N)7pdq&gC z>e=t~L}YL37hk=|-$~~{l@C`OLH7DedWDxd^s>8FGb&VU14_i4zhts-IIXdmy9Ce(U5)>8jZ6^!nhl>PB()a!=@s25(?VnH#?rGNyKULUQ z`Lk=hef|2!RHmvg&b@=rym#+p{V*qTnqWQ^MpmU4<}l~w3T>s=1Wx-{SCGV`rY<(j0*(J2NQy$CDEOB^3rmQOI zZlru)rm`2LcCR5+l_F~|vai$S+bFNcWolr`a1FjFMIIMnQ62U4fsq7kcouUSCnm7d6B^vn6l7LMvy2p>nG9r@J)Ttc) zz}+$1i4P~xCi!;s6^8k3_$dA#7Li8_Eqq!64@xP!Oy)$BH$ZRd1#P@O$B8%1t`1AR zQ<|3`UvyE{LVT1Vi7_;sFQ>61D57=|K~jv-sabySFaq=9WPl2jc>)c}PLUxJCm zJr`Iup8r)jAih-!IS_vK$+Cj0S9f5WH?{A<4@s8gjD(d@W%)LGhgUuceHS;^bo3HK z>W$;8Pz8|d02ALT8&}7-e%p=G| z6sj9RQ>D67*8)=aqFzRHolQ{l4f6c_=ni<-$C$3@e}h~W%xN)Dx`({SMFXPYihDV* zIp?I)fM@E5!wgKi0y6(!pvkGVw1|Ap)MMtBgeEYG0` zZJDJ_H<3(8S)OduO(N4!|3{Ki<+8?w- z@XzJo73>{$Jr~hJuW=_3|0_7$N%gvd=8)=^qnN}eXP|^&wR~kiP6kaq49Gsq{;%Ia zncYS$!ZO$13WGOX63g${RT{Xglh`d^d*#=gxqu!nkP;4(B}VaUarqYbC5kBXN}ld=4FhOH7G-L-F3ASzV4yZrTqUdYPX{D!mua zFbMFRY!1SN>aY*mzJE-J3lo1=r%+i4o4SwBca|$bdyXa<_QFgoeLq%6qmLz3Iih{k z55w9HUI+fkCwz=$3ORT^A*RiY<1O^sJvvye{R zDr!rS3E>&q0s-V8(~qM3(K%ku^Brl$5_NcQP~wJaF%7~_y9>G>Rcxp*>(ESFu-(e6 z9>U@NG*<6vhOLCt?E7Vv%Nu-{t&Z~FuW)Q_Fz)4@}PI1In`)Q4*ccA}XY$q&#}$X!*i zwMJ-JaI5(SQ#+YGQNcou{MX?=J zDA~w_2!+XEkOIK`BW%f=qdfLp70ZI7O>OHzMNX)?gt; z(^a6o-T;)$;)PgP4mN(X5qBn{HF2>J?uzzy!3H|B2HXZefK|DD8r)@0(Qr1UHtLt_ zw;5g)nAWdg>@=*fz~(?b{-i_?#tm^dMeW(nXE8%Wo!qJeCbDN0%8u_GN7btt_fk#i560S3Ljc1!aB{{VH z`%im(@edgvP_Nc4YW*x?7u5<$acE|Iwt4m-A^-X zI>cGUGwE zKXos`{Xy3$(zB3SMGy&c&9|-ggg_hxF_w@S#784p zWh#E$;MuoRfT1F#>z)ym3~J!E{lp$PUT3uJ*z7CyIA_yFCa}1F^V{eC?w)x=!7$mCwOY=vfLfmKx6zp1b zYp*`lC+HqVLt3n`cLVkqcIwmhQhvrO5 z<8(vQR4}bf!?$p!N-I_ zzl@g6y?cHSioI#P$(k%syKQ_~BVF`@RLA5)wbnniucTzT(&*|KYriXy#@>`Ny{#)G zDb019ProOeD_S&SqPRTfl%(e<=N*v`HOO}(YODUoWW(tpO!m2RJ!(^CXTztBd|{T| zxX10C7`UEY)>LyzzM(j_DS3N1%o?T+J($~5^z z^r7~=Ma~zsdyfO@L7M>IkR!-%H<_akUb>PX9^Ykz56P>lq%@ld(e|sW!bAl?ZEJ8 zaOYw0*!J&^dxnhigCCYO@oob7`kIKK4S?0B79TS?ZF?p{yfNAr_d_Ua{WM|z$>-wc z`b7Q<1>rQUH-JS`OtiJUikSJuYhx9Q6aGSm?F+GLcOn=zm z#z04OkK`Hr0kSw(gUf~NlDT)%c!q52{$b}c2s!UFj^&5eTK;L!^s$2(1F>$+zDkn; zN2qq>ah%orim^UUm0Cl2Z1sxS`=?dpAxUQ2;jvc#iDu4_e%Kvb9og;s62&A%TPKG z*!Jw|1)Db%eD=o+r_A^yWvCrOHwu&I*ZPfkTRJbQfwYS5beRE2-tfo%-L_X2Kq#CE zssgzSBMS-@OWd6Jb(&|S>li^VEZoPpLc>g9a$2i$@9y1eFUJx_@V3CM5iJ7P(Txz8 zi@SD)>K3c=wmjJxs$ZW1BS+TDTl$h)E(uLN$2qwE7#*I;=5&)AI;!8iIhXP-VBP9s z%p1UMzr6%Z#zPbHcca2Uor#|n;qn@0$E0y_D47gLt1mJ@D6(t=yW!3_A>~KQr6yT} z_LR4L=GvGh{^W$r?}y7GkpqpB(bzsIhe3T4JJ)p|Nz4 z0x1z9fzzr~?*25tuOOXmSNFolkx|mcaMqH1>Lm~d;I%*k5)DpYmY+-(fGG1 zC$!pHB-|S6dFuh;iH@#(HZe@MTUXaaScLve=x|XCc5rcWzzjw?Kj(<}@|aJ>b#Io? zRo29GF~_@@NfpKxWYDlY=jH%pg(W^-N*sd5v?oVxE5AgU?|=1G`y9%cMdFo{#R7|r zC!+_Hk>}x;iiLo8E?rOM_mpVk0Nq2%KF-~itjhQI zqg5Z+)e{AWHsMo4Bu~Shn7U!kGtA9YeK~4G(roEm!>u#ZPH%hB<4WSX)p8}8Fx#WB6RJ$I?ie5Y1I1m_pe|kAYsKJAxJ2|mv zlk)nEL9psJCoZ&)jgBHi6 zd@1vI4;U$1AgTaF9DOtu4BN|e@@JPIfhNQqY!SKE z%JO9(zh8|I4E{(h*`85mIyi`h#Eqbesvo1D7*<(3VCHHqTtf3{BL8>$V0V7 zx|DU1XR2UA1h8FX`!V`Tt^MW8gw`4-T!{(hR2MlV-T(EMb4HnpN%Hw-3uxpU&*_k> zc=fI)sG6y+pFw1rXYpuCeHw6$x)DXXwTuTQ3FBgxw*aF|0Lz3hnEv;;k9fPc%&Qmv ztLUDvIU5c$a+Tg$<{UN5xT0M1b*bAs)G8(iD_;KEdsX@Qua`I3%f}9a}6lPx-jZTsYQt&aj1eeno!F z7wKh$%*N+d+RXl;)s1bg4_PLmmy|WEAl#Y9{-%FE=SjkfK*<5vl41fiP<@p*jro*9 z2UxRUjHPN{7Nv&)ol@`jB0_Ch`)U8zk=y#nm;+R;OI#@3l!#11HiV6^9(~UG^Htww z5wa-52b&}7pWAO3ZK^&+Kfm)IiK7n|E2R&fyExPSN>RDr`peos`paRD;UDOIX-b!& zAmlGYowiF0lctp?SvqlncnAD(M~e_V3{t zhk)Jh%WLrm2w4?>mz9m*_6M0@CFEI?z~DusC3gqSM{;Zm&%x9YU>-RlpW?FDQA1w) zk4H1uQG4fipxG~>iJa0_N(%(m=vUE=d?O?eyD`wQ{LIazcreqkye)*>do?WKmVohp4wa!r z-7hsN%zi)HwgeBkr5xR}cjy$;si(50*88WYM(;VCm|NB0vnp$&Ad0Eg14cl3@{tg> zp5cn6tEp4idgd#ZcjKKPWXB`ZyR9vQk=nPBikX zNtf2`xJYOS3^bKrEUgB)`wJzlWtA$3zFCtBbs}#TinH=m>PCJp6sKX3J1_plh3$$2 z+{(yXgwS5~j_g|@Y@+$*SBrGUc2J^Z02q-_U$TXX~+kfmOzdHWX3lk z1_Otx{{FvfRENl`&z$4f0m3Edi?_ZFQe`EOvcO#xWMtVZq`5MpjKl69`+ry3ZOhx35_) z*lhFwB0y(jIky#B7*vajpI}$<#;EAf7N}->!E)s^QV%sRWyke#RF$veYp|u_8T2nzpTaCL{jpL>kf{&Fgchi%+#%1>#%BR zA$Gq_jp$jR?{=70LT0_5b-kVtEP z5*ZH=K!)lA+syxhXC29+%iYKigj-{NM;G_$e~}=%tTVYEr20xod~)CKj_z8Pp)+7V zLivUP(VxZwGprA>RsEMRPJz<^;_u|j2*UJQhw_~(Lc`uYbBNysX^q#44`M`6S? zK6bP%Q1pDfj;!_{PG6qi+U{2-|M-8uqQv>ZFD~&EN{XU=i_7sW#RWu1LS9699|G%| zs1JoLU%^V4@7T&Y?2NoMZ1rPj^;%}=jgw+GR2xI4$U>PRwFB)^n6NDYJBl1Z(roQXF^h1)ZDtBXdWkVeln)^F`hx%f?yocX7=+p>$!AX} z;p9V0G`JChp8}np-7xvcf^)ok9&TjUrDB@6v>z~RZ20^0A2d>qeq`LDbd;Ol;zE=& zb}|8IZgqOru?3CNXcKVZ&gp|?wZJT!PT#vA&nJ|~F9ks^IEF7eC#b&^bZ0#hT~GY& zz;qdD!`;?A^XYohhUm6d^PwgUxpvf4xSRJqmGYg+A^ye$k~LLk%Upi-$!jYpWt&7@* zoan=h^g%R@`J^Z6@HITKBclD#*`m!?UeR8lWN1U|h<@E!V@CE8Q6%N&t=0u~*2(_u z7L97{^sBiI72e{Og?_aLv^;`FH^==4WgGAz3$m%gUtu`DsS4F|uYy-VZ(d|!fd!TW zbUAn316MUH(k5;=3ctj(pFd?Xu$bvY4uDK?Wu6nB<;8L!p`0vIQ53BCJ4HrEj!5qIcAT&)cnqjBopW%#hSi zvrg6Cv+!tV*FuJ4@)~fU%rc?k-&^$l@iL3TgI1A_x6kZ#{{!w@A6==a=zyHjh(O+x zsgE_%vF}uS{kvjU-ZAKJrYsh+b-o(XC&PsG5^chUwoSNT)@>ihN+`}*v-4^^H-Sez z#zx9`pw1}lUPeI&io$y02z0dbnaDSLt;b%IoDjO!=1ALyBpI)BQ%rne4{l7C0gW5X z9pQDJy*hD)S9L(5dGoRO>C)m%*HjPDjR|3}4m zA(|$Mpq(e5y*=sF7%s;cbh!0w1HFY}vM$?%K)bd2ZC$np9qpIDtl`4cNrAWnFR$}L zqmvFDoyDJ>9w$kSMXB~6ZzQ2W62*R)9o}`YJuE}{+B@Xa{-hHdP!~ZlsQuePamThp zKG3RFf#KyJco32&-w5a?ily3X3Z||#{0N`2`n0-zre4O&!{xl=K)tQWJz5QNAL3j< zMzw|WxgH@|^huShwDAs6vF>3zL*pG_ zZ{iah)b(#T;frRH>mP!A<$hsZtU~i5J2ZJ0v$+Ix| z4kvQvVF{SU)s%)iN@xC78Q`7FXu*7?|0SEKD;PlS?wV2a6AG|)o&3odT$5@%lbsk@3_Na9tuKI$-xAH_PQbxbf@ii3cc7Ew~xw}L% z#yw8-@Oc(O9W#s(>{FsNUMN+e|1t*o#%8I$>Oxc@V~5nLhtg!=jilKM{&&t9ssaq2 zM(H~FJ$2^jIQdc{lndzIX$Qu|!P5!;S#W1T@<(xpRW#8Qy-+0HRdyaUQE9&NM1i6I zSI~g=H>=5vs>G95-JKLkqrZ<{4#^b(gRF zgfomIiX;DMZ746$Q-Il%n=ozw>FTaR4L+Or6wMRQ-F26Uj3TMwm8_#s+Dbw}5i8{I zOf(hR_Tve%uFP)!Xl`rB4rhR9+RBwrIe_5s@4UN>uq-SeOX=^S^=f~gfVs~{ zdR*dV6)+fEtc6(;sUHa(2WCKdKfK{GChmjQfL?`h7WC)C@Of`6;*U9=IXm*-F49T} zCT-yt55utHhJ1ix5lEbi)_~+jRj>tJ3(y zdnc=Z(iX zi1>51;Zq$zSrrU!=49+xkg}4gj4wu>Rt>KmB2Hp@o)J$)(*7q2|CSO+av2*YJkiXF z^$!Z&l0_eslsuxli5r56+a|P7b5!X+HPJ_M7V_1L=@FO$Xdv%v`~wu3>tw~AlRs~C z=mO&_iUnV<|8bBPK_QQ9z8lCOGi@l`c*K=+{*3U~Q9Xwy9-Xw$q5jgf(-4H4o;#C& z3#~R2J2;J;EFVq+Oy7RDT?PvC>OniO?TMIQ_%_0@5I^*W&<=IdpI^ADTb|)ch&^se z@q5;yyZoaSX(aWouIu{l+!tf}UtMd&_0si3+j9Er3GgAZJ#m#F`|8_bFMi0a*WGbU z#?A;81nG4a^u}fxNar7D3fMc@d5$R7=8O=F|5~$~tc$Ur6Q`LZ%P# zA;1>3;mMCbvh&rF`>E0jG&0m_|4Tn$%|zO>WOsw?MnxK0sh+|u(M2|s4NT#df%MY~ z*0=DPe)8uXH>U#Sf-%drXU{#cH^G>JPdaJGtaOHlfHKZwIxpyeRQL^(=f#JMj8EpO z;X%nk0bHWW_pDnBXjA!&@*)y?M`nj3swA(cLwT3}Qv#2()G5$|8%y$jCna0^tm2qdODG%`@eu%E=aeXhY+yP@vekI^s4 z=~MveOP~wyidE>tR8yRxWW%4M<1tqR5WH{0;u|A8OpJ zKm%*1(7M({XmDtZ?ogUUoC(}6TMRxSXYw-JyNycXE+|QqAX$XHu7bjq&fmX<2 z#@07xAKNMrYqttO=K&<1l*wPlW%qayB(imm(}+4)Zo@@ehcA~JMlQt7KXVJ+PCgy$ z$sp=6aZYBf1M>R{VD4g_4A;{Rtoi;NSD4A#1HkjGe_S6y1>d$eD3;%SF}Zp9Skqoi zb=!LM3hjTp>Yh52y{mwJx9joCOv+SCN7v&bERc?S2Un&__FLZ6bDD!+g{j|Mppof& zE)f}c_*K@=6A8zB9C|26ix-mR zG4vr(;Er*>;$r^d9l4-zCgXa|-@_%utToVw*F1=+a7SJ1O;gD^!p(YNs=zu2pN>EY zWiV-x1bRn4Q;%v49yAOqVqIVIb5QcZh8TeS7H(g6{MveRcbb^0tr zivI_s+~6yxjP`SbjvJA+k`Y@NUj%Q%tV4wMvLfiOdpA^K%|fICf1tYfP2K!q$-nP@ z4d3oTDd!C?8>D@n5!`qD3?m-|dH!#NB+PlJ9oSm{q&5S24$pw|c%btV4iB9F# zp{?jM2=@uH76XgsIIC_6!z!!hcj!&W$PyDmF&mIG$`7o!)2cyI^zm__ct<4m&6Z(J zaZ4>2%!Z|45px$s843JQ0TG!JJXhZ-{z^9KH9d<3XR_`k_MEH9`X}fbQ7TRnoA>Akh`{=7H3);6z#XVDa6lz>Vq$s~7L*|V z2y%lkc<(|(>)UB#;_JAX6*r=csru*q6>lWkQ5#tv$Rzc(K|lfsYX6d`u`s@7`exre zopL;Pf;h%F$TbPuxr)<-uuw(x()ye>=kb6?KJfI#wKnuc3)S#P9=RKjY;16F;@d`=bEsOI%d zYTV=4FFR4%@48tbQUTX*swK}eqWvCQYk0zLpZ?Hz0XD&iI|OqONXCM)! zfL^4lPZ22m%9^_j)i+Y~b2axEs_$GZg}cUs5M7A;==_(!m}?1MwQ!ex_3m8cI0@+X z7#(V--0`n_ByOcNUOoR3EKGD4*x_AKb^oWv*TG~EyoD&HInf~%ucL=X4*`$I{$RF` z7PgFH(epzhWWM(}TlD?S{*5p&ZoM$7hk3AeEa-f4s&#G%4HeHZvKS6 zjV=QGe*9_u5O~SAi&2VjiM&NTfb5|aUbr;7&cnyXH2y^%%mbD{qR~NK1_Qe#d$3gJ zbzo_GotsiyGPGy!tKfP*rLLcc3?0vbtO!Bw7iJbegOi1>F zC+xQ1H6yB3Fow*OsUm!P%HhbO0s_`=hF*ArdGY7YGhsX7ZQ0kNBqV zhm`nw7%iKhb7r!RpR2t?-Cihc|9*P!25h<2a2n_qrK(=Vj(iYfPv$6yu-l@<$^H|# z0XSK0`kV(s^peAPa&p9Z1L~56q9EK>L!1dPfHSArcW*(T&lO?)1@SejrY`%#=eGj+ zFL4}v*%NqEL^9T1^4?5%&*=5DV9iGV&x}R$)oaxJ!<=`s4_~af!88M*Yj|5Pwf&(v zt+#FXpn=oD(wK9gJmQtEgqt5NQh*H=dTxsy3zN0fux-;5;|fpg4ItCe$?LLY@ekGI zQy0WxJnUjwk5<;uGd4drz=VY@$SLc%%d2m>8k)9HxTs^jVzH%=ObD2i-6@>Qj^vlo;XD8`#_T8R-BUL&yub{}=MKguh09 zJc50o0xcfy7S$o`|NI?QEv>@JIsQm}qM#Y1F;OJJgcnn21SPNaE7jkF6K#5oh1C~q z6*sQ--H>DZ_~@KJT6mlX0ta=Hb`BBE+Mz?I$*<#%TgR+K6`GxRRoeRnHoY?)@22m< zX``tI|AVdA_nwDcllUJ(mz8u+EMk!5bPK{=2h`CVYR-?&i9|w&jTZwOB_{caO%x|P zF(Em-4mD@85@~k&CM+bZ8%g?@fpa52$xPbI0;9u5i$n=W2*9InJJ;;BafJqh>DVsW zcR_DM{cvLi%&^ioQ@g=q-yXtyv!ptD8-#pUleNR_?cmpMk^9zp>UN>=7RtH`U;;x( zHpFxjg~cDInhf|zHkpB$c$uES8P3E^6#1XJgPPRcgZYQ!c>_^%CuMKX6)1P@{n?28 zb>kv%?`wDwlsVg6vgh<-CVEHF_7#`e@t@2kkJLbI^yy&S60;7Alz>blPxlw*DYO4X z6$F=_NW8{&=+QPvN7({R%)ya1!mEq_KkzOVTaf;P5{G;=10d@MSi>4;Zm{T%)yz9n zCiO0fyw!Y&dI^~)7DK{ortD~Heez?zE@+RBNMmO(DrC}1{qD=NGatXjn|B3y{#kQK zh3`|dT0kTpX=5w;2idS|9EK3!?|-zPWp#9kw1asK=UTQAlxWlA>WyC{3yw6Eh|?M& zp7eB={MlOhRu6gC&GYQQoYar~`}IUyZZvXzL)jtS>vLXxlkfQIg)xwh$@B2v!Uj&C zv~qTcru6F!3v%s0)cuGA^B)}Ew@yatWT<_S%zgQ`50}(N19TcjF6PJmaB?LnE_0Le zIM9@@-}E@NCUN2WxW4`&!d2Px%(0ns$;$40EIgHcslCc_+pzcii3c-TegnSYp9c%B zzx1NChNY@nomcjmZLCbw2fD=7rWborHmJNRXv-wtQ+do?_c^7hAvk+zS~J@}&cZYE zC~ENZJjJ`4%^0^yW#4= zRnY5N;wcrI59X)joPsvg%eR$aS)1WR%2~E1sWyYGGb5m3`Wkpef$WZK?C1aFS^fci zCU%3fTL?z9)q!`D_eBK>!bl8v6Qa%svENUtN*hp2{1TVj7pgBgJepUD=b$NREAka= z@S7lg>Br@>55}B+Ma!gd9!G~JC-f9|Ewm+ zK6x7PGhO`D zwlQ*lX+_b|klUx;S`G%)cC)DIELMZNuq@J#8>l>-Qm%}3KXff~FTUrs0u%1~kqf`x zZzuIhiJLAAqSY+qPZE(o5^Hk9MuLB6J^BDs$HasH!Wr&80S^?bUzD2llq{QWYxI_7x8++lb`&^(!<{4l|!(9x@ z*R%;GL68Sf_g>j2W!}7SFxT7k&5pdD)Gdbc*1G6 z>U}V)k!W{&6PnM^n!>&$xO+!~kX6;}Ul;&ue$QYvFy_I3YB`{<(LVKSx4iYM?N+hh zyY5&`(jPCP@V)wT?##hr3g4H{mzes-#La$#bxjG<39MMXr%w6R^taR_a;WIY(RvW< z+{`7wO{~_T-N!E#W)Og*V#3Wgv%JH0jfh1Q+(=(l{OmMwx$rDqduLUoz2@x0!|Wq9 zP;>AeUvA*hRm@+ba(?`(wV^s>{=U+{suYy-B+~DU5%wh(uU9Ja0?>hw0X@7e*iWkA zr~4(ux$@iay#v2xR9Ai{skiMv-);U_r0&DkRh#~_{P_0qs%p`-H3!dNrgG_`h(@W& zWmOsp>J$Ew=Wj2fQL60?W=qmG^-E?it0^fe z#tYGOx7IS2u*hp++b1->GXzbK%nAs1yjOgv8Y^IwAJ(z2zsya~PH3inC-3^;fx_a^ zeGdKf`CQZ;0Lr1DPcChw}u}oP z@O9-=Rym%cCk~qmL5nsAQ$?;uXzO)uINIn9jSeq;!-K{iS%!9O2<(x6xV;$qi-N@} z?2~*#hU;SZFNu|)Hm8s)?yiXbnn;D0r+`O5&$dT-xTSzry^srpj0Wx|udZ4FIK#Uxt5MgRqOPqeesjd17#*rGQgaw;_!t@H9GKNeipYT29u1&bGu70V z-jup6p||e@v89vj@^ZAob`Z+E;Gf4f$_inv4Q5tvgot^{!#u zf@ztb^E@Qw^{%&iy{d#oBbkFmrfoos0qu9=; z>fk>fd|i`#|F_>%>-VrpVEO~=cVpQ2yS(MWdPdC|60+C3EVl2UXl|(w%DAzdKCNXr zI|K3d(MBCEQD$Q6oyN}Gapw#Asa?BD1iu<=(Ey(zT`lj$%@}2v?0)rD`0~rmUEi-X z`*2i>E)f;*=!5)raa1^beK=t_`nIoICV;*8MJ7B`(%*!mvduw|S5>5{ z);(@gMl*-&<3w8SOx>P4306Vcii}FI5x=+aw-t=ai!iLsa{l0k-_K&=!#FO26;tUu z2u2HrCP%vnE=H$~Ogxh4^NeRenV?Q3WLO4xT?cH1?cHE4D%^X!?> zsmMGVU|9Bw__w07Rj93F_&OfMQ7(dD@D~H&uT5^AIB|_F*rHi52=@jQoop6@GETiD zeM>REiNE3fG)l{2nfoBM`C!49U%Wr*z+?SGVM<5vJOsR%*Z20>K+}r1`o!%}B5?VL z46wsj?zgka{i_4`!GN?zf4sv?sY+Ko@?ak)Y!9M9eiQX!&WLB?tX!}Rr!vnwLscJK zo>0E#4GoQ;djI#zE7pbXdg>~hf+x3%U*LLAzaXxG>Hxt}A@0^JOZYaziw8nL# z^sK2Ta@!*{#27N^83hJOqHNWWW<`iX5Ffs11obC|_kW)#{|rXio*4$uQzlCoM~Gse z!Df%&qUqcu1Wt!C5T+9o_oBj}Y?%JuxpR%5>0qzMKX^;;lc95Zvl^+p!2404Zu=;k z8VqPUf8Tkd(sl~ooHZLnKEEATAoIKPFU=O0*9nmf_1%!B#4>C2Y=8LAjv!fog}kXJ z2qd5suX6v(_wA-&QcB9GRIqjhHD*=LMZc^ChOZV459N7>i#~Yw&?EMTOwEq3j~^_g z7)cP$h>~y%>pEiaQZ#AhZZk_W8sq;YWF^M^hoqQdrC=P{ERlWeY=Q^cy2XzfP(y%h z$wC@|wWZXmR-}QA`*x|7n^?nNUT!NN*`0&)8q3$h#)>xz;-9ZvcmK|2u~$sCU;xWU zpi)ql3>@a{8i#S~IVcDq@%c&+E{82nL;5_-pD3e#CG&#mIj`nSv%6z<;wC`wHI_gt zEx&>7>QIN?b&j2!fy{b6LOLVZ0Yb57snV}e_4D)fd33Ll3?$0+dBbAGfSSWDbr4-( z))nMeN?%hQwBC{nll=5$;oJ%c%o=wBX!)eJR6uUO20>{y&k@b^J`9%T)L6{X{41=1 zdAE1+I0WcgVIp7{KoQdlAv>hKYmG(JovvT$t!J-d}C-OvJ?K&#g z^bS`o?=!@=`N|$>i4qkT(eRtBz+`{0V517a9AR@gbEl(Qw18dm$m1OqhQeA-g!|8o z-)-nS{|TZ@Sp{i5ve~-`!-okd&+K6j9Dro89++x@$y;XqF2id%6;;xvlkm zO#E_$O<7?4!^_0C8@eO<>XKV4?>W#l>^+`UH8}Buhkke{l&Y`7SPmj!SwaM&0DcSS$Q{%fLi1=*hjUJ{FIwd3bKi#Esp+_>3 z?Y*at+KM$d6p`iX>dK?fb72izFf{*o2S~o}P~9gi0-RzKp~-?$ZLBG#wkR0sYuD!5 zF(Yk2K7dUfyZ>#>Wp!-z=qy#c<%~CAvI%=MOK(cdiP#jRWA(^xN1;Asj_=vK=Lj?M zT_VT7D`k`_RV^HZ5fbYv!4;Ft`B-H5`Y(~e_0T&7MtaxiD9Yp9>|Kx8qbXbp>rVYj zcX7*IZXeJd1hY-ERa8$tcg<4Ur2g_i=*^7AG8&t+@`Eqhk->vNW>v@MrFK8{XvJ9a z{DulxJ%_2NOg-O8t&$o4`>3MG=f}hAin}RVzqlDc?JZvb;ZAZv05MDC?=xHUd> zJE7rgND)e8tXg`67_;yCRWU}_7wL(acTN=;7b9d^WYf74zlSP>nhFIZ)`crDiv2TJ zA+Ij`dH9}*-}lM`2HlfGvmnQPuOkDgi#ATaBzNR}6xv9a{Ap|3Y!N|2nQ;Z9#E=_8 zp+kZ0sLrvDz}~C8J8Bh01AoA6U%f7RyC1yYs_2nme4_hiMz6)2C8(>(NZV#)*xG=ikX80YvUNPFTvLe4k`h3 zofzd5)`ajWE_kJ=eHFatXK<7lJ|5Y`pen_UioRoZ7KJX{pG|vXiIeU7GtF zgK9MytYH~R61#v}2ZBu?2U3u+dJ#mCb%>$LN}aQpj;=ZDQDr(0W!Kjfl}WYw9d^t$ zR4Q1by6Nv5P*}apue3?gqa7Fc2(3&jXLe)@LJCT8GF#t}v@t;2`fN$ZZVDudB{})OW?ts zXd`&@#KIo4laGSKyIewa?m(smSrJTSRrqoEPbmZv>A;K)wYfN#w`(e(IdCykf(@{- zp9ua^AbrAqV1$<|gU&B4fd7MvU-Dc*()qaGcOP&)T!0+jc^iVp4YDXY)1sW+!R=hn z8cNAbAUb!k|J4Laz)BFNc||wJs)JqPCU<&?E0}dyJw{8MY%A4L_5F@1^o7pFqkF(?tJ0ut6Hg6MSEL@Z`yn&?8M`q!alE?*(?;ZQn+KahX#_HqNSTLSxR zR8~xYTDSlG8YJnqbnK1i{!G*_L}}TxA`;HVXm#>CV{k@4FTA>w@SmY6Kk4gYihptlHXERtcMWuFC==aC#~tW*QVu?H3`BB3rzmFI{rmQp2)Vyx%)RC3(7O4 zw?MV3F8IBV^vWEUYr57coK}>A?pnvAZ4=Cd*KhoDD^_mXRkJl?MiHFT+k18FdAUZM zHClxtvQ+@#vdP{ef^{mrhzl1p?`E6CVe%v@L5YJC zXp}`24Bo*BE|eiF*20vA=5eLetCr%NW;m}n4H6?heSjR%3lJ7(m69%M!lb>HDT*2z zd?q>>^kh44xnMMY`FvqT7?k|loU-urIL_xt_* zKELzFojWsUX3m^B=ggV8_s$iTPP|(jH_Nt68l+*FT=)LDVRSnvHeI(oO7<7+^ya~% zv!7mb-82JdJ=y;fNDtA<;Mz1udutYa#W zAMN7^eb$V3z#TbY2ZiL{k^FssMiL(%jD-09NW80@a=00UWMll>*Ydy2M#01@94g<1 zjgb&+4-NT{#}%<2`)M%QN)-x;hz);IHyb@^y43VWOj~%KpJI~B$ti{+esSq7rFTs% z^N!<!;Boeercxt?(G z^-<5(=(C&ZX+QH@#KCl7Feza$;juX(8%B~1mJj4tbwK0O&)@W|fN=Avmc(zLW&4VP zX!h}FugK}8Xt{BlZJ@)cl~FC%34CMk%cqQT_KZlDh&jGo&^6AA7WT%vAQE7f06HB6 z#a~PFGcUIhG&rvy$Sex3H$Za*aGLfW88H+_7^r$}+qG7{^*(+6+Ex0;vLE&MBah>R z|A|eWS`#RB&Deh|P>$gQs0P5ze00V@Zk>92D!?p|uVVY-ND>0;w zw9T)K^kGSGl32-YGbIrC-#ful0cU<9U+EG)T0gljb@vM4dl7`QGYp}{pDJK4iR{&W zP-w_bkCH1RJH7?*D4am!vzL6@7D`XwEmQuMn0@$_kp~r3P7Mvv8S>}2wPaWR^_ZN< zuE*q3N-C}XAJW_$c>Bnr_bSC{{`^-XR~_k&-alz_l-lZ|dm~n)(g!S^x8glZORr?m zMEruep;G1@0aqZY4kl{m;}Dm=)m9sV~A zPAbnN6rDj{nGV=JxBUvpG9b>B=VoZVRT9pG&ERYSkSuO=xWm}UAp1ycS7mZkTlfAY z#(?SmT?;X;S+XHsjB@?JRqL6S_qSrRAdyhN)v4*|DNNE-o}JAspY8xc zsmuT73?El(UF8MpM<`!QpvdjWC3w7Vq=!amv-qWZ5?s%`qr1?!(semBoXkas4_=6d z$pTu^N4|Z|V<+n&?`G+LJG;8r%5Nt~MKcxc4J8pPj+XuJ3wfD@Ps?U-BQ~yw%u}t{ zZ2d+6>XZr7tPHxtY4fER#BBj=Ia}LT!Eo#r>qMXlrABD4O?sjx1^Z7*&GKOVl|c~9`G=B_naxyMi%p>l5( z>u1Z-BD!yUfJl}4y~}+zE|=k!YSQD5iHcL!^JCdq*XM~5HAr%pf@qO}^yaOY0?cPB z$5ITP9}wSJ-h4`|n=vYO)jIIqg3-0<(;C-$POqJDS8Vp{FZEiLVX<_Svr9B5>q*6o zsnGe#c*E(K`?lUXOBd89{~=ZP3rEo)8WtA z9#$=oakL9;!Kh5Nn{hon`cx{P-))VD`7m?8c3EBWLxYK;>C`O#ySL z!zdQVw|RNavLf3PCm#99Xuo>1_p4&V)BR58jbuiQ~ftKqs$bO zs(9hpN4^z$({UA|=-32?C<$_H;qV>dtbfh{;@GU;_iOH-JN3@R#63Xx+Y?=%S^6uT zp_R=7_>k(nFqYrnXzR!LUWUjUbi3&HF?%v^P-y6{A1lO+(!Yj{oE!k!JjP9Q2Q=4p z_LY5@r9Epl{G1GpjEENzH82Z^^$@!fZy$~x=$=n;*kLyxfx9yCU~d-~8~(jJT53TX zo+2t&Va{LlJWedC*OIF?Z@;x*B%feXA&R@-yt&2un3S88twVtjAKB#& z&d)^{y?$ydiotULvz2)5WPv=mP3$4k`0;dAnL32H&K8p@UR^?*t%d!3QK}D@zr7kb zg4U>ugJqDaAEP%jHZezTj>>VV~691u6pUr-G*nAkuMH zdk0DzQP$#cNfyHc&wJi#--u`?T(a2feUzNEZ~uH0E<_$JMg^bx6+QfDWtQl}rDjF~ zYl{5dizgTfoU&=W;7S_$89`k7C{%hWbQ1+ywi4R1L&Oj<0j5_C=Lj&?tGXdkh3zWm zFB<9V@gx{~_ZN`TIY7+k2{Lj%cI1{5_l=WPy%TagJ@s;;Z9_dTM(7n1Sjzz2YkDsp z-(f&FcgMsUglQhAvFf04U5Tv`-e~K@K5Qcl!Fb6WaQGG>Rl0~Jpy3k(ghP$Fjwlvn zsteqEkr$%fx?w3Yix2rB1r|fq^Q~xtzPZizTA&0BABo1m=9_93?n%;$+}F~c6gErL zTBUEmoisAwKW4(QM)>;Cfxh9^A7oVA7FP$-ARrC%RuT5vCuFu~j?a9??FGb_Y>>l> zD9he_*4q(6Q;{|JA~+aMs&|=Y`vhcmR=~LMKE>wj8De+I8N3PmtU0c%zYmGZ%B(N0 zDW6nEw(El=Xm;tR&pi7qrZDm=d0I8=_h+l-B(x;$A_uZN)&yjwzAewb^H*T)wXIis zZdBno^{fCA9~G>uYE1s_)g*617`)-Db2q73tu3S?`NX&h<5=E$-dwB_?`xG z3T07_6Y8&lKd$r#B6q4ml6lXvJdk>xF>UZ$>cww-C`tG2*ZIx{7_+IpOAF;Ye|}Mo z5IK08lKN-=-0w}K^j|-z6mv_Is5#4)fsZ(r1AIh*=r7y8-{*`b*@cy1=1l&YljbH{c8XS} z3l4s!V~{;prU^{L?B0wq6-(xP3MTeHAvVrpbq&ll)IaanzDG01v>eFmU={(JR#b=t z>POkL3I_6W4Y)aJ4FgrxkRg9(wLj}t`8P*@W;>at`l5_V2t2+yXEO7 zDpuqR2FugqRICW4S$}t+nn2FEE$xuD$h;0v)Qn!@q7vvZ{&OlgKM`*ULeUDVbXd<& zZtg_3qC}y~1{UmS{t#xz$Uy|Oi{uiM|EZT%}lFP?#loYN-ijKY+DU|rhVTl z*fv&yHkz5re)_;0J|SXFis8H5H;7@4#yl+Q>*MR<2GJJF?r((9(|Tfj@cvr<;ul)Q zcgElQOdUZpVMyD2aWSN)KtSd}mU(*e0`w$`Zi4Y7>V6WsAEucR{;Nb}y=kM7Rua4Z zCbLEcZB|(AV$i>&mV24nvRe;32|r4EbI|F9tf>Ah(4@1%SA;v-JF)PDAbbY#aTUD) z*>yE^Eio0Z01IaZatjSbFJKhVvxphaN{=|CkNBMXhBAZt_vA=9COUC<%-N1@9oaKq zNnWmrK7%s1^z(=pT={vsuBMn$cP{qVS8tDL&MYl*7*}xD*#`GWF>{sNXoP_X)U!|R5N>zL8m!ehe}R`x zdQ;btUc@_e{SA6kU%ys?JBWL0;`G6oFR#|By=&|~SiE1{0V_(s@^r1^;smqxS^!l} zdg`ALHdQtj5@W+-HhngBDrV}bIc}{-S?MSdsmLO`=e$t~ zU6>rg7=JQGXW|8I1%44z9U2khfBkIjf>byQcVvM98Rh{ps?JqDs=%!1B1|-Lc-G>W z)mWU{APEM9P&5^|;qGNz%1n3q7-b~h z^_<6~iL;_=Om*o(6D>Qu(a`?K`crR#6WB` zafI;?x^GkxfcIbi6L>HG`;er*FqPaxM)^oQU5HjmQmQ{M1q*I+!&+xe;JChVJ&yi` z(s6Q$2^+7#*q7;(cp@4)F`lv-k9za)7;#QHWttXYK;+C3)mO$Zjg)VvoiyG~U@tfx z_uBeZ1AEPRmNuI=HG4;4S^>}s#MA|ifF&T7<~O|CUPh@m=F54rr=#Taw=jzBDE2B& z<`zL!4;2_u_SgQ;4qRzHawO0(9-c@`8J8D|A zcWVeiXw~{JE%z>p+I{1x%np3Sq+C{Q_f+k#06i7GLNIYBu%Icya_1nES znCG9lS#;;tveAIX9|7;gYwuw%8IQeLeid=U0x$YOBg4s}ylS9o%4Tohv-vx%o&6bc zxwK6_h3{Up-bSsD8BbF3^lnLMBZLAFJIyyI_!&^R{9Ss#8n$Vk)kb<8iABo$=+cXDzE*Y0@&#kl?~mERL}jz6lqY1(Mtg?11k zX+}pm9YM-#17u%m_g8!@pc_NT#w>WlhR=zf-7rvRDK3FB5RSXv(@i`Ue}*zF-U?47 zGLNs|M*ed@2Le6GUuBd|j|lf9Uo*2j-TNtbo6QZsY#Z1beW8NaKi-ec-5?DK@4>GS zeK96RqE|syL^h96sFIAXyPx!w2em&TI=q5C?$XZERsO~Qp&`+*z_a`bU(H@$lWR@=#JvoD-%hL` zI;aWzJ55Uvsp^S)_W2HEqAde{$xH9ZF_bKF-A%N-gW;(ckB@6z%ZYRN)~l6U*Ba)6XyTYXOb5VRuV?sNGsXg1Pf4l9IfSC|u5Lm@!2GixFdLSE zlQj=ytpK~C&5Dj!s~G$<-3d7w57#7TR|Vvd26wQXq{)E`$|u#s5D3dk>_tfU$4=vX zY1+wP9cu|2@$UZMx)kx6z1{1>^c)H;x zmbg#yrvcEOekB6L`xCOk2(sB9H-}0f^{rcSRP7**`${^R=#?XdhFMy6w-Yl@MKMt0 zXAVc0xF<9wm#x0*?amT;Q)Iq0+!Y_ty|e*MiE2>`o}06ktlDt2*f)1b0Rm&=+~cAh zhcz`Por!LgT_1VE9j}xNxi(N4)Jl(@my`_cIOkqh|0YPHV))Pwi(REvhko@LWLl8J zLi$IZu7uS^39`w={Z&)_i_cR%7!J}E=+d`8FeYXZ&@aJ@DtLq7&X0L9aI-%eh?nd> zzKrAP7=&4egWS$`#T_tUQmj7_g)##0cUB5SMZ;uqt9QOI+!4da-^qDj++BJ&1vJ*G zOy`3Pp6{{h%XIY3F6E!+nfP1KL^tO@ol0_8KLH^w}5<*|tj)?(YgJ`kK>9c>H>-=-e$2hO|C1e+#%`e3k)3aLI`F zUuYdI)OfaI&S2H_R7CJ+36s&{JX^Yd@NTze~>V7~nJS(Y^=TwrpPtI0!Biwd-d zf?UD~-j+v`iPa8XTaO-dv;JI{^qyR`P3kfxRL~+zRY-(t*xichJR>QiMRnj2o{un> zp0!ZSxwiamE9(W^yQ4N!qgH=U)In_{hWlPvqlX5_jU3Q{gQt|SRN=&|VL0TZC$Dq2uJfqNR{NIIUv8Qm2_yGEft$?X@8D=`!=#e=_79AO ziZ){q=Ib@=QXZ}!^rk{s0kU)52==Qh+3X;XL7$HIk6w{z zmvm2zjVpZ9eUHy(cCs|U@IoH(!Gx=O3<(Gh%f~>nvgSksO++tL-JZSH(*91c z5Cv$v2#y9(HQO<;O67pS(XY!Ug?I%2JzO9JgyY$no-(6SQ+z_l!(~dOe@^DHp+(_B ze|j)Oc-FT~5|%Ql20+Q3?`Sr)_0IS^Sm&S+@(8mDA)V~c3+;kM5$y`L;EI)Y`g#G| zL6_1_$A#-~vou_o5%?Vy(S~2wyhZd#&R=Gdac})1g6(i{VQ*ug5aZY zFc6kk8kf#UzJ}f`g7(O7;b&n$xV}|tlW&T5KtCTB{4W~}-!z{&(FoqnSTjaARKqKH zFv8I}jJx3Z*CSjpLX{UjBuEX^7oy`U^&mNWXW=MCOR+~wms90J(`I`HGPC9KR<ynUJ8}oxRLbhQOyo`Hsvx+Lml%kQkr>>_T(3j|&jAXJmC0tM2e|Ne ztDzU235$GZoDdI-J)p+!#@}4~YGRMi@@|9xo&OM!%Ph-l7=I3$LZs$ndZ%HbD!2ESl8%Q=!p!7)xk*uXW?M5vW zQWkKURG@R{%XDY_q6rOa$*UAtroJ}M`GZf)t>I@2x^(z@sgVs#)8CPwOEn{cJu%BY zW2JEK!;;WitT1vVd7LoMs@Z#f_yIneWXkEc=Y^7vDaF;JD;8RX5sFTBxQm9Ct2lON z6v|C8)S4I@WPV>;)Q7nn1aaPko3bXxZzOnAt|=xMzqwo!>wV+K#yh9eBtG$=nt_(b zFXWs#jr?;0=GcLb^;BO_?#^m?1@ z?Im{wH)s21oQ^~>K0*4CN zl>DV_x;x(qwh2}bm$`n*h`;-fwymh;;lsBu8Qi|{jmP)jt1|b4XniD`vv{xexZ-#7 zAvtH6UhUD#FE^k`tCFgJ&VBSy5X`4S1&Os^iZc^%_9tRoWf@zV&zpVS*0OU-vL&i)!u_K;ACd9M7`oTu6MxmkYV%7c{1j!K<>QWc%0Jk zXYKwyw?B^NlT-Iu>YM=+VLZGd^q;>9M;MF}04-0Bfp~v2?cuJ8b|b4Zw1<~zJ$L_r zaa#Z*$RQjLe`DnB!!(xY`!mR7nSxPz1S0SOF+}TXa8_GREMWq_Y5^E-WaaBr->xJ) z1Bp^eN8^0aOmcZ~|Ld)<_+Ia3WUXURtAHWgrc^kl+x&TfZ1Q&G*s~Tn5iSy+=-$O9 z^W9xmiNmlg8XTGFv>_aO7vH=|ndE=$Q+&LqGWA~c@zCJSG2S_w@=+y+mddtqJpS?5 znr+hl$}djDW^{&T6^xbsMTt=AK?Cd}5>b!cBv{Z>iL9jVbCEQEXDcxn#jru03|MUVENi z!T}18NXjXLlmm;(pXDVe8$KTNY1^&E-R&$~V9tw|Ux23gv|tE(xpC%Jcgr!%>U=T; zcMVj7?L{AJSDhqCAjly3$TK-FR7J?qM?mbf+>2^fJ+tWAxwriXxDfzee?XY>4zTTI z!>7}-b-Ti4(Yc3QQ=i*^=={B#3{-@tIYDuRsG$9%5(s;P-%=q-OUUyJSUA!p-~1x* zs~lcqGVRb~ek}!9#n=$2!UaWV6Mqia1NuA=e+3Ue%(=gi7G zduu)xYB%G((_#}(03(Z}nc zp{DTd-q>vK_OsNiw)p#9?lY@59wKo2br#6B_YZLwRo#|t_nusNA3CmdknPUg*F9(@ zl&`nZF@{LV;3`L@pF6tH^fIM%HP7Pee-f#38f%|46?O(UG>EQgzgiv5dZ8j!MHN~m z98-KB$*qeMZfY^-H={M&TnpL{{ccJ8D4svWWGP~3rs5PX&>cmFUUa;RS3R*ISX=0y zq3jQaA5QIzwK^SkUeTQL42gH|^UCwrvM2pL&Tl5z$4=WI*;$t{fKv%o1_m9m%Q!i% zwK|-~V#^sv@e4JjDtUo}NrNdUXUT8YZho2yR^@&mLBAn@HRv27Pw?1&Y&TxX#zP?E zZv?j@$pnLIKF83X55o3EF0C5;W~qBVIOeZ*K4{Qa209WU1=>TC>b6e7t$qwVpZ4Z}KYzW#Fcp z`tCl|AYBRuwD>g)RI-4ppMdUnKWt7*h z=jd()e}cLeIrSd}2_j~)-YKHSoopco^%TcqExpT#dX1b#@rXULG&;fSg=~NLtBiF# z>o0M;ve#&evxNHu2rAGkqKNMRPH-yVCYGLz1yVeRp`Rt%U%i(Cl5go0@)9{fP>`$3 z9&w%=NMtMC{ysSvIT+7n2exBE;9=lxHR4_-&BM&wYRsc14U7og`+{PsYp}pObRcQ& zw>&tscy|SB6}eWa#9)0vBE%R?&PPP#4uL(h%l3UJNlq%9GsKXneI7Woaf|tz?|#$Y zpF6@R;}GnodUs~*%-(wn&pjHS9#BHl!!s9<(mRUJe{ir)pVBB#d zuxyg(B;0NdW!a(LN%+|sYH0@U-q^{_i^x~)z`B_xsQ-G0?bbI_$54qSDU3BviMcqL zcX>^Q(Cb6W6+e0QX#$;qsI3S0{|Q@E7ar+RfjZj@kMA^n9 z9A*y`$eZ7r3J+F`<4mUg+V@~Q^3ol|4l#Q=SElrMb1;XyUCwgcgK=RjN10v}3nUzq ziYCBt_>?hm7VYuvJ#ZD&Wb2C^R;R24!C*LO!McW84L=1SWapF4-;Fx+XW0yNZL=_N zUW)r~z&Cksa54y;p+s^o%15a!jDL_%ol(%(b_$AX&&Er}AM35(EfvVxx?8?FY6p?>;jd5{2sczQ?IV=Au)CeXqET*orRV?JNq* z&IA8+1VD_XxWs>Roq2o@ElOg94QT{?g(h@MElaGpzyqO~c4qDuEpS6c1@c6q6{NFK zqlBWug?RSOoORjT!V|baIe|Gm$fuVmupNhfcRy!jFF^&9>?yN(|KC^QYeAVV7ZA?s z)?CQn$aVItUhjka0~(-m01mW?r!*!nY%f4RG?VjYseNJ_%^vj8{h-=oQ4|TisEX}` z$^!=vO>h6>>^;byLVEZ4)QjC&TQ7rLrrwFK`57O2qR0nmi~sj%6QA|)3|frLy{lx9 z(fRU?!$t&i{d$KUkxOm^cyG8y+1f**%wBq`W1bgU;?E=Q>*fu@*h7~x_@VlWJccor z8a@{0{{^b4d2*=ku=ifcp)UKsgE5QHZ8s2M z8)~Kb<|x}CO+-0kgjqGj7}m?g2y;VtS&o@&fzd>ZW=3a$hc4@Q&pg($az0&b>DgZ<*_Y&Tg zQ#YaHDseOgy^sZ#3#J^#nK7S^-cg&aKz@=mx;AD@v>Fx#f*soHwE)PPTxJ89}dIhirU$KQ*Qy1O>Rot-tL!}xaDBId9i#l?z>ZG4lHZ}b_nm2iq zwL)L-^$F)y_7zn9WMgk+?<}xA2$X)ECd!=wYd07{8!bq8ks}m(^OiZRp9bYW?GLgI zAk2yy65r+&}@%e(9gxtqP({88JeV1U47_p;7A3>J(Ao^Ma`SR zuu)l#58KJl{E}Al4K{|~=hmuRLoiDfl{P$MqF@DKRDqL1{t!5p^+IcE95H+5rH^O= zfv7(N1Xs#{bf6G<;0yGkz!@&jTkN(X3_`)M3wDWc*t$j+F z#N?I|mMlWm4G2Ip6S;uhG-iR-DK4f)D&ni>OU!i05@y+BKANU`%_kD0)6mG|2S;Tl zAhYMf2MBU=Y|M?OfJT86w_xX?t(+K%1P?KsK12L<*!YX-Cj8?PjdHe0)qb_Y1%p&*mN#$l74px3NHTeD+ z1I?3(S(8t?_@x9ZvlfJ}I~GV^LxXYT`0|{A4r8f-sfvEPPlc?moxt(N$gifeu7B^| z=VE^dO8%cP;Dl)vo*rAtx*XXm2Ft$B_mDGYZP75;!0Pq!tm;nw(6@Aa)%5x*fs#ej zvlOXv`uYMVo$4PI&x_Jb7Meds2qw*95d1Rw6NB~)Z8|wpGTfNv(&ICn>(RTtO|N!l z!Q62D2tRlXt+2`fD0VVOLtxu_TH7aSLQFyBONOJyB{2oAajZufA3n0c^^cB6&=@TO z@A}6}E&Ui(xetyZD0e82EP3L=3mrHAAfPTb{_D4ktZKoM6TVNE=ey&RnlW^Q>I7ATF6_u-mWO8 z$QLh{ZL3VVJyk7XyXNYYG-hb%m_%+e2_tKtt+XmFPvKUd4<)#w53Z0RCC`FZU|L@W4o`Lnd?#Eck~A*t)D-ze=tb z?VkQA)%CAG++N=0+FLEP>5?=Zk0v9|Gx#qo6XJQmk4f4))o+X z$oF$Zp6oRiCEte@L`;`w5TV$2Fc@T>e?})DLJly9QMYbv4{F$tH}cHaIVv7lMuIx>rqul+Kp_p8CP!_WS84l_$Dy_XPd(nU`FuF*ZsRS3i;1#xLDV z`~HW!g_B)Xc5I*tl^gOqanv*reRoWzRuFjQQbf70HlLJ|OV#4mO31|vfUFNV-W*=w zxwdb_+TPr!k0OPiYx9M*Cpj$!H#KA<8{nf&9mu2kZ{9*?zC=vYWJ+Nay^L1gUa~+x zUq&{65ke)GAi%5p7o>yYyUSn*HXkbN=VqlKn_p`#`g>KFMD8+#MypJ*Po0vthYuqD5fdi>fU_KmqKJZL5!5BekYS__D{P)>Z08gzx(@w&kl9#dG<7b(` zM-(z=&!AzM=S-UK^%~OJGV#w3piz*Y2T15+ZzeCZk7(OA_wY9m`JEs@`^NxKba!|& zY60lq3erIwHUNBWe5VV%!{ZsP%$v-4LBkM`+NBlC($o;(6qcy@^2jgS_PfT5F}Jt>O+xd z)|AsoajyTePxCa=UaR0k3wVj0;kx~PvuOc`+-5tL`07-^;1jZ=WIj(Osbt8Wzr`XLm5su>XfKq$l_7W`%fT1yI%_bIh13jU+jtgi|B= zRx8VMH@`j#O-UU-vd0Ci3};^&d~iis~% z0!AtPSSI~2obj4fz|SqA^dfjbeIy9YJKV*m6{(P9hL;C#WWO+WsNIEn3+7hY0ee~wxA?FYOCG<>@SOat4#Z{U8i z{Xj>N3MD79?5iUD;2(Cqzo=wpz0Oj5Wub(-sXNobj{3VE`6`pIJk* z$hXV9@(#)ht@(PD9A;?F{DrFG2Lta}9;WMy3&1OxvdJxl&^A%RdR|oK z%MywMXSjDjzStl(5$R$&OyDoo*@K7-fbxoQi>IQURs9LKUJ&1=G47_miY=YZ%R5Xh ziYvWem^ZV5V5dWH9PMor5lq@`U+lG;a1C8-$2NVOQHQdCUS594bG44tRi z_|H3fQ7p)QjcxQgA!oldr|N*vu<4c3+rw7SBKtw|MQG zZ|0HU&Taci>9f_Thd|&5;p;+zW;#e5&kdLX0ro1JS_iURJb*@E)CP7VcM2w_o!vG0 zi3(;a2lr+16G5i0Pl{iSLa~Uew9s(No2M$bq!?hKh635#6y*k0Lj{IclMrlipr#D? z$o4B)%V~>O-xIB+Q?p-_&sFR)ITj4Go}iYDtgngrrIlIU`MvFLXrC@57C$a&`Hogc zjS^EOwES^Dx#klX0A&xXkuM`y_+MA*3mv5Y&6?^`D-V&GXFD<9 zg%D*NZQc|18I|1b8JPw>MS5lAAKm93wU!{CTRH*#{{>1^Y;3|S@3nEgH73)!*22>?tQ)0M(VOoJhI71)jf`Nb-!!}Y48UmrYa zK1!n}5^f!EJ4vbLFW%+6vDIs@(ZgskDFBqi%&;zQAWmKZX&{%5p#}ufcRrCv^P!cu z+FwP!&9>S%~dxq zt>78E3AEjJx$YnTDHRjl{|+=hxt0;E;1Bz3a%|)+YnEoTdlrK9o@fW zWSD$eu&@|8e;WrUflETh(2MLw9gmaaKDhi~*;cj0d&u)C;j?IO$UnCo+_?oW342~E zxS=f--MTxupGjvs%u2KgQyl%VzzX`UlqR+@2GQ0CY{~I3 zwTp><`U#E?;h9;Q?>CFEDSl38SI*TZf)dB@+hKr0k4Bs?#9P+xY3%iY!X z)XBe*%gs{B6?)S@UxoWFW8l7ii|a@ngTmKqSuSH?w>v&C`)sK|`3Ge;uQ*>Kh;dC~ z1)3Ogf_uqf*3jC4;vdc-vSYNhSLILH@Sx2w{-{5H9IQ$~P@moon3dF$l%33}>bH36 z(QFu~4}4&1P$2*MNZ>D;B1#Gh$M82s}0mbx$2K zU@26IMCQ5VrmDc~_eNPJytlgAEnenu3hukD!&`DZq7iH*$cwpH19P5(15K=vpr2)Y z;2e2%z@Cn{ctl7Mm>f&_>vzw4f~OD~#=wZMi)i_MrQy5-Z~w3O&5{Lj01uFxnP6X( zYm<&i(rO87u2VWL8Bso#4{g)GyIs;E1lh&ml|k4pbtOTTe+$YTvS9<>G9|OM>hhd& z!dm{h08)s2g5qYP45r&%FE(uxT207VB>f0OBojC=0C_?}A}=LAy|)u-wLSzwbub$& zzEn1!=Sc1k2a5}MoxGiYLiMD$Hbidi>sZIA)tS@(?cg$GZg2@9i?V3t97V$-`gcHq zFTqEdxKaR2K1jl8M4O`~tL&b*l*zm(5zT=MS)>B46J4;n*s%P;)s=XWvCV;Z^(JcK^cq^Df;_hBzOoEp&}HG^7wdxCfHk=^Udp2d|29@8yJqp z5ks!#yo)#%M|7wbohtm%ak~0TLBWQ7DR?t9vV6Ld{)hLA21}z%7SE@LR2Pdyy`f#c zq>v+O-)MWA|K$=-E^f3qafjEx3PYIk{av7+yA$kU^^TsFpu9&rz^0PKaOhB_>47_k ze3H6nZzmgnU6a)*+U~{<2UGsu@K5-eu#gR<+P3@;d zSxGW&uG>!Y;taf%o{S~julRV!{I(PXlT zBclVTHRUTPDRK2sP5oM7RfFR7?RxE>)D%v$AGi!2Y+2u}UA16H@&pAk_OstwDty#l zr=XQPzaedD%_jcqpZy5O{INqJ(K@k;PjrKFa=imN3EbRBxLqLiwJDi@%^f{y-=oJx z)=S{z5S8ltl1>QnP{ZAMv{?nF97yDR-{Nh%KfybIzvP9+^?|iM@n(NTW zkGX{cwIZH;{g-V>`#X`MxYl6P5n~sgLJkCpeC`Uvk;s(w*t8l(lX-s^ElarqqbMcr zXxwkjF!-r%8*U9=d0=+uz~vooZ2tW+C^CWM$kxZ*Bf}=;%+|NuQ5Ku>u`F;t8lD9n zj(K93-THV4+d`N~F^Brh7QQL;68Ui{4hM}v9w(W=#R=I|2m^LWeEA8QUn$UN>#E@R zb3D@mC&!IJl3qwd{tT$`baf|1p*H%=3FMI`3X?1gDu({ra6sL2P<8PZC4rGm5eZqC z*kBlOAZ~gtvRoC0HPJG$30zPpYEOYm11I2CvyTI%(BkDaS4i^d$HDv&9~{OD zqu~%cR2QP0xp_CV*lmkhbTntInQxsL`}#7R#zn;-()xV?2Pw;2hW4C0rJ>gY=mn{C zH@Ez#6j^E1Hblovm8|sEBLt-4$&=c$rarC}Cgkua@Jz-cbiR6K))W^%eCzAfblK66 z+<*1bZs{E?dJhHk9yhulDgq`7vMl7rLOQB4W9d<{IqGCA!z|r?^GAEy9_+!~Pb#am z&KmxUmrzVgBlo2%CG=iG^2|0<3Vbo=lgq$TH{!}gw zpyY=8DN1O>UB03Nizu2fvdnp?O?tPnzkK!m^x6bIBUQ2e&iC6F)Nef-#d?Kb!Q!SO!F_-izd;F3x3CJgfv~WK4VtIXHjO?yRh%v+|Fu9 z9_UOrCpcFaFeCqu*GPv|27vMdv_dQgzw!-)UiMDNkwfG5m6)a~jO}dj57lNAJrBb& zR#9x%3TzYerg%SQWDE(94KHD2&m*i!qIqyMse$&Wx9ZcK!dD8=i z>W{YWXXi(`Cb4@Lh_7`xGSw{v-UBbn3sB`Ga}O#iBy2odYim`v9_t10?pu=3zww-w zzbm}(YUCKUq7@7z4(6dkaFl_|y5~7_!YM{B>vMJNslxn%;M~VP5qfUF-xftxIwgBu zRL(f*e{ci+ipQx?42R1+!IgLYPbbt=+T<&30udZ|v zLT8UvN=M#f?#O1d_+4e`I-tU&{S8pX+W%Ucm~6sCu6^}vCjDguyeR^?Hq%?;B#d6Q z4N$`mkrNBk zG%S~2VjW)ADo?vpT#B|r8J;}wdw}N>Y9#P>iH{zmd=07i75A>|dP;+vNC}h4Kyv%& zoth>cb;;agWB0b_Qx7tlSZWbR-YmD$&S{OhJzOyFU4}IzP*UFdYd4~ z+{vEt_zr{~JQ)FsO=BDW@a4B3zR)rDSOYm}Xy)aj5u?*V4`_R!e)4I(MX6jT_AiQ~ zk%pE0Gi}R}7u^UJ-0KsVxEc5F`B+-pRwa&U1HA;<+CuQ%3+Ha!^6|!6LIV>clXf5s z)LqAu4Jz1z!yg`Da9~m8Gi>6Bb!1WH-yh2pJ5-0y#H3ygDDo58ZhaUAwrJu~#D9|` zXi+K_3N2GVVHz5@;k>yv#$+e#8y>U_jWV6?6UV%9>$WAEf~uur(SaiVn8b$zj9jt# zdGXW#T$>;0ovs&)=vib6s6Jlvm{4JrPCY@cZ-D8wCOeDV$y3Tmh_)kj57U4@udt2} zSucav0M$r9;2-cusnI4Axl6rA(RCu3+UQXSYfY+7#8-FgtB*JCFgT+&phYz7$!iOM4-4YtCGDL*mcLkp*Cb7-{HUI%{& z1=0jh%;oa*Y0!?d{Frl4gq^Jc*_n#Hhd<%!MlS$wmR-wZVNCC7;*^n{zzG0B&2u6f(K zmw4#t=*KDUZ2=_3m4P0YpB}&o?|zl*5sVrTVu}~}*vdMN-y9?Dy*}q%oo&OninK1 zfRK_x=24*pQM?J_?IJ*MARlma!YJ(Ij}5)0_kJ&M!ISqzF7*~!rv7tk_h_>6*E-`L zgcecK!;c_D#qjztc|G<^Tymt6HS(Jo0R0o|(J*m}=u|JVm#FlhqLn)4_f|gc$@j|E zKuV(c6xde|O z4Rv435;(svTWC*#5-21u;n<`WzWPjL z#FfOa_ig&a4wuyqzX|@f&ad#=eVMFH?i^@WDFHSpI@uF@MPA?M1HA$yprkG`LGy^{9rY`Sb3qJ@Z~&C-oJbn&2|QfvMAPCKsMYhZcjCUq<^=GiDpF{_Qi|Ke@+y zPxK8pbN|b`{_WCxExr1BoA5)@pez|>tOKM%x3;`w<#GQDlC60?x6*4;Q=pC?yYNYJ zqQAmeaKOW6d5-$hlR7uAsbAwsPz%p^^EpwRXD+AYFTkMwC{nZBz$e#{6kZRT}@r-Y-ltM_W>39KW|dOV~Y+ zPPF60%>MzZM?;2JWT*%dCLc<90&3_U zdtse$7DUqg*^Rz2tDHOM&%y8=!ba!x?$Zu+F5;iAqq*esmwrkJ8?Sm7BZLUNu0Sxp$z8kvcG`0lXMQ z*0XeIm0t^;PLYsx;m4EUnT>0HE6olppZ-!o?G18l5R!C-x1)N87l#XQA{we!-A+9unDGpK`({Hhj*X{(N;uX9iLEi&eG4j{^b4rcu zt%35FTv{d1dW4=ycc8`FA7Z1nJ8$tM$uF{KIf&x^$3;a&BCO1Uxk^>5ChMXFGZ92k zSxRB-`)yf3+&!dBN2jJEoEL7Em%UlD5A*o|Gx{3wx0>&odLF4bw+wk-z6201sjbM9 zSjY@Ln$!8s?8AQJPuI3$^78_D8@GZ_4{Tk_kf-)~!%GLXC_a)Mh)*_ zvyx>#ZmQABDC~0sz###{Nf89Tk;un?2R{2ccbFUPj{4HX83t?%lWLC;%6=9)uSPTCy&%=W2?8mc90>SW-+KVPxJWK0Z(s`{%ee|sYWY}A=H2(vGKB%zSWF`2!aI*6F{5wm?)wpLSv?#MGz|Qf*=B924K9@icW8%Gn6o=9=)DbZ z6R}qdJpSs;bU40&Ed{bfwn;^OO*ytt1s=n$sOwGV%NJBuIet15htH(dqf`Aic$jY`JO4q4X9Ha`NUS!zhNJZH#prF7Ldn zP0AZUkL#1o@ZvSf-}b>AL;Vvnblx}qM8=X@a0GWdqZSXx!9yx{GJYGPjCx$eF#YPT zp~vC9S_RO7SsvNPih$vQy+T6y#(8mK(HIaEu4W5g28^MqfJ*t=1mo5uNy6xR79ls*q!#B!j$}P=Nn@8p0Jm7PaOp!fSR4OHNnA>qm^1%9VtJN*b zde|*ia3@v13Ds=C^#*ESCV@5iPiUUH{LbTU+jd~r(+fzZ>P?~8k0SAm`UcCGzlbDb zgJw!ATYskZh^5~I+6zFVZ0ZF8*jO9(YiwzQDkL1!EdR@St324m1ZduZya-PRqC1bU z6E>}&DeXdDfiHgEm{}6s+;;aY_UUvT_XIq9Ww^Ti>V$ed-5?24ffSr!W^ z^$Y$$MbCH=FU^*Ij%{UMvJ5X*3vZf_pRBp4$Kk_Z9@OBCBciC9jv^VwCG*Jzz{G+9 zap#`Ff+x=&VjYEV2St(I%mLY}(rp9ydJ8~Rzv0r{oB5WT-Il38xuLdN5Sv~(Y*RIb z!V1yNY$!t>P`|UY4eyP_q`S)oXy^nHG@GUt#7Q@XlhisYX6SpxwYq#xmSB9`(YS(ovNYC_t}?SuQ#;B zZy~Z_G6qad)=&;?rw#srtL~TRN;2N~5vI%9X z4XZ6=g2eM%nYX-f6Wzbc^7zgYw1iaUbF=}EFts;bhxvtMkuCRam{1=J!D7GyiqF<)ZuSmSv<>`FhpG4gF--g63ft((Tgyl1VOi221IS zM);GbI@%lRHfK;dKkR=})V5_BY3z+lSkY34G1;am3yf6%pZ2~yo~rJP`(882lqo4P zlOjnJbq$HgoRD%2MIy6Qs!JK8WU7R2C}Sd&Xt<`*Bt=Lnl{6PBm5O(reeNlq=lOl! z_uu#Px$BR!&e_A-dw=&@YwvxAeJq*6D53AF;yg)5Pi*FWBe`Gm=m}q5)kXVLh@z5j z<=)1f=+OuI9Geo;4AZZ*_O~DEc19`8hCE>$aF024t|_An6rU$5V08j`72PAVuy~q@ zod6*TZn*`Cql-v*OM;}m#WHM%NiTha`LYezUTK^w9;B3(oSM`@@g|c79-dvuSK-2h z#_ojQs`h`l+3OQ&*}VcQSq|!vuzLlL6b@Qg#Qu8e1v#6%WS>-=cD&o>l2HnIc6Ad| z35tUktf-F99D2SU$qH;|PAV?L*g|`o2!VU$T9j|Lw&muHLbbNW{0sS5_Xny(HEX>c zk(mEHh)daKXB__$F?jd@pM?4b{`ZBXxys&ObB-l$uC^n`%LP@&agYrY6c#S31MbiY z%MG9>_@A=jOmZ00Ws7aRhW=OtX-^ogdbej1GdZDv@cyvzh#1wra1*Z|4v6;pk~l~=(IE7ad3S@8=Ae~T_j$-gJr4ITtL+Zj!naWTdp zV==3QMTOY)Pm<$+bGn`+KeoGhXVn+!Ka;B)r>c!dzWMSqrSyy6M%5yO_KmKACmcPv zq7#9LS6qETq!g4`?)JOR!e>f6x`=-$7qP?jySIU9aw_ZxLK0k06a>+OnJ|L3zPKD? zkNdQ#*#|ipcJV5vZ?17O98y$PzX8qNM~X?s$NRSdY%2FG8lD%;);Z{xTl~K#4w%fl z8Y9lXV6d>1ldOg8LvQopep0+TZe>^kn#oa-clh%qs>MAl1S4;3@Ul{3$FE4#^2HU~ z2mLmoSq;O$10ux~@NV&I17nK6r!8T2!{j)rG(c*$j^8DRdCShs*7wtQjGYj?2@(-Gy*ZCh@x_j}BCRI(ubZb-qz&ds6i)-Crul~X^XN#5K7dYdp<=XTpwCN5M@nWltbj8e{w>^#^On!hNih?AAq`X+}tsTo9Lj>qt`v= z28f?{(5eBBGi3A6+B*luI!3`6QRzQGV$o^LUU?7GlH^T>Rd|J?l2lEeEew2tG?;d6 zDQYub@}Y2|vuGdmQ4l>uQ~n-uAb1LNw0YKsxc;FX_m2=pN&De-t-a&~30O zoHWU23~=J&0G?Af0WSSKX{J1(zUXQK5I@@szBbjB+;GHSL z(};0ZQJ!dKKHBXyXmn@ry5Qr&Q@^gb%@8?=e*-OKxpq9xO{*R`V%ecv%xt6&zwmbd z-ZRM^#(jXh)IfLF2+|z29S9FaY9coTEQ0WFeJJ|xf%b4GDxC=FkBcO_12j%otd0^- zj#ls6b)coeGDiE$qnj};md=G9CoPk5;YKe(6)lI*Y0;sE$gr*p-xuv7EEj@{PDLMh z6F=y&L$vzh_9fS@k*f;$cEU>1;;S7;b&s1-73I}7{ea++sD`?GwPNPF?t_}_TWBhq z@2AP_fTlSG!=wnO7DnQt#PzqQi+i4PB77ue!!^EfB@`dgCsuwNx0mUtSJG0VBRm=v zXQ!p?uk~tNnK9^?6%7^&RAF-yaoODCh@*p0WfmUz5;~mnM7kl!_-~}-bPPX?Xg1Ed zHL}6^7nZGmXHW^s>AL*tt{@aOc5NLmHdKLjm^z^jL%|F--=X#`f^(s6{pv0#$fPD# z8T*QI8@8cqpu{D$E$zY5hi6`Y7FBw*p49dwburITFT=Kh8NC>Xe4jV+K3e{J1yT{3 z@9x=`p=2PAB2e2vy_`GMm#ubpax5_1eZr*&#k5a*;0oMdmr3F`yHolauWf;boOzGtKSWNNfF4%0 ztt(`Goa64OY~3U4iYShZ|1Jb zA%y7>(h9y!(rKupcrwK7tYs^o0B}RBJ-%-M-)QtJz`qr^F!q1xoBBp~CYqm^uD1C2 z1;Z7bkzv4s-JshhE32HbUxWd5RQpVCKxZIRfq%YMiv%&XyTKt_F4_Hkbpoesn`HOU zsUcTnNHpFZsQC=>O1-ryyDgwk3%KSnMf?YIkR;~PvWxk?c#IUMLn3PXW4l?M-BEcOMn!?O0_C6X5mppfy{IHl55TBI$ySTkEe{YObH%%~zLrvrCbErInLf zaLR%k13Q0(h*AO%lXQ6UP5K1n%^Z17?^l;GHUrgbeGvZ~>e#PK)+x$~X8fJx&{k+5 z{lUeTS$rzob6y+wno0Lykf;^kkD}7=Z)Mhi(qv635{!YCZ z@NuT&_fv6shw5MJ{Be+$TbvGFXXYiDWi&)I5*NUAU!!hdgri-*PV@ew`Hko^W zlqVnhT>F~b6UdxqWE10``b+poMV2 zRI>h*MD$(6HRGqDN4Wp7)Ia_l-tc&er#-;<(iFamq>!f>c^f8|l+aV9*jKo;MWTNE zTJYqF{@CNevy)ZF-fbl@D0Dd`evQMx6_i9ZPEIKd+FW5hS_-$GrVvBo-jr*&2y{~e zr~Ux?oPPXC#>M1PbIxystZrt&SsN#%stR>jx%9}v*N+1I!@l-FBjnhImhE+0MvBrfW^2$6ZIKt6 zUhlCYBD#4r*4^E?5lS9Xy0Ht}39z+`@cbw_i6 zf_X`M83YwSiv6|pdxOK3^PuB@i7}j`lut$1WjBt`3Ax2k$e4jU&gb2QJKn65)8q3= z>=7Q+15l2BhF+tsWCNYdWdG(*XcpvV)DDA=-)S+mz6&Yd6=FvE@+?JchQ~XRO@%9G zDtf#V?cBo)x5pkUp~6~6=S{nprQ2$H@2Mu~qj0)leM18^qi6e^0+q`6FS~Zi=s#-> zM=8O8lH0%zGgbnQ)Y7;Jo1lYn%ox&jiNX#u&2Y@mUQ)F>jVl1u!1p^cIS5{`D`RFX zWIuDaEc+Vh&0FmE6ZZ>0`6zs6Yy*MYJKX0)K)T^ULa9hLT&5Rwt_^upxmLe<9eeK` zQ5d+xS5&jbS#tO$dDH{WPlSX2MxrxW-YUjP?lnu0M`YZ;1%b0d5aP5RA zzdBPWgR`$VyNzSl#!md_WJE@Kw}hC{s=FP+XJJ+Yq-$>U#`KKx0VG>U-W{dW)T5JQ z6k}~_wkU+ftxlqz-(q$#KfC5w60LFpAqZd&s^r#64RaqEt53fT`jb~7ET)d+^q-tD zCw`pm0^tq=WBUGPYri2iJMPV)4lq`a)RwL1X4hJQ8&o(mYa_$biq3;49--W%|5?LL!a@Cno$&6H$`fJfAN=cGeZ_cRBl|4)K z?{y43A^U;IEpHYXnDB~E^QReo8nb8h@xXLkK~$_hK~du*`Q=80&3nyuj}#UC4^k!hPs)| zd@>skq3yt4-*!(EdX<*%e9G$#r|SPfLGBk&3zM^yNZ^ zroX7#K{=o?Yjzsttf_(PeC@KS7-7lZhFXxO}+tD;Ju6e)x*5 zxsaQ^C)@i~vQk_s4a0YPDNp8&S4$9qs+fzpWyYCHBXz>Jvx42-R(NLxcKrE z+B_aLJ=*t{l2^tA%(&RdtB)7&L1xUb1^+{2Q83M!a2jb zrmxN(1-eARG@|`Vu1?kC2;*lIx$eh1UBigW$PU!o9-B8g*N z0Hhr87;jsLs%a_ZBId@S@c>=uzw?_?cT$0r-Yu!iiP;5Oz#nE*qgbB4d59aS06MJ+ zrr_Ks%D_@f(mKV=ca1DZky{<`)kPNH(APdnkuEN`D@_Y#2DC6MwDO}}lPOn(7+GpP zWH-~HZb-(HK@uhCn_pD=Ztj`c-^GmA=KEFcHD)A;jvjLU`fJJF5&sy}AYDn^7#X@p z8i$3y91*yu3@>ao{vzErb>t?MaF8umxk`~5v5!E3Oe(3C*kA>;I%oLu%FbP5ZM&8) zT!BL1P%DhHvdaKy93!_(x0~zVs5YCnAvG*tx#(he(4M@jL7saPKlQRYp`un zh%X(~Ju->p8{2OOyN zuqWc#mla?Jd+`(uL$@Tn-(=w+-j#IC1r5%1ahcsVb zNsLPL zqz>H=`e6CacoyQ9W^CG_Fl$mDKWjJoa_EU>7sV1v);3k}q7solv#vkRvP_QT?jZaS zj}K@NT?Li{K(bf2fc~8R=plK3uN7hQfypI8$z3+QkYg{_0h&=!vz`LttLJU(eDy9I zj1x#Yy+C~ol|uP+8wTEo74CE9tSq}&W};EEJJz_@JN?zzXYw_975;Wko(LaEox6x!(X_? zr2YmE6=?lK(4ANTL6~L-krysx@5ESRpA$_XMz9^aPEy0y!dLXRDeFD?xQ!%{vp>g@ z*G5(1;{L_X!qkNYo2$sftl-1(TBKvbFmb);408heXT?nXP#1?x&%jbio-!EFzM zdV*dH(_vboUTkDM=ICJc_&TgRQaIq%YQs1Pe8@0Ebq8!6jbLi1+?{oKHP=OjGXVAXx{+k2N@;8FpjbGCc%m9mR+{3)DM4~q@RyyNS zBy%+z-#ZCXT0F!vZb+B}`r_faSF*+SdZQ?wB{2LE^kVyl%y2Ubw&g-7YJ|7SxeROq3UhAh#-TI3X66)B1)@( z8GG4X7<1Ym&S)(Ax*GdyTT$Z;(%Rz|panxl;|lJNQ}%sV6?%LXO~$gwuZdFz2NlTA_db7>Y*+n+*4#@)E|cge4Czy>WNG;^9=cSzB@c z^V407-MT>@@5K>(vpLAa`0}HAFK9(GrA`XACnfbWf1{XsjCX0oXU~0~v!JQkiA*2n zEW4=aP|1N^7(ZWf;E~Th@Z+F*_p5IN!CM84(OJxds5aQa@&tg2@eXBm{5^a|@|?Il z=8%t~U}bp^oE*rRa*?mf~sAiI#iEc9Xn43o5D*P(z`h9U$O z4r<(d@p3x^%3BlbkD2DHOfLs5+#gmHf3l%$*>g|S zex{WF2Sz!S0fPWRI_81`vtNsw^Y~&-|4&NL_KP+5k|YD?vH&h`fX`8sz7Qq1lb)cQ z+HYVU&|bJYe0bSkN%Xo$O}Ws6fp^^Fy-^co_k0cgiLY<)WLmbKy}3bEfMb{#sv9LA zZdT!>>53z3wIQo|(tF>u3r7pxCx}F1YoOqNQ4%}Zb2nd%8NU>HNl z(Hgpk58wV{Mb3zqjNtaMeP+O2)gu14zG{W$(^W zsSRdaPDaBskrsF-_OnykY-Ml|H~;KkKj*;UCVZ=3l#-_A3W&2UxKEBnT0^^R4l{Qd z&3}%dof*E9B2cP%Q z6-=Rk^fG8KwgWk-)g)mx+m{!48tu00=L15N2jF#ZDxXGxa$jE`hJ!upNoMmkXQr~b zKe3swshY~xFh(R@emwQ`IJAAQFsRB2?Ef5Jf*!|z090)3<%vWD?>MM9po%W$E6m0? zEi=?TZsYE`PzWM=L;&IypWh;riwNirw|bZUeQ<>k0s*jD=G3zTopg=|Oud`=5>dZw~Sb1#y`!}gw#iFhqH zVBhce^mQ{~+Z8Q)#+snLMIUA)tpS*TPMCtH8W9FCe6T+-5@hqjLAnF_0wbzx*=hgw zsOMDI+FIa^cqsFDZDD2i62iOKyj?LMPOHQaUG7YDJck2cfmxS;2H!&{gkA(aPR-GI zt(mW?Wo5OG+ju$5`8E``7{Sb|eO_cjHxznl^F00Egr~#H;a&~{@9oceZ`|g2Ldq%R zgUuAE*}n-@kr{)K5b~iM= zb+6xhr=`9F7WHZ|ry^||3l!~M#UDlg9jSrF0<|~u@Vb2z-f0&YFzuj$ng>uYUBo>siwGG8!2S!>gY%Xpt%E%l7ZcFGL5zp9i^!Hpl znuhEg^B%7i;0A;70chx23Ns0<<9x)P@{oDvrS8&KntDiLLA?ax`Z{1T6?eiRs*+$ zebFpB=rZxj;ztKKqzderD^U9Fvsn?ZU(brQvci_ZJHin7 z;|{@;;ai54px#3@y9F*112^Sy76#d$jV*|}h3`uFJ@0Oq#pV*|>#_@HBimt;`Io5o zhTvjWFqQU`D%y65C;#=@zme#S_Po5N*ac)8Cufcd56#7=dQ%bwIu=CrTt&Y$fbir9 zE}Lr>LD!RqxKg;bS0<+lio;#rn$h{Yh>~d=8aNk8_&s`UNZIuqfs^W1ABj>8=_ zQsP3GPT+~|?7gu3>@{w7=|{C|4bL1&pV)8_e`Z7ZeO#z(fb9jp<|0{r;aea1H63O3 zMP9n>zZvu5%r3!+bI>KRN`nH0BC`{bC|R+riXlFx+$n6RcXrXbxhTlKo&9o*yu2Bx z^M@|dL2?QYKBD|h->13g??u1gjYY)i%}rmaS0z8XjMk>E zA2yE9JUUD8sUm2_g*Mscl{@aaUd#?yryy0*mz_UReA&1bN2d#H7f7YSB9hZ8jJ)OZ zpQXkL*PCghD6(GxWTlWU3OUQd+Y*;WZ|s;am^oc>DMQ}roPnR0YOL`EQ>s1?v$`mx#Ys+2mJyoVi=(C2~K<6 z3fdCD{x0b%-iU5x?o)yC&`X;Lao4o}Ib=i3em+Hbd5%kfr0%glTqIk*2)IZ{wxSBk zMLlZY=K$)Q`Tmbi#~2eG?>lUy_MEt&Uf4^N1B>G&F#a<cTUP zX*)bThQlr2{@}@zQam=g{b4dciBSAYCC6s3*;0f(F@(uNCB-o8wCckITNt;2WXGiu zXC7!KM?ks64`C+qU>|woX35(Ea_{uJfr~4&y`}PxJ<1-}z3wNGzgo1|kY|m-Qv6HG zmZNV&k%&kG+eeePKW+3=$iXMAL@}2hMK#zXe?~dQTU=EHY8`Fhtg5nOUSse_A`w*V zhNL?OuFfKiB`EyE;w@J3%vB|tLjjw9w1G-4!?4t zF;1i{xd;ltcO8;FR^^4~0-cY077rbfaPfcOe|+KZilb-oft)FZMt(&I{5NNGnMzy_ z1~U%WaNh+G20D+u=dAui{&nC-&F5Et=`N4KhN_Q`d1lWrp`Gq}%5y`^gjNPMsERGB zzjO6d(-OCpD>v3m7msZ+4(GeSMUV3*f6(2B&{Gr>x#2xCkWi?7cN^hNAjCTckZwJ~ zr|jh(FVkznX(FYw;)%T`kDy_7ER6E9O*@#`a3*_>SmaP#|E%h#q-gI|0wub@Ew+`~ zMu&pfj$i|ayA|oxLcKNe@khutI?h(|JnK1MJ~?n)?{N58ZL1*r0fANflGb^>sp4I7 z>(Xbv>Fc26k1a{|VBbJSvd|q+@frRFkLx!ar@gI)?wwD%?p8rw>G+8YAIL?qmpT(Z zSACHotU~+%%7$gdh^9;-VqW|DvOAvp5Y|k26s)5mBck|hWESm2qsaD?YrZZtNHoMV z5Jww9(RwZ#+iJ!f(RzLwnv z0OYm8pKSeSi{K3_+RF=1AA2)gpw_R;G0l?Fss9+bVegc@fATO-=sG5l@{Yv66Vdk_ zJ=5RqmVg``l$KKF=NQ8$Sx8hg zka;i5Yw5HrM~J}Mv&EB&2|oVkqU|>IUV`)>Pg@VwHcNdFw17et9$PFOHi6s%ZTw#4 z7JM{P2<#17eO0OzD%9O{X$+fX|D^>p>Mv~so3YUQV~2WoVL>)u!h}QJKRIZycR`Qt z@HCl7Y<~(98n$4+FdbJj$~_RppllL((n|v_e#S=u~F?vMdS5{I$i2+f;p0oP^l$-hpV9Vj>xy_%OB! z4Xv}T_9k=~oE<-m`bbt~9k{P2V^)tPp`eNkQCTWmN^`SSA`f|5YV&^URN!ygLo85j+wd@9oX9w1Vs%=O?#vYr?VG&f!fDFA3+= z4g{@wXY%?~A$MhcU*5Luo2m1jsf_qvI$bMA!R7JIOBKXNAxYo;LqD%9Dc&zEGviB| z)M|uy{OUh_ne{Qd^HAR=Md(0@G*o;jw;7o6;Dt>Kcl=UnSrNK8ljtSQ5nANLcv!e= z9H+~brZ@5VLAbh!YbaRP6%L;cG&ufo6dvX34Bebj+IhEVA}A==pTs!SEwbO$PmOWt ziAX%~NiD@r8U4<8b85MA_g#A~v#8e|TYe!bTDt9{EoGBfYn6_H7atYNrZr%~{{g@? z2t+AtM7H2bnMYX-juZ04?8Yq37X_zl2)#JavbYP{z+pwSx|%yr{}XuAwu}jBWt4+* zS_cZgx(5*Vvqwq<_b2Cw$(m%o7jb?E{PcU#swenVUe0Sf|3Zr=;;s|la(D0e>O^li z=^>-y?s|(B8+bJ!%>I>cl_?A!mNI)_1x+TV|5K_;qHZzx8^DNlXI@ZtVw>6n{8#(^ zwkg0MImS~^w-nyfIhl%5^nLcB8$eTN$)%ZIPMO#}20kg-R9DEM_>|}-lE^e0W$~qP z6(UkL%1TQsZ2;uy_*9-A>q(>R|$hZ@&p<(-Clf-(%8edsD&qXP>n#NnnG`kDi*M;x{z8o~%{anviUnvV#k^H#w~PBF=+?w5c8&2l zef5tOW}(Q=`!JC~Buf%h`v|b}Z!o;MC)T65_N=7&_jn_@9vBCv}5yzzKWI@V$Ig(ktaT(V@!B9r?VXG+GI-!m@z*@KGan=*AN zr;&Oyj(QnkKVTt9+Z#*{5v>_pii0tpPr?nsL~xU&67{skg9tX8KQoeOHE`~^_&1@n z_G5ZqHSyg4b@UYkEdlDWWvca*L<2K62_0ypu@f9cc2?pyGjczS+ip)-wJu3F`z5{+ zl#oOak^MS5$m2;}g|4^lsmsBZE`Mq00=(gZ)2p!;()pmNcV7}UsI7Yeqx<;JNjVN0 z4vgb-+rKj4tSHk)Z>Q4WSN18PaFHy;8-I-ycmAM|aC|KJ;Q8Sfst@+)WmkuZ5xDYH zGKV=VtI+lHM0H-^n{v`*@=hvXd+tx6#8brOQT`^-aiQs<`(bdysiQ0Kaoxy6q8o@{ z;NQ>2Cv6mTB-63~*&!P0^noV+cqj428m3RJ!@kAf2!_^i!|$Z#P7`QJjF_j{$AS%#d`dn`Iav|;VSJZKqd6G=_ke!G5Ww|1D$-pwND%ZKe;0%MPYED zYYo|RT;?$%5QiYk?ZdIko_lzhNoebLd(rcP^)f4;Lf;&%A9Bu7uG4`j@&^Y(NKxOQ z+)j?(+IFI*Iy#&@qkfS4BF_!^kinOOK_&6v~06n+v$bUOQX6C@S z5GZ6~1wP!K!*q}TCBl(0x&y4??FHPt;Yo1xm+u@|$?}ut76~8ml=*H#W6FT|z+1~S zb0j!2qWUND_^UGvV=JkjsCF5Ksg=~r9x%z$J`dyT!}s6rIfEf>8GyQ%^2)&WgNp$~ zs*ovab0F931JQ72;xAG3opIyj3EKQ3#Vg%-C+;}S5;(h}h&uK+KRSZ7hbQ*J-<)v9 z@3i4uIW+Nkwzh{Mf9mcxx&xUfnrhP2-P}d{%d(9c8;Rsgk3T3!`%R0Jbv$@o6-C6! zn;Ut16hZH(VfL9^($HT?P+OcXe<1uA5rR17NTO09j2!gmGat9^8N+|)!B~y`niElD zUPtfsM?2Hig>j9Q`16#AnSfMK9(#Rf$@lsNBM%GC=^R=!U55`QQ4aG^ybT-fRlr1E zD*3ZB zE&DWzN#XxOeggA>DMaPV8h@Z5<(rqUK#CunZ!F8zP!&J8hJ7wq)MaKun$j7)K!x$#ykq6IsyTL(33a!!fb?Ng_B-k=cXRTR=&#^K)*4cNYj^%sub3xQy;R z$&96(u+=U`ROfi*gSg4z0r00T{NpO(lEx4gURgOVqmc0FVp~{JvdhNv$4sHL!1q5$ z3KBuypR?IH6_kU#t8&3TW86c>lBo_n9xpk5*mV^Bp7_YN!Rnhkrmq{Cv`pD-MRauky71g_NQjS?V zZMSH!FFfI)jXoxZRkJxbT9)N%M517LK2EceYq4}fg&#Hc`ruWj#EmYfrZB=W6s157 z)k3YV8y_k+L)pB%@LmK9Q(r=~f+>(x*Bd5_fBirS(>$)lTq6kyslPAL=C+d+(*AzV zOUsT(4I=h0^|yqvYYGiXADMD3>hdaVg@{g;n8c(>WyLVZYun~Cdn5LG$+M3fumN>m zk;(sKBWPH7`qnb!KT)cPY@y2#nN5puO!#tx!f_qMS+&yj~LpPx~kDUTfcg)<-1F6RPb8MarW1p zCWU6;edkZo+kBr8h=YoJOA5Be=M~wvaQF8f+lZbZpJF_%iNf{YXS;t`GBlv#CNGy^ zs^|d^uSD=$9gtkNERx8C_^7YnS%ogOPYldMh1bIv>mv$a06EQGYNKRDjJDVo zBA|c2X=%-L$F!FgfjQfGt4;>$P>Q=IUYaMZZ*{MD9Rgx1XHJS1fITXv*vatU!{`{xE3jgX_cmbI~== zv!{V)94a#qEE7VqNpe1N3Cm_0-BV~aN?4|9bPt4ol>M?AkIfsKI=M=40N?TJgr(%G zlIJE+M}H>X0NJUwn>J8rUkz?@HOFxAfMcQ3_9uhZ*ry%>svo4+-FCz~?5PMoe?I8; zTJ~9k{|aXM{t^?y#-V_I>?7%_uY}^Jd|nKJcRn{=(;_`iy=EpwTCDXoHOWqjw8_qQ zCrm+)>aOF-5iYa~kr8{Ba=yeLTEqVs$*VYh4V6HBT@PNROG#!TY_1A&>a@*mPBm-V zwXB-Q;pWZh_U+OE*vlNVka1FtvFCW}@^>*Rhz9*9pFJN2T0aRzh+Lbn&AH}IIg-sd zHWT%YP{Ql6pr{k*yD!xnzU(t+P()Sl2XktPQA7(=(A=4J)g451GPj!)uxahFf~*he zd|rp4I_uXCXsSTl>s+CP`4-wm-^vWmR#?GB2*<@&6GK;5PpKBq^L-4s*F{@m8U4Wc z>2$d10MuCA=)|4uv<}?XIOZPzlg9o$G*I{EgQeT8b*KSY+EamQA=y+;_ZHyQRQ^2K z2@$xNu^J!9JQi?FkDjCR^si9LL9)Zg8UaCsBZc%Hz^xg9+k}~7xC8!qabI&ZfD2=c z>&C=Oh5{zU7~9izcHgo${<@#sKz$>Ur`9!mWz$<1=u&=>=Y@=-<(&peNCsZ+#D+Jf zS5SY$bqddY1F2%ZQIbjXg4#o=2er+b7xdSQ2;z<-;LbKDhM{7y zbhihn%8b%ywZ5F6_~u z<2K|tNI4EV2t*$vAslOSESGlj>U?|uo-9cv2IB9jD*ZR0Ru1}L1@zWvtKfdq{G{%8 zbrLhO!B08(tICkF` zek}!5IC1cgd0}>JdZZ(G07(fHXtVQkCg;V8V*j{fS<)>>f{EZukwA}alK%XYHK3qwLVM| z2ol-(&r!X?>*kDLB2e~@kG4myNmgc%FBlUgZ1=FjDYg>{^EWPFd>(91sE?HPT=ewQ z;4>0gK}twAF$}nx5G663WcX)2LzG1DB4r#3uW_W{5KJ*2f}m`b&+dXX50!{?OiopX zn^ylpx_bmnj^i3^b`@6TI;)XRLP{qYafOMK@(RM(1fxGabX$P$rJsbN{!5aSfZdhT z(Fr%Sw7o8kMDt>vK?HhFdm+cec}<(i zHjdL>G)`I{%6P=g1dLHgS%|3w<(1u=xV?CIQuJjE%yNCgSpO!Ubk;7s4X)Zl>nm3n z&`KSD-O_fV7bS%hU6frry)FH~+#_ydo_ck+!WAb!O;NbzR{&FQ+E$~l>m5K<_;0_cKboPI9j`*ks)CdmMy!}FButH%%RhJl#M`*lIH z_+xOfjPSE4d%KhU=jXUnstyw^F-GO2EIV~R)=g&NnI+gDUQ+{thpaSWuUi>t4q$%} zBxzj~5^Cik6*M{vBMy=`Zbb4l186_1f*gjgpS^CDi>q~|!cy?~ zh=j~Tm}iq1Bk1}+bev}9i>!p=xHTl9l1RGW6;_o0clsiUdzDl=cmv6=?j%?5#uj_Z zfJ3{rX|Jte!CE9jC}M%4cihSACpSJ<+nP*^&g^#UOFOpa-q}28Ky6F=tc~led5umr zuU!5-0){?Bl!{XBlJKkoa!uKOCV>vcU};MarN744!)AtzZ$4G$lNe{{--)TQO}8OqzNC_8AE6%!HK$) zA0Z5-F1@dC)3g$mF1xQt6M;t9gfpb(rzZDC71G^0g!k_>{-Vf6k}xA8GHpEd?Jv#{ z>b)CryB%^E?M`j!rb0-9{4lvP$bm%iFV}@SEW?x(1Gi{l;yHRx~XI%R`=vzP_o>|7JsQ&wVHwU zwmqM__(N~=pi-eWWp~ehLYH&N3N`y7>NjF+_=%;q;ynY~QBG+18|HPOOf*~XS-#al zdna}NHUE+QLchDmCzN?0q}*&}zXS-{F)Vr^a92Kk(*YiY#PV)Y-McdM@!G2sTdp3W zORWg6RBeZj-<;hLt}Qt(9wpB*BZO<#?^4n0-mGr+&1k%zhX;Svkle15s)m<_oig)? z6`!R!t;{9C!1QwrNA9g!P|COW#Zl=B8w}^;)ylt{^h^a}kds{FP&BY^y7Ihgr+(@5}(3oG}c(H zeCy(>@!`cIzlFlLUo^JWoz7c5Sd^N>gOIhmGT2bgl^oF$u z4Ab79KQGJyQZasW(?IaIWRcU34JfSVgx>;)kfe7Sfa?7OYPEiO|chYUN&gRPFCYFAk}2Gb#~t{Q(g}PA3`>20aH6a_tP4x zVJH#mCvWxsyi9Mo1N02J{BrxE!*zjWqdr1L%>C5Yx4aUdEL<(rl|uMD%E)9Jc(12N z(%YQC`9Edhsc_2~F|H$)E7ljC5mGeu`Pm7&|JfWQzCl9NZ5xj$yVvHH)Qrux$3o$< zX0{T-9wTD?hF5zpW*K~Y?WauvdQI?i+$cT9t_xPiuHN4K2L{>6(fBJeAoQfR{G5KP z&|_&<7^N~L?gh#r7kB*v;|gsAB{oTA;a(8WtARG;%IOel-r^g61{l{PjNJW6ngJnB z?r)uId)_g{8zCel#nT-RgTozsBxU)=bMq#IQcAc8c?nQINlybWO{G|?18vjXrglH; z!+w&Q!EiODh%3l~{Fi#0GM+O5(VuE34hMo;b_2>KW*YX zeC&lfe~4<96%XHRdp^i|wrHLG|6J(`piY?KeuFpg!uH&aqHm)+N3q3xTT4`Fl<|+~ z8O$>-sNpxUl1uATy;Nn~@5}pUwFY1vD-cL(sr+rcWwmFQN5V0S`7}_dSNqI7UK8v? zzY|82>G>{VQlI9`a-MBt5X$5_-owX}(DO*l!Oet`(vhgiyB$m>v{eVKE_eAP^Z}fe zg5J)Rhd1uP;KziGrS!Ib<-($la*rqnIq#B#+qDsz#?BR5YyM{l8_8aaW<>5~v$!*7 z5yR|DNLQb;(ICB>j1>a46^+^`2y@L=5`Mj6@CZ1My?z=)!z8D%@!H0|k_O>+yuIUi z9kSkWu_JWAgZZw!eOaPe4>xmqh<5K@9KicHMy~W0t>lyM^jE))&GqvYJ*FklVAPfN z9^I0_Ccd>@&l5U|p>^dr2~+GazhjN5`B&~6vOV%sk7Y$L_-|zWf!rk zenkowd$1Ce?QP4KCb4HgJxY7M56gI%vk9GJ#^Ug@Ba|*rQx-VYmTnz#J{ZV2*%q03 z=QX{*Q@p+11s~_g>aBS^jdZjKC!}!T==UcE53bz7AiUQj)ur*KB)1zBCKl=l4Xp~- z=oo;7BhMv`R;nDd9>2RVnJ#s(P(t5)Y^xV{l5Pe#-DW=&kFT(nTtb#qsSy+as0v==K3bBmDcHx_r}$;pmP{ztk8td87(H%j3UyTF~V zcf(2(lSKObbvaxZGJ{W$1qp^K1-{8xckHB>RlKyEUaOlDxZ|x~=0FaXeWN7TZgrP7-dehW{;9;oJq&fY_}Pqg&&lq?f>3qH zuN69B$R~La0}?ox->pA2XpCT!ivT)M;7koOTGzNE{N0U+@jvcH6@LOZ(h z62=Wcy~kB3W}P~E>y0fh!tXVwxPK0&_-D;wGM&a?+ACIcCmvHEsp+reiZ*w)(3vL4 zYT%h_D6A5gncoJSIHQK}Gu(S?udm*e+35H89j9q!(m=!46?NK~h2izE@;06x>{Ae- zjF4df0m{^3L^b_%-{8DB4>O9HSn9R-`z^n8sC2Xwp0sHT3((UF1omd?l_Y1l5Fw*p z(vSR+8$>Ully*KF1IZZHe&n@bXB$skA-+b6Yr#-x>2*@rWrWm6c9BtD7&F0N>d%p5 z*IKbskh*5V1V%1EN*2x zbxNEUyG&bq$dQne3)}t->f@xIaW(}jb5|qwvXMIfbLqGW4g-k33e0};BTiC6@u+vv z18BW)fKq|%6)rZEB`$aJg(gTGeUY6yVO{YaChF287&Pj%A(U6f+(~Ce9pBoCw^x2_1W63ST;J78E-6tg^(xY5k4y!$WsdLJC^=T3#34G0rkq&|Iz|Hn+MdeN|2Tkc}zpM&$l{*yA8q}q^^ec@D01h0-R41 zZCb$)G50qOiR3XYmB{Qhba_vK*bkSa_AB_%U~W**NJu%Kf45njMnci^TC<^?TXppg z-wQvrpuUrb3IOYCRTaNu_$ldjg|0POPGs|Q-J(VfuLqhJ4M|ej=!SrAzapb0gXrKPd{83d z=xq`ViRbzxg%2&la~5)EoFog(=wUTW7QrU$5O^MvEQKw#M3pr2R8ssS(PcC?su-i_ zS+zm4FB2+IAglBrzk^qJO>fQg)Y~KNp z>&e{J(g==g_wj=pLKf=cGrMaaD-8QZ6B}+ndS9w@2d*G<{jpkFt$qyhAe z-n_N~S}f8{*LWjPTH$CQ4K7{#JP0UO4vR%XmDHn66PM0>QhF5nZEmjGuZ^yH33~3D zQKm*V4K1WL;KCxrtn||yq54U|_<=`?THt8wrf(c0+bi{&mLk3i$HIALd^(cd@&!+m z6G$Xo`XCK*y^&Ai1!rsR84GRxi%f$0$XK34O5ZP#ui_)jNGpeDQPaU>1wIZAVhtI- z`j>)0z2Jkz^5@0zqHG9{5?ezTJUFr%ce1>d!D5YZL(y>@gK8u3bAuufVF5Lqh4JnU zU7z6K*9m@CYj5429L>&L$$o9^pbO^Ffx=SwK4?iy>%@zbQ|vJ+E9sHVh9^e9&G4^k%xxua1ILS%0=bMj zn9>e1`fl*{n?;0IT;VwLboS>K3e@(B*>3^k!A4v3CB&}nnmtXg6@F- zmzbYn4A^$S*QLX^#qv=2VY$0Jgf zUjyOAVWSA&-vNOeyP>sqGT+LOe> zIrdfY(>%z#_Hn}COsSgz#6)LU4(42OH??LyLMl3u^b(|_qk5wmOj;qi=ge8it{03- zo5&$Ydy773<>%4gkC$2xvixqt5ZTDUli~*U9esDM(38Zt`j_=ynbR+DraqbtWI}}I z2JvcXER(9MrK-5Om*1wk5mi}i**SmtH>*2RJn3e4d^$b!Vn$PhR3HDr<$$UoG}^K( zsaa=Wtlm$@f`)<>)?0-Y;KEw&z#Fl_!Al>m1sOaksB!}TIO3j5H5O^y3BrwuE!w*S z;_--7MPAr10lHf;4GZm_I&F5qP1Py{?LZ=#x}sG^zpi2s^XefBolOvEtUA?x*&0@T zXWv^bgLDt=X%t?KCw^gk`Uy_sIx^Ea$aBzi0n*gOfk4{@A(uT zap-7wU3!Hc6~gtbTVHBaY^YZCdcXXeK9GS^h;GNj7f4FiaR~)aX!gj{XdpVL9WqgEUgpgyfAMs|q znkuOzSIFLBmK?%7|M%t@Tn5?L2`pE!c%s`1FokxM?8e zIb^d-(;Q}U5&?`$$+(U!!u|7GUW2sby)rUPR9{y_5GtQ*x%A0)pqJDXUp#~>HvBu- z5iSH!j{569lA&Na&Ixls?y=#&_DU0yK-?&>Mc7ZHXMDpC?$*@^@p%W9|JK2kdvq}x zFyv;D-Jew7he;o|0?p;8g_3dkXhsmMY<)!Ofs^`)Y0Wv3)C$;qd#6rimd2Iqal1aF zCR6e8({YQla(XXxQ7JVycj>CR`YDjY9EP8FX{7cFsN=NOWDD@vbQGR;AuwonL+)!#1!eFP`jl6pivJXH2wbS`AF zo$1l;5Jb42xIxaXIX>%KxV`!78vD-qef7{|P5^-l-K<=i5ki%_c}`@5gvbrK)svZ` zI&MYCB(rE^cbH6{9QzpQNya5;B43=fYF~PSXk+=`e=%bM^vQ`^_PlD+)Fkq|L3lzL zzf+|l?+1jK4=3c_-LOBdZp6(in2s0~W`yJD^epZke0=F?s_y#0%tLj_Dy&8rD*PDR_0y6k{7(zS;{;3m5KlhfElS=YE zzlcQv?B=Buo|-)*ix1g+Ta>}1wl+%Wf6I~AxVd98CazkM^fKrB96iDhv~Q8ytK%`| z-I`N(+&=8r%*V{Cc&ITVBWSZsCc}GYjuN?6wTXvpzDl)c?eJ0-+2E0!M%Ciwu3`Eh zggd(>><*5KT%0^dIWgU04=WWi=*O$HbC|4b`A|2OupDg>i>5J1d+3V`4EL@-hq5jp zwvoK#7ffI2nxOuyzl38C&fwpY`a8v=?nw42DE5RDA_&ih7aY>g$Vo`!L3ri8@t*Z3 z+gyfh+nxu76fE*qMzfSEV%H8D1a`ZVp?U|p{T|9@S>%Wy72bS&AWj7kCDuv*__lLO zVRw<~^*F^9Q%dWAxXE}2CAzU6xw3X|qxYAyX73zP3pKPc7lLV*)2-mU*m6+4gO)pBT(7!}{2 zn~Qbjd2)#XxlY^CA1QWAtPl#?WFVjZk{#h)RbSVgRFp|r8GIY+lD*?glbX)57x>te zs-Z_K(EOKKKkJ_@EJJKeD$gLk>Y%^v{f;<$`mN0Ib^3)!e-w##a7$y{MsqQT|L6=M zPjy4{`)l!A}HRJXPmHp_p8#3Si4w0J!lwR341pykN3pyCF;w*-7dee{2wdDnbt? zCd^{%kv|_k{`85k%qok?fz(`BX4eO-`l8@{kf+aI@Bj0d(_Zzj9NdTqEZHATsu`p$W>5nOB&q7<+3mzipstJE^*fd_WULqDTx$C;{QDfQpZ% z$hHI z+kGe%3NJoxZKiGk42a5p5qPn7tp^IdNH+`98I~>SO(oHn9&E$Hu>Ft?q5i1CE4~c)})Fo_1{_>vP9Do_vBi zjdfXZJr|MkdI#5K!GrSV+bIxUc7ZR5o#3Olf2{W7w{r>TYEB;GckDd~%gAyUk?Rie z)0r1zc-Cp<78tX2bZ7rc|NwK zhgh9A2kU<|1-HM6-mx3Ztx;g+U8P2g7iVvn7$-CKFnnvV_`xpzmVQK91eTvSrQRO_ zE%I$=X_~#)$GA`l^6Yvqv+eTWg{vmB!r5`7TOf!E_yJSr8tDo{?QZ~)1q*V9+W?|R zb*t0ltU_NjwKZOlr$s`>k2>BBKJ;H>Q2tVPII5H97@*hnvs-DJ<@mhw8C1CWcl={@ ze!4G((xct$!Ue%>N_uFKz#}EUd9xHIQnF{d-WhWl0HBBg07R2>Qy^wm&#H4YN0o-n zTjbrDeoJXuV5FYt^qAF9#1P@Wj=N4%Ha1lq*3|f-ovdJToK3q^J1nY~IXSfnq&Z3_ zfbfu`W#b^Ym%r%hfaUAQnbikn7QkXTJxKSV^V46X zzW?eD9zRDg75XY`h+;$&4OFDpP`<$!I)y`R&GN&)I#pO6nmuSqw3+Y?qu@>PJZAuyK`!PE z1t-2h=;uk;&mCR0AxFvI_1}~(^fDf;K16O(%2-c!q=zBSQa z^xouiRzxV{O!<6RA-`m7)5ctms1a&KEeXZ+Gfc0o%6$*UTbcJ zlnw19phUWqcbg*z!cFexHzxS%`%<3Z|Ja@P;nlP{kHAN%n|vR={FO+r#&+$kQ9`9BFh3*HE26B^*M@3L%d{@zB?4{nw=tr-l&*7iyRmK_ zb-V8R(Ou-ZHF-w%i-x?VFkpjlS(Vg@?ye3DBHyq3(Jto%?T^Ei`Mo;tTyNn(SR_iG z5R*y@&uUmtb^DX>1Bv+*Fra9K*dvkfEQk3q$f!Id^C6V*pafIn>8=3za$hgZ{z|Es z+QTujqVSY)qq>)=02d-9axD2s9u?OiVqmz=5<+g&#+u8obWtQ%m*&zl();y({<`*G zhW&*Na_5g?MzJB(X)D7Qbq=)boDfKIF57tdV4uDeHRZhNiSHlH#ZB#=*=k9*jDV)L z5qBGz-rP;2MI-A=jqDXxHGeQUeNV3Rok;&;oY)}Qc}Q3$wcMZ7mf{g{?>l0B5qG?2 z8}=15C2n*8(ku-Yf`c~hUpr)zS>HYut#6ewnVAcE)>%vTPk3@&-0g*`GdY2I0gNfG z>w|R>4k7JZb%%yDYOzf-x(wQKxjtIoRj{M5TA@yM>?3Or{T>8gr|>NmQ+BlzJc3_e z0K?_HcRe1$o>$_>wO`7Di14c=T2gL0U!2_%#BHOpL;Y38c3-_$uLPCDX~HwbAaI#G za_p2VcWPY7=W15_?#}Hdwt|jErj!C|o&XU+^w@Ik> zkxAlKLFMbB0sw>?r@?Y^DcmCsm-)7bd&tUp$w%|tVE#J2^9-Rx`i=$+ay8<=DN;{{ z(&L#kIkrPcm;buet9G&+dp zkcygBaKoIB@80tqZp^9l@+}4%&i$D-&?)1>qIxM^Ez1LTAKqPuMYpJXYN}%7pPAt}EC3YBRT09n!^;p&SaD7bjkS z{YEkiIMW{h{HQ80sk9@Z7iEsMH+|F4(0gSh@Mm9X;F2>|J*9E{@n;TtAUp5XBc13wepCPc%+_Q z|Clp4oqm*Pn(z0#?!NPzdo(QoE7%PxvJwV?NZ$oOWEzrqO^#W&1UY68YmC!g_Zy%3 zBvHtWfyh*E_YB}P@cHe0v=|(7$uvT@i2@NQ#BYAcg6fIKC1fmb)uimwD5;N0tqKS< ze-f5D|6#5`xJIS?`$JVk82D6oC5W=5Oy-ioQClCC(yXTYsjZva%^;lB+Pwb_YfsQA zhLLDvuZ~_ticdF&`U2L%VXl`y4YvChNm?vK)7hE_Cl7|3bM244KG4blTLy}19k%Dm zU)??qdl`4L?4C0roE3+^(#V&&N=MfLDbvt}<~w^ujjKz_N*y-kriRIRbQjGLm{loW zb$P~IEjBr)b+{j0|LM${Zb^_Whz3vGb>*>3jkU z*@gHzYhTv8^l4?-r(NtH8&735FfCM^)H|N$8X0OV{}3F|!%mF-d z0ik|AnGcGjAxP3g(wGRP#DfFRuD<+1`(#>dXXzbE&*O7qKP4Lp*`)UddfV>pn6B)o z*GNe9{lrlbYEqg32jqxb7%RheDLCUh0kbXhb2vDEE zC4HO|RB&=n@=cbTLYlyM)Mtyh5$wI)LYH5Kt%DLXOEO~k3~H!R>M6Iu5$tM=p1Joe znq`orec~c9IR7urMq6M|BbzeXlx+PoS+ttP zCO3b{BEG*82D+Uhgd7_0U@4WAxc%I7)l6s~4NMn_Y$z{}jl*&)XPdJ(CG?QNO}!cf zaGq~ju$#8wS}7^{Hr@qrzG=6aw`ah)BVXF$J9+jf$l=7r!S9SOuHYd~@W%e=c}9+g zT$sEq1TwXLIgji$-(BEVqEgJDVmmg~)VG}*w`E)Ij56~JuXYpmLJ8V#gFyU}c?JH^ z-X@0&Jb5-A?8u0*(5fQrw4j9q^@x{?GQ`Sw7TUn!Y5EF)A|!_3KrPVF0>gpl<@i{T zGgd@~n6p9nEyU9{Tp7(EvH+R+s}=D+$5rHtUO})@Xyl)SDn-73XZ|AKzN3$QHS7=! z@k7aATodKiwv`t>3I@^>Qihqd(SAFkvaH!LmnjjB+==!YteS*;H=XGwa~^?HCI{mD z34F0?#*bOXD?hxN@ni#MhA7f)OmGLst(`NygmlANceq=AX0pAH^VxKJd8T}i%XNUp zfw)e6rxVoYe(eRxmNNIhsJlb2L|Vd;c*kpytr`|QEu0r-yo*I=)&KHJU?Z_hP*RKG zk?y=d!c)G|vG2V>KCHe36L^9Hnwxegmrw^#&5KVQY&#QO#ld+)zQ%q0{VRko@J~6q zYgV342GmTbaMamU9gAGWhi}yQP+e5#+)Qo3(Troi7$|$?UaP$rYlW)%OkUdHobN87w2nztoVMNni-9QgM$u zRLkDM`SC`diZ?lj(;z;j{5NV;{u3u*i5}oamhnD;Pj@U2WqLwfxiF89w}=1BN?Z75nZOV=#Z8Hfe87O(ZI;nc8*W!XglL^NKEBf1 zboPJZW#8~{gwUS1ZJKGp5{{7kV#Jj*`wrGP0T2s;%!ro zIuFGS)~lB!^2Jg|L5?TXY)NIb?@TtN?hl@Z$nI6Gch+0qvtXdg_=%Gx+@ijl?(89k zT_-7#qUS%y1qI{HM6ZCb5A{hfgNY^tcymqB*P8OeM;ytoTeMY6yWhlMG02INHsg41 zlTmJ~E2*PVJMdpi{b~NuZ(uutW#CueHY{2@qEHYUxjYc8mNoT)xBa;CqQSMQgKQ7n ztziPmt{863KFB`A*=8vg@s53{H-3AZZ^4a8e`6KhV$ayrR@X4KoSg|mm$c(@OoX%V zle)q0SOTL)sq_J?Nj?2IuP;LR{-vYVN+aDyPk7YRN;Q^Ld@=LCcJXTU-%&~jY!h7N zl`a@aT$IwO@m^qr;}PHXvRa(N^;QM6Bbs5`jpnrD^{{h4q?BP30`oXzPC ze9d&-Qya;ro_yR_Prz=Haa+IJO{VLojy1!^?6|aO#e@9CO(sFjc5dM^TIUr@WJ#MV z>kRRY)8_ZzZgU1kwYnqI?7^h%wWZ(_(ZN={(Y(Ja#3IsV@iA!Z_Uinboa`=Y@b$yD z|BKA=J^RP*HEF4qWQ&9TguLtz0y`Vf@)c8zE~d(|HeD;gdlfzBp}bPdhG zO0i2zXKr7jIo{y{9{B$f?#4BfFk!HfZkQZ44&tdgJ7z2>WAZsJ*nN|CA36u1lFHmy zFb4=LKdYi%17^V}1WJ$y7bpPS0~?*(BzFssR6SFKcjt#cW02yRJh>9EKc(f(Q|V)? z0UhD9*n{1(5rH9lRLEkz5GHEGlYl>s=Z(Xz;K)<({9%;ZvL998Bj6=cqtF<^rys6i zsvBu%y{uj608KdV9^eu^e^(+~(%N|2lO?G3_D6`D{zU|L?N&|Ecc;wX`-glz_2-pI znY5|ubDx0^Iwki$ZmoPmdNUpQ^G}J5C&jL3bPH#XHz%+w23aO45x>o@Q{x$L=L-Q7 zuNvc}t#dtB{>lHQMVe;9ov1+vT`1}@Y2n&l+`SG>5bCWL!JA4383*2!{NT-&OA*5Nc~m0OF4D43(@tz4uJoz{QII=XZbVSg*jaS$|K+zS&11ziK`@w|FEQ#v<5huOuZ?yyIUPrbtRJNb z?H}z#_)8bmer<&omqJPe6-X`90b}cAv#p5>;l7ITx__cYuCbX(?YmqIm6;OaR9|IC zuz8On%HV=Te$h>Vl+h;N6*a^iA$~eT*|S-Hqk%v7u%7@8oTFa`7VUXh%QVhW3X5=i zg8Rt^KKw}vghPv8w^%)-Jy}tF?QHj)1t*T`h)*hVvcO-CHDta=4M$7BA0{54$`5!L ztNH3Sda&p^!e_y^bZO`N?$xOv8D}$CDIYi(eVaaGPVzi3kfp-7=K=sU*pQKVhcu%U z06w49K$;v_rd&+l+m z@rImN8JMFb&~snKqe>nxG)<2}Lt=zaj_0Ph=>sIz>-Y`#%JEs$7CA9++!juCnd(|n z&k>_WL->pH0Yg<298T$2I{mhHS0=w7UiF}?#oXa?XHLUKv1Iay>DlR08-0BJibCWu zZq#6|g@Hp|WD-OcTZ-F>8bQ{`C3E{ThzK^w)hxndoQag$$-2`hKn8v*S!=q}iiD#8 zmw~U+D=Q^1fnkT$ai>)K6Ub0U@D_KYfL7|uJDQcvxHsA%-f=(ft>cGkGX$0V#T()m zvQva@3X58bOSkb+$!^PUMvnQH zm3$M!JJm%e=8{;=K$UhJVYDXT!OIjp~s!Z7xn^7XJp@ss$@ty1daL6-4k zk{?++%6YG2P={ZvlgxJWg1ZbePt1IZg%&8m4+eZ6D7D)1yGuM%E}#ERUNO+nQ+^V_ z_#@fJgH%F`Eb!RtEq|B#=}T)Hbqvzc9bPeQ)MVdv7Q5}Xn|DRGciG$QxH+xBtj*gY zkja^s6{ca(-Ue7SngwC@%9*x}SB?xoY#M+3?3pwOa(VW)B@6FK1P4qaAx$?F?L~b` z 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 z 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