# Source code for velocyto.indexes

from typing import *
from collections import defaultdict
import logging
import velocyto as vcy

[docs]class TransciptsIndex:
__slots__ = ["transcipt_models", "tidx", "maxtidx"]
"""Search help class used to find the transcipt models that a read is spanning/contained into"""
def __init__(self, trascript_models: List[vcy.TranscriptModel]) -> None:
self.transcipt_models = trascript_models
# self.transcipt_models.sort()
self.tidx = 0  # index of the current interval
self.maxtidx = len(trascript_models) - 1

@ property
def scan_not_terminated(self) -> bool:
"""Return false when all the chromosome has been scanned"""
return self.tidx < self.maxtidx

[docs]    def find_overlapping_trascript_models(self, read: vcy.Read) -> Set[vcy.TranscriptModel]:
"""Finds all the Transcript models the Read overlaps with

Args
----
read: vcy.Read
the read object to be analyzed

Returns
-------
matched_transcripts: set of vcy.TranscriptModel
TranscriptModel the read is overlapping with and values the kind of overlapping
it is one of vcy.MATCH_INSIDE (1), vcy.MATCH_OVER5END (2), vcy.MATCH_OVER3END (4)

"""
matched_transcripts: Set[vcy.TranscriptModel] = set()
if len(self.transcipt_models) == 0:
logging.error("TransciptsIndex %s contains no intervals" % self)
return matched_transcripts

tmodel = self.transcipt_models[self.tidx]  # current transcript model
# Move forward until we find the position we will never search left of again (because the reads are ordered)
while self.scan_not_terminated and tmodel.ends_upstream_of(read):
# move to the next interval
self.tidx += 1
tmodel = self.transcipt_models[self.tidx]

# Loop trough the mapping segments of a read (e.g. just one of an internal exon, generally 2 for a splice or intron)
for segment in read.segments:
# Local search for each segment move a little forward (this just moves a coupple of intervals)
i = self.tidx
while i < self.maxtidx and tmodel.starts_upstream_of(segment):
matchtype = 0  # No match
if tmodel.intersects(segment):
# NOTE: do we need to append all the model or the id would be enough?
matched_transcripts.add(tmodel)
# move to the next transcript model
i += 1
tmodel = self.transcipt_models[i]
return matched_transcripts

[docs]class FeatureIndex:
""" Search help class used to find the intervals that a read is spanning """
def __init__(self, ivls: List[vcy.Feature]=[]) -> None:
self.ivls = ivls
self.ivls.sort()  # NOTE: maybe I am sorting twice check what I do upon creation
self.iidx = 0  # index of the current interval
self.maxiidx = len(ivls) - 1
# NOTE needs to be changed to consider situations of identical intervals but different objects

@ property
def last_interval_not_reached(self) -> bool:
return self.iidx < self.maxiidx

[docs]    def reset(self) -> None:
"""It set the current feature to the first feature
"""
self.iidx = 0

[docs]    def has_ivls_enclosing(self, read: vcy.Read) -> bool:
"""Finds out if there are intervals that are fully containing all the read segments

Args
----
read: vcy.Read
the read object to be analyzed

Returns
-------
respones: bool
if one has been found

"""
if len(self.ivls) == 0:
return False

ivl = self.ivls[self.iidx]  # current interval

# Move forward until we find the position we will never search left of again (because the reads are ordered)
while self.last_interval_not_reached and ivl.ends_upstream_of(read):
# move to the next interval
self.iidx += 1
ivl = self.ivls[self.iidx]

for segment in read.segments:
segment_matchtype = 0
# Local search for each segment move a little forward (this just moves a coupple of intervals)
i = self.iidx
ivl = self.ivls[self.iidx]
while i < self.maxiidx and ivl.doesnt_start_after(segment):
matchtype = 0  # No match
if ivl.contains(segment):
matchtype = vcy.MATCH_INSIDE
if ivl.start_overlaps_with_part_of(segment):  # NOTE: should this be elif or it makes sense to allow both?
matchtype |= vcy.MATCH_OVER5END
if ivl.end_overlaps_with_part_of(segment):  # NOTE: should this be elif or it makes sense to allow both?
matchtype |= vcy.MATCH_OVER3END

segment_matchtype |= matchtype
# move to the next interval
i += 1
ivl = self.ivls[i]

# If one of the segments does not match inside a repeat we return false
if segment_matchtype ^ vcy.MATCH_INSIDE:
return False
# If I arrive at this point of the code all the segments matched inside
return True

[docs]    def mark_overlapping_ivls(self, read: vcy.Read) -> None:
"""Finds the overlap between Read and Features and mark intronic features if spanned

Args
----
read: vcy.Read
the read object to be analyzed

Returns
-------
Nothing, it marks the vcy.Feature object (is_validated = True) if there is evidence of exon-intron spanning
"""

# ## TEMPORARY ######
# dump_list = []
####################

if len(self.ivls) == 0:
return
feature: vcy.Feature = self.ivls[self.iidx]  # current interval
# Move forward until we find the position we will never search left of again (because the reads are ordered)
while self.last_interval_not_reached and feature.ends_upstream_of(read):
# move to the next interval
self.iidx += 1
feature = self.ivls[self.iidx]

# Loop trough the mapping segments of a read (e.g. just one of an internal exon, generally 2 for a splice. for intron???)
for n_seg, segment in enumerate(read.segments):
# Local search for each segment move a little forward (this just moves a couple of intervals)
i = self.iidx
feature = self.ivls[self.iidx]
while i < self.maxiidx and feature.doesnt_start_after(segment):
# NOTE in this way the checks will be repeated for every clone of an interval in each transcript model
# I might want to cache the check results of the previous feature
if feature.kind == ord("i"):
if feature.end_overlaps_with_part_of(segment):
downstream_exon = feature.get_downstream_exon()
if downstream_exon.start_overlaps_with_part_of(segment):
feature.is_validated = True
# ## TEMPORARY ######
# dump_list.append((read, segment, feature, "r"))
####################
if feature.start_overlaps_with_part_of(segment):
upstream_exon = feature.get_upstream_exon()
if upstream_exon.end_overlaps_with_part_of(segment):
feature.is_validated = True
# ## TEMPORARY ######
# dump_list.append((read, segment, feature, "l"))
####################
# if feature.contains(segment):
#     pass  # here the intron is not validated !
elif feature.kind == ord("e"):
pass  # here I can pass if I do the proper checks at the intron level
else:
raise ValueError(f"Unrecognized type of genomic feature {chr(feature.kind)}")

#  move to the next interval
i += 1
feature = self.ivls[i]

# ## TEMPORARY ######
# return dump_list
####################

[docs]    def find_overlapping_ivls(self, read: vcy.Read) -> Dict[vcy.TranscriptModel, List[vcy.SegmentMatch]]:
"""Finds the possible overlaps between Read and Features and return a 1 read derived mapping record

Arguments
---------
read: vcy.Read
the read object to be analyzed

Returns
-------
mapping_record: Dict[vcy.TranscriptModel, List[vcy.SegmentMatch]]
A record of the mappings by transcript model.
Every entry contains a list of segment matches that in turn contains information on the segment and the feature

Note
----
- It is possible that a segment overalps at the same time an exon and an intron (spanning segment)
- It is not possible that a segment overalps at the same time two exons. In that case the read is splitted
into two segments and  the Read attribute is_spliced == True.
- Notice that the name of the function might be confousing. if there is a non valid overallapping an empty mappign record will be return
- Also notice that returning an empty mapping record will cause the suppression of the counting of the molecule

"""

mapping_record: Dict[vcy.TranscriptModel, List[vcy.SegmentMatch]] = defaultdict(list)

if len(self.ivls) == 0:
return mapping_record

feature: vcy.Feature = self.ivls[self.iidx]  # current interval
# Move forward until we find the position we will never search left of again (because the reads are ordered)
while self.last_interval_not_reached and feature.ends_upstream_of(read):
# move to the next interval
self.iidx += 1
feature = self.ivls[self.iidx]

# Loop trough the mapping segments of a read (e.g. just one of an internal exon, generally 2 for a splice. for intron???)
for seg_n, segment in enumerate(read.segments):
# Local search for each segment move a little forward (this just moves a couple of intervals)
i = self.iidx
feature = self.ivls[i]
while i < self.maxiidx and feature.doesnt_start_after(segment):
# NOTE in this way the checks will be repeated for every clone of an interval in each transcript model
# I might want to cache the check results of the previous feature
# it was  if feature.contains(segment) or feature.start_overlaps_with_part_of(segment) or feature.end_overlaps_with_part_of(segment)
# but I changed to the more simple
if feature.intersects(segment) and (segment[-1] - segment[0]) > vcy.MIN_FLANK:
mapping_record[feature.transcript_model].append(vcy.SegmentMatch(segment, feature, read.is_spliced))
#  move to the next interval
i += 1
feature = self.ivls[i]

# NOTE: Removing first the one with less match and then requiring a splicing matching the transcript model is very stringent
# It could be that for short ~10bp SKIP sequences the alligner has made a mistake and this might kill the whole molecule
# NOTE: the code below is not very efficient
if len(mapping_record) != 0:
# Remove transcript models that are suboptimal match
# in alternative one could use len(read.segment)
max_n_segments = len(max(mapping_record.values(), key=len))
for tm, segmatch_list in list(mapping_record.items()):
if len(segmatch_list) < max_n_segments:
del mapping_record[tm]

# NOTE: the code below is not very efficient, would be nice to avoid for loops
# NOTE: potentailly bad effects: it could kill a lot of molecules if transcript models are not annotated correctly or missing
if len(mapping_record) != 0:
# A SKIP mapping needs to be explainaible by some kind of exon-exon exon-intron junction!
# So if it falls internally, the TM needs to be removed from the mapping record
for tm, segmatch_list in list(mapping_record.items()):
for sm in segmatch_list:
if not sm.skip_makes_sense:
del mapping_record[tm]
break

return mapping_record