diff --git a/bedboss/bbuploader/main.py b/bedboss/bbuploader/main.py index b177190..fb054d8 100644 --- a/bedboss/bbuploader/main.py +++ b/bedboss/bbuploader/main.py @@ -45,7 +45,7 @@ run_initial_qc, ) from bedboss.utils import standardize_pep as pep_standardizer -from bedboss.bedstat.r_service import RServiceManager +from bedboss.bedstat.backends import StatBackend, build_backend from bedboss._version import __version__ _LOGGER = logging.getLogger(PKG_NAME) @@ -140,11 +140,11 @@ def upload_all( ) if not lite: - _LOGGER.info("Initializing R service for statistics") - r_service = RServiceManager() + _LOGGER.info("Initializing stats backend") + stat_backend = build_backend(bbagent.config.config.analysis.backend) else: - _LOGGER.info("Lite mode: R service disabled") - r_service = None + _LOGGER.info("Lite mode: stats backend disabled") + stat_backend = None for gse_pep in pep_annotation_list.results: count += 1 @@ -205,7 +205,7 @@ def upload_all( overwrite=overwrite, overwrite_bedset=overwrite_bedset, lite=lite, - r_service=r_service, + backend=stat_backend, pm=pm, ) except Exception as err: @@ -224,6 +224,8 @@ def upload_all( break pm.stop_pipeline() + if stat_backend is not None: + stat_backend.cleanup() return None @@ -532,7 +534,7 @@ def _upload_gse( preload: bool = True, lite=False, max_file_size: int = 20 * 1000000, - r_service: RServiceManager = None, + backend: StatBackend = None, pm: pypiper.PipelineManager = None, ) -> ProjectProcessingStatus: """ @@ -557,7 +559,9 @@ def _upload_gse( :param lite: lite mode, where skipping statistic processing for memory optimization and time saving :param max_file_size: maximum file size in bytes. Default: 20MB :param pypiper.PipelineManager pm: pypiper object - :param r_service: RServiceManager object + :param StatBackend backend: pre-built stats backend (reused across all samples + in the GSE). When None, one is built locally — use a pre-built backend + when processing many GSEs so persistent resources are amortized. :return: None """ if isinstance(bedbase_config, str): @@ -607,12 +611,12 @@ def _upload_gse( else: stop_pipeline = False - if not lite and not r_service: - r_service = RServiceManager() + owns_backend = False + if not lite and backend is None: + backend = build_backend(bedbase_config.config.config.analysis.backend) + owns_backend = True elif lite: - r_service = None - else: - r_service = r_service + backend = None for counter, project_sample in enumerate(project.samples): _LOGGER.info(f">> Processing {counter+1} / {total_sample_number}") @@ -757,7 +761,7 @@ def _upload_gse( force_overwrite=overwrite, lite=lite, pm=pm, - r_service=r_service, + backend=backend, reference_genome_validator=reference_validator, ) _LOGGER.info( @@ -852,6 +856,9 @@ def _upload_gse( if stop_pipeline: pm.stop_pipeline() + if owns_backend and backend is not None: + backend.cleanup() + _LOGGER.info( f"Processing of '{gse_id}' completed: " f"{project_status.number_of_processed}/{project_status.number_of_samples} processed, " diff --git a/bedboss/bedboss.py b/bedboss/bedboss.py index aa693de..4f1b114 100644 --- a/bedboss/bedboss.py +++ b/bedboss/bedboss.py @@ -41,7 +41,7 @@ run_initial_qc, ) from bedboss.utils import standardize_pep as pep_standardizer -from bedboss.bedstat.r_service import RServiceManager +from bedboss.bedstat.backends import StatBackend, build_backend _LOGGER = logging.getLogger(PKG_NAME) @@ -90,7 +90,7 @@ def run_all( universe_method: str = None, universe_bedset: str = None, pm: pypiper.PipelineManager = None, - r_service: RServiceManager = None, + backend: StatBackend = None, reference_genome_validator: ReferenceValidator = None, ) -> str: """ @@ -125,7 +125,11 @@ def run_all( :param str universe_method: method used to create the universe [Default: None] :param str universe_bedset: bedset identifier for the universe [Default: None] :param pypiper.PipelineManager pm: pypiper object - :param RServiceManager r_service: RServiceManager object that will run R services + :param StatBackend backend: pre-built statistics backend instance (reused across + calls by batch orchestrators). When None, one is built from the bbconf + config's ``analysis.backend`` value and cleaned up before return — use + that path for single-file invocations; batch callers should build and + reuse one via :func:`bedboss.bedstat.backends.build_backend`. :param reference_genome_validator: ReferenceValidator object that will validate reference genome compatibility :return str bed_digest: bed digest """ @@ -192,18 +196,27 @@ def run_all( statistics_dict = {} statistics_dict["number_of_regions"] = len(bed_metadata.bed_object) else: - statistics_dict = bedstat( - bedfile=bed_metadata.bed_file, - outfolder=outfolder, - genome=genome, - ensdb=ensdb, - bed_digest=bed_metadata.bed_digest, - open_signal_matrix=open_signal_matrix, - just_db_commit=just_db_commit, - rfg_config=rfg_config, - pm=pm, - r_service=r_service, - ) + # Build a backend for this call if none was passed in (single-file use). + # Batch orchestrators pass a pre-built backend to amortize setup costs. + owns_backend = backend is None + if owns_backend: + backend = build_backend(bbagent.config.config.analysis.backend) + try: + statistics_dict = bedstat( + bedfile=bed_metadata.bed_file, + outfolder=outfolder, + genome=genome, + ensdb=ensdb, + bed_digest=bed_metadata.bed_digest, + open_signal_matrix=open_signal_matrix, + just_db_commit=just_db_commit, + rfg_config=rfg_config, + pm=pm, + backend=backend, + ) + finally: + if owns_backend: + backend.cleanup() if "mean_region_width" not in statistics_dict: statistics_dict["mean_region_width"] = ( @@ -418,10 +431,12 @@ def insert_pep( if rerun: skipper.reinitialize() - if not lite: - r_service = RServiceManager() - else: - r_service = None + # Build the stats backend once for the whole batch. The backend holds + # per-backend resources (persistent R service, gtars reference caches, + # etc.) that should be reused across files. + stat_backend = ( + build_backend(bbagent.config.config.analysis.backend) if not lite else None + ) for i, pep_sample in enumerate(pep.samples): is_processed = skipper.is_processed(pep_sample.sample_name) @@ -475,7 +490,7 @@ def insert_pep( universe_bedset=pep_sample.get("universe_bedset"), lite=lite, pm=pm, - r_service=r_service, + backend=stat_backend, ) processed_ids.append(bed_id) @@ -486,6 +501,9 @@ def insert_pep( failed_samples.append(pep_sample.sample_name) skipper.add_failed(pep_sample.sample_name, f"{e}") + if stat_backend is not None: + stat_backend.cleanup() + if create_bedset: _LOGGER.info(f"Creating bedset from {pep.name}") run_bedbuncher( @@ -554,8 +572,6 @@ def reprocess_all( else: stop_pipeline = False - r_service = RServiceManager() - if isinstance(bedbase_config, str): bbagent = BedBaseAgent(config=bedbase_config) elif isinstance(bedbase_config, bbconf.BedBaseAgent): @@ -563,6 +579,9 @@ def reprocess_all( else: raise BedBossException("Incorrect bedbase_config type. Exiting...") + # Build the stats backend once for the reprocess batch. + stat_backend = build_backend(bbagent.config.config.analysis.backend) + unprocessed_beds = bbagent.bed.get_unprocessed( limit=limit, genome=["hg38", "hg19", "mm10"] ) @@ -601,7 +620,7 @@ def reprocess_all( universe_method=None, universe_bedset=None, pm=pm, - r_service=r_service, + backend=stat_backend, ) except Exception as e: _LOGGER.error(f"Failed to process {bed_annot.name}. See {e}") @@ -615,6 +634,8 @@ def reprocess_all( } ) + stat_backend.cleanup() + if failed_samples: date_now = datetime.datetime.now().strftime("%Y%m%d%H%M%S") with open( diff --git a/bedboss/bedstat/backends/__init__.py b/bedboss/bedstat/backends/__init__.py new file mode 100644 index 0000000..96e7810 --- /dev/null +++ b/bedboss/bedstat/backends/__init__.py @@ -0,0 +1,45 @@ +from bedboss.bedstat.backends.base import StatBackend +from bedboss.bedstat.backends.r_backend import RStatBackend +from bedboss.const import BACKEND_GTARS, BACKEND_R + +__all__ = ["StatBackend", "RStatBackend", "create_backend", "build_backend"] + + +def create_backend(name: str, **kwargs) -> StatBackend: + """Create a statistics computation backend by name. + + Low-level factory: pass backend-specific kwargs directly. Most callers + should use :func:`build_backend` instead, which handles backend-specific + prerequisites (e.g. starting an RServiceManager for the R backend). + + :param name: Backend name (BACKEND_R or BACKEND_GTARS) + :param kwargs: Backend-specific keyword arguments + :return: StatBackend instance + """ + if name == BACKEND_R: + return RStatBackend(**kwargs) + elif name == BACKEND_GTARS: + raise NotImplementedError("gtars backend not yet available. Install via PR 2a.") + else: + raise ValueError(f"Unknown analysis backend: {name!r}. Use 'r' or 'gtars'.") + + +def build_backend(name: str) -> StatBackend: + """Build a backend with any backend-specific prerequisites attached. + + High-level constructor used by batch orchestrators. Handles the lifetime + of backend-internal resources (e.g. starts an RServiceManager for the R + backend so the single R process is reused across all files in a batch). + + Callers are responsible for calling :meth:`StatBackend.cleanup` when + done to release backend-held resources. See `StatBackend` as a context + manager for automatic cleanup. + + :param name: Backend name (BACKEND_R or BACKEND_GTARS) + :return: StatBackend instance ready for batch processing + """ + if name == BACKEND_R: + from bedboss.bedstat.r_service import RServiceManager + + return create_backend(name, r_service=RServiceManager()) + return create_backend(name) diff --git a/bedboss/bedstat/backends/base.py b/bedboss/bedstat/backends/base.py new file mode 100644 index 0000000..1d40581 --- /dev/null +++ b/bedboss/bedstat/backends/base.py @@ -0,0 +1,40 @@ +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Union + +import pypiper + + +class StatBackend(ABC): + """Interface for BED file statistics computation backends.""" + + @abstractmethod + def compute( + self, + bedfile: str, + genome: str, + outfolder: str, + bed_digest: str = None, + ensdb: str = None, + open_signal_matrix: str = None, + just_db_commit: bool = False, + rfg_config: Union[str, Path] = None, + pm: pypiper.PipelineManager = None, + ) -> dict: + """Compute statistics for a single BED file. + + Returns a dict with at minimum scalar keys matching BedStatsModel + field names. May also include plot dicts and/or a 'distributions' key. + """ + ... + + def cleanup(self): + """Release any resources held by this backend.""" + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.cleanup() + return False diff --git a/bedboss/bedstat/backends/r_backend.py b/bedboss/bedstat/backends/r_backend.py new file mode 100644 index 0000000..808ab45 --- /dev/null +++ b/bedboss/bedstat/backends/r_backend.py @@ -0,0 +1,162 @@ +import json +import logging +import os +import statistics +from pathlib import Path +from typing import Union + +import pypiper +from gtars.models import RegionSet + +from bedboss.bedstat.backends.base import StatBackend +from bedboss.bedstat.gc_content import calculate_gc_content, create_gc_plot +from bedboss.bedstat.r_service import RServiceManager +from bedboss.const import BEDSTAT_OUTPUT, OUTPUT_FOLDER_NAME +from bedboss.exceptions import BedBossException + +_LOGGER = logging.getLogger("bedboss") + + +class RStatBackend(StatBackend): + """R-based statistics backend using regionstat.R.""" + + def __init__(self, r_service: RServiceManager = None, **kwargs): + self._r_service = r_service + + @property + def r_service(self) -> RServiceManager: + return self._r_service + + @r_service.setter + def r_service(self, value: RServiceManager): + self._r_service = value + + def compute( + self, + bedfile: str, + genome: str, + outfolder: str, + bed_digest: str = None, + ensdb: str = None, + open_signal_matrix: str = None, + just_db_commit: bool = False, + rfg_config: Union[str, Path] = None, + pm: pypiper.PipelineManager = None, + ) -> dict: + outfolder_stats = os.path.join(outfolder, OUTPUT_FOLDER_NAME, BEDSTAT_OUTPUT) + os.makedirs(outfolder_stats, exist_ok=True) + + bed_object = RegionSet(bedfile) + if not bed_digest: + bed_digest = bed_object.identifier + + outfolder_stats_results = os.path.abspath( + os.path.join(outfolder_stats, bed_digest) + ) + os.makedirs(outfolder_stats_results, exist_ok=True) + + json_file_path = os.path.abspath( + os.path.join(outfolder_stats_results, bed_digest + ".json") + ) + json_plots_file_path = os.path.abspath( + os.path.join(outfolder_stats_results, bed_digest + "_plots.json") + ) + + # Used to stop pipeline if bedstat is used independently + stop_pipeline = not pm + + if not just_db_commit: + if not pm: + pm_out_path = os.path.abspath( + os.path.join(outfolder_stats, "pypiper", bed_digest) + ) + os.makedirs(pm_out_path, exist_ok=True) + pm = pypiper.PipelineManager( + name="bedstat-pipeline", + outfolder=pm_out_path, + pipestat_sample_name=bed_digest, + ) + + rscript_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "tools", + "regionstat_cli.R", + ) + assert os.path.exists(rscript_path), FileNotFoundError( + f"'{rscript_path}' script not found" + ) + + if not self._r_service: + try: + _LOGGER.info("#=>>> Running local R instance!") + command = ( + f"Rscript {rscript_path} --bedfilePath={bedfile} " + f"--openSignalMatrix={open_signal_matrix} " + f"--outputFolder={outfolder_stats_results} --genome={genome} " + f"--ensdb={ensdb} --digest={bed_digest}" + ) + pm.run(cmd=command, target=json_file_path) + except Exception as e: + _LOGGER.error(f"Pipeline failed: {e}") + raise BedBossException(f"Pipeline failed: {e}") + else: + _LOGGER.info("#=>>> Running R service ") + self._r_service.run_file( + file_path=bedfile, + digest=bed_digest, + outpath=outfolder_stats_results, + genome=genome, + openSignalMatrix=open_signal_matrix, + gtffile=ensdb, + ) + + data = {} + if os.path.exists(json_file_path): + with open(json_file_path, "r", encoding="utf-8") as f: + data = json.loads(f.read()) + if os.path.exists(json_plots_file_path): + with open(json_plots_file_path, "r", encoding="utf-8") as f_plots: + plots = json.loads(f_plots.read()) + else: + plots = [] + + # unlist the data, since the output of regionstat.R is a dict of lists of + # length 1 and force keys to lower to correspond with the + # postgres column identifiers + data = {k.lower(): v[0] if isinstance(v, list) else v for k, v in data.items()} + + try: + gc_contents = calculate_gc_content( + bedfile=bed_object, genome=genome, rfg_config=rfg_config + ) + except BaseException: + gc_contents = None + + if gc_contents: + gc_mean = statistics.mean(gc_contents) + data["gc_content"] = round(gc_mean, 2) + gc_plot = create_gc_plot( + bed_id=bed_digest, + gc_contents=gc_contents, + outfolder=os.path.join(outfolder_stats_results), + gc_mean=gc_mean, + ) + plots.append(gc_plot) + + for plot in plots: + plot_id = plot["name"] + data.update({plot_id: plot}) + + if "md5sum" in data: + del data["md5sum"] + if "name" in data: + del data["name"] + + if stop_pipeline: + pm.stop_pipeline() + + return data + + def cleanup(self): + if self._r_service: + self._r_service.terminate_service() diff --git a/bedboss/bedstat/bedstat.py b/bedboss/bedstat/bedstat.py index c694348..1565b27 100755 --- a/bedboss/bedstat/bedstat.py +++ b/bedboss/bedstat/bedstat.py @@ -1,27 +1,21 @@ -import json import logging import os -import statistics from pathlib import Path from typing import Union import pypiper -from gtars.models import RegionSet -from bedboss.bedstat.gc_content import calculate_gc_content, create_gc_plot +from bedboss.bedstat.backends import StatBackend from bedboss.const import ( - BEDSTAT_OUTPUT, HOME_PATH, OPEN_SIGNAL_FOLDER_NAME, OPEN_SIGNAL_URL, OS_HG19, OS_HG38, OS_MM10, - OUTPUT_FOLDER_NAME, ) -from bedboss.exceptions import BedBossException, OpenSignalMatrixException +from bedboss.exceptions import OpenSignalMatrixException from bedboss.utils import download_file -from bedboss.bedstat.r_service import RServiceManager _LOGGER = logging.getLogger("bedboss") @@ -38,7 +32,6 @@ def get_osm_path(genome: str, out_path: str = None) -> Union[str, None]: :param out_path: working directory, where osm should be saved. If None, current working directory will be used :return: path to the Open Signal Matrix """ - # TODO: add more osm _LOGGER.info("Getting Open Signal Matrix file path...") if genome == "hg19" or genome == "GRCh37": osm_name = OS_HG19 @@ -71,41 +64,39 @@ def bedstat( bedfile: str, genome: str, outfolder: str, + backend: StatBackend, bed_digest: str = None, ensdb: str = None, open_signal_matrix: str = None, just_db_commit: bool = False, rfg_config: Union[str, Path] = None, pm: pypiper.PipelineManager = None, - r_service: RServiceManager = None, ) -> dict: """ - Run bedstat pipeline - pipeline for obtaining statistics about bed files - and inserting them into the database + Run bedstat pipeline — compute statistics for a BED file using the + provided analysis backend. + + The backend is passed in as an instance so callers can reuse it across + many files (amortizing R service startup, FASTA loads, etc.) without + this function managing backend lifetime. Build one via + :func:`bedboss.bedstat.backends.build_backend` at the top of a batch + and call :meth:`StatBackend.cleanup` (or use the backend as a context + manager) when done. :param str bedfile: the full path to the bed file to process + :param str genome: genome assembly of the sample + :param str outfolder: The folder for storing the pipeline results. + :param StatBackend backend: statistics backend instance (reused across + calls; lifetime managed by the caller) :param str bed_digest: the digest of the bed file. Defaults to None. + :param str ensdb: a full path to the ensdb gtf file :param str open_signal_matrix: a full path to the openSignalMatrix - required for the tissue specificity plots - :param str outfolder: The folder for storing the pipeline results. - :param str genome: genome assembly of the sample - :param bool just_db_commit: if True, the pipeline will only commit to the database + :param bool just_db_commit: if True, skip computation and just read existing results :param str rfg_config: path to the refgenie config file - :param str ensdb: a full path to the ensdb gtf file required for genomes - not in GDdata :param pm: pypiper object - :param r_service: RServiceManager object - :return: dict with statistics and plots metadata """ - outfolder_stats = os.path.join(outfolder, OUTPUT_FOLDER_NAME, BEDSTAT_OUTPUT) - try: - os.makedirs(outfolder_stats) - except FileExistsError: - pass - - # TODO: osm commented to speed up code - # find/download open signal matrix + # Resolve open signal matrix if not open_signal_matrix or not os.path.exists(open_signal_matrix): try: open_signal_matrix = get_osm_path(genome) @@ -113,124 +104,15 @@ def bedstat( _LOGGER.warning( f"Open Signal Matrix was not found for {genome}. Skipping..." ) - # open_signal_matrix = None - - # Used to stop pipeline bedstat is used independently - if not pm: - stop_pipeline = True - else: - stop_pipeline = False - - bed_object = RegionSet(bedfile) - if not bed_digest: - bed_digest = bed_object.identifier - - outfolder_stats_results = os.path.abspath(os.path.join(outfolder_stats, bed_digest)) - try: - os.makedirs(outfolder_stats_results) - except FileExistsError: - pass - json_file_path = os.path.abspath( - os.path.join(outfolder_stats_results, bed_digest + ".json") - ) - json_plots_file_path = os.path.abspath( - os.path.join(outfolder_stats_results, bed_digest + "_plots.json") + return backend.compute( + bedfile=bedfile, + genome=genome, + outfolder=outfolder, + bed_digest=bed_digest, + ensdb=ensdb, + open_signal_matrix=open_signal_matrix, + just_db_commit=just_db_commit, + rfg_config=rfg_config, + pm=pm, ) - if not just_db_commit: - if not pm: - pm_out_path = os.path.abspath( - os.path.join(outfolder_stats, "pypiper", bed_digest) - ) - try: - os.makedirs(pm_out_path) - except FileExistsError: - pass - pm = pypiper.PipelineManager( - name="bedstat-pipeline", - outfolder=pm_out_path, - pipestat_sample_name=bed_digest, - ) - - rscript_path = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - "bedstat", - "tools", - "regionstat_cli.R", - ) - assert os.path.exists(rscript_path), FileNotFoundError( - f"'{rscript_path}' script not found" - ) - - if not r_service: - try: - _LOGGER.info("#=>>> Running local R instance!") - command = ( - f"Rscript {rscript_path} --bedfilePath={bedfile} " - f"--openSignalMatrix={open_signal_matrix} " - f"--outputFolder={outfolder_stats_results} --genome={genome} " - f"--ensdb={ensdb} --digest={bed_digest}" - ) - pm.run(cmd=command, target=json_file_path) - except Exception as e: - _LOGGER.error(f"Pipeline failed: {e}") - raise BedBossException(f"Pipeline failed: {e}") - else: - _LOGGER.info("#=>>> Running R service ") - r_service.run_file( - file_path=bedfile, - digest=bed_digest, - outpath=outfolder_stats_results, - genome=genome, - openSignalMatrix=open_signal_matrix, - gtffile=ensdb, - ) - - data = {} - if os.path.exists(json_file_path): - with open(json_file_path, "r", encoding="utf-8") as f: - data = json.loads(f.read()) - if os.path.exists(json_plots_file_path): - with open(json_plots_file_path, "r", encoding="utf-8") as f_plots: - plots = json.loads(f_plots.read()) - else: - plots = [] - - # unlist the data, since the output of regionstat.R is a dict of lists of - # length 1 and force keys to lower to correspond with the - # postgres column identifiers - data = {k.lower(): v[0] if isinstance(v, list) else v for k, v in data.items()} - try: - gc_contents = calculate_gc_content( - bedfile=bed_object, genome=genome, rfg_config=rfg_config - ) - except BaseException as e: - gc_contents = None - - if gc_contents: - gc_mean = statistics.mean(gc_contents) - - data["gc_content"] = round(gc_mean, 2) - - gc_plot = create_gc_plot( - bed_id=bed_digest, - gc_contents=gc_contents, - outfolder=os.path.join(outfolder_stats_results), - gc_mean=gc_mean, - ) - plots.append(gc_plot) - - for plot in plots: - plot_id = plot["name"] - data.update({plot_id: plot}) - - if "md5sum" in data: - del data["md5sum"] - - if "name" in data: - del data["name"] - - if stop_pipeline: - pm.stop_pipeline() - - return data diff --git a/bedboss/cli.py b/bedboss/cli.py index e899ca4..e9ee7c2 100644 --- a/bedboss/cli.py +++ b/bedboss/cli.py @@ -394,22 +394,30 @@ def run_stats( None, help="Path to the open signal matrix file" ), just_db_commit: bool = typer.Option(False, help="Just commit to the database?"), + backend: str = typer.Option( + "r", help="Analysis backend ('r' or 'gtars'). Default: 'r'." + ), # PipelineManager multi: bool = typer.Option(False, help="Run multiple samples"), recover: bool = typer.Option(True, help="Recover from previous run"), dirty: bool = typer.Option(False, help="Run without removing existing files"), ): from bedboss.bedstat.bedstat import bedstat - - bedstat( - bedfile=bed_file, - genome=genome, - outfolder=outfolder, - ensdb=ensdb, - open_signal_matrix=open_signal_matrix, - just_db_commit=just_db_commit, - pm=create_pm(outfolder=outfolder, multi=multi, recover=recover, dirty=dirty), - ) + from bedboss.bedstat.backends import build_backend + + with build_backend(backend) as backend_obj: + bedstat( + bedfile=bed_file, + genome=genome, + outfolder=outfolder, + backend=backend_obj, + ensdb=ensdb, + open_signal_matrix=open_signal_matrix, + just_db_commit=just_db_commit, + pm=create_pm( + outfolder=outfolder, multi=multi, recover=recover, dirty=dirty + ), + ) @app.command( diff --git a/bedboss/const.py b/bedboss/const.py index 115cf03..4cc6b1a 100644 --- a/bedboss/const.py +++ b/bedboss/const.py @@ -28,6 +28,8 @@ MIN_REGION_WIDTH = 10 # bedstat +BACKEND_R = "r" +BACKEND_GTARS = "gtars" # bedbuncher DEFAULT_BEDBASE_CACHE_PATH = "./bedabse_cache"