Disjoint Interval Sequences
Motivation
When working with transcripts, it is often necessary to operate on the exonic
sequence with introns (or other intervening regions) removed. The remaining exons
form a disjoint set of genomic Interval objects.
Operations such as indexing into the spliced sequence or defining sub-ranges
across exon boundaries require a coordinate system that accounts for the gaps
between these intervals.
The DisjointIntervalSequence (DIS) class provides
this coordinate system. While it builds on Interval, a DIS
is conceptually distinct in several ways:
Spliced coordinate space. Positions in a DIS are offsets into the concatenated exonic (or other) sequence, not genomic coordinates.
5’→3’ index direction. DIS indices always increase from 5’ to 3’ with respect to the transcript. Index 0 corresponds to the transcript’s 5’ end regardless of genomic strand. This contrasts with
Interval, wherestart < endalways holds in genomic coordinates, so on the-strandstartis the 3’ end.Same-strand / opposite-strand semantics. Because a DIS models spliced RNA rather than raw DNA, the concept of
+/-strand is replaced byon_coordinate_strand(same strand as the transcript) versus opposite strand. The underlying genomic strand is accessible viacoord_strand, but segments within the DIS are described relative to the coordinate space rather than in absolute genomic terms.
Overview
A DisjointIntervalSequence (DIS) represents
a flattened coordinate system over a sequence of disjoint genomic intervals.
For example, the exons of a transcript form a disjoint set of genomic intervals that,
when concatenated, represent the spliced RNA sequence.
A DIS has two aspects:
A coordinate space: the underlying genomic
Intervalobjects (e.g. exons) that define the flattened index system. These intervals are sorted 5’→3’ and must not overlap. Adjacent (touching) intervals are merged into a single interval — for example,[100, 200)and[200, 300)become[100, 300).A segment: a sub-range within that coordinate space, defined by a start and end index, where start <= end.
The following examples illustrate how the coordinate space and segment interact, using both diagrams and code.
Consider a transcript on the + strand with the following genomic layout:
Genomic Coordinates: 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
DNA Sequence: | A T G C C G C A T G C C G C |
| |<------->| |<------->| |<--->| |<----------->| |<--->| |
5' Exon1 Intron1 Exon2 Intron2 Exon3 3'
Extracting only the exons yields the following disjoint intervals:
Genomic Coordinates: 153 154 155 159 160 165 166 167
DNA Sequence: | A T G C A G C |
| |<------->| |<--->| |<--->| |
5' Exon1 Exon2 Exon3 3'
These exon intervals can be represented as Interval objects
>>> from genome_kit import Interval
>>> from genome_kit.diseq import DisjointIntervalSequence
>>> exon1 = Interval("chr1", "+", 153, 156, "hg38")
>>> exon2 = Interval("chr1", "+", 159, 161, "hg38")
>>> exon3 = Interval("chr1", "+", 165, 167, "hg38")
To define a segment spanning the full exonic sequence (from the start of Exon1 to the end of Exon3), the exon intervals are first converted into a DIS coordinate space:
DIS Coordinates: 0 1 2 3 4 5 6 7
DNA Sequence: | A T G C A G C |
| |<------->| |<--->| |<--->| |
5' Exon1 Exon2 Exon3 3'
The default segment spans the entire coordinate space:
DIS Coordinates: 0 1 2 3 4 5 6 7
DNA Sequence: A T G C A G C
|<--------------------->|
end5 Segment end3
Start Index: 0
End Index: 7
The segment spans the full length of the coordinate space, with a start index of 0 and an end index of 7
>>> dis = DisjointIntervalSequence.from_intervals(
... [exon1, exon2, exon3], coord_name="tx_example"
... )
>>> dis.start
0
>>> dis.end
7
>>> dis.on_coordinate_strand
True
Note
The DIS segment follows the convention of
Interval where ranges are half-open
(the end index is exclusive).
A DIS can also represent a segment on the strand opposite the coordinate space. This is useful for modeling the complementary sequence or a binding partner.
Starting from the coordinate space defined above:
DIS Coordinates: 0 1 2 3 4 5 6 7
DNA Sequence: | A T G C A G C |
| |<----->| |<->| |<->| |
5' Exon1 Exon2 Exon3 3'
The opposite strand shares the same DIS coordinate indices:
5' Positive strand 3'
DIS Coordinates: | 0 1 2 3 4 5 6 7 |
DNA Sequence (+): | A T G C A G C |
-----------------------------------------------------
DNA Sequence (-): | T A C G T C G |
DIS Coordinates: | 0 1 2 3 4 5 6 7 |
3' Negative Strand 5'
The DIS coordinate indices are identical on both strands. To obtain the complement
of a given segment, the same start and end indices apply; only the
on_coordinate_strand flag changes. The following shows the full-length segment
on the opposite strand:
5' Coordinate Strand 3'
DIS Coordinates: | 0 1 2 3 4 5 6 7 |
DNA Sequence (+): | A T G C A G C |
-----------------------------------------------------
DNA Sequence (-): T A C G T C G
DIS Coordinates: 0 1 2 3 4 5 6 7
Opposite Strand
|<--------------------->|
end3 Segment end5
Start Index: 0
End Index: 7
On Coordinate Strand: False
The on_coordinate_strand flag distinguishes same-strand from opposite-strand
segments, since the start and end indices alone do not encode strand information
>>> dis_opp = DisjointIntervalSequence(
... [exon1, exon2, exon3],
... coord_name="tx_example",
... on_coordinate_strand=False,
... )
>>> dis_opp.on_coordinate_strand
False
>>> dis_opp.end5_index
7
>>> dis_opp.end3_index
0
Note
The preceding examples used + strand coordinate intervals. When the coordinate intervals
lie on the negative strand, the DIS behaves differently from Interval:
in one key aspect. Indices still increase in the 5’→3’ direction of the transcript.
>>> iv = Interval("chr1", "-", 0, 100, genome.refg)
>>> assert iv.end5.start > iv.end3.start
>>> dis = DisjointIntervalSequence.from_intervals([iv])
>>> assert dis.end5.start < dis.end3.start
Consider a transcript on the negative strand:
3' Exon3 Intron2 Exon2 Intron1 Exon1 5'
| |<------->| |<------->| |<--->| |<----------->| |<--->| |
DNA Sequence (-): | G T C A G T C A G T C A G T |
Genomic Coordinates: 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
Negative Strand
Extracting only the exons:
3' Exon3 Exon2 Exon1 5'
| |<------->| |<--->| |<--->| |
DNA Sequence (-): | G T C C A G T |
Genomic Coordinates: 153 154 155 159 160 165 166 167
Negative Strand
Converting these exons into a DIS coordinate space:
DIS Coordinates: 0 1 2 3 4 5 6 7
DNA Sequence: | T G A C C T G |
| |<->| |<->| |<----->| |
5' Exon1 Exon2 Exon3 3'
The sequence appears reversed relative to genomic coordinates because the DIS coordinate space is oriented 5’→3’ with respect to the transcript, regardless of genomic strand. Index 0 always corresponds to the transcript’s 5’ end, and the largest index to the 3’ end
>>> neg_exon1 = Interval("chr1", "-", 165, 167, "hg38")
>>> neg_exon2 = Interval("chr1", "-", 159, 161, "hg38")
>>> neg_exon3 = Interval("chr1", "-", 153, 156, "hg38")
>>> dis_neg = DisjointIntervalSequence.from_intervals(
... [neg_exon1, neg_exon2, neg_exon3],
... coord_name="tx_neg_example",
... )
>>> dis_neg.coord_strand
'-'
>>> dis_neg.coordinate_length
7
A full-length segment on the coordinate strand:
DIS Coordinates: 0 1 2 3 4 5 6 7
DNA Sequence: T G A C C T G
|<--------------------->|
end5 Segment end3
Start Index: 0
End Index: 7
On Coordinate Strand: True
Despite creating the DIS from the negative strand, the full-length segment on the coordinate strand is identical to the + strand example. When working with DIS objects, strand is expressed only as “same strand” or “opposite strand”
>>> dis_neg.start
0
>>> dis_neg.end
7
>>> dis_neg.on_coordinate_strand
True
The same coordinate space with an opposite-strand segment:
DIS Coordinates: 0 1 2 3 4 5 6 7
DNA Sequence (-): T G A C C T G
-----------------------------------------------------
DNA Sequence (+): A C T G G A C
DIS Coordinates: 0 1 2 3 4 5 6 7
|<--------------------->|
end3 Segment end5
Start Index: 0
End Index: 7
On Coordinate Strand: False
>>> dis_neg_opp = DisjointIntervalSequence(
... [neg_exon1, neg_exon2, neg_exon3],
... coord_name="tx_neg_example",
... on_coordinate_strand=False,
... )
>>> dis_neg_opp.on_coordinate_strand
False
>>> dis_neg_opp.end5_index
7
>>> dis_neg_opp.end3_index
0
Construction
From a Transcript
The most common way to create a DIS is from a
Transcript
>>> from genome_kit import Genome
>>> from genome_kit.diseq import DisjointIntervalSequence
>>> genome = Genome("gencode.v29")
>>> transcript = genome.transcripts[100]
>>> dis = DisjointIntervalSequence.from_transcript(transcript)
By default, the coordinate space is built from the transcript’s exons. It’s also possible to specify a region to use CDS or UTR intervals
>>> dis_cds = DisjointIntervalSequence.from_transcript(transcript, region="cds")
>>> dis_utr5 = DisjointIntervalSequence.from_transcript(transcript, region="utr5")
>>> dis_utr3 = DisjointIntervalSequence.from_transcript(transcript, region="utr3")
The coord_name and interval_name default to transcript.id but can
be overridden
>>> dis = DisjointIntervalSequence.from_transcript(
... transcript, coord_name="my_coord", interval_name="my_interval")
From Intervals
A DIS can be constructed from any sequence of
Interval objects
>>> from genome_kit import Interval
>>> exon_intervals = [e.interval for e in transcript.exons]
>>> dis = DisjointIntervalSequence.from_intervals(exon_intervals, coord_name="my_coord")
The intervals must all share the same chromosome, strand, reference
genome, and must not overlap. They are automatically sorted 5’→3’, and
adjacent (touching) intervals are merged into a single interval — for
example, [100, 200) and [200, 300) become [100, 300).
Coordinate Space
The coordinate space is defined by the underlying genomic intervals, which are accessible as a tuple
>>> dis.coordinate_intervals
(Interval("chr1", "+", 100, 200, "hg38"), Interval("chr1", "+", 300, 450, "hg38"))
>>> dis.coordinate_length
250
Metadata about the coordinate space is available through properties
>>> dis.chromosome
'chr1'
>>> dis.coord_strand
'+'
>>> dis.reference_genome
'hg38'
>>> dis.coord_name
'ENST00000...'
Segment Start and End
The segment within the coordinate space is defined by start and end
indices, following the same half-open convention as Interval
(start <= end always)
>>> dis.start
0
>>> dis.end
250
>>> dis.length
250
>>> len(dis)
250
By default, the segment spans the full coordinate space (start=0,
end=coordinate_length). Indices can extend beyond [0, coordinate_length], but
the DNA sequence returned by genome.dna() will be N-padded.
End5 and End3
The end5_index and end3_index properties give the 5’ and 3’ positions
of the segment. These are derived from start and end based on the
segment’s strand.
When on_coordinate_strand is True, end5_index equals start and
end3_index equals end:
Start Index: 1
End Index: 6
DIS Coordinates: 0 1 2 3 4 5 6 7
DNA Sequence: T G A C C T G
|<------------->|
-----------------------------------------------------
DNA Sequence: A C T G G A C
DIS Coordinates: 0 1 2 3 4 5 6 7
Opposite Strand
>>> dis = DisjointIntervalSequence.from_transcript(transcript)
>>> dis.end5_index # same as start when on coordinate strand
1
>>> dis.end3_index # same as end when on coordinate strand
6
When on_coordinate_strand is False, the mapping reverses:
end5_index equals end and end3_index equals start:
Start Index: 3
End Index: 7
DIS Coordinates: 0 1 2 3 4 5 6 7
DNA Sequence: T G A C C T G
-----------------------------------------------------
DNA Sequence: A C T G G A C
DIS Coordinates: 0 1 2 3 4 5 6 7
|<--------->|
Opposite Strand
>>> opp = dis.as_opposite_strand()
>>> opp.end5_index # same as end when off coordinate strand
7
>>> opp.end3_index # same as start when off coordinate strand
3
Boundary Properties
Zero-length DIS objects at the segment and coordinate boundaries are available as properties
>>> dis.end5 # 0-length DIS at the segment's 5' boundary
>>> dis.end3 # 0-length DIS at the segment's 3' boundary
>>> dis.coord_end5 # 0-length DIS at the coordinate space's 5' boundary
>>> dis.coord_end3 # 0-length DIS at the coordinate space's 3' boundary
Strand Methods
A DIS segment can sit on either ‘virtual’ strand independently of the coordinate
intervals. The on_coordinate_strand property indicates whether the
segment is on the same strand as the coordinate intervals:
On Coordinate Strand: True
Start Index: 1
End Index: 6
DIS Coordinates: 0 1 2 3 4 5 6 7
DNA Sequence (+): A T C C G A C
|<------------->|
-----------------------------------------------------
DNA Sequence (-): T A G G C T G
DIS Coordinates: 0 1 2 3 4 5 6 7
Opposite Strand
>>> dis.on_coordinate_strand
True
>>> dis.is_same_strand()
True
>>> dis.is_positive_strand()
True
Strand methods (is_same_strand(),
flip_strand(), etc.) only affect the
segment layer, not the coordinate intervals.
Shifting and Expanding
Both shift() and
expand() return a new DIS with
modified segment indices. The coordinate space is always unchanged.
shift
shift() moves the segment downstream
by amount bases. A negative value shifts upstream. The segment length is preserved.
On the coordinate strand, downstream means increasing indices:
Before shift(1):
DIS Coordinates: 0 1 2 3 4 5 6 7
|<--------->|
end5 end3
After shift(1):
DIS Coordinates: 0 1 2 3 4 5 6 7
|<--------->|
end5 end3
On the opposite strand, “downstream” is the reverse direction in index
space, so shift(1) moves the segment toward lower indices:
Before shift(1) (on_coordinate_strand=False):
DIS Coordinates: 0 1 2 3 4 5 6 7
|<--------->|
end3 end5
After shift(1):
DIS Coordinates: 0 1 2 3 4 5 6 7
|<--------->|
end3 end5
>>> dis.start, dis.end
(30, 150)
>>> shifted = dis.shift(10)
>>> shifted.start, shifted.end
(40, 160)
>>> shifted.coordinate_intervals == dis.coordinate_intervals
True
>>> # Negative values shift upstream
>>> dis.shift(-10).start, dis.shift(-10).end
(20, 140)
>>> # On the opposite strand, downstream reverses in index space
>>> opp = dis.as_opposite_strand()
>>> opp.start, opp.end
(30, 150)
>>> shifted_opp = opp.shift(10)
>>> shifted_opp.start, shifted_opp.end
(20, 140)
Note
shift() can move the segment beyond
the coordinate space bounds (start < 0 or end > coordinate_length).
expand
expand() grows (or shrinks) the segment
toward its 5’ and 3’ ends. When dnstream is omitted the expansion is
symmetric:
Before expand(1):
DIS Coordinates: 0 1 2 3 4 5 6 7
|<--------->|
end5 end3
After expand(1):
DIS Coordinates: 0 1 2 3 4 5 6 7
|<----------------->|
end5 end3
Negative values contract the segment:
Before expand(-1, -1):
DIS Coordinates: 0 1 2 3 4 5 6 7
|<----------------->|
end5 end3
After expand(-1, -1):
DIS Coordinates: 0 1 2 3 4 5 6 7
|<--------->|
end5 end3
>>> dis.start, dis.end
(30, 150)
>>> # Symmetric expansion
>>> dis.expand(5).start, dis.expand(5).end
(25, 155)
>>> # Asymmetric expansion
>>> dis.expand(5, 10).start, dis.expand(5, 10).end
(25, 160)
>>> # Upstream-only expansion
>>> dis.expand(5, 0).start, dis.expand(5, 0).end
(25, 150)
>>> # Contraction with negative values
>>> dis.expand(-10, -20).start, dis.expand(-10, -20).end
(40, 130)
Note
Contracting to exactly zero length is valid, but contracting past
zero raises ValueError.
expand_coord
expand() and
expand_coord() both extend the DIS,
but they do so at different layers.
expand() stretches the segment within
the existing coordinate space; the coord intervals are unchanged.
expand_coord() instead extends the
coordinate space itself by lengthening the outermost coord intervals. Whether the
segment also grows depends on what it currently covers:
If the segment spans the coordinate space exactly (
start == 0andend == coordinate_length), it grows by the same amount so it continues to span the enlarged coordinate space.Otherwise the segment keeps its original size and simply shifts to track the re-indexed coordinate space, so it still points at the same genomic bases (its
lower()projection is unchanged).
This distinction matters when you want flanking genomic context to become part of the indexed coordinate space rather than just a virtual extension, such as when getting the DNA of the DIS segment (off-coordinate bases are N-padded).
For example, when modeling a transcript plus its surrounding promoter and
poly-A regions, a full-span segment grows with the coordinate space:
expand_coord(50) adds 50 genomic bases to each end of the coordinate
space, and those bases are now real, indexable, DNA-backed positions rather
than N-padding
>>> dis.coord_strand
'+'
>>> dis.coordinate_intervals
(Interval("chr1", "+", 11000, 11005, "hg38"),
Interval("chr1", "+", 11010, 11020, "hg38"),
Interval("chr1", "+", 11025, 11030, "hg38"))
>>> dis.coordinate_length
20
>>> dis.start, dis.end # segment spans the coord space exactly
(0, 20)
>>> dis.expand(5).dna() # expanded segment is N-padded
'NNNNNAAGCCGCGGGGGTTGGCGTGNNNNN'
>>> expanded = dis.expand_coord(5)
>>> expanded.coordinate_intervals
(Interval("chr1", "+", 10995, 11005, "hg38"),
Interval("chr1", "+", 11010, 11020, "hg38"),
Interval("chr1", "+", 11025, 11035, "hg38"))
>>> expanded.coordinate_length
30
>>> expanded.start, expanded.end # grew to span the enlarged coord space
(0, 30)
>>> expanded.dna() # new genomic context is now part of the DNA
'GACGCAAGCCGCGGGGGTTGGCGTGTGTTG'
A segment that does not span the coordinate space exactly is left at its original size; only the coordinate space grows, and the segment shifts so its genomic projection is unchanged
>>> partial = DisjointIntervalSequence(
... [Interval("chr1", "+", 1000, 1100, "hg38"),
... Interval("chr1", "+", 1200, 1300, "hg38"),
... Interval("chr1", "+", 1500, 1600, "hg38")],
... start=50, end=250,
... )
>>> partial.lower()
[Interval("chr1", "+", 1050, 1100, "hg38"),
Interval("chr1", "+", 1200, 1300, "hg38"),
Interval("chr1", "+", 1500, 1550, "hg38")]
>>> expanded = partial.expand_coord(50)
>>> expanded.coordinate_length # coord space still grows by 50 + 50
400
>>> expanded.start, expanded.end # same size (200), shifted by upstream (50)
(100, 300)
>>> expanded.lower() # genomic projection unchanged
[Interval("chr1", "+", 1050, 1100, "hg38"),
Interval("chr1", "+", 1200, 1300, "hg38"),
Interval("chr1", "+", 1500, 1550, "hg38")]
Only the outer 5’ edge of the first coord interval and the outer 3’ edge
of the last coord interval are extended; gaps between coord intervals are
preserved. On the negative strand, upstream and dnstream still
refer to the transcript’s 5’ and 3’ ends, so the underlying genomic
adjustments mirror those on the plus strand.
Unlike expand(),
expand_coord() does not accept negative
values — shrinking the coordinate space is not supported.
Positional Comparisons
upstream_of() and
dnstream_of() compare two DIS segments
that share the same coordinate space and the same on_coordinate_strand. Both
methods require strict separation — any overlap returns False.
upstream_of
upstream_of() returns True if
self is strictly 5’ of other with no overlap. Adjacent segments (where
self.end equals other.start) count as upstream:
DIS Coordinates: 0 1 2 3 4 5 6 7 8 9
|<->| |<->|
a b
a.upstream_of(b) is True (no overlap)
DIS Coordinates: 0 1 2 3 4 5 6 7 8 9
|<----->|
a |<----->|
b
a.upstream_of(b) is True (adjacent: a.end == b.start)
DIS Coordinates: 0 1 2 3 4 5 6 7 8 9
|<--------->|
a |<----->|
b
a.upstream_of(b) is False (overlap)
>>> a = DisjointIntervalSequence(coord_ivs, start=10, end=30)
>>> b = DisjointIntervalSequence(coord_ivs, start=50, end=80)
>>> a.upstream_of(b)
True
>>> b.upstream_of(a)
False
>>> # Adjacent segments count as upstream
>>> a2 = DisjointIntervalSequence(coord_ivs, start=10, end=50)
>>> a2.upstream_of(b)
True
Note
Both DIS objects must share the same coordinate_intervals and the
same on_coordinate_strand, otherwise ValueError is raised.
Two zero-length segments at the same position are neither upstream
nor downstream of each other.
dnstream_of
dnstream_of() is the mirror of
upstream_of(): it returns
True if self is strictly 3’ of other with no overlap.
Adjacent segments count as downstream. The same requirements on shared
coordinate space and strand apply
>>> a = DisjointIntervalSequence(coord_ivs, start=50, end=80)
>>> b = DisjointIntervalSequence(coord_ivs, start=10, end=30)
>>> a.dnstream_of(b)
True
>>> b.dnstream_of(a)
False
within
within() returns True if self’s
segment is fully contained within other’s segment. Boundary-inclusive: a segment
is within another if it shares the same start and/or end. A segment
is always within itself. The same requirements on shared coordinate
space and strand apply:
DIS Coordinates: 0 1 2 3 4 5 6 7 8 9
|<->|
a
|<------------->|
b
a.within(b) is True
DIS Coordinates: 0 1 2 3 4 5 6 7 8 9
|<------------->|
a
|<->|
b
a.within(b) is False
>>> a = DisjointIntervalSequence(coord_ivs, start=30, end=50)
>>> b = DisjointIntervalSequence(coord_ivs, start=10, end=80)
>>> a.within(b)
True
>>> b.within(a)
False
>>> # A segment is within itself
>>> a.within(a)
True
>>> # Zero-length segments are within any enclosing segment
>>> z = DisjointIntervalSequence(coord_ivs, start=50, end=50)
>>> z.within(a)
True
Mapping Between Genomic and DIS Coordinates
A DIS lives in two coordinate systems at once: the genomic coordinates of
its underlying intervals, and the spliced DIS index space.
lower() and
lift_interval() are the two directions of
travel between them.
lower() projects the segment back to
genomic space. lift_interval()
projects a genomic interval into the DIS’s index space and clips it
against the segment. They are conceptual inverses, but each has to deal
with a structural mismatch — gaps in genomic space have no representation
in DIS space, and contiguous DIS indices may correspond to two
non-adjacent genomic regions — so neither is a clean bijection.
lower
Because a segment can straddle one or more boundaries between coord
intervals, lower() returns a list of genomic
Interval objects rather than a single one. The
list is in 5’→3’ order with respect to the segment, regardless of the
underlying coord strand or whether the segment is on the coord strand.
A segment that fits inside a single coord interval lowers to a one-element list. A segment that crosses N coord-interval boundaries lowers to N+1 intervals — the gaps between coord intervals are skipped
>>> dis = DisjointIntervalSequence(
... [Interval("chr1", "+", 100, 200, "hg38"),
... Interval("chr1", "+", 300, 400, "hg38")],
... start=50, end=150,
... )
>>> dis.lower()
[Interval("chr1", "+", 150, 200, "hg38"),
Interval("chr1", "+", 300, 350, "hg38")]
When the segment extends past the coord space (start < 0 or
end > coordinate_length), the out-of-bounds portion is linearly
extrapolated from the nearest outer edge of the coord intervals. The
returned intervals can therefore have negative starts or ends past the
chromosome length — there is no clipping to chromosome boundaries.
A segment on the opposite strand lowers to genomic intervals on the opposite strand, listed in segment 5’→3’ order (which is the reverse of coord 5’→3’ order)
>>> dis_opp = dis.as_opposite_strand()
>>> dis_opp.lower()
[Interval("chr1", "-", 300, 350, "hg38"),
Interval("chr1", "-", 150, 200, "hg38")]
lift_interval
lift_interval() is the inverse: it takes
a genomic Interval and returns a new DIS whose segment is
the intersection of that interval with this DIS’s segment, expressed
in the DIS coordinate space. The input must share the same chromosome, reference
genome, and effective strand as the DIS.
The lift is well-defined even when the input straddles gaps between coord
intervals: positions inside a gap collapse to the boundary index of the
adjacent coord interval, and positions outside the coord space extrapolate
linearly. The result is then clipped against the DIS’s segment, so a
genomic interval that overlaps coord space but falls entirely outside the
segment returns None
>>> dis = DisjointIntervalSequence(
... [Interval("chr1", "+", 100, 200, "hg38"),
... Interval("chr1", "+", 300, 400, "hg38")],
... start=0, end=200,
... )
>>> lifted = dis.lift_interval(Interval("chr1", "+", 320, 360, "hg38"))
>>> lifted.start, lifted.end
(120, 160)
>>> # Falls in a coord-interval gap with no overlap of any coord interval
>>> dis.lift_interval(Interval("chr1", "+", 250, 260, "hg38")) is None
True
>>> lifted2 = dis.lift_interval(Interval("chr1", "+", 150, 450, "hg38"))
>>> lifted2.start, lifted2.end
(50, 200) # clipped to the DIS segment
Extracting DNA
dna() returns the spliced DNA sequence
corresponding to the segment as a single string in 5’→3’ order. Internally, the
segment is decomposed via lower() into
one or more genomic intervals, each is read from the
reference, and the pieces are concatenated. This returns the spliced sequence
of the transcript: the gaps between coord intervals are dropped so that introns
(or other intervening regions) never appear in the output.
The returned string already accounts for strand: if the segment is on the opposite strand, the segment’s bases are returned, and the bases are ordered 5’→3’
>>> dis = DisjointIntervalSequence.from_transcript(transcript)
>>> dis.dna()
'ACGTGGTTTCA' # full spliced sequence, 5'→3'
>>> opp = dis.as_opposite_strand()
>>> opp.dna() # reverse complement, 5'→3' along the opposite strand
'TGAAACCACGT'
When the segment extends past the coord space — e.g. after
shift() or
expand() pushed the indices outside
[0, coordinate_length) — those
out-of-coord positions are returned as N by default. This is in
contrast to expand_coord(), which
extends the coord space itself so
that new positions are backed by real reference DNA. Set
allow_outside_coord=False to opt out of N-padding and raise an error instead.