RiboParser 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {riboparser-0.2.2 → riboparser-0.2.3}/PKG-INFO +1 -1
- {riboparser-0.2.2 → riboparser-0.2.3}/RiboParser.egg-info/PKG-INFO +1 -1
- {riboparser-0.2.2 → riboparser-0.2.3}/pyproject.toml +1 -1
- riboparser-0.2.3/utils/smorf/overlap.py +234 -0
- riboparser-0.2.3/utils/smorf/pipeline.py +287 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/smorf_scanner.py +9 -0
- riboparser-0.2.2/utils/smorf/overlap.py +0 -76
- riboparser-0.2.2/utils/smorf/pipeline.py +0 -158
- {riboparser-0.2.2 → riboparser-0.2.3}/README.md +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/RiboParser.egg-info/SOURCES.txt +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/RiboParser.egg-info/dependency_links.txt +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/RiboParser.egg-info/entry_points.txt +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/RiboParser.egg-info/requires.txt +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/RiboParser.egg-info/top_level.txt +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/bedgraph/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/bedgraph/bg2meta.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/bedgraph/rpm_smooth.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/bowtie/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/bowtie/merge_bwt_log.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fasta/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fasta/fa_gc_sum.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fasta/fa_len_flt.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fasta/fa_len_sum.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fasta/fa_split.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fasta/line_feed.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fasta/nt2aa.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fasta/rand_seq.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fasta/retrieve_seq.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fasta/revs.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fastq/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fastq/fq2fa.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fastq/fq2txt.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fastq/fq_len_flt.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fastq/fq_len_sum.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fastq/fq_length.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fastq/fq_split.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fastq/fq_trim.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fastq/phred_quality.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/fastq/simulate_fastq.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_cdt.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_coverage.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_cst.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_digestion.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_dst_list.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_length.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_metagene.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_occupancy.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_odd_ratio.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_offset.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_offset_detail.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_offset_end.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_pausing.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_period.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_quant.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/merge_ribo/merge_saturation.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/oligo/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/oligo/get_overlap_seq.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/oligo/get_tissue_freq.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/oligo/get_win_seq.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/ribocode/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/ribocode/ribocode_bed_format.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/ribotish/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/ribotish/ribotish_format.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/rsem/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/rsem/merge_rsem.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/unix/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/scripts/unix/dos2unix.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/setup.cfg +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/data/RiboParser.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/ArgsParser.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Bam2Wig.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/BamFilter.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/CDT.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/CST.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Codon.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Coefficient_of_Variation.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Coverage.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Cumulative_CoV.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Density.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Digestion.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/EndSite.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Ensembl_Ref.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/GenePred.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/MetaCodon.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Metaplot.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Occupancy.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Odd_Ratio.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Offset.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Offset_RSBM.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Pausing.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Percentage.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Periodicity.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Quality.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Quant.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/RNA.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/RPFs.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Retrieve.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Ribo.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Shift.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/Shuffle.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/ribo/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/riboparser.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rna_Density.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rna_Offset.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Bam2bw.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Bam_Filter.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_CDT.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_CST.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Check.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_CoV.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Corr.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Coverage.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Cumulative_CoV.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Density.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Digest.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Geneplot.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Merge.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Meta_Codon.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Metaplot.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Occupancy.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Odd_Ratio.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Offset.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Offset_RSBM.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Pausing.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Percent.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Periodicity.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Quant.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Reference.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Retrieve.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Shift.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_Shuffle.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/rpf_end.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/serp/Properties.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/serp/SeRP.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/serp/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/serp_overlap.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/serp_peak.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/serp_properties.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/smorf/__init__.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/smorf/classifier.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/smorf/coordinate.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/smorf/fasta.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/smorf/genepred.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/smorf/models.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/smorf/scanner.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/smorf/sequence.py +0 -0
- {riboparser-0.2.2 → riboparser-0.2.3}/utils/smorf/writer.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "RiboParser"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
authors = [{ name = "Ren Shuchao", email = "rensc0718@163.com" }]
|
|
9
9
|
description = "A pipeline for ribosome profiling data analysis"
|
|
10
10
|
readme = "README.md"
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Author: Rensc
|
|
2
|
+
# date: 2026-05-21
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
ORF overlap marker.
|
|
6
|
+
|
|
7
|
+
This module marks ORF overlap types and assigns priority labels.
|
|
8
|
+
|
|
9
|
+
Priority rule:
|
|
10
|
+
1. annotated_mORF is preferred.
|
|
11
|
+
2. complete ORF is preferred over partial ORF.
|
|
12
|
+
3. ATG start codon is preferred over non-ATG start codons.
|
|
13
|
+
4. Stronger Kozak context is preferred.
|
|
14
|
+
5. Longer ORF is preferred.
|
|
15
|
+
6. More upstream start site is preferred.
|
|
16
|
+
|
|
17
|
+
Important:
|
|
18
|
+
Different-frame overlapping ORFs are not suppressed by default.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import List, Dict, Tuple
|
|
22
|
+
from .models import ORFRecord
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
START_CODON_RANK = {
|
|
26
|
+
"ATG": 1,
|
|
27
|
+
"CTG": 2,
|
|
28
|
+
"GTG": 3,
|
|
29
|
+
"TTG": 4,
|
|
30
|
+
"ACG": 5,
|
|
31
|
+
"ATA": 6,
|
|
32
|
+
"ATT": 7,
|
|
33
|
+
"ATC": 8,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ORFOverlapMarker:
|
|
38
|
+
"""
|
|
39
|
+
Mark ORF overlap type and priority.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def mark(records: List[ORFRecord]) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Mark ORF overlap relationships.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
records : list
|
|
50
|
+
List of ORFRecord objects.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
ORFOverlapMarker._mark_different_frame_overlap(records)
|
|
54
|
+
ORFOverlapMarker._mark_same_frame_overlap(records)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _mark_same_frame_overlap(records: List[ORFRecord]) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Mark overlaps among ORFs with the same transcript, strand, and frame.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
grouped: Dict[Tuple[str, str, int], List[ORFRecord]] = {}
|
|
63
|
+
|
|
64
|
+
for rec in records:
|
|
65
|
+
key = (rec.transcript_id, rec.source_strand, rec.frame)
|
|
66
|
+
grouped.setdefault(key, []).append(rec)
|
|
67
|
+
|
|
68
|
+
for _, items in grouped.items():
|
|
69
|
+
items.sort(key=lambda x: (x.tx_orf_start, x.tx_orf_end))
|
|
70
|
+
|
|
71
|
+
for rec in items:
|
|
72
|
+
competitors = [
|
|
73
|
+
other for other in items
|
|
74
|
+
if other is not rec
|
|
75
|
+
and ORFOverlapMarker._is_overlap(rec, other)
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
if not competitors:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
best = ORFOverlapMarker._select_best_orf([rec] + competitors)
|
|
82
|
+
|
|
83
|
+
if rec is best:
|
|
84
|
+
if rec.overlap_type == "none":
|
|
85
|
+
rec.overlap_type = "same_frame_overlap"
|
|
86
|
+
rec.priority = "primary"
|
|
87
|
+
else:
|
|
88
|
+
ORFOverlapMarker._downgrade_orf(rec, best)
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _mark_different_frame_overlap(records: List[ORFRecord]) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Mark different-frame overlaps without suppressing either ORF.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
grouped: Dict[Tuple[str, str], List[ORFRecord]] = {}
|
|
97
|
+
|
|
98
|
+
for rec in records:
|
|
99
|
+
key = (rec.transcript_id, rec.source_strand)
|
|
100
|
+
grouped.setdefault(key, []).append(rec)
|
|
101
|
+
|
|
102
|
+
for _, items in grouped.items():
|
|
103
|
+
for i, rec in enumerate(items):
|
|
104
|
+
for j, other in enumerate(items):
|
|
105
|
+
if i >= j:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if rec.frame == other.frame:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
if ORFOverlapMarker._is_overlap(rec, other):
|
|
112
|
+
if rec.overlap_type == "none":
|
|
113
|
+
rec.overlap_type = "overlap_different_frame"
|
|
114
|
+
if other.overlap_type == "none":
|
|
115
|
+
other.overlap_type = "overlap_different_frame"
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _downgrade_orf(rec: ORFRecord, best: ORFRecord) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Downgrade an ORF according to its relationship with the selected best ORF.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
rec.priority = "secondary"
|
|
124
|
+
|
|
125
|
+
if ORFOverlapMarker._is_identical(rec, best):
|
|
126
|
+
rec.overlap_type = "identical_ORF"
|
|
127
|
+
elif ORFOverlapMarker._is_nested(rec, best):
|
|
128
|
+
if rec.start_codon != "ATG" and best.start_codon == "ATG":
|
|
129
|
+
rec.overlap_type = "secondary_noncanonical_start"
|
|
130
|
+
else:
|
|
131
|
+
rec.overlap_type = "nested"
|
|
132
|
+
elif rec.tx_orf_end == best.tx_orf_end:
|
|
133
|
+
if rec.start_codon != best.start_codon:
|
|
134
|
+
rec.overlap_type = "alternative_start_same_stop"
|
|
135
|
+
else:
|
|
136
|
+
rec.overlap_type = "same_stop_overlap"
|
|
137
|
+
elif rec.tx_orf_start == best.tx_orf_start:
|
|
138
|
+
rec.overlap_type = "same_start_different_stop"
|
|
139
|
+
else:
|
|
140
|
+
rec.overlap_type = "same_frame_overlap_different_stop"
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _select_best_orf(records: List[ORFRecord]) -> ORFRecord:
|
|
144
|
+
"""
|
|
145
|
+
Select the most reliable ORF from overlapping ORFs.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
return sorted(records, key=ORFOverlapMarker._priority_key)[0]
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def _priority_key(rec: ORFRecord):
|
|
152
|
+
"""
|
|
153
|
+
Build sorting key for ORF priority.
|
|
154
|
+
|
|
155
|
+
Lower value means higher priority.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
annotated_rank = 0 if rec.category == "annotated_mORF" else 1
|
|
159
|
+
completeness_rank = 0 if rec.completeness == "complete" else 1
|
|
160
|
+
start_rank = START_CODON_RANK.get(rec.start_codon, 99)
|
|
161
|
+
kozak_rank = -ORFOverlapMarker._kozak_score(rec.kozak_seq)
|
|
162
|
+
|
|
163
|
+
# Longer ORFs are preferred after biological confidence rules.
|
|
164
|
+
length_rank = -rec.aa_length
|
|
165
|
+
|
|
166
|
+
# More upstream start site is preferred if all other ranks are equal.
|
|
167
|
+
start_position_rank = rec.tx_orf_start
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
annotated_rank,
|
|
171
|
+
completeness_rank,
|
|
172
|
+
start_rank,
|
|
173
|
+
kozak_rank,
|
|
174
|
+
length_rank,
|
|
175
|
+
start_position_rank,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _kozak_score(kozak_seq: str) -> int:
|
|
180
|
+
"""
|
|
181
|
+
Calculate a simple Kozak score.
|
|
182
|
+
|
|
183
|
+
Rule:
|
|
184
|
+
- Position -3 is A/G: +1
|
|
185
|
+
- Position +4 is G: +1
|
|
186
|
+
|
|
187
|
+
The input sequence is expected to contain:
|
|
188
|
+
upstream sequence + start codon + downstream sequence.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
if not kozak_seq:
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
seq = kozak_seq.upper()
|
|
195
|
+
score = 0
|
|
196
|
+
|
|
197
|
+
# Default scanner extracts 6 nt upstream + 3 nt start codon + downstream.
|
|
198
|
+
# Therefore start codon begins at index 6 if full Kozak sequence exists.
|
|
199
|
+
start_index = 6 if len(seq) >= 9 else max(0, len(seq) // 2 - 1)
|
|
200
|
+
|
|
201
|
+
minus3_index = start_index - 3
|
|
202
|
+
plus4_index = start_index + 3
|
|
203
|
+
|
|
204
|
+
if 0 <= minus3_index < len(seq) and seq[minus3_index] in {"A", "G"}:
|
|
205
|
+
score += 1
|
|
206
|
+
|
|
207
|
+
if 0 <= plus4_index < len(seq) and seq[plus4_index] == "G":
|
|
208
|
+
score += 1
|
|
209
|
+
|
|
210
|
+
return score
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _is_overlap(a: ORFRecord, b: ORFRecord) -> bool:
|
|
214
|
+
"""
|
|
215
|
+
Check whether two ORFs overlap in transcript coordinates.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
return a.tx_orf_start < b.tx_orf_end and a.tx_orf_end > b.tx_orf_start
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def _is_nested(a: ORFRecord, b: ORFRecord) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Check whether ORF a is fully contained within ORF b.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
return b.tx_orf_start <= a.tx_orf_start and b.tx_orf_end >= a.tx_orf_end
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def _is_identical(a: ORFRecord, b: ORFRecord) -> bool:
|
|
230
|
+
"""
|
|
231
|
+
Check whether two ORFs have identical transcript coordinates.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
return a.tx_orf_start == b.tx_orf_start and a.tx_orf_end == b.tx_orf_end
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Author: Rensc
|
|
2
|
+
# date: 2026-05-21
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Main smORF pipeline.
|
|
6
|
+
|
|
7
|
+
This pipeline connects all functional modules:
|
|
8
|
+
1. Read genome FASTA.
|
|
9
|
+
2. Read genePred annotation.
|
|
10
|
+
3. Reconstruct transcript sequences.
|
|
11
|
+
4. Scan ORFs.
|
|
12
|
+
5. Classify ORFs.
|
|
13
|
+
6. Mark ORF overlaps.
|
|
14
|
+
7. Write output files.
|
|
15
|
+
|
|
16
|
+
Parallel mode uses multiprocessing instead of threading because ORF scanning
|
|
17
|
+
is CPU-intensive and Python threads are limited by the GIL.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
21
|
+
|
|
22
|
+
from .fasta import FastaParser
|
|
23
|
+
from .genepred import GenePredParser
|
|
24
|
+
from .coordinate import CoordinateMapper
|
|
25
|
+
from .scanner import ORFScanner
|
|
26
|
+
from .classifier import ORFClassifier
|
|
27
|
+
from .overlap import ORFOverlapMarker
|
|
28
|
+
from .writer import GenePredWriter, MessageWriter, FastaWriter
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_WORKER_GENOME = None
|
|
32
|
+
_WORKER_CONFIG = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _init_worker(genome, config):
|
|
36
|
+
"""
|
|
37
|
+
Initialize worker-level global objects.
|
|
38
|
+
|
|
39
|
+
This avoids sending the genome dictionary to every single transcript task.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
global _WORKER_GENOME
|
|
43
|
+
global _WORKER_CONFIG
|
|
44
|
+
|
|
45
|
+
_WORKER_GENOME = genome
|
|
46
|
+
_WORKER_CONFIG = config
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _scan_transcript_worker(task):
|
|
50
|
+
"""
|
|
51
|
+
Worker function for scanning one transcript.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
task : tuple
|
|
56
|
+
Tuple of transcript index and Transcript object.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
tuple
|
|
61
|
+
Transcript index, transcript ID, gene ID, and ORF records.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
idx, tx = task
|
|
65
|
+
|
|
66
|
+
scanner = ORFScanner(
|
|
67
|
+
start_codons=_WORKER_CONFIG["start_codons"],
|
|
68
|
+
min_aa=_WORKER_CONFIG["min_aa"],
|
|
69
|
+
max_aa=_WORKER_CONFIG["max_aa"],
|
|
70
|
+
scan_strand=_WORKER_CONFIG["scan_strand"],
|
|
71
|
+
kozak_up=_WORKER_CONFIG["kozak_up"],
|
|
72
|
+
kozak_down=_WORKER_CONFIG["kozak_down"],
|
|
73
|
+
include_stop=_WORKER_CONFIG["include_stop"],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Reconstruct spliced transcript sequence before ORF scanning.
|
|
77
|
+
CoordinateMapper.build_transcript_sequence(tx, _WORKER_GENOME)
|
|
78
|
+
|
|
79
|
+
# Scan candidate ORFs from the transcript sequence.
|
|
80
|
+
tx_records = scanner.scan_transcript(tx)
|
|
81
|
+
|
|
82
|
+
# Assign ORF category labels.
|
|
83
|
+
ORFClassifier.classify(tx, tx_records)
|
|
84
|
+
|
|
85
|
+
# Mark nested or overlapping ORFs if requested.
|
|
86
|
+
if _WORKER_CONFIG["mark_overlap"]:
|
|
87
|
+
ORFOverlapMarker.mark(tx_records)
|
|
88
|
+
|
|
89
|
+
# Remove same-frame internal ORFs if requested.
|
|
90
|
+
if _WORKER_CONFIG["remove_discarded"]:
|
|
91
|
+
tx_records = [x for x in tx_records if x.priority != "discarded"]
|
|
92
|
+
|
|
93
|
+
return idx, tx.transcript_id, tx.gene_id, tx_records
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SmORFPipeline:
|
|
97
|
+
"""
|
|
98
|
+
High-level pipeline for transcript-centric smORF detection.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
genome: str,
|
|
104
|
+
annotation: str,
|
|
105
|
+
out_prefix: str = "ORF",
|
|
106
|
+
orf_prefix: str = "ORF",
|
|
107
|
+
start_codons: str = "ATG",
|
|
108
|
+
min_aa: int = 8,
|
|
109
|
+
max_aa: int = 10000,
|
|
110
|
+
scan_strand: str = "sense",
|
|
111
|
+
kozak_up: int = 6,
|
|
112
|
+
kozak_down: int = 6,
|
|
113
|
+
mark_overlap: bool = False,
|
|
114
|
+
remove_discarded: bool = False,
|
|
115
|
+
include_stop: bool = False,
|
|
116
|
+
threads: int = 1,
|
|
117
|
+
):
|
|
118
|
+
"""
|
|
119
|
+
Initialize smORF pipeline.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
self.genome_path = genome
|
|
123
|
+
self.annotation_path = annotation
|
|
124
|
+
self.out_prefix = out_prefix
|
|
125
|
+
self.orf_prefix = orf_prefix
|
|
126
|
+
self.start_codons = [x.strip().upper() for x in start_codons.split(",")]
|
|
127
|
+
self.min_aa = min_aa
|
|
128
|
+
self.max_aa = max_aa
|
|
129
|
+
self.scan_strand = scan_strand
|
|
130
|
+
self.kozak_up = kozak_up
|
|
131
|
+
self.kozak_down = kozak_down
|
|
132
|
+
self.mark_overlap = mark_overlap
|
|
133
|
+
self.remove_discarded = remove_discarded
|
|
134
|
+
self.include_stop = include_stop
|
|
135
|
+
self.threads = max(1, int(threads))
|
|
136
|
+
self.records = []
|
|
137
|
+
|
|
138
|
+
def run(self) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Run the complete smORF scanning pipeline.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
genome = FastaParser.read_fasta(self.genome_path)
|
|
144
|
+
transcripts = GenePredParser.read_genepred(self.annotation_path)
|
|
145
|
+
|
|
146
|
+
if self.threads == 1:
|
|
147
|
+
self._run_single_process(genome, transcripts)
|
|
148
|
+
else:
|
|
149
|
+
self._run_multi_process(genome, transcripts)
|
|
150
|
+
|
|
151
|
+
self.write_outputs()
|
|
152
|
+
|
|
153
|
+
def _run_single_process(self, genome, transcripts) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Run smORF scanning in single-process mode.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
scanner = ORFScanner(
|
|
159
|
+
start_codons=self.start_codons,
|
|
160
|
+
min_aa=self.min_aa,
|
|
161
|
+
max_aa=self.max_aa,
|
|
162
|
+
scan_strand=self.scan_strand,
|
|
163
|
+
kozak_up=self.kozak_up,
|
|
164
|
+
kozak_down=self.kozak_down,
|
|
165
|
+
include_stop=self.include_stop,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
total_tx = len(transcripts)
|
|
169
|
+
orf_index = 1
|
|
170
|
+
|
|
171
|
+
for idx, tx in enumerate(transcripts, start=1):
|
|
172
|
+
# Print scanning progress.
|
|
173
|
+
print(
|
|
174
|
+
"[smORFScanner] [{}/{}] Scanning gene={}, transcript={}, chrom={}, strand={}".format(
|
|
175
|
+
idx,
|
|
176
|
+
total_tx,
|
|
177
|
+
tx.gene_id,
|
|
178
|
+
tx.transcript_id,
|
|
179
|
+
tx.chrom,
|
|
180
|
+
tx.strand,
|
|
181
|
+
),
|
|
182
|
+
flush=True,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Reconstruct spliced transcript sequence before ORF scanning.
|
|
186
|
+
CoordinateMapper.build_transcript_sequence(tx, genome)
|
|
187
|
+
|
|
188
|
+
# Scan candidate ORFs from the transcript sequence.
|
|
189
|
+
tx_records = scanner.scan_transcript(tx)
|
|
190
|
+
|
|
191
|
+
# Assign ORF category labels.
|
|
192
|
+
ORFClassifier.classify(tx, tx_records)
|
|
193
|
+
|
|
194
|
+
# Mark nested or overlapping ORFs if requested.
|
|
195
|
+
if self.mark_overlap:
|
|
196
|
+
ORFOverlapMarker.mark(tx_records)
|
|
197
|
+
|
|
198
|
+
# Remove same-frame internal ORFs if requested.
|
|
199
|
+
if self.remove_discarded:
|
|
200
|
+
tx_records = [x for x in tx_records if x.priority != "discarded"]
|
|
201
|
+
|
|
202
|
+
# Assign stable ORF IDs.
|
|
203
|
+
for rec in tx_records:
|
|
204
|
+
rec.orf_id = "{}{:08d}".format(self.orf_prefix, orf_index)
|
|
205
|
+
orf_index += 1
|
|
206
|
+
|
|
207
|
+
self.records.extend(tx_records)
|
|
208
|
+
|
|
209
|
+
def _run_multi_process(self, genome, transcripts) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Run smORF scanning in multiprocessing mode.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
total_tx = len(transcripts)
|
|
215
|
+
|
|
216
|
+
config = {
|
|
217
|
+
"start_codons": self.start_codons,
|
|
218
|
+
"min_aa": self.min_aa,
|
|
219
|
+
"max_aa": self.max_aa,
|
|
220
|
+
"scan_strand": self.scan_strand,
|
|
221
|
+
"kozak_up": self.kozak_up,
|
|
222
|
+
"kozak_down": self.kozak_down,
|
|
223
|
+
"include_stop": self.include_stop,
|
|
224
|
+
"mark_overlap": self.mark_overlap,
|
|
225
|
+
"remove_discarded": self.remove_discarded,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
print(
|
|
229
|
+
"[smORFScanner] Running in multiprocessing mode with {} workers.".format(
|
|
230
|
+
self.threads
|
|
231
|
+
),
|
|
232
|
+
flush=True,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
results_by_index = {}
|
|
236
|
+
|
|
237
|
+
with ProcessPoolExecutor(
|
|
238
|
+
max_workers=self.threads,
|
|
239
|
+
initializer=_init_worker,
|
|
240
|
+
initargs=(genome, config),
|
|
241
|
+
) as executor:
|
|
242
|
+
future_to_index = {
|
|
243
|
+
executor.submit(_scan_transcript_worker, (idx, tx)): idx
|
|
244
|
+
for idx, tx in enumerate(transcripts, start=1)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
finished = 0
|
|
248
|
+
|
|
249
|
+
for future in as_completed(future_to_index):
|
|
250
|
+
idx, transcript_id, gene_id, tx_records = future.result()
|
|
251
|
+
results_by_index[idx] = tx_records
|
|
252
|
+
|
|
253
|
+
finished += 1
|
|
254
|
+
|
|
255
|
+
# Print completed transcript progress.
|
|
256
|
+
print(
|
|
257
|
+
"[smORFScanner] [{}/{}] Finished gene={}, transcript={}, ORFs={}".format(
|
|
258
|
+
finished,
|
|
259
|
+
total_tx,
|
|
260
|
+
gene_id,
|
|
261
|
+
transcript_id,
|
|
262
|
+
len(tx_records),
|
|
263
|
+
),
|
|
264
|
+
flush=True,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Rebuild records in original transcript order and assign stable ORF IDs.
|
|
268
|
+
orf_index = 1
|
|
269
|
+
|
|
270
|
+
for idx in range(1, total_tx + 1):
|
|
271
|
+
tx_records = results_by_index.get(idx, [])
|
|
272
|
+
|
|
273
|
+
for rec in tx_records:
|
|
274
|
+
rec.orf_id = "{}{:08d}".format(self.orf_prefix, orf_index)
|
|
275
|
+
orf_index += 1
|
|
276
|
+
|
|
277
|
+
self.records.extend(tx_records)
|
|
278
|
+
|
|
279
|
+
def write_outputs(self) -> None:
|
|
280
|
+
"""
|
|
281
|
+
Write all output files.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
GenePredWriter.write("{}.genePred".format(self.out_prefix), self.records)
|
|
285
|
+
MessageWriter.write("{}.message.txt".format(self.out_prefix), self.records)
|
|
286
|
+
FastaWriter.write_nt("{}.nt.fa".format(self.out_prefix), self.records)
|
|
287
|
+
FastaWriter.write_pep("{}.pep.fa".format(self.out_prefix), self.records)
|
|
@@ -106,6 +106,14 @@ def parse_args():
|
|
|
106
106
|
help="Number of downstream nucleotides after start codon for Kozak sequence."
|
|
107
107
|
)
|
|
108
108
|
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"-t",
|
|
111
|
+
"--threads",
|
|
112
|
+
type=int,
|
|
113
|
+
default=1,
|
|
114
|
+
help="Number of worker processes for parallel ORF scanning."
|
|
115
|
+
)
|
|
116
|
+
|
|
109
117
|
parser.add_argument(
|
|
110
118
|
"--mark-overlap",
|
|
111
119
|
action="store_true",
|
|
@@ -148,6 +156,7 @@ def main():
|
|
|
148
156
|
mark_overlap=args.mark_overlap,
|
|
149
157
|
remove_discarded=args.remove_discarded,
|
|
150
158
|
include_stop=args.include_stop,
|
|
159
|
+
threads=args.threads,
|
|
151
160
|
)
|
|
152
161
|
|
|
153
162
|
pipeline.run()
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Author: Rensc
|
|
2
|
-
# date: 2026-05-21
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
ORF overlap marker.
|
|
6
|
-
|
|
7
|
-
This module marks nested or partially overlapping ORFs within the same:
|
|
8
|
-
1. transcript
|
|
9
|
-
2. source strand
|
|
10
|
-
3. reading frame
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from typing import List
|
|
14
|
-
from .models import ORFRecord
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ORFOverlapMarker:
|
|
18
|
-
"""
|
|
19
|
-
Mark nested and overlapping ORFs.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
@staticmethod
|
|
23
|
-
def mark(records: List[ORFRecord]) -> None:
|
|
24
|
-
"""
|
|
25
|
-
Mark ORF overlap type and priority.
|
|
26
|
-
|
|
27
|
-
A shorter ORF fully contained in a longer ORF with the same frame
|
|
28
|
-
is marked as secondary.
|
|
29
|
-
|
|
30
|
-
Parameters
|
|
31
|
-
----------
|
|
32
|
-
records : list
|
|
33
|
-
List of ORFRecord objects.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
grouped = {}
|
|
37
|
-
|
|
38
|
-
# Group ORFs by transcript, strand, and frame.
|
|
39
|
-
for rec in records:
|
|
40
|
-
key = (rec.transcript_id, rec.source_strand, rec.frame)
|
|
41
|
-
grouped.setdefault(key, []).append(rec)
|
|
42
|
-
|
|
43
|
-
for _, items in grouped.items():
|
|
44
|
-
items.sort(key=lambda x: (x.tx_orf_start, -(x.tx_orf_end - x.tx_orf_start)))
|
|
45
|
-
|
|
46
|
-
for i, rec in enumerate(items):
|
|
47
|
-
for j, other in enumerate(items):
|
|
48
|
-
if i == j:
|
|
49
|
-
continue
|
|
50
|
-
|
|
51
|
-
is_nested = (
|
|
52
|
-
other.tx_orf_start <= rec.tx_orf_start
|
|
53
|
-
and other.tx_orf_end >= rec.tx_orf_end
|
|
54
|
-
and (other.tx_orf_end - other.tx_orf_start)
|
|
55
|
-
> (rec.tx_orf_end - rec.tx_orf_start)
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
if is_nested:
|
|
59
|
-
rec.priority = "secondary"
|
|
60
|
-
rec.overlap_type = "nested"
|
|
61
|
-
break
|
|
62
|
-
|
|
63
|
-
# If the ORF is not nested, check partial overlap.
|
|
64
|
-
if rec.priority == "primary":
|
|
65
|
-
for other in items:
|
|
66
|
-
if rec is other:
|
|
67
|
-
continue
|
|
68
|
-
|
|
69
|
-
is_overlap = (
|
|
70
|
-
rec.tx_orf_start < other.tx_orf_end
|
|
71
|
-
and rec.tx_orf_end > other.tx_orf_start
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
if is_overlap:
|
|
75
|
-
rec.overlap_type = "partial_overlap"
|
|
76
|
-
break
|