Skip to content

merge_criteria

Predicate functions consumed by FeatureDB.merge. Each has the signature (acc: Feature, cur: Feature, components: list[Feature]) -> bool.

Merge predicates — pure functions consumed by FeatureDB.merge.

Signature: (acc: Feature, cur: Feature, components: list[Feature]) -> bool. acc is the running accumulator; cur is the candidate to fold in; components is the list of already-folded features. All callables return True if the candidate should be merged into the accumulator.

Mirrors the legacy gffutils.merge_criteria module.

seqid

seqid(acc, cur, components)

Same sequence. Almost always wanted -- omitting it merges features on different chromosomes into one.

Source code in python/gffbase/merge_criteria.py
def seqid(acc, cur, components):
    """Same sequence. Almost always wanted -- omitting it merges features on
    different chromosomes into one."""
    return acc.seqid == cur.seqid

strand

strand(acc, cur, components)

Same orientation. Leave this out to merge regardless of strand.

Source code in python/gffbase/merge_criteria.py
def strand(acc, cur, components):
    """Same orientation. Leave this out to merge regardless of strand."""
    return acc.strand == cur.strand

feature_type

feature_type(acc, cur, components)

Same featuretype, so exons do not merge with CDSs.

Source code in python/gffbase/merge_criteria.py
def feature_type(acc, cur, components):
    """Same featuretype, so exons do not merge with CDSs."""
    return acc.featuretype == cur.featuretype

exact_coordinates_only

exact_coordinates_only(acc, cur, components)

Identical span. Merges duplicates, nothing else.

Source code in python/gffbase/merge_criteria.py
def exact_coordinates_only(acc, cur, components):
    """Identical span. Merges duplicates, nothing else."""
    return acc.start == cur.start and acc.end == cur.end

overlap_end_inclusive

overlap_end_inclusive(acc, cur, components)

cur starts within acc, or immediately after it.

"Immediately after" is the + 1: two features that abut with no gap are adjacent, not overlapping, and merging them is normally what a caller wants when collapsing exon runs.

Source code in python/gffbase/merge_criteria.py
def overlap_end_inclusive(acc, cur, components):
    """`cur` starts within `acc`, or immediately after it.

    "Immediately after" is the `+ 1`: two features that abut with no gap are
    adjacent, not overlapping, and merging them is normally what a caller
    wants when collapsing exon runs.
    """
    return acc.start <= cur.start <= acc.end + 1

overlap_start_inclusive

overlap_start_inclusive(acc, cur, components)

cur ends within acc, or immediately before it.

Source code in python/gffbase/merge_criteria.py
def overlap_start_inclusive(acc, cur, components):
    """`cur` ends within `acc`, or immediately before it."""
    return acc.start <= cur.end + 1 <= acc.end + 1

overlap_any_inclusive

overlap_any_inclusive(acc, cur, components)

Either end qualifies.

Source code in python/gffbase/merge_criteria.py
def overlap_any_inclusive(acc, cur, components):
    """Either end qualifies."""
    return overlap_end_inclusive(acc, cur, components) or overlap_start_inclusive(
        acc, cur, components
    )

overlap_end_threshold

overlap_end_threshold(threshold: int)

cur starts within the accumulator, allowing a gap of threshold.

Changed in 0.2.0. This and the two factories below are RANGE tests, not distance tests. They used to compute abs(acc.end - cur.start) <= threshold, which reads naturally but answers a different question: it asks how far apart two boundaries are, and so rejects a feature lying entirely inside the accumulator -- the most unambiguous overlap there is.

Concretely, with acc = (1, 100), cur = (50, 200), threshold = 5, the old form gave abs(100 - 50) = 50 <= 5 -> False, and these two plainly overlapping features did not merge. This form gives 1 <= 50 <= 105 -> True.

If you call merge or merge_all with one of these, the set of features that merge has changed. Nothing else in the merge machinery did.

Source code in python/gffbase/merge_criteria.py
def overlap_end_threshold(threshold: int):
    """`cur` starts within the accumulator, allowing a gap of `threshold`.

    **Changed in 0.2.0.** This and the two factories below are RANGE tests,
    not distance tests. They used to compute
    `abs(acc.end - cur.start) <= threshold`, which reads naturally but answers
    a different question: it asks how far apart two boundaries are, and so
    *rejects a feature lying entirely inside the accumulator* -- the most
    unambiguous overlap there is.

    Concretely, with `acc = (1, 100)`, `cur = (50, 200)`, `threshold = 5`, the
    old form gave `abs(100 - 50) = 50 <= 5` -> False, and these two plainly
    overlapping features did not merge. This form gives `1 <= 50 <= 105` ->
    True.

    If you call `merge` or `merge_all` with one of these, the set of features
    that merge has changed. Nothing else in the merge machinery did.
    """

    def predicate(acc, cur, components):
        return acc.start <= cur.start <= acc.end + threshold

    return predicate

overlap_start_threshold

overlap_start_threshold(threshold: int)

cur ends within the accumulator, allowing a gap of threshold.

Source code in python/gffbase/merge_criteria.py
def overlap_start_threshold(threshold: int):
    """`cur` ends within the accumulator, allowing a gap of `threshold`."""

    def predicate(acc, cur, components):
        return acc.start - threshold <= cur.end + 1 <= acc.end + 1

    return predicate

overlap_any_threshold

overlap_any_threshold(threshold: int)

Either end qualifies.

Source code in python/gffbase/merge_criteria.py
def overlap_any_threshold(threshold: int):
    """Either end qualifies."""
    end_thr = overlap_end_threshold(threshold)
    start_thr = overlap_start_threshold(threshold)

    def predicate(acc, cur, components):
        return end_thr(acc, cur, components) or start_thr(acc, cur, components)

    return predicate