Skip to content

Trimming support in gaf overlap filter - #2008

Merged
glennhickey merged 5 commits into
masterfrom
overlap-filter-work
Sep 15, 2026
Merged

glennhickey merged 5 commits into
masterfrom
overlap-filter-work

Conversation

@glennhickey

Copy link
Copy Markdown
Collaborator

Note: this is off-by-default. But am merging it now as I've re-invented flavours of it several times while trying to clean up complex regions, and I think this is the most promising incarnation.

The root issue in many cases is poor inversion handling in the base sv graph (tackled in #2006), but this work should provide a complimentary angle. Needs full-genome testing before activiating by default.

Basically instead of removing "dominated" mappings in presence of query overlaps, it cuts ends (plus a trim factor).

glennhickey and others added 5 commits September 12, 2026 18:43
…records

GAFOverlapFilterTrim, off by default, passes gaffilter -t so a record that
loses a query overlap gives up only the contested span rather than the whole
record.  Whole-record deletion also destroys the uncontested flanks, which on
HPRC v2.1 chr15 at 464 haplotypes is about half of the 1.373 Gb it removes.

The trim takes its own GAFOverlapFilterTrimMinLengthRatio, defaulting to 0,
rather than inheriting GAFOverlapFilterMinLengthRatio.  That guard exists
because deleting a record is expensive and a trim is not, so with trimming on
it only leaves small conflicts between long contigs unadjudicated: measured on
a 12-sample chr15 graph, keeping it at 0.25 while trimming doubly places
409,584 bp on one segmental-duplication pair that 0 does not.  Flipping the
one attribute named Trim therefore selects the configuration that was
measured, not a worse neighbour of it.

GAFOverlapFilterTrimRescueWeak (gaffilter -R) is off and should stay off
absent evidence this run cannot provide.  A cut can leave an unaligned hole
with alignment on both sides, and closing it means handing the span to one
claimant -- but no such choice can meet the bar GAFOverlapFilterRatio sets,
because a hole only forms where every record spanning it lost to a record it
does not dominate.  On the same graph, declining costs 7.97 Mb of recovered
path but avoids 25,299 nodes and 121,960 bp of new graph sequence: the
recovery that needs no decision lands at 1,023 bp per new node and makes the
graph 1,267 bp shorter, while the guessed part lands at 315 bp per new node
and accounts for essentially all of the growth.

Measured end to end, 12-sample chr15, against the current default:
+10,103,364 bp of haplotype path, +20 path fragments, graph 1,267 bp shorter,
no haplotype losing sequence and the reference untouched.

The -l file gaffilter needs is written by gaf2unstable in full before it emits
its first GAF line, and gaffilter reads all of its input before opening it, so
there is no race in the pipe -- but only in that order, which is why -l is
passed the same path gaf2unstable -o writes.

NOTE: build-tools/downloadPangenomeTools still pins a cactus-gfa-tools commit
without -t.  With the attribute off nothing changes, and turning it on against
that pin fails loudly rather than silently, but the pin has to be bumped to the
gaffilter commit once it is pushed, in a follow-up, before this is usable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCSd5hqBNbBk11pcbC53Wx
Passes gaffilter -e.  The bases abutting an overlap are the least certain part
of the alignment, so with trimming on the cut extends a little past the
contested span on each side.

5000 is half the cactus-graphmap-join clip threshold, and that is the point:
where only the losing record pulls back the gap it leaves is exactly this size,
so cactus carries it instead of clipping it.  At 10000 it would sit on the
boundary.

Cost measured on HPRC v2.1 at 464 haplotypes, as a fraction of the block length
the trim keeps: chr15 0.8%, chr9 1.7%, chr21 5.2%, chr20 9.6% -- largest where
the absolute amount recovered is smallest.  Holes barely move (chr15 1132 ->
1138, chr9 1008 -> 1012).

Still gated behind GAFOverlapFilterTrim, which remains off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCSd5hqBNbBk11pcbC53Wx
…e records

Passes gaffilter -Q.  A record that loses a query overlap and is itself poorly
placed keeps the behaviour it has today, deleted whole, rather than
contributing its uncontested flanks to the graph: losing an overlap was always
evidence about the record and not only about the overlapping part.

20 sits above minMAPQ=5, so it is strictly more conservative than filter_paf,
and well below the mapq-60 mass that carries the benefit.  Block length kept,
at 464 haplotypes: chr15 -0.04%, chr9 -1.68%, chr20 -0.97%, chr21 -19.17%.

Still gated behind GAFOverlapFilterTrim, which remains off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCSd5hqBNbBk11pcbC53Wx
GAFOverlapFilterTrim needs a gaffilter built with -t, and the previous pin
(b37287b1) predates it: turning the attribute on against that build failed
every minigraph_map_one job with "invalid option -- 't'".  The attribute is off
by default, so nothing changed for anyone, but the config documented a
capability the shipped binary did not have.

a32588c is on cactus-gfa-tools overlap-work, not yet merged to main.  It stays
valid as long as that branch is not squashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCSd5hqBNbBk11pcbC53Wx
Of the six the trim originally added, three could not earn their place.

GAFOverlapFilterTrimMinGap and GAFOverlapFilterTrimRescueWeak are a dead pair.
--close-holes is off and cannot be justified -- a hole only forms where every
record spanning it lost to one it does not dominate, so no claimant to it can
meet the bar GAFOverlapFilterRatio sets -- and with it off, -g provably does not
change the output: identical md5 over -g 0, 1000, 10000 and 1000000 on the
12-sample chr15 GAF.  Both remain available as gaffilter flags for a hand-run.

GAFOverlapFilterTrimMinLengthRatio was a knob with one defensible setting.  The
guard it mirrors exists because deleting a record is expensive, which a trim is
not; trimming at 0.25 doubly places 409,584 bp on one segmental-duplication pair
that 0 does not, and nothing favours a middle value.  It is now just 0, with the
reason in a comment, so the attribute named Trim still selects the configuration
that was measured.

Left: GAFOverlapFilterTrim, GAFOverlapFilterTrimEdge, GAFOverlapFilterTrimMinMAPQ,
each with a measured effect.  Output is unchanged -- the command this now builds
is byte-identical in result to the one the measured arms ran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCSd5hqBNbBk11pcbC53Wx
@glennhickey
glennhickey merged commit 0932c1b into master Sep 15, 2026
1 check passed
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.

1 participant