Repository navigation
Conversation
Hoeze
force-pushed
the
feat/interval-resize-rule
branch
2 times, most recently
from
October 16, 2026 19:49
57cedc0 to
a418804
Compare
Hoeze
force-pushed
the
feat/interval-resize-rule
branch
from
October 16, 2026 23:51
a418804 to
746d11e
Compare
Hoeze
force-pushed
the
feat/interval-resize-rule
branch
2 times, most recently
from
October 17, 2026 16:22
05a3a49 to
ce18d4f
Compare
… rule All interval resizing now follows Interval.resize: a center in the middle of a base is rounded towards the 3' end, up on "+" and "." intervals and down on "-" intervals. - resize_interval with anchor "center", and so ResizeInterval, returns Interval.resize. Anchors "start" and "end" keep the genomic start and end. - Interval.center(use_strand=False) returns the center of the unstranded interval. - Interval.slop(use_strand=True) swaps upstream and downstream on "-" intervals. resize_interval passes use_strand=False. - Interval.trim and Interval.truncate always return a copy. BREAKING CHANGE: Interval.center(use_strand=False) rounds up instead of down, so it grows by 1 for intervals with an odd start + end. resize_interval with anchor "center" and ResizeInterval give the result of Interval.resize: on "+" and "." intervals, the result moves by one base if the interval width and the new width differ in parity. Interval.slop moves end by upstream and start by downstream on "-" intervals; pass use_strand=False for the old behavior. Interval.trim and Interval.truncate return a new interval also if nothing changes.
ValueError is a subclass of Exception, so code that catches Exception still works.
Interval validated nothing and took name and score positionally, so
Interval("chr1", 0, 10, "-") set name="-" and kept the strand ".".
strand="x" and start > end passed, and len() raised later. Its fields
and attrs could change after it served as a dict key.
Interval is now a frozen dataclass:
- strand, name, score and attrs are keyword-only.
- The constructor raises TypeError if chrom, strand or name is not a
str, if start or end is not an int, if score is not a number, or if
attrs is not a mapping. It raises ValueError if start > end, if
strand is not "+", "-" or ".", or if score is NaN. The methods build
their results with the constructor, so they raise the same errors.
- attrs is a read-only copy of the given mapping.
- Two intervals are equal if all their fields are equal. The hash
leaves out attrs, whose values may be unhashable.
- width is a property. The aliases chr and stop, __len__ and copy are
gone.
- from_str parses negative coordinates and chromosome names with
colons, and raises ValueError for another format.
IntervalSeqBuilder of VariantSeqExtractor marks the reference ranges
with (start, end) tuples instead of Intervals, because overlapping
variants give ranges whose start is greater than their end.
VariantSeqExtractor also turns a float chrom_len into an int end,
because Interval rejects a float end.
BREAKING CHANGE: Interval is immutable, takes strand, name, score and
attrs only as keywords, and raises TypeError or ValueError for invalid
fields. Use interval.width instead of interval.width() and
len(interval), chrom instead of chr, and end instead of stop.
Interval.copy is gone. An Interval equals only an Interval with equal
fields, including name, score and attrs.
resize_interval duplicated Interval.resize for "center", and its anchors
followed two strand rules: on a "-" interval 10-20 resized to width 4,
"start" gave 10-14 (genomic) and "center" gave 13-17 (stranded).
Interval("chr1", 10, 20).resize(-2) returned 16-14, and ResizeInterval
checked its anchor only when called.
- Interval.resize(width, *, anchor="center", use_strand=True) keeps the
start, center or end of the interval. "start" and "end" follow the
strand like "center": on a "-" interval, "start" keeps the genomic
end. use_strand=False resizes as an unstranded interval.
resize raises TypeError if width is not an int, and ValueError if
width is negative or anchor is not "start", "center" or "end".
- resize_interval is gone.
- ResizeInterval(width, *, anchor="center", use_strand=True) checks its
arguments in __init__ and calls Interval.resize.
BREAKING CHANGE: resize_interval is removed. Use
interval.resize(width, anchor=anchor, use_strand=False) for the anchors
"start" and "end", and interval.resize(width) for "center".
ResizeInterval takes anchor only as a keyword, and its anchors "start"
and "end" follow the strand unless use_strand=False. Interval.resize
takes use_strand only as a keyword. It raises ValueError for a negative
width and TypeError for width=None, instead of returning an invalid
interval or a copy.
Interval.trim(i, j) had the name of functional.trim(seq, length, anchor), but sliced the interval by relative positions. It checked j > i with assert, and extended the interval past its end: trim(0, 20) on an interval of width 10 gave width 20. Interval.slice(i, j, *, use_strand=True) raises TypeError if i or j is not an int, and ValueError unless 0 <= i < j <= width. BREAKING CHANGE: Interval.trim(i, j) is renamed to Interval.slice(i, j), and use_strand is keyword-only. slice raises ValueError instead of AssertionError if j <= i, and also if i < 0 or j > width.
Interval.center, shift and slop took use_strand positionally, unlike resize and slice. The name x of the shift amount hid that it is an offset, and slop(2, 5) did not say which side moves. - center(*, use_strand=True) - shift(offset, *, use_strand=True) - slop(*, upstream=0, downstream=0, use_strand=True) shift and slop raise TypeError if an amount is not an int. BREAKING CHANGE: use_strand of Interval.center and Interval.shift is keyword-only, and the parameter x of shift is renamed to offset. Interval.slop takes upstream, downstream and use_strand only as keywords.
pad and trim with anchor="center" rounded the other way than
Interval.resize: trimming 10 letters to 7 kept 2-9, but
Interval("chr1", 0, 10).resize(7) gives 1-8. trim(seq, 0, anchor="end")
returned seq, because seq[-0:] is the whole sequence. The functions
checked their arguments with assert, and pad(..., value="") raised
ZeroDivisionError. pad took (value, anchor) and fixed_len
(anchor, value) positionally, and the support for lists was a kipoi
leftover.
- pad(seq, length, *, anchor="center", value="N"),
trim(seq, length, *, anchor="center") and
fixed_len(seq, length, *, anchor="center", value="N") take a str.
They keep the letters that Interval.resize keeps of an interval from
0 to len(seq).
- They raise TypeError if seq or value is not a str or if length is
not an int. They raise ValueError for a length out of range, an
unknown anchor or an empty value.
BREAKING CHANGE: pad, trim and fixed_len take only strings, and anchor
and value only as keywords. With anchor="center", the result can move
by one letter. They raise TypeError or ValueError instead of
AssertionError, and trim(seq, 0, anchor="end") returns "".
name and score are the BED6 fields of pybedtools. With attrs, an
Interval had three places for metadata, and only the matchers filled
name and score. A NaN score also needed its own check, because it made
the interval unequal to itself.
Interval keeps chrom, start, end, strand and attrs. The repr leaves out
name and score.
BREAKING CHANGE: Interval.name and Interval.score are removed, and
Interval no longer takes name or score. Keep them in attrs, e.g.
Interval("chr1", 0, 10, attrs={"name": "a", "score": 1.5}).
Interval is a frozen dataclass now, so each of these is one expression
on its fields.
BREAKING CHANGE: Interval.unstrand, swap_strand, neg_strand and
is_valid are removed:
- interval.unstrand() becomes dataclasses.replace(interval, strand=".").
- interval.neg_strand becomes interval.strand == "-".
- interval.swap_strand() becomes replace(interval, strand=s), with s
from {"+": "-", "-": "+", ".": "."}[interval.strand].
- interval.is_valid(chrom_len) becomes
0 <= interval.start and interval.end <= chrom_len.
The default chrom_len=math.inf and float lengths forced an int() cast on the end. A chromosome length is an int, so truncate now takes one. BREAKING CHANGE: Interval.truncate requires chrom_len, and raises a TypeError if it is not an int. interval.truncate() without chrom_len becomes replace(interval, start=max(interval.start, 0)).
Five Interval methods repeated `use_strand and self.strand == "-"`. The helper _is_reverse now holds this rule, so "." counts as "+" in one place.
Interval wrote its own __repr__, which 0.1 needed for name, score and attrs. The repr that dataclass generates names each field, and eval of it gives an equal Interval too. A test checks the repr.
Tokenizer copied the int rule of _check_int: an int or a numpy integer, but not a bool. It now calls _check_int, so its TypeError reads like the other int checks, e.g. "the token of 'N' in token_map must be an int, not float".
Hoeze
force-pushed
the
feat/interval-resize-rule
branch
from
October 17, 2026 17:19
ce18d4f to
d766003
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
tl;dr:
Intervalis an immutable, validated value object with fewer members, and all interval resizing follows one strand rule.Interval.resize(width, *, anchor, use_strand)replacesresize_interval, andpad,trimandfixed_lenround like it.Intervalis a frozen dataclass, and its constructor checks the fields once.startorendthat is not an int raises a TypeError.start > endand astrandother than "+", "-" and "." raise a ValueError.strandis keyword-only. Before,Interval("chr1", 0, 10, "-")set the name to "-" and kept the strand ".".nameandscoreare gone. Keep them in the columns of a polars frame instead.widthis a property. The aliaseschrandstop,__len__andcopyare gone.from_strparses negative coordinates and chromosome names with colons.unstrand,swap_strand,neg_strandandis_validare gone. Each is one expression on the fields, listed in MIGRATING.md.truncate(chrom_len)requireschrom_len, and raises a TypeError if it is not an int. The defaultmath.infforced an int cast on the end.A center in the middle of a base is rounded towards the 3' end: up on "+" and "." intervals, down on "-" intervals. This is what
Interval.resizealready did.Interval.resize(width, *, anchor="center", use_strand=True)keeps the start, center or end. "start" and "end" follow the strand: on a "-" interval, "start" keeps the genomic end. A negativewidthraises a ValueError.resize_intervalis gone.Interval.center(use_strand=False)returns the center of the unstranded interval.Interval.slop(use_strand=True)swapsupstreamanddownstreamon "-" intervals.Interval.trim(i, j)becomesInterval.slice(i, j), becausetrimofkipoiseq2.transformstrims a sequence to a length. It raises a ValueError unless0 <= i < j <= width, instead of an AssertionError or a longer interval.shift(x)becomesshift(offset).pad,trimandfixed_lenkeep the letters thatInterval.resizekeeps of an interval from 0 tolen(seq). They take only a str,anchorandvalueonly as keywords, and raise a TypeError or ValueError instead of an AssertionError.trim(seq, 0, anchor="end")returns "" instead ofseq.Verified against the previous implementation:
Interval.resize,shift,slopandslice(trimbefore) give the same coordinates on 396 intervals of all strands, with many widths and offsets.center(use_strand=False)moves up by 1 exactly whenstart + endis odd.pad,trimandfixed_lenwithanchor="center"change in 56 of 225 combinations of lengths 1 to 15. With "start" and "end", onlytrim(seq, 0, anchor="end")changes.BREAKING CHANGE:
Intervalis immutable and takesstrandonly as a keyword. It raises a TypeError or ValueError for invalid fields, and equals only intervals with equal fields.Interval.nameandInterval.scoreare removed: keep them in the columns of a polars frame.Interval.unstrand,swap_strand,neg_strandandis_validare removed.Interval.truncaterequires an intchrom_len. Useinterval.widthinstead ofwidth()andlen(),chrominstead ofchr,endinstead ofstop, anddataclasses.replaceinstead ofcopy.resize_intervalis removed: useinterval.resize(width, anchor=anchor, use_strand=False)for "start" and "end", andinterval.resize(width)for "center", whose result moves by one base on "+" and "." intervals if the widths differ in parity.Interval.trimis renamed toInterval.slice. The options of theIntervalmethods are keyword-only.Interval.center(use_strand=False)rounds up instead of down.Interval.slopmovesendbyupstreamon "-" intervals.pad,trimandfixed_lentake only strings and takeanchorandvalueonly as keywords, and withanchor="center"their result can move by one letter.feat(dataclasses)!: validate the fields of Interval
feat(dataclasses)!: resize at an anchor and drop resize_interval
feat(dataclasses)!: rename Interval.trim to slice and check its bounds
feat(dataclasses)!: take the options of Interval methods as keywords
feat(transforms)!: pad and trim with the center rule of Interval.resize
feat(dataclasses)!: drop Interval.name and score
feat(dataclasses)!: drop unstrand, swap_strand, neg_strand and is_valid
feat(dataclasses)!: require an int chrom_len in Interval.truncate