diff --git a/README.md b/README.md
index e45a472..b072dfe 100755
--- a/README.md
+++ b/README.md
@@ -57,6 +57,14 @@ Easiest is to check [datasets](datasets) examples to see how the above files loo
## Changelog
+**v0.8.1**: released in July 2026
+
+* added `bam_file` column support in `samples.tab` for per-sample BAM paths (subfolder layouts)
+* new `get_bam_path(sample_id)` helper in `splicekit/core/annotation.py` — falls back to `{bam_path}/{sample_id}.bam` when no per-sample path is given
+* `exons.py`, `genes.py`, `anchors.py`: replaced `os.listdir()` BAM discovery with `annotation.samples` list + `get_bam_path()` (enables subfolder BAMs, no dir scan needed)
+* `junctions.py`, `jbrowse2.py`: same `get_bam_path()` adoption
+* default `bam_column = "bam_file"` added to config
+
**v0.8**: released in October 2025
* removed platform config option (now snakemake submits jobs to the cluster)
diff --git a/splicekit/__init__.py b/splicekit/__init__.py
index 27514d5..be4fc66 100644
--- a/splicekit/__init__.py
+++ b/splicekit/__init__.py
@@ -62,6 +62,7 @@
splicekit.core.annotation.samples = set()
splicekit.core.annotation.comparisons = []
splicekit.core.annotation.genes = {}
+splicekit.core.annotation.bam_path_map = {}
splicekit.core.annotation.make_comparisons() # load existing comparisons
def setup():
diff --git a/splicekit/config/__init__.py b/splicekit/config/__init__.py
index b2ae621..f1e3cf7 100644
--- a/splicekit/config/__init__.py
+++ b/splicekit/config/__init__.py
@@ -74,4 +74,9 @@ def jbrowse2_config():
try:
clip
except:
- clip = None
\ No newline at end of file
+ clip = None
+
+try:
+ bam_column
+except:
+ bam_column = "bam_file"
\ No newline at end of file
diff --git a/splicekit/core/anchors.py b/splicekit/core/anchors.py
index ba3aa3e..91a14e3 100644
--- a/splicekit/core/anchors.py
+++ b/splicekit/core/anchors.py
@@ -7,6 +7,7 @@
import os
import sys
import splicekit.config as config
+import splicekit.core.annotation as annotation
import gzip
def write_anchor_gtf():
@@ -69,7 +70,6 @@ def write_jobs_featureCounts(library_type='single-end', library_strand='NONE'):
for anchor_type in ["donor", "acceptor"]:
gtf_fname = f"reference/{anchor_type}_anchors.gtf.gz"
- bam_dir = f"{config.bam_path}" # files inside end with .bam
out_dir = f'data/sample_{anchor_type}_anchors_data'
jobs_dir = f'jobs/count_{anchor_type}_anchors'
logs_dir = f'logs/count_{anchor_type}_anchors'
@@ -131,14 +131,13 @@ def write_jobs_featureCounts(library_type='single-end', library_strand='NONE'):
gzip -f {out_fname}
"""
- bam_files = [fi for fi in os.listdir(bam_dir) if fi.endswith('.bam')]
- sample_ids = [fi.replace('.bam', '') for fi in bam_files]
+ sample_ids = annotation.samples
header_line = '\t'.join(['anchor_id', 'count'])
fsh = open(f"{jobs_dir}/process.sh", "wt")
for sample in sample_ids:
out_fname = f"{out_dir}/sample_{sample}.tab"
job_fname = f'{jobs_dir}/{anchor_type}_anchors_{sample}.job'
- sam_fname = f"{bam_dir}/{sample}.bam"
+ sam_fname = annotation.get_bam_path(sample)
# cluster job
job_out = job_anchors.format(container=config.container, library_type_insert=library_type_insert, library_strand_insert=library_strand_insert, anchor_type=anchor_type, gtf_fname=gtf_fname, sample_id=sample, sam_fname=sam_fname, out_fname=out_fname, logs_dir=logs_dir, header_line=header_line)
job_file = open(job_fname, "w")
diff --git a/splicekit/core/annotation.py b/splicekit/core/annotation.py
index 9416d10..dc8a11d 100644
--- a/splicekit/core/annotation.py
+++ b/splicekit/core/annotation.py
@@ -56,12 +56,25 @@ def run(self):
self.returncode = self.process.returncode
return output.decode("utf-8"), error.decode("utf-8")
+def get_bam_path(sample_id):
+ """Return the BAM file path for a sample.
+
+ If samples.tab contained a bam_file column (config.bam_column), returns
+ that per-sample path, allowing BAM files in arbitrary sub-folders.
+ Otherwise falls back to the flat {bam_path}/{sample_id}.bam convention.
+ """
+ if sample_id in annotation.bam_path_map:
+ return annotation.bam_path_map[sample_id]
+ return os.path.join(config.bam_path, f"{sample_id}.bam")
+
def make_comparisons():
if not os.path.exists("samples.tab"):
return
annotation.comparisons = []
annotation.treatments = {}
+ annotation.bam_path_map = {}
samples = set()
+ bam_column = getattr(config, "bam_column", "bam_file")
f = open("samples.tab")
r = f.readline()
while r.startswith("#"):
@@ -80,6 +93,8 @@ def make_comparisons():
treatment_id = data[config.treatment_column]
separates.add(data.get(config.separate_column, ""))
annotation.treatments.setdefault(treatment_id, []).append((sample_id, treatment_id, data.get(config.separate_column, ""), data.get(config.group_column, "")))
+ if bam_column in header and data.get(bam_column, "").strip():
+ annotation.bam_path_map[sample_id] = data[bam_column].strip()
r = f.readline()
f.close()
for treatment, data in annotation.treatments.items(): # sort by sample ID
@@ -173,7 +188,7 @@ def write_comparisons():
for rtype, rfile in [("control", control_ids), ("test", test_ids)]:
bams = []
for sample_id in rfile:
- bam_fname = os.path.abspath(os.path.join(f"{splicekit.config.bam_path}", f"{sample_id}.bam"))
+ bam_fname = os.path.abspath(get_bam_path(sample_id))
bams.append(bam_fname)
f_rmats = open(f"results/rmats/{comparison_name}_{rtype}.tab", "wt")
f_rmats.write(",".join(bams))
@@ -454,7 +469,7 @@ def bam_count():
while r:
r = r.replace("\r", "").replace("\n", "").split("\t")
data = dict(zip(header, r))
- bam_fname = os.path.join(config.bam_path, data[config.sample_column]+".bam")
+ bam_fname = get_bam_path(data[config.sample_column])
output = subprocess.check_output(f"samtools view -@ 4 -c {bam_fname}", shell=True)
count = int(output.decode().split("\n")[0])
print(f"splicekit | bam read counts: {data[splicekit.config.sample_column]}, bam file {bam_fname}, counts = {count}")
diff --git a/splicekit/core/exons.py b/splicekit/core/exons.py
index 330d76f..a62e75e 100644
--- a/splicekit/core/exons.py
+++ b/splicekit/core/exons.py
@@ -5,6 +5,7 @@
import sys
import splicekit.config as config
import splicekit.core as core
+import splicekit.core.annotation as annotation
import gzip
def write_exons_gtf():
@@ -61,7 +62,6 @@ def write_jobs_featureCounts(library_type='single-end', library_strand='NONE'):
library_strand_insert = {"FIRST_READ_TRANSCRIPTION_STRAND":1, "SINGLE_STRAND":1, "SINGLE_REVERSE":1, "SECOND_READ_TRANSCRIPTION_STRAND":2, "NONE":0}[library_strand]
gtf_fname = f"reference/exons.gtf.gz"
- bam_dir = f"{config.bam_path}" # files inside end with .bam
out_dir = f'data/sample_exons_data'
jobs_dir = f'jobs/count_exons'
logs_dir = f'logs/count_exons'
@@ -124,14 +124,13 @@ def write_jobs_featureCounts(library_type='single-end', library_strand='NONE'):
gzip -f {out_fname}
"""
- bam_files = [fi for fi in os.listdir(bam_dir) if fi.endswith('.bam')]
- sample_ids = [fi.replace('.bam', '') for fi in bam_files]
+ sample_ids = annotation.samples
header_line = '\t'.join(['exon_id', 'count'])
fsh = open(f"{jobs_dir}/process.sh", "wt")
for sample in sample_ids:
out_fname = f"{out_dir}/sample_{sample}.tab"
job_fname = f'{jobs_dir}/exons_{sample}.job'
- sam_fname = f"{bam_dir}/{sample}.bam"
+ sam_fname = annotation.get_bam_path(sample)
# cluster job
job_out = job_exons.format(container=config.container, library_type_insert=library_type_insert, library_strand_insert=library_strand_insert, gtf_fname=gtf_fname, sample_id=sample, sam_fname=sam_fname, out_fname=out_fname, logs_dir=logs_dir, header_line=header_line)
job_file = open(job_fname, "w")
diff --git a/splicekit/core/genes.py b/splicekit/core/genes.py
index 24d97b7..91fe71e 100644
--- a/splicekit/core/genes.py
+++ b/splicekit/core/genes.py
@@ -6,6 +6,7 @@
import sys
import splicekit.core as core
import splicekit.config as config
+import splicekit.core.annotation as annotation
import gzip
import re
@@ -75,7 +76,6 @@ def write_jobs_featureCounts(library_type='single-end', library_strand='NONE'):
library_strand_insert = {"FIRST_READ_TRANSCRIPTION_STRAND":1, "SINGLE_STRAND":1, "SINGLE_REVERSE":1, "SECOND_READ_TRANSCRIPTION_STRAND":2, "NONE":0}[library_strand]
gtf_fname = f"reference/genes.gtf.gz"
- bam_dir = f"{config.bam_path}" # files inside end with .bam
out_dir = f"data/sample_genes_data"
jobs_dir = f"jobs/count_genes"
logs_dir = f"logs/count_genes"
@@ -139,14 +139,13 @@ def write_jobs_featureCounts(library_type='single-end', library_strand='NONE'):
gzip -f {out_fname}
"""
- bam_files = [fi for fi in os.listdir(bam_dir) if fi.endswith('.bam')]
- sample_ids = [fi.replace('.bam', '') for fi in bam_files]
+ sample_ids = annotation.samples
header_line = '\t'.join(['gene_id', 'count'])
fsh = open(f"{jobs_dir}/process.sh", "wt")
for sample in sample_ids:
out_fname = f"{out_dir}/sample_{sample}.tab"
job_fname = f'{jobs_dir}/genes_{sample}.job'
- sam_fname = f"{bam_dir}/{sample}.bam"
+ sam_fname = annotation.get_bam_path(sample)
# cluster job
job_out = job_genes.format(container=config.container, library_type_insert=library_type_insert, library_strand_insert=library_strand_insert, gtf_fname=gtf_fname, sample_id=sample, sam_fname=sam_fname, out_fname=out_fname, logs_dir=logs_dir, header_line=header_line)
job_file = open(job_fname, "w")
diff --git a/splicekit/core/jbrowse2.py b/splicekit/core/jbrowse2.py
index b82ff38..3fa25bd 100644
--- a/splicekit/core/jbrowse2.py
+++ b/splicekit/core/jbrowse2.py
@@ -1,6 +1,7 @@
import os
import splicekit.config as config
import splicekit.core as core
+import splicekit.core.annotation as annotation
import http.server
import socket
import socketserver
@@ -40,6 +41,7 @@ def setup():
try:
bam_files = [fi for fi in os.listdir(bam_dir) if fi.endswith('.bam')]
except:
+ bam_files = []
print(f"{module_desc} no bam files found in provided bam folder")
genome_fa = config.fasta_path
gff_fname = config.gff3_path
@@ -100,10 +102,14 @@ def write_sample_jobs(force_samples):
os.mkdir(dir)
# create jobs
+ # If samples.tab provides per-sample bam paths, use those; otherwise scan bam_dir.
+ if annotation.bam_path_map:
+ sample_bam_pairs = [(sid + ".bam", annotation.get_bam_path(sid)) for sid in annotation.samples]
+ else:
+ sample_bam_pairs = [(fi, bam_dir + '/' + fi) for fi in bam_files]
bamCoverage_binSize= 3
fsh = open(f"jobs/jobs_jbrowse/process.sh", "wt")
- for sample in bam_files:
- bam_fname = bam_dir +'/'+ sample
+ for sample, bam_fname in sample_bam_pairs:
cram_fname = dirs_to_check[2] + '/'+sample.replace('.bam', '.cram')
bigwig_fname = dirs_to_check[2] +'/'+ sample.replace('.bam', '.bw')
if (not (os.path.exists(cram_fname) and os.path.exists(bigwig_fname))) or (force_samples == True):
diff --git a/splicekit/core/junctions.py b/splicekit/core/junctions.py
index 6a06a99..cd8a908 100644
--- a/splicekit/core/junctions.py
+++ b/splicekit/core/junctions.py
@@ -135,7 +135,7 @@ def make_jobs():
for sample_id in splicekit.core.annotation.samples:
core_path = os.path.dirname(splicekit.core.__file__)
- bam_fname = f"{splicekit.config.bam_path}/{sample_id}.bam"
+ bam_fname = splicekit.core.annotation.get_bam_path(sample_id)
f = open("jobs/count_junctions/sample_{sample_id}.job".format(sample_id=sample_id), "wt")
f.write(job_junctions.format(sample_id=sample_id, core_path=core_path, bam_fname=bam_fname, job_name="count_junctions_{sample_id}".format(sample_id=sample_id)))
f.close()
diff --git a/splicekit/version b/splicekit/version
index aec258d..6f4eebd 100644
--- a/splicekit/version
+++ b/splicekit/version
@@ -1 +1 @@
-0.8
+0.8.1