Report duplicated library sequences actionably, and record unmapped pairs - #30
Conversation
…airs A library row whose guide sequence duplicates an earlier row is dropped on load, and the only feedback was a bare count: "There are N sgRNAs with duplicated sequences." It named no file and no IDs, and fired at WARNING level even when N was zero. The consequence surfaced much later and elsewhere, as "sgRNA ID X not in the first library" against the --pg-pair-only file -- a true statement with no stated cause and no remedy. Issue #15 took four rounds to get from that warning to the one-line fix, on a dual-guide library indexed by construct rather than by distinct guide, which hits this on essentially every row. Report the drop where it happens: name the library file, the dropped IDs, and the representative ID that now carries their reads. Say nothing when nothing was dropped. Carry a {dropped_id: representative_id} map out of the loader so the --pg-pair-only check can tell the common cause from a genuine mismatch, and name the ID to use instead. Also stop discarding the pair-level unmapped records. mageckcount_printpgdict already accepts an unmapped file handle but was called with None, so a pair excluded from pg_count.txt -- because its second guide matched no entry in --list-seq-2, or because the combination is absent from --pg-pair-only -- left no trace, and a filtered pair was indistinguishable from one never sequenced. Write them to <prefix>.pg_unmapped.txt under the existing --unmapped-to-file flag. Enabling that writer exposed a latent crash it had been masking. In --pairguide secondpair mode the second-read window is recorded even when the first read matched nothing, under a None key; labelling such a row raises TypeError on None + '_' + seq. Those pairs identify no first guide and are already reported in the read-level unmapped.txt, so skip them. Tested end to end: the fixture is a two-construct dual-guide library with one construct allowed by the pair file, covering the allowed-pair filter, the new unmapped file, and unmatched first reads. Refs #27 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Improves paired-guide diagnostics and unmapped-pair reporting.
Changes:
- Reports duplicate library sequences with dropped and representative IDs.
- Adds actionable pair-file warnings.
- Writes paired-guide unmapped output with regression tests.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
mageck2/mageckCount.py |
Implements diagnostics and pair-level unmapped output. |
tests/test_smoke.py |
Adds duplicate and paired-guide tests. |
CHANGELOG.md |
Documents the new behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if ounmappedfile != None: | ||
| print('sgRNA1_sgRNA2'+sep+'Gene1_Gene2'+sep+sep.join(slabel),file=ounmappedfile) |
There was a problem hiding this comment.
Correct, and this was a real defect. Confirmed at mageckCount.py:399-400: the unmatched-second-guide branch wrote seq1_seq2 in column 1 and id1_seq2 in column 2, under a header declaring sgRNA1_sgRNA2 / Gene1_Gene2. The pair-filter branch was serialized correctly, which is exactly why the tests missed it — they only covered that path.
Fixed in 954af82. Such a pair has no second ID and no second gene, so the row now reports the first guide by ID and gene, keeps the unmatched second sequence as its only available identifier, and marks the absent gene as None (the marker mageckcount_printdict already uses for an unknown gene):
sg1_CCCCCCCCCCCCCCCCCCCC GENE1_None 2
Added test_pg_unmapped_rows_match_their_header, which asserts both cases in one file — the pair-filter row (sg1_sg2b / GENE1_GENE3) and the unmatched-R2 row above — so the two serializations cannot drift apart again.
One thing I considered and did not do: adding a reason column to distinguish "second guide unmatched" from "excluded by --pg-pair-only". It would be more useful than inferring the cause from whether a field looks like a sequence, but it changes the file's shape away from pg_count.txt. Deferred rather than rejected.
The pair-level unmapped file gained a header declaring "sgRNA1_sgRNA2 / Gene1_Gene2", but one of the two cases it exists to expose did not match it. A pair whose second guide matched no entry in --list-seq-2 was written as seq1_seq2 in column 1 and id1_seq2 in column 2, so anything parsing the file reads a 20-mer where an sgRNA ID belongs and an ID where a gene belongs. The pair-filter case, which the tests covered, was serialized correctly, which is why this was missed. Such a pair has no second ID and no second gene. Report the first guide by ID and gene, keep the unmatched second sequence as its only available identifier, and mark the absent gene as None -- the same marker printdict already uses for an unknown gene. Found in review of PR #30. Refs #27 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
issues related to #27.
What changed
1. The duplicate-sequence report says what happened.
mageckcount_checklistsdrops a library row whose sequence is already used by an earlier row. It now names the library file, the dropped IDs, and the representative ID that carries their reads:2. It is silent when nothing was dropped. The old message fired at
WARNINGon every clean run announcingThere are 0 sgRNAs with duplicated sequences, which trains users to ignore it.3. The
--pg-pair-onlywarning names the cause and the remedy. The loader now returns a{dropped_id: representative_id}map, so the pair-file check can distinguish a dropped duplicate from a genuine mismatch:Genuinely absent IDs keep the original wording.
4. Pair-level unmapped output.
mageckcount_printpgdictalready accepted an unmapped file handle but was called withNone, so a pair excluded frompg_count.txtleft no trace and a filtered pair was indistinguishable from one that was never sequenced. Those rows now go to<prefix>.pg_unmapped.txtunder the existing--unmapped-to-fileflag.One deviation from the issue: this writes a separate file rather than reusing
<prefix>.unmapped.txt. The read-level file holds bare sequences and the pair-level one holdsid1_id2labels with aGene1_Gene2column; interleaving two row shapes in one file would be worse than a second file. The pair-level file carries a header, matchingpg_count.txt(the read-level file has none, unchanged).5. A latent crash that item 4 exposed. In
--pairguide secondpairmode the second-read window is recorded even when the first read matched nothing, under aNonekey (mageckCountIO.py:504-509). Labelling such a row raisesTypeErroronNone + '_' + seq. It was invisible only because the file handle wasNone. Those pairs identify no first guide and are already reported in the read-levelunmapped.txt, so they are skipped.Tests
Six new tests, each written before the change it covers and watched fail first:
--pg-pair-onlywarning explains the duplicate drop and names the replacementpg_count.txtcontentspg_unmapped.txtNonerow, and still reports those reads at the read levelThe end-to-end fixture is the first test to exercise
--pairguidefrom FASTQ through topg_count.txt; previously only the pair-file parser was covered. It is a partial down payment on #29, which should extend it to--reverse-complement-2,--pairguide auto, and--pg-min-read.Full suite: 25 passed (19 before this branch).