From 80e9eb46f631083247880fba380a6460ef40ea3b Mon Sep 17 00:00:00 2001 From: Gregor Rot Date: Wed, 15 Jul 2026 15:23:50 +0200 Subject: [PATCH 1/2] What changed (6 files): MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit annotation.py — added get_bam_path(sample_id) helper; make_comparisons() now reads the bam_file column from samples.tab into bam_path_map; write_comparisons() and bam_count() use get_bam_path() config/__init__.py — adds default bam_column = "bam_file" __init__.py — initialises bam_path_map = {} junctions.py, exons.py, genes.py, anchors.py — use annotation.get_bam_path(sample_id) instead of {bam_path}/{sample_id}.bam; exons/genes/anchors now iterate annotation.samples instead of scanning the directory jbrowse2.py — write_sample_jobs() uses per-sample paths when bam_path_map is populated How to use it — just add a bam_file column to samples.tab: sample_id treatment_id bam_file sample1 control data/samples/gregor@gmail.com/sample1/sample1.bam sample2 test data/samples/gregor@gmail.com/sample2/sample2.bam Existing samples.tab files without the column continue to work exactly as before using bam_path from splicekit.config. --- splicekit/__init__.py | 1 + splicekit/config/__init__.py | 7 ++++++- splicekit/core/anchors.py | 7 +++---- splicekit/core/annotation.py | 19 +++++++++++++++++-- splicekit/core/exons.py | 7 +++---- splicekit/core/genes.py | 7 +++---- splicekit/core/jbrowse2.py | 10 ++++++++-- splicekit/core/junctions.py | 2 +- 8 files changed, 42 insertions(+), 18 deletions(-) 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() From e156f761568e30daf3ccd93400d9169fef4ed43a Mon Sep 17 00:00:00 2001 From: Gregor Rot Date: Thu, 16 Jul 2026 15:48:17 +0200 Subject: [PATCH 2/2] 0.8.1 --- README.md | 8 ++++++++ splicekit/version | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) 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/version b/splicekit/version index aec258d..6f4eebd 100644 --- a/splicekit/version +++ b/splicekit/version @@ -1 +1 @@ -0.8 +0.8.1