Skip to content

feat: add the LSH fast layout algorithm - #343

Merged
eseiler merged 39 commits into
seqan:mainfrom
smehringer:final_fast_layout
Oct 8, 2026
Merged

eseiler merged 39 commits into
seqan:mainfrom
smehringer:final_fast_layout

Conversation

@smehringer

@smehringer smehringer commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

This would add the fast layout algorithm in its current status (dissertation status mehringer) to chopper.

Open questions: Should the fast layout algorithm instead be added to the hibf library?

Note to the code:

Currently most of the algorithm lives in src/layout/fast_layout.cpp
During coding, I added some detail tests so there are two files with some extra code (that is again only used in fast_layout.cpp)

  • determine_split_bins.hpp/cpp
  • fast_layout_cluster.hpp
    with their respective tests.

@seqan-actions

Copy link
Copy Markdown
Member

Documentation preview available at https://docs.seqan.de/preview/seqan/chopper/343

@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.43494% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.59%. Comparing base (3f3a389) to head (eac653a).

Files with missing lines Patch % Lines
src/layout/lsh_distributed_ibf_layout.cpp 95.66% 12 Missing ⚠️
src/layout/fast_layout.cpp 94.23% 6 Missing ⚠️
include/chopper/layout/fast_layout_cluster.hpp 98.07% 1 Missing ⚠️
src/set_up_parser.cpp 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #343      +/-   ##
==========================================
+ Coverage   93.35%   94.59%   +1.23%     
==========================================
  Files          20       25       +5     
  Lines         753     1295     +542     
  Branches       18       21       +3     
==========================================
+ Hits          703     1225     +522     
- Misses         50       70      +20     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

eseiler and others added 25 commits October 8, 2026 11:57
lsh_sim_approach runs concurrently for different merged bins, but
concurrent_timer::start() and stop() write shared, non-atomic time
points; only operator+=() is thread-safe. Besides wrong timings, stop()'s
assertion stop_point >= start_point failed in Debug when another thread
restarted the timer in between (12 of 40 runs of the new fast layout
recursion test with 8 threads).

Time with local serial_timers and add them to the configuration's timers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No test reached fast_layout_recursion and add_level_to_layout: a merged
bin needs at least 64 * tmax user bins for that, and the largest fast
layout test had 96 user bins with tmax = 64. With tmax = 4 and 2000 user
bins, each of the 4 top-level merged bins (about 500 user bins) is laid
out recursively, and the DP layout handles the level below.

The layout is checked structurally: every user bin is stored exactly
once, merged and stored technical bins do not overlap, and there is one
max bin entry per lower-level IBF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
find_best_partition computed union_estimate - current_partition_size and
asserted that the union estimate is not smaller. HyperLogLog estimates are
not monotonic under merging: at the switch from linear counting to the
raw estimate, a superset can be estimated smaller (4 of 800000 steps in a
probe with 12 sketch bits, by up to 51). Debug builds then aborted and
Release builds wrapped to a huge penalty that excluded the partition.

Clamp the penalty to 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The documentation claimed that the threshold is multiplied by
max(1.01, sum / (threshold * max_bins)) and that the number of split user
bins is clamped. The code sets the threshold to
max(threshold + 1, sum / max_bins), and the number of split user bins
cannot exceed the number of technical bins, which is only asserted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
post_process_clusters sorted the clusters after the first tmax by the
cardinality of their largest user bin, with 0 for empty clusters. This
put empty clusters last only if every non-empty cluster has a cardinality
estimate above 0. Otherwise, a non-empty cluster could end up among the
empty ones. Debug builds then failed the is_partitioned assertion, and in
Release builds lsh_sim_approach stopped at the first empty cluster, so
the user bins of such clusters were not assigned.

Sort by (non-empty, cardinality) instead. For all other clusters, the
order is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ined macros

partition_user_bins_test.cpp and check_filenames.cpp use the
CHOPPER_WORKAROUND_GCC_BOGUS_* macros without including
chopper/workarounds.hpp. The undefined macros evaluate to 0 in #if, so
the diagnostic pragmas were never active. This is why the GCC 16
-Warray-bounds workaround in partition_user_bins_test.cpp had no effect.

-Wundef turns such mistakes into errors. For it, the feature test in
fast_layout_cluster.hpp uses #ifdef, because __cpp_lib_containers_ranges
is not defined before C++23 or with older standard libraries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fast_layout duplicated add_level_to_layout to record the top-level IBF.
add_level_to_layout now sets the idx of the user bins it records and
returns the maximum technical bin. fast_layout stores it as
top_level_max_bin_id, fast_layout_recursion appends it to max_bins.

The layouts are unchanged (compared on a recursive and a mixed input).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
find_best_partition always returned true and threw only if no partition
was selected. With at least one partition, partition 0 is always
selected unless its cost is SIZE_MAX, and lsh_sim_approach always passes
at least one partition. Assert that instead of tracking best_p_found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document Cluster instead of the "foo" placeholder, including that
  is_valid accepts both valid and moved clusters.
* Make the members private; nothing derives from Cluster.
* Make the single-argument constructor explicit.
* Replace the comments above LSH_find_representative_cluster and in
  very_similar_LSH_clustering, which described an older representation
  (cluster[i][0] == i).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
append_range and insert do the same for a std::vector<size_t>, but each
toolchain only compiled and tested one of the two branches. Keep insert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document determine_split_bins.
* Remove commented-out debug output and the unused max_id.
* Remove the unused print_matrix.hpp include, include what is used
  instead of <numeric>, and fix the namespace in the file brief.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
general_layout started and stopped a local dp_algorithm_timer that was
never read. Return the result of compute_layout directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
remaining_clusters.insert(remaining_clusters.end(), x) is push_back(x).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parameter holds the user bins per partition, and the caller passes
partitions. Name it partitions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename intital_partition_timer to initial_partition_timer. This also
changes the column header in the --timing-output file to
initial_partition_timer_in_seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
determine_best_number_of_technical_bins always uses the DP layout, so
execute silently ignored fast_layout if determine_best_tmax was set. The
command line already rejects the combination; the API now throws
std::invalid_argument.

The two execute_estimation_test cases with both options set expected the
DP output. They are kept under #if 0 until the combination is supported,
and same-named tests check the throw instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It had a plain comment, so it was missing from the documentation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Many names were left over from the PHIBF, for which the algorithm was
first written. Mostly, "partition" is replaced by "technical bin":

* partition_user_bins -> lsh_distributed_ibf_layout, including the
  header, source and test file
* find_best_partition -> find_best_technical_bin
* partitions -> technical_bins for parameters and variables
* initial_partition_timer -> top_level_lsh_distribution_timer and
  search_partition_algorithm_timer -> find_best_technical_bin_algorithm_timer

The columns of these timers in the --timing-output file are renamed
accordingly: initial_partition_timer_in_seconds ->
top_level_lsh_distribution_timer_in_seconds and search_best_p_in_seconds ->
find_best_technical_bin_algorithm_in_seconds.
Replace the test's own consistency check (check_layout) with
seqan::hibf::layout::layout::validate. The test uses tmax 4, so the build
would round each IBF up to 64 technical bins, and validate warns about
the trailing empty bins (unexpected_empty_bins). The test accepts exactly
these warnings and fails on any other finding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
execute validates every layout with seqan::hibf::layout::layout::validate
before it computes statistics or writes the layout file: the DP layout,
the layout of determine_best_tmax and the fast layout. Errors throw
std::invalid_argument; warnings, and notes in debug builds of the HIBF
library, are printed to std::cerr.

This replaces the debug-only check in fast_layout that all user bins are
assigned. The verbose statistics are now printed after the validation,
outside of the branch that computes the layout, so the condition
excludes determine_best_tmax, which writes its own statistics.

The tests that call execute check stderr: no findings, except for the
note about the empty technical bins of few_ubs_fast_layout, which the
fast layout produces by design.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
read_layout_file does not validate the layout; both subcommands of
display_layout validate it right after reading. Document that
read_layout_file does not validate.

The layout of the display_layout tests has tmax 4, so each IBF ends with
60 empty technical bins instead of none (unexpected_empty_bins). The
tests expect these warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
display_layout only caught parser errors. Any other exception, e.g., an
invalid layout, terminated the program. Catch std::exception, print
"[ERROR] <message>" and return EXIT_FAILURE, like chopper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eseiler
eseiler force-pushed the final_fast_layout branch from d9813a6 to eac653a Compare October 8, 2026 12:52
@seqan-actions seqan-actions added lint and removed lint labels Oct 8, 2026
@eseiler eseiler changed the title WIP: [FEATURE] Add the LSH fast layout algorithm. feat: add the LSH fast layout algorithm Oct 8, 2026
@eseiler
eseiler merged commit fa49783 into seqan:main Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants