Skip to content

Make remove_all_gap_columns linear and fix empty-row misalignment - #124

Open
DylanCosto wants to merge 3 commits into
bxlab:mainfrom
DylanCosto:perf-linear-gap-column-removal
Open

DylanCosto wants to merge 3 commits into
bxlab:mainfrom
DylanCosto:perf-linear-gap-column-removal

Conversation

@DylanCosto

Copy link
Copy Markdown

Alignment.remove_all_gap_columns() deletes each all-gap column from every sequence list one at a time, which shifts the rest of each list on every deletion. On long alignments with many gap columns this becomes very slow (about 5 s for 80,000 columns where 90% are all-gap).

This works out which columns to keep in one pass and rebuilds each component's text once. That example drops to about 0.03 s. It also returns early when some component has no gaps at all, since then no column can be all-gap.

It also fixes a bug: for an empty component, the old code added two entries to its list of sequences instead of one, so every later row was misaligned. An empty MAF row before the sequence rows could get another row's text. If the same component object appears twice in components, it is now rebuilt only once.

test_remove_all_gap_column now places the empty row first, in the middle and last, and also covers a repeated component. The first two placements fail on main.

DylanCosto and others added 3 commits October 10, 2026 23:43
Resolve the conflict in `Alignment.remove_all_gap_columns()`, where
upstream/main had fixed the misalignment of the sequences after an empty
component (commit 3ba2cc9) and added type annotations (commit 7a02fcd), by
keeping the new single-pass algorithm from this branch, which already
includes that fix, adapted to type-check:

- Collect `(component, text)` pairs instead of only the components. With
  `Component.text` annotated as `str | None`, mypy only narrows `c.text` to
  `str` inside the comprehension that filters out `None`, so using
  `c.text` later failed with:

  ```
  lib/bx/align/core.py:225: error: Unsupported right operand type for in
  lib/bx/align/core.py:229: error: Value of type "str | None" is not indexable
  lib/bx/align/core.py:233: error: Value of type "str | None" is not indexable
  ```

  Storing the text in the pair keeps it typed as `str`, without needing
  `cast()` or `# type: ignore`.
- Deduplicate the components by `id()` in a single dict comprehension and
  keep only its values as a list, since the keys are not needed afterwards.

Checked against the implementation on upstream/main with 3000 random
alignments, including empty components at the start, middle and end and
components repeated in `components`: the results are identical.

For 80,000 columns of which 90% are all-gap, `remove_all_gap_columns()`
now takes about 0.02 s instead of 3 s. With 100 components, its peak extra
memory use (measured with `tracemalloc`) drops from 61 MiB to 1.1 MiB,
since it no longer converts each component's text into a list of
characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nsoranzo

Copy link
Copy Markdown
Collaborator

Thanks a lot for the contribution! I've just added type annotations for this code to the main branch to better understand this old code, and pushed here a merge commit, hope that's OK and is still correct?

Comment thread lib/bx/align/maf_tests.py
complex_maf_gap.components.pop()
# Return the empty row to its fixture position for the equality check.
complex_maf_gap.components.append(complex_maf_gap.components.pop(empty_index))
assert complex_maf_gap == complex_maf

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert complex_maf_gap == complex_maf
assert complex_maf_gap == complex_maf
assert complex_maf_gap.text_size == complex_maf.text_size

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.

2 participants