Source code for finaletoolkit.utils._agg_bw

"""
Aggregate a bigWig signal across strand-oriented intervals.
"""
from __future__ import annotations

import gzip
import time
from os import PathLike
from sys import stderr
from typing import Union

import numpy as np
import pyBigWig as pbw

__all__ = ["agg_bw"]


[docs] def agg_bw( input_file: Union[str, PathLike], interval_file: Union[str, PathLike], output_file: Union[str, PathLike], median_window_size: int = 1, mean: bool = False, verbose: bool = False, ) -> np.ndarray: """Aggregate bigWig signal over BED intervals (strand-aware). The median filter used by ``adjust-wps`` trims each interval by half the window size. Account for that either by supplying smaller intervals or by passing the original ``median_window_size`` here (do not do both). Parameters ---------- input_file : str or path bigWig of per-base signal. interval_file : str or path BED of intervals; column 6 must contain the strand. output_file : str or path Output WIG path. median_window_size : int, optional Filter window used upstream (default 1 = no trimming; 120 replicates Snyder et al.). mean : bool, optional Divide the aggregate by the number of intervals (mean instead of sum). verbose : bool, optional Print progress/timing. Returns ------- numpy.ndarray The aggregated per-position signal. Raises ------ ValueError If ``interval_file`` is not BED or ``output_file`` is not ``.wig``. """ if verbose: start_time = time.time() stderr.write("Reading intervals from bed...\n") if not ( str(interval_file).endswith(".bed") or str(interval_file).endswith(".bed.gz") ): raise ValueError("Invalid filetype for interval_file.") intervals = [] opener = gzip.open if str(interval_file).endswith(".gz") else open with opener(interval_file, "rt") as file: for line in file: contents = line.split("\t") contig = contents[0] start = int(contents[1]) stop = int(contents[2]) strand = contents[5] intervals.append((contig, int(start), int(stop), strand.strip())) with pbw.open(str(input_file), "r") as raw_wps: # pbw needs str, not Path # Interval length after trimming by the median-filter window. interval_size = intervals[0][2] - intervals[0][1] - median_window_size agg_scores = np.zeros(interval_size, dtype=np.int64) num_intervals_added = 0 for contig, start, stop, strand in intervals: try: signal = raw_wps.values(contig, start, stop) if signal is None: print( "There was no information found in the interval: ", contig, start, stop, ) continue values = np.nan_to_num(np.array(signal), nan=0) except RuntimeError as e: print(e) continue # Trim the ends removed by the upstream median filter. trimmed = values[median_window_size // 2 : -median_window_size // 2] if trimmed.shape[0] != interval_size: print( f"Trimmed size {trimmed.shape[0]} for {contig}:{start}" f"-{stop} is not equal to " f"interval size {interval_size}. Skipping." ) continue if strand == "+": agg_scores = agg_scores + trimmed num_intervals_added += 1 elif strand == "-": agg_scores = agg_scores + np.flip(trimmed) num_intervals_added += 1 elif verbose: stderr.write( "A segment without strand was encountered. Skipping." ) if mean: agg_scores = agg_scores / num_intervals_added if str(output_file).endswith("wig"): with open(output_file, "wt") as out: if verbose: stderr.write("File opened! Writing...\n") out.write( f"fixedStep\tchrom=.\tstart={-interval_size // 2}\tstep={1}\t" f"span={interval_size}\n" ) for score in agg_scores: out.write(f"{score}\n") else: raise ValueError( "The output_file is an unaccepted type. Must be a wiggle file " "ending in .wig" ) if verbose: end_time = time.time() stderr.write( f"Aggregating bigWig took {end_time - start_time} s to run.\n" ) return agg_scores