This workshop is about Approximate Membership Query (AMQ) filters: data structures that answer "could this item be in that set?" using much less memory and time than checking the set directly. They can give false positives ("maybe in the set" when it isn't) but never false negatives (if the item is really in the set, the filter will never say no). That one-sided error is what makes them useful as a fast pre-filter: you use the AMQ to throw out everything that is definitely irrelevant, and only run your slow, exact check (reading a whole file, aligning a sequence, ...) on the small number of candidates it leaves behind.
The specific AMQ filter we use here is the HIBF (Hierarchical Interleaved Bloom Filter), built on top of the classic Bloom filter. It can index many "user bins" (e.g. one bin per file, or per genome) at once and answer "which of these bins might contain hash X?" very quickly.
You don't need prior experience with any of these — every task below tells you exactly which calls to use.
| Library | What it's for | Rough analogy |
|---|---|---|
| sharg | Command-line argument parsing (already wired up for you in each task's include/taskN.hpp) |
Python's argparse |
| seqan3 | Reading FASTA/FASTQ files, computing k-mer hashes, pairwise sequence alignment | Biopython |
| HIBF | The AMQ filter (Hierarchical Interleaved Bloom Filter) itself | — |
| cereal | Serializing C++ objects to/from a binary file, so you can build an index once and query it later | Python's pickle |
A couple of C++ idioms you'll see that may look unfamiliar coming from another language:
[&](size_t id, insert_iterator it) { ... }is a lambda, an inline anonymous function.[&]means "capture surrounding variables by reference", similar to a Python closure.sequence | seqan3::views::kmer_hash(seqan3::ungapped{20})is a range pipeline:sequenceis lazily transformed into a range of k-mer hashes, similar to chaining.map()/generator expressions.
| Path | What it is |
|---|---|
include/ |
Each task has a helper .hpp file you can include. |
src/ |
Put source files here that you will use during this workshop. |
scaffolds/ |
If you are inexperienced in C++ use the scaffolds to solve the tasks. |
solutions/ |
Don't peek! |
mkdir build && cd build
cmake ..
make task1 # build a single task, once you've added it (see below)Executables land directly in build/, so once built you run them as:
./build/task1 -i /path/to/input/dirEach task needs its own line pair in src/CMakeLists.txt (not the
top-level CMakeLists.txt). Copy the pattern already used for the
solutions, e.g. for task1.cpp:
add_executable (task1 task1.cpp)
target_link_libraries (task1 PRIVATE gcb2026_lib)Re-run cmake . in the build directory once after adding a new executable so CMake picks it up.
We have a directory of plain-text "papers". We want to find every paper
that mentions all three words "3D", "protein", "structure", and the
total word count across those papers.
- Task 1 does this the naive way: open and read every file.
- Task 2 builds an HIBF index over all files (one file = one user bin).
- Task 3 queries that index first, and only opens the (usually much smaller) set of files the filter flagged as candidates — this is the speed-up an AMQ filter buys you.
If you haven't been coding the scaffold during the workshop with us, copy over scaffolds/task1_scaffold.cpp to src/task1.cpp.
For each file, check if the file contains the words "3D", "protein" and "structure" and if so add the word count of the file to a total word counter. In the end print out
- the total number of files,
- the number of files that contained the three query words and
- the total word count of all files that contained the three query words
Then run your program:
./build/task1 -i data/mock_papers
Tip: src/task1.py implements the exact same logic in Python — useful to
compare against once your C++ version compiles and runs.
Need more guidance? Check out the step by step guide at the end
Write the file task2.cpp that uses the helper functions in task2.hpp for parsing the command line (parse_cmd) and storing a hibf (store).
Create an HIBF on the same files as before, supplying only required fields to the HIBF config object. The input to the HIBF is a lambda function, that should open a file and hash each word in the file using std::hash. In the end, store the index using the store function from task2.hpp and use the parameter args.index_path parsed from the command line as the output filename.
Documentation you'll need:
- HIBF lib: https://github.com/seqan/hibf (note the snippet on the landing page)
- HIBF API documentation: https://docs.seqan.de/hibf/main/index.html
std::hash: https://en.cppreference.com/cpp/utility/hash
Need more guidance? Check out the step by step guide at the end
Write the file task3.cpp that uses the helper functions in task3.hpp for parsing the command line (parse_cmd) and loading a hibf (load).
Parse the command line with parse_cmd. Create an HIBF and filenames object and load them from disk given the args.index_path path. Create a vector of three query hashes, that contain the words "3D", "protein" and "structure" hashed by std::hash. Use the HIBF to query these words in the index with an appropriate threshold. Loop over the resulting hits and do the same as in task 1: Open the candidate file, count the words, if all three query words are contained, add the count to the total word counter and output the results in the end.
Documentation you'll need:
- HIBF lib on GitHub (note the snippet on the landing page)
- HIBF API documentation
std::hash
Need more guidance? Check out the step by step guide at the end
Same idea as Part 1, but on real sequence data (FASTA/FASTQ) instead of
plain text, and using k-mer hashes instead of whole-word hashes. A
k-mer is just a substring of fixed length k taken from a sequence; hashing
all k-mers of a sequence is the standard way to represent it for a Bloom
filter. Instead of an exact string match, "confirmation" here means running
a real pairwise semi-global alignment and checking its score.
Tasks 4–6 mirror tasks 1–3 one-to-one: task 4 is the naive baseline, task 5 builds the AMQ index, task 6 queries it and only aligns the candidates.
In task4.cpp using task4.hpp helper functions parse_cmd you should parse the command line with parse_cmd that now has, additionally to the -i/--input option, a -q/--query option for the input query path. Create seqan3::sequence_file_input on the query filepath and store the sequence from the first and only record of the file in a variable query. Loop over all files and open each reference file using seqan3::sequence_file_input again. Loop over each record in the sequence file (there can be plasmid DNA additional to the genome). For each reference sequence compute a semi global alignment using seqan3::align_pairwise with a seqan3::align_cfg config object that configures seqan3::align_cfg::method_global for a semi global alignment, seqan3::align_cfg::scoring_scheme with a seqan3::nucleotide_scoring_scheme{} and seqan3::align_cfg::band_fixed_size with seqan3::align_cfg::lower_diagonal{-9000} and seqan3::align_cfg::upper_diagonal{500}. Print the filename, reference name and alignment score for each file to the command line.
Documentation you'll need:
Need more guidance? Check out the step by step guide at the end
Write the file task5.cpp that uses the helper functions in task5.hpp for parsing the command line (parse_cmd) and storing a hibf (store).
Parse the command line as usual. Create a file_data lambda function as you have done in task 2 but now open a seqan3::sequence_file_input for the current file, loop over the records (of reference sequences) in the file and then loop over each record's sequence(), hashed by the seqan3::views::kmer_hash. Construct the seqan3::views::kmer_hash with a k-mer size of 20 and assign each hash to the iterator it of the lambda. Construct and store the hibf as you have done in task 2.
Documentation you'll need:
Need more guidance? Check out the step by step guide at the end
Write the file task6.cpp that uses the helper functions in task6.hpp for parsing the command line (parse_cmd) and loading a hibf (load).
After parsing the command line, load the hibf and filenames from disk as you have done in task 3. Then read in the query sequence as you have done in task 4. Hash the query using the seqan3::views::kmer_hash with a kmer size of 20. Query the HIBF as you have done in task 3, this time providing the hashed query and a threshold that reflects that 90% of the hashes match. Loop over the resulting hits as you have done in task 3 and compute a semi global alignment between each reference file and the query sequence as you have done in task 4.
Documentation you'll need:
Need more guidance? Check out the step by step guide at the end
If you haven't been coding the scaffold during the workshop with us, copy over scaffolds/task1_scaffold.cpp to src/task1.cpp.
- Before the for loop, create three counter variables of type
unsignedinitialized to0:file_counter,paper_counter,total_word_count, like thisunsigned count{0}; - Before the while loop, create a counter variable of type
unsignedinitialized to0namedword_counter. - Before the while loop, create three
booleans namedtext_protein,text_3D,text_structureinitialized tofalse. - Within the while loop, increase
word_counterby one, since we can count the words while checking them right away. - Within the while loop, create an if clause for each query word (
"3D","protein"and"structure") checking if the variablewordequals the query word and if so, set the respective boolean to true (e.g.if (word == "foo") text_foo = true;). - After the while loop, check with an if clause if all three booleans from step 3 are set to true and if so, increase
paper_counterby one and addword_countertototal_word_count. - After the while loop, increase
file_counterby one. - After the for loop, print out all counters using
std::cout, e.g.std::cout << "done: " << paper_counter << "/" << file_counter << " files, total words: " << total_word_count;.
Then run your program:
./build/task1 -i data/mock_papers
Tip: src/task1.py implements the exact same logic in Python — useful to
compare against once your C++ version compiles and runs.
Building an HIBF on the mock paper data
- Create
src/task2.cppand add it to thesrc/CMakeLists.txtfile as an additional executable. - Create a
mainfunction like in task 1. - Include
#include <iostream>,#include <fstream>and#include <task2.hpp>. - Parse the command line just as in task 1 (using
parse_cmd). If you take a look intask2.hppyou can see that it now has two options-i/--inputfor the input directory-o/--output-indexfor the index filename
- Copy over this lambda signature
auto file_data = [&](size_t const file_idx, seqan::hibf::insert_iterator it) { // todo };
- Within the
file_datalambda- Open a
std::fstreamnamedfilethat is constructed with the filename at positionfile_idxin the vectorargs.filenames. A vector is accessed via[]. - Create a
std::stringnamedword. - Create a while loop just as in task 1 that loops over the words in
fileand for each word assigns its hash to the iterator like this:it = std::hash<std::string>{}(word).
- Open a
- Add includes:
<hibf/config.hpp>and<hibf/hierarchical_interleaved_bloom_filter.hpp>. - Create a
seqan::hibf::configconfig object namedconfigthat is initialized using designated initialisers. Set.input_fntofile_dataand.number_of_user_binsto the number of files (hint: the size of a vectorvcan be accessed withv.size()). - Construct the filter:
seqan::hibf::hierarchical_interleaved_bloom_filter hibfinitialized with the config object from step 8. - Store both
hibfandargs.filenamesto disk using the store functionstore(hibf, args.filenames, args.index_path).
Need more help yet? Use the scaffold scaffolds/task2_scaffold.cpp.
Querying an HIBF on the mock paper data and counting words
- Create
src/task3.cppand add it to thesrc/CMakeLists.txtfile as an additional executable. - Create a
mainfunction like in task 1. - Include
#include <iostream>,#include <fstream>and#include <task3.hpp>. - Parse the command line just as in task 1 (using
parse_cmd). If you take a look intask3.hppyou can see that it has the only option-i/--indexfor the index filename. - Create a
seqan::hibf::hierarchical_interleaved_bloom_filternamedhibfwithout initialising it. As an example, creating a number without initialising it is done byint a;. - Create
std::vector<std::filesystem::path>named filenames without initialising it. - Load hibf index and filenames using the load function
load(hibf, filenames, args.index_path). - Construct a
std::vector<uint64_t>on three input arguments:"3D","protein"and"structure"each hashed bystd::hashjust as in task 2. A vector with three elements is for example constructed like this:std::vector<uint64_t>{1,2,3}. - Create a hibf agent called
agentusing the member functionmembership_agent()on thehibfobject. The type of theagentvariable can beauto. Example withauto:auto a = 3, a is typeint. - Use the
agentto query the HIBF by using the member functionagent.membership_for(query, threshold), passing the query vector and an appropriate numeric threshold value (e.g.1or1000). Theresultof the member function is again stored in a variable usingauto. - The result is a vector of numeric values indicating the file indices of files that likely contain the three query words. Loop over the
resultvector with a for loopfor (file_idx : result). - Now you need to do the exact same analysis as you have done in task 1 for the candidate files. Follow the steps 1-8 of task 1 again or copy over the code and adapt it to this for loop.
Need more help yet? Use the scaffold scaffolds/task3_scaffold.cpp.
- Create
src/task4.cppand add it to thesrc/CMakeLists.txtfile as an additional executable. - Create a
mainfunction like in task 1. - Include
#include <iostream>and#include <task4.hpp>. - Parse the command line just as in task 1 (using
parse_cmd). If you take a look intask4.hppyou can see that it now has two options-i/--inputfor the input directory-q/--queryfor the query filename
- Include
<seqan3/io/sequence_file/input.hpp>. - Construct a
seqan3::sequence_file_inputfile namedquery_filegiven the query pathargs.query_path. (sequence_file_inputdocs) - Retrieve the query sequence of the first and only record like this:
auto & query = query_file.begin()->sequence(). Explanation:query_file.begin()gives an iterator to the query_file range.->gives access to the member function of the object pointed to by the iterator andsequence()gives you the sequence of the record. - Loop over the filenames in
args.filenamesas you have done in task 1. - Within the for loop, construct a
seqan3::sequence_file_inputnamedreference_fileon the current filename. - Loop over the records of the
reference_fileviafor (auto && record : reference_file). - Within this for loop, create the following alignment config object, but replace each XXX with either true or false to configure a correct semi-global alignment:
auto config = seqan3::align_cfg::method_global{seqan3::align_cfg::free_end_gaps_sequence1_leading{XXX},
seqan3::align_cfg::free_end_gaps_sequence2_leading{XXX},
seqan3::align_cfg::free_end_gaps_sequence1_trailing{XXX},
seqan3::align_cfg::free_end_gaps_sequence2_trailing{XXX}}
| seqan3::align_cfg::scoring_scheme{seqan3::nucleotide_scoring_scheme{}}
| seqan3::align_cfg::band_fixed_size{seqan3::align_cfg::lower_diagonal{-9000},
seqan3::align_cfg::upper_diagonal{500}};- Then, invoke the pairwise alignment which returns a lazy range over alignment results with
auto result_range = seqan3::align_pairwise(std::tie(record.sequence(), query), config). - Given the
result_rangerange, acquire its start iterator with the member functionbegin()and directly dereference this iterator using*to get thealignment_resultof typeauto &. - Print out the current
filename, the reference name via the member functionid()of therecordand the score of the alignment viaalignment_result.score().
Need more help yet? Use the scaffold scaffolds/task4_scaffold.cpp.
- Create
src/task5.cppand add it to thesrc/CMakeLists.txtfile as an additional executable. - Create a
mainfunction like in task 1. - Include
#include <iostream>and#include <task5.hpp>. - Parse the command line just as in task 1 (using
parse_cmd). If you take a look intask5.hppyou can see that it now has two options-i/--inputfor the input directory-o/--output-indexfor the index filename
- Include
<seqan3/io/sequence_file/input.hpp>. - Copy over this lambda signature
auto file_data = [&](size_t const file_idx, seqan::hibf::insert_iterator it) { // todo };
- Within the lambda, construct a
seqan3::sequence_file_inputnamedfileon the current filename. - Loop over the
records of thefileas you have for example done in task 4. - Within this loop create the
hashesof typeautoby piping|theseqan3::views::kmer_hashonto the record's sequence (record.sequence()). Construct theseqan3::views::kmer_hashwith aseqan3::ungappedshape that represents a standard kmer of size20. - Loop over the
hashesview and add each hash to the lambda iterator viait = hash. - After the
file_datalambda construct and store the hibf just as in task 2. Follow task 2 steps 7-10.
Need more help yet? Use the scaffold scaffolds/task5_scaffold.cpp.
- Create
src/task6.cppand add it to thesrc/CMakeLists.txtfile as an additional executable. - Create a
mainfunction like in task 1. - Include
#include <iostream>and#include <task6.hpp>. - Parse the command line just as in task 1 (using
parse_cmd). If you take a look intask6.hppyou can see that it now has two options-i/--indexfor the index filename-q/--queryfor the query filename
- Follow task 3 steps 5-7 to load the
hibfindex andfilenames. - Follow task 4 steps 5-7 to read the
querysequence from file. - Create a
query_hashesrange of typeautoby piping (|) theseqan3::views::kmer_hashonto thequerysequence. Construct thekmer_hashwith aseqan3::ungappedshape initialised to a size of20. - Query the HIBF and loop over the result by following task 3 steps 9-11.
- Compute a semi-global alignment for each reference sequence by following task 4 steps 9-14.
Need more help yet? Use the scaffold scaffolds/task6_scaffold.cpp.
With the default seqan3::nucleotide_scoring_scheme{} (match 0, mismatch
-1) and the default linear gap cost (-1 per gap character), score is
exactly the negative edit distance between the query and its best-matching
window in the reference: score = -(mismatches + indel_bases), max 0.
| score range | approx. identity | interpretation |
|---|---|---|
| ≥ −400 | ≥95% | strong/confident hit |
| −1600 to −400 | 80–95% | good, likely true positive |
| −3200 to −1600 | 60–80% | marginal, scrutinize |
| < −3200 | <60% | noise floor (random background ≈ −0.75·L_query) |
Caveats: N/ambiguous IUPAC bases score as hard mismatches, not neutral, so
assembly gaps inflate the apparent divergence.