From a3420fd7b722d1ff14ffe7c588ae2225a37f2763 Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Sun, 6 Aug 2023 01:12:39 -0400 Subject: [PATCH 01/19] Update to v0.3.0 (#237) * Checkpointing, more input formats, HTML report, test suite, documentation, new posterior and estimators, fix readthedocs, improve prior heuristics, Github actions for tests, address memory issues --- .dockerignore | 10 + .dockstore.yml | 6 + .github/workflows/miniwdl_check.yml | 17 + .github/workflows/run_packaging_check.yml | 45 + .github/workflows/run_pytest.yml | 32 + .gitignore | 6 + MANIFEST.in | 5 +- README.rst | 138 +- REQUIREMENTS-DOCKER.txt | 9 - REQUIREMENTS.txt | 13 - build_docker_local.sh | 8 + build_docker_release.sh | 10 + cellbender/__init__.py | 1 + cellbender/base_cli.py | 40 +- cellbender/monitor.py | 41 + cellbender/remove_background/argparse.py | 166 -- cellbender/remove_background/argparser.py | 284 +++ cellbender/remove_background/checkpoint.py | 368 ++++ cellbender/remove_background/cli.py | 231 +- cellbender/remove_background/consts.py | 68 +- cellbender/remove_background/data/dataprep.py | 106 +- cellbender/remove_background/data/dataset.py | 1763 ++++----------- .../remove_background/data/extras/simulate.py | 1200 +++++++---- cellbender/remove_background/data/io.py | 1207 +++++++++++ cellbender/remove_background/data/priors.py | 391 ++++ .../NegativeBinomialPoissonConvApprox.py | 2 +- cellbender/remove_background/downstream.py | 479 +++++ cellbender/remove_background/estimation.py | 902 ++++++++ cellbender/remove_background/exceptions.py | 13 +- cellbender/remove_background/infer.py | 781 ------- cellbender/remove_background/model.py | 452 ++-- cellbender/remove_background/posterior.py | 1708 +++++++++++++++ cellbender/remove_background/report.ipynb | 133 ++ cellbender/remove_background/report.py | 1888 +++++++++++++++++ cellbender/remove_background/run.py | 779 +++++++ cellbender/remove_background/sparse_utils.py | 101 + .../tests/benchmarking/README.md | 62 + .../tests/benchmarking/benchmark.wdl | 29 + .../tests/benchmarking/benchmark_inputs.json | 19 + .../tests/benchmarking/cuda_check_inputs.json | 4 + .../docker_image_check_cuda_status.wdl | 38 + .../tests/benchmarking/run_benchmark.py | 225 ++ .../run_benchmark_result_tabulation.py | 146 ++ .../benchmarking/run_usual_benchmarks.sh | 74 + .../remove_background/tests/conftest.py | 202 ++ .../tests/mckp_memory_profiling.py | 146 ++ .../tests/miniwdl_check_wdl.sh | 13 + cellbender/remove_background/tests/test.py | 193 -- .../tests/test_checkpoint.py | 626 ++++++ .../remove_background/tests/test_dataprep.py | 101 + .../remove_background/tests/test_dataset.py | 40 + .../tests/test_downstream.py | 119 ++ .../tests/test_estimation.py | 358 ++++ .../remove_background/tests/test_infer.py | 301 +++ .../tests/test_integration.py | 52 + cellbender/remove_background/tests/test_io.py | 223 ++ .../remove_background/tests/test_monitor.py | 21 + .../remove_background/tests/test_posterior.py | 428 ++++ .../tests/test_sparse_utils.py | 203 ++ .../remove_background/tests/test_train.py | 135 ++ cellbender/remove_background/train.py | 362 ++-- cellbender/remove_background/vae/base.py | 164 ++ cellbender/remove_background/vae/decoder.py | 42 +- cellbender/remove_background/vae/encoder.py | 258 ++- docker/Dockerfile | 65 +- docker/DockerfileGit | 35 + .../PCL_rat_A_LA6_learning_curve.png | Bin 0 -> 23433 bytes .../remove_background/bad_learning_curves.png | Bin 0 -> 214054 bytes .../simulated_s6_learning_curve.png | Bin 0 -> 30054 bytes docs/source/_static/theme_overrides.css | 15 - docs/source/changelog/index.rst | 85 + docs/source/citation/index.rst | 13 +- docs/source/conf.py | 7 +- docs/source/contributing/index.rst | 10 +- docs/source/getting_started/index.rst | 11 - .../remove_background/index.rst | 138 -- docs/source/help_and_reference/index.rst | 10 - .../remove_background/index.rst | 81 - docs/source/index.rst | 25 +- docs/source/installation/index.rst | 76 +- docs/source/introduction/index.rst | 20 +- docs/source/reference/index.rst | 188 ++ .../license/index.rst | 0 docs/source/troubleshooting/index.rst | 426 ++++ docs/source/tutorial/index.rst | 224 ++ docs/source/usage/index.rst | 49 +- examples/remove_background/.gitignore | 8 +- .../generate_tiny_10x_dataset.py | 79 + .../generate_tiny_10x_pbmc.py | 103 - readthedocs.yml | 2 +- REQUIREMENTS-RTD.txt => requirements-rtd.txt | 0 requirements.txt | 15 + setup.py | 56 +- wdl/README.rst | 86 +- wdl/cellbender_remove_background.wdl | 309 ++- 95 files changed, 15843 insertions(+), 4280 deletions(-) create mode 100644 .dockerignore create mode 100644 .dockstore.yml create mode 100644 .github/workflows/miniwdl_check.yml create mode 100644 .github/workflows/run_packaging_check.yml create mode 100644 .github/workflows/run_pytest.yml delete mode 100644 REQUIREMENTS-DOCKER.txt delete mode 100644 REQUIREMENTS.txt create mode 100755 build_docker_local.sh create mode 100755 build_docker_release.sh create mode 100644 cellbender/monitor.py delete mode 100644 cellbender/remove_background/argparse.py create mode 100644 cellbender/remove_background/argparser.py create mode 100644 cellbender/remove_background/checkpoint.py create mode 100644 cellbender/remove_background/data/io.py create mode 100644 cellbender/remove_background/data/priors.py create mode 100644 cellbender/remove_background/downstream.py create mode 100644 cellbender/remove_background/estimation.py delete mode 100644 cellbender/remove_background/infer.py create mode 100644 cellbender/remove_background/posterior.py create mode 100644 cellbender/remove_background/report.ipynb create mode 100644 cellbender/remove_background/report.py create mode 100644 cellbender/remove_background/run.py create mode 100644 cellbender/remove_background/sparse_utils.py create mode 100644 cellbender/remove_background/tests/benchmarking/README.md create mode 100644 cellbender/remove_background/tests/benchmarking/benchmark.wdl create mode 100644 cellbender/remove_background/tests/benchmarking/benchmark_inputs.json create mode 100644 cellbender/remove_background/tests/benchmarking/cuda_check_inputs.json create mode 100644 cellbender/remove_background/tests/benchmarking/docker_image_check_cuda_status.wdl create mode 100644 cellbender/remove_background/tests/benchmarking/run_benchmark.py create mode 100644 cellbender/remove_background/tests/benchmarking/run_benchmark_result_tabulation.py create mode 100755 cellbender/remove_background/tests/benchmarking/run_usual_benchmarks.sh create mode 100644 cellbender/remove_background/tests/conftest.py create mode 100644 cellbender/remove_background/tests/mckp_memory_profiling.py create mode 100755 cellbender/remove_background/tests/miniwdl_check_wdl.sh delete mode 100644 cellbender/remove_background/tests/test.py create mode 100644 cellbender/remove_background/tests/test_checkpoint.py create mode 100644 cellbender/remove_background/tests/test_dataprep.py create mode 100644 cellbender/remove_background/tests/test_dataset.py create mode 100644 cellbender/remove_background/tests/test_downstream.py create mode 100644 cellbender/remove_background/tests/test_estimation.py create mode 100644 cellbender/remove_background/tests/test_infer.py create mode 100644 cellbender/remove_background/tests/test_integration.py create mode 100644 cellbender/remove_background/tests/test_io.py create mode 100644 cellbender/remove_background/tests/test_monitor.py create mode 100644 cellbender/remove_background/tests/test_posterior.py create mode 100644 cellbender/remove_background/tests/test_sparse_utils.py create mode 100644 cellbender/remove_background/tests/test_train.py create mode 100644 cellbender/remove_background/vae/base.py create mode 100644 docker/DockerfileGit create mode 100644 docs/source/_static/remove_background/PCL_rat_A_LA6_learning_curve.png create mode 100644 docs/source/_static/remove_background/bad_learning_curves.png create mode 100644 docs/source/_static/remove_background/simulated_s6_learning_curve.png delete mode 100644 docs/source/_static/theme_overrides.css create mode 100644 docs/source/changelog/index.rst delete mode 100644 docs/source/getting_started/index.rst delete mode 100644 docs/source/getting_started/remove_background/index.rst delete mode 100644 docs/source/help_and_reference/index.rst delete mode 100644 docs/source/help_and_reference/remove_background/index.rst create mode 100644 docs/source/reference/index.rst rename docs/source/{help_and_reference => reference}/license/index.rst (100%) create mode 100644 docs/source/troubleshooting/index.rst create mode 100644 docs/source/tutorial/index.rst create mode 100755 examples/remove_background/generate_tiny_10x_dataset.py delete mode 100755 examples/remove_background/generate_tiny_10x_pbmc.py rename REQUIREMENTS-RTD.txt => requirements-rtd.txt (100%) create mode 100644 requirements.txt diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..186f305 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +**/.git +.github +examples +docs +wdl +dist +cellbender.egg-info +.pytest_cache +*.h5 +*.tar.gz \ No newline at end of file diff --git a/.dockstore.yml b/.dockstore.yml new file mode 100644 index 0000000..0f2192a --- /dev/null +++ b/.dockstore.yml @@ -0,0 +1,6 @@ +version: 1.2 + +workflows: + - subclass: WDL + primaryDescriptorPath: /wdl/cellbender_remove_background.wdl + name: cellbender_remove_background diff --git a/.github/workflows/miniwdl_check.yml b/.github/workflows/miniwdl_check.yml new file mode 100644 index 0000000..8a8de2c --- /dev/null +++ b/.github/workflows/miniwdl_check.yml @@ -0,0 +1,17 @@ +name: 'validate WDL' +on: [pull_request] +env: + MINIWDL_VERSION: 1.8.0 +jobs: + miniwdl-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.7' + - name: 'Install miniwdl and Run Validation Test' + run: | + pip install miniwdl==$MINIWDL_VERSION; + ./cellbender/remove_background/tests/miniwdl_check_wdl.sh; + shell: bash diff --git a/.github/workflows/run_packaging_check.yml b/.github/workflows/run_packaging_check.yml new file mode 100644 index 0000000..8a9fc1c --- /dev/null +++ b/.github/workflows/run_packaging_check.yml @@ -0,0 +1,45 @@ +# Package for PyPI + +name: 'packaging' + +on: pull_request + +jobs: + build: + + runs-on: 'ubuntu-latest' + strategy: + matrix: + python-version: ['3.7'] + + steps: + - name: 'Checkout repo' + uses: actions/checkout@v3 + + - name: 'Set up Python ${{ matrix.python-version }}' + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: 'Install packaging software' + run: pip install --upgrade setuptools build twine + + - name: 'Print package versions' + run: pip list + + - name: 'Package code using build' + run: python -m build + + - name: 'Check package using twine' + run: python -m twine check dist/* + + - name: 'Extract branch name' + shell: bash + run: echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >>$GITHUB_OUTPUT + id: extract_branch + + - name: 'Install from github branch and run a test' + run: | + pip install pytest git+https://github.com/broadinstitute/CellBender@${{ steps.extract_branch.outputs.branch }} + cellbender -v diff --git a/.github/workflows/run_pytest.yml b/.github/workflows/run_pytest.yml new file mode 100644 index 0000000..37e652b --- /dev/null +++ b/.github/workflows/run_pytest.yml @@ -0,0 +1,32 @@ +# Run cellbender's tests + +name: 'pytest' + +on: pull_request + +jobs: + build: + + runs-on: 'ubuntu-latest' + strategy: + matrix: + python-version: ['3.7'] + + steps: + - name: 'Checkout repo' + uses: actions/checkout@v3 + + - name: 'Set up Python ${{ matrix.python-version }}' + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: 'Install package including pytest' + run: pip install .[dev] + + - name: 'Print package versions' + run: pip list + + - name: 'Test with pytest' + run: pytest -v diff --git a/.gitignore b/.gitignore index b1e889a..80b6c0c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,9 @@ dist/ .eggs/ .idea/ *.iml +*.h5 +*.h5ad +*.tsv +*.csv +*.npz +*.tar.gz diff --git a/MANIFEST.in b/MANIFEST.in index c4bf456..66e89a1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,4 @@ -include README.rst \ No newline at end of file +include README.rst +include LICENSE +include requirements.txt +include requirements-rtd.txt \ No newline at end of file diff --git a/README.rst b/README.rst index a12cd22..ea39e2c 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,27 @@ CellBender ========== +.. image:: https://img.shields.io/github/license/broadinstitute/CellBender?color=white + :target: LICENSE + :alt: License + .. image:: https://readthedocs.org/projects/cellbender/badge/?version=latest :target: https://cellbender.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status -.. image:: https://github.com/broadinstitute/CellBender/blob/master/docs/source/_static/design/logo_250_185.png +.. image:: https://img.shields.io/pypi/v/CellBender.svg + :target: https://pypi.org/project/CellBender + :alt: PyPI + +.. image:: https://static.pepy.tech/personalized-badge/cellbender?period=total&units=international_system&left_color=grey&right_color=blue&left_text=pypi%20downloads + :target: https://pepy.tech/project/CellBender + :alt: Downloads + +.. image:: https://img.shields.io/github/stars/broadinstitute/CellBender?color=yellow&logoColor=yellow) + :target: https://github.com/broadinstitute/CellBender/stargazers + :alt: Stars + +.. image:: docs/source/_static/design/logo_250_185.png :alt: CellBender Logo CellBender is a software package for eliminating technical artifacts from @@ -16,67 +32,123 @@ The current release contains the following modules. More modules will be added i * ``remove-background``: This module removes counts due to ambient RNA molecules and random barcode swapping from (raw) - UMI-based scRNA-seq count matrices. At the moment, only the count matrices produced by the - CellRanger ``count`` pipeline is supported. Support for additional tools and protocols will be - added in the future. A quick start tutorial can be found - `here `_. + UMI-based scRNA-seq count matrices. Also works for snRNA-seq and CITE-seq. -Please refer to the `documentation `_ for a quick start tutorial on using CellBender. +Please refer to `the documentation `_ for a quick start tutorial. Installation and Usage ---------------------- -Manual installation -~~~~~~~~~~~~~~~~~~~ +CellBender can be installed via + +.. code-block:: console + + $ pip install cellbender + +(and we recommend installing in its own ``conda`` environment to prevent +conflicts with other software). + +CellBender is run as a command-line tool, as in + +.. code-block:: console + + (cellbender) $ cellbender remove-background \ + --cuda \ + --input my_raw_count_matrix_file.h5 \ + --output my_cellbender_output_file.h5 + +See `the usage documentation `_ +for details. + + +Using The Official Docker Image +------------------------------- + +A GPU-enabled docker image is available from the Google Container Registry (GCR) as: + +``us.gcr.io/broad-dsde-methods/cellbender:latest`` + +Available image tags track release tags in GitHub, and include ``latest``, +``0.1.0``, ``0.2.0``, ``0.2.1``, ``0.2.2``, and ``0.3.0``. + + +WDL Users +--------- + +A workflow written in the +`workflow description language (WDL) `_ +is available for CellBender remove-background. + +For `Terra `_ users, a workflow called +``cellbender/remove-background`` is +`available from the Broad Methods repository +`_. + +There is also a `version available on Dockstore +`_. -The recommended installation is as follows. Create a conda environment and activate it: -.. code-block:: bash +Advanced installation +--------------------- - $ conda create -n cellbender python=3.7 - $ source activate cellbender +From source for development +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a conda environment and activate it: + +.. code-block:: console + + $ conda create -n cellbender python=3.7 + $ conda activate cellbender Install the `pytables `_ module: -.. code-block:: bash +.. code-block:: console + + (cellbender) $ conda install -c anaconda pytables - (cellbender) $ conda install -c anaconda pytables +Install `pytorch `_ via +`these instructions `_, for example: -Install `pytorch `_ (shown below for CPU; if you have a CUDA-ready GPU, please skip -this part and follow `these `_ instructions instead): +.. code-block:: console -.. code-block:: bash + (cellbender) $ pip install torch - (cellbender) $ conda install pytorch torchvision -c pytorch +and ensure that your installation is appropriate for your hardware (i.e. that +the relevant CUDA drivers get installed and that ``torch.cuda.is_available()`` +returns ``True`` if you have a GPU available. -Clone this repository and install CellBender: +Clone this repository and install CellBender (in editable ``-e`` mode): -.. code-block:: bash +.. code-block:: console + (cellbender) $ git clone https://github.com/broadinstitute/CellBender.git (cellbender) $ pip install -e CellBender -Using The Official Docker Image -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A GPU-enabled docker image is available from the Google Container Registry (GCR) as: +From a specific commit +~~~~~~~~~~~~~~~~~~~~~~ -``us.gcr.io/broad-dsde-methods/cellbender:latest`` +This can be achieved via -Terra Users -~~~~~~~~~~~ +.. code-block:: console -For `Terra `_ users, a `workflow `_ -is available as: + (cellbender) $ pip install --no-cache-dir -U git+https://github.com/broadinstitute/CellBender.git@ -``cellbender/remove-background`` +where ```` must be replaced by any reference to a particular git commit, +such as a tag, a branch name, or a commit sha. Citing CellBender ----------------- If you use CellBender in your research (and we hope you will), please consider -citing `our paper on bioRxiv `_. +citing our paper in Nature Methods: + +Stephen J Fleming, Mark D Chaffin, Alessandro Arduini, Amer-Denis Akkad, +Eric Banks, John C Marioni, Anthony A Phillipakis, Patrick T Ellinor, +and Mehrtash Babadi. Unsupervised removal of systematic background noise from +droplet-based single-cell experiments using CellBender. +`Nature Methods` (in press), 2023. -Stephen J Fleming, John C Marioni, and Mehrtash Babadi. CellBender remove-background: -a deep generative model for unsupervised removal of background noise from scRNA-seq -datasets. bioRxiv 791699; doi: `https://doi.org/10.1101/791699 `_ +See also `our preprint on bioRxiv `_. diff --git a/REQUIREMENTS-DOCKER.txt b/REQUIREMENTS-DOCKER.txt deleted file mode 100644 index 3884f08..0000000 --- a/REQUIREMENTS-DOCKER.txt +++ /dev/null @@ -1,9 +0,0 @@ -anndata>=0.7 -numpy -scipy -tables -pandas -pyro-ppl>=0.3.2 -torch>=1.9.0 -scikit-learn -matplotlib diff --git a/REQUIREMENTS.txt b/REQUIREMENTS.txt deleted file mode 100644 index 535e3e7..0000000 --- a/REQUIREMENTS.txt +++ /dev/null @@ -1,13 +0,0 @@ -anndata>=0.7 -numpy -scipy -tables -pandas -pyro-ppl>=0.3.2 -scikit-learn -matplotlib -sphinx>=2.1 -sphinx-rtd-theme -sphinx-autodoc-typehints -sphinxcontrib-programoutput -sphinx-argparse diff --git a/build_docker_local.sh b/build_docker_local.sh new file mode 100755 index 0000000..c89367f --- /dev/null +++ b/build_docker_local.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +tag=$(cat cellbender/__init__.py | sed -e 's?__version__ = ??' | sed "s/^'\(.*\)'$/\1/") + +docker build \ + -t us.gcr.io/broad-dsde-methods/cellbender:${tag} \ + -f docker/Dockerfile \ + . diff --git a/build_docker_release.sh b/build_docker_release.sh new file mode 100755 index 0000000..ff0511f --- /dev/null +++ b/build_docker_release.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +tag=$(cat cellbender/__init__.py | sed -e 's?__version__ = ??' | sed "s/^'\(.*\)'$/\1/") +release=v${tag} + +docker build \ + -t us.gcr.io/broad-dsde-methods/cellbender:${tag} \ + --build-arg GIT_SHA=${release} \ + -f docker/DockerfileGit \ + . diff --git a/cellbender/__init__.py b/cellbender/__init__.py index e69de29..0404d81 100644 --- a/cellbender/__init__.py +++ b/cellbender/__init__.py @@ -0,0 +1 @@ +__version__ = '0.3.0' diff --git a/cellbender/base_cli.py b/cellbender/base_cli.py index 1489772..bf5a71b 100644 --- a/cellbender/base_cli.py +++ b/cellbender/base_cli.py @@ -5,16 +5,32 @@ """ import sys +import os import argparse from abc import ABC, abstractmethod from typing import Dict import importlib - +import codecs # New tools should be added to this list. TOOL_NAME_LIST = ['remove-background'] +def read(rel_path): + here = os.path.abspath(os.path.dirname(__file__)) + with codecs.open(os.path.join(here, rel_path), 'r') as fp: + return fp.read() + + +def get_version() -> str: + for line in read('__init__.py').splitlines(): + if line.startswith('__version__'): + delim = '"' if '"' in line else "'" + return line.split(delim)[1] + else: + raise RuntimeError("Unable to find version string.") + + class AbstractCLI(ABC): """Abstract class for cellbender command-line interface tools. @@ -29,13 +45,15 @@ def get_name(self) -> str: """Return the command-line name of the tool.""" pass + @staticmethod @abstractmethod - def validate_args(self, parser: argparse): + def validate_args(parser: argparse) -> argparse.Namespace: """Do tool-specific argument validation, returning args.""" pass + @staticmethod @abstractmethod - def run(self, args): + def run(args): """Run the tool using the parsed arguments.""" pass @@ -62,8 +80,12 @@ def get_populated_argparser() -> argparse.ArgumentParser: # Set up argument parser. parser = argparse.ArgumentParser( prog="cellbender", - description="CellBender is a software package for eliminating technical artifacts from high-throughput " - "single-cell RNA sequencing (scRNA-seq) data.") + description="CellBender is a software package for eliminating technical " + "artifacts from high-throughput single-cell RNA sequencing " + "(scRNA-seq) data.") + + # Add the ability to display the version. + parser.add_argument('-v', '--version', action='version', version=get_version()) # Declare the existence of sub-parsers. subparsers = parser.add_subparsers( @@ -72,7 +94,7 @@ def get_populated_argparser() -> argparse.ArgumentParser: dest="tool") for tool_name in TOOL_NAME_LIST: - module_argparse_str_list = ["cellbender", tool_name.replace("-", "_"), "argparse"] + module_argparse_str_list = ["cellbender", tool_name.replace("-", "_"), "argparser"] module_argparse = importlib.import_module('.'.join(module_argparse_str_list)) subparsers = module_argparse.add_subparser_args(subparsers) @@ -103,3 +125,9 @@ def main(): else: parser.print_help() + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/cellbender/monitor.py b/cellbender/monitor.py new file mode 100644 index 0000000..e7dca61 --- /dev/null +++ b/cellbender/monitor.py @@ -0,0 +1,41 @@ +"""Utility functions for hardware monitoring""" + +# Inspiration for the nvidia-smi command comes from here: +# https://pytorch-lightning.readthedocs.io/en/latest/_modules/pytorch_lightning/callbacks/gpu_stats_monitor.html#GPUStatsMonitor +# but here it is stripped down to the absolute minimum + +import torch +import psutil +from psutil._common import bytes2human +import shutil +import subprocess + + +def get_hardware_usage(use_cuda: bool) -> str: + """Get a current snapshot of RAM, CPU, GPU memory, and GPU utilization as a string""" + + mem = psutil.virtual_memory() + + if use_cuda: + # Run nvidia-smi to get GPU utilization + gpu_query = 'utilization.gpu' + format = 'csv,nounits,noheader' + result = subprocess.run( + [shutil.which("nvidia-smi"), f"--query-gpu={gpu_query}", f"--format={format}"], + encoding="utf-8", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, # for backward compatibility with python version 3.6 + check=True, + ) + pct_gpu_util = result.stdout.strip() + gpu_string = (f'Volatile GPU utilization: {pct_gpu_util} %\n' + f'GPU memory reserved: {torch.cuda.memory_reserved() / 1e9} GB\n' + f'GPU memory allocated: {torch.cuda.memory_allocated() / 1e9} GB\n') + else: + gpu_string = '' + + cpu_string = (f'Avg CPU load over past minute: ' + f'{psutil.getloadavg()[0] / psutil.cpu_count() * 100:.1f} %\n' + f'RAM in use: {bytes2human(mem.used)} ({mem.percent} %)') + + return gpu_string + cpu_string diff --git a/cellbender/remove_background/argparse.py b/cellbender/remove_background/argparse.py deleted file mode 100644 index a52a3eb..0000000 --- a/cellbender/remove_background/argparse.py +++ /dev/null @@ -1,166 +0,0 @@ -import argparse -from cellbender.remove_background import consts - - -def add_subparser_args(subparsers: argparse) -> argparse: - """Add tool-specific arguments for remove-background. - - Args: - subparsers: Parser object before addition of arguments specific to - remove-background. - - Returns: - parser: Parser object with additional parameters. - - """ - - subparser = subparsers.add_parser("remove-background", - description="Remove background RNA " - "from count matrix.", - help="Remove background ambient RNA " - "and barcode-swapped reads from " - "a count matrix, producing a " - "new count matrix and " - "determining which barcodes " - "contain real cells.") - - subparser.add_argument("--input", nargs=None, type=str, - dest='input_file', default=None, - required=True, - help="Data file on which to run tool, as either " - "_raw_gene_barcode_matrices_h5.h5 file, or " - "as the path containing the raw " - "matrix.mtx, barcodes.tsv, and genes.tsv " - "files. Supported for outputs of " - "CellRanger v2 and v3.") - subparser.add_argument("--output", nargs=None, type=str, - dest='output_file', default=None, - required=True, - help="Output file location (the path must " - "exist, and the file name must have .h5 " - "extension).") - subparser.add_argument("--expected-cells", nargs=None, type=int, - default=None, - dest="expected_cell_count", - help="Number of cells expected in the dataset " - "(a rough estimate within a factor of 2 " - "is sufficient).") - subparser.add_argument("--total-droplets-included", - nargs=None, type=int, - default=consts.TOTAL_DROPLET_DEFAULT, - dest="total_droplets", - help="The number of droplets from the " - "rank-ordered UMI plot that will be " - "analyzed. The largest 'total_droplets' " - "droplets will have their cell " - "probabilities inferred as an output. (default: %(default)s)") - subparser.add_argument("--model", nargs=None, type=str, - default="full", - choices=["simple", "ambient", "swapping", "full"], - dest="model", - help="Which model is being used for count data. " - " 'simple' does not model either ambient " - "RNA or random barcode swapping (for " - "debugging purposes -- not recommended). " - "'ambient' assumes background RNA is " - "incorporated into droplets. 'swapping' " - "assumes background RNA comes from random " - "barcode swapping. 'full' uses a " - "combined ambient and swapping model. " - "(default: %(default)s)") - subparser.add_argument("--epochs", nargs=None, type=int, default=150, - dest="epochs", - help="Number of epochs to train. (default: %(default)s)") - subparser.add_argument("--cuda", - dest="use_cuda", action="store_true", - help="Including the flag --cuda will run the " - "inference on a GPU.") - subparser.add_argument("--low-count-threshold", type=int, - default=consts.LOW_UMI_CUTOFF, - dest="low_count_threshold", - help="Droplets with UMI counts below this " - "number are completely excluded from the " - "analysis. This can help identify the " - "correct prior for empty droplet counts " - "in the rare case where empty counts " - "are extremely high (over 200). (default: %(default)s)") - subparser.add_argument("--z-dim", type=int, default=100, - dest="z_dim", - help="Dimension of latent variable z. (default: %(default)s)") - subparser.add_argument("--z-layers", nargs="+", type=int, default=[500], - dest="z_hidden_dims", - help="Dimension of hidden layers in the encoder " - "for z. (default: %(default)s)") - subparser.add_argument("--training-fraction", - type=float, nargs=None, - default=consts.TRAINING_FRACTION, - dest="training_fraction", - help="Training detail: the fraction of the " - "data used for training. The rest is never " - "seen by the inference algorithm. Speeds up " - "learning. (default: %(default)s)") - subparser.add_argument("--empty-drop-training-fraction", - type=float, nargs=None, - default=consts.FRACTION_EMPTIES, - dest="fraction_empties", - help="Training detail: the fraction of the " - "training data each epoch " - "that is drawn (randomly sampled) from " - "surely empty droplets. (default: %(default)s)") - subparser.add_argument("--blacklist-genes", nargs="+", type=int, - default=[], - dest="blacklisted_genes", - help="Integer indices of genes to ignore " - "entirely. In the output count matrix, " - "the counts for these genes will be set " - "to zero.") - subparser.add_argument("--fpr", nargs="+", - type=float, default=[0.01], - dest="fpr", - help="Target false positive rate in (0, 1). A false " - "positive is a true signal count that is " - "erroneously removed. More background removal " - "is accompanied by more signal removal " - "at high values of FPR. You can specify " - "multiple values, which will create multiple " - "output files. (default: %(default)s)") - subparser.add_argument("--exclude-antibody-capture", - dest="exclude_antibodies", action="store_true", - help="Including the flag --exclude-antibody-capture " - "will cause remove-background to operate on " - "gene counts only, ignoring other features.") - subparser.add_argument("--learning-rate", nargs=None, - type=float, default=1e-4, - dest="learning_rate", - help="Training detail: lower learning rate for " - "inference. A OneCycle learning rate schedule " - "is used, where the upper learning rate is ten " - "times this value. (For this value, probably " - "do not exceed 1e-3). (default: %(default)s)") - subparser.add_argument("--final-elbo-fail-fraction", type=float, - help="Training is considered to have failed if " - "(best_test_ELBO - final_test_ELBO)/(best_test_DLBO - initial_train_ELBO) > FINAL_ELBO_FAIL_FRACTION." - "(default: do not fail training based on final_training_ELBO)") - subparser.add_argument("--epoch-elbo-fail-fraction", type=float, - help="Training is considered to have failed if " - "(previous_epoch_test_ELBO - current_epoch_test_ELBO)/(previous_epoch_test_ELBO - initial_train_ELBO) > EPOCH_ELBO_FAIL_FRACTION." - "(default: do not fail training based on epoch_training_ELBO)") - subparser.add_argument("--num-training-tries", type=int, default=1, - help="Number of times to attempt to train the model. Each subsequent " - "attempt the learning rate is multiplied by LEARNING_RATE_RETRY_MULT. " - "(default: %(default)s)") - subparser.add_argument("--learning-rate-retry-mult", type=float, default=0.5, - help="Learning rate is multiplied by this amount each time " - "(default: %(default)s)") - - subparser.add_argument("--posterior-batch-size", type=int, default=consts.PROP_POSTERIOR_BATCH_SIZE, - dest="posterior_batch_size", - help="Size of batches when creating the posterior. Reduce this to avoid " - "running out of GPU memory creating the posterior (will be slower). " - "(default: %(default)s)") - subparser.add_argument("--cells-posterior-reg-calc", type=int, default=consts.CELLS_POSTERIOR_REG_CALC, - dest="cells_posterior_reg_calc", - help="Number of cells used to estimate posterior regularization lambda. " - "(default: %(default)s)") - - return subparsers diff --git a/cellbender/remove_background/argparser.py b/cellbender/remove_background/argparser.py new file mode 100644 index 0000000..d53aca3 --- /dev/null +++ b/cellbender/remove_background/argparser.py @@ -0,0 +1,284 @@ +import argparse +from cellbender.remove_background import consts + + +def add_subparser_args(subparsers: argparse) -> argparse: + """Add tool-specific arguments for remove-background. + + Args: + subparsers: Parser object before addition of arguments specific to + remove-background. + + Returns: + parser: Parser object with additional parameters. + + """ + + subparser = subparsers.add_parser("remove-background", + description="Remove background RNA " + "from count matrix.", + help="Remove background ambient RNA " + "and barcode-swapped reads from " + "a count matrix, producing a " + "new count matrix and " + "determining which barcodes " + "contain real cells.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + subparser.add_argument("--input", nargs=None, type=str, + dest='input_file', + required=True, + help="Data file on which to run tool. Data must be " + "un-filtered: it should include empty droplets. " + "The following input formats are supported: " + "CellRanger v2 and v3 (.h5 or the directory that " + "contains the .mtx file), Dropseq " + "DGE (.txt or .txt.gz), BD Rhapsody (.csv or " + ".csv.gz), AnnData (.h5ad), and Loom (.loom).") + subparser.add_argument("--output", nargs=None, type=str, + dest='output_file', + required=True, + help="Output file location (the path must " + "exist, and the file name must have .h5 " + "extension).") + subparser.add_argument("--cuda", + dest="use_cuda", action="store_true", + help="Including the flag --cuda will run the " + "inference on a GPU.") + subparser.add_argument("--checkpoint", nargs=None, type=str, + dest='input_checkpoint_tarball', + required=False, default=consts.CHECKPOINT_FILE_NAME, + help="Checkpoint tarball produced by the same version " + "of CellBender remove-background. If present, " + "and the workflow hashes match, training will " + "restart from this checkpoint.") + subparser.add_argument("--expected-cells", nargs=None, type=int, + default=None, + dest="expected_cell_count", + help="Number of cells expected in the dataset " + "(a rough estimate within a factor of 2 " + "is sufficient).") + subparser.add_argument("--total-droplets-included", + nargs=None, type=int, default=None, + dest="total_droplets", + help="The number of droplets from the " + "rank-ordered UMI plot that will have their " + "cell probabilities inferred as an output. " + "Include the droplets which might contain " + "cells. Droplets beyond TOTAL_DROPLETS_INCLUDED " + "should be 'surely empty' droplets.") + subparser.add_argument("--force-cell-umi-prior", + nargs=None, type=float, default=None, + dest="force_cell_umi_prior", + help="Ignore CellBender's heuristic prior estimation, " + "and use this prior for UMI counts in cells.") + subparser.add_argument("--force-empty-umi-prior", + nargs=None, type=float, default=None, + dest="force_empty_umi_prior", + help="Ignore CellBender's heuristic prior estimation, " + "and use this prior for UMI counts in empty droplets.") + subparser.add_argument("--model", nargs=None, type=str, + default="full", + choices=["naive", "simple", "ambient", "swapping", "full"], + dest="model", + help="Which model is being used for count data. " + "'naive' subtracts the estimated ambient profile." + " 'simple' does not model either ambient " + "RNA or random barcode swapping (for " + "debugging purposes -- not recommended). " + "'ambient' assumes background RNA is " + "incorporated into droplets. 'swapping' " + "assumes background RNA comes from random " + "barcode swapping (via PCR chimeras). 'full' " + "uses a combined ambient and swapping model.") + subparser.add_argument("--epochs", nargs=None, type=int, default=150, + dest="epochs", + help="Number of epochs to train.") + subparser.add_argument("--low-count-threshold", type=int, + default=consts.LOW_UMI_CUTOFF, + dest="low_count_threshold", + help="Droplets with UMI counts below this " + "number are completely excluded from the " + "analysis. This can help identify the " + "correct prior for empty droplet counts " + "in the rare case where empty counts " + "are extremely high (over 200).") + subparser.add_argument("--z-dim", type=int, default=64, + dest="z_dim", + help="Dimension of latent variable z.") + subparser.add_argument("--z-layers", nargs="+", type=int, default=[512], + dest="z_hidden_dims", + help="Dimension of hidden layers in the encoder for z.") + subparser.add_argument("--training-fraction", + type=float, nargs=None, + default=consts.TRAINING_FRACTION, + dest="training_fraction", + help="Training detail: the fraction of the " + "data used for training. The rest is never " + "seen by the inference algorithm. Speeds up " + "learning.") + subparser.add_argument("--empty-drop-training-fraction", + type=float, nargs=None, + default=consts.FRACTION_EMPTIES, + dest="fraction_empties", + help="Training detail: the fraction of the " + "training data each epoch " + "that is drawn (randomly sampled) from " + "surely empty droplets.") + subparser.add_argument("--ignore-features", nargs="+", type=int, + default=[], + dest="blacklisted_genes", + help="Integer indices of features to ignore " + "entirely. In the output count matrix, the " + "counts for these features will be unchanged.") + subparser.add_argument("--fpr", nargs="+", + default=[0.01], + dest="fpr", + help="Target 'delta' false positive rate in [0, 1). " + "Use 0 for a cohort of samples which will be " + "jointly analyzed for differential expression. " + "A false positive is a true signal count that is " + "erroneously removed. More background removal " + "is accompanied by more signal removal " + "at high values of FPR. You can specify " + "multiple values, which will create multiple " + "output files.") + subparser.add_argument("--exclude-feature-types", + type=str, nargs="+", default=[], + dest="exclude_features", + help="Feature types to ignore during the analysis. " + "These features will be left unchanged in the " + "output file.") + subparser.add_argument("--projected-ambient-count-threshold", + type=float, nargs=None, + default=consts.AMBIENT_COUNTS_IN_CELLS_LOW_LIMIT, + dest="ambient_counts_in_cells_low_limit", + help="Controls how many features are included in the " + "analysis, which can lead to a large speedup. " + "If a feature is expected to have less than " + "PROJECTED_AMBIENT_COUNT_THRESHOLD counts total " + "in all cells (summed), then that gene is " + "excluded, and it will be unchanged in the " + "output count matrix. For example, " + "PROJECTED_AMBIENT_COUNT_THRESHOLD = 0 will " + "include all features which have even a single " + "count in any empty droplet.") + subparser.add_argument("--learning-rate", + type=float, default=1e-4, + dest="learning_rate", + help="Training detail: lower learning rate for " + "inference. A OneCycle learning rate schedule " + "is used, where the upper learning rate is ten " + "times this value. (For this value, probably " + "do not exceed 1e-3).") + subparser.add_argument("--checkpoint-mins", + type=float, default=7., + dest="checkpoint_min", + help="Checkpoint file will be saved periodically, " + "with this many minutes between each checkpoint.") + subparser.add_argument("--final-elbo-fail-fraction", type=float, + dest="final_elbo_fail_fraction", + help="Training is considered to have failed if " + "(best_test_ELBO - final_test_ELBO)/(best_test_ELBO " + "- initial_test_ELBO) > FINAL_ELBO_FAIL_FRACTION. Training will " + "automatically re-run if --num-training-tries > " + "1. By default, will not fail training based " + "on final_training_ELBO.") + subparser.add_argument("--epoch-elbo-fail-fraction", type=float, + dest="epoch_elbo_fail_fraction", + help="Training is considered to have failed if " + "(previous_epoch_test_ELBO - current_epoch_test_ELBO)" + "/(previous_epoch_test_ELBO - initial_train_ELBO) " + "> EPOCH_ELBO_FAIL_FRACTION. Training will " + "automatically re-run if --num-training-tries > " + "1. By default, will not fail training based " + "on epoch_training_ELBO.") + subparser.add_argument("--num-training-tries", type=int, default=1, + dest="num_training_tries", + help="Number of times to attempt to train the model. " + "At each subsequent attempt, the learning rate is " + "multiplied by LEARNING_RATE_RETRY_MULT.") + subparser.add_argument("--learning-rate-retry-mult", type=float, default=0.2, + dest="learning_rate_retry_mult", + help="Learning rate is multiplied by this amount each " + "time a new training attempt is made. (This " + "parameter is only used if training fails based " + "on EPOCH_ELBO_FAIL_FRACTION or " + "FINAL_ELBO_FAIL_FRACTION and NUM_TRAINING_TRIES" + " is > 1.)") + subparser.add_argument("--posterior-batch-size", type=int, + default=consts.PROB_POSTERIOR_BATCH_SIZE, + dest="posterior_batch_size", + help="Training detail: size of batches when creating " + "the posterior. Reduce this to avoid running " + "out of GPU memory creating the posterior " + "(will be slower).") + subparser.add_argument("--posterior-regularization", type=str, + default=None, + choices=["PRq", "PRmu", "PRmu_gene"], + dest="posterior_regularization", + help="Posterior regularization method. (For experts: " + "not required for normal usage, see " + "documentation). PRq is approximate quantile-" + "targeting. PRmu is approximate mean-targeting " + "aggregated over genes (behavior of v0.2.0). " + "PRmu_gene is approximate mean-targeting per " + "gene.") + subparser.add_argument("--alpha", + type=float, default=None, + dest="prq_alpha", + help="Tunable parameter alpha for the PRq posterior " + "regularization method (not normally used: see " + "documentation).") + subparser.add_argument("--q", + type=float, default=None, + dest="cdf_threshold_q", + help="Tunable parameter q for the CDF threshold " + "estimation method (not normally used: see " + "documentation).") + subparser.add_argument("--estimator", type=str, + default="mckp", + choices=["map", "mean", "cdf", "sample", "mckp"], + dest="estimator", + help="Output denoised count estimation method. (For " + "experts: not required for normal usage, see " + "documentation).") + subparser.add_argument("--estimator-multiple-cpu", + dest="use_multiprocessing_estimation", action="store_true", + default=False, + help="Including the flag --estimator-multiple-cpu will " + "use more than one CPU to compute the MCKP " + "output count estimator in parallel (does " + "nothing for other estimators).") + subparser.add_argument("--constant-learning-rate", + dest="constant_learning_rate", action="store_true", + default=False, + help="Including the flag --constant-learning-rate will " + "use the ClippedAdam optimizer instead of the " + "OneCycleLR learning rate schedule, which is " + "the default. Learning is faster with the " + "OneCycleLR schedule. However, training can " + "easily be continued from a checkpoint for more " + "epochs than the initial command specified when " + "using ClippedAdam. On the other hand, if using " + "the OneCycleLR schedule with 150 epochs " + "specified, it is not possible to pick up from " + "that final checkpoint and continue training " + "until 250 epochs.") + subparser.add_argument("--cpu-threads", + type=int, default=None, + dest="n_threads", + help="Number of threads to use when pytorch is run " + "on CPU. Defaults to the number of logical cores.") + subparser.add_argument("--debug", + dest="debug", action="store_true", + help="Including the flag --debug will log " + "extra messages useful for debugging.") + subparser.add_argument("--truth", + type=str, default=None, + dest="truth_file", + help="This is only used by developers for report " + "generation. Truth h5 file (for " + "simulated data only).") + + return subparsers diff --git a/cellbender/remove_background/checkpoint.py b/cellbender/remove_background/checkpoint.py new file mode 100644 index 0000000..5b0903d --- /dev/null +++ b/cellbender/remove_background/checkpoint.py @@ -0,0 +1,368 @@ +"""Handle the saving and loading of models from checkpoint files.""" + +from cellbender.remove_background import consts +from cellbender.remove_background.data.dataprep import DataLoader + +import torch +import numpy as np +from typing import Tuple, List, Optional, Union, Dict +import logging +import argparse +import hashlib +import os +import tarfile +import glob +import random +import pickle +import tempfile +import shutil +import traceback + + +logger = logging.getLogger('cellbender') + +USE_PYRO = True +try: + import pyro +except ImportError: + USE_PYRO = False +USE_CUDA = torch.cuda.is_available() + + +def save_random_state(filebase: str) -> List[str]: + """Write states of various random number generators to files. + + NOTE: the pyro.util.get_rng_state() is a compilation of python, numpy, and + torch random states. Here, we save the three explicitly ourselves. + This is useful for potential future usages outside of pyro. + """ + # https://stackoverflow.com/questions/32808686/storing-a-random-state/32809283 + + file_dict = {} + + # Random state information + if USE_PYRO: + pyro_random_state = pyro.util.get_rng_state() # this is shorthand for the following + file_dict.update({filebase + '_random.pyro': pyro_random_state}) + else: + python_random_state = random.getstate() + numpy_random_state = np.random.get_state() + torch_random_state = torch.get_rng_state() + file_dict.update({filebase + '_random.python': python_random_state, + filebase + '_random.numpy': numpy_random_state, + filebase + '_random.torch': torch_random_state}) + if USE_CUDA: + cuda_random_state = torch.cuda.get_rng_state_all() + file_dict.update({filebase + '_random.cuda': cuda_random_state}) + + # Save it + for file, state in file_dict.items(): + with open(file, 'wb') as f: + pickle.dump(state, f) + + return list(file_dict.keys()) + + +def load_random_state(filebase: str): + """Load random states from files and update generators with them.""" + + if USE_PYRO: + with open(filebase + '_random.pyro', 'rb') as f: + pyro_random_state = pickle.load(f) + pyro.util.set_rng_state(pyro_random_state) + + else: + with open(filebase + '_random.python', 'rb') as f: + python_random_state = pickle.load(f) + random.setstate(python_random_state) + + with open(filebase + '_random.numpy', 'rb') as f: + numpy_random_state = pickle.load(f) + np.random.set_state(numpy_random_state) + + with open(filebase + '_random.torch', 'rb') as f: + torch_random_state = pickle.load(f) + torch.set_rng_state(torch_random_state) + + if USE_CUDA: + with open(filebase + '_random.cuda', 'rb') as f: + cuda_random_state = pickle.load(f) + torch.cuda.set_rng_state_all(cuda_random_state) + + +def save_checkpoint(filebase: str, + model_obj: 'RemoveBackgroundPyroModel', + scheduler: pyro.optim.PyroOptim, + args: argparse.Namespace, + train_loader: Optional[DataLoader] = None, + test_loader: Optional[DataLoader] = None, + tarball_name: str = consts.CHECKPOINT_FILE_NAME) -> bool: + """Save trained model and optimizer state in a checkpoint file. + Any hyperparameters or metadata should be part of the model object.""" + + logger.info(f'Saving a checkpoint...') + + try: + + # Work in a temporary directory. + with tempfile.TemporaryDirectory() as tmp_dir: + + basename = os.path.basename(filebase) + filebase = os.path.join(tmp_dir, basename) + + file_list = save_random_state(filebase=filebase) + + torch.save(model_obj, filebase + '_model.torch') + torch.save(scheduler, filebase + '_optim.torch') + scheduler.save(filebase + '_optim.pyro') # use PyroOptim method + pyro.get_param_store().save(filebase + '_params.pyro') + file_list = file_list + [filebase + '_model.torch', + filebase + '_optim.torch', + filebase + '_optim.pyro', + filebase + '_params.pyro'] + + if train_loader is not None: + # train_loader_file = save_dataloader_state(filebase=filebase, + # data_loader_state=train_loader.get_state(), + # name='train') + torch.save(train_loader, filebase + '_train.loaderstate') + file_list.append(filebase + '_train.loaderstate') + if test_loader is not None: + # test_loader_file = save_dataloader_state(filebase=filebase, + # data_loader_state=test_loader.get_state(), + # name='test') + torch.save(test_loader, filebase + '_test.loaderstate') + file_list.append(filebase + '_test.loaderstate') + + np.save(filebase + '_args.npy', args) + file_list.append(filebase + '_args.npy') + + make_tarball(files=file_list, tarball_name=tarball_name) + + logger.info(f'Saved checkpoint as {os.path.abspath(tarball_name)}') + return True + + except KeyboardInterrupt: + logger.warning('Keyboard interrupt: will not save checkpoint') + return False + + except Exception: + logger.warning('Could not save checkpoint') + logger.warning(traceback.format_exc()) + return False + + +def load_checkpoint(filebase: Optional[str], + tarball_name: str = consts.CHECKPOINT_FILE_NAME, + force_device: Optional[str] = None)\ + -> Dict[str, Union['RemoveBackgroundPyroModel', pyro.optim.PyroOptim, DataLoader, bool]]: + """Load checkpoint and prepare a RemoveBackgroundPyroModel and optimizer.""" + + out = load_from_checkpoint( + filebase=filebase, + tarball_name=tarball_name, + to_load=['model', 'optim', 'param_store', 'dataloader', 'args', 'random_state'], + force_device=force_device, + ) + out.update({'loaded': True}) + logger.info(f'Loaded partially-trained checkpoint from {tarball_name}') + return out + + +def load_from_checkpoint(filebase: Optional[str], + tarball_name: str = consts.CHECKPOINT_FILE_NAME, + to_load: List[str] = ['model'], + force_device: Optional[str] = None) -> Dict: + """Load specific files from a checkpoint tarball.""" + + load_kwargs = {} + if force_device is not None: + load_kwargs.update({'map_location': torch.device(force_device)}) + + # Work in a temporary directory. + with tempfile.TemporaryDirectory() as tmp_dir: + + # Unpack the checkpoint tarball. + logger.info(f'Attempting to unpack tarball "{tarball_name}" to {tmp_dir}') + success = unpack_tarball(tarball_name=tarball_name, directory=tmp_dir) + if success: + unpacked_files = '\n'.join(glob.glob(os.path.join(tmp_dir, "*"))) + logger.info(f'Successfully unpacked tarball to {tmp_dir}\n' + f'{unpacked_files}') + else: + # no tarball loaded, so do not continue trying to load files + raise FileNotFoundError + + # See if files have a hash matching input filebase. + if filebase is not None: + basename = os.path.basename(filebase) + filebase = os.path.join(tmp_dir, basename) + logger.debug(f'Looking for files with base name matching {filebase}*') + if not os.path.exists(filebase + '_model.torch'): + logger.info('Workflow hash does not match that of checkpoint.') + raise ValueError('Workflow hash does not match that of checkpoint.') + else: + filebase = (glob.glob(os.path.join(tmp_dir, '*_model.torch'))[0] + .replace('_model.torch', '')) + logger.debug(f'Accepting any file hash, so loading {filebase}*') + + out = {} + + # Load the saved model. + if 'model' in to_load: + model_obj = torch.load(filebase + '_model.torch', **load_kwargs) + logger.debug('Model loaded from ' + filebase + '_model.torch') + out.update({'model': model_obj}) + + # Load the saved optimizer. + if 'optim' in to_load: + scheduler = torch.load(filebase + '_optim.torch', **load_kwargs) + scheduler.load(filebase + '_optim.pyro', **load_kwargs) # use PyroOptim method + logger.debug('Optimizer loaded from ' + filebase + '_optim.*') + out.update({'optim': scheduler}) + + # Load the pyro param store. + if 'param_store' in to_load: + pyro.get_param_store().load(filebase + '_params.pyro', map_location=force_device) + logger.debug('Pyro param store loaded from ' + filebase + '_params.pyro') + + # Load dataloader states. + if 'dataloader' in to_load: + # load_dataloader_state(data_loader=train_loader, file=filebase + '_train.loaderstate') + # load_dataloader_state(data_loader=test_loader, file=filebase + '_test.loaderstate') + train_loader = None + test_loader = None + if os.path.exists(filebase + '_train.loaderstate'): + train_loader = torch.load(filebase + '_train.loaderstate', **load_kwargs) + logger.debug('Train loader loaded from ' + filebase + '_train.loaderstate') + out.update({'train_loader': train_loader}) + if os.path.exists(filebase + '_test.loaderstate'): + test_loader = torch.load(filebase + '_test.loaderstate', **load_kwargs) + logger.debug('Test loader loaded from ' + filebase + '_test.loaderstate') + out.update({'test_loader': test_loader}) + + # Load args, which can be modified in the case of auto-learning-rate updates. + if 'args' in to_load: + args = np.load(filebase + '_args.npy', allow_pickle=True).item() + out.update({'args': args}) + + # Update states of random number generators across the board. + if 'random_state' in to_load: + load_random_state(filebase=filebase) + logger.debug('Loaded random state globally for python, numpy, pytorch, and cuda') + + # Copy the posterior file outside the temp dir so it can be loaded later. + if 'posterior' in to_load: + posterior_file = os.path.join(os.path.dirname(filebase), 'posterior.h5') + if os.path.exists(posterior_file): + shutil.copyfile(posterior_file, 'posterior.h5') + out.update({'posterior_file': 'posterior.h5'}) + logger.debug(f'Copied posterior file from {posterior_file} to posterior.h5') + else: + logger.debug('Trying to load posterior in load_from_checkpoint(), but posterior ' + 'is not present in checkpoint tarball.') + + return out + + +def attempt_load_checkpoint(filebase: str, + tarball_name: str = consts.CHECKPOINT_FILE_NAME, + force_device: Optional[str] = None)\ + -> Dict[str, Union['RemoveBackgroundPyroModel', pyro.optim.PyroOptim, DataLoader, bool]]: + """Load checkpoint and prepare a RemoveBackgroundPyroModel and optimizer, + or return the inputs if loading fails.""" + + try: + logger.debug('Attempting to load checkpoint from ' + tarball_name) + return load_checkpoint(filebase=filebase, + tarball_name=tarball_name, + force_device=force_device) + + except FileNotFoundError: + logger.debug('No tarball found') + return {'loaded': False} + + except ValueError: + logger.debug('Unpacked tarball files have a different workflow hash: will not load') + return {'loaded': False} + + +def make_tarball(files: List[str], tarball_name: str) -> bool: + """Tar and gzip a list of files as a checkpoint tarball. + NOTE: used by automated checkpoint handling in Cromwell + NOTE2: .tmp file is used so that incomplete checkpoint files do not exist + even temporarily + """ + with tarfile.open(tarball_name + '.tmp', 'w:gz', compresslevel=1) as tar: + for file in files: + # without arcname, unpacking results in unpredictable file locations! + tar.add(file, arcname=os.path.basename(file)) + os.rename(tarball_name + '.tmp', tarball_name) + return True + + +def unpack_tarball(tarball_name: str, directory: str) -> bool: + """Untar a checkpoint tarball and put the files in directory.""" + if not os.path.exists(tarball_name): + logger.info("No saved checkpoint.") + return False + + try: + with tarfile.open(tarball_name, 'r:gz') as tar: + tar.extractall(path=directory) + return True + + except Exception: + logger.warning("Failed to unpack existing tarball.") + return False + + +def create_workflow_hashcode(module_path: str, + args: argparse.Namespace, + args_to_remove: List[str] = ['epochs', 'fpr'], + name: str = 'md5', + verbose: bool = False) -> str: + """Create a hash blob from cellbender python code plus input arguments.""" + + hasher = hashlib.new(name=name) + + files_safe_to_ignore = ['infer.py', 'simulate.py', 'report.py', + 'downstream.py', 'monitor.py', 'fpr.py', + 'posterior.py', 'estimation.py', 'sparse_utils.py'] + + if not os.path.exists(module_path): + return '' + + try: + + # files + for root, _, files in os.walk(module_path): + for file in files: + if not file.endswith('.py'): + continue + if file in files_safe_to_ignore: + continue + if 'test' in file: + continue + if verbose: + print(file) + with open(os.path.join(root, file), 'rb') as f: + buf = b'\n'.join(f.readlines()) + hasher.update(buf) # encode python files + + # inputs + args_dict = vars(args).copy() + for arg in args_to_remove: + args_dict.pop(arg, None) + hasher.update(str(args_dict).encode('utf-8')) # encode parsed input args + + # input file + # TODO + # this is probably not necessary for real data... why would two different + # files have the same name? + # but it's useful for development where simulated datasets change + + except Exception: + return '' + + return hasher.hexdigest() diff --git a/cellbender/remove_background/cli.py b/cellbender/remove_background/cli.py index 16e93e8..ad49c01 100644 --- a/cellbender/remove_background/cli.py +++ b/cellbender/remove_background/cli.py @@ -1,15 +1,17 @@ """Command-line tool functionality for remove-background.""" import torch -from cellbender.remove_background.data.dataset import SingleCellRNACountsDataset -from cellbender.remove_background.train import run_inference -from cellbender.base_cli import AbstractCLI -from cellbender.remove_background import consts +import cellbender + +from cellbender.base_cli import AbstractCLI, get_version +from cellbender.remove_background.checkpoint import create_workflow_hashcode +from cellbender.remove_background.run import run_remove_background +from cellbender.remove_background.posterior import Posterior import logging import os import sys -from datetime import datetime +import argparse class CLI(AbstractCLI): @@ -22,16 +24,25 @@ def __init__(self): def get_name(self) -> str: return self.name - def validate_args(self, args): + @staticmethod + def validate_args(args) -> argparse.Namespace: """Validate parsed arguments.""" # Ensure that if there's a tilde for $HOME in the file path, it works. try: args.input_file = os.path.expanduser(args.input_file) args.output_file = os.path.expanduser(args.output_file) + if args.truth_file is not None: + args.truth_file = os.path.expanduser(args.truth_file) except TypeError: raise ValueError("Problem with provided input and output paths.") + # Ensure that if truth data is specified, it is accessible + if args.truth_file is not None: + assert os.access(args.truth_file, os.R_OK), \ + f"Cannot read specified simulated truth file {args.truth_file}. " \ + f"Ensure the file exists and is read accessible." + # Ensure write access to the save directory. file_dir, _ = os.path.split(args.output_file) # Get the output directory if file_dir: @@ -76,6 +87,10 @@ def validate_args(self, args): "significant speed-ups.\n\n") sys.stdout.flush() # Write immediately + # Make sure n_threads makes sense. + if args.n_threads is not None: + assert args.n_threads > 0, "--cpu-threads must be an integer >= 1" + # Ensure all network layer dimensions are positive. for n in args.z_hidden_dims: assert n > 0, "--z-layers must be all positive integers." @@ -88,117 +103,131 @@ def validate_args(self, args): args.use_jit = False # Ensure false positive rate is between zero and one. + fpr_list_correct_dtypes = [] # for a mix of floats and strings later on for fpr in args.fpr: - assert (fpr > 0.) and (fpr < 1.), \ - "False positive rate --fpr must be between 0 and 1." - - assert args.num_training_tries > 0, "num-training-tries must be > 0" - assert args.epoch_elbo_fail_fraction is None or args.epoch_elbo_fail_fraction > 0., \ - "--epoch-elbo-fail-fraction must be > 0" - assert args.final_elbo_fail_fraction is None or args.final_elbo_fail_fraction > 0., \ - "--epoch-elbo-fail-fraction must be > 0" - - self.args = args + try: + fpr = float(fpr) + assert (fpr >= 0.) and (fpr < 1.), \ + "False positive rate --fpr must be in [0, 1)" + except ValueError: + # the input is not a float + assert fpr == 'cohort', \ + "The only allowed non-float value for FPR is the word 'cohort'." + fpr_list_correct_dtypes.append(fpr) + args.fpr = fpr_list_correct_dtypes + + # Ensure that "exclude_features" specifies allowed features. + # As of CellRanger 6.0, the possible features are: + # Gene Expression + # Antibody Capture + # CRISPR Guide Capture + # Custom + # Peaks + allowed_features = ['Gene Expression', 'Antibody Capture', + 'CRISPR Guide Capture', 'Custom', 'Peaks'] + for feature in args.exclude_features: + if feature not in allowed_features: + sys.stdout.write(f"Specified '{feature}' using --exclude-feature-types, " + f"but this is not a valid CellRanger feature " + f"designation: {allowed_features}. Ensure that " + f"this feature appears in your dataset, and " + f"ensure that this log file makes note of the " + f"exclusion of the appropriate features below.\n\n") + sys.stdout.flush() # Write immediately + if 'Gene Expression' in args.exclude_features: + sys.stdout.write("WARNING: Excluding 'Gene Expression' features from the analysis " + "is not recommended, since other features alone are typically " + "too sparse to form a good prior on cell type, and CellBender " + "relies on being able to construct this sort of prior\n\n") + sys.stdout.flush() # Write immediately + + # Automatic training failures and restarts. + assert args.num_training_tries > 0, "--num-training-tries must be > 0 (default 1)" + if args.epoch_elbo_fail_fraction is not None: + assert (args.epoch_elbo_fail_fraction > 0.), \ + "--epoch-elbo-fail-fraction must be in > 0" + if args.final_elbo_fail_fraction is not None: + assert (args.final_elbo_fail_fraction > 0.), \ + "--final-elbo-fail-fraction must be in > 0" + + # Ensure timing of checkpoints is within bounds. + assert args.checkpoint_min > 0, "--checkpoint-min must be > 0" + if args.checkpoint_min > 15: + sys.stdout.write(f"Warning: Timing between checkpoints is specified as " + f"{args.checkpoint_min} minutes. Consider reducing " + f"this number if you are concerned about the " + f"possibility of lost work upon preemption.\n\n") + sys.stdout.flush() # Write immediately + + # Posterior regularization checking. + if args.cdf_threshold_q is not None: + assert (args.cdf_threshold_q >= 0.) and (args.cdf_threshold_q <= 1.), \ + f"Argument --q must be in range [0, 1] since it is a CDF threshold." + if args.posterior_regularization == 'PRq': + # We need q for the CDF threshold estimator. + assert args.prq_alpha is not None, \ + 'Input argument --alpha must be specified when using ' \ + '--posterior-regularization PRq' + + # Estimator checking. + if args.estimator == 'cdf': + # We need q for the CDF threshold estimator. + assert args.cdf_threshold_q is not None, \ + 'Input argument --q must be specified when using --estimator cdf' return args - def run(self, args): + @staticmethod + def run(args) -> Posterior: """Run the main tool functionality on parsed arguments.""" # Run the tool. - main(args) - + return main(args) -def run_remove_background(args): - """The full script for the command line tool to remove background RNA. - Args: - args: Inputs from the command line, already parsed using argparse. - - Note: Returns nothing, but writes output to a file(s) specified from - command line. - - """ - - # Load dataset, run inference, and write the output to a file. +def setup_and_logging(args): + """Take command-line input, parse arguments, and run tests or tool.""" # Send logging messages to stdout as well as a log file. file_dir, file_base = os.path.split(args.output_file) file_name = os.path.splitext(os.path.basename(file_base))[0] log_file = os.path.join(file_dir, file_name + ".log") - logging.basicConfig(level=logging.INFO, - format="cellbender:remove-background: %(message)s", - filename=log_file, - filemode="w") - console = logging.StreamHandler() - formatter = logging.Formatter("cellbender:remove-background: " - "%(message)s") - console.setFormatter(formatter) # Use the same format for stdout. - logging.getLogger('').addHandler(console) # Log to stdout and a file. + logger = logging.getLogger('cellbender') # name of the logger + logger.setLevel(logging.INFO if not args.debug else logging.DEBUG) + formatter = logging.Formatter('cellbender:remove-background: %(message)s') + file_handler = logging.FileHandler(filename=log_file, mode='w', encoding='UTF-8') + console_handler = logging.StreamHandler() + file_handler.setFormatter(formatter) # set the file format + console_handler.setFormatter(formatter) # use the same format for stdout + logger.addHandler(file_handler) # log to file + logger.addHandler(console_handler) # log to stdout # Log the command as typed by user. - logging.info("Command:\n" + ' '.join(['cellbender', 'remove-background'] - + sys.argv[2:])) - - # Log the start time. - logging.info(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) - - logging.info("Running remove-background") - - # Load data from file and choose barcodes and genes to analyze. - try: - dataset_obj = \ - SingleCellRNACountsDataset(input_file=args.input_file, - expected_cell_count=args.expected_cell_count, - total_droplet_barcodes=args.total_droplets, - fraction_empties=args.fraction_empties, - model_name=args.model, - gene_blacklist=args.blacklisted_genes, - exclude_antibodies=args.exclude_antibodies, - low_count_threshold=args.low_count_threshold, - fpr=args.fpr) - - except OSError: - logging.error(f"OSError: Unable to open file {args.input_file}.") - sys.exit(1) - - # Instantiate latent variable model and run full inference procedure. - inferred_model = run_inference(dataset_obj, args) - - # Write outputs to file. - try: - dataset_obj.save_to_output_file(args.output_file, - inferred_model, - posterior_batch_size=args.posterior_batch_size, - cells_posterior_reg_calc=args.cells_posterior_reg_calc, - save_plots=True) - - logging.info("Completed remove-background.") - logging.info(datetime.now().strftime('%Y-%m-%d %H:%M:%S\n')) - - # The exception allows user to end inference prematurely with CTRL-C. - except KeyboardInterrupt: - - # If partial output has been saved, delete it. - full_file = args.output_file - - # Name of the filtered (cells only) file. - file_dir, file_base = os.path.split(full_file) - file_name = os.path.splitext(os.path.basename(file_base))[0] - filtered_file = os.path.join(file_dir, - file_name + "_filtered.h5") - - if os.path.exists(full_file): - os.remove(full_file) - - if os.path.exists(filtered_file): - os.remove(filtered_file) - - logging.info("Keyboard interrupt. Terminated without saving.\n") - - -def main(args): + logger.info("Command:\n" + + ' '.join(['cellbender', 'remove-background'] + sys.argv[2:])) + logger.info("CellBender " + get_version()) + + # Set up checkpointing by creating a unique workflow hash. + hashcode = create_workflow_hashcode( + module_path=os.path.dirname(cellbender.__file__), + args_to_remove=(['output_file', 'fpr', 'input_checkpoint_tarball', 'debug', + 'posterior_batch_size', 'checkpoint_min', 'truth_file', + 'posterior_regularization', 'cdf_threshold_q', 'prq_alpha', + 'estimator', 'use_multiprocessing_estimation', 'cpu_threads'] + + (['epochs'] if args.constant_learning_rate else [])), + args=args)[:10] + args.checkpoint_filename = hashcode # store this in args + logger.info(f'(Workflow hash {hashcode})') + return args, file_handler + + +def main(args) -> Posterior: """Take command-line input, parse arguments, and run tests or tool.""" + args, file_handler = setup_and_logging(args) + # Run the tool. - run_remove_background(args) + posterior = run_remove_background(args) + file_handler.close() + + return posterior diff --git a/cellbender/remove_background/consts.py b/cellbender/remove_background/consts.py index 2874913..ee5fc11 100644 --- a/cellbender/remove_background/consts.py +++ b/cellbender/remove_background/consts.py @@ -1,13 +1,21 @@ """Constant numbers used in remove-background.""" +# Seed for random number generators. +RANDOM_SEED = 1234 + # Factor by which the mode UMI count of the empty droplet plateau is # multiplied to come up with a UMI cutoff below which no barcodes are used. EMPIRICAL_LOW_UMI_TO_EMPTY_DROPLET_THRESHOLD = 0.5 +# Features with fewer than this many counts summed over all empty droplets +# are ignored during analysis, and the output count matrix for these features +# is identical to the input. +COUNTS_IN_EMPTIES_LOW_LIMIT = 0 +AMBIENT_COUNTS_IN_CELLS_LOW_LIMIT = 0.1 + # Default prior for the standard deviation of the LogNormal distribution for # cell size, used only in the case of the 'simple' model. SIMPLE_MODEL_D_STD_PRIOR = 0.2 -D_STD_PRIOR = 0.02 # Probability cutoff for determining which droplets contain cells and which # are empty. The droplets n with inferred probability q_n > CELL_PROB_CUTOFF @@ -16,7 +24,7 @@ # Default UMI cutoff. Droplets with UMI less than this will be completely # excluded from the analysis. -LOW_UMI_CUTOFF = 15 +LOW_UMI_CUTOFF = 5 # Default total number of droplets to include in the analysis. TOTAL_DROPLET_DEFAULT = 25000 @@ -28,32 +36,34 @@ DEFAULT_BATCH_SIZE = 128 # Fraction of totally empty droplets that makes up each minibatch, by default. -FRACTION_EMPTIES = 0.5 +FRACTION_EMPTIES = 0.2 # Prior on rho, the swapping fraction: the two concentration parameters alpha and beta. -RHO_ALPHA_PRIOR = 18. -RHO_BETA_PRIOR = 200. +RHO_ALPHA_PRIOR = 1.5 +RHO_BETA_PRIOR = 50. # Constraints on rho posterior latents. -RHO_PARAM_MIN = 1. +RHO_PARAM_MIN = 0.001 RHO_PARAM_MAX = 1000. # Prior on epsilon, the RT efficiency concentration parameter [Gamma(alpha, alpha)]. -EPSILON_PRIOR = 500. +EPSILON_PRIOR = 50. # Prior used for the global overdispersion parameter. PHI_LOC_PRIOR = 0.2 PHI_SCALE_PRIOR = 0.2 +# Prior for mixture weights in Gaussian mixture model. +GMM_ALPHA_PRIOR = 1e-1 +GMM_COMPONENTS = 10 +GMM_EPOCHS = 500 + # Initial value of global latent scale for d_cell. D_CELL_SCALE_INIT = 0.02 # Scale used to regularize values of logit cell probability (mean zero). P_LOGIT_SCALE = 2. -# Hidden layer sizes of non-z latent encoder neural network. -ENC_HIDDEN_DIMS = [100, 50] - # False to use an approximate log_prob computation which is much faster. USE_EXACT_LOG_PROB = False @@ -64,23 +74,43 @@ NBPC_MU_EPS_SAFEGAURD = 1e-10 NBPC_ALPHA_EPS_SAFEGAURD = 1e-10 NBPC_LAM_EPS_SAFEGAURD = 1e-10 +POISSON_EPS_SAFEGAURD = 1e-10 # Scale factors for loss function regularization terms: semi-supervision. REG_SCALE_AMBIENT_EXPRESSION = 0.01 -REG_SCALE_EMPTY_PROB = 1.0 -REG_SCALE_CELL_PROB = 10.0 +REG_SCALE_SOFT_SUPERVISION = 10.0 + +# Regularize logit probabilities toward this lognormal distribution. +REG_LOGIT_MEAN = 10.0 +REG_LOGIT_SCALE = 0.2 +REG_LOGIT_SOFT_SCALE = 1.0 -# Number of cells used to estimate posterior regularization lambda. Memory hungry. +# Number of cells used to esitmate posterior regularization lambda. Memory hungry. CELLS_POSTERIOR_REG_CALC = 100 # Posterior regularization constant's upper and lower bounds. -POSTERIOR_REG_MIN = 0.1 +POSTERIOR_REG_MIN = 0.01 POSTERIOR_REG_MAX = 500 -POSTERIOR_REG_SEARCH_MAX_ITER = 20 +POSTERIOR_REG_SEARCH_MAX_ITER = 50 -# Minimum number of barcodes we expect in an unfiltered `h5ad` input file. -# Throws a warning if the input has fewer than this number. +# For AnnData h5ad files, fewer than this many barcodes will trigger a warning, +# since it indicates that the file might be "filtered" to cells only. MINIMUM_BARCODES_H5AD = 1e5 -# reduce this if running out of GPU memory https://github.com/broadinstitute/CellBender/issues/67 -PROP_POSTERIOR_BATCH_SIZE = 20 +# Batch size for posterior inference. +PROB_POSTERIOR_BATCH_SIZE = 128 + +# Name of checkpoint file. +CHECKPOINT_FILE_NAME = 'ckpt.tar.gz' + +# Whether to create an extended report (for development purposes). +EXTENDED_REPORT = False + +# Maximum batch size +MAX_BATCH_SIZE = 256 +SMALLEST_ALLOWED_BATCH = 4 # BatchNorm chokes if there is only 1 cell in last batch + +# Guesses during prior estimation +MAX_TOTAL_DROPLETS_GUESSED = 70000 +MAX_EMPTIES_TO_INCLUDE = 20000 +NUM_EMPTIES_INCREMENT = 20000 # if input expected_cells > heuristic prior total_drops diff --git a/cellbender/remove_background/data/dataprep.py b/cellbender/remove_background/data/dataprep.py index b53003f..00e1d5d 100644 --- a/cellbender/remove_background/data/dataprep.py +++ b/cellbender/remove_background/data/dataprep.py @@ -1,5 +1,8 @@ """Helper functions for preparing dataloaders, as well as a class to implement loading data from a sparse matrix in mini-batches. + +Intentionally uses global random state, and does not keep its own random number +generator, in order to facilitate checkpointing. """ import numpy as np @@ -10,7 +13,11 @@ import torch import torch.utils.data -from typing import Tuple, List, Optional +import logging +from typing import Tuple, List, Optional, Callable + + +logger = logging.getLogger('cellbender') class SparseDataset(torch.utils.data.Dataset): @@ -51,9 +58,37 @@ def __init__(self, batch_size: int = consts.DEFAULT_BATCH_SIZE, fraction_empties: float = consts.FRACTION_EMPTIES, shuffle: bool = True, + sort_by: Optional[Callable[[sp.csr_matrix], np.ndarray]] = None, use_cuda: bool = True): + """ + Args: + dataset: Droplet count matrix [cell, gene] + empty_drop_dataset: Surely empty droplets count matrix + batch_size: Number of droplets in minibatch + fraction_empties: Fraction of each minibatch that will consist of + surely empty droplets + shuffle: True to shuffle data. Incompatible with sort_by. + sort_by: Lambda function which, when applied to the sparse matrix, + will return values that can be sorted to give a sort order to + the dataset. Dataloader will load data in order of increasing + values. Object attributes sort_order and unsort_order will be + made available. + use_cuda: True to load data to GPU + """ + if shuffle: + assert sort_by is None, 'Cannot sort_by and shuffle at the same time' + self.sort_fn = sort_by self.dataset = dataset - self.ind_list = np.arange(self.dataset.shape[0]) + if self.sort_fn is not None: + sort_values = self.sort_fn(self.dataset) + sort_order = np.argsort(sort_values) + self.ind_list = sort_order + self.sort_order = sort_order.copy() + self._unsort_dict = {i: val for i, val in enumerate(self.sort_order)} + else: + self.ind_list = np.arange(self.dataset.shape[0]) + self.sort_order = self.ind_list.copy() + self._unsort_dict = {i: i for i in self.ind_list} self.empty_drop_dataset = empty_drop_dataset if self.empty_drop_dataset is None: self.empty_ind_list = np.array([]) @@ -63,27 +98,64 @@ def __init__(self, self.fraction_empties = fraction_empties self.cell_batch_size = int(batch_size * (1. - fraction_empties)) self.shuffle = shuffle - self.random = np.random.RandomState(seed=1234) self.device = 'cpu' self.use_cuda = use_cuda if self.use_cuda: self.device = 'cuda' + self._length = None self._reset() + @torch.no_grad() + def unsort_inds(self, bcs): + if self.sort_fn is None: + return bcs # just for speed + else: + return torch.tensor([self._unsort_dict[bc.item()] for bc in bcs], device='cpu') + def _reset(self): if self.shuffle: - self.random.shuffle(self.ind_list) # Shuffle cell inds in place + np.random.shuffle(self.ind_list) # Shuffle cell inds in place self.ptr = 0 + def get_state(self): + """Internal state of the data loader, used for checkpointing""" + return {'ind_list': self.ind_list, 'ptr': self.ptr} + + def set_state(self, ind_list: np.ndarray, ptr: int): + self.ind_list = ind_list + self.ptr = ptr + assert self.ptr <= len(self.ind_list), \ + f'Problem setting dataloader state: pointer ({ptr}) is outside the ' \ + f'length of the ind_list ({len(ind_list)})' + + def reset_ptr(self): + self.ptr = 0 + + @property + def length(self): + if self._length is None: + self._length = self._get_length() + return self._length + + def _get_length(self): + # avoid the potential for an off-by-one error by just going through it + i = 0 + for _ in self: + i += 1 + return i + def __len__(self): - return int(self.ind_list.size * - (1 + (self.fraction_empties / (1 - self.fraction_empties)))) # ...ish + return self.length def __iter__(self): return self def __next__(self): - if self.ptr == self.ind_list.size: + # Skip last batch if the size is < smallest allowed batch + remaining_cells = self.ind_list.size - self.ptr + if remaining_cells < consts.SMALLEST_ALLOWED_BATCH: + if remaining_cells > 0: + logger.debug(f'Dropped last minibatch of {remaining_cells} cells') self._reset() raise StopIteration() @@ -101,9 +173,9 @@ def __next__(self): (1 - self.fraction_empties))) if self.empty_ind_list.size > 0: # This does not happen for 'simple' model. - empty_inds = self.random.choice(self.empty_ind_list, - size=n_empties, - replace=True) + empty_inds = np.random.choice(self.empty_ind_list, + size=n_empties, + replace=True) if empty_inds.size > 0: csr_list = [self.dataset[cell_inds, :], @@ -121,9 +193,8 @@ def __next__(self): return dense_tensor.to(device=self.device) -def prep_sparse_data_for_training(dataset: sp.csr.csr_matrix, - empty_drop_dataset: sp.csr.csr_matrix, - random_state: np.random.RandomState, +def prep_sparse_data_for_training(dataset: sp.csr_matrix, + empty_drop_dataset: sp.csr_matrix, training_fraction: float = consts.TRAINING_FRACTION, fraction_empties: float = consts.FRACTION_EMPTIES, batch_size: int = consts.DEFAULT_BATCH_SIZE, @@ -144,8 +215,6 @@ def prep_sparse_data_for_training(dataset: sp.csr.csr_matrix, columns are genes. empty_drop_dataset: Matrix of gene counts, where rows are surely-empty droplet barcodes and columns are genes. - random_state: numpy.random.RandomState from the Dataset object, for - deterministic outputs. training_fraction: Fraction of data to use as the training set. The rest becomes the test set. fraction_empties: Fraction of each minibatch to be composed of empty @@ -167,15 +236,14 @@ def prep_sparse_data_for_training(dataset: sp.csr.csr_matrix, """ # Choose train and test indices from analysis dataset. - training_mask = random_state.rand(dataset.shape[0]) < training_fraction + training_mask = np.random.rand(dataset.shape[0]) < training_fraction training_indices = [idx for idx in range(dataset.shape[0]) if training_mask[idx]] test_indices = [idx for idx in range(dataset.shape[0]) if not training_mask[idx]] # Choose train and test indices from empty drop dataset. - training_mask_empty = (random_state.rand(empty_drop_dataset.shape[0]) - < training_fraction) + training_mask_empty = (np.random.rand(empty_drop_dataset.shape[0]) < training_fraction) training_indices_empty = [idx for idx in range(empty_drop_dataset.shape[0]) if training_mask_empty[idx]] test_indices_empty = [idx for idx in range(empty_drop_dataset.shape[0]) @@ -204,7 +272,7 @@ def prep_sparse_data_for_training(dataset: sp.csr.csr_matrix, return train_loader, test_loader -def sparse_collate(batch: List[Tuple[sp.csr.csr_matrix]]) -> torch.Tensor: +def sparse_collate(batch: List[Tuple[sp.csr_matrix]]) -> torch.Tensor: """Load a minibatch of sparse data as a dense torch.Tensor in memory. Puts each data field into a tensor with leftmost dimension batch size. diff --git a/cellbender/remove_background/data/dataset.py b/cellbender/remove_background/data/dataset.py index a4eb84b..4a9f97e 100644 --- a/cellbender/remove_background/data/dataset.py +++ b/cellbender/remove_background/data/dataset.py @@ -1,26 +1,25 @@ """Class and functions for working with a count matrix dataset.""" -import tables import numpy as np import scipy.sparse as sp -import scipy.io as io -from scipy.stats import mode -from sklearn.decomposition import PCA import torch -import anndata -import cellbender.remove_background.model import cellbender.remove_background.consts as consts -from cellbender.remove_background.infer import ProbPosterior from cellbender.remove_background.data.dataprep import DataLoader - -from typing import Dict, List, Union, Tuple, Optional +from cellbender.remove_background.data.io import load_data +from cellbender.remove_background.data.priors import get_priors, \ + get_cell_count_given_expected_cells, \ + get_empty_count_given_expected_cells_and_total_droplets, \ + compute_crossover_surely_empty_and_stds +from cellbender.remove_background.sparse_utils import csr_set_rows_to_zero, \ + overwrite_matrix_with_columns_from_another + +from typing import Dict, List, Optional, Iterable, Callable import logging -import os +import argparse + -import matplotlib -matplotlib.use('Agg') -import matplotlib.pyplot as plt # This needs to be after matplotlib.use('Agg') +logger = logging.getLogger('cellbender') class SingleCellRNACountsDataset: @@ -32,12 +31,18 @@ class SingleCellRNACountsDataset: expected_cell_count: Expected number of real cells a priori. total_droplet_barcodes: Total number of droplets to include in the cell-calling analysis. + force_cell_umi_prior: User wants to force cell UMI prior to be this + force_empty_umi_prior: User wants to force empty UMI prior to be this + ambient_counts_in_cells_low_limit: Limit to determine how many features + are included in the analysis model_name: Model to use. gene_blacklist: List of integer indices of genes to exclude entirely. + exclude_features: List of feature types to exclude from the analysis. + Must be in ['Gene Expression', 'Antibody Capture', + 'CRISPR Guide Capture', 'Custom'] low_count_threshold: Droplets with UMI counts below this number are excluded entirely from the analysis. - lambda_multiplier: Float that multiplies background estimate before - MAP estimation. + fpr: Target expected false positive rate. Attributes: input_file: Name of data source file. @@ -52,7 +57,7 @@ class SingleCellRNACountsDataset: trim_dataset_for_analysis(). model_name: Name of model being run. priors: Priors estimated from the data useful for modelling. - posterior: Posterior estimated after inference. + posterior: BasePosterior estimated after inference. empty_UMI_threshold: This UMI count is the maximum UMI count in the user-defined surely empty droplets. @@ -64,397 +69,388 @@ class SingleCellRNACountsDataset: def __init__(self, input_file: str, model_name: str, - exclude_antibodies: bool, + exclude_features: List[str], low_count_threshold: int, fpr: List[float], expected_cell_count: Optional[int] = None, - total_droplet_barcodes: int = consts.TOTAL_DROPLET_DEFAULT, + total_droplet_barcodes: Optional[int] = None, + force_cell_umi_prior: Optional[float] = None, + force_empty_umi_prior: Optional[float] = None, fraction_empties: Optional[float] = None, + ambient_counts_in_cells_low_limit: float = consts.AMBIENT_COUNTS_IN_CELLS_LOW_LIMIT, gene_blacklist: List[int] = []): assert input_file is not None, "Attempting to load data, but no " \ "input file was specified." self.input_file = input_file self.analyzed_barcode_inds = np.array([]) # Barcodes trained each epoch - self.analyzed_gene_inds = np.array([]) - self.empty_barcode_inds = np.array([]) # Barcodes randomized each epoch + self.empty_barcode_inds = np.array([]) # Barcodes sampled randomly each epoch + self.low_count_cutoff = low_count_threshold self.data = None - self.exclude_antibodies = exclude_antibodies + self.gmm = None + self.exclude_features = exclude_features self.model_name = model_name self.fraction_empties = fraction_empties self.is_trimmed = False self.low_count_threshold = low_count_threshold - self.priors = {'n_cells': expected_cell_count} # Expected cells could be None. + self.ambient_counts_in_cells_low_limit = ambient_counts_in_cells_low_limit + self.priors = {} self.posterior = None self.fpr = fpr - self.random = np.random.RandomState(seed=1234) - self.EMPIRICAL_LOW_UMI_TO_EMPTY_DROPLET_THRESHOLD = \ - consts.EMPIRICAL_LOW_UMI_TO_EMPTY_DROPLET_THRESHOLD - self.SIMPLE_MODEL_D_STD_PRIOR = consts.SIMPLE_MODEL_D_STD_PRIOR - self.CELL_PROB_CUTOFF = consts.CELL_PROB_CUTOFF - - # Load the dataset. - self._load_data() - - # Trim the dataset. - self._trim_dataset_for_analysis(total_droplet_barcodes=total_droplet_barcodes, - low_UMI_count_cutoff=low_count_threshold, - gene_blacklist=gene_blacklist) - - # Estimate priors. - self._estimate_priors() - def _detect_input_data_type(self) -> str: - """Detect the type of input data.""" - - # Error if no input data file has been specified. - assert self.input_file is not None, \ - "Attempting to load data, but no input file was specified." - - # Detect type. - if os.path.isdir(self.input_file): - return 'cellranger_mtx' - - elif os.path.splitext(self.input_file)[1] == '.h5ad': - # assume unfiltered AnnData object, e.g. from kallisto | bustools - return 'anndata' - - else: - return 'cellranger_h5' - - def _load_data(self): - """Load a dataset into the SingleCellRNACountsDataset object from - the self.input_file""" - - # Detect input data type. - data_type = self._detect_input_data_type() + # Are empty droplets included in this model? + self.include_empties = False if (self.model_name in ['simple']) else True # Load the dataset. - if data_type == 'cellranger_mtx': + self.data = load_data(self.input_file) + self.analyzed_gene_logic = True - logging.info(f"Loading data from directory {self.input_file}") - self.data = get_matrix_from_cellranger_mtx(self.input_file) + # Eliminate feature types not used in the analysis. + self._exclude_feature_types() - elif data_type == 'cellranger_h5': + # Eliminate zero-count features and blacklisted features. + self._clean_features(gene_blacklist=gene_blacklist) - logging.info(f"Loading data from file {self.input_file}") - self.data = get_matrix_from_cellranger_h5(self.input_file) - - elif data_type == 'anndata': - - logging.info(f"Loading data from file {self.input_file}") - self.data = get_matrix_from_anndata(self.input_file) + # Estimate priors. + counts = np.array(self.data['matrix'] + [:, self.analyzed_gene_logic].sum(axis=1)).squeeze() + self.priors = get_priors(umi_counts=counts, low_count_threshold=low_count_threshold) + + # Overwrite heuristic priors with user inputs. + if expected_cell_count is not None: + logger.debug(f'Fixing expected_cells at {expected_cell_count}') + self.priors['expected_cells'] = expected_cell_count + self.priors.update( + get_cell_count_given_expected_cells( + umi_counts=counts, expected_cells=expected_cell_count, + ) + ) + if (expected_cell_count + consts.NUM_EMPTIES_INCREMENT) > self.priors['total_droplets']: + # Bump this up to avoid an immediate error + total_drops = expected_cell_count + consts.NUM_EMPTIES_INCREMENT + self.priors['total_droplets'] = total_drops + logger.debug(f'Incrementing total_droplets to be {total_drops}') + if total_droplet_barcodes is None: + # If this isn't getting recomputed next, recompute now + self.priors.update( + get_empty_count_given_expected_cells_and_total_droplets( + umi_counts=counts, expected_cells=self.priors['expected_cells'], + total_droplets=total_drops, + ) + ) + if total_droplet_barcodes is not None: + logger.debug(f'Fixing total_droplets at {total_droplet_barcodes}') + self.priors['total_droplets'] = total_droplet_barcodes + self.priors.update( + get_empty_count_given_expected_cells_and_total_droplets( + umi_counts=counts, expected_cells=self.priors['expected_cells'], + total_droplets=total_droplet_barcodes, + ) + ) + + # Force priors if user elects to do so. + if force_cell_umi_prior is not None: + logger.debug(f'Forcing cell UMI count prior to be {force_cell_umi_prior}') + self.priors['cell_counts'] = force_cell_umi_prior + if force_empty_umi_prior is not None: + logger.debug(f'Forcing empty droplet UMI count prior to be {force_empty_umi_prior}') + self.priors['empty_counts'] = force_empty_umi_prior + middle = np.sqrt(self.priors['cell_counts'] * force_empty_umi_prior) + self.priors['empty_count_upper_limit'] = min(middle, 2 * force_empty_umi_prior) + + # Recompute a few quantities if some things were replaced by user input. + compute_crossover_surely_empty_and_stds(umi_counts=counts, priors=self.priors) + logger.info(f"Prior on counts for cells is {int(self.priors['cell_counts'])}") + logger.info(f"Prior on counts for empty droplets is {int(self.priors['empty_counts'])}") + logger.debug('\n'.join(['Priors:'] + [f'{k}: {v}' for k, v in self.priors.items()])) + + # Do not analyze features which are not expected to contribute to noise. + self._trim_noiseless_features() + self.analyzed_gene_inds = np.where(self.analyzed_gene_logic)[0].astype(dtype=int) + logger.info(f"Including {len(self.analyzed_gene_inds)} features in the analysis.") + + # Determine barcodes to be analyzed. + self._trim_droplets() + self.is_trimmed = True - else: - raise NotImplementedError + # Estimate gene expression priors. + self.priors.update(self._estimate_chi_ambient()) - def _trim_dataset_for_analysis( - self, - total_droplet_barcodes: int = consts.TOTAL_DROPLET_DEFAULT, - low_UMI_count_cutoff: int = consts.LOW_UMI_CUTOFF, - gene_blacklist: List[int] = []): - """Trim the dataset for inference, choosing barcodes and genes to use. + logger.debug('\n'.join(['Priors:'] + [f'{k}: {v}' for k, v in self.priors.items()])) - Sets the values of self.analyzed_barcode_inds, and - self.empty_barcode_inds, which are used throughout training. + def _exclude_feature_types(self): + """Exclude feature types as specified by user. + Sets the value of self.analyzed_gene_logic + """ + + # Skip if feature types are not specified. + if self.data['feature_types'] is None: + if len(self.exclude_features) > 0: + logger.warning(f"WARNING: specified --exclude-feature-types " + f"{self.exclude_features} but no feature_type " + f"information was found in the input file. Proceeding " + f"without excluding any features. If this is not the " + f"intended outcome, please check your input file " + f"format.") + else: + logger.info('No feature type information specified in the input ' + 'file. Including all features in the analysis.') + return + + # Feature types are specified. + dtype = type(self.data['feature_types'][0]) + is_bytestring = (dtype != str) and (dtype != np.str_) + + def convert(s): + if is_bytestring: + return str(s, encoding='utf-8') + return s + + feature_type_array = np.array([convert(f) for f in self.data['feature_types']], dtype=str) + feature_types = np.unique(feature_type_array) + feature_info = [f"{(feature_type_array == f).sum()} {f}" for f in feature_types] + logger.info(f"Features in dataset: {', '.join(feature_info)}") + + # Keep track of a logical array for which features to include. + inclusion_logic = np.ones(len(feature_type_array), dtype=bool) # all True + + for feature in self.exclude_features: + logger.info(f"Excluding {feature} features (output will equal input).") + logic = np.array([f not in self.exclude_features + for f in feature_type_array], dtype=bool) + inclusion_logic = np.logical_and(inclusion_logic, logic) + logger.info(f" - This results in the exclusion of " + f"{len(feature_type_array) - logic.sum()} " + f"features.") + + # Update self.analyzed_gene_logic + self.analyzed_gene_logic = np.logical_and( + self.analyzed_gene_logic, + inclusion_logic, + ) + + def _clean_features(self, gene_blacklist: List[int] = []): + """Trim the dataset by removing zero-count and blacklisted features. + Sets the value of self.analyzed_gene_logic Args: - total_droplet_barcodes: Number of total droplets to include - during inference each epoch, and for which to make cell calls. - low_UMI_count_cutoff: Barcodes with total UMI counts below - this number are excluded. gene_blacklist: List of gene indices to trim out and exclude. - - Note: - self.priors['n_cells'] is only used to choose which barcodes to - include in the analysis, as well as to estimate the sizes of cells - and empty droplets (elsewhere). It need only be a reasonable guess. - The full analysis makes inferences about which barcodes contain - cells, and ultimately will determine this number. - However, if running only the 'simple' model, the expected_cell_count - is taken to be the true number of cells, since empty droplets are - not part of the 'simple' model. - """ - logging.info("Trimming dataset for inference.") + logger.info("Trimming features for inference.") # Get data matrix and barcode order that sorts barcodes by UMI count. matrix = self.data['matrix'] - umi_counts = np.array(matrix.sum(axis=1)).squeeze() - umi_count_order = np.argsort(umi_counts)[::-1] - - # Initially set the default to be the whole dataset. - self.analyzed_barcode_inds = np.arange(start=0, stop=matrix.shape[0]) - self.analyzed_gene_inds = np.arange(start=0, stop=matrix.shape[1]) - - # Expected cells must not exceed nonzero count barcodes. - num_nonzero_barcodes = np.sum(umi_counts > 0).item() # Choose which genes to use based on their having nonzero counts. # (All barcodes must be included so that inference can generalize.) - gene_counts_per_barcode = np.array(matrix.sum(axis=0)).squeeze() - if self.exclude_antibodies: - antibody_logic = (self.data['feature_types'] == b'Antibody Capture') - # Exclude these by setting their counts to zero - logging.info(f"Excluding {antibody_logic.sum()} features that " - f"correspond to antibody capture.") - gene_counts_per_barcode[antibody_logic] = 0 - nonzero_gene_counts = (gene_counts_per_barcode > 0) - self.analyzed_gene_inds = np.where(nonzero_gene_counts)[0].astype(dtype=int) - - if self.analyzed_gene_inds.size == 0: - logging.warning("During data loading, found no genes with > 0 " - "counts. Terminating analysis.") - raise AssertionError("No genes with nonzero counts. Check the " - "input data file.") + feature_counts_per_barcode = np.array(matrix.sum(axis=0)).squeeze() + inclusion_logic = (feature_counts_per_barcode > 0) + + # Update self.analyzed_gene_inds + self.analyzed_gene_logic = np.logical_and(self.analyzed_gene_logic, inclusion_logic) # Remove blacklisted genes. if len(gene_blacklist) > 0: - - # Make gene_blacklist a set for fast inclusion testing. gene_blacklist = set(gene_blacklist) + inclusion_logic = np.array([g not in gene_blacklist + for g in range(len(self.analyzed_gene_logic))]) + logger.info(f"Ignoring {np.logical_not(inclusion_logic).sum()} " + f"specified features.") + self.analyzed_gene_logic = np.logical_and( + self.analyzed_gene_logic, + inclusion_logic, + ) + + if self.analyzed_gene_logic.sum() == 0: + logger.warning("No features remain after eliminating zero-count " + "and ignored features. Terminating analysis.") + raise AssertionError("No features with nonzero counts remain. Check " + "the input data file and check " + "--exclude-feature-types and --ignore-features") + + logger.info(f"{self.analyzed_gene_logic.sum()} features have nonzero counts.") + + def _trim_noiseless_features(self): + """Trim the dataset for inference, choosing genes to use. + + To be run after trimming features first, then trimming barcodes. + + Sets the value of self.analyzed_gene_inds + """ - # Ensure genes on the blacklist are excluded. - self.analyzed_gene_inds = np.array([g for g in - self.analyzed_gene_inds - if g not in gene_blacklist]) - - if self.analyzed_gene_inds.size == 0: - logging.warning("All nonzero genes have been blacklisted. " - "Terminating analysis.") - raise AssertionError("All genes with nonzero counts have been " - "blacklisted. Examine the dataset and " - "reduce the blacklist.") - - logging.info(f"Including {self.analyzed_gene_inds.size} genes that have" - f" nonzero counts.") - - # Estimate priors on cell size and 'empty' droplet size. - self.priors['cell_counts'], self.priors['empty_counts'] = \ - get_d_priors_from_dataset(self) # After gene trimming + assert len(self.priors.keys()) > 0, 'Run self.priors = get_priors() before ' \ + 'self._trim_noiseless_features()' + + # Find average counts per gene in empty droplets. + count_matrix = self.data['matrix'][:, self.analyzed_gene_logic] + counts = np.array(count_matrix.sum(axis=1)).squeeze() + cutoff = self.priors['empty_count_upper_limit'] + count_matrix_empties = count_matrix[(counts < cutoff) + & (counts > self.low_count_threshold), :] + mean_counts_per_empty_g = np.array(count_matrix_empties.mean(axis=0)).squeeze() + + # Estimate counts in cells, but use a minimum number of cells just as a floor. + ambient_counts_in_cells_g = (max(self.priors['expected_cells'], 5000) + * mean_counts_per_empty_g) + + # Include features that are projected to have noise above the low limit. + inclusion_logic_subset = (ambient_counts_in_cells_g + > self.ambient_counts_in_cells_low_limit) + inclusion_logic = np.ones(len(self.analyzed_gene_logic), dtype=bool) + inclusion_logic[self.analyzed_gene_logic] = inclusion_logic_subset + + # Record in self.analyzed_gene_logic + self.analyzed_gene_logic = np.logical_and( + self.analyzed_gene_logic, + inclusion_logic, + ) + logger.info(f"Excluding {np.logical_not(inclusion_logic).sum()} features that " + f"are estimated to have <= {self.ambient_counts_in_cells_low_limit} " + f"background counts in cells.") + + def _trim_droplets(self): + """Trim the dataset for inference, choosing barcodes to use. - # Estimate the number of real cells if it was not specified. - if self.priors['n_cells'] is None: - self.priors['n_cells'] = estimate_cell_count_from_dataset(self) + Sets the values of self.analyzed_barcode_inds, and + self.empty_barcode_inds, which are used throughout training. + """ - # n_cells is at most the number of nonzero barcodes. - n_cells = min(self.priors['n_cells'], num_nonzero_barcodes) + logger.info("Trimming barcodes for inference.") - assert n_cells > 0, "No cells identified. Try to use the option " \ - "--expected-cells to specify a prior on cell " \ - "count. Also ensure that --low-count-threshold " \ - "is not too large (less than empty droplets)." + # Get data matrix and barcode order that sorts barcodes by UMI count. + matrix = self.get_count_matrix_all_barcodes() # self.data['matrix'] + umi_counts = np.array(matrix.sum(axis=1)).squeeze() + # umi_counts_features_trimmed = self.get_count_matrix_all_barcodes() + umi_count_order = np.argsort(umi_counts)[::-1] + n_cells = self.priors['expected_cells'] + total_droplet_barcodes = self.priors['total_droplets'] # If running the simple model, just use the expected cells, no more. - if self.model_name == "simple": + if not self.include_empties: self.analyzed_barcode_inds = np.array(umi_count_order[:n_cells], dtype=int) - logging.info(f"Simple model: using " - f"{self.analyzed_barcode_inds.size} cell barcodes.") + logger.info(f"Excluding empty droplets due to '{self.model_name}' model: " + f"using {self.analyzed_barcode_inds.size} cell barcodes.") # If not using the simple model, include empty droplets. else: - # Get the cell barcodes. - cell_barcodes = umi_count_order[:n_cells] + assert total_droplet_barcodes - n_cells > 0, \ + f"The number of cells is {n_cells}, but the " \ + f"number of total droplets included is " \ + f"{total_droplet_barcodes}. Increase " \ + f"--total_droplet_barcodes above {n_cells}, or " \ + f"specify a different number of expected cells using " \ + f"--expected-cells" # Set the low UMI count cutoff to be the greater of either # the user input value, or an empirically-derived value. - empirical_low_UMI = int(self.priors['empty_counts'] * - self.EMPIRICAL_LOW_UMI_TO_EMPTY_DROPLET_THRESHOLD) - low_UMI_count_cutoff = max(low_UMI_count_cutoff, - empirical_low_UMI) - logging.info(f"Excluding barcodes with counts below " - f"{low_UMI_count_cutoff}") + factor = consts.EMPIRICAL_LOW_UMI_TO_EMPTY_DROPLET_THRESHOLD + empirical_low_count_cutoff = int(self.priors['empty_counts'] * factor) + low_count_cutoff = max(self.low_count_threshold, empirical_low_count_cutoff) + self.low_count_cutoff = low_count_cutoff + logger.info(f"Excluding barcodes with counts below {low_count_cutoff}") # See how many barcodes there are to work with total. - num_barcodes_above_umi_cutoff = \ - np.sum(umi_counts > low_UMI_count_cutoff).item() + num_barcodes_above_umi_cutoff = np.sum(umi_counts > low_count_cutoff).item() assert num_barcodes_above_umi_cutoff > 0, \ f"There are no barcodes with UMI counts over the lower " \ - f"cutoff of {low_UMI_count_cutoff}" + f"cutoff of {low_count_cutoff}" assert num_barcodes_above_umi_cutoff > n_cells, \ f"There are no empty droplets with UMI counts over the lower " \ - f"cutoff of {low_UMI_count_cutoff}. Some empty droplets are " \ + f"cutoff of {low_count_cutoff}. Some empty droplets are " \ f"necessary for the analysis. Reduce the " \ f"--low-count-threshold parameter." - # Get a number of transition-region barcodes. - num_transition_barcodes = (total_droplet_barcodes - - cell_barcodes.size) - - assert num_transition_barcodes > 0, \ - f"The number of cells is {cell_barcodes.size}, but the " \ - f"number of total droplets included is " \ - f"{total_droplet_barcodes}. Increase " \ - f"--total_droplet_barcodes above {cell_barcodes.size}, or " \ - f"specify a different number of expected cells using " \ - f"--expected-cells" - - num = min(num_transition_barcodes, - num_barcodes_above_umi_cutoff - cell_barcodes.size) - num = max(0, num) - transition_barcodes = umi_count_order[n_cells: - (n_cells + num)] - - assert transition_barcodes.size > 0, \ - f"There are no barcodes identified from the transition " \ - f"region between cell and empty. The intended number of " \ - f"transition barcodes was {num_transition_barcodes}. " \ - f"This indicates that the low UMI count cutoff, " \ - f"{low_UMI_count_cutoff}, was likely too high. Try to " \ - f"reduce --low-count-threshold" - # Use the cell barcodes and transition barcodes for analysis. - self.analyzed_barcode_inds = np.concatenate(( - cell_barcodes, - transition_barcodes)).astype(dtype=int) - - # Identify probable empty droplet barcodes. - if num < num_transition_barcodes: + self.analyzed_barcode_inds = umi_count_order[:total_droplet_barcodes] - # This means we already used all the barcodes. - empty_droplet_barcodes = np.array([]) - - else: - - # Decide which empty barcodes to include. - empty_droplet_sorted_barcode_inds = \ - np.arange(n_cells + num, num_barcodes_above_umi_cutoff, - dtype=int) # The entire range - empty_droplet_barcodes = \ - umi_count_order[empty_droplet_sorted_barcode_inds] - - self.empty_barcode_inds = empty_droplet_barcodes\ - .astype(dtype=int) + # Decide which empty barcodes to include. + empty_droplet_sorted_barcode_inds = \ + np.arange(total_droplet_barcodes, num_barcodes_above_umi_cutoff, + dtype=int) # The entire range + self.empty_barcode_inds = umi_count_order[empty_droplet_sorted_barcode_inds] # Find the UMI threshold for surely empty droplets. - last_analyzed_bc = min(cell_barcodes.size + transition_barcodes.size, - umi_count_order.size) - self.empty_UMI_threshold = (umi_counts[umi_count_order] - [last_analyzed_bc]) + self.empty_UMI_threshold = umi_counts[umi_count_order][total_droplet_barcodes] # Find the max UMI count for any cell. self.max_UMI_count = umi_counts.max() - logging.info(f"Using {cell_barcodes.size} probable cell " - f"barcodes, plus an additional " - f"{transition_barcodes.size} barcodes, " - f"and {empty_droplet_barcodes.size} empty " - f"droplets.") - logging.info(f"Largest surely-empty droplet has " - f"{self.empty_UMI_threshold} UMI counts.") - - if ((low_UMI_count_cutoff == self.low_count_threshold) - and (empty_droplet_barcodes.size == 0)): - logging.warning("Warning: few empty droplets identified. Low " - "UMI cutoff may be too high. Check the UMI " - "decay curve, and decrease the " - "--low-count-threshold parameter if necessary.") - - self.is_trimmed = True - - def _estimate_priors(self): - """Estimate relevant priors, populating fields in the self.priors dict. - """ - - # Estimate the log UMI count turning point between cells and 'empties'. - self.priors['log_counts_crossover'] = \ - np.mean(np.log1p([self.priors['cell_counts'], - self.priors['empty_counts']])).item() - - # TODO: overhaul estimation of d_std. add estimate of d_empty_std - - # Estimate prior for the scale param of LogNormal for d. - if self.model_name != "simple": - self.priors['d_std'] = (np.log1p(self.priors['cell_counts']) - - self.priors['log_counts_crossover']) / 5 - else: - # Use a reasonable prior in log space. - self.priors['d_std'] = self.SIMPLE_MODEL_D_STD_PRIOR - - # Priors for models that include empty droplets: - if self.model_name != "simple": - # Estimate fraction of trimmed dataset that contains cells. - # cell_prob = self.priors['n_cells'] - # / self.analyzed_barcode_inds.size - cell_prob = (1 - self.fraction_empties) \ - * (self.priors['n_cells'] - / self.analyzed_barcode_inds.size) - self.priors['cell_prob'] = cell_prob - - assert cell_prob > 0, f"Fraction of trimmed dataset " \ - f"containing cells should be > 0, " \ - f"but is {cell_prob}." - - assert cell_prob <= 1, f"Fraction of trimmed dataset " \ - f"containing cells should be at most 1, " \ - f"but is {cell_prob}." - - # Turn cell probability into logit. - self.priors['cell_logit'] = np.log(cell_prob - / (1 - cell_prob)).item() - - # Estimate the ambient gene expression profile. - self.priors['chi_ambient'], self.priors['chi_bar'] = \ - estimate_chi_ambient_from_dataset(self) - - def get_count_matrix(self) -> sp.csr.csr_matrix: + # Estimate cell logit probability prior. + cell_prob = (n_cells / total_droplet_barcodes) * (1. - self.fraction_empties) + self.priors['cell_logit'] = np.log(cell_prob) - np.log(1. - cell_prob) + + logger.info(f"Using {n_cells} probable cell " + f"barcodes, plus an additional " + f"{total_droplet_barcodes - n_cells} barcodes, " + f"and {len(self.empty_barcode_inds)} empty " + f"droplets.") + logger.info(f"Largest surely-empty droplet has " + f"{int(self.empty_UMI_threshold)} UMI counts.") + + if ((low_count_cutoff == self.low_count_threshold) + and (len(self.empty_barcode_inds) == 0)): + logger.warning("Warning: few empty droplets identified. Low " + "UMI cutoff may be too high. Check the UMI " + "decay curve, and decrease the " + "--low-count-threshold parameter if necessary.") + + def _estimate_chi_ambient(self) -> Dict[str, torch.Tensor]: + """Estimate chi_ambient and chi_bar""" + + matrix = self.data['matrix'].tocsc() + count_matrix = matrix[:, self.analyzed_gene_inds].tocsr() + umi_counts = np.array(count_matrix.sum(axis=1)).squeeze() + + # Estimate the ambient gene expression profile. + ep = np.finfo(np.float32).eps.item() # small value + empty_droplet_logic = ((umi_counts < self.priors['surely_empty_counts']) + & (umi_counts > self.low_count_cutoff)) + chi_ambient = np.array(count_matrix[empty_droplet_logic, :].sum(axis=0)).squeeze() + ep + chi_ambient = torch.tensor(chi_ambient / chi_ambient.sum()).float() + chi_bar = np.array(count_matrix.sum(axis=0)).squeeze() + ep + chi_bar = torch.tensor(chi_bar / chi_bar.sum()).float() + + return {'chi_ambient': chi_ambient, 'chi_bar': chi_bar} + + def get_count_matrix(self) -> sp.csr_matrix: """Get the count matrix, trimmed if trimming has occurred.""" - if self.is_trimmed: - - # Return the count matrix for selected barcodes and genes. - trimmed_bc_matrix = self.data['matrix'][self.analyzed_barcode_inds, - :].tocsc() - trimmed_matrix = trimmed_bc_matrix[:, self.analyzed_gene_inds].tocsr() - return trimmed_matrix + # Return the count matrix for selected barcodes and genes. + trimmed_bc_matrix = self.data['matrix'][self.analyzed_barcode_inds, + :].tocsc() + trimmed_matrix = trimmed_bc_matrix[:, self.analyzed_gene_inds].tocsr() + return trimmed_matrix - else: - logging.warning("Using full count matrix, without any trimming. " - "Could be slow.") - return self.data['matrix'] - - def get_count_matrix_empties(self) -> sp.csr.csr_matrix: + def get_count_matrix_empties(self) -> sp.csr_matrix: """Get the count matrix for empty drops, trimmed if possible.""" - if self.is_trimmed: - - # Return the count matrix for selected barcodes and genes. - trimmed_bc_matrix = self.data['matrix'][self.empty_barcode_inds, - :].tocsc() - trimmed_matrix = trimmed_bc_matrix[:, self.analyzed_gene_inds].tocsr() - return trimmed_matrix - - else: - logging.error("Trying to get empty count matrix without " - "trimmed data.") - return self.data['matrix'] + # Return the count matrix for selected barcodes and genes. + trimmed_bc_matrix = self.data['matrix'][self.empty_barcode_inds, + :].tocsc() + trimmed_matrix = trimmed_bc_matrix[:, self.analyzed_gene_inds].tocsr() + return trimmed_matrix - def get_count_matrix_all_barcodes(self) -> sp.csr.csr_matrix: + def get_count_matrix_all_barcodes(self) -> sp.csr_matrix: """Get the count matrix, trimming only genes, not barcodes.""" - if self.is_trimmed: - - # Return the count matrix for selected barcodes and genes. - trimmed_bc_matrix = self.data['matrix'].tocsc() - trimmed_matrix = trimmed_bc_matrix[:, self.analyzed_gene_inds].tocsr() - return trimmed_matrix - - else: - logging.warning("Using full count matrix, without any trimming. " - "Could be slow.") - return self.data['matrix'] + # Return the count matrix for selected barcodes and genes. + trimmed_bc_matrix = self.data['matrix'].tocsc() + trimmed_matrix = trimmed_bc_matrix[:, self.analyzed_gene_inds].tocsr() + return trimmed_matrix def get_dataloader(self, use_cuda: bool = True, batch_size: int = 200, shuffle: bool = False, - analyzed_bcs_only: bool = True) -> DataLoader: + analyzed_bcs_only: bool = True, + sort_by: Optional[Callable[[sp.csr_matrix], float]] = None, + ) -> DataLoader: """Return a dataloader for the count matrix. Args: @@ -463,6 +459,10 @@ def get_dataloader(self, shuffle: Whether dataloader should shuffle the data. analyzed_bcs_only: Only include the barcodes that have been analyzed, not the surely empty droplets. + sort_by: Lambda function which, when applied to the sparse matrix, + will return values that can be sorted to give a sort order to + the dataset. Dataloader will load data in order of increasing + values. Returns: data_loader: A dataloader that yields the entire dataset in batches. @@ -474,1083 +474,66 @@ def get_dataloader(self, else: count_matrix = self.get_count_matrix_all_barcodes() - return DataLoader(count_matrix, - empty_drop_dataset=None, - batch_size=batch_size, - fraction_empties=0., - shuffle=shuffle, - use_cuda=use_cuda) - - def save_to_output_file( + data_loader = DataLoader( + count_matrix, + empty_drop_dataset=None, + batch_size=batch_size, + fraction_empties=0., + shuffle=shuffle, + sort_by=sort_by, + use_cuda=use_cuda, + ) + return data_loader + + def restore_eliminated_features_in_cells( self, - output_file: str, - inferred_model: 'RemoveBackgroundPyroModel', - posterior_batch_size: int, - cells_posterior_reg_calc: int, - save_plots: bool = False) -> bool: - """Write the results of an inference procedure to an output file. - - Output is an HDF5 file. To be written: - Inferred ambient-subtracted UMI count matrix. - Inferred probabilities that each barcode contains a real cell. - Inferred cell size scale factors. - Inferred ambient gene expression count vector for droplets without - cells. - Inferred contamination fraction hyperparameters. - Embeddings of gene expression of cells into a low-dimensional latent - space. + inferred_count_matrix: sp.csc_matrix, + cell_probabilities_analyzed_bcs: np.ndarray + ) -> sp.csc_matrix: + """Add the data back in for any features not used during inference, + (the used features being defined in self.analyzed_gene_inds), + so that the output for these features exactly matches the input. Args: - inferred_model: RemoveBackgroundPyroModel which has - already had the inference procedure run. - output_file: Name of output .h5 file - save_plots: Setting this to True will save plots of outputs. + inferred_count_matrix: A sparse count matrix + cell_probabilities_analyzed_bcs: Cell probabilities for the subset of + barcodes that were analyzed. Returns: - True if the output was written to file successfully. + output: A sparse count matrix, with the unused feature values filled + in with the raw data. """ - logging.info("Preparing to write outputs to file...") - - # Create posterior. - self.posterior = ProbPosterior(dataset_obj=self, - vi_model=inferred_model, - fpr=self.fpr[0], - batch_size=posterior_batch_size, - cells_posterior_reg_calc=cells_posterior_reg_calc) - - # Encoded values of latent variables. - enc = self.posterior.latents - z = enc['z'] - d = enc['d'] - p = enc['p'] - epsilon = enc['epsilon'] - phi_params = enc['phi_loc_scale'] - - # Estimate the ambient-background-subtracted UMI count matrix. - if self.model_name != "simple": - - inferred_count_matrix = self.posterior.mean - - else: - - # No need to generate a new count matrix for simple model. - inferred_count_matrix = self.data['matrix'].tocsc() - logging.info("Simple model: outputting un-altered count matrix.") - - # Inferred ambient gene expression vector. - ambient_expression_trimmed = cellbender.remove_background.model.\ - get_ambient_expression() - - # Convert the indices from trimmed gene set to original gene indices. - ambient_expression = np.zeros(self.data['matrix'].shape[1]) - ambient_expression[self.analyzed_gene_inds] = ambient_expression_trimmed - - # Figure out the indices of barcodes that have cells. - if p is not None: - p[np.isnan(p)] = 0. - cell_barcode_inds = self.analyzed_barcode_inds - if np.sum(p > 0.5) == 0: - logging.warning("Warning: Found no cells!") - filtered_inds_of_analyzed_barcodes = p > self.CELL_PROB_CUTOFF - else: - cell_barcode_inds = self.analyzed_barcode_inds - filtered_inds_of_analyzed_barcodes = np.arange(0, d.size) - - # CellRanger version (format output like input). - if self._detect_input_data_type() == 'cellranger_h5': - cellranger_version = detect_cellranger_version_h5(self.input_file) - elif self._detect_input_data_type() == 'anndata': - # Arbitrarily peg output format for anndata inputs to be CellRanger v3 format - cellranger_version = 3 - else: - cellranger_version = detect_cellranger_version_mtx(self.input_file) - - # Write to output file, for each lambda specified by user. - if len(self.fpr) == 1: - write_succeeded = write_matrix_to_cellranger_h5( - cellranger_version=cellranger_version, - output_file=output_file, - gene_names=self.data['gene_names'], - gene_ids=self.data['gene_ids'], - feature_types=self.data['feature_types'], - genomes=self.data['genomes'], - barcodes=self.data['barcodes'], - inferred_count_matrix=inferred_count_matrix, - cell_barcode_inds=cell_barcode_inds, - ambient_expression=ambient_expression, - z=z, d=d, p=p, phi=phi_params, epsilon=epsilon, - rho=cellbender.remove_background.model.get_rho(), - fpr=self.fpr[0], - lambda_multiplier=self.posterior.lambda_multiplier, - loss=inferred_model.loss) - - # Generate filename for filtered matrix output. - file_dir, file_base = os.path.split(output_file) - file_name = os.path.splitext(os.path.basename(file_base))[0] - filtered_output_file = os.path.join(file_dir, file_name + "_filtered.h5") - - # Write filtered matrix (cells only) to output file. - if self.model_name != "simple": - cell_barcode_inds = \ - self.analyzed_barcode_inds[filtered_inds_of_analyzed_barcodes] - - cell_barcodes = self.data['barcodes'][cell_barcode_inds] - - write_matrix_to_cellranger_h5( - cellranger_version=cellranger_version, - output_file=filtered_output_file, - gene_names=self.data['gene_names'], - gene_ids=self.data['gene_ids'], - feature_types=self.data['feature_types'], - genomes=self.data['genomes'], - barcodes=cell_barcodes, - inferred_count_matrix=inferred_count_matrix[cell_barcode_inds, :], - cell_barcode_inds=None, - ambient_expression=ambient_expression, - z=z[filtered_inds_of_analyzed_barcodes, :], - d=d[filtered_inds_of_analyzed_barcodes], - p=p[filtered_inds_of_analyzed_barcodes], - phi=phi_params, - epsilon=epsilon[filtered_inds_of_analyzed_barcodes], - rho=cellbender.remove_background.model.get_rho(), - fpr=self.fpr[0], - lambda_multiplier=self.posterior.lambda_multiplier, - loss=inferred_model.loss) - - else: - - file_dir, file_base = os.path.split(output_file) - file_name = os.path.splitext(os.path.basename(file_base))[0] - - for i, fpr in enumerate(self.fpr): - - if i > 0: # first FPR has already been computed - - # Re-compute posterior counts for each new lambda. - self.posterior.fpr = fpr # reach in and change the FPR - self.posterior._get_mean() # force re-computation of posterior - inferred_count_matrix = self.posterior.mean - - # Create an output filename for this lambda value. - temp_output_filename = os.path.join(file_dir, file_name + f"_FPR_{fpr}.h5") - - write_succeeded = write_matrix_to_cellranger_h5( - cellranger_version=cellranger_version, - output_file=temp_output_filename, - gene_names=self.data['gene_names'], - gene_ids=self.data['gene_ids'], - feature_types=self.data['feature_types'], - genomes=self.data['genomes'], - barcodes=self.data['barcodes'], - inferred_count_matrix=inferred_count_matrix, - cell_barcode_inds=self.analyzed_barcode_inds, - ambient_expression=ambient_expression, - z=z, d=d, p=p, phi=phi_params, epsilon=epsilon, - rho=cellbender.remove_background.model.get_rho(), - fpr=fpr, - lambda_multiplier=self.posterior.lambda_multiplier, - loss=inferred_model.loss) - - # Generate filename for filtered matrix output. - filtered_output_file = os.path.join(file_dir, file_name + f"_FPR_{fpr}_filtered.h5") - - # Write filtered matrix (cells only) to output file. - if self.model_name != "simple": - cell_barcode_inds = \ - self.analyzed_barcode_inds[filtered_inds_of_analyzed_barcodes] - - cell_barcodes = self.data['barcodes'][cell_barcode_inds] - - write_matrix_to_cellranger_h5( - cellranger_version=cellranger_version, - output_file=filtered_output_file, - gene_names=self.data['gene_names'], - gene_ids=self.data['gene_ids'], - feature_types=self.data['feature_types'], - genomes=self.data['genomes'], - barcodes=cell_barcodes, - inferred_count_matrix=inferred_count_matrix[cell_barcode_inds, :], - cell_barcode_inds=None, - ambient_expression=ambient_expression, - z=z[filtered_inds_of_analyzed_barcodes, :], - d=d[filtered_inds_of_analyzed_barcodes], - p=p[filtered_inds_of_analyzed_barcodes], - phi=phi_params, - epsilon=epsilon[filtered_inds_of_analyzed_barcodes], - rho=cellbender.remove_background.model.get_rho(), - fpr=fpr, - lambda_multiplier=self.posterior.lambda_multiplier, - loss=inferred_model.loss) - - # Save barcodes determined to contain cells as _cell_barcodes.csv - try: - barcode_names = np.array([str(cell_barcodes[i], - encoding='UTF-8') - for i in range(cell_barcodes.size)]) - except UnicodeDecodeError: - # necessary if barcodes are ints - barcode_names = cell_barcodes - except TypeError: - # necessary if barcodes are already decoded - barcode_names = cell_barcodes - bc_file_name = os.path.join(file_dir, - file_name + "_cell_barcodes.csv") - np.savetxt(bc_file_name, barcode_names, delimiter=',', fmt='%s') - logging.info(f"Saved cell barcodes in {bc_file_name}") - - try: - # Save plots, if called for. - if save_plots: - plt.figure(figsize=(6, 18)) - - # Plot the train error. - plt.subplot(3, 1, 1) - plt.plot(inferred_model.loss['train']['elbo'], '.--', - label='Train') - - # Plot the test error, if there was held-out test data. - if 'test' in inferred_model.loss.keys(): - if len(inferred_model.loss['test']['epoch']) > 0: - plt.plot(inferred_model.loss['test']['epoch'], - inferred_model.loss['test']['elbo'], 'o:', - label='Test') - plt.legend() - - plt.gca().set_ylim(bottom=max(inferred_model.loss['train'] - ['elbo'][0], - inferred_model.loss['train'] - ['elbo'][-1] - 2000)) - plt.xlabel('Epoch') - plt.ylabel('ELBO') - plt.title('Progress of the training procedure') - - # Plot the barcodes used, along with the inferred - # cell probabilities. - plt.subplot(3, 1, 2) - count_mat = self.get_count_matrix() - counts = np.array(count_mat.sum(axis=1)).squeeze() - count_order = np.argsort(counts)[::-1] - plt.semilogy(counts[count_order], color='black') - plt.ylabel('UMI counts') - plt.xlabel('Barcode index, sorted by UMI count') - if p is not None: # The case of a simple model. - plt.gca().twinx() - plt.plot(p[count_order], '.:', color='red', alpha=0.3) - plt.ylabel('Cell probability', color='red') - plt.ylim([-0.05, 1.05]) - plt.title('Determination of which barcodes contain cells') - else: - plt.title('The subset of barcodes used for training') - - # Plot the latent encoding via PCA. - plt.subplot(3, 1, 3) - pca = PCA(n_components=2) - if p is None: - p = np.ones_like(d) - z_pca = pca.fit_transform(z[p >= 0.5]) - plt.plot(z_pca[:, 0], z_pca[:, 1], - '.', ms=3, color='black', alpha=0.3) - plt.ylabel('PC 1') - plt.xlabel('PC 0') - plt.title('PCA of latent encoding of cell gene expression') - - file_dir, file_base = os.path.split(output_file) - file_name = os.path.splitext(os.path.basename(file_base))[0] - fig_name = os.path.join(file_dir, file_name + ".pdf") - plt.savefig(fig_name, bbox_inches='tight', format='pdf') - logging.info(f"Saved summary plots as {fig_name}") - - except Exception: - logging.warning("Unable to save plot.") - - return write_succeeded - - -def detect_cellranger_version_mtx(filedir: str) -> int: - """Detect which version of CellRanger (2 or 3) created this mtx directory. - - Args: - filedir: string path to .mtx file that contains the raw gene - barcode matrix in a sparse coo text format. - - Returns: - CellRanger version, either 2 or 3, as an integer. - - """ - - assert os.path.isdir(filedir), f"The directory {filedir} is not accessible." - - if os.path.isfile(os.path.join(filedir, 'features.tsv.gz')): - return 3 - - else: - return 2 - - -def get_matrix_from_cellranger_mtx(filedir: str) \ - -> Dict[str, Union[sp.csr.csr_matrix, List[np.ndarray], np.ndarray]]: - """Load a count matrix from an mtx directory from CellRanger's output. - - For CellRanger v2: - The directory must contain three files: - matrix.mtx - barcodes.tsv - genes.tsv - - For CellRanger v3: - The directory must contain three files: - matrix.mtx.gz - barcodes.tsv.gz - features.tsv.gz - - This function returns a dictionary that includes the count matrix, the gene - names (which correspond to columns of the count matrix), and the barcodes - (which correspond to rows of the count matrix). - - Args: - filedir: string path to .mtx file that contains the raw gene - barcode matrix in a sparse coo text format. - - Returns: - out['matrix']: scipy.sparse.csr.csr_matrix of unique UMI counts, with - barcodes as rows and genes as columns - out['barcodes']: numpy array of strings which are the nucleotide - sequences of the barcodes that correspond to the rows in - the out['matrix'] - out['gene_names']: List of numpy arrays, where the number of elements - in the list is the number of genomes in the dataset. Each numpy - array contains the string names of genes in the genome, which - correspond to the columns in the out['matrix']. - out['gene_ids']: List of numpy arrays, where the number of elements - in the list is the number of genomes in the dataset. Each numpy - array contains the string Ensembl ID of genes in the genome, which - also correspond to the columns in the out['matrix']. - out['feature_types']: List of numpy arrays, where the number of elements - in the list is the number of genomes in the dataset. Each numpy - array contains the string feature types of genes (or possibly - antibody capture reads), which also correspond to the columns - in the out['matrix']. - - """ - - assert os.path.isdir(filedir), "The directory {filedir} is not accessible." - - # Decide whether data is CellRanger v2 or v3. - cellranger_version = detect_cellranger_version_mtx(filedir=filedir) - logging.info(f"CellRanger v{cellranger_version} format") - - # CellRanger version 3 - if cellranger_version == 3: - - matrix_file = os.path.join(filedir, 'matrix.mtx.gz') - gene_file = os.path.join(filedir, 'features.tsv.gz') - barcode_file = os.path.join(filedir, 'barcodes.tsv.gz') - - # Read in feature names. - features = np.genfromtxt(fname=gene_file, - delimiter="\t", skip_header=0, - dtype=' int: - """Detect which version of CellRanger (2 or 3) created this h5 file. - - Args: - filename: string path to .mtx file that contains the raw gene - barcode matrix in a sparse coo text format. - - Returns: - version: CellRanger version, either 2 or 3, as an integer. - - """ - - with tables.open_file(filename, 'r') as f: - - # For CellRanger v2, each group in the table (other than root) - # contains a genome. - # For CellRanger v3, there is a 'matrix' group that contains 'features'. - - version = 2 - - try: - - # This works for version 3 but not for version 2. - getattr(f.root.matrix, 'features') - version = 3 - - except tables.NoSuchNodeError: - pass - - return version - - -def get_matrix_from_cellranger_h5(filename: str) \ - -> Dict[str, Union[sp.csr.csr_matrix, List[np.ndarray], np.ndarray]]: - """Load a count matrix from an h5 file from CellRanger's output. - - The file needs to be a _raw_gene_bc_matrices_h5.h5 file. This function - returns a dictionary that includes the count matrix, the gene names (which - correspond to columns of the count matrix), and the barcodes (which - correspond to rows of the count matrix). - - This function works for CellRanger v2 and v3 HDF5 formats. - - Args: - filename: string path to .h5 file that contains the raw gene - barcode matrices - - Returns: - out['matrix']: scipy.sparse.csr.csr_matrix of unique UMI counts, with - barcodes as rows and genes as columns - out['barcodes']: numpy array of strings which are the nucleotide - sequences of the barcodes that correspond to the rows in - the out['matrix'] - out['gene_names']: List of numpy arrays, where the number of elements - in the list is the number of genomes in the dataset. Each numpy - array contains the string names of genes in the genome, which - correspond to the columns in the out['matrix']. - out['gene_ids']: List of numpy arrays, where the number of elements - in the list is the number of genomes in the dataset. Each numpy - array contains the string Ensembl ID of genes in the genome, which - also correspond to the columns in the out['matrix']. - out['feature_types']: List of numpy arrays, where the number of elements - in the list is the number of genomes in the dataset. Each numpy - array contains the string feature types of genes (or possibly - antibody capture reads), which also correspond to the columns - in the out['matrix']. - - """ - - # Detect CellRanger version. - cellranger_version = detect_cellranger_version_h5(filename=filename) - logging.info(f"CellRanger v{cellranger_version} format") - - with tables.open_file(filename, 'r') as f: - # Initialize empty lists. - csc_list = [] - barcodes = None - feature_ids = None - feature_types = None - genomes = None - - # CellRanger v2: - # Each group in the table (other than root) contains a genome, - # so walk through the groups to get data for each genome. - if cellranger_version == 2: - - feature_names = [] - feature_ids = [] - - for group in f.walk_groups(): - try: - # Read in data for this genome, and put it into a - # scipy.sparse.csc.csc_matrix - barcodes = getattr(group, 'barcodes').read() - data = getattr(group, 'data').read() - indices = getattr(group, 'indices').read() - indptr = getattr(group, 'indptr').read() - shape = getattr(group, 'shape').read() - csc_list.append(sp.csc_matrix((data, indices, indptr), - shape=shape)) - feature_names.extend(getattr(group, 'gene_names').read()) - feature_ids.extend(getattr(group, 'genes').read()) - - except tables.NoSuchNodeError: - # This exists to bypass the root node, which has no data. - pass - - # Create numpy arrays. - feature_names = np.array(feature_names) - if len(feature_ids) > 0: - feature_ids = np.array(feature_ids) - else: - feature_ids = None - - # CellRanger v3: - # There is only the 'matrix' group. - elif cellranger_version == 3: - - # Read in data for this genome, and put it into a - # scipy.sparse.csc.csc_matrix - barcodes = getattr(f.root.matrix, 'barcodes').read() - data = getattr(f.root.matrix, 'data').read() - indices = getattr(f.root.matrix, 'indices').read() - indptr = getattr(f.root.matrix, 'indptr').read() - shape = getattr(f.root.matrix, 'shape').read() - csc_list.append(sp.csc_matrix((data, indices, indptr), - shape=shape)) - - # Read in 'feature' information - feature_group = f.get_node(f.root.matrix, 'features') - feature_names = getattr(feature_group, 'name').read() - - try: - feature_types = getattr(feature_group, 'feature_type').read() - except tables.NoSuchNodeError: - # This exists in case someone produced a file without feature_type. - pass - try: - feature_ids = getattr(feature_group, 'id').read() - except tables.NoSuchNodeError: - # This exists in case someone produced a file without feature id. - pass - try: - genomes = getattr(feature_group, 'genome').read() - except tables.NoSuchNodeError: - # This exists in case someone produced a file without feature genome. - pass - - # Put the data together (possibly from several genomes for v2 datasets). - count_matrix = sp.vstack(csc_list, format='csc') - count_matrix = count_matrix.transpose().tocsr() - - # Issue warnings if necessary, based on dimensions matching. - if count_matrix.shape[1] != feature_names.size: - logging.warning(f"Number of gene names ({feature_names.size}) in {filename} " - f"does not match the number expected from the count " - f"matrix ({count_matrix.shape[1]}).") - if count_matrix.shape[0] != barcodes.size: - logging.warning(f"Number of barcodes ({barcodes.size}) in {filename} " - f"does not match the number expected from the count " - f"matrix ({count_matrix.shape[0]}).") - - return {'matrix': count_matrix, - 'gene_names': feature_names, - 'gene_ids': feature_ids, - 'genomes': genomes, - 'feature_types': feature_types, - 'barcodes': barcodes} - - -def get_matrix_from_anndata(filename: str) \ - -> Dict[str, Union[sp.csr.csr_matrix, List[np.ndarray], np.ndarray]]: - """Load a count matrix from an h5ad AnnData file. - - The file needs to contain raw counts for all measured barcodes in the - `.X` attribute or a `.layer[{'counts', 'spliced'}]` attribute. This function - returns a dictionary that includes the count matrix, the gene names (which - correspond to columns of the count matrix), and the barcodes (which - correspond to rows of the count matrix). - - This function works for any AnnData object meeting the above requirements, - as generated by alignment methods like `kallisto | bustools`. - - Args: - filename: string path to .h5ad file that contains the raw gene - barcode matrices - - Returns: - out['matrix']: scipy.sparse.csr.csr_matrix of unique UMI counts, with - barcodes as rows and genes as columns - out['barcodes']: numpy array of strings which are the nucleotide - sequences of the barcodes that correspond to the rows in - the out['matrix'] - out['gene_names']: List of numpy arrays, where the number of elements - in the list is the number of genomes in the dataset. Each numpy - array contains the string names of genes in the genome, which - correspond to the columns in the out['matrix']. - out['gene_ids']: List of numpy arrays, where the number of elements - in the list is the number of genomes in the dataset. Each numpy - array contains the string Ensembl ID of genes in the genome, which - also correspond to the columns in the out['matrix']. - out['feature_types']: List of numpy arrays, where the number of elements - in the list is the number of genomes in the dataset. Each numpy - array contains the string feature types of genes (or possibly - antibody capture reads), which also correspond to the columns - in the out['matrix']. - - """ - logging.info(f"Detected AnnData format") - - adata = anndata.read_h5ad(filename) - - if "counts" in adata.layers.keys(): - # this is a common manual setting for users of scVI - # given the manual convention, we prefer this matrix to - # .X since it is less likely to represent something other - # than counts - logging.info("Found `.layers['counts']`. Using for count data.") - count_matrix = adata.layers["counts"] - elif "spliced" in adata.layers.keys() and adata.X is None: - # alignment using kallisto | bustools with intronic counts - # does not populate `.X` by default, but does populate - # `.layers['spliced'], .layers['unspliced']`. - # we use spliced counts for analysis - logging.info("Found `.layers['spliced']`. Using for count data.") - count_matrix = adata.layers["spliced"] - else: - logging.info("Using `.X` for count data.") - count_matrix = adata.X - - # check that `count_matrix` contains a large number of barcodes, - # consistent with a raw single cell experiment - if count_matrix.shape[0] < consts.MINIMUM_BARCODES_H5AD: - # this experiment might be prefiltered - msg = f"Only {count_matrix.shape[0]} barcodes were found.\n" - msg += "This suggests the matrix was prefiltered.\n" - msg += "CellBender requires a raw, unfiltered [Barcodes, Genes] matrix." - logging.warning(msg) - - # AnnData is [Cells, Genes], no need to transpose - # we typecast explicitly in the off chance `count_matrix` was dense. - count_matrix = sp.csr_matrix(count_matrix) - # feature names and ids are not consistently delineated in AnnData objects - # so we attempt to find relevant features using common values. - feature_names = np.array(adata.var_names, dtype=str) - barcodes = np.array(adata.obs_names, dtype=str) - - # Make an attempt to find feature_IDs if they are present. - feature_ids = None - if 'gene_ids' in adata.var.keys(): - feature_ids = np.array(adata.var['gene_ids'].values, dtype=str) - elif 'gene_id' in adata.var.keys(): - feature_ids = np.array(adata.var['gene_id'].values, dtype=str) - - # Make an attempt to find feature_types if they are present. - feature_types = None - if 'feature_types' in adata.var.keys(): - feature_types = np.array(adata.var['feature_types'].values, dtype=str) - elif 'feature_type' in adata.var.keys(): - feature_types = np.array(adata.var['feature_type'].values, dtype=str) - - # Make an attempt to find genomes if they are present. - genomes = None - if 'genomes' in adata.var.keys(): - genomes = np.array(adata.var['genomes'].values, dtype=str) - elif 'genome' in adata.var.keys(): - genomes = np.array(adata.var['genome'].values, dtype=str) - - # Issue warnings if necessary, based on dimensions matching. - if count_matrix.shape[1] != feature_names.size: - logging.warning(f"Number of gene names ({feature_names.size}) in {filename} " - f"does not match the number expected from the count " - f"matrix ({count_matrix.shape[1]}).") - if count_matrix.shape[0] != barcodes.size: - logging.warning(f"Number of barcodes ({barcodes.size}) in {filename} " - f"does not match the number expected from the count " - f"matrix ({count_matrix.shape[0]}).") - - return {'matrix': count_matrix, - 'gene_names': feature_names, - 'gene_ids': feature_ids, - 'genomes': genomes, - 'feature_types': feature_types, - 'barcodes': barcodes} - - -def write_matrix_to_cellranger_h5( - cellranger_version: int, - output_file: str, - gene_names: np.ndarray, - barcodes: np.ndarray, - inferred_count_matrix: sp.csc.csc_matrix, - feature_types: Optional[np.ndarray] = None, - gene_ids: Optional[np.ndarray] = None, - genomes: Optional[np.ndarray] = None, - cell_barcode_inds: Optional[np.ndarray] = None, - ambient_expression: Optional[np.ndarray] = None, - rho: Optional[np.ndarray] = None, - z: Optional[np.ndarray] = None, - d: Optional[np.ndarray] = None, - p: Optional[np.ndarray] = None, - phi: Optional[np.ndarray] = None, - epsilon: Optional[np.ndarray] = None, - fpr: Optional[float] = None, - lambda_multiplier: Optional[float] = None, - loss: Optional[Dict] = None) -> bool: - """Write count matrix data to output HDF5 file using CellRanger format. - - Args: - cellranger_version: Either 2 or 3. Determines the format of the output - h5 file. - output_file: Path to output .h5 file (e.g., 'output.h5'). - gene_names: Name of each gene (column of count matrix). - gene_ids: Ensembl ID of each gene (column of count matrix). - genomes: Name of the genome that each gene comes from. - feature_types: Type of each feature (column of count matrix). - barcodes: Name of each barcode (row of count matrix). - inferred_count_matrix: Count matrix to be written to file, in sparse - format. Rows are barcodes, columns are genes. - cell_barcode_inds: Indices into the original cell barcode array that - were found to contain cells. - ambient_expression: Vector of gene expression of the ambient RNA - background counts that contaminate cell counts. - rho: Hyperparameters for the contamination fraction distribution. - epsilon: Latent encoding of droplet RT efficiency. - z: Latent encoding of gene expression. - d: Latent cell size scale factor. - p: Latent probability that a barcode contains a cell. - phi: Latent global overdispersion mean and scale. - fpr: Target false positive rate for the regularized posterior denoised - counts, where false positives are true counts that are (erroneously) - removed. - lambda_multiplier: The lambda multiplier value used to achieve the - targeted false positive rate. - loss: Training and test error, as ELBO, for each epoch. - - Note: - To match the CellRanger .h5 files, the matrix is stored as its - transpose, with rows as genes and cell barcodes as columns. - - """ - - assert isinstance(inferred_count_matrix, - sp.csc_matrix), "The count matrix must be csc_matrix " \ - "format in order to write to HDF5." - - assert gene_names.size == inferred_count_matrix.shape[1], \ - "The number of gene names must match the number of columns in the " \ - "count matrix." - - if gene_ids is not None: - assert gene_names.size == gene_ids.size, \ - f"The number of gene_names {gene_names.shape} must match " \ - f"the number of gene_ids {gene_ids.shape}." - - if feature_types is not None: - assert gene_names.size == feature_types.size, \ - f"The number of gene_names {gene_names.shape} must match " \ - f"the number of feature_types {feature_types.shape}." - - if genomes is not None: - assert gene_names.size == genomes.size, \ - "The number of gene_names must match the number of genome designations." - - assert barcodes.size == inferred_count_matrix.shape[0], \ - "The number of barcodes must match the number of rows in the count" \ - "matrix." - - # This reverses the role of rows and columns, to match CellRanger format. - inferred_count_matrix = inferred_count_matrix.transpose().tocsc() - - # Write to output file. - try: - with tables.open_file(output_file, "w", - title="CellBender remove-background output") as f: - - if cellranger_version == 2: - - # Create the group where data will be stored. - group = f.create_group("/", "background_removed", - "Counts after background correction") - - # Create arrays within that group for gene info. - f.create_array(group, "gene_names", gene_names) - if gene_ids is not None: - f.create_array(group, "genes", gene_ids) - - elif cellranger_version == 3: - - # Create the group where data will be stored: name is "matrix". - group = f.create_group("/", "matrix", - "Counts after background correction") - - # Create a sub-group called "features" - feature_group = f.create_group(group, "features", - "Genes and other features measured") - - # Create arrays within that group for feature info. - f.create_array(feature_group, "name", gene_names) - if gene_ids is not None: - f.create_array(feature_group, "id", gene_ids) - if feature_types is not None: - f.create_array(feature_group, "feature_type", feature_types) - if genomes is not None: - f.create_array(feature_group, "genome", genomes) - - # Copy the other extraneous information from the input file. - # (Some user might need it for some reason.) - # TODO - - else: - raise NotImplementedError(f'Trying to save to CellRanger ' - f'v{cellranger_version} format, which ' - f'is not implemented.') - - # Code for both versions. - f.create_array(group, "barcodes", barcodes) - - # Create arrays to store the count data. - f.create_array(group, "data", inferred_count_matrix.data) - f.create_array(group, "indices", inferred_count_matrix.indices) - f.create_array(group, "indptr", inferred_count_matrix.indptr) - f.create_array(group, "shape", inferred_count_matrix.shape) - - # Store background gene expression, barcode_inds, z, d, and p. - if cell_barcode_inds is not None: - f.create_array(group, "barcode_indices_for_latents", - cell_barcode_inds) - if ambient_expression is not None: - f.create_array(group, "ambient_expression", ambient_expression) - if z is not None: - f.create_array(group, "latent_gene_encoding", z) - if d is not None: - f.create_array(group, "latent_scale", d) - if p is not None: - f.create_array(group, "latent_cell_probability", p) - if phi is not None: - f.create_array(group, "overdispersion_mean_and_scale", phi) - if rho is not None: - f.create_array(group, "contamination_fraction_params", rho) - if epsilon is not None: - f.create_array(group, "latent_RT_efficiency", epsilon) - if fpr is not None: - f.create_array(group, "target_false_positive_rate", fpr) - if lambda_multiplier is not None: - f.create_array(group, "lambda_multiplier", lambda_multiplier) - if loss is not None: - f.create_array(group, "training_elbo_per_epoch", - np.array(loss['train']['elbo'])) - if 'test' in loss.keys(): - f.create_array(group, "test_elbo", - np.array(loss['test']['elbo'])) - f.create_array(group, "test_epoch", - np.array(loss['test']['epoch'])) - f.create_array(group, "fraction_data_used_for_testing", - 1. - consts.TRAINING_FRACTION) - - logging.info(f"Succeeded in writing CellRanger v{cellranger_version} " - f"format output to file {output_file}") - - return True - - except Exception: - logging.warning(f"Encountered an error writing output to file " - f"{output_file}. " - f"Output may be incomplete.") - - return False - - -def get_d_priors_from_dataset(dataset: SingleCellRNACountsDataset) \ - -> Tuple[float, float]: - """Compute an estimate of reasonable priors on cell size and ambient size. - - Given a dataset (scipy.sparse.csr matrix of counts where - rows are barcodes and columns are genes), and an expected - cell count, compute an estimate of reasonable priors on cell size - and ambient count size. This is done by a series of heuristics. - - Args: - dataset: Dataset object containing a matrix of unique UMI counts, - where rows are barcodes and columns are genes. - - Returns: - cell_counts: Estimated mean number of UMI counts per real cell, in - terms of transformed count data. - empty_counts: Estimated mean number of UMI counts per 'empty' - droplet, in terms of transformed count data. - - NOTE: Picks barcodes using cutoffs in untransformed count data. The output - is in terms of transformed counts. - - """ - - # Count the total unique UMIs per barcode (summing after transforming). - counts = np.array(dataset.data['matrix'] - [:, dataset.analyzed_gene_inds].sum(axis=1)).squeeze() - - # If it's a model that does not model empty droplets, the dataset is cells. - if dataset.model_name == 'simple': - - if dataset.priors['n_cells'] is None: - # No prior on number of cells. Assume all are cells. - dataset.priors['n_cells'] = int(np.sum(counts > 0).item()) - - # Sort order the cells by counts. - sort_order = np.argsort(counts)[::-1] - - # Estimate cell count by median, taking 'cells' to be largest counts. - cell_counts = int(np.median(counts[sort_order] - [:dataset.priors['n_cells']]).item()) - - empty_counts = 0 - - # Models that include both cells and empty droplets. - else: - - # Cutoff for original data. Empirical. - cut = dataset.low_count_threshold - - # Estimate the number of UMI counts in empty droplets. - - # Mode of (rounded) log counts (for counts > cut) is a robust - # empty estimator. - empty_log_counts = mode(np.round(np.log1p(counts[counts > cut]), - decimals=1))[0] - empty_counts = int(np.expm1(empty_log_counts).item()) - - # Estimate the number of UMI counts in cells. - - # Use expected cells if it is available. - if dataset.priors['n_cells'] is not None: - - # Sort order the cells by counts. - sort_order = np.argsort(counts)[::-1] - - cell_counts = int(np.median(counts[sort_order] - [:dataset.priors['n_cells']]).item()) - - else: - - # Median of log counts above 5 * empty counts is a robust - # cell estimator. - cell_log_counts = np.median(np.log1p(counts[counts > 5 * empty_counts])) - cell_counts = int(np.expm1(cell_log_counts).item()) - - logging.info(f"Prior on counts in empty droplets is {empty_counts}") - - logging.info(f"Prior on counts for cells is {cell_counts}") - - return cell_counts, empty_counts - - -def estimate_cell_count_from_dataset(dataset: SingleCellRNACountsDataset) \ - -> int: - """Compute an estimate of number of real cells in a dataset. - - Given a Dataset, compute an estimate of the number of real cells. - - Args: - dataset: Dataset object containing a matrix of unique UMI counts, - where rows are barcodes and columns are genes. - - Returns: - cell_count_est: Estimated number of real cells. - - """ - - # If it's a model that does not model empty droplets, the dataset is cells. - # NOTE: this is overridden if --expected_cells is specified. - if dataset.model_name == 'simple': - return dataset.data['matrix'].shape[0] - - # Count number of UMIs in each barcode. - counts = np.array(dataset.data['matrix'].sum(axis=1), - dtype=int).squeeze() - - # Find mid-way between cell_counts and empty_counts in log space. - midway = np.mean([np.log1p(dataset.priors['cell_counts']), - np.log1p(dataset.priors['empty_counts'])]) - umi_cutoff = np.expm1(midway) - - # Count the number of barcodes with UMI counts above the cutoff. - cell_count_est = int(np.sum(counts > umi_cutoff).item()) - - return cell_count_est - - -def estimate_chi_ambient_from_dataset(dataset: SingleCellRNACountsDataset) \ - -> Tuple[torch.Tensor, torch.Tensor]: - """Compute an estimate of ambient RNA levels. - - Given a Dataset, compute an estimate of the ambient gene expression and - compute the average gene expression. - - Args: - dataset: Dataset object containing a matrix of unique UMI counts, - where rows are barcodes and columns are genes. - - Returns: - chi_ambient_init: Estimated number of real cells. - chi_bar: Average gene expression over dataset. - - NOTE: This must be done on transformed data. - - """ - - # Ensure that an estimate of the log count crossover point between cells - # and empty droplets has already been calculated. - try: - log_crossover = dataset.priors['log_counts_crossover'] - except KeyError: - raise AssertionError("Could not find dataset parameter " - "log_counts_crossover.") - - ep = np.finfo(np.float32).eps.item() # Small value - - # Trimmed and appropriately transformed count matrix. - count_matrix = dataset.get_count_matrix() - - # Empty droplets have log counts < log_crossover. - empty_barcodes = (np.log1p(np.array(count_matrix.sum(axis=1)).squeeze()) - < log_crossover) - - # Sum gene expression for the empty droplets. - gene_expression = np.array(count_matrix[empty_barcodes, :].sum(axis=0))\ - .squeeze() - - # As a vector on a simplex. - gene_expression = gene_expression + ep - chi_ambient_init = \ - torch.Tensor(gene_expression / np.sum(gene_expression)) - - # Full count matrix, appropriately transformed. - full_count_matrix = dataset.get_count_matrix_all_barcodes() - - # Sum all gene expression. - gene_expression_total = np.array(full_count_matrix.sum(axis=0)).squeeze() - - # As a vector on a simplex. - gene_expression_total = gene_expression_total + ep - chi_bar = \ - torch.Tensor(gene_expression_total / np.sum(gene_expression_total)) - - return chi_ambient_init, chi_bar + # Rescue the raw data for ignored features. + out = overwrite_matrix_with_columns_from_another( + mat1=inferred_count_matrix, + mat2=self.data['matrix'], + column_inds=self.analyzed_gene_inds) + + # But ensure that empty droplets are empty in the output. + cell_probabilities_all_bcs = np.zeros(out.shape[0]) + cell_probabilities_all_bcs[self.analyzed_barcode_inds] = cell_probabilities_analyzed_bcs + empty_inds = np.where(cell_probabilities_all_bcs <= consts.CELL_PROB_CUTOFF)[0] + out = csr_set_rows_to_zero(csr=out.tocsr(), row_inds=empty_inds) + + return out.tocsc() + + +def get_dataset_obj(args: argparse.Namespace) -> SingleCellRNACountsDataset: + """Helper function that uses the argparse namespace""" + + return SingleCellRNACountsDataset( + input_file=args.input_file, + expected_cell_count=args.expected_cell_count, + total_droplet_barcodes=args.total_droplets, + force_cell_umi_prior=args.force_cell_umi_prior, + force_empty_umi_prior=args.force_empty_umi_prior, + fraction_empties=args.fraction_empties, + model_name=args.model, + gene_blacklist=args.blacklisted_genes, + exclude_features=args.exclude_features, + low_count_threshold=args.low_count_threshold, + ambient_counts_in_cells_low_limit=args.ambient_counts_in_cells_low_limit, + fpr=args.fpr, + ) diff --git a/cellbender/remove_background/data/extras/simulate.py b/cellbender/remove_background/data/extras/simulate.py index da6d27f..1654c66 100644 --- a/cellbender/remove_background/data/extras/simulate.py +++ b/cellbender/remove_background/data/extras/simulate.py @@ -1,382 +1,704 @@ -"""Simulate a basic scRNA-seq count matrix dataset, for unit tests.""" +"""Simulate a basic scRNA-seq count matrix dataset, for tests.""" + +from cellbender.remove_background.model import calculate_mu, calculate_lambda +from cellbender.remove_background.data.io import write_matrix_to_cellranger_h5 +from cellbender.remove_background.checkpoint import load_from_checkpoint import numpy as np import scipy.sparse as sp +from sklearn.cluster import DBSCAN +from sklearn.decomposition import PCA import torch -from typing import Tuple, List, Union +import pyro +import random +import matplotlib.pyplot as plt +from typing import List, Union, Dict, Optional -def simulate_dataset_without_ambient_rna( - n_cells: int = 100, - clusters: int = 1, - n_genes: int = 10000, - cells_in_clusters: Union[List[int], None] = None, - d_cell: int = 5000) -> Tuple[sp.csr.csr_matrix, - np.ndarray, - np.ndarray, - np.ndarray]: - """Simulate a dataset with ambient background RNA counts. +if torch.cuda.is_available(): + USE_CUDA = True + DEVICE = 'cuda' +else: + USE_CUDA = False + DEVICE = 'cpu' + - Empty drops have ambient RNA only, while barcodes with cells have cell RNA - plus some amount of ambient background RNA (in proportion to the sizes of - cell and droplet). +def comprehensive_random_seed(seed, use_cuda=USE_CUDA): + """Establish a base random state + https://pytorch.org/docs/stable/notes/randomness.html + """ + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + pyro.util.set_rng_seed(seed) + if use_cuda: + torch.cuda.manual_seed_all(seed) + + +def generate_sample_inferred_model_dataset( + checkpoint_file: str, + cells_of_each_type: List[int] = [100, 100, 100], + n_droplets: int = 10000, + model_type: str = 'full', + plot_pca: bool = True, + dbscan_eps: float = 0.3, + dbscan_min_samples: int = 10, + random_seed: int = 0) -> Dict[str, Union[float, np.ndarray, sp.csr_matrix]]: + """Create a sample dataset for use as 'truth' data, based on a trained checkpoint. Args: - n_cells: Number of cells. - clusters: Number of distinct cell types to simulate. - n_genes: Number of genes. - d_cell: Cell size scale factor. - cells_in_clusters: Number of cells of each cell type. If specified, - the number of ints in this list must be equal to clusters. + checkpoint_file: ckpt.tar.gz file from a remove-background run + cells_of_each_type: Number of cells of each cell type to simulate + n_droplets: Total number of droplets to simulate + model_type: The --model argument to specify remove-background's model + plot_pca: True to plot the PCA plot used to define "cell types" + dbscan_eps: Smaller makes the clusters finer - Returns: - csr_barcode_gene_synthetic: The simulated barcode by gene matrix of UMI - counts, as a scipy.sparse.csr.csr_matrix. - z: The simulated cell type identities. A numpy array of integers, one - for each barcode. The number 0 is used to denote barcodes - without a cell present. - chi: The simulated gene expression, one corresponding to each z. - Access the vector of gene expression for a given z using chi[z, :]. - d: The simulated size scale factors, one for each barcode. """ - assert d_cell > 0, "Location parameter, d_cell, of LogNormal " \ - "distribution must be greater than zero." - assert clusters > 0, "clusters must be a positive integer." - assert n_cells > 0, "n_cells must be a positive integer." - assert n_genes > 0, "n_genes must be a positive integer." - - # Figure out how many cells are in each cell cluster. - if cells_in_clusters is None: - # No user input: make equal numbers of each cell type - cells_in_clusters = np.ones(clusters) * int(n_cells / clusters) + # Input checking. + assert len(cells_of_each_type) > 0, 'cells_of_each_type must be a List of ints' + n_cell_types = len(cells_of_each_type) + + # Load a trained model from a checkpoint file. + ckpt = load_from_checkpoint( + filebase=None, + tarball_name=checkpoint_file, + to_load=['model', 'dataloader', 'param_store'], + force_device='cpu' if not torch.cuda.is_available() else None) + model = ckpt['model'] + n_genes = model.n_genes + + # Reach in and set model type. + model.model_type = model_type + + # Seed random number generators. + comprehensive_random_seed(seed=random_seed) + + # Find z values for cells. + data_loader = ckpt['train_loader'] + if torch.cuda.is_available(): + data_loader.use_cuda = True + data_loader.device = 'cuda' + model.use_cuda = True + model.device = 'cuda' else: - assert len(cells_in_clusters) == clusters, "len(cells_in_clusters) " \ - "must equal clusters." - assert sum(cells_in_clusters) == n_cells, "sum(cells_in_clusters) " \ - "must equal n_cells." - - # Initialize arrays and lists. - chi = np.zeros((clusters + 1, n_genes)) - csr_list = [] - z = [] - d = [] - - # Get chi for cell expression. - for i in range(clusters): - chi[i, :] = generate_chi(alpha=1.0, n_genes=n_genes) - csr, d_n = sample_expression_from(chi[i, :], - n=int(cells_in_clusters[i]), - d_mu=np.log(d_cell).item()) - csr_list.append(csr) - z = z + [i for _ in range(csr.shape[0])] - d = d + [j for j in d_n] - - # Package the results. - csr_barcode_gene_synthetic = sp.vstack(csr_list) - z = np.array(z) - d = np.array(d) - - # Permute the barcode order and return results. - order = np.random.permutation(z.size) - csr_barcode_gene_synthetic = csr_barcode_gene_synthetic[order, ...] - z = z[order] - d = d[order] - - return csr_barcode_gene_synthetic, z, chi, d - - -def simulate_dataset_with_ambient_rna( - n_cells: int = 150, - n_empty: int = 300, - clusters: int = 3, + data_loader.use_cuda = False + data_loader.device = 'cpu' + model.use_cuda = False + model.device = 'cpu' + z = np.zeros((len(data_loader), model.encoder['z'].output_dim)) + p = np.zeros(len(data_loader)) + chi_ambient = pyro.param('chi_ambient').detach() + for i, data in enumerate(data_loader): + enc = model.encoder(x=data, + chi_ambient=chi_ambient, + cell_prior_log=model.d_cell_loc_prior) + ind = i * data_loader.batch_size + z[ind:(ind + data.shape[0]), :] = enc['z']['loc'].detach().cpu().numpy() + p[ind:(ind + data.shape[0])] = enc['p_y'].sigmoid().detach().cpu().numpy() + z = z[p > 0.5, :] # select cells only + + # Cluster cells based on embeddings. + db = DBSCAN(eps=dbscan_eps, min_samples=dbscan_min_samples).fit(z) + core_samples_mask = np.zeros_like(db.labels_, dtype=bool) + core_samples_mask[db.core_sample_indices_] = True + labels = db.labels_ + n_clusters_ = len(set(labels)) - (1 if -1 in labels else 0) + if n_clusters_ < len(cells_of_each_type): + print(f'WARNING: DBSCAN found only {n_clusters_} clusters, but the input ' + f'cells_of_each_type dictates {len(cells_of_each_type)} cell types ' + f'are required.') + + # Show a plot. + if plot_pca: + pca = PCA(n_components=2).fit_transform(z) + for label in np.unique(labels): + if label == -1: + color = 'lightgray' + else: + color = None + plt.plot(pca[labels == label, 0], pca[labels == label, 1], + '.', color=color, label=label) + plt.title('Embedding PCA') + plt.xlabel('PCA 0') + plt.ylabel('PCA 1') + plt.legend() + plt.show() + + unique_labels = set(np.unique(labels)) - {-1} + print({label: (labels == label).sum() for label in unique_labels}) + + # Sample z values for cells of each cluster based on this posterior. + z_cluster = [] + for label, n_cells in zip(np.unique(labels)[:len(cells_of_each_type)], cells_of_each_type): + if n_cells == 0: + continue + logic = (labels == label) + z_tmp = z[logic][torch.randperm(logic.sum())][:n_cells] + while len(z_tmp) < n_cells: + z_tmp = np.concatenate((z_tmp, z[logic][torch.randperm(logic.sum())][:n_cells]), axis=0) + z_cluster.append(z_tmp[:n_cells]) + + # Account for the possibility that one cell type has zero cells (useful for selecting populations of interest) + cells_of_each_type = [c for c in cells_of_each_type if c > 0] + n_cell_types = len(cells_of_each_type) + + # Prep variables. + c_real = np.zeros((n_droplets, n_genes)) + c_bkg = np.zeros((n_droplets, n_genes)) + labels = np.zeros(n_droplets) + epsilon = np.zeros(n_droplets) + rho = np.zeros(n_droplets) + d_cell = np.zeros(n_droplets) + d_empty = np.zeros(n_droplets) + chi = np.zeros((n_cell_types, n_genes)) + d_cell_type = np.zeros(n_cell_types) + + # Sample counts from cell-containing droplets. + i = 0 + for t, (num_cells, z_chunk) in enumerate(zip(cells_of_each_type, z_cluster)): + sim = sample_from_inferred_model( + num=num_cells, + model=model, + z=torch.tensor(z_chunk).to('cuda' if torch.cuda.is_available() else 'cpu').float(), + y=torch.ones(num_cells).to('cuda' if torch.cuda.is_available() else 'cpu').float(), + ) + c_real[i:(i + num_cells), :] = sim['counts_real'] + c_bkg[i:(i + num_cells), :] = sim['counts_bkg'] + epsilon[i:(i + num_cells)] = sim['epsilon'] + rho[i:(i + num_cells)] = sim['rho'] + d_cell[i:(i + num_cells)] = sim['cell_counts'] + d_empty[i:(i + num_cells)] = sim['empty_counts'] + labels[i:(i + num_cells)] = t + 1 # label cell types with integers > 0 + chi[t, :] = sim['chi'].sum(axis=0) / sim['chi'].sum() + d_cell_type[t] = sim['cell_counts'].mean() + i = i + num_cells + + # Sample counts from empty droplets. + sim = sample_from_inferred_model( + num=n_droplets - i, + model=model, + y=torch.zeros(n_droplets - i).to('cuda' if torch.cuda.is_available() else 'cpu').float(), + ) + c_real[i:, :] = sim['counts_real'] + c_bkg[i:, :] = sim['counts_bkg'] + epsilon[i:] = sim['epsilon'] + rho[i:] = sim['rho'] + + # Get sparse matrices from dense. + counts_true = sp.csr_matrix(c_real, shape=c_real.shape, dtype=int) + counts_bkg = sp.csr_matrix(c_bkg, shape=c_bkg.shape, dtype=int) + + return {'counts_true': counts_true, + 'counts_bkg': counts_bkg, + 'barcodes': np.array([f'bc{n:06d}' for n in np.arange(n_droplets)]), + 'gene_names': model.analyzed_gene_names, + 'chi': {i + 1: chi[i, :] for i in range(chi.shape[0])}, + 'chi_ambient': chi_ambient.detach().cpu().numpy(), + 'droplet_labels': labels.astype(int), + 'd_cell': d_cell, + 'd_empty': d_empty, + 'epsilon': epsilon, + 'rho': rho, + 'cell_mean_umi': {i + 1: u for i, u in enumerate(d_cell_type)}, + 'cell_lognormal_sigma': model.d_cell_scale_prior.item(), + 'empty_mean_umi': model.d_empty_loc_prior.exp().item(), + 'empty_lognormal_sigma': model.d_empty_scale_prior.item(), + 'epsilon_param': model.epsilon_prior.item(), + 'model': [model_type]} + + +def generate_sample_dirichlet_dataset( n_genes: int = 10000, - d_cell: int = 5000, - d_empty: int = 100, - cells_in_clusters: Union[List[int], None] = None, - ambient_different: bool = False, - chi_input: Union[np.ndarray, None] = None) \ - -> Tuple[sp.csr.csr_matrix, np.ndarray, np.ndarray, np.ndarray]: - """Simulate a dataset with ambient background RNA counts. - - Empty drops have ambient RNA only, while barcodes with cells have cell - RNA plus some amount of ambient background RNA (in proportion to the - sizes of cell and droplet). - - Args: - n_cells: Number of cells. - n_empty: Number of empty droplets with only ambient RNA. - clusters: Number of distinct cell types to simulate. - n_genes: Number of genes. - d_cell: Cell size scale factor. - d_empty: Empty droplet size scale factor. - cells_in_clusters: Number of cells of each cell type. If specified, - the number of ints in this list must be equal to clusters. - ambient_different: If False, the gene expression profile of ambient - RNA is drawn from the sum of cellular gene expression. If True, - the ambient RNA expression is completely different from cellular - gene expression. - chi_input: Gene expression arrays in a matrix, with rows as clusters and - columns as genes. Expression should add to one for each row. - Setting chi=None will generate new chi randomly according to a - Dirichlet distribution. - - Returns: - csr_barcode_gene_synthetic: The simulated barcode by gene matrix of - UMI counts, as a scipy.sparse.csr.csr_matrix. - z: The simulated cell type identities. A numpy array of integers, - one for each barcode. The number 0 is used to denote barcodes - without a cell present. - chi: The simulated gene expression, one corresponding to each z. - Access the vector of gene expression for a given z using chi[z, :]. - d: The simulated size scale factors, one for each barcode. - + cells_of_each_type: List[int] = [100, 100, 100], + n_droplets: int = 5000, + model_type: str = 'full', + dirichlet_alpha: Union[float, np.ndarray] = 0.05, + chi: Optional[np.ndarray] = None, + chi_artificial_similarity: float = 0, + vector_to_add_to_chi_ambient: Optional[np.ndarray] = None, + chi_ambient: Optional[np.ndarray] = None, + cell_mean_umi: List[int] = [5000], + cell_lognormal_sigma: float = 0.2, + empty_mean_umi: int = 200, + empty_lognormal_sigma: float = 0.4, + alpha0: float = 5000, + epsilon_param: float = 100, + rho_alpha: float = 3, + rho_beta: float = 80, + random_seed: int = 0) -> Dict[str, Union[float, np.ndarray, sp.csr_matrix]]: + """Create a sample dataset for use as 'truth' data. """ - assert d_cell > 0, "Location parameter, d_cell, of LogNormal " \ - "distribution must be greater than zero." - assert d_empty > 0, "Location parameter, d_cell, of LogNormal " \ - "distribution must be greater than zero." - assert clusters > 0, "clusters must be a positive integer." - assert n_cells > 0, "n_cells must be a positive integer." - assert n_empty > 0, "n_empty must be a positive integer." - assert n_genes > 0, "n_genes must be a positive integer." - if chi_input is not None: - assert chi_input.shape[0] == clusters, "Chi was specified, but the " \ - "number of rows must match " \ - "the number of clusters." - assert chi_input.shape[1] == n_genes, "Chi was specified, but the " \ - "number of columns must match " \ - "the number of genes." - - # Figure out how many cells are in each cell cluster. - if cells_in_clusters is None: - # No user input: make equal numbers of each cell type - cells_in_clusters = (np.ones(clusters, dtype=int) - * int(n_cells/clusters)) + # Input checking. + assert len(cells_of_each_type) > 0, 'cells_of_each_type must be a List of ints' + n_cell_types = len(cells_of_each_type) + if vector_to_add_to_chi_ambient is not None: + assert len(vector_to_add_to_chi_ambient) == n_genes, \ + f'vector_to_add_to_chi_ambient {vector_to_add_to_chi_ambient.shape} must ' \ + f'be of length n_genes ({n_genes})' + if type(dirichlet_alpha) == np.ndarray: + assert dirichlet_alpha.size == n_genes, \ + f'If you input a vector for dirichlet_alpha, its length ' \ + f'({dirichlet_alpha.size}) must be n_genes ({n_genes})' + assert cell_lognormal_sigma > 0, 'cell_lognormal_sigma must be > 0' + assert empty_lognormal_sigma > 0, 'empty_lognormal_sigma must be > 0' + cell_mean_umi = list(cell_mean_umi) + if len(cell_mean_umi) == 1: + cell_mean_umi = cell_mean_umi * n_cell_types # repeat the entry + for umi in cell_mean_umi: + assert umi > empty_mean_umi, \ + f'cell_size_mean_umi ({umi}) must be > empty_mean_umi ({empty_mean_umi})' + assert len(cell_mean_umi) == len(cells_of_each_type), \ + 'cells_of_each_type and cell_mean_umi (if a list) must be of the same length' + assert (chi_artificial_similarity >= 0) and (chi_artificial_similarity <= 1), \ + 'chi_artificial_similarity must be in the range [0, 1]' + + # Seed random number generators. + comprehensive_random_seed(seed=random_seed) + + # Draw gene expression profiles for each cell type. + if chi is None: + chi = np.zeros((n_cell_types, n_genes)) + for i in range(chi.shape[0]): + chi[i, :] = draw_random_chi(alpha=dirichlet_alpha, n_genes=n_genes).cpu().numpy() + if i > 0: + # Make expression profiles similar (if chi_artificial_similarity > 0) via gating. + chi[i, :] = chi[i, :] * (1 - chi_artificial_similarity) + chi[0, :] * chi_artificial_similarity else: - assert len(cells_in_clusters) == clusters, "len(cells_in_clusters) " \ - "must equal clusters." - assert sum(cells_in_clusters) == n_cells, "sum(cells_in_clusters) " \ - "must equal n_cells." - - # Initialize arrays and lists. - chi = np.zeros((clusters+1, n_genes)) - csr_list = [] - z = [] - d = [] - - if chi_input is not None: - - # Go with the chi that was input. - chi[1:, :] = chi_input - + dirichlet_alpha = ['None: chi was input to generate_sample_dataset()'] + + # Get chi_ambient: a weighted average of expression, possibly with extra. + if chi_ambient is None: + chi_ambient = np.zeros(n_genes) + for i in range(n_cell_types): + chi_ambient = chi_ambient + chi[i, :] * cells_of_each_type[i] * cell_mean_umi[i] + if vector_to_add_to_chi_ambient is None: + vector_to_add_to_chi_ambient = np.zeros(n_genes) + chi_ambient = chi_ambient + vector_to_add_to_chi_ambient else: - - # Get chi for cell expression. - for i in range(1, clusters+1): - chi[i, :] = generate_chi(alpha=0.01, n_genes=n_genes) - - # Get chi for ambient expression. This becomes chi[0, :]. - if ambient_different: - - # Ambient expression is unrelated to cells, and is itself random. - chi[0, :] = generate_chi(alpha=0.001, n_genes=n_genes) # Sparse + if vector_to_add_to_chi_ambient is not None: + print('You specified both `chi_ambient` and `vector_to_add_to_chi_ambient`. ' + 'Ignoring `vector_to_add_to_chi_ambient` and using `chi_ambient` ' + 'as provided.') + chi_ambient = chi_ambient / chi_ambient.sum() + + c_real = np.zeros((n_droplets, n_genes)) + c_bkg = np.zeros((n_droplets, n_genes)) + labels = np.zeros(n_droplets) + epsilon = np.zeros(n_droplets) + rho = np.zeros(n_droplets) + d_cell = np.zeros(n_droplets) + d_empty = np.zeros(n_droplets) + + # Sample counts from cell-containing droplets. + i = 0 + for t, (num_cells, celltype_umi) in enumerate(zip(cells_of_each_type, cell_mean_umi)): + sim = sample_from_dirichlet_model( + num=num_cells, + alpha=chi[t, :] * alpha0 + 1e-20, # must be > 0 + d_mu=np.log(celltype_umi), + d_sigma=cell_lognormal_sigma, + v_mu=np.log(empty_mean_umi), + v_sigma=empty_lognormal_sigma, + y=1, # cell present + chi_ambient=chi_ambient, + eps_param=epsilon_param, + rho_alpha=rho_alpha, + rho_beta=rho_beta, + model_type=model_type, + ) + c_real[i:(i + num_cells), :] = sim['counts_real'] + c_bkg[i:(i + num_cells), :] = sim['counts_bkg'] + epsilon[i:(i + num_cells)] = sim['epsilon'] + rho[i:(i + num_cells)] = sim['rho'] + d_cell[i:(i + num_cells)] = sim['cell_counts'] + d_empty[i:(i + num_cells)] = sim['empty_counts'] + labels[i:(i + num_cells)] = t + 1 # label cell types with integers > 0 + i = i + num_cells + + # Sample counts from empty droplets. + sim = sample_from_dirichlet_model( + num=n_droplets - i, + alpha=np.ones(n_genes), # this doesn't get used since y=0 + d_mu=1, # this doesn't get used since y=0 + d_sigma=1, # this doesn't get used since y=0 + v_mu=np.log(empty_mean_umi), + v_sigma=empty_lognormal_sigma, + y=0, # no cell present + chi_ambient=chi_ambient, + eps_param=epsilon_param, + rho_alpha=rho_alpha, + rho_beta=rho_beta, + model_type=model_type, + ) + c_real[i:, :] = sim['counts_real'] + c_bkg[i:, :] = sim['counts_bkg'] + epsilon[i:] = sim['epsilon'] + rho[i:] = sim['rho'] + d_empty[i:] = sim['empty_counts'] + + # Get sparse matrices from dense. + counts_true = sp.csr_matrix(c_real, shape=c_real.shape, dtype=int) + counts_bkg = sp.csr_matrix(c_bkg, shape=c_bkg.shape, dtype=int) + + return {'counts_true': counts_true, + 'counts_bkg': counts_bkg, + 'barcodes': np.array([f'bc{n:06d}' for n in np.arange(n_droplets)]), + 'gene_names': np.array([f'g{n:05d}' for n in np.arange(n_genes)]), + 'chi': {str(i + 1): chi[i, :] for i in range(chi.shape[0])}, + 'chi_ambient': chi_ambient, + 'droplet_labels': labels.astype(int), + 'd_cell': d_cell, + 'd_empty': d_empty, + 'epsilon': epsilon, + 'rho': rho, + 'cell_mean_umi': {str(i + 1): u for i, u in enumerate(cell_mean_umi)}, + 'cell_lognormal_sigma': cell_lognormal_sigma, + 'empty_mean_umi': empty_mean_umi, + 'empty_lognormal_sigma': empty_lognormal_sigma, + 'alpha0': alpha0, + 'epsilon_param': epsilon_param, + 'dirichlet_alpha': dirichlet_alpha, + 'model': [model_type]} + + +@torch.no_grad() +def generate_sample_model_dataset( + n_genes: int = 10000, + cells_of_each_type: List[int] = [100, 100, 100], + n_droplets: int = 5000, + model_type: str = 'full', + dirichlet_alpha: Union[float, np.ndarray] = 0.05, + chi: Optional[np.ndarray] = None, + chi_artificial_similarity: float = 0, + vector_to_add_to_chi_ambient: Optional[np.ndarray] = None, + cell_mean_umi: List[int] = [5000], + cell_lognormal_sigma: float = 0.2, + empty_mean_umi: int = 200, + empty_lognormal_sigma: float = 0.4, + epsilon_param: float = 100, + rho_alpha: float = 3, + rho_beta: float = 80, + phi: float = 0.1, + random_seed: int = 0) -> Dict[str, Union[float, np.ndarray, sp.csr_matrix]]: + """Create a sample dataset for use as 'truth' data. + """ + # Input checking. + assert len(cells_of_each_type) > 0, 'cells_of_each_type must be a List of ints' + n_cell_types = len(cells_of_each_type) + if vector_to_add_to_chi_ambient is not None: + assert len(vector_to_add_to_chi_ambient) == n_genes, \ + f'vector_to_add_to_chi_ambient {vector_to_add_to_chi_ambient.shape} must ' \ + f'be of length n_genes ({n_genes})' + if type(dirichlet_alpha) == np.ndarray: + assert dirichlet_alpha.size == n_genes, \ + f'If you input a vector for dirichlet_alpha, its length ' \ + f'({dirichlet_alpha.size}) must be n_genes ({n_genes})' + assert cell_lognormal_sigma > 0, 'cell_lognormal_sigma must be > 0' + assert empty_lognormal_sigma > 0, 'empty_lognormal_sigma must be > 0' + cell_mean_umi = list(cell_mean_umi) + if len(cell_mean_umi) == 1: + cell_mean_umi = cell_mean_umi * n_cell_types # repeat the entry + for umi in cell_mean_umi: + assert umi > empty_mean_umi, \ + f'cell_size_mean_umi ({umi}) must be > empty_mean_umi ({empty_mean_umi})' + assert len(cell_mean_umi) == len(cells_of_each_type), \ + 'cells_of_each_type and cell_mean_umi (if a list) must be of the same length' + assert (chi_artificial_similarity >= 0) and (chi_artificial_similarity <= 1), \ + 'chi_artificial_similarity must be in the range [0, 1]' + assert phi > 0, 'phi must be > 0' + + # Seed random number generators. + comprehensive_random_seed(seed=random_seed) + + # Draw gene expression profiles for each cell type. + if chi is None: + chi = torch.zeros((n_cell_types, n_genes)).to(DEVICE) + for i in range(chi.shape[0]): + chi[i, :] = draw_random_chi(alpha=dirichlet_alpha, n_genes=n_genes) + if i > 0: + # Make expression profiles similar (if chi_artificial_similarity > 0) via gating. + chi[i, :] = chi[i, :] * (1 - chi_artificial_similarity) + chi[0, :] * chi_artificial_similarity else: - - # Ambient gene expression comes from the sum of cell expression. - for i in range(1, clusters+1): - - chi[0, :] += cells_in_clusters[i-1] * chi[i, :] # Weighted sum - - chi[0, :] = chi[0, :] / np.sum(chi[0, :]) # Normalize - - # Sample gene expression for ambient. - csr, d_n = sample_expression_from(chi[0, :], - n=n_empty, - d_mu=np.log(d_empty).item()) - - # Add data to lists. - csr_list.append(csr) - z = z + [0 for _ in range(csr.shape[0])] - d = d + [i for i in d_n] - - # Sample gene expression for cells. - for i in range(1, clusters+1): - - # Get chi for cells once ambient expression is added. - chi_tilde = chi[i, :] * d_cell + chi[0, :] * d_empty - chi_tilde = chi_tilde / np.sum(chi_tilde) # Normalize - csr, d_n = sample_expression_from(chi_tilde, - n=cells_in_clusters[i-1], - d_mu=np.log(d_cell).item()) - - # Add data to lists. - csr_list.append(csr) - z = z + [i for _ in range(csr.shape[0])] - d = d + [j for j in d_n] - - # Package the results. - csr_barcode_gene_synthetic = sp.vstack(csr_list) - z = np.array(z) - d = np.array(d) - - # Permute the barcode order and return results. - order = np.random.permutation(z.size) - csr_barcode_gene_synthetic = csr_barcode_gene_synthetic[order, ...] - z = z[order] - d = d[order] - - return csr_barcode_gene_synthetic, z, chi, d - - -def generate_chi(alpha: float = 1., n_genes: int = 10000) -> np.ndarray: + dirichlet_alpha = ['None: chi was input to generate_sample_model_dataset()'] + + # Get chi_ambient: a weighted average of expression, possibly with extra. + chi_ambient = torch.zeros(n_genes).to(DEVICE) + for i in range(n_cell_types): + chi_ambient = chi_ambient + chi[i, :] * cells_of_each_type[i] * cell_mean_umi[i] + if vector_to_add_to_chi_ambient is None: + vector_to_add_to_chi_ambient = torch.zeros(n_genes) + chi_ambient = chi_ambient + vector_to_add_to_chi_ambient + chi_ambient = chi_ambient / chi_ambient.sum() + + c_real = torch.zeros((n_droplets, n_genes)).to(DEVICE) + c_bkg = torch.zeros((n_droplets, n_genes)).to(DEVICE) + labels = torch.zeros(n_droplets).to(DEVICE) + epsilon = torch.zeros(n_droplets).to(DEVICE) + rho = torch.zeros(n_droplets).to(DEVICE) + d_cell = torch.zeros(n_droplets).to(DEVICE) + d_empty = torch.zeros(n_droplets).to(DEVICE) + + # Sample counts from cell-containing droplets. + i = 0 + for t, (num_cells, celltype_umi) in enumerate(zip(cells_of_each_type, cell_mean_umi)): + sim = sample_from_model( + num=num_cells, + chi=chi[t, :], + d_mu=np.log(celltype_umi), + d_sigma=cell_lognormal_sigma, + v_mu=np.log(empty_mean_umi), + v_sigma=empty_lognormal_sigma, + y=1, # cell present + chi_ambient=chi_ambient, + eps_param=epsilon_param, + rho_alpha=rho_alpha, + rho_beta=rho_beta, + phi=phi, + model_type=model_type, + ) + c_real[i:(i + num_cells), :] = sim['counts_real'] + c_bkg[i:(i + num_cells), :] = sim['counts_bkg'] + epsilon[i:(i + num_cells)] = sim['epsilon'] + rho[i:(i + num_cells)] = sim['rho'] + d_cell[i:(i + num_cells)] = sim['cell_counts'] + d_empty[i:(i + num_cells)] = sim['empty_counts'] + labels[i:(i + num_cells)] = t + 1 # label cell types with integers > 0 + i = i + num_cells + + # Sample counts from empty droplets. + sim = sample_from_model( + num=n_droplets - i, + chi=chi[0, :], # this doesn't get used since y=0 + d_mu=1, # this doesn't get used since y=0 + d_sigma=1, # this doesn't get used since y=0 + v_mu=np.log(empty_mean_umi), + v_sigma=empty_lognormal_sigma, + y=0, # no cell present + chi_ambient=chi_ambient, + eps_param=epsilon_param, + rho_alpha=rho_alpha, + rho_beta=rho_beta, + phi=phi, + model_type=model_type, + ) + c_real[i:, :] = sim['counts_real'] + c_bkg[i:, :] = sim['counts_bkg'] + epsilon[i:] = sim['epsilon'] + rho[i:] = sim['rho'] + d_empty[i:] = sim['empty_counts'] + + # Get sparse matrices from dense. + counts_true = sp.csr_matrix(c_real.cpu().numpy(), shape=c_real.shape, dtype=int) + counts_bkg = sp.csr_matrix(c_bkg.cpu().numpy(), shape=c_bkg.shape, dtype=int) + + return {'counts_true': counts_true, + 'counts_bkg': counts_bkg, + 'barcodes': np.array([f'bc{n:06d}' for n in np.arange(n_droplets)]), + 'gene_names': np.array([f'g{n:05d}' for n in np.arange(n_genes)]), + 'chi': {str(i + 1): chi[i, :].cpu().numpy() for i in range(chi.shape[0])}, + 'chi_ambient': chi_ambient.cpu().numpy(), + 'droplet_labels': labels.int().cpu().numpy(), + 'd_cell': d_cell.cpu().numpy(), + 'd_empty': d_empty.cpu().numpy(), + 'epsilon': epsilon.cpu().numpy(), + 'rho': rho.cpu().numpy(), + 'cell_mean_umi': {str(i + 1): u for i, u in enumerate(cell_mean_umi)}, + 'cell_lognormal_sigma': cell_lognormal_sigma, + 'empty_mean_umi': empty_mean_umi, + 'empty_lognormal_sigma': empty_lognormal_sigma, + 'epsilon_param': epsilon_param, + 'dirichlet_alpha': dirichlet_alpha, + 'phi': phi, + 'model': [model_type]} + + +def draw_random_chi(alpha: float = 1., + n_genes: int = 10000) -> torch.Tensor: """Sample a gene expression vector, chi, from a Dirichlet prior. - Args: alpha: Concentration parameter for Dirichlet distribution, to be expanded into a vector to use as the Dirichlet concentration parameter. n_genes: Number of genes. - Returns: chi: Vector of fractional gene expression, drawn from a Dirichlet distribution. - """ assert alpha > 0, "Concentration parameter, alpha, must be > 0." assert n_genes > 0, "Number of genes, n_genes, must be > 0." # Draw gene expression from a Dirichlet distribution. - chi = np.random.dirichlet(alpha * np.ones(n_genes), size=1).squeeze() - - # Normalize gene expression and return result. - chi = chi / np.sum(chi) - + chi = torch.distributions.Dirichlet(alpha * torch.ones(n_genes).to(DEVICE)).sample().squeeze() + return chi -def sample_expression_from(chi: np.ndarray, - n: int = 100, - d_mu: float = np.log(5000).item(), - d_sigma: float = 0.2, - phi: float = 0.3) -> Tuple[sp.csr.csr_matrix, - np.ndarray]: - """Generate a count matrix given a mean expression distribution. - +def sample_from_inferred_model(num: int, + model: 'RemoveBackgroundPyroModel', + z: Optional[torch.Tensor] = None, + y: Optional[torch.Tensor] = None) -> Dict[str, np.ndarray]: + """Sample data from the model, where the model is not completely naive, but + uses a decoder trained on real data.""" + + # Run the model, . + data = torch.zeros(size=(num, model.n_genes)) # model uses shape only + tracing_model = model.model + do_list = [] + if z is not None: + tracing_model = pyro.poutine.do(tracing_model, data={'z': z}) + do_list.append('z') + if y is not None: + tracing_model = pyro.poutine.do(tracing_model, data={'y': y}) + do_list.append('z') + + model_trace = pyro.poutine.trace(tracing_model).get_trace(data) + + # Get outputs of model (mu, alpha, lambda). + model_output = model_trace.nodes['_RETURN']['value'] + + # Get traced parameters. + params = {name: (model_trace.nodes[name]['value'].unconstrained() + if (model_trace.nodes[name]['type'] == 'param') + else model_trace.nodes[name]['value']) + for name in ['chi'] + model_trace.param_nodes + model_trace.stochastic_nodes + if (not ('$$$' in name) and name not in do_list)} # the do-samples are dummies + + # Extract latent values. + epsilon = params['epsilon'] + rho = params['rho'] + d = params['d_cell'] + d_empty = params['d_empty'] + y = y if (y is not None) else params['y'] # the "do" does not show up in the trace, apparently + chi = params['chi'] + chi_bar = model.avg_gene_expression + chi_ambient = params['chi_ambient'] # pull from the (loaded) param store + + # Because the model integrates out real counts and noise counts, we sample here. + logit = torch.log(model_output['mu']) - torch.log(model_output['alpha']) + c_real = pyro.distributions.NegativeBinomial(total_count=model_output['alpha'], + logits=logit).to_event(1).sample() + c_bkg = pyro.distributions.Poisson(model_output['lam']).to_event(1).sample() + + # Output observed counts are the sum, but return them separately. + return {'counts_real': c_real.detach().cpu().numpy(), + 'counts_bkg': c_bkg.detach().cpu().numpy(), + 'cell_counts': (d * y).detach().cpu().numpy(), + 'empty_counts': d_empty.detach().cpu().numpy(), + 'epsilon': epsilon.detach().cpu().numpy(), + 'rho': rho.detach().cpu().numpy(), + 'chi': chi.detach().cpu().numpy()} + + +def sample_from_model( + num: int, + chi: torch.Tensor, + d_mu: float, + d_sigma: float, + v_mu: float, + v_sigma: float, + y: int, + chi_ambient: torch.Tensor, + eps_param: float, + model_type: str = 'full', + rho_alpha: float = 3, + rho_beta: float = 80, + phi: float = 0.1, +) -> Dict[str, torch.Tensor]: + """Draw samples of cell expression profiles using the negative binomial - + Poisson sum model. + + NOTE: Single cell type with expression chi. + NOTE: Random number seeding should be done before this function call. + Args: - chi: Normalized gene expression vector (sums to one). - n: Number of desired cells to simulate. - d_mu: Log mean number of UMI counts per cell. - d_sigma: Standard deviation of a normal in log space for the number - of UMI counts per cell. - phi: The overdispersion parameter of a negative binomial, - i.e., variance = mean + phi * mean^2 - + num: Number of expression profiles to draw. + chi: Cell expression profile, size (n_gene). + d_mu: Mean of LogNormal cell size distribution. + d_sigma: Scale parameter of LogNormal cell size distribution. + v_mu: Mean of LogNormal empty size distribution. + v_sigma: Scale parameter of LogNormal empty size distribution. + y: 1 for cell(s), 0 for empties. + chi_ambient: Ambient gene expression profile (sums to one). + eps_param: Parameter for gamma distribution of the epsilon droplet + efficiency factor ~ Gamma(eps_param, 1/eps_param), i.e. mean is 1. + model_type: ['ambient', 'swapping', 'full'] + rho_alpha: Beta distribution param alpha for swapping fraction. + rho_beta: Beta distribution param beta for swapping fraction. + phi: The negative binomial overdispersion parameter. Returns: - csr_cell_gene: scipy.sparse.csr_matrix of gene expression - counts per cell, with cells in axis=0 and genes in axis=1. - - Note: - Draw gene expression from a negative binomial distribution - counts ~ NB(d*chi, phi) - + Tuple of (c_real, c_bkg) + c_real: Count matrix (cells, genes) for real cell counts. + c_bkg: Count matrix (cells, genes) for background RNA. """ - assert phi > 0, "Phi must be greater than zero in the negative binomial." - assert d_sigma > 0, "Scale parameter, d_sigma, of LogNormal distribution " \ - " must be greater than zero." - assert d_mu > 0, "Location parameter, d_mu, of LogNormal distribution " \ - " must be greater than zero." - assert n > 0, "Number of cells to simulate, n, must be a positive integer." - assert chi.min() >= 0, "Minimum allowed value in chi vector is zero." - - n_genes = chi.size # Number of genes - - # Initialize arrays. - barcodes = np.arange(n) - genes = np.arange(n_genes) - predicted_reads = int(np.exp(d_mu) * n * 2) # Guess array sizes - coo_bc_list = np.zeros(predicted_reads, dtype=np.uint32) - coo_gene_list = np.zeros(predicted_reads, dtype=np.uint32) - coo_count_list = np.zeros(predicted_reads, dtype=np.uint32) - d = np.zeros(n) - a = 0 - - # Go barcode by barcode, sampling UMI counts per gene. - for i in range(n): - - # Sample cell size parameter from a LogNormal distribution. - d[i] = np.exp(np.random.normal(loc=d_mu, scale=d_sigma, size=1)) - - # Sample counts from a negative binomial distribution. - gene_counts = neg_binom(d[i] * chi, phi, size=n_genes) - - # Keep only the non-zero counts to populate the sparse matrix. - num_nonzeros = np.sum(gene_counts > 0) - - # Check whether arrays need to be re-sized to accommodate more entries. - if (a + num_nonzeros) < coo_count_list.size: - # Fill in. - coo_bc_list[a:a+num_nonzeros] = barcodes[i] - coo_gene_list[a:a+num_nonzeros] = genes[gene_counts > 0] - coo_count_list[a:a+num_nonzeros] = gene_counts[gene_counts > 0] - else: - # Resize arrays by doubling. - coo_bc_list = np.resize(coo_bc_list, coo_bc_list.size * 2) - coo_gene_list = np.resize(coo_gene_list, coo_gene_list.size * 2) - coo_count_list = np.resize(coo_count_list, coo_count_list.size * 2) - # Fill in. - coo_bc_list[a:a+num_nonzeros] = barcodes[i] - coo_gene_list[a:a+num_nonzeros] = genes[gene_counts > 0] - coo_count_list[a:a+num_nonzeros] = gene_counts[gene_counts > 0] - - a += num_nonzeros - - # Lop off any unused zero entries at the end of the arrays. - coo_bc_list = coo_bc_list[coo_count_list > 0] - coo_gene_list = coo_gene_list[coo_count_list > 0] - coo_count_list = coo_count_list[coo_count_list > 0] - - # Package data into a scipy.sparse.coo.coo_matrix. - count_matrix = sp.coo_matrix((coo_count_list, (coo_bc_list, coo_gene_list)), - shape=(barcodes.size, n_genes), - dtype=np.uint32) - - # Convert to a scipy.sparse.csr.csr_matrix and return. - count_matrix = count_matrix.tocsr() - - return count_matrix, d - - -def neg_binom(mu: float, phi: float, size: int = 1) -> np.ndarray: - """Parameterize numpy's negative binomial distribution - in terms of the mean and the overdispersion. + # Check inputs. + assert y in [0, 1], f'y must be 0 or 1, but was {y}' + assert d_mu > 0, f'd_mu must be > 0, but was {d_mu}' + assert d_sigma > 0, f'd_sigma must be > 0, but was {d_sigma}' + assert v_mu > 0, f'v_mu must be > 0, but was {v_mu}' + assert v_sigma > 0, f'v_sigma must be > 0, but was {v_sigma}' + assert (chi >= 0).all(), 'all chi values must be >= 0.' + assert (chi_ambient >= 0).all(), 'all chi_ambient must be >= 0.' + assert (1. - chi_ambient.sum()).abs() < 1e-6, f'chi_ambient must sum ' \ + f'to 1, but it sums to {chi_ambient.sum()}' + assert len(chi_ambient.shape) == 1, 'chi_ambient should be 1-dimensional.' + assert chi.shape[-1] == chi_ambient.shape[-1], 'chi and chi_ambient must ' \ + 'be the same size in the rightmost dimension.' + assert (1. - chi.sum()).abs() < 1e-6, f'chi must sum ' \ + f'to 1, but it sums to {chi.sum()}' + assert num > 0, f'num must be > 0, but was {num}' + assert eps_param > 1, f'eps_param must be > 1, but was {eps_param}' - Args: - mu: Mean of the distribution - phi: Overdispersion, such that variance = mean + phi * mean^2 - size: How many numbers to return + # Draw epsilon ~ Gamma(eps_param, 1 / eps_param) + epsilon = torch.distributions.Gamma(concentration=eps_param, rate=eps_param).sample([num]) - Returns: - 'size' number of random draws from a negative binomial distribution. + # Draw d ~ LogNormal(d_mu, d_sigma) + d = torch.distributions.LogNormal(loc=d_mu, scale=d_sigma).sample([num]) - Note: - Setting phi=0 turns the negative binomial distribution into a - Poisson distribution. + # Draw d ~ LogNormal(d_mu, d_sigma) + v = torch.distributions.LogNormal(loc=v_mu, scale=v_sigma).sample([num]) - """ + # Draw rho ~ Beta(rho_alpha, rho_beta) + rho = torch.distributions.Beta(rho_alpha, rho_beta).sample([num]) + + mu = calculate_mu(epsilon=epsilon, + d_cell=d, + chi=chi, + y=torch.ones(d.shape).to(DEVICE) * y, + rho=rho, + model_type=model_type) + 1e-30 + + # Draw cell counts ~ NB(mean = y * epsilon * d * chi, overdispersion = phi) + alpha = 1. / phi + logits = (mu.log() - np.log(alpha)) + c_real = torch.distributions.NegativeBinomial(total_count=alpha, + logits=logits).sample() + + lam = calculate_lambda(epsilon=epsilon, + chi_ambient=chi_ambient, + d_cell=d, + d_empty=v, + y=torch.ones(d.shape).to(DEVICE) * y, + rho=rho, + chi_bar=chi_ambient, + model_type=model_type) + 1e-30 - assert phi > 0, "Phi must be greater than zero in the negative binomial." - assert size > 0, "Number of draws from negative binomial, size, must " \ - "be a positive integer." + # Draw empty counts ~ Poisson(epsilon * v * chi_ambient) + c_bkg = torch.distributions.Poisson(rate=lam).sample() - n = 1. / phi - p = n / (mu + n) - return np.random.negative_binomial(n, p, size=size) + # Output observed counts are the sum, but return them separately. + return {'counts_real': c_real, + 'counts_bkg': c_bkg, + 'cell_counts': d * y, + 'empty_counts': v, + 'epsilon': epsilon, + 'rho': rho} def sample_from_dirichlet_model(num: int, @@ -388,14 +710,12 @@ def sample_from_dirichlet_model(num: int, y: int, chi_ambient: np.ndarray, eps_param: float, - random_seed: int = 0, - include_swapping: bool = False, + rng: Optional[np.random.RandomState] = None, + model_type: str = 'full', rho_alpha: float = 3, - rho_beta: float = 80) -> Tuple[np.ndarray, - np.ndarray]: + rho_beta: float = 80) -> Dict[str, np.ndarray]: """Draw samples of cell expression profiles using the Dirichlet-Poisson, Poisson sum model. - Args: num: Number of expression profiles to draw. alpha: Dirichlet concentration parameters for cell expression profile, @@ -408,16 +728,14 @@ def sample_from_dirichlet_model(num: int, chi_ambient: Ambient gene expression profile (sums to one). eps_param: Parameter for gamma distribution of the epsilon droplet efficiency factor ~ Gamma(eps_param, 1/eps_param), i.e. mean is 1. - random_seed: Seed a random number generator. - include_swapping: Whether to include swapping in the model. + rng: A random number generator. + model_type: ['ambient', 'swapping', 'full'] rho_alpha: Beta distribution param alpha for swapping fraction. rho_beta: Beta distribution param beta for swapping fraction. - Returns: Tuple of (c_real, c_bkg) c_real: Count matrix (cells, genes) for real cell counts. c_bkg: Count matrix (cells, genes) for background RNA. - """ # Check inputs. @@ -427,9 +745,9 @@ def sample_from_dirichlet_model(num: int, assert v_mu > 0, f'v_mu must be > 0, but was {v_mu}' assert v_sigma > 0, f'v_sigma must be > 0, but was {v_sigma}' assert np.all(alpha > 0), 'all alphas must be > 0.' - assert np.all(chi_ambient > 0), 'all chi_ambient must be > 0.' - assert np.abs(1. - chi_ambient.sum()) < 1e-10, f'chi_ambient must sum to 1, but it sums ' \ - f'to {chi_ambient.sum()}' + assert np.all(chi_ambient >= 0), 'all chi_ambient must be >= 0.' + assert np.abs(1. - chi_ambient.sum()) < 1e-10, f'chi_ambient must sum ' \ + f'to 1, but it sums to {chi_ambient.sum()}' assert len(chi_ambient.shape) == 1, 'chi_ambient should be 1-dimensional.' assert alpha.shape[0] == chi_ambient.size, 'alpha and chi_ambient must ' \ 'be the same size in the rightmost dimension.' @@ -437,7 +755,8 @@ def sample_from_dirichlet_model(num: int, assert eps_param > 1, f'eps_param must be > 1, but was {eps_param}' # Seed random number generator. - rng = np.random.RandomState(seed=random_seed) + if rng is None: + rng = np.random.RandomState(seed=0) # Draw chi ~ Dir(alpha) chi = rng.dirichlet(alpha=alpha, size=num) @@ -454,91 +773,138 @@ def sample_from_dirichlet_model(num: int, # Draw rho ~ Beta(rho_alpha, rho_beta) rho = rng.beta(a=rho_alpha, b=rho_beta, size=num) - # print(f'eps.shape is {epsilon.shape}') - # print(f'd.shape is {d.shape}') - # print(f'chi.shape is {chi.shape}') - # print(f'rho.shape is {rho.shape}') - - mu = _calculate_mu(model_type='ambient' if not include_swapping else 'full', - epsilon=torch.Tensor(epsilon), - d_cell=torch.Tensor(d), - chi=torch.Tensor(chi), - y=torch.ones(d.shape) * y, - rho=torch.Tensor(rho)).numpy() + mu = calculate_mu(epsilon=torch.tensor(epsilon), + d_cell=torch.tensor(d), + chi=torch.tensor(chi), + y=torch.ones(d.shape) * y, + rho=torch.tensor(rho), + model_type=model_type).numpy() # Draw cell counts ~ Poisson(y * epsilon * d * chi) - c_real = rng.poisson(lam=mu, - size=(num, chi_ambient.size)) - - lam = _calculate_lambda(model_type='ambient' if not include_swapping else 'full', - epsilon=torch.Tensor(epsilon), - chi_ambient=torch.Tensor(chi_ambient), - d_cell=torch.Tensor(d), - d_empty=torch.Tensor(v), - y=torch.ones(d.shape) * y, - rho=torch.Tensor(rho), - chi_bar=torch.Tensor(chi_ambient)).numpy() + c_real = rng.poisson(lam=mu, size=(num, chi_ambient.size)) + + lam = calculate_lambda(epsilon=torch.tensor(epsilon), + chi_ambient=torch.tensor(chi_ambient), + d_cell=torch.tensor(d), + d_empty=torch.tensor(v), + y=torch.ones(d.shape) * y, + rho=torch.tensor(rho), + chi_bar=torch.tensor(chi_ambient), + model_type=model_type).numpy() # Draw empty counts ~ Poisson(epsilon * v * chi_ambient) - c_bkg = rng.poisson(lam=lam, - size=(num, chi_ambient.size)) + c_bkg = rng.poisson(lam=lam, size=(num, chi_ambient.size)) # Output observed counts are the sum, but return them separately. - return c_real, c_bkg - - -def _calculate_lambda(model_type: str, - epsilon: torch.Tensor, - chi_ambient: torch.Tensor, - d_empty: torch.Tensor, - y: Union[torch.Tensor, None] = None, - d_cell: Union[torch.Tensor, None] = None, - rho: Union[torch.Tensor, None] = None, - chi_bar: Union[torch.Tensor, None] = None): - """Calculate noise rate based on the model.""" - - if model_type == "simple" or model_type == "ambient": - lam = epsilon.unsqueeze(-1) * d_empty.unsqueeze(-1) * chi_ambient - - elif model_type == "swapping": - lam = (rho.unsqueeze(-1) * y.unsqueeze(-1) - * epsilon.unsqueeze(-1) * d_cell.unsqueeze(-1) - + d_empty.unsqueeze(-1)) * chi_bar - - elif model_type == "full": - lam = ((1 - rho.unsqueeze(-1)) * d_empty.unsqueeze(-1) * chi_ambient.unsqueeze(0) - + rho.unsqueeze(-1) - * (y.unsqueeze(-1) * epsilon.unsqueeze(-1) * d_cell.unsqueeze(-1) - + d_empty.unsqueeze(-1)) * chi_bar) - else: - raise NotImplementedError(f"model_type was set to {model_type}, " - f"which is not implemented.") + return {'counts_real': c_real, + 'counts_bkg': c_bkg, + 'cell_counts': d * y, + 'empty_counts': v, + 'epsilon': epsilon, + 'rho': rho} - return lam +def get_dataset_dict_as_anndata( + sample_dataset: Dict[str, Union[float, np.ndarray, sp.csr_matrix]] +) -> 'anndata.AnnData': + """Return a simulated dataset as an AnnData object.""" -def _calculate_mu(model_type: str, - epsilon: torch.Tensor, - d_cell: torch.Tensor, - chi: torch.Tensor, - y: Union[torch.Tensor, None] = None, - rho: Union[torch.Tensor, None] = None): - """Calculate mean expression based on the model.""" + import anndata - if model_type == 'simple': - mu = epsilon.unsqueeze(-1) * d_cell.unsqueeze(-1) * chi + d = sample_dataset.copy() - elif model_type == 'ambient': - mu = (y.unsqueeze(-1) * epsilon.unsqueeze(-1) - * d_cell.unsqueeze(-1) * chi) + # counts + if 'counts_true' in d.keys() and 'counts_bkg' in d.keys(): + adata = anndata.AnnData(X=(d['counts_true'] + d['counts_bkg']).astype(np.float32), + obs={'barcode': d.pop('barcodes')}, + var={'gene': d.pop('gene_names')}) + adata.layers['counts_true'] = d.pop('counts_true') + adata.layers['counts'] = adata.layers['counts_true'] + d.pop('counts_bkg') + elif 'matrix' in d.keys(): + adata = anndata.AnnData(X=d.pop('matrix').astype(np.float32), + obs={'barcode': d.pop('barcodes')}, + var={'gene': d.pop('gene_names')}) - elif model_type == 'swapping' or model_type == 'full': - mu = ((1 - rho.unsqueeze(-1)) - * y.unsqueeze(-1) * epsilon.unsqueeze(-1) - * d_cell.unsqueeze(-1) * chi) + # obs + obs_keys = ['droplet_labels', 'd_cell', 'epsilon'] + for key in obs_keys: + adata.obs[key] = d.pop(key, None) - else: - raise NotImplementedError(f"model_type was set to {model_type}, " - f"which is not implemented.") + # uns + for key, value in d.items(): + adata.uns[key] = value + + return adata + + +def write_simulated_data_to_h5(output_file: str, + d: Dict[str, Union[float, np.ndarray, sp.csr_matrix]], + cellranger_version: int = 3) -> bool: + """Helper function to write the full (noisy) simulate dataset to an H5 file. + + Args: + output_file: File name + d: Resulting dict from `generate_sample_dataset` + + Returns: + True if save was successful. + """ + + assert cellranger_version in [2, 3], 'cellranger_version must be 2 or 3' + + return write_matrix_to_cellranger_h5( + output_file=output_file, + gene_names=d['gene_names'], + feature_types=np.array(['Gene Expression'] * d['gene_names'].size), + genomes=(np.array(['simulated'] * d['gene_names'].size) + if ('genome' not in d.keys()) else d['genome']), + barcodes=d['barcodes'], + count_matrix=(d['counts_true'] + d['counts_bkg']).tocsc(), + cellranger_version=cellranger_version, + ) + + +def write_simulated_truth_to_h5(output_file: str, + d: Dict[str, Union[float, np.ndarray, sp.csr_matrix]], + cellranger_version: int = 3) -> bool: + """Helper function to write the full (noisy) simulate dataset to an H5 file. + + Args: + output_file: File name + d: Resulting dict from `generate_sample_dataset` + + Returns: + True if save was successful. + """ - return mu + assert cellranger_version in [2, 3], 'cellranger_version must be 2 or 3' + + latents = set(d.keys()) + extra_latents = latents - {'gene_names', 'genome', 'barcodes', 'counts_true', + 'droplet_labels', 'd_cell', 'd_empty', 'epsilon', 'rho', + 'chi', 'cell_mean_umi', 'counts_bkg'} + global_latents = {f'truth_{key.replace("chi_ambient", "ambient_expression")}': d[key] + for key in extra_latents} + + for i, v in d['chi'].items(): + global_latents.update({f'truth_gene_expression_cell_label_{i}': v}) + for i, v in d['cell_mean_umi'].items(): + global_latents.update({f'truth_mean_umi_cell_label_{i}': v}) + + return write_matrix_to_cellranger_h5( + output_file=output_file, + gene_names=d['gene_names'], + feature_types=np.array(['Gene Expression'] * d['gene_names'].size), + genomes=(np.array(['simulated'] * d['gene_names'].size) + if ('genome' not in d.keys()) else d['genome']), + barcodes=d['barcodes'], + count_matrix=d['counts_true'].tocsc(), # just the truth, no background + cellranger_version=cellranger_version, + local_latents={'truth_cell_label': d['droplet_labels'], + 'truth_cell_size': d['d_cell'], + 'truth_empty_droplet_size': d['d_empty'], + 'truth_droplet_efficiency': d['epsilon'], + 'truth_cell_probability': (d['droplet_labels'] != 0).astype(float), + 'truth_swapping_fraction': d['rho']}, + global_latents=global_latents, + ) diff --git a/cellbender/remove_background/data/io.py b/cellbender/remove_background/data/io.py new file mode 100644 index 0000000..8c1fec7 --- /dev/null +++ b/cellbender/remove_background/data/io.py @@ -0,0 +1,1207 @@ +"""Handle input parsing and output writing.""" + +import tables +import anndata +import numpy as np +import scipy.sparse as sp +import scipy.io as io + +from cellbender.remove_background import consts + +from typing import Dict, Union, List, Optional, Callable +import logging +import os +import gzip +import traceback + + +logger = logging.getLogger('cellbender') + + +class IngestedData(dict): + """Small container object for the results of file loading. This is a way to + ensure that all filetypes are loaded into the same general format that can + be used in dataset.py + + NOTE: This really exists to ensure all these fields are present, and to + force each loader to specify each field + """ + + def __init__(self, matrix, barcodes, + gene_names, gene_ids, feature_types, genomes, + **kwargs): + # Fill in some fields no matter the input source (for loading in scanpy) + blank_array = np.array(['NA'] * len(gene_names)) + if genomes is None: + genomes = blank_array + if gene_ids is None: + gene_ids = blank_array + if feature_types is None: + feature_types = blank_array + + # Warn if file looks filtered. + if len(barcodes) < consts.MINIMUM_BARCODES_H5AD: + logger.warning(f'WARNING: Only {len(barcodes)} barcodes in the input file. ' + f'Ensure this is a raw (unfiltered) file with all barcodes, ' + f'including the empty droplets.') + + # Required values, some of which can be None + super().__init__([('matrix', matrix), + ('barcodes', barcodes), + ('gene_names', gene_names), + ('gene_ids', gene_ids), + ('feature_types', feature_types), + ('genomes', genomes)]) + self.update(**kwargs) # cellranger version, for example, is optional + + +class FileLoader: + """Make explicit guarantees about what a file-loading method yields.""" + + def __init__(self, load_fn): + self.load_fn = load_fn + + def load(self, file) -> IngestedData: + data = self.load_fn(file) + return IngestedData(**data) + + +def write_matrix_to_cellranger_h5( + cellranger_version: int, + output_file: str, + gene_names: np.ndarray, + barcodes: np.ndarray, + count_matrix: sp.csc_matrix, + feature_types: Optional[np.ndarray] = None, + gene_ids: Optional[np.ndarray] = None, + genomes: Optional[np.ndarray] = None, + local_latents: Dict[str, Optional[np.ndarray]] = {}, + global_latents: Dict[str, Optional[np.ndarray]] = {}, + metadata: Dict[str, Optional[Union[np.ndarray, int, str, Dict]]] = {}) -> bool: + """Write count matrix data to output HDF5 file using CellRanger format. + + Args: + cellranger_version: Either 2 or 3. Determines the format of the output + h5 file. + output_file: Path to output .h5 file (e.g., 'output.h5'). + gene_names: Name of each gene (column of count matrix). + gene_ids: Ensembl ID of each gene (column of count matrix). + genomes: Name of the genome that each gene comes from. + feature_types: Type of each feature (column of count matrix). + barcodes: Name of each barcode (row of count matrix). + count_matrix: Count matrix to be written to file, in sparse + format. Rows are barcodes, columns are genes. + local_latents: Local latent variables. Should include one key called + 'barcodes' which specifies the droplets being referred to. + global_latents: Global latent variables. + metadata: Other metadata like loss per epoch and FPR, etc. + + Note: + To match the CellRanger .h5 files, the matrix is stored as its + transpose, with rows as genes and cell barcodes as columns. + + """ + + assert isinstance(count_matrix, sp.csc_matrix), \ + "The count matrix must be csc_matrix format in order to write to HDF5." + + assert gene_names.size == count_matrix.shape[1], \ + "The number of gene names must match the number of columns in the count matrix." + + if gene_ids is not None: + assert gene_names.size == gene_ids.size, \ + f"The number of gene_names {gene_names.shape} must match " \ + f"the number of gene_ids {gene_ids.shape}." + + if feature_types is not None: + assert gene_names.size == feature_types.size, \ + f"The number of gene_names {gene_names.shape} must match " \ + f"the number of feature_types {feature_types.shape}." + + if genomes is not None: + assert gene_names.size == genomes.size, \ + "The number of gene_names must match the number of genome designations." + + assert barcodes.size == count_matrix.shape[0], \ + "The number of barcodes must match the number of rows in the count matrix." + + # This reverses the role of rows and columns, to match CellRanger format. + count_matrix = count_matrix.transpose().tocsc() + + # Write to output file. + filters = tables.Filters(complevel=1, complib='zlib', shuffle=True) + filter_noshuffle = tables.Filters(complevel=1, complib='zlib', shuffle=False) + with tables.open_file(output_file, "w", + title="CellBender remove-background output") as f: + + if cellranger_version == 2: + + # Create the group where count data will be stored + group = f.create_group("/", "matrix_v2", "Counts after background correction") + + # Create arrays within that group for gene info. + f.create_carray(group, "gene_names", obj=gene_names, filters=filters) + if gene_ids is None: + # some R loaders require unique values here + gene_ids = np.array([f'NA_{i}' for i in range(gene_names.size)]) + f.create_carray(group, "genes", obj=gene_ids, filters=filters) + if genomes is None: + genomes = np.array(['NA'] * gene_names.size) + f.create_carray(group, "genome", obj=genomes, filters=filters) + + elif cellranger_version == 3: + + # Create the group where count data will be stored + group = f.create_group("/", "matrix", "Counts after background correction") + + # Create a sub-group called "features" + feature_group = f.create_group(group, "features", + "Genes and other features measured") + + # Create arrays within that group for feature info. + f.create_carray(feature_group, "name", obj=gene_names, filters=filters) + if gene_ids is None: + # some R loaders require unique values here + gene_ids = np.array([f'NA_{i}' for i in range(gene_names.size)]) + f.create_carray(feature_group, "id", obj=gene_ids, filters=filters) + if feature_types is None: + feature_types = np.array(['Gene Expression'] * gene_names.size) + f.create_carray(feature_group, "feature_type", obj=feature_types, filters=filters) + if genomes is None: + genomes = np.array(['NA'] * gene_names.size) + f.create_carray(feature_group, "genome", obj=genomes, filters=filters) + + # TODO: Copy the other extraneous information from the input file. + # (Some user might need it for some reason.) + + else: + raise ValueError(f'Trying to save to CellRanger v{cellranger_version} ' + f'format, which is not implemented.') + + # Code for both versions. + f.create_carray(group, "barcodes", obj=barcodes, filters=filter_noshuffle) + + # Create arrays to store the count data. + f.create_carray(group, "data", obj=count_matrix.data, filters=filters) + f.create_carray(group, "indices", obj=count_matrix.indices, filters=filters) + f.create_carray(group, "indptr", obj=count_matrix.indptr, filters=filters) + f.create_carray(group, "shape", atom=tables.Int32Atom(), + obj=np.array(count_matrix.shape, dtype=np.int32), filters=filters) + + # Store local latent variables. + droplet_latent_group = f.create_group("/", "droplet_latents", "Latent variables per droplet") + for key, value in local_latents.items(): + if value is not None: + f.create_carray(droplet_latent_group, key, obj=value, filters=filters) + + # Store global latent variables. + global_group = f.create_group("/", "global_latents", "Global latent variables") + for key, value in global_latents.items(): + if value is not None: + f.create_array(global_group, key, value) + + def create_nonscalar_metadata_array(f, group, k, v): + """Wrap scalar or string values in lists""" + if v is None: + return + if (type(v) == list) or (type(v) == np.ndarray): + f.create_array(group, k, v) + else: + f.create_array(group, k, [v]) + + # Store metadata. + metadata_group = f.create_group("/", "metadata", "Metadata") + for key, value in metadata.items(): + for k, v in unravel_dict(key, value).items(): + create_nonscalar_metadata_array(f, metadata_group, k, v) + + logger.info(f"Succeeded in writing CellRanger " + f"format output to file {output_file}") + + return True + + +def write_posterior_coo_to_h5( + output_file: str, + posterior_coo: sp.coo_matrix, + noise_count_offsets: Dict[int, int], + latents: Dict[str, np.ndarray], + feature_inds: np.ndarray, + barcode_inds: np.ndarray, + regularized_posterior_coo: Optional[sp.coo_matrix] = None, + posterior_kwargs: Optional[Dict] = None, + regularized_posterior_kwargs: Optional[Dict] = None) -> bool: + """Write sparse COO matrix to an HDF5 file, using compression. + + NOTE: COO matrix is indexed by rows 'm' which each map to a unique + (cell, feature). The cell and feature are denoted in the barcode_inds + and feature_inds arrays. The column indices for the COO matrix are the + number of noise counts for each entry in count matrix, starting with zero, + except these noise count values get added to noise_count_offsets, which is + length m. + + Args: + output_file: Path to output .h5 file (e.g., 'output.h5'). + posterior_coo: Posterior to be written to file, in sparse COO [m, c] + format. Rows are 'm'-index, columns are number of noise counts. + noise_count_offsets: The number of noise counts at which each 'm' starts. + Absence of an 'm'-index from the keys of this dict means that the + corresponding 'm'-index starts at 0 noise counts. + latents: MAP values of latent variables for each analyzed barcode. + barcode_inds: Index of each barcode (row of input count matrix). + feature_inds: Index of each feature (column of input count matrix). + regularized_posterior_coo: Regularized posterior. + posterior_kwargs: Keyword arguments used to generate posterior (for + caching) + regularized_posterior_kwargs: Keyword arguments used to generate + posterior (for caching) + + """ + + assert isinstance(posterior_coo, sp.coo_matrix), \ + "The posterior must be coo_matrix format in order to write to HDF5." + + assert barcode_inds.size == posterior_coo.row.size, \ + "len(barcode_inds) must match the number of entries in the posterior COO" + + assert feature_inds.size == posterior_coo.row.size, \ + "len(feature_inds) must match the number of entries in the posterior COO" + + # Write to output file. + filters = tables.Filters(complevel=1, complib='zlib', shuffle=True) + with tables.open_file( + output_file, + "w", + title="CellBender remove-background posterior noise count probabilities" + ) as f: + + # metadata + extras = f.create_group("/", "metadata", "Posterior metadata") + f.create_carray(extras, "barcode_inds", obj=barcode_inds, filters=filters) + f.create_carray(extras, "feature_inds", obj=feature_inds, filters=filters) + if noise_count_offsets != {}: + f.create_carray(extras, "noise_count_offsets_keys", + obj=list(noise_count_offsets.keys()), filters=filters) + f.create_carray(extras, "noise_count_offsets_values", + obj=list(noise_count_offsets.values()), filters=filters) + + # posterior COO + group = f.create_group("/", "posterior_noise_log_prob", "Posterior noise count log probabilities") + f.create_carray(group, "log_prob", obj=posterior_coo.data, filters=filters) + f.create_carray(group, "m_index", obj=posterior_coo.row, filters=filters) + f.create_carray(group, "noise_count", obj=posterior_coo.col, filters=filters) + f.create_carray(group, "shape", atom=tables.Int64Atom(), + obj=np.array(posterior_coo.shape, dtype=np.int64), filters=filters) + + # regularized posterior COO + if regularized_posterior_coo is not None: + group = f.create_group("/", "regularized_posterior_noise_log_prob", + "Regularized posterior noise count log probabilities") + f.create_carray(group, "log_prob", obj=regularized_posterior_coo.data, filters=filters) + f.create_carray(group, "m_index", obj=regularized_posterior_coo.row, filters=filters) + f.create_carray(group, "noise_count", obj=regularized_posterior_coo.col, filters=filters) + f.create_carray(group, "shape", atom=tables.Int64Atom(), + obj=np.array(regularized_posterior_coo.shape, dtype=np.int64), filters=filters) + + # latents + droplet_latent_group = f.create_group("/", "droplet_latents_map", "Latent variables per droplet") + for key, value in latents.items(): + if value is not None: + f.create_carray(droplet_latent_group, key, obj=value, filters=filters) + + # kwargs + if posterior_kwargs is not None: + kwargs_group = f.create_group("/", "kwargs", "Function arguments for posterior") + for key, value in posterior_kwargs.items(): + for k, v in unravel_dict(key, value).items(): + if type(v) == str: + v = np.array([v], dtype=str) + f.create_array(kwargs_group, k, v) + reg_kwargs_group = f.create_group("/", "kwargs_regularized", + "Function arguments for regularized posterior") + if regularized_posterior_kwargs is not None: + for key, value in regularized_posterior_kwargs.items(): + for k, v in unravel_dict(key, value).items(): + if type(v) == str: + v = np.array([v], dtype=str) + f.create_array(reg_kwargs_group, k, v) + + logger.info(f"Succeeded in writing posterior to file {output_file}") + + return True + + +def load_posterior_from_h5(filename: str) -> Dict[str, Union[sp.coo_matrix, np.ndarray]]: + """Load a posterior noise count COO from an h5 file. + + Args: + filename: string path to .h5 file that contains the raw gene + barcode matrices + + Returns: + Dict with ['coo', 'noise_count_offsets', 'barcode_inds', 'feature_inds'] + Posterior noise count COO + Noise count offsets for COO rows + Droplet indices for COO rows + Feature indices for COO rows + """ + + with tables.open_file(filename, 'r') as f: + + # read metadata + barcode_inds = getattr(f.root.metadata, 'barcode_inds').read() + feature_inds = getattr(f.root.metadata, 'barcode_inds').read() + if hasattr(f.root.metadata, 'noise_count_offsets_keys'): + noise_count_offsets_keys = getattr(f.root.metadata, 'noise_count_offsets_keys').read() + noise_count_offsets_values = getattr(f.root.metadata, 'noise_count_offsets_values').read() + noise_count_offsets = dict(zip(noise_count_offsets_keys, noise_count_offsets_values)) + else: + noise_count_offsets = {} + + def _read_coo(group: tables.Group) -> sp.coo_matrix: + data = getattr(group, 'log_prob').read() + row = getattr(group, 'm_index').read() + col = getattr(group, 'noise_count').read() + shape = getattr(group, 'shape').read() + return sp.coo_matrix((data, (row, col)), shape=shape) + + # read coo + posterior_coo = _read_coo(group=f.root.posterior_noise_log_prob) + + # read regularized coo + if hasattr(f.root, 'regularized_posterior_noise_log_prob'): + regularized_posterior_coo = _read_coo(group=f.root.regularized_posterior_noise_log_prob) + else: + regularized_posterior_coo = None + + def _read_as_dict(group: tables.Group) -> Dict: + d = {} + for n in group._f_walknodes('Leaf'): + val = n.read() + if (type(val) == np.ndarray) and ('S' in val.dtype.kind): + val = val.item().decode() + d.update({n.name: val}) + return d + + # read latents + latents = _read_as_dict(group=f.root.droplet_latents_map) + + # read kwargs + if hasattr(f.root, 'kwargs'): + kwargs = _read_as_dict(group=f.root.kwargs) + else: + kwargs = None + + if hasattr(f.root, 'kwargs_regularized'): + kwargs_regularized = _read_as_dict(group=f.root.kwargs_regularized) + else: + kwargs_regularized = None + + # Issue warnings if necessary, based on dimensions matching. + if posterior_coo.row.size != barcode_inds.size: + logger.warning(f"Number of barcode_inds ({barcode_inds.size}) " + f"in {filename} does not match the number expected from " + f"the sparse COO matrix ({posterior_coo.shape[0]}).") + if posterior_coo.row.size != feature_inds.size: + logger.warning(f"Number of feature_inds ({feature_inds.size}) " + f"in {filename} does not match the number expected from " + f"the sparse COO matrix ({posterior_coo.shape[0]}).") + + return {'coo': posterior_coo, + 'kwargs': kwargs, + 'regularized_coo': regularized_posterior_coo, + 'kwargs_regularized': kwargs_regularized, + 'latents': latents, + 'noise_count_offsets': noise_count_offsets, + 'feature_inds': feature_inds, + 'barcode_inds': barcode_inds} + + +def unravel_dict(pref: str, d: Dict) -> Dict: + """Unravel a nested dict, returning a dict with values that are not dicts""" + + if type(d) != dict: + return {pref: d} + out_d = {} + for k, v in d.items(): + out_d.update({pref + '_' + key: val for key, val in unravel_dict(k, v).items()}) + return out_d + + +def load_data(input_file: str)\ + -> Dict[str, Union[sp.csr_matrix, List[np.ndarray], np.ndarray]]: + """Load a dataset into the SingleCellRNACountsDataset object from + the self.input_file""" + + # Detect input data type. + load_fn = choose_data_loader(input_file=input_file) + + # Load data using the appropriate loader. + logger.info(f"Loading data from {input_file}") + data = FileLoader(load_fn).load(input_file) + + return data + + +def choose_data_loader(input_file: str) -> Callable: + """Detect the type of input data and return the relevant load function.""" + + # Error if no input data file has been specified. + assert input_file is not None, \ + 'Attempting to load data, but no input file was specified.' + + file_ext = os.path.splitext(input_file)[1] + + # Detect type. + if os.path.isdir(input_file): + return get_matrix_from_cellranger_mtx + + elif file_ext == '.h5': + return get_matrix_from_cellranger_h5 + + elif input_file.endswith('.txt.gz') or input_file.endswith('.txt'): + return get_matrix_from_dropseq_dge + + elif input_file.endswith('.csv.gz') or input_file.endswith('.csv'): + return get_matrix_from_bd_rhapsody + + elif file_ext == '.h5ad': + return get_matrix_from_anndata + + elif file_ext == '.loom': + return get_matrix_from_loom + + elif file_ext == '.npz': + return get_matrix_from_npz + + else: + raise ValueError('Failed to determine input file type for ' + + input_file + '\n' + + 'This must either be: a directory that contains ' + 'CellRanger-format MTX outputs; a single CellRanger ' + '".h5" file; a DropSeq-format DGE ".txt.gz" file; ' + 'a BD-Rhapsody-format ".csv" file; a ".h5ad" file ' + 'produced by anndata (include all barcodes); a ' + '".loom" file (include all barcodes); or a ".npz" ' + 'sparse matrix file') + + +def detect_cellranger_version_mtx(filedir: str) -> int: + """Detect which version of CellRanger (2 or 3) created this mtx directory. + + Args: + filedir: string path to .mtx file that contains the raw gene + barcode matrix in a sparse coo text format. + + Returns: + CellRanger version, either 2 or 3, as an integer. + + """ + + assert os.path.isdir(filedir), f"The directory {filedir} is not accessible." + + if os.path.isfile(os.path.join(filedir, 'features.tsv.gz')): + return 3 + + else: + return 2 + + +def detect_cellranger_version_h5(filename: str) -> int: + """Detect which version of CellRanger (2 or 3) created this h5 file. + + Args: + filename: string path to .mtx file that contains the raw gene + barcode matrix in a sparse coo text format. + + Returns: + version: CellRanger version, either 2 or 3, as an integer. + + """ + + with tables.open_file(filename, 'r') as f: + + # For CellRanger v2, each group in the table (other than root) + # contains a genome. + # For CellRanger v3, there is a 'matrix' group that contains 'features'. + + version = 2 + + try: + + # This works for version 3 but not for version 2. + getattr(f.root.matrix, 'features') + version = 3 + + except tables.NoSuchNodeError: + pass + + return version + + +def get_matrix_from_cellranger_mtx(filedir: str) \ + -> Dict[str, Union[sp.csr_matrix, List[np.ndarray], np.ndarray]]: + """Load a count matrix from an mtx directory from CellRanger's output. + + For CellRanger v2: + The directory must contain three files: + matrix.mtx + barcodes.tsv + genes.tsv + + For CellRanger v3: + The directory must contain three files: + matrix.mtx.gz + barcodes.tsv.gz + features.tsv.gz + + This function returns a dictionary that includes the count matrix, the gene + names (which correspond to columns of the count matrix), and the barcodes + (which correspond to rows of the count matrix). + + Args: + filedir: string path to .mtx file that contains the raw gene + barcode matrix in a sparse coo text format. + + Returns: + out['matrix']: scipy.sparse.csr.csr_matrix of unique UMI counts, with + barcodes as rows and genes as columns + out['barcodes']: numpy array of strings which are the nucleotide + sequences of the barcodes that correspond to the rows in + the out['matrix'] + out['gene_names']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string names of genes in the genome, which + correspond to the columns in the out['matrix']. + out['gene_ids']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string Ensembl ID of genes in the genome, which + also correspond to the columns in the out['matrix']. + out['feature_types']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string feature types of genes (or possibly + antibody capture reads), which also correspond to the columns + in the out['matrix']. + + """ + + assert os.path.isdir(filedir), "The directory {filedir} is not accessible." + + # Decide whether data is CellRanger v2 or v3. + cellranger_version = detect_cellranger_version_mtx(filedir=filedir) + logger.info(f"CellRanger v{cellranger_version} format") + + # CellRanger version 3 + if cellranger_version == 3: + + matrix_file = os.path.join(filedir, 'matrix.mtx.gz') + gene_file = os.path.join(filedir, 'features.tsv.gz') + barcode_file = os.path.join(filedir, 'barcodes.tsv.gz') + + # Read in feature names. + features = np.genfromtxt(fname=gene_file, + delimiter="\t", + skip_header=0, + dtype=str) + + # Read in gene expression and feature data. + gene_ids = features[:, 0].squeeze() # first column + gene_names = features[:, 1].squeeze() # second column + feature_types = features[:, 2].squeeze() # third column + + # CellRanger version 2 + elif cellranger_version == 2: + + # Read in the count matrix using scipy. + matrix_file = os.path.join(filedir, 'matrix.mtx') + gene_file = os.path.join(filedir, 'genes.tsv') + barcode_file = os.path.join(filedir, 'barcodes.tsv') + + # Read in gene names. + gene_data = np.genfromtxt(fname=gene_file, + delimiter="\t", + skip_header=0, + dtype=str) + if len(gene_data.shape) == 1: # custom file format with just gene names + gene_names = gene_data.squeeze() + gene_ids = None + else: # the 10x CellRanger v2 format with two columns + gene_names = gene_data[:, 1].squeeze() # second column + gene_ids = gene_data[:, 0].squeeze() # first column + feature_types = None + + else: + raise NotImplementedError('MTX format was not identifiable as CellRanger ' + 'v2 or v3. Please check 10x Genomics formatting.') + + # For both versions: + + # Read in sparse count matrix. + count_matrix = io.mmread(matrix_file).tocsr().transpose() + + # Read in barcode names. + barcodes = np.genfromtxt(fname=barcode_file, + delimiter="\t", + skip_header=0, + dtype=str) + + # Issue warnings if necessary, based on dimensions matching. + if count_matrix.shape[1] != len(gene_names): + logger.warning(f"Number of gene names in {filedir}/genes.tsv " + f"does not match the number expected from the " + f"count matrix.") + if count_matrix.shape[0] != len(barcodes): + logger.warning(f"Number of barcodes in {filedir}/barcodes.tsv " + f"does not match the number expected from the " + f"count matrix.") + + return {'matrix': count_matrix, + 'gene_names': gene_names, + 'feature_types': feature_types, + 'gene_ids': gene_ids, + 'genomes': None, + 'barcodes': barcodes, + 'cellranger_version': cellranger_version} + + +def get_matrix_from_cellranger_h5(filename: str) \ + -> Dict[str, Union[sp.csr_matrix, np.ndarray]]: + """Load a count matrix from an h5 file from CellRanger's output. + + The file needs to be a _raw_gene_bc_matrices_h5.h5 file. This function + returns a dictionary that includes the count matrix, the gene names (which + correspond to columns of the count matrix), and the barcodes (which + correspond to rows of the count matrix). + + This function works for CellRanger v2 and v3 HDF5 formats. + + Args: + filename: string path to .h5 file that contains the raw gene + barcode matrices + + Returns: + out['matrix']: scipy.sparse.csr.csr_matrix of unique UMI counts, with + barcodes as rows and genes as columns + out['barcodes']: numpy array of strings which are the nucleotide + sequences of the barcodes that correspond to the rows in + the out['matrix'] + out['gene_names']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string names of genes in the genome, which + correspond to the columns in the out['matrix']. + out['gene_ids']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string Ensembl ID of genes in the genome, which + also correspond to the columns in the out['matrix']. + out['feature_types']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string feature types of genes (or possibly + antibody capture reads), which also correspond to the columns + in the out['matrix']. + + """ + + # Detect CellRanger version. + cellranger_version = detect_cellranger_version_h5(filename=filename) + logger.info(f"CellRanger v{cellranger_version} format") + + with tables.open_file(filename, 'r') as f: + # Initialize empty lists. + csc_list = [] + barcodes = None + feature_ids = None + feature_types = None + genomes = None + + # CellRanger v2: + # Each group in the table (other than root) contains a genome, + # so walk through the groups to get data for each genome. + if cellranger_version == 2: + + feature_names = [] + feature_ids = [] + genomes = [] + + for group in f.walk_groups(): + try: + # Read in data for this genome, and put it into a + # scipy.sparse.csc.csc_matrix + barcodes = getattr(group, 'barcodes').read() + data = getattr(group, 'data').read() + indices = getattr(group, 'indices').read() + indptr = getattr(group, 'indptr').read() + shape = getattr(group, 'shape').read() + csc_list.append(sp.csc_matrix((data, indices, indptr), + shape=shape)) + fnames_this_genome = getattr(group, 'gene_names').read() + feature_names.extend(fnames_this_genome) + feature_ids.extend(getattr(group, 'genes').read()) + genomes.extend([group._g_gettitle()] * fnames_this_genome.size) + + except tables.NoSuchNodeError: + # This exists to bypass the root node, which has no data. + pass + + # Create numpy arrays. + feature_names = np.array(feature_names, dtype=str) + genomes = np.array(genomes, dtype=str) + if len(feature_ids) > 0: + feature_ids = np.array(feature_ids) + else: + feature_ids = None + + # CellRanger v3: + # There is only the 'matrix' group. + elif cellranger_version == 3: + + # Read in data for this genome, and put it into a + # scipy.sparse.csc.csc_matrix + barcodes = getattr(f.root.matrix, 'barcodes').read() + data = getattr(f.root.matrix, 'data').read() + indices = getattr(f.root.matrix, 'indices').read() + indptr = getattr(f.root.matrix, 'indptr').read() + shape = getattr(f.root.matrix, 'shape').read() + csc_list.append(sp.csc_matrix((data, indices, indptr), + shape=shape)) + + # Read in 'feature' information + feature_group = f.get_node(f.root.matrix, 'features') + feature_names = getattr(feature_group, 'name').read() + + try: + feature_types = getattr(feature_group, 'feature_type').read() + except tables.NoSuchNodeError: + # This exists in case someone produced a file without feature_type. + pass + try: + feature_ids = getattr(feature_group, 'id').read() + except tables.NoSuchNodeError: + # This exists in case someone produced a file without feature id. + pass + try: + genomes = getattr(feature_group, 'genome').read() + except tables.NoSuchNodeError: + # This exists in case someone produced a file without feature genome. + pass + + # Put the data together (possibly from several genomes for v2 datasets). + count_matrix = sp.vstack(csc_list, format='csc') + count_matrix = count_matrix.transpose().tocsr() + + # Issue warnings if necessary, based on dimensions matching. + if count_matrix.shape[1] != feature_names.size: + logger.warning(f"Number of gene names ({feature_names.size}) in {filename} " + f"does not match the number expected from the count " + f"matrix ({count_matrix.shape[1]}).") + if count_matrix.shape[0] != barcodes.size: + logger.warning(f"Number of barcodes ({barcodes.size}) in {filename} " + f"does not match the number expected from the count " + f"matrix ({count_matrix.shape[0]}).") + + return {'matrix': count_matrix, + 'gene_names': feature_names, + 'gene_ids': feature_ids, + 'genomes': genomes, + 'feature_types': feature_types, + 'barcodes': barcodes, + 'cellranger_version': cellranger_version} + + +def get_matrix_from_dropseq_dge(filename: str) \ + -> Dict[str, Union[sp.csr_matrix, np.ndarray]]: + """Load a count matrix from a DropSeq DGE matrix file. + + The file needs to be a gzipped text file in DGE format. This function + returns a dictionary that includes the count matrix, the gene names (which + correspond to columns of the count matrix), and the barcodes (which + correspond to rows of the count matrix). Reads in the file line by line + instead of trying to read in an entire dense matrix at once, which might + require quite a bit of memory. + + Args: + filename: string path to .txt.gz file that contains the raw gene + barcode matrix + + Returns: + out['matrix']: scipy.sparse.csr.csr_matrix of unique UMI counts, with + barcodes as rows and genes as columns + out['barcodes']: numpy array of strings which are the nucleotide + sequences of the barcodes that correspond to the rows in + the out['matrix'] + out['gene_names']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string names of genes in the genome, which + correspond to the columns in the out['matrix']. + out['gene_ids']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string Ensembl ID of genes in the genome, which + also correspond to the columns in the out['matrix']. + + """ + + logger.info(f"DropSeq DGE format") + + load_fcn = gzip.open if filename.endswith('.gz') else open + + with load_fcn(filename, 'rt') as f: + + # Skip the comment '#' lines in header + for header in f: + if header[0] == '#': + continue + else: + break + + # Read in first row with droplet barcodes + barcodes = header.split('\n')[0].split('\t')[1:] + + # Gene names are first entry per row + gene_names = [] + + # Arrays used to construct a sparse matrix + row = [] + col = [] + data = [] + + # Read in rest of file row by row + for i, line in enumerate(f): + # Parse row into gene name and count data + parsed_line = line.split('\n')[0].split('\t') + gene_names.append(parsed_line[0]) + counts = np.array(parsed_line[1:], dtype=int) + + # Create sparse version of data and add to arrays + nonzero_col_inds = np.nonzero(counts)[0] + row.extend([i] * nonzero_col_inds.size) + col.extend(nonzero_col_inds) + data.extend(counts[nonzero_col_inds]) + + count_matrix = sp.csc_matrix((data, (row, col)), + shape=(len(gene_names), len(barcodes)), + dtype=float).transpose() + + return {'matrix': count_matrix, + 'gene_names': np.array(gene_names), + 'gene_ids': None, + 'genomes': None, + 'feature_types': None, + 'barcodes': np.array(barcodes)} + + +def get_matrix_from_bd_rhapsody(filename: str) \ + -> Dict[str, Union[sp.csr_matrix, np.ndarray]]: + """Load a count matrix from a BD Rhapsody MolsPerCell.csv file. + + The file needs to be in MolsPerCell_Unfiltered format, which is comma + separated, where rows are barcodes and columns are genes. Can be gzipped + or not. This function returns a dictionary that includes the count matrix, + the gene names (which correspond to columns of the count matrix), and the + barcodes (which correspond to rows of the count matrix). Reads in the file + line by line instead of trying to read in an entire dense matrix at once, + which might require quite a bit of memory. + + Args: + filename: string path to .csv file that contains the raw gene + barcode matrix MolsPerCell_Unfiltered.csv + + Returns: + out['matrix']: scipy.sparse.csr.csr_matrix of unique UMI counts, with + barcodes as rows and genes as columns + out['barcodes']: numpy array of strings which are the nucleotide + sequences of the barcodes that correspond to the rows in + the out['matrix'] + out['gene_names']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string names of genes in the genome, which + correspond to the columns in the out['matrix']. + out['gene_ids']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string Ensembl ID of genes in the genome, which + also correspond to the columns in the out['matrix']. + + """ + + logger.info(f"BD Rhapsody MolsPerCell_Unfiltered.csv format") + + load_fcn = gzip.open if filename.endswith('.gz') else open + + with load_fcn(filename, 'rt') as f: + + # Skip the comment '#' lines in header + for header in f: + if header[0] == '#': + continue + else: + break + + # Read in first row with gene names + gene_names = header.split('\n')[0].split(',')[1:] + + # Barcode names are first entry per row + barcodes = [] + + # Arrays used to construct a sparse matrix + row = [] + col = [] + data = [] + + # Read in rest of file row by row + for i, line in enumerate(f): + # Parse row into gene name and count data + parsed_line = line.split('\n')[0].split(',') + barcodes.append(parsed_line[0]) + counts = np.array(parsed_line[1:], dtype=np.int) + + # Create sparse version of data and add to arrays + nonzero_col_inds = np.nonzero(counts)[0] + row.extend([i] * nonzero_col_inds.size) + col.extend(nonzero_col_inds) + data.extend(counts[nonzero_col_inds]) + + count_matrix = sp.csc_matrix((data, (row, col)), + shape=(len(barcodes), len(gene_names)), + dtype=np.float) + + return {'matrix': count_matrix, + 'gene_names': np.array(gene_names), + 'gene_ids': None, + 'genomes': None, + 'feature_types': None, + 'barcodes': np.array(barcodes)} + + +def get_matrix_from_npz(filename: str) \ + -> Dict[str, Union[sp.csr_matrix, np.ndarray]]: + """Load a count matrix from a sparse NPZ file, accompanied by barcode and + gene NPY files. + NOTE: This format is one output of the Optimus pipeline. It loads much + faster than a Loom file. The NPZ file requires two accompanying files: + 'col_index.npy' and 'row_index.npy', named exactly as shown, and in the + same directory as the NPZ file. + Args: + filename: string path to .h5ad file that contains the raw gene + barcode matrices + Returns: + out['matrix']: scipy.sparse.csr.csr_matrix of unique UMI counts, with + barcodes as rows and genes as columns + out['barcodes']: numpy array of strings which are the nucleotide + sequences of the barcodes that correspond to the rows in + the out['matrix'] + out['gene_names']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string names of genes in the genome, which + correspond to the columns in the out['matrix']. + out['gene_ids']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string Ensembl ID of genes in the genome, which + also correspond to the columns in the out['matrix']. + out['feature_types']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string feature types of genes (or possibly + antibody capture reads), which also correspond to the columns + in the out['matrix']. + """ + logger.info(f"Optimus sparse NPZ format") + try: + count_matrix = sp.load_npz(file=filename) + file_dir, _ = os.path.split(filename) + gene_ids = np.load(os.path.join(file_dir, 'col_index.npy')) + barcodes = np.load(os.path.join(file_dir, 'row_index.npy')) + except IOError as e: + logger.error('Loading an NPZ file requires two additional files in the ' + f'same directory ({file_dir}): ' + 'one called "col_index.npy" that contains genes, and one ' + 'called "row_index.npy" that contains barcodes.') + logger.error(traceback.format_exc()) + raise e + return {'matrix': count_matrix, + 'gene_names': gene_ids, # that's all we have access to, so we'll use it + 'gene_ids': gene_ids, + 'genomes': None, + 'feature_types': None, + 'barcodes': barcodes} + + +def get_matrix_from_anndata(filename: str) \ + -> Dict[str, Union[sp.csr_matrix, np.ndarray]]: + """Load a count matrix from an h5ad AnnData file. + The file needs to contain raw counts for all measured barcodes in the + `.X` attribute or a `.layer[{'counts', 'spliced'}]` attribute. This function + returns a dictionary that includes the count matrix, the gene names (which + correspond to columns of the count matrix), and the barcodes (which + correspond to rows of the count matrix). + This function works for any AnnData object meeting the above requirements, + as generated by alignment methods like `kallisto | bustools`. + Args: + filename: string path to .h5ad file that contains the raw gene + barcode matrices + Returns: + out['matrix']: scipy.sparse.csr.csr_matrix of unique UMI counts, with + barcodes as rows and genes as columns + out['barcodes']: numpy array of strings which are the nucleotide + sequences of the barcodes that correspond to the rows in + the out['matrix'] + out['gene_names']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string names of genes in the genome, which + correspond to the columns in the out['matrix']. + out['gene_ids']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string Ensembl ID of genes in the genome, which + also correspond to the columns in the out['matrix']. + out['feature_types']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string feature types of genes (or possibly + antibody capture reads), which also correspond to the columns + in the out['matrix']. + """ + logger.info(f"AnnData format") + try: + adata = anndata.read_h5ad(filename) + except anndata._io.utils.AnnDataReadError as e: + logger.error(f'A call to anndata.read_h5ad() with anndata {anndata.__version__} ' + f'threw AnnDataReadError: ') + logger.error(traceback.format_exc()) + raise e + return _dict_from_anndata(adata) + + +def get_matrix_from_loom(filename: str) \ + -> Dict[str, Union[sp.csr_matrix, np.ndarray]]: + """Load a count matrix from a loom file. + The file needs to contain raw counts for all measured barcodes in the + layer '', as in + https://broadinstitute.github.io/warp/docs/Pipelines/Optimus_Pipeline/Loom_schema/ + Returns a dictionary that includes the count matrix, the gene names (which + correspond to columns of the count matrix), and the barcodes (which + correspond to rows of the count matrix). + + Args: + filename: string path to .h5ad file that contains the raw gene + barcode matrices + + Returns: + out['matrix']: scipy.sparse.csr.csr_matrix of unique UMI counts, with + barcodes as rows and genes as columns + out['barcodes']: numpy array of strings which are the nucleotide + sequences of the barcodes that correspond to the rows in + the out['matrix'] + out['gene_names']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string names of genes in the genome, which + correspond to the columns in the out['matrix']. + out['gene_ids']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string Ensembl ID of genes in the genome, which + also correspond to the columns in the out['matrix']. + out['feature_types']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string feature types of genes (or possibly + antibody capture reads), which also correspond to the columns + in the out['matrix']. + """ + logger.info(f"Loom format, expecting Optimus pipeline conventions") + try: + adata = anndata.read_loom(filename, sparse=True, X_name='') + except anndata._io.utils.AnnDataReadError as e: + logger.error(f'A call to anndata.read_loom() with anndata {anndata.__version__} ' + f'threw AnnDataReadError: ') + logger.error(traceback.format_exc()) + raise e + return _dict_from_anndata(adata) + + +def _dict_from_anndata(adata: anndata.AnnData) -> Dict[str, Union[sp.csr_matrix, np.ndarray]]: + """Extract relevant information from AnnData and format it as a dict + + Args: + adata: AnnData object + + Returns: + out['matrix']: scipy.sparse.csr.csr_matrix of unique UMI counts, with + barcodes as rows and genes as columns + out['barcodes']: numpy array of strings which are the nucleotide + sequences of the barcodes that correspond to the rows in + the out['matrix'] + out['gene_names']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string names of genes in the genome, which + correspond to the columns in the out['matrix']. + out['gene_ids']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string Ensembl ID of genes in the genome, which + also correspond to the columns in the out['matrix']. + out['feature_types']: List of numpy arrays, where the number of elements + in the list is the number of genomes in the dataset. Each numpy + array contains the string feature types of genes (or possibly + antibody capture reads), which also correspond to the columns + in the out['matrix'].""" + + if "counts" in adata.layers.keys(): + # this is a common manual setting for users of scVI + # given the manual convention, we prefer this matrix to + # .X since it is less likely to represent something other + # than counts + logger.info("Found `.layers['counts']`. Using for count data.") + count_matrix = adata.layers["counts"] + elif "spliced" in adata.layers.keys() and adata.X is None: + # alignment using kallisto | bustools with intronic counts + # does not populate `.X` by default, but does populate + # `.layers['spliced'], .layers['unspliced']`. + # we use spliced counts for analysis + logger.info("Found `.layers['spliced']`. Using for count data.") + count_matrix = adata.layers["spliced"] + else: + logger.info("Using `.X` for count data.") + count_matrix = adata.X + + # check that `count_matrix` contains a large number of barcodes, + # consistent with a raw single cell experiment + if count_matrix.shape[0] < consts.MINIMUM_BARCODES_H5AD: + # this experiment might be prefiltered + logger.warning(f"Only {count_matrix.shape[0]} barcodes were found.\n" + "This suggests the matrix was prefiltered.\n" + "CellBender requires a raw, unfiltered [Barcodes, Genes] matrix.") + + # AnnData is [Cells, Genes], no need to transpose + # we typecast explicitly in the off chance `count_matrix` was dense. + count_matrix = sp.csr_matrix(count_matrix) + # feature names and ids are not consistently delineated in AnnData objects + # so we attempt to find relevant features using common values. + feature_names = np.array(adata.var_names, dtype=str) + barcodes = np.array(adata.obs_names, dtype=str) + + # Make an attempt to find feature_IDs if they are present. + feature_ids = None + for key in ['gene_id', 'gene_ids', 'ensembl_ids']: + if key in adata.var.keys(): + feature_ids = np.array(adata.var[key].values, dtype=str) + + # Make an attempt to find feature_types if they are present. + feature_types = None + for key in ['feature_type', 'feature_types']: + if key in adata.var.keys(): + feature_types = np.array(adata.var[key].values, dtype=str) + + # Make an attempt to find genomes if they are present. + genomes = None + for key in ['genome', 'genomes']: + if key in adata.var.keys(): + genomes = np.array(adata.var[key].values, dtype=str) + + # Issue warnings if necessary, based on dimensions matching. + if count_matrix.shape[1] != feature_names.size: + logger.warning(f"Number of gene names ({feature_names.size}) " + f"does not match the number expected from the count " + f"matrix ({count_matrix.shape[1]}).") + if count_matrix.shape[0] != barcodes.size: + logger.warning(f"Number of barcodes ({barcodes.size}) " + f"does not match the number expected from the count " + f"matrix ({count_matrix.shape[0]}).") + + return {'matrix': count_matrix, + 'gene_names': feature_names, + 'gene_ids': feature_ids, + 'genomes': genomes, + 'feature_types': feature_types, + 'barcodes': barcodes} diff --git a/cellbender/remove_background/data/priors.py b/cellbender/remove_background/data/priors.py new file mode 100644 index 0000000..3989769 --- /dev/null +++ b/cellbender/remove_background/data/priors.py @@ -0,0 +1,391 @@ +"""Functionality for estimating various priors from the data""" + +import numpy as np +import torch +from scipy.stats import gaussian_kde + +from cellbender.remove_background import consts + +from typing import Dict, Tuple, Union +import logging + + +logger = logging.getLogger('cellbender') + + +def _threshold_otsu(umi_counts: np.ndarray, n_bins: int = 256) -> float: + """Return threshold value based on fast implementation of Otsu's method. + + From skimage, with slight modifications: + https://github.com/scikit-image/scikit-image/blob/ + a4e533ea2a1947f13b88219e5f2c5931ab092413/skimage/filters/thresholding.py#L312 + + Args: + umi_counts: Array of UMI counts + n_bins: Number of bins used to calculate histogram + + Returns: + threshold: Upper threshold value. All droplets with UMI counts greater + than this value are assumed to contain cells. + + References + ---------- + .. [1] Wikipedia, https://en.wikipedia.org/wiki/Otsu's_Method + .. [2] https://scikit-image.org/docs/stable/auto_examples/applications/plot_thresholding.html + + Notes + ----- + The input image must be grayscale. + """ + + # create a UMI count histogram + counts, bin_centers = _create_histogram(umi_counts=umi_counts, n_bins=n_bins) + + # class probabilities for all possible thresholds + weight1 = np.cumsum(counts) + weight2 = np.cumsum(counts[::-1])[::-1] + + # class means for all possible thresholds + mean1 = np.cumsum(counts * bin_centers) / weight1 + mean2 = (np.cumsum((counts * bin_centers)[::-1]) / weight2[::-1])[::-1] + + # Clip ends to align class 1 and class 2 variables: + # The last value of ``weight1``/``mean1`` should pair with zero values in + # ``weight2``/``mean2``, which do not exist. + variance12 = weight1[:-1] * weight2[1:] * (mean1[:-1] - mean2[1:]) ** 2 + + idx = np.argmax(variance12) + threshold = bin_centers[idx] + + return threshold + + +def _create_histogram(umi_counts: np.ndarray, n_bins: int) -> Tuple[np.ndarray, np.ndarray]: + """Return a histogram. + + Args: + umi_counts: Array of UMI counts + n_bins: Number of bins used to calculate histogram + + Returns: + counts: Each element is the number of droplets falling in each UMI + count bin + bin_centers: Each element is the value corresponding to the center of + each UMI count bin + """ + counts, bin_edges = np.histogram(umi_counts.reshape(-1), n_bins) + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2 + return counts.astype('float32', copy=False), bin_centers + + +def _peak_density_given_cutoff(umi_counts: np.ndarray, + cutoff: float, + cell_count_low_limit: float) -> Tuple[float, float]: + """Run scipy.stats gaussian_kde on part of the UMI curve""" + + # get the UMI count values we are including + noncell_counts = umi_counts[umi_counts <= cutoff] + + # resample them: the magic of looking at a log log plot + n_putative_cells = (umi_counts > cell_count_low_limit).sum() + n_putative_empties = len(noncell_counts) + inds = np.logspace(np.log10(n_putative_cells), + np.log10(n_putative_cells + n_putative_empties), + num=1000, + base=10) + inds = [max(0, min(int(ind - n_putative_cells), len(noncell_counts) - 1)) for ind in inds] + + noncell_counts = np.sort(noncell_counts)[::-1][inds] + + # find the peak density: that is the empty count prior + + # calculate range of data, rounding out to make sure we cover everything + log_noncell_counts = np.log(noncell_counts) + x = np.arange( + np.floor(log_noncell_counts.min()) - 0.01, + np.ceil(log_noncell_counts.max()) + 0.01, + 0.1 + ) + + # fit a KDE to estimate density + k = gaussian_kde(log_noncell_counts) + density = k.evaluate(x) + + # the density peak is almost surely the empty droplets + log_peak_ind = np.argmax(density) + log_peak = x[log_peak_ind] + empty_count_prior = np.exp(log_peak) + + # try to go about 1 stdev up from the peak + peak_density = np.max(density) + one_std_density = 0.6 * peak_density + one_std_inds = np.where(density[log_peak_ind:] < one_std_density)[0] + if len(one_std_inds) > 0: + one_std_ind = one_std_inds[0] + else: + one_std_ind = len(density[log_peak_ind:]) - 1 + empty_count_upper_limit = np.exp(x[log_peak_ind:][one_std_ind]) + + return empty_count_prior, empty_count_upper_limit + + +def get_cell_count_given_expected_cells(umi_counts: np.ndarray, + expected_cells: int) -> Dict[str, float]: + """In the case where a prior is passed in as input, use it + + Args: + umi_counts: Array of UMI counts per droplet, in no particular order + expected_cells: Input by user + + Returns: + Dict with keys ['cell_counts'] + """ + order = np.argsort(umi_counts)[::-1] + cell_counts = np.exp(np.mean(np.log(umi_counts[order][:expected_cells]))).item() + return {'cell_counts': cell_counts} + + +def get_empty_count_given_expected_cells_and_total_droplets( + umi_counts: np.ndarray, + expected_cells: int, + total_droplets: int, +) -> Dict[str, float]: + """In the case where a prior is passed in as input, use it + + Args: + umi_counts: Array of UMI counts per droplet, in no particular order + expected_cells: Input by user, or prior estimate + total_droplets: Input by user + + Returns: + Dict with keys ['empty_counts', 'empty_count_upper_limit'] + """ + + order = np.argsort(umi_counts)[::-1] + starting_point = max(expected_cells, total_droplets - 500) + empty_counts = np.median(umi_counts[order] + [int(starting_point):int(total_droplets)]).item() + + # need to estimate here + cell_counts = np.exp(np.mean(np.log(umi_counts[order][:expected_cells]))).item() + middle = np.sqrt(cell_counts * empty_counts) + empty_count_upper_limit = min(middle, 1.5 * empty_counts) + + return {'empty_counts': empty_counts, + 'empty_count_upper_limit': empty_count_upper_limit} + + +def get_cell_count_empty_count(umi_counts: np.ndarray, + low_count_threshold: float = 15) -> Dict[str, float]: + """Obtain priors on cell counts and empty droplet counts from a UMI curve + using heuristics, and without applying any other prior information. + + Heuristics: + 0. Ignore droplets with counts below low_count_threshold + 1. Use Otsu's method to threshold the log UMI count data (ignoring droplets + past 1/4 of the total droplets above low_count_threshold, as we go down + the UMI curve). This is used as a lower limit on cell counts. + It seems quite robust. + 2. Use the following iterative approach, until converged: + a. Establish an upper cutoff on possible empty droplets, using the + current estimate of empty counts and our cell count prior (the + estimate is 3/4 of the geometric mean of the two). + b. Use gaussian_kde from scipy.stats to create a smooth histogram of + the log UMI counts, for droplets with counts below the cutoff. + - A trick is used to resample the droplets before creating the + histogram, so that it looks more like a log-log plot + c. Identify the peak density of the histogram as the empty count + estimate. + - Convergence happens when our estimate of empty counts stops changing. + + Args: + umi_counts: Array of UMI counts per droplet, in no particular order + low_count_threshold: Ignore droplets with counts below this value + + Returns: + Dict with keys ['cell_counts', 'empty_counts'] + """ + + logger.debug('Beginning priors.get_cell_count_empty_count()') + reverse_sorted_umi_counts = np.sort(umi_counts)[::-1] + umi_counts_for_otsu = reverse_sorted_umi_counts[:(umi_counts > low_count_threshold).sum() // 4] + + log_cell_count_low_limit = _threshold_otsu(np.log(umi_counts_for_otsu)) + cell_count_low_limit = np.exp(log_cell_count_low_limit) + + logger.debug(f'cell_count_low_limit is {cell_count_low_limit}') + cell_count_prior = np.mean(umi_counts[umi_counts > cell_count_low_limit]) + + umi_counts_for_kde = reverse_sorted_umi_counts[reverse_sorted_umi_counts > low_count_threshold] + + # initial conditions for the loop + # start low, but have a failsafe (especially for simulated data) + cutoff = max(0.1 * cell_count_low_limit, umi_counts_for_kde[-100]) + empty_count_prior = -100 + empty_count_upper_limit = None + delta = np.inf + a = 0 + + # iterate to convergence, at most 5 times + while delta > 10: + logger.debug(f'cutoff = {cutoff}') + + # use gaussian_kde to find the peak in the histogram + new_empty_count_prior, empty_count_upper_limit = _peak_density_given_cutoff( + umi_counts=umi_counts_for_kde, + cutoff=cutoff, + cell_count_low_limit=cell_count_low_limit, + ) + logger.debug(f'new_empty_count_prior = {new_empty_count_prior}') + + # 3/4 of the geometric mean is our new upper cutoff + cutoff = 0.75 * np.sqrt(cell_count_prior * new_empty_count_prior) + delta = np.abs(new_empty_count_prior - empty_count_prior) + logger.debug(f'delta = {delta}') + empty_count_prior = new_empty_count_prior + a += 1 + if a >= 5: + logger.debug('Heuristics for determining empty counts exceeded 5 ' + 'iterations without converging') + break + + # do a final estimation of cell counts: + # go to the halfway point and then take the median of the droplets above + count_crossover = np.sqrt(cell_count_prior * empty_count_prior) + cell_count_prior = np.median(umi_counts[umi_counts > count_crossover]) + + logger.debug(f'cell_count_prior is {cell_count_prior}') + logger.debug(f'empty_count_prior is {empty_count_prior}') + logger.debug('End of priors.get_cell_count_empty_count()') + + return {'cell_counts': cell_count_prior, + 'empty_counts': empty_count_prior, + 'empty_count_upper_limit': empty_count_upper_limit} + + +def get_expected_cells_and_total_droplets(umi_counts: np.ndarray, + cell_counts: float, + empty_counts: float, + empty_count_upper_limit: float, + max_empties: int = consts.MAX_EMPTIES_TO_INCLUDE) \ + -> Dict[str, int]: + """Obtain priors on cell counts and empty droplet counts from a UMI curve + using heuristics, and without applying any other prior information. + + NOTE: to be run using inputs from get_cell_count_empty_count() + + Args: + umi_counts: Array of UMI counts per droplet, in no particular order + cell_counts: Prior from get_cell_count_empty_count() + empty_counts: Prior from get_cell_count_empty_count() + empty_count_upper_limit: Prior from get_cell_count_empty_count() + max_empties: Do not include more putative empty droplets than this + + Returns: + Dict with keys ['expected_cells', 'total_droplets', 'transition_point'] + + Example: + >>> priors = get_cell_count_empty_count(umi_counts) + >>> priors.update(get_expected_cells_and_total_droplets(umi_counts, **priors)) + """ + # expected cells does well when you give it a very conservative estimate + expected_cells = (umi_counts >= cell_counts).sum() + + # total droplets will be between empty_count_prior and its upper limit + total_droplets_count_value = np.sqrt(empty_counts * empty_count_upper_limit) + total_droplets = (umi_counts >= total_droplets_count_value).sum() + + # find the transition point + count_crossover = np.sqrt(cell_counts * empty_counts) + transition_point = (umi_counts >= count_crossover).sum() + + logger.debug(f'In get_expected_cells_and_total_droplets(), found transition ' + f'point at droplet {transition_point}') + + # ensure out heuristics don't go too far out datasets with many cells + total_droplets = min(total_droplets, transition_point + max_empties) + + return {'expected_cells': expected_cells, + 'total_droplets': total_droplets, + 'transition_point': transition_point} + + +def get_priors(umi_counts: np.ndarray, + low_count_threshold: float, + max_total_droplets: int = consts.MAX_TOTAL_DROPLETS_GUESSED) \ + -> Dict[str, Union[int, float]]: + """Get all priors using get_cell_count_empty_count() and + get_expected_cells_and_total_droplets(), employing a failsafe if + total_droplets is improbably large. + + Args: + umi_counts: Array of UMI counts per droplet, in no particular order + low_count_threshold: Ignore droplets with counts below this value + max_total_droplets: If the initial heuristics come up with a + total_droplets value greater than this, we re-run the heuristics + with higher low_count_threshold + + Returns: + Dict with keys ['cell_counts', 'empty_counts', + 'empty_count_upper_limit', 'surely_empty_counts', + 'expected_cells', 'total_droplets', 'log_counts_crossover'] + """ + + logger.debug("Computing priors from the UMI curve") + priors = get_cell_count_empty_count( + umi_counts=umi_counts, + low_count_threshold=low_count_threshold, + ) + priors.update(get_expected_cells_and_total_droplets(umi_counts=umi_counts, **priors)) + logger.debug(f'Automatically computed priors: {priors}') + + a = 0 + while priors['total_droplets'] > max_total_droplets: + logger.debug(f'Heuristics for estimating priors resulted in ' + f'{priors["total_droplets"]} total_droplets, which is ' + f'typically too large. Recomputing with ' + f'low_count_threshold = {priors["empty_count_upper_limit"]:.0f}') + priors = get_cell_count_empty_count( + umi_counts=umi_counts, + low_count_threshold=priors['empty_count_upper_limit'], + ) + priors.update(get_expected_cells_and_total_droplets(umi_counts=umi_counts, **priors)) + logger.debug(f'Automatically computed priors: {priors}') + a += 1 + if a > 5: + break + + # compute a few last things + compute_crossover_surely_empty_and_stds(umi_counts=umi_counts, priors=priors) + + return priors + + +def compute_crossover_surely_empty_and_stds(umi_counts, priors): + """Given cell_counts and total_droplets, compute a few more quantities + + Args: + umi_counts: Array of UMI counts per droplet, in no particular order + priors: Dict of priors + + Returns: + None. Modifies priors dict in place. + """ + + assert 'total_droplets' in priors.keys(), \ + 'Need total_droplets in priors to run compute_crossover_surely_empty_and_stds()' + assert 'cell_counts' in priors.keys(), \ + 'Need cell_counts in priors to run compute_crossover_surely_empty_and_stds()' + + # Compute a crossover point in log count space. + reverse_sorted_counts = np.sort(umi_counts)[::-1] + surely_empty_counts = reverse_sorted_counts[priors['total_droplets']] + log_counts_crossover = (np.log(surely_empty_counts) + np.log(priors['cell_counts'])) / 2 + priors.update({'log_counts_crossover': log_counts_crossover, + 'surely_empty_counts': surely_empty_counts}) + + # Compute several other priors. + log_nonzero_umi_counts = np.log(umi_counts[umi_counts > 0]) + d_std = np.std(log_nonzero_umi_counts[log_nonzero_umi_counts > log_counts_crossover]).item() / 5. + d_empty_std = 0.01 # this is basically turned off in favor of epsilon + priors.update({'d_std': d_std, 'd_empty_std': d_empty_std}) diff --git a/cellbender/remove_background/distributions/NegativeBinomialPoissonConvApprox.py b/cellbender/remove_background/distributions/NegativeBinomialPoissonConvApprox.py index 1f08481..1420df3 100644 --- a/cellbender/remove_background/distributions/NegativeBinomialPoissonConvApprox.py +++ b/cellbender/remove_background/distributions/NegativeBinomialPoissonConvApprox.py @@ -118,7 +118,7 @@ def log_prob(self, value): value=value) # Use a poisson for small mu, where the above approximation is bad. - empty_indices = (mu < 1e-5) + empty_indices = (mu < 1e-5 * lam) poisson_log_prob = self._poisson_log_prob(lam=lam[empty_indices], value=value[empty_indices]) diff --git a/cellbender/remove_background/downstream.py b/cellbender/remove_background/downstream.py new file mode 100644 index 0000000..7f66937 --- /dev/null +++ b/cellbender/remove_background/downstream.py @@ -0,0 +1,479 @@ +"""Functions for downstream work with outputs of remove-background.""" + +from cellbender.remove_background.data.io import load_data + +import tables +import numpy as np +import scipy.sparse as sp +import anndata +from typing import Dict, Optional + + +def dict_from_h5(file: str) -> Dict[str, np.ndarray]: + """Read in everything from an h5 file and put into a dictionary. + + Args: + file: The h5 file + + Returns: + Dictionary containing all the information from the h5 file + """ + d = {} + with tables.open_file(file) as f: + # read in everything + for array in f.walk_nodes("/", "Array"): + d[array.name] = array.read() + return d + + +def anndata_from_h5(file: str, + analyzed_barcodes_only: bool = True) -> anndata.AnnData: + """Load an output h5 file into an AnnData object for downstream work. + + Args: + file: The h5 file + analyzed_barcodes_only: False to load all barcodes, so that the size of + the AnnData object will match the size of the input raw count matrix. + True to load a limited set of barcodes: only those analyzed by the + algorithm. This allows relevant latent variables to be loaded + properly into adata.obs and adata.obsm, rather than adata.uns. + + Returns: + anndata.AnnData: The anndata object, populated with inferred latent variables + and metadata. + + """ + + d = dict_from_h5(file) + X = sp.csc_matrix((d.pop('data'), d.pop('indices'), d.pop('indptr')), + shape=d.pop('shape')).transpose().tocsr() + + # check and see if we have barcode index annotations, and if the file is filtered + barcode_key = [k for k in d.keys() if (('barcode' in k) and ('ind' in k))] + if len(barcode_key) > 0: + max_barcode_ind = d[barcode_key[0]].max() + filtered_file = (max_barcode_ind >= X.shape[0]) + else: + filtered_file = True + + if analyzed_barcodes_only: + if filtered_file: + # filtered file being read, so we don't need to subset + print('Assuming we are loading a "filtered" file that contains only cells.') + pass + elif 'barcode_indices_for_latents' in d.keys(): + X = X[d['barcode_indices_for_latents'], :] + d['barcodes'] = d['barcodes'][d['barcode_indices_for_latents']] + elif 'barcodes_analyzed_inds' in d.keys(): + X = X[d['barcodes_analyzed_inds'], :] + d['barcodes'] = d['barcodes'][d['barcodes_analyzed_inds']] + else: + print('Warning: analyzed_barcodes_only=True, but the key ' + '"barcodes_analyzed_inds" or "barcode_indices_for_latents" ' + 'is missing from the h5 file. ' + 'Will output all barcodes, and proceed as if ' + 'analyzed_barcodes_only=False') + + # Construct the anndata object. + adata = anndata.AnnData(X=X, + obs={'barcode': d.pop('barcodes').astype(str)}, + var={'gene_name': (d.pop('gene_names') if 'gene_names' in d.keys() + else d.pop('name')).astype(str)}, + dtype=X.dtype) + adata.obs.set_index('barcode', inplace=True) + adata.var.set_index('gene_name', inplace=True) + + # For CellRanger v2 legacy format, "gene_ids" was called "genes"... rename this + if 'genes' in d.keys(): + d['id'] = d.pop('genes') + + # For purely aesthetic purposes, rename "id" to "gene_id" + if 'id' in d.keys(): + d['gene_id'] = d.pop('id') + + # If genomes are empty, try to guess them based on gene_id + if 'genome' in d.keys(): + if np.array([s.decode() == '' for s in d['genome']]).all(): + if '_' in d['gene_id'][0].decode(): + print('Genome field blank, so attempting to guess genomes based on gene_id prefixes') + d['genome'] = np.array([s.decode().split('_')[0] for s in d['gene_id']], dtype=str) + + # Add other information to the anndata object in the appropriate slot. + _fill_adata_slots_automatically(adata, d) + + # Add a special additional field to .var if it exists. + if 'features_analyzed_inds' in adata.uns.keys(): + adata.var['cellbender_analyzed'] = [True if (i in adata.uns['features_analyzed_inds']) + else False for i in range(adata.shape[1])] + elif 'features_analyzed_inds' in adata.var.keys(): + adata.var['cellbender_analyzed'] = [True if (i in adata.var['features_analyzed_inds'].values) + else False for i in range(adata.shape[1])] + + if analyzed_barcodes_only: + for col in adata.obs.columns[adata.obs.columns.str.startswith('barcodes_analyzed') + | adata.obs.columns.str.startswith('barcode_indices')]: + try: + del adata.obs[col] + except Exception: + pass + else: + # Add a special additional field to .obs if all barcodes are included. + if 'barcodes_analyzed_inds' in adata.uns.keys(): + adata.obs['cellbender_analyzed'] = [True if (i in adata.uns['barcodes_analyzed_inds']) + else False for i in range(adata.shape[0])] + elif 'barcodes_analyzed_inds' in adata.obs.keys(): + adata.obs['cellbender_analyzed'] = [True if (i in adata.obs['barcodes_analyzed_inds'].values) + else False for i in range(adata.shape[0])] + + return adata + + +def _fill_adata_slots_automatically(adata, d): + """Add other information to the adata object in the appropriate slot.""" + + # TODO: what about "features_analyzed_inds"? If not all features are analyzed, does this work? + + for key, value in d.items(): + try: + if value is None: + continue + value = np.asarray(value) + if len(value.shape) == 0: + adata.uns[key] = value + elif value.shape[0] == adata.shape[0]: + if (len(value.shape) < 2) or (value.shape[1] < 2): + adata.obs[key] = value + else: + adata.obsm[key] = value + elif value.shape[0] == adata.shape[1]: + if value.dtype.name.startswith('bytes'): + adata.var[key] = value.astype(str) + else: + adata.var[key] = value + else: + adata.uns[key] = value + except Exception: + print('Unable to load data into AnnData: ', key, value, type(value)) + + +def load_anndata_from_input(input_file: str) -> anndata.AnnData: + """Load an input file into an AnnData object (used in report generation). + Equivalent to something like scanpy.read(), but uses cellbender's io. + + Args: + input_file: The raw data file + + Returns: + adata.AnnData: The anndata object + + """ + + # Load data as dict. + d = load_data(input_file=input_file) + + # For purely aesthetic purposes, rename slots from the plural to singluar. + for key in ['gene_id', 'barcode', 'genome', 'feature_type', 'gene_name']: + if key + 's' in d.keys(): + d[key] = d.pop(key + 's') + + # Create anndata object from dict. + adata = anndata.AnnData(X=d.pop('matrix'), + obs={'barcode': d.pop('barcode').astype(str)}, + var={'gene_name': d.pop('gene_name').astype(str)}, + dtype=int) + adata.obs.set_index('barcode', inplace=True) + adata.var.set_index('gene_name', inplace=True) + + # Add other information to the anndata object in the appropriate slot. + _fill_adata_slots_automatically(adata, d) + + return adata + + +def load_anndata_from_input_and_output(input_file: str, + output_file: str, + analyzed_barcodes_only: bool = True, + input_layer_key: str = 'cellranger', + retain_input_metadata: bool = False, + gene_expression_encoding_key: str = 'cellbender_embedding', + truth_file: Optional[str] = None) -> anndata.AnnData: + """Load remove-background output count matrix into an anndata object, + together with remove-background metadata and the raw input counts. + + Args: + input_file: Raw h5 file (or other compatible remove-background input) + used as input for remove-background. + output_file: Output h5 file created by remove-background (can be + filtered or not). + analyzed_barcodes_only: Argument passed to anndata_from_h5(). + False to load all barcodes, so that the size of + the AnnData object will match the size of the input raw count matrix. + True to load a limited set of barcodes: only those analyzed by the + algorithm. This allows relevant latent variables to be loaded + properly into adata.obs and adata.obsm, rather than adata.uns. + input_layer_key: Key of the anndata.layer that is created for the raw + input count matrix. + retain_input_metadata: In addition to loading the CellBender metadata, + which happens automatically, set this to True to retain all the + metadata from the raw input file as well. + gene_expression_encoding_key: The CellBender gene expression embedding + will be loaded into adata.obsm[gene_expression_encoding_key] + truth_file: File containing truth data if this is a simulation + + Return: + anndata.AnnData: AnnData object with counts before and after remove-background, + as well as inferred latent variables from remove-background. + + """ + + # Load input data. + adata_raw = load_anndata_from_input(input_file=input_file) + + # Load remove-background output data. + adata_out = anndata_from_h5(output_file, analyzed_barcodes_only=analyzed_barcodes_only) + + # Subset the raw dataset to the relevant barcodes. + adata_raw = adata_raw[adata_out.obs.index] + + # TODO: keep the stuff from the raw file too: from obs and var and uns + # TODO: maybe use _fill_adata_slots_automatically()? or just copy stuff + + # Put count matrices into 'layers' in anndata for clarity. + adata_out.layers[input_layer_key] = adata_raw.X.copy() + adata_out.layers['cellbender'] = adata_out.X.copy() + + # Pre-compute a bit of metadata. + adata_out.var['n_' + input_layer_key] = \ + np.array(adata_out.layers[input_layer_key].sum(axis=0), dtype=int).squeeze() + adata_out.var['n_cellbender'] = \ + np.array(adata_out.layers['cellbender'].sum(axis=0), dtype=int).squeeze() + adata_out.obs['n_' + input_layer_key] = \ + np.array(adata_out.layers[input_layer_key].sum(axis=1), dtype=int).squeeze() + adata_out.obs['n_cellbender'] = \ + np.array(adata_out.layers['cellbender'].sum(axis=1), dtype=int).squeeze() + + # Load truth data, if present. + if truth_file is not None: + adata_truth = anndata_from_h5(truth_file, analyzed_barcodes_only=False) + adata_truth = adata_truth[adata_out.obs.index] + adata_out.layers['truth'] = adata_truth.X.copy() + adata_out.var['n_truth'] = np.array(adata_out.layers['truth'].sum(axis=0), dtype=int).squeeze() + adata_out.obs['n_truth'] = np.array(adata_out.layers['truth'].sum(axis=1), dtype=int).squeeze() + for key in adata_truth.obs.keys(): + if key.startswith('truth_'): + adata_out.obs[key] = adata_truth.obs[key].copy() + for key in adata_truth.uns.keys(): + if key.startswith('truth_'): + adata_out.uns[key] = adata_truth.uns[key].copy() + for key in adata_truth.var.keys(): + if key.startswith('truth_'): + adata_out.var[key] = adata_truth.var[key].copy() + + # Rename the CellBender encoding of gene expression. + if analyzed_barcodes_only: + slot = adata_out.obsm + else: + slot = adata_out.uns + embedding_key = None + for key in ['gene_expression_encoding', 'latent_gene_encoding']: + if key in slot.keys(): + embedding_key = key + break + if gene_expression_encoding_key != embedding_key: + slot[gene_expression_encoding_key] = slot[embedding_key].copy() + del slot[embedding_key] + + return adata_out + + +def _load_anndata_from_input_and_decontx(input_file: str, + output: str, + input_layer_key: str = 'cellranger', + truth_file: Optional[str] = None) -> anndata.AnnData: + """Load decontX output count matrix into an anndata object, + together with remove-background metadata and the raw input counts. + + NOTE: this is used only for dev purposes and only in the report + + Args: + input_file: Raw h5 file (or other compatible remove-background input) + used as input for remove-background. + output: Output h5 file, or a directory where decontX MTX and TSV files + are stored + input_layer_key: Key of the anndata.layer that is created for the raw + input count matrix. + truth_file: File containing truth data if this is a simulation + + Return: + anndata.AnnData: AnnData object with counts before and after remove-background, + as well as inferred latent variables from remove-background. + + """ + + # Load decontX output data. + print('UNSTABLE FEATURE: Trying to load decontX format MTX output') + adata_out = load_anndata_from_input(input_file=output) + adata_out.var_names_make_unique() + + # Load input data. + adata_raw = load_anndata_from_input(input_file=input_file) + adata_raw.var_names_make_unique() + + adata_raw = adata_raw[:, [g in adata_out.var.index for g in adata_raw.var.index]].copy() + adata_out.var['genome'] = adata_raw.var['genome'].copy() + adata_out.var['feature_type'] = adata_raw.var['feature_type'].copy() + adata_out.var['gene_id'] = adata_raw.var['gene_id'].copy() + + # Subset the raw dataset to the relevant barcodes. + empty_logic = np.array([b not in adata_out.obs.index for b in adata_raw.obs.index]) + empty_counts = np.array(adata_raw.X[empty_logic].sum(axis=1)).squeeze() + approx_ambient = np.array(adata_raw.X[empty_logic][empty_counts > 5].sum(axis=0)).squeeze() + approx_ambient = approx_ambient / (approx_ambient.sum() + 1e-10) + print(f'Estimated that there are about {np.median(empty_counts[empty_counts > 5])} counts in empties') + adata_raw = adata_raw[adata_out.obs.index].copy() + adata_out.uns['empty_droplet_size_lognormal_loc'] = np.log(np.median(empty_counts[empty_counts > 5])) + + # Put count matrices into 'layers' in anndata for clarity. + adata_out.layers[input_layer_key] = adata_raw.X.copy() + adata_out.layers['decontx'] = adata_out.X.copy() + + # Pre-compute a bit of metadata. + adata_out.var['n_' + input_layer_key] = np.array(adata_out.layers[input_layer_key].sum(axis=0)).squeeze() + adata_out.var['n_decontx'] = np.array(adata_out.layers['decontx'].sum(axis=0)).squeeze() + adata_out.obs['n_' + input_layer_key] = np.array(adata_out.layers[input_layer_key].sum(axis=1)).squeeze() + adata_out.obs['n_decontx'] = np.array(adata_out.layers['decontx'].sum(axis=1)).squeeze() + adata_out.obs['cell_probability'] = 1. # because decontx data contains only cells + adata_out.uns['target_false_positive_rate'] = 0.01 # TODO: placeholder + adata_out.uns['approximate_ambient_profile'] = approx_ambient + adata_out.var['ambient_expression'] = np.nan + + # Load truth data, if present. + if truth_file is not None: + adata_truth = anndata_from_h5(truth_file, analyzed_barcodes_only=False) + adata_truth = adata_truth[adata_out.obs.index] + + # TODO; a check + adata_truth = adata_truth[:, [g in adata_out.var.index for g in adata_truth.var.index]].copy() + + adata_out.layers['truth'] = adata_truth.X.copy() + adata_out.var['n_truth'] = np.array(adata_out.layers['truth'].sum(axis=0)).squeeze() + adata_out.obs['n_truth'] = np.array(adata_out.layers['truth'].sum(axis=1)).squeeze() + for key in adata_truth.obs.keys(): + if key.startswith('truth_'): + adata_out.obs[key] = adata_truth.obs[key].copy() + for key in adata_truth.uns.keys(): + if key.startswith('truth_'): + adata_out.uns[key] = adata_truth.uns[key].copy() + for key in adata_truth.var.keys(): + if key.startswith('truth_'): + adata_out.var[key] = adata_truth.var[key].copy() + + return adata_out + + +def load_anndata_from_input_and_outputs(input_file: str, + output_files: Dict[str, str], + analyzed_barcodes_only: bool = True, + input_layer_key: str = 'cellranger', + gene_expression_encoding_key: str = 'cellbender_embedding', + truth_file: Optional[str] = None) -> anndata.AnnData: + """Load remove-background output count matrices into an anndata object, + together with remove-background metadata and the raw input counts. + + The use case would typically be cellbender runs with multiple output files + at different FPRs, which we want to compare. + + Args: + input_file: Raw h5 file (or other compatible remove-background input) + used as input for remove-background. + output_files: Output h5 files created by remove-background (can be + filtered or not) or some other method. Dict whose keys are layer keys + and whose values are file names. + analyzed_barcodes_only: Argument passed to anndata_from_h5(). + False to load all barcodes, so that the size of + the AnnData object will match the size of the input raw count matrix. + True to load a limited set of barcodes: only those analyzed by the + algorithm. This allows relevant latent variables to be loaded + properly into adata.obs and adata.obsm, rather than adata.uns. + input_layer_key: Key of the anndata.layer that is created for the raw + input count matrix. + gene_expression_encoding_key: The CellBender gene expression embedding + will be loaded into adata.obsm[gene_expression_encoding_key] + truth_file: File containing truth data if this is a simulation + + Return: + anndata.AnnData: AnnData object with counts before and after remove-background, + as well as inferred latent variables from remove-background. + + """ + + # Load input data. + adata_raw = load_anndata_from_input(input_file=input_file) + adata_raw.var_names_make_unique() + + # Load remove-background output data. + assert type(output_files) == dict, 'output_files must be a dict whose keys are ' \ + 'layer names and whose values are file paths.' + outs = {} + for key, output_file in output_files.items(): + outs[key] = anndata_from_h5(output_file, analyzed_barcodes_only=analyzed_barcodes_only) + outs[key].var_names_make_unique() + + # Subset all datasets to the relevant barcodes and features. + relevant_barcodes = set(adata_raw.obs_names) + relevant_features = set(adata_raw.var_names) + for key, ad in outs.items(): + relevant_barcodes = relevant_barcodes.intersection(set(ad.obs_names)) + relevant_features = relevant_features.intersection(set(ad.var_names)) + if len(relevant_barcodes) < len(adata_raw): + print(f'Warning: subsetting to barcodes common to all datasets: there ' + f'are {len(relevant_barcodes)}') + if len(relevant_features) < adata_raw.shape[1]: + print(f'Warning: subsetting to features common to all datasets: there ' + f'are {len(relevant_features)}') + adata_raw = adata_raw[list(relevant_barcodes)].copy() + adata_raw = adata_raw[:, list(relevant_features)].copy() + for i, (key, ad) in enumerate(outs.items()): + outs[key] = ad[list(relevant_barcodes)].copy() + outs[key] = outs[key][:, list(relevant_features)].copy() + if i == 0: + print(f'Loading latent variables from one output file: {key}') + adata_out = outs[key].copy() + + # Put count matrices into 'layers' in anndata for clarity. + adata_out.layers[input_layer_key] = adata_raw.X.copy() + for key, ad in outs.items(): + adata_out.layers[key] = ad.X.copy() + + # Load truth data, if present. + if truth_file is not None: + adata_truth = anndata_from_h5(truth_file, analyzed_barcodes_only=False) + adata_truth = adata_truth[adata_out.obs.index] + adata_out.layers['truth'] = adata_truth.X.copy() + adata_out.var['n_truth'] = np.array(adata_out.layers['truth'].sum(axis=0)).squeeze() + adata_out.obs['n_truth'] = np.array(adata_out.layers['truth'].sum(axis=1)).squeeze() + for key in adata_truth.obs.keys(): + if key.startswith('truth_'): + adata_out.obs[key] = adata_truth.obs[key].copy() + for key in adata_truth.uns.keys(): + if key.startswith('truth_'): + adata_out.uns[key] = adata_truth.uns[key].copy() + for key in adata_truth.var.keys(): + if key.startswith('truth_'): + adata_out.var[key] = adata_truth.var[key].copy() + + # Rename the CellBender encoding of gene expression. + if analyzed_barcodes_only: + slot = adata_out.obsm + else: + slot = adata_out.uns + embedding_key = None + for key in ['gene_expression_encoding', 'latent_gene_encoding']: + if key in slot.keys(): + embedding_key = key + break + if gene_expression_encoding_key != embedding_key: + slot[gene_expression_encoding_key] = slot[embedding_key].copy() + del slot[embedding_key] + + return adata_out diff --git a/cellbender/remove_background/estimation.py b/cellbender/remove_background/estimation.py new file mode 100644 index 0000000..2443724 --- /dev/null +++ b/cellbender/remove_background/estimation.py @@ -0,0 +1,902 @@ +"""Classes and methods for estimation of noise counts, given a posterior.""" + +import scipy.sparse as sp +import numpy as np +import pandas as pd +import torch +from torch.distributions.categorical import Categorical + +from cellbender.remove_background.sparse_utils import log_prob_sparse_to_dense + +from abc import ABC, abstractmethod +from functools import partial +from itertools import repeat +import multiprocessing as mp +import concurrent.futures +import time +from datetime import datetime +import logging +from typing import Callable, Union, Dict, Generator, Tuple, List, Optional + + +logger = logging.getLogger('cellbender') + + +class EstimationMethod(ABC): + """Base class for estimation of noise counts, given a posterior.""" + + def __init__(self, index_converter: 'IndexConverter'): + """Instantiate the EstimationMethod. + Args: + index_converter: The IndexConverter that can be used to translate + back and forth between count matrix (n, g) indices, and the + flattened, generalized 'm' indices that index the posterior COO. + """ + self.index_converter = index_converter + super(EstimationMethod, self).__init__() + + @abstractmethod + def estimate_noise(self, + noise_log_prob_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + **kwargs) -> sp.csr_matrix: + """Given the full probabilistic posterior, compute noise counts. + Args: + noise_log_prob_coo: The noise log prob data structure: log prob + values in a (m, c) COO matrix + noise_offsets: Noise count offset values keyed by 'm'. + """ + pass + + def _estimation_array_to_csr(self, + data: np.ndarray, + m: np.ndarray, + noise_offsets: Optional[Dict[int, int]], + dtype=np.int64) -> sp.csr_matrix: + """Say you have point estimates for each count matrix element (data) and + you have the 'm'-indices for each value (m). This returns a CSR matrix + that has the shape of the count matrix, where duplicate entries have + been summed. + + Args: + data: Point estimates for each nonzero entry of the count matrix, in + a flat format, indexed by 'm'. + m: Array of the same length as data, where each entry is an m-index. + noise_offsets: Noise count offset values keyed by 'm'. + dtype: Data type for sparse matrix. Int32 is too small for 'm' indices. + + Results: + noise_csr: Noise point estimate, as a CSR sparse matrix. + + """ + return _estimation_array_to_csr( + index_converter=self.index_converter, + data=data, + m=m, + noise_offsets=noise_offsets, + dtype=dtype, + ) + # row, col = self.index_converter.get_ng_indices(m_inds=m) + # if noise_offsets is not None: + # data = data + np.array([noise_offsets[i] for i in m]) + # coo = sp.coo_matrix((data.astype(dtype), (row.astype(dtype), col.astype(dtype))), + # shape=self.index_converter.matrix_shape, dtype=dtype) + # coo.sum_duplicates() + # return coo.tocsr() + + +class SingleSample(EstimationMethod): + """A single sample from the noise count posterior""" + + @torch.no_grad() + def estimate_noise(self, + noise_log_prob_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + device: str = 'cpu', + **kwargs) -> sp.csr_matrix: + """Given the full probabilistic posterior, compute noise counts by + taking a single sample from each probability distribution. + + Args: + noise_log_prob_coo: The noise log prob data structure: log prob + values in a (m, c) COO matrix + noise_offsets: Noise count offset values keyed by 'm'. + device: ['cpu', 'cuda'] - whether to perform the pytorch sampling + operation on CPU or GPU. It's pretty fast on CPU already. + + Returns: + noise_count_csr: Estimated noise count matrix. + """ + def _torch_sample(x): + return Categorical(logits=x, validate_args=False).sample() + + result = apply_function_dense_chunks(noise_log_prob_coo=noise_log_prob_coo, + fun=_torch_sample, + device=device) + return self._estimation_array_to_csr(data=result['result'], m=result['m'], + noise_offsets=noise_offsets) + + +class Mean(EstimationMethod): + """Posterior mean""" + + def estimate_noise(self, + noise_log_prob_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + device: str = 'cpu', + **kwargs) -> sp.csr_matrix: + """Given the full probabilistic posterior, compute noise counts by + taking the mean of each probability distribution. + + Args: + noise_log_prob_coo: The noise log prob data structure: log prob + values in a (m, c) COO matrix + noise_offsets: Noise count offset values keyed by 'm'. + + Returns: + noise_count_csr: Estimated noise count matrix. + """ + # c = torch.arange(noise_log_prob_coo.shape[1], dtype=float).to(device).t() + + def _torch_mean(x): + c = torch.arange(x.shape[1], dtype=float).to(x.device) + return torch.matmul(x.exp(), c.t()) + + result = apply_function_dense_chunks(noise_log_prob_coo=noise_log_prob_coo, + fun=_torch_mean, + device=device) + return self._estimation_array_to_csr(data=result['result'], m=result['m'], + noise_offsets=noise_offsets, + dtype=np.float32) + + +class MAP(EstimationMethod): + """The canonical maximum a posteriori""" + + @staticmethod + def torch_argmax(x): + return x.argmax(dim=-1) + + def estimate_noise(self, + noise_log_prob_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + device: str = 'cpu', + **kwargs) -> sp.csr_matrix: + """Given the full probabilistic posterior, compute noise counts by + taking the maximum a posteriori (MAP) of each probability distribution. + + Args: + noise_log_prob_coo: The noise log prob data structure: log prob + values in a (m, c) COO matrix + noise_offsets: Noise count offset values keyed by 'm'. + device: ['cpu', 'cuda'] - whether to perform the pytorch argmax + operation on CPU or GPU. It's pretty fast on CPU already. + + Returns: + noise_count_csr: Estimated noise count matrix. + """ + result = apply_function_dense_chunks(noise_log_prob_coo=noise_log_prob_coo, + fun=self.torch_argmax, + device=device) + return self._estimation_array_to_csr(data=result['result'], m=result['m'], + noise_offsets=noise_offsets) + + +class ThresholdCDF(EstimationMethod): + """Noise estimation via thresholding the noise count CDF""" + + @staticmethod + def torch_cdf_fun(x: torch.Tensor, q: float): + return (x.exp().cumsum(dim=-1) <= q).sum(dim=-1) + + def estimate_noise(self, + noise_log_prob_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + q: float = 0.5, + device: str = 'cpu', + **kwargs) -> sp.csr_matrix: + """Given the full probabilistic posterior, compute noise counts + + Args: + noise_log_prob_coo: The noise log prob data structure: log prob + values in a (m, c) COO matrix + noise_offsets: Noise count offset values keyed by 'm'. + q: The CDF threshold value. + + Returns: + noise_count_csr: Estimated noise count matrix. + """ + result = apply_function_dense_chunks(noise_log_prob_coo=noise_log_prob_coo, + fun=self.torch_cdf_fun, + device=device, + q=q) + return self._estimation_array_to_csr(data=result['result'], m=result['m'], + noise_offsets=noise_offsets) + + +def _estimation_array_to_csr(index_converter, + data: np.ndarray, + m: np.ndarray, + noise_offsets: Optional[Dict[int, int]], + dtype=np.int64) -> sp.csr_matrix: + """Say you have point estimates for each count matrix element (data) and + you have the 'm'-indices for each value (m). This returns a CSR matrix + that has the shape of the count matrix, where duplicate entries have + been summed. + + Args: + data: Point estimates for each nonzero entry of the count matrix, in + a flat format, indexed by 'm'. + m: Array of the same length as data, where each entry is an m-index. + noise_offsets: Noise count offset values keyed by 'm'. + dtype: Data type for sparse matrix. Int32 is too small for 'm' indices. + + Results: + noise_csr: Noise point estimate, as a CSR sparse matrix. + + """ + row, col = index_converter.get_ng_indices(m_inds=m) + if noise_offsets is not None: + data = data + np.array([noise_offsets.get(i, 0) for i in m]) + coo = sp.coo_matrix((data.astype(dtype), (row.astype(dtype), col.astype(dtype))), + shape=index_converter.matrix_shape, dtype=dtype) + coo.sum_duplicates() + return coo.tocsr() + + +def _mckp_chunk_estimate_noise( + noise_log_prob_coo: sp.coo_matrix, + index_and_logic: Tuple[int, np.ndarray], + noise_offsets: Dict[int, int], + noise_targets_per_gene: np.ndarray, + index_converter: 'IndexConverter', + n_chunks: int, + verbose: bool = False) -> sp.csr_matrix: + """Given the full probabilistic posterior, compute noise counts. This is + to be run for a given chunk of genes at a time. + + Args: + noise_log_prob_coo: The noise log prob data structure: log prob + values in a (m, c) COO matrix. One chunk. + index_and_logic: (chunk_index, logical_coo_indexer) from the chunked + iterator, as you would get from enumerate() + noise_targets_per_gene: Integer noise count target for each gene + noise_offsets: Noise count offset values keyed by 'm'. + index_converter: IndexConverter to go from 'm' to (n, g) + n_chunks: Total chunks, for logging purposes only + verbose: True to print lots of intermediate information (for tests) + + Returns: + noise_count_csr: Estimated noise count matrix. + """ + + i = index_and_logic[0] + + if i == 0: + tt = time.time() + + coo = _subset_coo(noise_log_prob_coo, index_and_logic[1]) + + assert noise_targets_per_gene.size == index_converter.total_n_genes, \ + f'The number of noise count targets ({noise_targets_per_gene.size}) ' \ + f'must match the number of genes ({index_converter.total_n_genes})' + + # First we need to compute the MAP to find out which direction to go. + t = time.time() + map_dict = apply_function_dense_chunks( + noise_log_prob_coo=coo, + fun=MAP.torch_argmax, + device='cpu', + ) + map_csr = _estimation_array_to_csr( + data=map_dict['result'], + m=map_dict['m'], + noise_offsets=noise_offsets, + index_converter=index_converter, + ) + logger.debug(f'{timestamp()} Computed initial MAP estimate') + logger.debug(f'{timestamp()} Time for MAP calculation = {(time.time() - t):.2f} sec') + map_noise_counts_per_gene = np.array(map_csr.sum(axis=0)).squeeze() + additional_noise_counts_per_gene = (noise_targets_per_gene + - map_noise_counts_per_gene).astype(int) + set_positive_genes = set(np.where(additional_noise_counts_per_gene > 0)[0]) + set_negative_genes = set(np.where(additional_noise_counts_per_gene < 0)[0]) # leave out exact matches + abs_additional_noise_counts_per_gene = np.abs(additional_noise_counts_per_gene) + + # Determine which genes need to add and which need to subtract noise counts. + n, g = index_converter.get_ng_indices(m_inds=coo.row) + df = pd.DataFrame(data={'m': coo.row, + 'n': n, + 'g': g, + 'c': coo.col, + 'log_prob': coo.data}) + logger.debug(f'{timestamp()} Computing step directions') + df['positive_step_gene'] = df['g'].apply(lambda gene: gene in set_positive_genes) + df['negative_step_gene'] = df['g'].apply(lambda gene: gene in set_negative_genes) + df['step_direction'] = (df['positive_step_gene'].astype(int) + - df['negative_step_gene'].astype(int)) # -1 or 1 + logger.debug(f'{timestamp()} Step directions done') + + if verbose: + pd.set_option('display.width', 120) + pd.set_option('display.max_columns', 20) + print(df, end='\n\n') + + # Remove all 'm' entries corresponding to genes where target is met by MAP. + df = df[df['step_direction'] != 0] + + # Now we mask out log probs (-np.inf) that represent steps in the wrong direction. + logger.debug(f'{timestamp()} Masking') + lookup_map_from_m = dict(zip(map_dict['m'], map_dict['result'])) + df['map'] = df['m'].apply(lambda x: lookup_map_from_m[x]) + df['mask'] = ((df['negative_step_gene'] & (df['c'] > df['map'])) + | (df['positive_step_gene'] & (df['c'] < df['map']))) # keep MAP + df.loc[df['mask'], 'log_prob'] = -np.inf + logger.debug(f'{timestamp()} Masking done') + + # And we remove those entries. + df = df[~df['mask']] + df = df[[c for c in df.columns if (c != 'mask')]] + + # Sort + logger.debug(f'{timestamp()} Sorting') + df = df.sort_values(by=['m', 'c']) + logger.debug(f'{timestamp()} Sorting done') + + if verbose: + print(df, end='\n\n') + + # Do diff for positive and negative separately, without grouping (faster) + df_positive_steps = df[df['step_direction'] == 1].copy() + df_negative_steps = df[df['step_direction'] == -1].copy() + + logger.debug(f'{timestamp()} Computing deltas') + if len(df_positive_steps > 0): + df_positive_steps.loc[:, 'delta'] = df_positive_steps['log_prob'].diff(periods=1).apply(np.abs) + df_positive_steps.loc[df_positive_steps['c'] == df_positive_steps['map'], 'delta'] = np.nan + if len(df_negative_steps > 0): + df_negative_steps.loc[:, 'delta'] = df_negative_steps['log_prob'].diff(periods=-1).apply(np.abs) + df_negative_steps.loc[df_negative_steps['c'] == df_negative_steps['map'], 'delta'] = np.nan + df = pd.concat([df_positive_steps, df_negative_steps], axis=0) + logger.debug(f'{timestamp()} Computing deltas done') + + if verbose: + print(df, end='\n\n') + + # if this is an empty dataframe, we are not doing anything here beyond MAP + if len(df) == 0: + return map_csr + + # Remove irrelevant entries: those with infinite delta. + df = df[df['delta'].apply(np.isfinite)] + + # if this is an empty dataframe, we are not doing anything here beyond MAP + if len(df) == 0: + return map_csr + + # How many additional noise counts ("steps") we will need for each gene. + logger.debug(f'{timestamp()} Computing nsmallest') + df['topk'] = df['g'].apply(lambda gene: abs_additional_noise_counts_per_gene[gene]) + + if verbose: + print(df, end='\n\n') + + # Now we want the smallest additional_noise_counts_per_gene deltas for each gene. + # https://stackoverflow.com/questions/55179493/ + df_out = df[['m', 'g', 'delta', 'topk']].groupby('g', group_keys=False).apply( + lambda x: x.nsmallest(x['topk'].iat[0], columns='delta') + ) + logger.debug(f'{timestamp()} Computing nsmallest done') + + if verbose: + print(df_out, end='\n\n') + + # if this is an empty dataframe, we are not doing anything here beyond MAP + if len(df_out) == 0: + return map_csr + + # And the number by which to increment noise counts per entry 'm' is + # now the number of times that each m value appears in this dataframe. + logger.debug(f'{timestamp()} Summarizing steps') + vc = df_out['m'].value_counts() + vc_df = pd.DataFrame(data={'m': vc.index, 'steps': vc.values}) + step_direction_lookup_from_m = dict(zip(df['m'], df['step_direction'])) + vc_df['step_direction'] = vc_df['m'].apply(lambda x: step_direction_lookup_from_m[x]) + vc_df['counts'] = vc_df['steps'] * vc_df['step_direction'] + steps_csr = _estimation_array_to_csr( + data=vc_df['counts'], + m=vc_df['m'], + noise_offsets=None, + index_converter=index_converter, + ) + logger.debug(f'{timestamp()} Summarizing steps done') + + if verbose: + print(vc_df, end='\n\n') + print('MAP:') + print(map_csr.todense()) + + logger.info(f'Completed chunk ({i + 1} / {n_chunks})') + print(f'Completed chunk ({i + 1} / {n_chunks})') # because logging from a process does not work right + if i == 0: + logger.info(f' [single chunk took {(time.time() - tt):.2f} mins]') + print(f' [single chunk took {(time.time() - tt):.2f} mins]') + + # The final output is those tabulated steps added to the MAP. + # The MAP already has the noise offsets, so they are not added to steps_csr. + return map_csr + steps_csr + + +class MultipleChoiceKnapsack(EstimationMethod): + """Noise estimation via solving a constrained multiple choice knapsack problem""" + + def estimate_noise(self, + noise_log_prob_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + noise_targets_per_gene: np.ndarray, + verbose: bool = False, + n_chunks: Optional[int] = None, + use_multiple_processes: bool = False, + **kwargs) -> sp.csr_matrix: + """Given the full probabilistic posterior, compute noise counts + + Args: + noise_log_prob_coo: The noise log prob data structure: log prob + values in a (m, c) COO matrix + noise_targets_per_gene: Integer noise count target for each gene + noise_offsets: Noise count offset values keyed by 'm'. + verbose: True to print lots of intermediate information (for tests) + n_chunks: Target number of chunks over which to split estimation. + If None, targets about 5000 genes per chunk. + use_multiple_processes: True to use multiprocessing. Seems faster + without using it, not entirely clear why + + Returns: + noise_count_csr: Estimated noise count matrix. + """ + if n_chunks is None: + n_chunks = max(1, self.index_converter.total_n_genes // 5000) + logger.debug(f'Running MCKP estimator in {n_chunks} chunks') + + t0 = time.time() + + if use_multiple_processes: + + logger.info('Dividing dataset into chunks of genes') + chunk_logic_list = list( + self._gene_chunk_iterator( + noise_log_prob_coo=noise_log_prob_coo, + n_chunks=n_chunks, + ) + ) + + logger.info('Computing the output in asynchronous chunks in parallel...') + + # with mp.get_context('spawn').Pool(processes=mp.cpu_count()) as pool: + # csr_matrices = pool.starmap( + # _mckp_chunk_estimate_noise, + # zip( + # repeat(noise_log_prob_coo), + # enumerate(chunk_logic_list), + # repeat(noise_offsets), + # repeat(noise_targets_per_gene), + # repeat(self.index_converter), + # repeat(n_chunks), + # repeat(False), # verbose + # ), + # ) + + futures = [] + with concurrent.futures.ProcessPoolExecutor( + max_workers=mp.cpu_count(), + mp_context=mp.get_context('spawn')) as executor: + for i, logic in enumerate(chunk_logic_list): + kwargs = { + 'noise_log_prob_coo': noise_log_prob_coo, + 'index_and_logic': (i, logic), + 'noise_offsets': noise_offsets, + 'noise_targets_per_gene': noise_targets_per_gene, + 'index_converter': self.index_converter, + 'n_chunks': n_chunks, + 'verbose': False, + } + future = executor.submit(_mckp_chunk_estimate_noise, **kwargs) + futures.append(future) + + done, not_done = concurrent.futures.wait( + futures, + return_when=concurrent.futures.ALL_COMPLETED, + ) + csr_matrices = [f.result() for f in futures] + + else: + + t = time.time() + + csr_matrices = [] + for i, logic in enumerate( + self._gene_chunk_iterator( + noise_log_prob_coo=noise_log_prob_coo, + n_chunks=n_chunks, + ) + ): + logger.info(f'Working on chunk ({i + 1}/{n_chunks})') + chunk_csr = self._chunk_estimate_noise( + noise_log_prob_coo=_subset_coo(noise_log_prob_coo, logic), + noise_offsets=noise_offsets, + noise_targets_per_gene=noise_targets_per_gene, + verbose=verbose, + ) + csr_matrices.append(chunk_csr) + if i == 0: + logger.info(f' [{(time.time() - t) / 60:.2f} mins per chunk]') + logger.debug(f'{timestamp()} Estimator chunk {i}: shape is {chunk_csr.shape}') + + logger.info(f'{timestamp()} Total MCKP estimation time = {(time.time() - t0):.2f} sec') + return sum(csr_matrices) + + def _gene_chunk_iterator(self, + noise_log_prob_coo: sp.coo_matrix, + n_chunks: int) \ + -> Generator[np.ndarray, None, None]: + """Yields chunks of the posterior that can be treated as independent, + from the standpoint of MCKP count estimation. That is, they contain all + matrix entries for any genes they include. + + Args: + noise_log_prob_coo: Full noise log prob posterior COO + n_chunks: For testing, force this many chunks + + Yields: + Logical array which indexes elements of coo posterior for the chunk + """ + + # TODO this generator is way too slow + + # approximate number of entries in a chunk + # approx_chunk_entries = (noise_log_prob_coo.data.size - 1) // n_chunks + + # get gene annotations + _, genes = self.index_converter.get_ng_indices(m_inds=noise_log_prob_coo.row) + genes_series = pd.Series(genes) + + # things we need to keep track of for each chunk + # current_chunk_genes = [] + # entry_logic = np.zeros(noise_log_prob_coo.data.size, dtype=bool) + + # TODO eliminate for loop to speed this up + # take the list of genes from the coo, sort it, and divide it evenly + # somehow break ties for genes overlapping boundaries of divisions + sorted_genes = np.sort(genes) + gene_arrays = np.array_split(sorted_genes, n_chunks) + last_gene_set = {} + for gene_array in gene_arrays: + gene_set = set(gene_array) + gene_set = gene_set.difference(last_gene_set) # only the new stuff + # if there is a second chunk, make sure there is a gene unique to it + if (n_chunks > 1) and (len(gene_set) == len(set(genes))): # all genes in first set + # this mainly exists for tests + gene_set = gene_set - {gene_arrays[-1][-1]} + last_gene_set = gene_set + entry_logic = genes_series.isin(gene_set).values + if sum(entry_logic) > 0: + yield entry_logic + + def _chunk_estimate_noise(self, + noise_log_prob_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + noise_targets_per_gene: np.ndarray, + verbose: bool = False) -> sp.csr_matrix: + """Given the full probabilistic posterior, compute noise counts. This is + to be run for a given chunk of genes at a time. + + Args: + noise_log_prob_coo: The noise log prob data structure: log prob + values in a (m, c) COO matrix. One chunk. + noise_targets_per_gene: Integer noise count target for each gene + noise_offsets: Noise count offset values keyed by 'm'. + verbose: True to print lots of intermediate information (for tests) + + Returns: + noise_count_csr: Estimated noise count matrix. + """ + + assert noise_targets_per_gene.size == self.index_converter.total_n_genes, \ + f'The number of noise count targets ({noise_targets_per_gene.size}) ' \ + f'must match the number of genes ({self.index_converter.total_n_genes})' + + coo = noise_log_prob_coo.copy() # we will be modifying values + + # First we need to compute the MAP to find out which direction to go. + t = time.time() + map_dict = apply_function_dense_chunks(noise_log_prob_coo=noise_log_prob_coo, + fun=MAP.torch_argmax, + device='cpu') + map_csr = self._estimation_array_to_csr(data=map_dict['result'], + m=map_dict['m'], + noise_offsets=noise_offsets) + logger.debug(f'{timestamp()} Computed initial MAP estimate') + logger.debug(f'{timestamp()} Time for MAP calculation = {(time.time() - t):.2f} sec') + map_noise_counts_per_gene = np.array(map_csr.sum(axis=0)).squeeze() + additional_noise_counts_per_gene = (noise_targets_per_gene + - map_noise_counts_per_gene).astype(int) + set_positive_genes = set(np.where(additional_noise_counts_per_gene > 0)[0]) + set_negative_genes = set(np.where(additional_noise_counts_per_gene < 0)[0]) # leave out exact matches + abs_additional_noise_counts_per_gene = np.abs(additional_noise_counts_per_gene) + + # Determine which genes need to add and which need to subtract noise counts. + n, g = self.index_converter.get_ng_indices(m_inds=coo.row) + df = pd.DataFrame(data={'m': coo.row, + 'n': n, + 'g': g, + 'c': coo.col, + 'log_prob': coo.data}) + logger.debug(f'{timestamp()} Computing step directions') + df['positive_step_gene'] = df['g'].apply(lambda gene: gene in set_positive_genes) + df['negative_step_gene'] = df['g'].apply(lambda gene: gene in set_negative_genes) + df['step_direction'] = (df['positive_step_gene'].astype(int) + - df['negative_step_gene'].astype(int)) # -1 or 1 + logger.debug(f'{timestamp()} Step directions done') + + if verbose: + pd.set_option('display.width', 120) + pd.set_option('display.max_columns', 20) + print(df, end='\n\n') + + # Remove all 'm' entries corresponding to genes where target is met by MAP. + df = df[df['step_direction'] != 0] + + # Now we mask out log probs (-np.inf) that represent steps in the wrong direction. + logger.debug(f'{timestamp()} Masking') + lookup_map_from_m = dict(zip(map_dict['m'], map_dict['result'])) + df['map'] = df['m'].apply(lambda x: lookup_map_from_m[x]) + df['mask'] = ((df['negative_step_gene'] & (df['c'] > df['map'])) + | (df['positive_step_gene'] & (df['c'] < df['map']))) # keep MAP + df.loc[df['mask'], 'log_prob'] = -np.inf + logger.debug(f'{timestamp()} Masking done') + + # And we remove those entries. + df = df[~df['mask']] + df = df[[c for c in df.columns if (c != 'mask')]] + + # Sort + logger.debug(f'{timestamp()} Sorting') + df = df.sort_values(by=['m', 'c']) + logger.debug(f'{timestamp()} Sorting done') + + if verbose: + print(df, end='\n\n') + + # Do diff for positive and negative separately, without grouping (faster) + df_positive_steps = df[df['step_direction'] == 1].copy() + df_negative_steps = df[df['step_direction'] == -1].copy() + + logger.debug(f'{timestamp()} Computing deltas') + if len(df_positive_steps > 0): + df_positive_steps.loc[:, 'delta'] = df_positive_steps['log_prob'].diff(periods=1).apply(np.abs) + df_positive_steps.loc[df_positive_steps['c'] == df_positive_steps['map'], 'delta'] = np.nan + if len(df_negative_steps > 0): + df_negative_steps.loc[:, 'delta'] = df_negative_steps['log_prob'].diff(periods=-1).apply(np.abs) + df_negative_steps.loc[df_negative_steps['c'] == df_negative_steps['map'], 'delta'] = np.nan + df = pd.concat([df_positive_steps, df_negative_steps], axis=0) + logger.debug(f'{timestamp()} Computing deltas done') + + if verbose: + print(df, end='\n\n') + + # if this is an empty dataframe, we are not doing anything here beyond MAP + if len(df) == 0: + return map_csr + + # Remove irrelevant entries: those with infinite delta. + df = df[df['delta'].apply(np.isfinite)] + + # if this is an empty dataframe, we are not doing anything here beyond MAP + if len(df) == 0: + return map_csr + + # How many additional noise counts ("steps") we will need for each gene. + logger.debug(f'{timestamp()} Computing nsmallest') + df['topk'] = df['g'].apply(lambda gene: abs_additional_noise_counts_per_gene[gene]) + + if verbose: + print(df, end='\n\n') + + # Now we want the smallest additional_noise_counts_per_gene deltas for each gene. + # https://stackoverflow.com/questions/55179493/ + df_out = df[['m', 'g', 'delta', 'topk']].groupby('g', group_keys=False).apply( + lambda x: x.nsmallest(x['topk'].iat[0], columns='delta') + ) + logger.debug(f'{timestamp()} Computing nsmallest done') + + if verbose: + print(df_out, end='\n\n') + + # if this is an empty dataframe, we are not doing anything here beyond MAP + if len(df_out) == 0: + return map_csr + + # And the number by which to increment noise counts per entry 'm' is + # now the number of times that each m value appears in this dataframe. + logger.debug(f'{timestamp()} Summarizing steps') + vc = df_out['m'].value_counts() + vc_df = pd.DataFrame(data={'m': vc.index, 'steps': vc.values}) + step_direction_lookup_from_m = dict(zip(df['m'], df['step_direction'])) + vc_df['step_direction'] = vc_df['m'].apply(lambda x: step_direction_lookup_from_m[x]) + vc_df['counts'] = vc_df['steps'] * vc_df['step_direction'] + steps_csr = self._estimation_array_to_csr(data=vc_df['counts'], + m=vc_df['m'], + noise_offsets=None) + logger.debug(f'{timestamp()} Summarizing steps done') + + if verbose: + print(vc_df, end='\n\n') + print('MAP:') + print(map_csr.todense()) + + # The final output is those tabulated steps added to the MAP. + # The MAP already has the noise offsets, so they are not added to steps_csr. + return map_csr + steps_csr + + +def chunked_iterator(coo: sp.coo_matrix, + max_dense_batch_size_GB: float = 1.) \ + -> Generator[Tuple[sp.coo_matrix, np.ndarray], None, None]: + """Return an iterator which yields the full dataset in chunks. + + NOTE: Idea is to prevent memory overflow. The use case is for worst-case + scenario algorithms that have to make things into dense matrix chunks in + order to do their compute. + + Args: + coo: Sparse COO matrix with rows as generalized 'm'-indices and + columns as noise count values. + max_dense_batch_size_GB: Size of a batch on disk, in gigabytes. + + Returns: + A generator that yields compact CSR sparse matrices until the whole dataset + has been yielded. "Compact" in the sense that if they are made dense, there + will be no all-zero rows. + Tuple[chunk csr, actual row values in the full matrix] + + """ + n_elements_in_batch = max_dense_batch_size_GB * 1e9 / 4 # torch float32 is 4 bytes + batch_size = max(1, int(np.floor(n_elements_in_batch / coo.shape[1]))) + + # COO rows are not necessarily contiguous or in order + unique_m_values = np.unique(coo.row) + n_chunks = max(1, len(unique_m_values) // batch_size) + row_m_value_chunks = np.array_split(unique_m_values, n_chunks) + coo_row_series = pd.Series(coo.row) + + for row_m_values in row_m_value_chunks: + logic = coo_row_series.isin(set(row_m_values)) + # Map these row values to a compact set of unique integers + unique_row_values, rows = np.unique(coo.row[logic], return_inverse=True) + unique_col_values, cols = np.unique(coo.col[logic], return_inverse=True) + chunk_coo = sp.coo_matrix( + (coo.data[logic], (rows, cols)), + shape=(len(unique_row_values), len(unique_col_values)), + ) + yield (chunk_coo, unique_row_values, unique_col_values) + + +def apply_function_dense_chunks(noise_log_prob_coo: sp.coo_matrix, + fun: Callable[[torch.Tensor], torch.Tensor], + device: str = 'cpu', + **kwargs) \ + -> Dict[str, np.ndarray]: + """Uses chunked_iterator to densify chunked portions of a COO sparse + matrix and then applies a function to the dense chunks, keeping track + of the results per row. + + NOTE: The function should produce one value per row of the dense matrix. + The COO should contain log probability in data. + + Args: + noise_log_prob_coo: The posterior noise count log prob data structure, + indexed by 'm' as rows + fun: Pytorch function that operates on a dense tensor and produces + one value per row + device: ['cpu', 'cuda'] - whether to perform the pytorch sampling + operation on CPU or GPU. It's pretty fast on CPU already. + **kwargs: Passed to fun + + Returns: + Dict containing + 'm': np.ndarray of indices + 'result': the values computed by the function + + """ + array_length = len(np.unique(noise_log_prob_coo.row)) + + m = np.zeros(array_length) + out = np.zeros(array_length) + a = 0 + + for coo, row, col in chunked_iterator(coo=noise_log_prob_coo): + dense_tensor = torch.tensor(log_prob_sparse_to_dense(coo)).to(device) + if torch.numel(dense_tensor) == 0: + # github issue 207 + continue + s = fun(dense_tensor, **kwargs) + if s.ndim == 0: + # avoid "TypeError: len() of a 0-d tensor" + len_s = 1 + else: + len_s = len(s) + m[a:(a + len_s)] = row + out[a:(a + len_s)] = s.detach().cpu().numpy() + a = a + len_s + + return {'m': m.astype(int), 'result': out} + + +def pandas_grouped_apply(coo: sp.coo_matrix, + fun: Callable[[pd.DataFrame], Union[int, float]], + extra_data: Optional[Dict[str, np.ndarray]] = None, + sort_first: bool = False, + parallel: bool = False) -> Dict[str, np.array]: + """Apply function on a sparse COO format (noise log probs) to compute output + noise counts using pandas groupby and apply operations on CPU. + + TODO: consider numpy-groupies or np.ufunc.reduceat or similar + https://stackoverflow.com/questions/31790819/scipy-sparse-csr-matrix-how-to-get-top-ten-values-and-indices + + Args: + coo: COO data structure: (m, c) with 'm'-indexing. + fun: Function to be applied with pandas .apply(): turns + all the values for a matrix entry (m, :) into one number. + extra_data: To include extra information other than 'm', 'c', and the + 'log_prob', you can pass in a dict here with array values the same + length as 'm' and in the same order. + sort_first: Sort the whole dataframe by ['m', 'c'] before applying + function (much faster than sorting inside the function call). + parallel: Use multiprocessing to run on all cores. This is 4x slower for + a dataset of 300 cells. Not clear if it's faster for larger data. + + Returns: + output_csr: The aggregated sparse matrix, in row format + """ + df = pd.DataFrame(data={'m': coo.row, 'c': coo.col, 'log_prob': coo.data}) + if extra_data is not None: + for k, v in extra_data.items(): + df[k] = v + if sort_first: + df = df.sort_values(by=['m', 'c'], ascending=[True, True]) + if parallel: + m, result_per_m = _parallel_pandas_apply(df_grouped=df.groupby('m'), fun=fun) + else: + df = df.groupby('m').apply(fun).reset_index() + m = df['m'].values + result_per_m = df[0].values + return {'m': m, 'result': result_per_m} + + +def _parallel_pandas_apply(df_grouped: pd.core.groupby.DataFrameGroupBy, + fun: Callable[[pd.DataFrame], Union[int, float]])\ + -> Tuple[np.ndarray, np.ndarray]: + """Use multiprocessing to apply a function to a grouped dataframe in pandas. + + Args: + df_grouped: Grouped dataframe, as in df.groupby('m') + fun: Function to be applied to dataframe, as in df.groupby('m').apply(fun) + + Returns: + Tuple of (groupby_value, groupby_apply_output_for_each_value) + + NOTE: see https://stackoverflow.com/questions/26187759/parallelize-apply-after-pandas-groupby + """ + groupby_val, group_df_list = zip(*[(group_val, group_df) + for group_val, group_df in df_grouped]) + with mp.Pool(mp.cpu_count()) as p: + output_list = p.map(fun, group_df_list) + return np.array(groupby_val), np.array(output_list) + + +def _subset_coo(coo: sp.coo_matrix, logic: np.ndarray) -> sp.coo_matrix: + return sp.coo_matrix((coo.data[logic], (coo.row[logic], coo.col[logic]))) + + +def timestamp() -> str: + return datetime.now().strftime('%Y-%m-%d %H:%M:%S') diff --git a/cellbender/remove_background/exceptions.py b/cellbender/remove_background/exceptions.py index 4136b7d..c8f321d 100644 --- a/cellbender/remove_background/exceptions.py +++ b/cellbender/remove_background/exceptions.py @@ -1,4 +1,4 @@ -# Exceptions defined by CellBender +"""Exceptions defined by CellBender""" class NanException(ArithmeticError): @@ -11,3 +11,14 @@ class NanException(ArithmeticError): def __init__(self, param: str): self.param = param self.message = 'A wild NaN appeared! In param {' + self.param + '}' + + +class ElboException(ValueError): + """Exception raised when training procedure is failing to meet expectations. + + Attributes: + message: Message to write to log + """ + + def __init__(self, message: str): + self.message = message diff --git a/cellbender/remove_background/infer.py b/cellbender/remove_background/infer.py deleted file mode 100644 index a6a2dac..0000000 --- a/cellbender/remove_background/infer.py +++ /dev/null @@ -1,781 +0,0 @@ -# Posterior inference. - -import logging -from abc import ABC, abstractmethod -from typing import Tuple, List, Dict, Optional - -import numpy as np -import pyro -import pyro.distributions as dist -import scipy.sparse as sp -import torch - -import cellbender.remove_background.consts as consts - - -class Posterior(ABC): - """Base class Posterior handles posterior count inference. - - Args: - dataset_obj: Dataset object. - vi_model: Trained RemoveBackgroundPyroModel. - counts_dtype: Data type of posterior count matrix. Can be one of - [np.uint32, np.float] - float_threshold: For floating point count matrices, counts below - this threshold will be set to zero, for the purposes of constructing - a sparse matrix. Unused if counts_dtype is np.uint32 - - Properties: - mean: Posterior count mean, as a sparse matrix. - - """ - - def __init__(self, - dataset_obj: 'SingleCellRNACountsDataset', # Dataset - vi_model: 'RemoveBackgroundPyroModel', - counts_dtype: np.dtype = np.uint32, - float_threshold: float = 0.5): - self.dataset_obj = dataset_obj - self.vi_model = vi_model - self.use_cuda = vi_model.use_cuda - self.analyzed_gene_inds = dataset_obj.analyzed_gene_inds - self.count_matrix_shape = dataset_obj.data['matrix'].shape - self.barcode_inds = np.arange(0, self.count_matrix_shape[0]) - self.dtype = counts_dtype - self.float_threshold = float_threshold - self._mean = None - self._latents = None - super(Posterior, self).__init__() - - @abstractmethod - def _get_mean(self): - """Obtain mean posterior counts and store in self._mean""" - pass - - @property - def mean(self) -> sp.csc_matrix: - if self._mean is None: - self._get_mean() - return self._mean - - @property - def latents(self) -> sp.csc_matrix: - if self._latents is None: - self._get_latents() - return self._latents - - @property - def variance(self): - raise NotImplemented("Posterior count variance not implemented.") - - @torch.no_grad() - def _get_latents(self): - """Calculate the encoded latent variables.""" - - data_loader = self.dataset_obj.get_dataloader(use_cuda=self.use_cuda, - analyzed_bcs_only=True, - batch_size=500, - shuffle=False) - - z = np.zeros((len(data_loader), self.vi_model.encoder['z'].output_dim)) - d = np.zeros(len(data_loader)) - p = np.zeros(len(data_loader)) - epsilon = np.zeros(len(data_loader)) - - for i, data in enumerate(data_loader): - - if 'chi_ambient' in pyro.get_param_store().keys(): - chi_ambient = pyro.param('chi_ambient').detach() - else: - chi_ambient = None - - enc = self.vi_model.encoder.forward(x=data, chi_ambient=chi_ambient) - ind = i * data_loader.batch_size - z[ind:(ind + data.shape[0]), :] = enc['z']['loc'].detach().cpu().numpy() - - phi_loc = pyro.param('phi_loc') - phi_scale = pyro.param('phi_scale') - - d[ind:(ind + data.shape[0])] = \ - dist.LogNormal(loc=enc['d_loc'], - scale=pyro.param('d_cell_scale')).mean.detach().cpu().numpy() - - p[ind:(ind + data.shape[0])] = enc['p_y'].sigmoid().detach().cpu().numpy() - - epsilon[ind:(ind + data.shape[0])] = dist.Gamma(enc['epsilon'] * self.vi_model.epsilon_prior, - self.vi_model.epsilon_prior).mean.detach().cpu().numpy() - - self._latents = {'z': z, 'd': d, 'p': p, - 'phi_loc_scale': [phi_loc.item(), phi_scale.item()], - 'epsilon': epsilon} - - @torch.no_grad() - def _param_map_estimates(self, - data: torch.Tensor, - chi_ambient: torch.Tensor) -> Dict[str, torch.Tensor]: - """Calculate MAP estimates of mu, the mean of the true count matrix, and - lambda, the rate parameter of the Poisson background counts. - - Args: - data: Dense tensor minibatch of cell by gene count data. - chi_ambient: Point estimate of inferred ambient gene expression. - - Returns: - mu_map: Dense tensor of Negative Binomial means for true counts. - lambda_map: Dense tensor of Poisson rate params for noise counts. - alpha_map: Dense tensor of Dirichlet concentration params that - inform the overdispersion of the Negative Binomial. - - """ - - # Encode latents. - enc = self.vi_model.encoder.forward(x=data, - chi_ambient=chi_ambient) - z_map = enc['z']['loc'] - - chi_map = self.vi_model.decoder.forward(z_map) - phi_loc = pyro.param('phi_loc') - phi_scale = pyro.param('phi_scale') - phi_conc = phi_loc.pow(2) / phi_scale.pow(2) - phi_rate = phi_loc / phi_scale.pow(2) - alpha_map = 1. / dist.Gamma(phi_conc, phi_rate).mean - - y = (enc['p_y'] > 0).float() - d_empty = dist.LogNormal(loc=pyro.param('d_empty_loc'), - scale=pyro.param('d_empty_scale')).mean - d_cell = dist.LogNormal(loc=enc['d_loc'], - scale=pyro.param('d_cell_scale')).mean - epsilon = dist.Gamma(enc['epsilon'] * self.vi_model.epsilon_prior, - self.vi_model.epsilon_prior).mean - - if self.vi_model.include_rho: - rho = pyro.param("rho_alpha") / (pyro.param("rho_alpha") - + pyro.param("rho_beta")) - else: - rho = None - - # Calculate MAP estimates of mu and lambda. - mu_map = self.vi_model.calculate_mu(epsilon=epsilon, - d_cell=d_cell, - chi=chi_map, - y=y, - rho=rho) - lambda_map = self.vi_model.calculate_lambda(epsilon=epsilon, - chi_ambient=chi_ambient, - d_empty=d_empty, - y=y, - d_cell=d_cell, - rho=rho, - chi_bar=self.vi_model.avg_gene_expression) - - return {'mu': mu_map, 'lam': lambda_map, 'alpha': alpha_map} - - def dense_to_sparse(self, - chunk_dense_counts: np.ndarray) -> Tuple[List, List, List]: - """Distill a batch of dense counts into sparse format. - Barcode numbering is relative to the tensor passed in. - """ - - # TODO: speed up by keeping it a torch tensor as long as possible - - if chunk_dense_counts.dtype != np.int32: - - if self.dtype == np.uint32: - - # Turn the floating point count estimates into integers. - decimal_values, _ = np.modf(chunk_dense_counts) # Stuff after decimal. - roundoff_counts = np.random.binomial(1, p=decimal_values) # Bernoulli. - chunk_dense_counts = np.floor(chunk_dense_counts).astype(dtype=int) - chunk_dense_counts += roundoff_counts - - elif self.dtype == np.float32: - - # Truncate counts at a threshold value. - chunk_dense_counts = (chunk_dense_counts * - (chunk_dense_counts > self.float_threshold)) - - else: - raise NotImplementedError(f"Count matrix dtype {self.dtype} is not " - f"supported. Choose from [np.uint32, " - f"np.float32]") - - # Find all the nonzero counts in this dense matrix chunk. - nonzero_barcode_inds_this_chunk, nonzero_genes_trimmed = \ - np.nonzero(chunk_dense_counts) - nonzero_counts = \ - chunk_dense_counts[nonzero_barcode_inds_this_chunk, - nonzero_genes_trimmed].flatten(order='C') - - # Get the original gene index from gene index in the trimmed dataset. - nonzero_genes = self.analyzed_gene_inds[nonzero_genes_trimmed] - - return nonzero_barcode_inds_this_chunk, nonzero_genes, nonzero_counts - - -class ImputedPosterior(Posterior): - """Posterior count inference using imputation to infer cell mean (d * chi). - - Args: - dataset_obj: Dataset object. - vi_model: Trained RemoveBackgroundPyroModel. - guide: Variational posterior pyro guide function, optional. Only - specify if the required guide function is not vi_model.guide. - encoder: Encoder that provides encodings of data. - counts_dtype: Data type of posterior count matrix. Can be one of - [np.uint32, np.float] - float_threshold: For floating point count matrices, counts below - this threshold will be set to zero, for the purposes of constructing - a sparse matrix. Unused if counts_dtype is np.uint32 - - Properties: - mean: Posterior count mean, as a sparse matrix. - encodings: Encoded latent variables, one per barcode in the dataset. - - """ - - def __init__(self, - dataset_obj: 'SingleCellRNACountsDataset', # Dataset - vi_model: 'RemoveBackgroundPyroModel', # Trained variational inference model - guide=None, - encoder=None, #: Union[CompositeEncoder, None] = None, - counts_dtype: np.dtype = np.uint32, - float_threshold: float = 0.5): - self.vi_model = vi_model - self.use_cuda = vi_model.use_cuda - self.guide = guide if guide is not None else vi_model.encoder - self.encoder = encoder if encoder is not None else vi_model.encoder - self._encodings = None - self._mean = None - super(ImputedPosterior, self).__init__(dataset_obj=dataset_obj, - vi_model=vi_model, - counts_dtype=counts_dtype, - float_threshold=float_threshold) - - @torch.no_grad() - def _get_mean(self): - """Send dataset through a guide that returns mean posterior counts. - - Keep track of only what is necessary to distill a sparse count matrix. - - """ - - data_loader = self.dataset_obj.get_dataloader(use_cuda=self.use_cuda, - analyzed_bcs_only=False, - batch_size=500, - shuffle=False) - - barcodes = [] - genes = [] - counts = [] - ind = 0 - - for data in data_loader: - - # Get return values from guide. - dense_counts_torch = self._param_map_estimates(data=data, - chi_ambient=pyro.param("chi_ambient")) - dense_counts = dense_counts_torch.detach().cpu().numpy() - bcs_i_chunk, genes_i, counts_i = self.dense_to_sparse(dense_counts) - - # Translate chunk barcode inds to overall inds. - bcs_i = self.barcode_inds[bcs_i_chunk + ind] - - # Add sparse matrix values to lists. - barcodes.append(bcs_i) - genes.append(genes_i) - counts.append(counts_i) - - # Increment barcode index counter. - ind += data.shape[0] # Same as data_loader.batch_size - - # Convert the lists to numpy arrays. - counts = np.array(np.concatenate(tuple(counts)), dtype=self.dtype) - barcodes = np.array(np.concatenate(tuple(barcodes)), dtype=np.uint32) - genes = np.array(np.concatenate(tuple(genes)), dtype=np.uint32) - - # Put the counts into a sparse csc_matrix. - self._mean = sp.csc_matrix((counts, (barcodes, genes)), - shape=self.count_matrix_shape) - - -class ProbPosterior(Posterior): - """Posterior count inference using a noise count probability distribution. - - Args: - dataset_obj: Dataset object. - vi_model: Trained model: RemoveBackgroundPyroModel - fpr: Desired false positive rate for construction of the final regularized - posterior on true counts. False positives are true counts that are - (incorrectly) removed from the dataset. - float_threshold: For floating point count matrices, counts below - this threshold will be set to zero, for the purposes of constructing - a sparse matrix. Unused if counts_dtype is np.uint32 - - Properties: - mean: Posterior count mean, as a sparse matrix. - encodings: Encoded latent variables, one per barcode in the dataset. - - """ - - def __init__(self, - dataset_obj: 'SingleCellRNACountsDataset', - vi_model: 'RemoveBackgroundPyroModel', - fpr: float = 0.01, - float_threshold: float = 0.5, - batch_size: int = consts.PROP_POSTERIOR_BATCH_SIZE, - cells_posterior_reg_calc: int = consts.CELLS_POSTERIOR_REG_CALC): - self.vi_model = vi_model - self.use_cuda = vi_model.use_cuda - self.fpr = fpr - self.lambda_multiplier = None - self._encodings = None - self._mean = None - self.batch_size = batch_size - self.cells_posterior_reg_calc = cells_posterior_reg_calc - self.random = np.random.RandomState(seed=1234) - super(ProbPosterior, self).__init__(dataset_obj=dataset_obj, - vi_model=vi_model, - counts_dtype=np.uint32, - float_threshold=float_threshold) - - @torch.no_grad() - def _get_mean(self): - """Send dataset through a guide that returns mean posterior counts. - - Keep track of only what is necessary to distill a sparse count matrix. - - """ - - # Get a dataset of ten cells. - cell_inds = np.where(self.latents['p'] > 0.9)[0] - lambda_mults = np.zeros(5) - - for i in range(lambda_mults.size): - - n_cells = min(self.cells_posterior_reg_calc, cell_inds.size) - if n_cells == 0: - raise ValueError('No cells found! Cannot compute expected FPR.') - cell_ind_subset = self.random.choice(cell_inds, size=n_cells, replace=False) - cell_data = (torch.tensor(np.array(self.dataset_obj.get_count_matrix() - [cell_ind_subset, :].todense()).squeeze()) - .float().to(self.vi_model.device)) - - # Get the latents mu, alpha, and lambda for those cells. - chi_ambient = pyro.param("chi_ambient") - map_est = self._param_map_estimates(data=cell_data, chi_ambient=chi_ambient) - - # Find the optimal lambda_multiplier value using those cells and target FPR. - lambda_mult = self._lambda_binary_search_given_fpr(cell_data=cell_data, - fpr=self.fpr, - mu_est=map_est['mu'], - lambda_est=map_est['lam'], - alpha_est=map_est['alpha']) - lambda_mults[i] = lambda_mult - - optimal_lambda_mult = np.mean(lambda_mults) - self.lambda_multiplier = optimal_lambda_mult - logging.info(f'Optimal posterior regularization factor = {optimal_lambda_mult:.2f}') - - # Compute posterior in mini-batches. - analyzed_bcs_only = True - data_loader = self.dataset_obj.get_dataloader(use_cuda=self.use_cuda, - analyzed_bcs_only=analyzed_bcs_only, - batch_size=self.batch_size, - shuffle=False) - barcodes = [] - genes = [] - counts = [] - ind = 0 - - for data in data_loader: - - # Compute an estimate of the true counts. - dense_counts = self._compute_true_counts(data=data, - chi_ambient=chi_ambient, - lambda_multiplier=optimal_lambda_mult, - use_map=False, - n_samples=9) # must be odd number - bcs_i_chunk, genes_i, counts_i = self.dense_to_sparse(dense_counts) - - # Translate chunk barcode inds to overall inds. - if analyzed_bcs_only: - bcs_i = self.dataset_obj.analyzed_barcode_inds[bcs_i_chunk + ind] - else: - bcs_i = self.barcode_inds[bcs_i_chunk + ind] - - # Add sparse matrix values to lists. - barcodes.append(bcs_i) - genes.append(genes_i) - counts.append(counts_i) - - # Increment barcode index counter. - ind += data.shape[0] # Same as data_loader.batch_size - - # Convert the lists to numpy arrays. - counts = np.array(np.concatenate(tuple(counts)), dtype=self.dtype) - barcodes = np.array(np.concatenate(tuple(barcodes)), dtype=np.uint32) - genes = np.array(np.concatenate(tuple(genes)), dtype=np.uint32) # uint16 is too small! - - # Put the counts into a sparse csc_matrix. - self._mean = sp.csc_matrix((counts, (barcodes, genes)), - shape=self.count_matrix_shape) - - @torch.no_grad() - def _compute_true_counts(self, - data: torch.Tensor, - chi_ambient: torch.Tensor, - lambda_multiplier: float, - use_map: bool = True, - n_samples: int = 1) -> np.ndarray: - """Compute the true de-noised count matrix for this minibatch. - - Can use either a MAP estimate of lambda and mu, or can use a sampling - approach. - - Args: - data: Dense tensor minibatch of cell by gene count data. - chi_ambient: Point estimate of inferred ambient gene expression. - lambda_multiplier: Value by which lambda gets multiplied in order - to compute regularized posterior true counts. - use_map: True to use a MAP estimate of lambda and mu. - n_samples: If not using a MAP estimate, this specifies the number - of samples to use in calculating the posterior mean. - - Returns: - dense_counts: Dense matrix of true de-noised counts. - - """ - - if use_map: - - # Calculate MAP estimates of mu and lambda. - est = self._param_map_estimates(data, chi_ambient) - mu_map = est['mu'] - lambda_map = est['lam'] - alpha_map = est['alpha'] - - # Compute the de-noised count matrix given the MAP estimates. - dense_counts_torch = self._true_counts_from_params(data, - mu_map, - lambda_map * lambda_multiplier + 1e-30, - alpha_map + 1e-30) - - dense_counts = dense_counts_torch.detach().cpu().numpy() - - else: - - assert n_samples > 0, f"Posterior mean estimate needs to be derived " \ - f"from at least one sample: here {n_samples} " \ - f"samples are called for." - - dense_counts_torch = torch.zeros((data.shape[0], - data.shape[1], - n_samples), - dtype=torch.float32).to(data.device) - - for i in range(n_samples): - - # Sample from mu and lambda. - mu_sample, lambda_sample, alpha_sample = \ - self._param_sample(data) - - # Compute the de-noised count matrix given the estimates. - dense_counts_torch[..., i] = \ - self._true_counts_from_params(data, - mu_sample, - lambda_sample * lambda_multiplier + 1e-30, - alpha_sample + 1e-30) - - # Take the median of the posterior true count distribution... torch cuda does not implement mode - dense_counts = dense_counts_torch.median(dim=2, keepdim=False)[0] - dense_counts = dense_counts.detach().cpu().numpy().astype(np.int32) - - return dense_counts - - @torch.no_grad() - def _param_sample(self, - data: torch.Tensor) -> Tuple[torch.Tensor, - torch.Tensor, - torch.Tensor]: - """Calculate a single sample estimate of mu, the mean of the true count - matrix, and lambda, the rate parameter of the Poisson background counts. - - Args: - data: Dense tensor minibatch of cell by gene count data. - - Returns: - mu_sample: Dense tensor sample of Negative Binomial mean for true - counts. - lambda_sample: Dense tensor sample of Poisson rate params for noise - counts. - alpha_sample: Dense tensor sample of Dirichlet concentration params - that inform the overdispersion of the Negative Binomial. - - """ - - # Use pyro poutine to trace the guide and sample parameter values. - guide_trace = pyro.poutine.trace(self.vi_model.guide).get_trace(x=data) - replayed_model = pyro.poutine.replay(self.vi_model.model, guide_trace) - - # Run the model using these sampled values. - replayed_model_output = replayed_model(x=data) - - # The model returns mu, alpha, and lambda. - mu_sample = replayed_model_output['mu'] - lambda_sample = replayed_model_output['lam'] - alpha_sample = replayed_model_output['alpha'] - - return mu_sample, lambda_sample, alpha_sample - - @torch.no_grad() - def _true_counts_from_params(self, - data: torch.Tensor, - mu_est: torch.Tensor, - lambda_est: torch.Tensor, - alpha_est: torch.Tensor) -> torch.Tensor: - """Calculate a single sample estimate of mu, the mean of the true count - matrix, and lambda, the rate parameter of the Poisson background counts. - - Args: - data: Dense tensor minibatch of cell by gene count data. - mu_est: Dense tensor of Negative Binomial means for true counts. - lambda_est: Dense tensor of Poisson rate params for noise counts. - alpha_est: Dense tensor of Dirichlet concentration params that - inform the overdispersion of the Negative Binomial. - - Returns: - dense_counts_torch: Dense matrix of true de-noised counts. - - """ - - # Estimate a reasonable low-end to begin the Poisson summation. - n = min(100., data.max().item()) # No need to exceed the max value - poisson_values_low = (lambda_est.detach() - n / 2).int() - - poisson_values_low = torch.clamp(torch.min(poisson_values_low, - (data - n + 1).int()), min=0).float() - - # Construct a big tensor of possible noise counts per cell per gene, - # shape (batch_cells, n_genes, max_noise_counts) - noise_count_tensor = torch.arange(start=0, end=n) \ - .expand([data.shape[0], data.shape[1], -1]) \ - .float().to(device=data.device) - noise_count_tensor = noise_count_tensor + poisson_values_low.unsqueeze(-1) - - # Compute probabilities of each number of noise counts. - # NOTE: some values will be outside the support (negative values for NB). - # The resulting NaNs are ignored by torch.argmax(). - logits = (mu_est.log() - alpha_est.log()).unsqueeze(-1) - log_prob_tensor = (dist.Poisson(lambda_est.unsqueeze(-1)) - .log_prob(noise_count_tensor) - + dist.NegativeBinomial(total_count=alpha_est.unsqueeze(-1), - logits=logits) - .log_prob(data.unsqueeze(-1) - noise_count_tensor)) - log_prob_tensor = torch.where(noise_count_tensor <= data.unsqueeze(-1), - log_prob_tensor, - torch.ones_like(log_prob_tensor) * -np.inf) - - # Find the most probable number of noise counts per cell per gene. - noise_count_map = torch.argmax(log_prob_tensor, - dim=-1, - keepdim=False).float() - - # Handle the cases where y = 0 (no cell): all counts are noise. - noise_count_map = torch.where(mu_est == 0, - data, - noise_count_map) - - # Compute the number of true counts. - dense_counts_torch = torch.clamp(data - noise_count_map, min=0.) - - return dense_counts_torch - - @torch.no_grad() - def _calculate_expected_fpr_from_map(self, - data: torch.Tensor, - data_map: torch.Tensor) -> torch.Tensor: - """Given inferred latent variables and observed total counts, - generate a MAP estimate for noise counts. Use that MAP estimate to - compute the expected false positive rate. - - Args: - data: Dense tensor tiny minibatch of cell by gene count data. - data_map: Dense tensor tiny minibatch of MAP output for that data. - - Returns: - fpr: Expected false positive rate. - - """ - - counts_per_cell = data.sum(dim=-1) - map_counts_per_cell = data_map.sum(dim=-1) - fraction_counts_removed_per_cell = (counts_per_cell - map_counts_per_cell) / counts_per_cell - empty_droplet_mean_counts = dist.LogNormal(loc=pyro.param('d_empty_loc'), - scale=pyro.param('d_empty_scale')).mean - ambient_fraction = empty_droplet_mean_counts / counts_per_cell - if self.vi_model.include_rho: - swapping_fraction = dist.Beta(pyro.param('rho_alpha'), pyro.param('rho_beta')).mean - else: - swapping_fraction = 0. - - mean_cell_epsilon = (self.latents['epsilon'][self.latents['p'] > 0.5]).mean() - target_fraction_counts_removed_per_cell = (ambient_fraction - + swapping_fraction) / mean_cell_epsilon - fpr_expectation = (fraction_counts_removed_per_cell - target_fraction_counts_removed_per_cell).mean() - - fpr_expectation = torch.clamp(fpr_expectation, min=0.) - - return fpr_expectation.mean() - - @torch.no_grad() - def _calculate_expected_fpr_given_lambda_mult(self, - data: torch.Tensor, - lambda_mult: float, - mu_est: torch.Tensor, - alpha_est: torch.Tensor, - lambda_est: torch.Tensor) -> torch.Tensor: - """Given a float lambda_mult, calculate a MAP estimate of output counts, - and use that estimate to calculate an expected false positive rate. - - Args: - data: Dense tensor tiny minibatch of cell by gene count data. - lambda_mult: Value of the lambda multiplier. - mu_est: Dense tensor of Negative Binomial means for true counts. - lambda_est: Dense tensor of Poisson rate params for noise counts. - alpha_est: Dense tensor of Dirichlet concentration params that - inform the overdispersion of the Negative Binomial. - - Returns: - fpr: Expected false positive rate. - - """ - - # Compute MAP estimate of true de-noised counts. - data_map = self._true_counts_from_params(data=data, - mu_est=mu_est, - lambda_est=lambda_est * lambda_mult, - alpha_est=alpha_est) - - # Compute expected false positive rate. - expected_fpr = self._calculate_expected_fpr_from_map(data=data, - data_map=data_map) - - return expected_fpr - - @torch.no_grad() - def _lambda_binary_search_given_fpr(self, - cell_data: torch.Tensor, - fpr: float, - mu_est: torch.Tensor, - lambda_est: torch.Tensor, - alpha_est: torch.Tensor, - lam_mult_init: float = 1., - fpr_tolerance: Optional[float] = None, - max_iterations: int = consts.POSTERIOR_REG_SEARCH_MAX_ITER) -> float: - """Perform a binary search for the appropriate lambda-multiplier which will - achieve a desired false positive rate. - - NOTE: It is assumed that - expected_fpr(lam_mult_bracket[0]) < fpr < expected_fpr(lam_mult_bracket[1]). - If this is not the case, the algorithm will produce an output close to one - of the brackets, and FPR control will not be achieved. - - Args: - cell_data: Data from a fraction of the total number of cells. - fpr: Desired false positive rate. - mu_est: Dense tensor of Negative Binomial means for true counts. - lambda_est: Dense tensor of Poisson rate params for noise counts. - alpha_est: Dense tensor of Dirichlet concentration params that - inform the overdispersion of the Negative Binomial. - lam_mult_init: Initial value of the lambda multiplier, hopefully - close to the unknown target value. - fpr_tolerance: Tolerated error in expected false positive rate. If - input is None, defaults to 0.001 or fpr / 10, whichever is - smaller. - max_iterations: A cutoff to ensure termination. Even if a tolerable - solution is not found, the algorithm will stop after this many - iterations and return the best answer so far. - - Returns: - lam_mult: Value of the lambda-multiplier. - - """ - - assert (fpr > 0) and (fpr < 1), "Target FPR should be in the interval (0, 1)." - if fpr_tolerance is None: - fpr_tolerance = min(fpr / 10., 0.001) - - # Begin at initial value. - lam_mult = lam_mult_init - - # Calculate an expected false positive rate for this lam_mult value. - expected_fpr = self._calculate_expected_fpr_given_lambda_mult( - data=cell_data, - lambda_mult=lam_mult, - mu_est=mu_est, - lambda_est=lambda_est, - alpha_est=alpha_est) - - # Find out what direction we need to move in. - residual = fpr - expected_fpr - initial_residual_sign = residual.sign() # plus or minus one - if initial_residual_sign.item() == 0: - # In the unlikely event that we hit the value exactly. - return lam_mult - - # Travel in one direction until the direction of FPR error changes. - lam_limit = lam_mult_init - i = 0 - while ((lam_limit < consts.POSTERIOR_REG_MAX) - and (lam_limit > consts.POSTERIOR_REG_MIN) - and (residual.sign() == initial_residual_sign) - and (i < max_iterations)): - - lam_limit = lam_limit * (initial_residual_sign * 2).exp().item() - - # Calculate an expected false positive rate for this lam_mult value. - expected_fpr = self._calculate_expected_fpr_given_lambda_mult( - data=cell_data, - lambda_mult=lam_limit, - mu_est=mu_est, - lambda_est=lambda_est, - alpha_est=alpha_est) - - residual = fpr - expected_fpr - i = i + 1 # one dataset had this go into an infinite loop, taking lam_limit -> 0 - - # Define the values that bracket the correct answer. - lam_mult_bracket = np.sort(np.array([lam_mult_init, lam_limit])) - - # Binary search algorithm. - for i in range(max_iterations): - - # Current test value for the lambda-multiplier. - lam_mult = np.mean(lam_mult_bracket) - - # Calculate an expected false positive rate for this lam_mult value. - expected_fpr = self._calculate_expected_fpr_given_lambda_mult( - data=cell_data, - lambda_mult=lam_mult, - mu_est=mu_est, - lambda_est=lambda_est, - alpha_est=alpha_est) - - # Check on false positive rate and update our bracket values. - if (expected_fpr < (fpr + fpr_tolerance)) and (expected_fpr > (fpr - fpr_tolerance)): - break - elif expected_fpr > fpr: - lam_mult_bracket[1] = lam_mult - elif expected_fpr < fpr: - lam_mult_bracket[0] = lam_mult - - # If we stopped due to iteration limit, take the average lam_mult value. - if i == max_iterations: - lam_mult = np.mean(lam_mult_bracket) - - # Check to see if we have achieved the desired FPR control. - if not ((expected_fpr < (fpr + fpr_tolerance)) and (expected_fpr > (fpr - fpr_tolerance))): - print(f'FPR control not achieved in {max_iterations} attempts. ' - f'Output FPR is esitmated to be {expected_fpr.item():.4f}') - - return lam_mult diff --git a/cellbender/remove_background/model.py b/cellbender/remove_background/model.py index 30f5d44..205e886 100644 --- a/cellbender/remove_background/model.py +++ b/cellbender/remove_background/model.py @@ -20,10 +20,66 @@ from cellbender.remove_background.distributions.NullDist import NullDist from cellbender.remove_background.exceptions import NanException -from typing import Optional, Union +from typing import Optional, Union, Dict, Callable from numbers import Number +def calculate_lambda(model_type: str, + epsilon: torch.Tensor, + chi_ambient: torch.Tensor, + d_empty: torch.Tensor, + y: Union[torch.Tensor, None] = None, + d_cell: Union[torch.Tensor, None] = None, + rho: Union[torch.Tensor, None] = None, + chi_bar: Union[torch.Tensor, None] = None): + """Calculate noise rate based on the model.""" + + if model_type == "simple" or model_type == "ambient": + lam = epsilon.unsqueeze(-1) * d_empty.unsqueeze(-1) * chi_ambient + + elif model_type == "swapping": + lam = (rho.unsqueeze(-1) * chi_bar * epsilon.unsqueeze(-1) * + (y.unsqueeze(-1) * d_cell.unsqueeze(-1) + d_empty.unsqueeze(-1))) + + elif model_type == "full": + lam = (epsilon.unsqueeze(-1) + * ((1. - rho.unsqueeze(-1)) * chi_ambient * d_empty.unsqueeze(-1) + + rho.unsqueeze(-1) * chi_bar * (y.unsqueeze(-1) * d_cell.unsqueeze(-1) + + d_empty.unsqueeze(-1)))) + else: + raise NotImplementedError(f"model_type was set to {model_type}, " + f"which is not implemented.") + + return lam + + +def calculate_mu(model_type: str, + epsilon: torch.Tensor, + d_cell: torch.Tensor, + chi: torch.Tensor, + y: Union[torch.Tensor, None] = None, + rho: Union[torch.Tensor, None] = None): + """Calculate mean expression based on the model.""" + + if model_type == 'simple': + mu = epsilon.unsqueeze(-1) * d_cell.unsqueeze(-1) * chi + + elif model_type == 'ambient': + mu = (y.unsqueeze(-1) * epsilon.unsqueeze(-1) + * d_cell.unsqueeze(-1) * chi) + + elif model_type == 'swapping' or model_type == 'full': + mu = ((1. - rho.unsqueeze(-1)) + * y.unsqueeze(-1) * epsilon.unsqueeze(-1) + * d_cell.unsqueeze(-1) * chi) + + else: + raise NotImplementedError(f"model_type was set to {model_type}, " + f"which is not implemented.") + + return mu + + class RemoveBackgroundPyroModel(nn.Module): """Class that contains the model and guide used for variational inference. @@ -32,8 +88,10 @@ class RemoveBackgroundPyroModel(nn.Module): 'swapping', 'full']. encoder: An instance of an encoder object. Can be a CompositeEncoder. decoder: An instance of a decoder object. - dataset_obj: Dataset object which contains relevant priors. + dataset_obj_priors: Dict which contains relevant priors. use_cuda: Will use GPU if True. + analyzed_gene_names: Here only so that when we save a checkpoint, if we + ever want to look at it, we will know which genes are which. phi_loc_prior: Mean of gamma distribution for global overdispersion. phi_scale_prior: Scale of gamma distribution for global overdispersion. rho_alpha_prior: Param of beta distribution for swapping fraction. @@ -52,12 +110,18 @@ def __init__(self, model_type: str, encoder: Union[nn.Module, encoder_module.CompositeEncoder], decoder: nn.Module, - dataset_obj: 'SingleCellRNACountsDataset', + dataset_obj_priors: Dict[str, float], + n_analyzed_genes: int, + n_droplets: int, + analyzed_gene_names: np.ndarray, + empty_UMI_threshold: int, + log_counts_crossover: float, use_cuda: bool, phi_loc_prior: float = consts.PHI_LOC_PRIOR, phi_scale_prior: float = consts.PHI_SCALE_PRIOR, rho_alpha_prior: float = consts.RHO_ALPHA_PRIOR, rho_beta_prior: float = consts.RHO_BETA_PRIOR, + epsilon_prior: float = consts.EPSILON_PRIOR, use_exact_log_prob: bool = consts.USE_EXACT_LOG_PROB): super(RemoveBackgroundPyroModel, self).__init__() @@ -69,13 +133,19 @@ def __init__(self, if (self.model_type == "full") or (self.model_type == "swapping"): self.include_rho = True - self.n_genes = dataset_obj.analyzed_gene_inds.size + self.n_genes = n_analyzed_genes + self.n_droplets = n_droplets + self.analyzed_gene_names = analyzed_gene_names self.z_dim = decoder.input_dim self.encoder = encoder self.decoder = decoder self.use_exact_log_prob = use_exact_log_prob self.loss = {'train': {'epoch': [], 'elbo': []}, - 'test': {'epoch': [], 'elbo': []}} + 'test': {'epoch': [], 'elbo': []}, + 'learning_rate': {'epoch': [], 'value': []}} + self.empty_UMI_threshold = empty_UMI_threshold + self.log_counts_crossover = log_counts_crossover + self.counts_crossover = np.exp(log_counts_crossover) # Determine whether we are working on a GPU. if use_cuda: @@ -93,21 +163,20 @@ def __init__(self, self.use_cuda = use_cuda # Priors - assert dataset_obj.priors['d_std'] > 0, \ - f"Issue with prior: d_std is {dataset_obj.priors['d_std']}, " \ + assert dataset_obj_priors['d_std'] > 0, \ + f"Issue with prior: d_std is {dataset_obj_priors['d_std']}, " \ f"but should be > 0." - assert dataset_obj.priors['cell_counts'] > 0, \ + assert dataset_obj_priors['cell_counts'] > 0, \ f"Issue with prior: cell_counts is " \ - f"{dataset_obj.priors['cell_counts']}, but should be > 0." + f"{dataset_obj_priors['cell_counts']}, but should be > 0." - self.d_cell_loc_prior = torch.tensor(np.log1p(dataset_obj.priors['cell_counts']))\ + self.d_cell_loc_prior = torch.tensor(np.log1p(dataset_obj_priors['cell_counts']))\ .float().to(self.device) - self.d_cell_scale_prior = (torch.tensor(consts.D_STD_PRIOR).to(self.device)) + self.d_cell_scale_prior = torch.tensor(dataset_obj_priors['d_std']).to(self.device) self.z_loc_prior = torch.zeros(torch.Size([self.z_dim])).to(self.device) - self.z_scale_prior = torch.ones(torch.Size([self.z_dim]))\ - .to(self.device) - self.epsilon_prior = torch.tensor(consts.EPSILON_PRIOR).to(self.device) + self.z_scale_prior = torch.ones(torch.Size([self.z_dim])).to(self.device) + self.epsilon_prior = torch.tensor(epsilon_prior).to(self.device) self.phi_loc_prior = (phi_loc_prior * torch.ones(torch.Size([])).to(self.device)) @@ -120,100 +189,46 @@ def __init__(self, if self.model_type != "simple": - assert dataset_obj.priors['empty_counts'] > 0, \ + assert dataset_obj_priors['empty_counts'] > 0, \ f"Issue with prior: empty_counts should be > 0, but is " \ - f"{dataset_obj.priors['empty_counts']}" - chi_ambient_sum = dataset_obj.priors['chi_ambient'].sum() + f"{dataset_obj_priors['empty_counts']}" + chi_ambient_sum = dataset_obj_priors['chi_ambient'].sum() assert np.isclose(a=chi_ambient_sum, b=[1.], atol=1e-5), \ f"Issue with prior: chi_ambient should sum to 1, but it sums " \ f"to {chi_ambient_sum}" - chi_bar_sum = dataset_obj.priors['chi_bar'].sum() + chi_bar_sum = dataset_obj_priors['chi_bar'].sum() assert np.isclose(a=chi_bar_sum, b=[1.], atol=1e-5), \ f"Issue with prior: chi_bar should sum to 1, but is {chi_bar_sum}" - self.d_empty_loc_prior = (np.log1p(dataset_obj - .priors['empty_counts'], + self.d_empty_loc_prior = (np.log1p(dataset_obj_priors['empty_counts'], dtype=np.float32).item() * torch.ones(torch.Size([])) .to(self.device)) - self.d_empty_scale_prior = (np.array(dataset_obj.priors['d_std'], - dtype=np.float32).item() + self.d_empty_scale_prior = (dataset_obj_priors['d_empty_std'] * torch.ones(torch.Size([])).to(self.device)) - self.p_logit_prior = (dataset_obj.priors['cell_logit'] + self.p_logit_prior = (dataset_obj_priors['cell_logit'] * torch.ones(torch.Size([])).to(self.device)) - self.chi_ambient_init = dataset_obj.priors['chi_ambient']\ - .to(self.device) - self.avg_gene_expression = dataset_obj.priors['chi_bar'] \ - .to(self.device) + self.chi_ambient_init = dataset_obj_priors['chi_ambient'].to(self.device) + self.avg_gene_expression = dataset_obj_priors['chi_bar'].to(self.device) - self.empty_UMI_threshold = (torch.tensor(dataset_obj.empty_UMI_threshold) + self.empty_UMI_threshold = (torch.tensor(empty_UMI_threshold) .float().to(self.device)) else: self.avg_gene_expression = None - self.rho_alpha_prior = (rho_alpha_prior - * torch.ones(torch.Size([])).to(self.device)) - self.rho_beta_prior = (rho_beta_prior - * torch.ones(torch.Size([])).to(self.device)) + self.rho_alpha_prior = rho_alpha_prior * torch.ones(torch.Size([])).to(self.device) + self.rho_beta_prior = rho_beta_prior * torch.ones(torch.Size([])).to(self.device) - def calculate_lambda(self, - epsilon: torch.Tensor, - chi_ambient: torch.Tensor, - d_empty: torch.Tensor, - y: Optional[torch.Tensor] = None, - d_cell: Optional[torch.Tensor] = None, - rho: Optional[torch.Tensor] = None, - chi_bar: Optional[torch.Tensor] = None): - """Calculate noise rate based on the model.""" - - if self.model_type == "simple" or self.model_type == "ambient": - lam = epsilon.unsqueeze(-1) * d_empty.unsqueeze(-1) * chi_ambient - - elif self.model_type == "swapping": - lam = (rho.unsqueeze(-1) * chi_bar * - (y.unsqueeze(-1) * d_cell.unsqueeze(-1) + d_empty.unsqueeze(-1))) - - elif self.model_type == "full": - lam = (epsilon.unsqueeze(-1) - * ((1. - rho.unsqueeze(-1)) * chi_ambient * d_empty.unsqueeze(-1) - + rho.unsqueeze(-1) * chi_bar * (y.unsqueeze(-1) * d_cell.unsqueeze(-1) - + d_empty.unsqueeze(-1)))) - else: - raise ValueError(f"model_type was set to {self.model_type}, " - f"which is not implemented.") + def _calculate_mu(self, **kwargs): + return calculate_mu(model_type=self.model_type, **kwargs) - return lam - - def calculate_mu(self, - epsilon: torch.Tensor, - d_cell: torch.Tensor, - chi: torch.Tensor, - y: Optional[torch.Tensor] = None, - rho: Optional[torch.Tensor] = None): - """Calculate mean expression based on the model.""" - - if self.model_type == 'simple': - mu = epsilon.unsqueeze(-1) * d_cell.unsqueeze(-1) * chi - - elif self.model_type == 'ambient': - mu = (y.unsqueeze(-1) * epsilon.unsqueeze(-1) - * d_cell.unsqueeze(-1) * chi) - - elif self.model_type == 'swapping' or self.model_type == 'full': - mu = ((1. - rho.unsqueeze(-1)) - * y.unsqueeze(-1) * epsilon.unsqueeze(-1) - * d_cell.unsqueeze(-1) * chi) - - else: - raise NotImplementedError(f"model_type was set to {self.model_type}, " - f"which is not implemented.") - - return mu + def _calculate_lambda(self, **kwargs): + return calculate_lambda(model_type=self.model_type, **kwargs) def model(self, x: torch.Tensor): """Data likelihood model. @@ -224,7 +239,7 @@ def model(self, x: torch.Tensor): """ # Register the decoder with pyro. - pyro.module("decoder", self.decoder) + pyro.module("decoder", self.decoder, update_module_params=True) # Register the hyperparameter for ambient gene expression. if self.include_empties: @@ -235,13 +250,16 @@ def model(self, x: torch.Tensor): else: chi_ambient = None - # Sample phi from Gamma prior. - phi = pyro.sample("phi", - dist.Gamma(self.phi_conc_prior, - self.phi_rate_prior)) + POISSON_APPROX = False + + if not POISSON_APPROX: + # Sample phi from Gamma prior. + phi = pyro.sample("phi", + dist.Gamma(self.phi_conc_prior, + self.phi_rate_prior)) # Happens in parallel for each data point (cell barcode) independently: - with pyro.plate("data", x.size(0), + with pyro.plate("data", x.shape[0], use_cuda=self.use_cuda, device=self.device): # Sample z from prior. @@ -251,7 +269,7 @@ def model(self, x: torch.Tensor): .expand_by([x.size(0)]).to_event(1)) # Decode the latent code z to get fractional gene expression, chi. - chi = self.decoder.forward(z) + chi = pyro.deterministic("chi", self.decoder(z)) # Sample d_cell based on priors. d_cell = pyro.sample("d_cell", @@ -283,53 +301,62 @@ def model(self, x: torch.Tensor): .expand_by([x.size(0)])) # Sample y, the presence of a real cell, based on p_logit_prior. - y = pyro.sample("y", - dist.Bernoulli(logits=self.p_logit_prior - 100.) # TODO - .expand_by([x.size(0)])) + p_logit_prior = get_p_logit_prior( + log_counts=x.sum(dim=-1).log(), + log_cell_prior_counts=self.d_cell_loc_prior, + surely_empty_counts=self.empty_UMI_threshold, + naive_p_logit_prior=self.p_logit_prior, + ) + y = pyro.sample("y", dist.Bernoulli(logits=p_logit_prior)) else: d_empty = None y = None # Calculate the mean gene expression counts (for each barcode). - mu_cell = self.calculate_mu(epsilon=epsilon, - d_cell=d_cell, - chi=chi, - y=y, - rho=rho) + mu_cell = self._calculate_mu(epsilon=epsilon, + d_cell=d_cell, + chi=chi, + y=y, + rho=rho) if self.include_empties: # Calculate the background rate parameter (for each barcode). - lam = self.calculate_lambda(epsilon=epsilon, - chi_ambient=chi_ambient, - d_empty=d_empty, - y=y, - d_cell=d_cell, - rho=rho, - chi_bar=self.avg_gene_expression) + lam = self._calculate_lambda(epsilon=epsilon, + chi_ambient=chi_ambient, + d_empty=d_empty, + y=y, + d_cell=d_cell, + rho=rho, + chi_bar=self.avg_gene_expression) else: lam = torch.zeros([self.n_genes]).to(self.device) - alpha = phi.reciprocal() - - if self.use_exact_log_prob: - - # Sample gene expression from our Negative Binomial Poisson - # Convolution distribution, and compare with observed data. - c = pyro.sample("obs", NBPC(mu=mu_cell + consts.NBPC_MU_EPS_SAFEGAURD, - alpha=alpha + consts.NBPC_ALPHA_EPS_SAFEGAURD, - lam=lam + consts.NBPC_LAM_EPS_SAFEGAURD, - max_poisson=consts.NBPC_EXACT_N_TERMS).to_event(1), + if POISSON_APPROX: + # Data distributed as the sum of two Poissons. + c = pyro.sample("obs", + dist.Poisson(rate=mu_cell + lam + consts.POISSON_EPS_SAFEGAURD).to_event(1), obs=x.reshape(-1, self.n_genes)) + alpha = None else: + alpha = phi.reciprocal() - # Use a negative binomial approximation as the observation model. - c = pyro.sample("obs", NBPCapprox(mu=mu_cell + consts.NBPC_MU_EPS_SAFEGAURD, - alpha=alpha + consts.NBPC_ALPHA_EPS_SAFEGAURD, - lam=lam + consts.NBPC_LAM_EPS_SAFEGAURD).to_event(1), - obs=x.reshape(-1, self.n_genes)) + if not consts.USE_EXACT_LOG_PROB: + + # Use a negative binomial approximation as the observation model. + c = pyro.sample("obs", NBPCapprox(mu=mu_cell + consts.NBPC_MU_EPS_SAFEGAURD, + alpha=alpha + consts.NBPC_ALPHA_EPS_SAFEGAURD, + lam=lam + consts.NBPC_LAM_EPS_SAFEGAURD).to_event(1), + obs=x.reshape(-1, self.n_genes)) + + else: + c = pyro.sample("obs", NBPC(mu=mu_cell + consts.NBPC_MU_EPS_SAFEGAURD, + alpha=alpha + consts.NBPC_ALPHA_EPS_SAFEGAURD, + lam=lam + consts.NBPC_LAM_EPS_SAFEGAURD, + max_poisson=100).to_event(1), + obs=x.reshape(-1, self.n_genes)) if self.include_empties: @@ -341,10 +368,9 @@ def model(self, x: torch.Tensor): # Additionally use the surely empty droplets for regularization, # since we know these droplets by their UMI counts. counts = x.sum(dim=-1, keepdim=False) - surely_empty_mask = ((counts < self.empty_UMI_threshold) - .bool().to(self.device)) + surely_cell_mask = (counts >= self.d_cell_loc_prior.exp()).bool().to(self.device) - with poutine.mask(mask=surely_empty_mask): + with poutine.mask(mask=y.detach().bool().logical_not()): # surely_empty_mask): with poutine.scale(scale=consts.REG_SCALE_AMBIENT_EXPRESSION): @@ -353,45 +379,57 @@ def model(self, x: torch.Tensor): else: r = None - # Semi-supervision of ambient expression. - lam = self.calculate_lambda(epsilon=epsilon.detach(), - chi_ambient=chi_ambient, - d_empty=d_empty, - y=torch.zeros_like(d_empty), - d_cell=d_cell.detach(), - rho=r, - chi_bar=self.avg_gene_expression) + # Semi-supervision of ambient expression using all empties. + lam = self._calculate_lambda(epsilon=torch.tensor(1.).to(d_empty.device), # epsilon.detach(), + chi_ambient=chi_ambient, + d_empty=d_empty, + y=torch.zeros_like(d_empty), + d_cell=d_cell.detach(), + rho=r, + chi_bar=self.avg_gene_expression) pyro.sample("obs_empty", - dist.Poisson(rate=lam + 1e-10).to_event(1), + dist.Poisson(rate=lam + consts.POISSON_EPS_SAFEGAURD).to_event(1), obs=x.reshape(-1, self.n_genes)) - # Semi-supervision of cell probabilities. - with poutine.scale(scale=consts.REG_SCALE_EMPTY_PROB): - - p_logit_posterior = pyro.sample("p_passback", - NullDist(torch.zeros(1) - .to(self.device)) - .expand_by([x.size(0)])) - - pyro.sample("obs_empty_y", - dist.Normal(loc=p_logit_posterior, - scale=1.), - obs=torch.ones_like(y) * -5.) - - # Additionally use some high-count droplets for cell prob regularization. - surely_cell_mask = (torch.where(counts >= self.d_cell_loc_prior.exp(), - torch.ones_like(counts), - torch.zeros_like(counts)) - .bool().to(self.device)) - - with poutine.mask(mask=surely_cell_mask): - with poutine.scale(scale=consts.REG_SCALE_CELL_PROB): - pyro.sample("obs_cell_y", - dist.Normal(loc=p_logit_posterior, - scale=1.), - obs=torch.ones_like(y) * 5.) - - return {'mu': mu_cell, 'lam': lam, 'alpha': alpha, 'counts': c} + # Grab our posterior for the logit cell probability (this is a workaround). + p_logit_posterior = pyro.sample("p_passback", + NullDist(torch.zeros(1).to(self.device)) + .expand_by([x.size(0)])) + + # Softer semi-supervision to encourage cell probabilities to do the right thing. + probably_empty_mask = (counts < self.counts_crossover).bool().to(self.device) + probably_cell_mask = (counts >= self.counts_crossover).bool().to(self.device) + + with poutine.mask(mask=probably_empty_mask): + with poutine.scale(scale=consts.REG_SCALE_SOFT_SUPERVISION): + pyro.sample("obs_probably_empty_y", + dist.Normal(loc=-1 * torch.ones_like(y) * consts.REG_LOGIT_MEAN, + scale=consts.REG_LOGIT_SOFT_SCALE), + obs=p_logit_posterior) + + with poutine.mask(mask=probably_cell_mask): + with poutine.scale(scale=consts.REG_SCALE_SOFT_SUPERVISION): + pyro.sample("obs_probably_cell_y", + dist.Normal(loc=torch.ones_like(y) * consts.REG_LOGIT_MEAN, + scale=consts.REG_LOGIT_SOFT_SCALE), + obs=p_logit_posterior) + + # Regularization of epsilon.mean() + if surely_cell_mask.sum() >= 2: + epsilon_median = epsilon[probably_cell_mask].median() + # with poutine.scale(scale=probably_cell_mask.sum() / 10.): + pyro.sample("epsilon_mean", + dist.Normal(loc=epsilon_median, scale=0.01), + obs=torch.ones_like(epsilon_median)) + + epsilon_median_empty = epsilon[probably_empty_mask].median() + # with poutine.scale(scale=probably_cell_mask.sum() / 10.): + pyro.sample("epsilon_empty_mean", + dist.Normal(loc=epsilon_median_empty, scale=0.01), + obs=torch.ones_like(epsilon_median_empty)) + + return {'chi_ambient': chi_ambient, 'z': z, + 'mu': mu_cell, 'lam': lam, 'alpha': alpha, 'counts': c} @config_enumerate(default='parallel') def guide(self, x: torch.Tensor): @@ -411,7 +449,7 @@ def guide(self, x: torch.Tensor): # Register the encoder(s) with pyro. for name, module in self.encoder.items(): - pyro.module("encoder_" + name, module) + pyro.module("encoder_" + name, module, update_module_params=True) # Initialize variational parameters for d_cell. d_cell_scale = pyro.param("d_cell_scale", @@ -466,7 +504,7 @@ def guide(self, x: torch.Tensor): pyro.sample("phi", dist.Gamma(phi_conc, phi_rate)) # Happens in parallel for each data point (cell barcode) independently: - with pyro.plate("data", x.size(0), + with pyro.plate("data", x.shape[0], use_cuda=self.use_cuda, device=self.device): # Sample swapping fraction rho. @@ -483,10 +521,14 @@ def guide(self, x: torch.Tensor): scale=d_empty_scale) .expand_by([x.size(0)])) - enc = self.encoder.forward(x=x, chi_ambient=chi_ambient) + enc = self.encoder(x=x, + chi_ambient=chi_ambient.detach(), + cell_prior_log=self.d_cell_loc_prior) else: - enc = self.encoder.forward(x=x, chi_ambient=None) + enc = self.encoder(x=x, + chi_ambient=None, + cell_prior_log=self.d_cell_loc_prior) # Code specific to models with empty droplets. if self.include_empties: @@ -500,63 +542,52 @@ def guide(self, x: torch.Tensor): # Sample the Bernoulli y from encoded p(y). y = pyro.sample("y", dist.Bernoulli(logits=enc['p_y'])) - # Gate d_cell_loc so empty droplets do not give big gradients. - prob = enc['p_y'].sigmoid() # Logits to probability - d_cell_loc_gated = (prob * enc['d_loc'] + (1 - prob) - * self.d_cell_loc_prior) # NOTE: necessary to pass on sim6 - - # Sample d based on the encoding. - d_cell = pyro.sample("d_cell", dist.LogNormal(loc=d_cell_loc_gated, - scale=d_cell_scale)) + # Get cell probabilities for gating. + prob = enc['p_y'].sigmoid().detach() # Logits to probability # Mask out empty droplets. - with poutine.mask(mask=y.bool()): + with poutine.mask(mask=y.bool().detach()): # Sample latent code z for the barcodes containing cells. - pyro.sample("z", - dist.Normal(loc=enc['z']['loc'], - scale=enc['z']['scale']) - .to_event(1)) + z = pyro.sample("z", dist.Normal(loc=enc['z']['loc'], scale=enc['z']['scale']) + .to_event(1)) - # Gate epsilon and sample. - epsilon_gated = (prob * enc['epsilon'] + (1 - prob) * 1.) + # Gate d based and sample. + d_cell_loc_gated = (prob * enc['d_loc'] + (1 - prob) + * self.d_cell_loc_prior) # NOTE: necessary to pass on sim6 + d_cell = pyro.sample("d_cell", dist.LogNormal(loc=d_cell_loc_gated, + scale=d_cell_scale)) - epsilon = pyro.sample("epsilon", - dist.Gamma(epsilon_gated * self.epsilon_prior, - self.epsilon_prior)) + # Gate epsilon and sample. + epsilon_gated = (prob * enc['epsilon'] + (1 - prob) * 1.) + epsilon = pyro.sample("epsilon", + dist.Gamma(concentration=epsilon_gated * self.epsilon_prior, + rate=self.epsilon_prior)) else: # Sample d based on the encoding. - pyro.sample("d_cell", dist.LogNormal(loc=enc['d_loc'], - scale=d_cell_scale)) + pyro.sample("d_cell", dist.LogNormal(loc=enc['d_loc'], scale=d_cell_scale)) # Sample latent code z for each cell. - pyro.sample("z", - dist.Normal(loc=enc['z']['loc'], - scale=enc['z']['scale']) + pyro.sample("z", dist.Normal(loc=enc['z']['loc'], scale=enc['z']['scale']) .to_event(1)) -def get_ambient_expression() -> Optional[np.ndarray]: - """Get ambient RNA expression for 'empty' droplets. - - Return: - chi_ambient: The ambient gene expression profile, as a normalized - vector that sums to one. - - Note: - Inference must have been performed on a model with a 'chi_ambient' - hyperparameter prior to making this call. - - """ - - chi_ambient = None - - if 'chi_ambient' in pyro.get_param_store().keys(): - chi_ambient = to_ndarray(pyro.param('chi_ambient')).squeeze() - - return chi_ambient +def get_p_logit_prior(log_counts: torch.Tensor, + log_cell_prior_counts: float, + surely_empty_counts: torch.Tensor, + naive_p_logit_prior: float) -> torch.Tensor: + """Compute the logit cell probability prior per droplet based on counts""" + ones = torch.ones_like(log_counts) + p_logit_prior = ones * naive_p_logit_prior + p_logit_prior = torch.where(log_counts <= (surely_empty_counts.log() + log_cell_prior_counts) / 2, + ones * -100., + p_logit_prior) + p_logit_prior = torch.where(log_counts >= log_cell_prior_counts, + ones * consts.REG_LOGIT_MEAN, + p_logit_prior) + return p_logit_prior def get_rho() -> Optional[np.ndarray]: @@ -582,6 +613,15 @@ def get_rho() -> Optional[np.ndarray]: return rho +def get_param_store_key(key: str) -> Union[np.ndarray, None]: + val = None + + if key in pyro.get_param_store().keys(): + val = to_ndarray(pyro.param(key)).squeeze() + + return val + + def to_ndarray(x: Union[Number, np.ndarray, torch.Tensor]) -> np.ndarray: """Convert a numeric value or array to a numpy array on cpu.""" diff --git a/cellbender/remove_background/posterior.py b/cellbender/remove_background/posterior.py new file mode 100644 index 0000000..3758702 --- /dev/null +++ b/cellbender/remove_background/posterior.py @@ -0,0 +1,1708 @@ +"""Posterior generation and regularization.""" + +import pyro +import pyro.distributions as dist +import torch +import numpy as np +import scipy.sparse as sp +import pandas as pd + +import cellbender.remove_background.consts as consts +from cellbender.remove_background.model import calculate_mu, calculate_lambda +from cellbender.monitor import get_hardware_usage +from cellbender.remove_background.data.dataprep import DataLoader +from cellbender.remove_background.data.dataset import get_dataset_obj +from cellbender.remove_background.estimation import EstimationMethod, \ + MultipleChoiceKnapsack, Mean, MAP, apply_function_dense_chunks +from cellbender.remove_background.sparse_utils import dense_to_sparse_op_torch, \ + log_prob_sparse_to_dense, csr_set_rows_to_zero +from cellbender.remove_background.checkpoint import load_from_checkpoint, \ + unpack_tarball, make_tarball, load_checkpoint +from cellbender.remove_background.data.io import \ + write_posterior_coo_to_h5, load_posterior_from_h5 + +from typing import Tuple, List, Dict, Optional, Union, Callable +from abc import ABC, abstractmethod +import logging +import argparse +import tempfile +import shutil +import time +import os + + +logger = logging.getLogger('cellbender') + + +def load_or_compute_posterior_and_save(dataset_obj: 'SingleCellRNACountsDataset', + inferred_model: 'RemoveBackgroundPyroModel', + args: argparse.Namespace) -> 'Posterior': + """After inference, compute the full posterior noise count log probability + distribution. Save it and make it part of the checkpoint file. + + NOTE: Loads posterior from checkpoint file if available. + NOTE: Saves posterior as args.output_file + '_posterior.npz' and adds this + file to the checkpoint tarball as well. + + Args: + dataset_obj: Input data in the form of a SingleCellRNACountsDataset + object. + inferred_model: Model after inference is complete. + args: Input command line parsed arguments. + + Returns: + posterior: Posterior object with noise count log prob computed, as well + as regularization if called for. + + """ + + assert os.path.exists(args.input_checkpoint_tarball), \ + f'Checkpoint file {args.input_checkpoint_tarball} does not exist, ' \ + f'presumably because saving of the checkpoint file has been manually ' \ + f'interrupted. load_or_compute_posterior_and_save() will not work ' \ + f'properly without an existing checkpoint file. Please re-run and ' \ + f'allow a checkpoint file to be saved.' + + def _do_posterior_regularization(posterior: Posterior): + + # Optional posterior regularization. + if args.posterior_regularization is not None: + if args.posterior_regularization == 'PRq': + posterior.regularize_posterior( + regularization=PRq, + alpha=args.prq_alpha, + device='cuda', + ) + elif args.posterior_regularization == 'PRmu': + posterior.regularize_posterior( + regularization=PRmu, + raw_count_matrix=dataset_obj.data['matrix'], + fpr=args.fpr[0], + per_gene=False, + device='cuda', + ) + elif args.posterior_regularization == 'PRmu_gene': + posterior.regularize_posterior( + regularization=PRmu, + raw_count_matrix=dataset_obj.data['matrix'], + fpr=args.fpr[0], + per_gene=True, + device='cuda', + ) + else: + raise ValueError(f'Got a posterior regularization input of ' + f'"{args.posterior_regularization}", which is not ' + f'allowed. Use ["PRq", "PRmu", "PRmu_gene"]') + + else: + # Delete a pre-existing posterior regularization in case an old one was saved. + posterior.clear_regularized_posterior() + + posterior = Posterior( + dataset_obj=dataset_obj, + vi_model=inferred_model, + posterior_batch_size=args.posterior_batch_size, + debug=args.debug, + ) + try: + ckpt_posterior = load_from_checkpoint(tarball_name=args.input_checkpoint_tarball, + filebase=args.checkpoint_filename, + to_load=['posterior']) + except ValueError: + # input checkpoint tarball was not a match for this workflow + # but we still may have saved a new tarball + ckpt_posterior = load_from_checkpoint(tarball_name=consts.CHECKPOINT_FILE_NAME, + filebase=args.checkpoint_filename, + to_load=['posterior']) + if os.path.exists(ckpt_posterior.get('posterior_file', 'does_not_exist')): + # Load posterior if it was saved in the checkpoint. + posterior.load(file=ckpt_posterior['posterior_file']) + _do_posterior_regularization(posterior) + else: + + # Compute posterior. + logger.info('Posterior not currently included in checkpoint.') + posterior.cell_noise_count_posterior_coo() + _do_posterior_regularization(posterior) + + # Save posterior and add it to checkpoint tarball. + posterior_file = args.output_file[:-3] + '_posterior.h5' + saved = posterior.save(file=posterior_file) + success = False + if saved: + with tempfile.TemporaryDirectory() as tmp_dir: + unpacked = unpack_tarball(tarball_name=args.input_checkpoint_tarball, + directory=tmp_dir) + if unpacked: + shutil.copy(posterior_file, os.path.join(tmp_dir, 'posterior.h5')) + all_ckpt_files = [os.path.join(tmp_dir, f) for f in os.listdir(tmp_dir) + if os.path.isfile(os.path.join(tmp_dir, f))] + success = make_tarball(files=all_ckpt_files, + tarball_name=args.input_checkpoint_tarball) + if success: + logger.info('Added posterior object to checkpoint file.') + else: + logger.warning('Failed to add posterior object to checkpoint file.') + + return posterior + + +class Posterior: + """Posterior handles posteriors on latent variables and denoised counts. + + Args: + dataset_obj: Dataset object. + vi_model: Trained RemoveBackgroundPyroModel. + posterior_batch_size: Number of barcodes in a minibatch, used to + calculate posterior probabilities (memory hungry). + counts_dtype: Data type of posterior count matrix. Can be one of + [np.uint32, np.float] + float_threshold: For floating point count matrices, counts below + this threshold will be set to zero, for the purposes of constructing + a sparse matrix. Unused if counts_dtype is np.uint32 + debug: True to print debugging messages (involves extra compute) + + Properties: + full_noise_count_posterior_csr: The posterior noise log probability + distribution, as a sparse matrix. + latents_map: MAP estimate of latent variables + + Examples: + + posterior = Posterior() + + """ + + def __init__(self, + dataset_obj: Optional['SingleCellRNACountsDataset'], # Dataset + vi_model: Optional['RemoveBackgroundPyroModel'], + posterior_batch_size: int = 128, + counts_dtype: np.dtype = np.uint32, + float_threshold: Optional[float] = 0.5, + debug: bool = False): + self.dataset_obj = dataset_obj + self.vi_model = vi_model + if vi_model is not None: + self.vi_model.eval() + self.vi_model.encoder['z'].eval() + self.vi_model.encoder['other'].eval() + self.vi_model.decoder.eval() + self.use_cuda = (torch.cuda.is_available() if vi_model is None + else vi_model.use_cuda) + self.device = 'cuda' if self.use_cuda else 'cpu' + self.analyzed_gene_inds = (None if (dataset_obj is None) + else dataset_obj.analyzed_gene_inds) + self.count_matrix_shape = (None if (dataset_obj is None) + else dataset_obj.data['matrix'].shape) + self.barcode_inds = (None if (dataset_obj is None) + else np.arange(0, self.count_matrix_shape[0])) + self.dtype = counts_dtype + self.debug = debug + self.float_threshold = float_threshold + self.posterior_batch_size = posterior_batch_size + self._noise_count_posterior_coo = None + self._noise_count_posterior_kwargs = None + self._noise_count_posterior_coo_offsets = None + self._noise_count_regularized_posterior_coo = None + self._noise_count_regularized_posterior_kwargs = None + self._latents = None + if dataset_obj is not None: + self.index_converter = IndexConverter( + total_n_cells=dataset_obj.data['matrix'].shape[0], + total_n_genes=dataset_obj.data['matrix'].shape[1], + ) + + def save(self, file: str) -> bool: + """Save the full posterior in HDF5 format.""" + + if self._noise_count_posterior_coo is None: + self.cell_noise_count_posterior_coo() + + n, g = self.index_converter.get_ng_indices(self._noise_count_posterior_coo.row) + + d = {'posterior_coo': self._noise_count_posterior_coo, + 'noise_count_offsets': self._noise_count_posterior_coo_offsets, + 'posterior_kwargs': self._noise_count_posterior_kwargs, + 'regularized_posterior_coo': self._noise_count_regularized_posterior_coo, + 'regularized_posterior_kwargs': self._noise_count_regularized_posterior_kwargs, + 'latents': self.latents_map, + 'feature_inds': g, + 'barcode_inds': n} + + try: + logger.info(f'Writing full posterior to {file}') + return write_posterior_coo_to_h5(output_file=file, **d) + except MemoryError: + logger.warning('Attempting to save the posterior as an h5 file ' + 'resulted in an out-of-memory error. Please report ' + 'this as a github issue.') + return False + + def load(self, file: str) -> bool: + """Load a saved posterior in compressed array .npz format.""" + + d = load_posterior_from_h5(filename=file) + self._noise_count_posterior_coo = d['coo'] + self._noise_count_posterior_coo_offsets = d['noise_count_offsets'] + self._noise_count_posterior_kwargs = d['kwargs'] + self._noise_count_regularized_posterior_coo = d['regularized_coo'] + self._noise_count_regularized_posterior_kwargs = d['kwargs_regularized'] + self._latents = d['latents'] + logger.info(f'Loaded pre-computed posterior from {file}') + return True + + def compute_denoised_counts(self, + estimator_constructor: EstimationMethod, + **kwargs) -> sp.csc_matrix: + """Probably the most important method: computation of the clean output count matrix. + + Args: + estimator_constructor: A noise count estimator class derived from + the EstimationMethod base class, and implementing the + .estimate_noise() method, which creates a point estimate of + noise. Pass in the constructor, not an object. + **kwargs: Keyword arguments for estimator_constructor().estimate_noise() + + Returns: + denoised_counts: Denoised output CSC sparse matrix (CSC for saving) + + """ + + # Only compute using defaults if the cache is empty. + if self._noise_count_regularized_posterior_coo is not None: + # Priority is taken by a regularized posterior, since presumably + # the user computed it for a reason. + logger.debug('Using regularized posterior to compute denoised counts') + logger.debug(self._noise_count_regularized_posterior_kwargs) + posterior_coo = self._noise_count_regularized_posterior_coo + else: + # Use exact posterior if a regularized version is not computed. + posterior_coo = (self._noise_count_posterior_coo + if (self._noise_count_posterior_coo is not None) + else self.cell_noise_count_posterior_coo()) + + # Instantiate Estimator object. + estimator = estimator_constructor(index_converter=self.index_converter) + + # Compute point estimate of noise in cells. + noise_csr = estimator.estimate_noise( + noise_log_prob_coo=posterior_coo, + noise_offsets=self._noise_count_posterior_coo_offsets, + **kwargs, + ) + + # Subtract cell noise from observed cell counts. + count_matrix = self.dataset_obj.data['matrix'] # all barcodes + cell_inds = self.dataset_obj.analyzed_barcode_inds[self.latents_map['p'] + > consts.CELL_PROB_CUTOFF] + empty_inds = set(range(count_matrix.shape[0])) - set(cell_inds) + cell_counts = csr_set_rows_to_zero(csr=count_matrix, row_inds=empty_inds) + denoised_counts = cell_counts - noise_csr + + return denoised_counts.tocsc() + + def regularize_posterior(self, + regularization: 'PosteriorRegularization', + **kwargs) -> sp.coo_matrix: + """Do posterior regularization. This modifies self._noise_count_regularized_posterior_coo + in place, and returns it. + + Args: + regularization: A particular PosteriorRegularization ['PRmu', 'PRq'] + **kwargs: Arguments passed to the PosteriorRegularization's + .regularize() method + + Returns: + Returns the regularized posterior, which is also stored in + self._noise_count_regularized_posterior_coo + + """ + + # Check if this posterior regularization has already been computed. + currently_cached = False if self._noise_count_regularized_posterior_kwargs is None else True + if currently_cached: + # Check if it's the right thing. + for k, v in self._noise_count_regularized_posterior_kwargs.items(): + if k == 'method': + if v != regularization.name(): + currently_cached = False + break + elif k not in kwargs.keys(): + currently_cached = False + break + elif kwargs[k] != v: + currently_cached = False + break + if currently_cached: + # What's been requested is what's cached. + logger.debug('Regularized posterior is already cached') + return self._noise_count_regularized_posterior_coo + + # Compute the regularized posterior. + self._noise_count_regularized_posterior_coo = regularization.regularize( + noise_count_posterior_coo=self._noise_count_posterior_coo, + noise_offsets=self._noise_count_posterior_coo_offsets, + index_converter=self.index_converter, + **kwargs, + ) + kwargs.update({'method': regularization.name()}) + kwargs.pop('raw_count_matrix', None) # do not store a copy here + self._noise_count_regularized_posterior_kwargs = kwargs + logger.debug('Updated posterior after performing regularization') + return self._noise_count_regularized_posterior_coo + + def clear_regularized_posterior(self): + """Remove the saved regularized posterior (so that compute_denoised_counts() + will not default to using it). + """ + self._noise_count_regularized_posterior_coo = None + self._noise_count_regularized_posterior_kwargs = None + + def cell_noise_count_posterior_coo(self, **kwargs) -> sp.coo_matrix: + """Compute the full-blown posterior on noise counts for all cells, + and store it in COO sparse format on CPU, and cache in + self._noise_count_posterior_csr + + NOTE: This is the main entrypoint for this class. + + Args: + **kwargs: Passed to _get_cell_noise_count_posterior_coo() + + Returns: + self._noise_count_posterior_coo: This sparse COO object contains all + the information about the posterior noise count distribution, + but it is a bit complicated. The data per entry (m, c) are + stored in COO format. The rows "m" represent a combined + cell-and-gene index, with a one-to-one mapping from m to + (n, g). The columns "c" represent noise count values. Values + are the log probabilities of a noise count value. A smaller + matrix can be constructed by increasing the threshold + smallest_log_probability. + """ + + if ((self._noise_count_posterior_coo is None) + or (kwargs != self._noise_count_posterior_kwargs)): + logger.debug('Running _get_cell_noise_count_posterior_coo() to compute posterior') + self._get_cell_noise_count_posterior_coo(**kwargs) + self._noise_count_posterior_kwargs = kwargs + + return self._noise_count_posterior_coo + + @property + def latents_map(self) -> Dict[str, np.ndarray]: + if self._latents is None: + self._get_latents_map() + return self._latents + + @torch.no_grad() + def _get_cell_noise_count_posterior_coo( + self, + n_samples: int = 20, + y_map: bool = True, + n_counts_max: int = 20, + smallest_log_probability: float = -10.) -> sp.coo_matrix: # TODO: default -7 ? + """Compute the full-blown posterior on noise counts for all cells, + and store log probability in COO sparse format on CPU. + + Args: + n_samples: Number of samples to use to compute the posterior log + probability distribution. Samples have high variance, so it is + important to use at least 20. However, they are expensive. + y_map: Use the MAP value for y (cell / no cell) when sampling, to + avoid samples with a cell and samples without a cell. + n_counts_max: Maximum number of noise counts. + smallest_log_probability: Do not store log prob values smaller than + this -- they get set to zero (saves space) + + Returns: + noise_count_posterior_coo: This sparse CSR object contains all + the information about the posterior noise count distribution, + but it is a bit complicated. The data per entry (m, c) are + stored in COO format. The rows "m" represent a combined + cell-and-gene index, and there is a one-to-one mapping from m to + (n, g). The columns "c" represent noise count values. Values + are the log probabilities of a noise count value. A smaller + matrix can be constructed by increasing the threshold + smallest_log_probability. + + """ + + logger.debug('Computing full posterior noise counts') + + # Compute posterior in mini-batches. + torch.cuda.empty_cache() + + # Dataloader for cells only. + analyzed_bcs_only = True + count_matrix = self.dataset_obj.get_count_matrix() # analyzed barcodes + cell_logic = (self.latents_map['p'] > consts.CELL_PROB_CUTOFF) + + # Raise an error if there are no cells found. + if cell_logic.sum() == 0: + logger.error(f'ERROR: Found zero droplets with posterior cell ' + f'probability > {consts.CELL_PROB_CUTOFF}. Please ' + f'check the log for estimated priors on expected cells, ' + f'total droplets included, UMI counts per cell, and ' + f'UMI counts in empty droplets, and see whether these ' + f'values make sense. Consider using additional input ' + f'arguments like --expected-cells, ' + f'--total-droplets-included, --force-cell-umi-prior, ' + f'and --force-empty-umi-prior, to make these values ' + f'accurate for your dataset.') + raise RuntimeError('Zero cells found!') + + dataloader_index_to_analyzed_bc_index = np.where(cell_logic)[0] + cell_data_loader = DataLoader( + count_matrix[cell_logic], + empty_drop_dataset=None, + batch_size=self.posterior_batch_size, + fraction_empties=0., + shuffle=False, + use_cuda=self.use_cuda, + ) + + bcs = [] # barcode index + genes = [] # gene index + c = [] # noise count value + c_offset = [] # noise count offsets from zero + log_probs = [] + ind = 0 + n_minibatches = len(cell_data_loader) + + logger.info('Computing posterior noise count probabilities in mini-batches.') + + for i, data in enumerate(cell_data_loader): + + if i == 0: + t = time.time() + elif i == 1: + logger.info(f' [{(time.time() - t) / 60:.2f} mins per chunk]') + logger.info(f'Working on chunk ({i + 1}/{n_minibatches})') + + if self.debug: + logger.debug(f'Posterior minibatch starting with droplet {ind}') + logger.debug('\n' + get_hardware_usage(use_cuda=self.use_cuda)) + + # Compute noise count probabilities. + noise_log_pdf_NGC, noise_count_offset_NG = self.noise_log_pdf( + data=data, + n_samples=n_samples, + y_map=y_map, + n_counts_max=n_counts_max, + ) + + # Compute a tensor to indicate sparsity. + # First we want data = 0 to be all zeros + # We also want anything below the threshold to be a zero + tensor_for_nonzeros = noise_log_pdf_NGC.clone().exp() # probability + tensor_for_nonzeros.data[data == 0, :] = 0. # remove data = 0 + tensor_for_nonzeros.data[noise_log_pdf_NGC < smallest_log_probability] = 0. + + # Convert to sparse format using "m" indices. + bcs_i_chunk, genes_i_analyzed, c_i, log_prob_i = dense_to_sparse_op_torch( + noise_log_pdf_NGC, + tensor_for_nonzeros=tensor_for_nonzeros, + ) + + # Get the original gene index from gene index in the trimmed dataset. + genes_i = self.analyzed_gene_inds[genes_i_analyzed] + + # Barcode index in the dataloader. + bcs_i = bcs_i_chunk + ind + + # Obtain the real barcode index since we only use cells. + bcs_i = dataloader_index_to_analyzed_bc_index[bcs_i] + + # Translate chunk barcode inds to overall inds. + if analyzed_bcs_only: + bcs_i = self.dataset_obj.analyzed_barcode_inds[bcs_i] + else: + bcs_i = self.barcode_inds[bcs_i] + + # Add sparse matrix values to lists. + try: + bcs.extend(bcs_i.tolist()) + genes.extend(genes_i.tolist()) + c.extend(c_i.tolist()) + log_probs.extend(log_prob_i.tolist()) + c_offset.extend(noise_count_offset_NG[bcs_i_chunk, genes_i_analyzed] + .detach().cpu().numpy()) + except TypeError as e: + # edge case of a single value + bcs.append(bcs_i) + genes.append(genes_i) + c.append(c_i) + log_probs.append(log_prob_i) + c_offset.append(noise_count_offset_NG[bcs_i_chunk, genes_i_analyzed] + .detach().cpu().numpy()) + + # Increment barcode index counter. + ind += data.shape[0] # Same as data_loader.batch_size + + # Convert the lists to numpy arrays. + log_probs = np.array(log_probs, dtype=float) + c = np.array(c, dtype=np.uint32) + barcodes = np.array(bcs, dtype=np.uint64) # uint32 is too small! + genes = np.array(genes, dtype=np.uint64) # use same as above for IndexConverter + noise_count_offsets = np.array(c_offset, dtype=np.uint32) + + # Translate (barcode, gene) inds to 'm' format index. + m = self.index_converter.get_m_indices(cell_inds=barcodes, gene_inds=genes) + + # Put the counts into a sparse csr_matrix. + self._noise_count_posterior_coo = sp.coo_matrix( + (log_probs, (m, c)), + shape=[np.prod(self.count_matrix_shape), n_counts_max], + ) + noise_offset_dict = dict(zip(m, noise_count_offsets)) + nonzero_noise_offset_dict = {k: v for k, v in noise_offset_dict.items() if (v > 0)} + self._noise_count_posterior_coo_offsets = nonzero_noise_offset_dict + return self._noise_count_posterior_coo + + @torch.no_grad() + def sample(self, data, lambda_multiplier=1., y_map: bool = False) -> torch.Tensor: + """Draw a single posterior sample for the count matrix conditioned on data + + Args: + data: Count matrix (slice: some droplets, all genes) + lambda_multiplier: BasePosterior regularization multiplier + y_map: True to enforce the use of the MAP estimate of y, cell or + no cell. Useful in the case where many samples are collected, + since typically in those cases it is confusing to have samples + where a droplet is both cell-containing and empty. + + Returns: + denoised_output_count_matrix: Single sample of the denoised output + count matrix, sampling all stochastic latent variables in the model. + + """ + + # Sample all the latent variables in the model and get mu, lambda, alpha. + mu_sample, lambda_sample, alpha_sample = self.sample_mu_lambda_alpha(data, y_map=y_map) + + # Compute the big tensor of log probabilities of possible c_{ng}^{noise} values. + log_prob_noise_counts_NGC, poisson_values_low_NG = self._log_prob_noise_count_tensor( + data=data, + mu_est=mu_sample + 1e-30, + lambda_est=lambda_sample * lambda_multiplier + 1e-30, + alpha_est=alpha_sample + 1e-30, + debug=self.debug, + ) + + # Use those probabilities to draw a sample of c_{ng}^{noise} + noise_count_increment_NG = dist.Categorical(logits=log_prob_noise_counts_NGC).sample() + noise_counts_NG = noise_count_increment_NG + poisson_values_low_NG + + # Subtract from data to get the denoised output counts. + denoised_output_count_matrix = data - noise_counts_NG + + return denoised_output_count_matrix + + @torch.no_grad() + def map_denoised_counts_from_sampled_latents(self, + data, + n_samples: int, + lambda_multiplier: float = 1., + y_map: bool = False) -> torch.Tensor: + """Draw posterior samples for all stochastic latent variables in the model + and use those values to compute a MAP estimate of the denoised count + matrix conditioned on data. + + Args: + data: Count matrix (slice: some droplets, all genes) + lambda_multiplier: BasePosterior regularization multiplier + y_map: True to enforce the use of the MAP estimate of y, cell or + no cell. Useful in the case where many samples are collected, + since typically in those cases it is confusing to have samples + where a droplet is both cell-containing and empty. + + Returns: + denoised_output_count_matrix: MAP estimate of the denoised output + count matrix, sampling all stochastic latent variables in the model. + + """ + + noise_log_pdf, offset_noise_counts = self.noise_log_pdf( + data=data, + n_samples=n_samples, + lambda_multiplier=lambda_multiplier, + y_map=y_map, + ) + + noise_counts = torch.argmax(noise_log_pdf, dim=-1) + offset_noise_counts + denoised_output_count_matrix = torch.clamp(data - noise_counts, min=0.) + + return denoised_output_count_matrix + + @torch.no_grad() + def noise_log_pdf(self, + data, + n_samples: int = 1, + lambda_multiplier=1., + y_map: bool = True, + n_counts_max: int = 50) -> Tuple[torch.Tensor, torch.Tensor]: + """Compute the posterior noise-count probability density function + using n_samples samples. This is a big matrix [n, g, c] where the last + dimension c is of variable size depending on the computation in + _log_prob_noise_count_tensor(), but is limited there to be no more than + 100. The c dimension represents an index to the number of noise counts + in [n, g]: specifically, the noise count once poisson_values_low_NG is added + + Args: + data: Count matrix (slice: some droplets, all genes) + n_samples: Number of samples (of all stochastic latent variables in + the model) used to generate the CDF + lambda_multiplier: BasePosterior regularization multiplier + y_map: True to enforce the use of the MAP estimate of y, cell or + no cell. Useful in the case where many samples are collected, + since typically in those cases it is confusing to have samples + where a droplet is both cell-containing and empty. + n_counts_max: Size of count axis (need not start at zero noise + counts, but should be enough to cover the meat of the posterior) + + Returns: + noise_log_pdf_NGC: Consensus noise count log_pdf (big tensor) from the samples. + noise_count_offset_NG: The offset for the noise count axis [n, g]. + + """ + + noise_log_pdf_NGC = None + noise_count_offset_NG = None + + for s in range(1, n_samples + 1): + + # Sample all the latent variables in the model and get mu, lambda, alpha. + mu_sample, lambda_sample, alpha_sample = self.sample_mu_lambda_alpha(data, y_map=y_map) + + # Compute the big tensor of log probabilities of possible c_{ng}^{noise} values. + log_prob_noise_counts_NGC, noise_count_offset_NG = self._log_prob_noise_count_tensor( + data=data, + mu_est=mu_sample + 1e-30, + lambda_est=lambda_sample * lambda_multiplier + 1e-30, + alpha_est=alpha_sample + 1e-30, + n_counts_max=n_counts_max, + debug=self.debug, + ) + + # Normalize the PDFs (not necessarily normalized over the count range). + log_prob_noise_counts_NGC = (log_prob_noise_counts_NGC + - torch.logsumexp(log_prob_noise_counts_NGC, + dim=-1, keepdim=True)) + + # Add the probability from this sample to our running total. + # Update rule is + # log_prob_total_n = LAE [ log(1 - 1/n) + log_prob_total_{n-1}, log(1/n) + log_prob_sample ] + if s == 1: + noise_log_pdf_NGC = log_prob_noise_counts_NGC + else: + # This is a (normalized) running sum over samples in log-probability space. + noise_log_pdf_NGC = torch.logaddexp( + noise_log_pdf_NGC + torch.log(torch.tensor(1. - 1. / s).to(device=data.device)), + log_prob_noise_counts_NGC + torch.log(torch.tensor(1. / s).to(device=data.device)), + ) + + return noise_log_pdf_NGC, noise_count_offset_NG + + @torch.no_grad() + def sample_mu_lambda_alpha(self, + data: torch.Tensor, + y_map: bool) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Calculate a single sample estimate of mu, the mean of the true count + matrix, and lambda, the rate parameter of the Poisson background counts. + + Args: + data: Dense tensor minibatch of cell by gene count data. + y_map: True to enforce the use of a MAP estimate of y rather than + sampling y. This prevents some samples from having a cell and + some not, which can lead to strange summary statistics over + many samples. + + Returns: + mu_sample: Dense tensor sample of Negative Binomial mean for true + counts. + lambda_sample: Dense tensor sample of Poisson rate params for noise + counts. + alpha_sample: Dense tensor sample of Dirichlet concentration params + that inform the overdispersion of the Negative Binomial. + + """ + + logger.debug('Replaying model with guide to sample mu, alpha, lambda') + + # Use pyro poutine to trace the guide and sample parameter values. + guide_trace = pyro.poutine.trace(self.vi_model.guide).get_trace(x=data) + + # If using MAP for y (so that you never get samples of cell and no cell), + # then intervene and replace a sampled y with the MAP + if y_map: + guide_trace.nodes['y']['value'] = ( + guide_trace.nodes['p_passback']['value'] > 0 + ).clone().detach() + + replayed_model = pyro.poutine.replay(self.vi_model.model, guide_trace) + + # Run the model using these sampled values. + replayed_model_output = replayed_model(x=data) + + # The model returns mu, alpha, and lambda. + mu_sample = replayed_model_output['mu'] + lambda_sample = replayed_model_output['lam'] + alpha_sample = replayed_model_output['alpha'] + + return mu_sample, lambda_sample, alpha_sample + + @staticmethod + @torch.no_grad() + def _log_prob_noise_count_tensor(data: torch.Tensor, + mu_est: torch.Tensor, + lambda_est: torch.Tensor, + alpha_est: Optional[torch.Tensor], + n_counts_max: int = 100, + debug: bool = False) -> Tuple[torch.Tensor, torch.Tensor]: + """Compute the log prob of noise counts [n, g, c] given mu, lambda, alpha, and the data. + + NOTE: this is un-normalized log probability + + Args: + data: Dense tensor minibatch of cell by gene count data. + mu_est: Dense tensor of Negative Binomial means for true counts. + lambda_est: Dense tensor of Poisson rate params for noise counts. + alpha_est: Dense tensor of Dirichlet concentration params that + inform the overdispersion of the Negative Binomial. None will + use an all-Poisson model + n_counts_max: Size of noise count dimension c + debug: True will go slow and check for NaNs and zero-probability entries + + Returns: + log_prob_tensor: Probability of each noise count value. + poisson_values_low: The starting point for noise counts for each + cell and gene, because they can be different. + + """ + + # Estimate a reasonable low-end to begin the Poisson summation. + n = min(n_counts_max, data.max().item()) # No need to exceed the max value + poisson_values_low = (lambda_est.detach() - n / 2).int() + + poisson_values_low = torch.clamp(torch.min(poisson_values_low, + (data - n + 1).int()), min=0).float() + + # Construct a big tensor of possible noise counts per cell per gene, + # shape (batch_cells, n_genes, max_noise_counts) + noise_count_tensor = torch.arange(start=0, end=n) \ + .expand([data.shape[0], data.shape[1], -1]) \ + .float().to(device=data.device) + noise_count_tensor = noise_count_tensor + poisson_values_low.unsqueeze(-1) + + # Compute probabilities of each number of noise counts. + # NOTE: some values will be outside the support (negative values for NB). + # This results in NaNs. + if alpha_est is None: + # Poisson only model + log_prob_tensor = (dist.Poisson(lambda_est.unsqueeze(-1), validate_args=False) + .log_prob(noise_count_tensor) + + dist.Poisson(mu_est.unsqueeze(-1), validate_args=False) + .log_prob(data.unsqueeze(-1) - noise_count_tensor)) + logger.debug('Using all poisson model (since alpha is not supplied to posterior)') + else: + logits = (mu_est.log() - alpha_est.log()).unsqueeze(-1) + log_prob_tensor = (dist.Poisson(lambda_est.unsqueeze(-1), validate_args=False) + .log_prob(noise_count_tensor) + + dist.NegativeBinomial(total_count=alpha_est.unsqueeze(-1), + logits=logits, + validate_args=False) + .log_prob(data.unsqueeze(-1) - noise_count_tensor)) + + # Set log_prob to -inf if noise > data. + neg_inf_tensor = torch.ones_like(log_prob_tensor) * -np.inf + log_prob_tensor = torch.where((noise_count_tensor <= data.unsqueeze(-1)), + log_prob_tensor, + neg_inf_tensor) + + logger.debug(f'Prob computation with tensor of shape {log_prob_tensor.shape}') + + if debug: + assert not torch.isnan(log_prob_tensor).any(), \ + 'log_prob_tensor contains a NaN' + if torch.isinf(log_prob_tensor).all(dim=-1).any(): + print(torch.where(torch.isinf(log_prob_tensor).all(dim=-1))) + raise AssertionError('There is at least one log_prob_tensor[n, g, :] ' + 'that has all-zero probability') + + return log_prob_tensor, poisson_values_low + + @torch.no_grad() + def _get_latents_map(self): + """Calculate the encoded latent variables.""" + + logger.debug('Computing latent variables') + + if self.vi_model is None: + self._latents = {'z': None, 'd': None, 'p': None, 'phi_loc_scale': None, 'epsilon': None} + return None + + data_loader = self.dataset_obj.get_dataloader(use_cuda=self.use_cuda, + analyzed_bcs_only=True, + batch_size=500, + shuffle=False) + + n_analyzed = data_loader.dataset.shape[0] + + z = np.zeros((n_analyzed, self.vi_model.encoder['z'].output_dim)) + d = np.zeros(n_analyzed) + p = np.zeros(n_analyzed) + epsilon = np.zeros(n_analyzed) + + phi_loc = pyro.param('phi_loc') + phi_scale = pyro.param('phi_scale') + if 'chi_ambient' in pyro.get_param_store().keys(): + chi_ambient = pyro.param('chi_ambient').detach() + else: + chi_ambient = None + + start = 0 + for i, data in enumerate(data_loader): + + end = start + data.shape[0] + + enc = self.vi_model.encoder(x=data, + chi_ambient=chi_ambient, + cell_prior_log=self.vi_model.d_cell_loc_prior) + z[start:end, :] = enc['z']['loc'].detach().cpu().numpy() + + d[start:end] = \ + dist.LogNormal(loc=enc['d_loc'], + scale=pyro.param('d_cell_scale')).mean.detach().cpu().numpy() + + p[start:end] = enc['p_y'].sigmoid().detach().cpu().numpy() + + epsilon[start:end] = \ + dist.Gamma(enc['epsilon'] * self.vi_model.epsilon_prior, + self.vi_model.epsilon_prior).mean.detach().cpu().numpy() + + start = end + + self._latents = {'z': z, + 'd': d, + 'p': p, + 'phi_loc_scale': [phi_loc.item(), phi_scale.item()], + 'epsilon': epsilon} + + @torch.no_grad() + def _get_mu_alpha_lambda_map(self, + data: torch.Tensor, + chi_ambient: torch.Tensor) -> Dict[str, torch.Tensor]: + """Calculate MAP estimates of mu, the mean of the true count matrix, and + lambda, the rate parameter of the Poisson background counts. + + Args: + data: Dense tensor minibatch of cell by gene count data. + chi_ambient: Point estimate of inferred ambient gene expression. + + Returns: + mu_map: Dense tensor of Negative Binomial means for true counts. + lambda_map: Dense tensor of Poisson rate params for noise counts. + alpha_map: Dense tensor of Dirichlet concentration params that + inform the overdispersion of the Negative Binomial. + + """ + + logger.debug('Computing MAP esitmate of mu, lambda, alpha') + + # Encode latents. + enc = self.vi_model.encoder(x=data, + chi_ambient=chi_ambient, + cell_prior_log=self.vi_model.d_cell_loc_prior) + z_map = enc['z']['loc'] + + chi_map = self.vi_model.decoder(z_map) + phi_loc = pyro.param('phi_loc') + phi_scale = pyro.param('phi_scale') + phi_conc = phi_loc.pow(2) / phi_scale.pow(2) + phi_rate = phi_loc / phi_scale.pow(2) + alpha_map = 1. / dist.Gamma(phi_conc, phi_rate).mean + + y = (enc['p_y'] > 0).float() + d_empty = dist.LogNormal(loc=pyro.param('d_empty_loc'), + scale=pyro.param('d_empty_scale')).mean + d_cell = dist.LogNormal(loc=enc['d_loc'], + scale=pyro.param('d_cell_scale')).mean + epsilon = dist.Gamma(enc['epsilon'] * self.vi_model.epsilon_prior, + self.vi_model.epsilon_prior).mean + + if self.vi_model.include_rho: + rho = pyro.param("rho_alpha") / (pyro.param("rho_alpha") + + pyro.param("rho_beta")) + else: + rho = None + + # Calculate MAP estimates of mu and lambda. + mu_map = calculate_mu( + epsilon=epsilon, + d_cell=d_cell, + chi=chi_map, + y=y, + rho=rho, + ) + lambda_map = calculate_lambda( + epsilon=epsilon, + chi_ambient=chi_ambient, + d_empty=d_empty, + y=y, + d_cell=d_cell, + rho=rho, + chi_bar=self.vi_model.avg_gene_expression, + ) + + return {'mu': mu_map, 'lam': lambda_map, 'alpha': alpha_map} + + +class PosteriorRegularization(ABC): + + def __init__(self): + super(PosteriorRegularization, self).__init__() + + @staticmethod + @abstractmethod + def name(): + """Short name of this regularization method""" + pass + + @staticmethod + @abstractmethod + def regularize(noise_count_posterior_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + **kwargs) -> sp.coo_matrix: + """Perform posterior regularization""" + pass + + +class PRq(PosteriorRegularization): + """Approximate noise CDF quantile targeting: + + E_reg[noise_counts] >= E[noise_counts] + \alpha * Std[noise_counts] + + """ + + @staticmethod + def name(): + return 'PRq' + + @staticmethod + def _log_mean_plus_alpha_std(log_prob: torch.Tensor, alpha: float): + c = torch.arange(log_prob.shape[1]).float().to(log_prob.device).unsqueeze(0) + prob = log_prob.exp() + mean = (c * prob).sum(dim=-1) + std = (((c - mean.unsqueeze(-1)).pow(2) * prob).sum(dim=-1)).sqrt() + return (mean + alpha * std).log() + + @staticmethod + def _compute_log_target_dict(noise_count_posterior_coo: sp.coo_matrix, + alpha: float) -> Dict[int, float]: + """Given the noise count posterior, return log(mean + alpha * std) + for each 'm' index + + NOTE: noise_log_pdf_BC should be normalized + + Args: + noise_count_posterior_coo: The noise count posterior data structure + alpha: The tunable parameter of mean-targeting posterior + regularization. The output distribution has a mean which is + input_mean + alpha * input_std (if possible) + + Returns: + log_mean_plus_alpha_std: Dict keyed by 'm', where values are + log(mean + alpha * std) + + """ + result = apply_function_dense_chunks(noise_log_prob_coo=noise_count_posterior_coo, + fun=PRq._log_mean_plus_alpha_std, + alpha=alpha) + return dict(zip(result['m'], result['result'])) + + @staticmethod + def _get_alpha_log_constraint_violation_given_beta( + beta_B: torch.Tensor, + log_pdf_noise_counts_BC: torch.Tensor, + noise_count_BC: torch.Tensor, + log_mu_plus_alpha_sigma_B: torch.Tensor) -> torch.Tensor: + r"""Returns log constraint violation for the regularized posterior of p(x), which + here is p(\omega) = p(x) e^{\beta x}, and we want + E[\omega] = E[x] + \alpha * Std[x] = log_mu_plus_alpha_sigma_B.exp() + + NOTE: Binary search to find the root of this function can yield a value for beta_B. + + Args: + beta_B: The parameter of the regularized posterior, with batch dimension + log_pdf_noise_counts_BC: The probability density of noise counts, with batch + and count dimensions + noise_count_BC: Noise counts, with batch and count dimensions + log_mu_plus_alpha_sigma_B: The constraint value to be satisfied, with batch dimension + + Returns: + The amount by which the desired equality with log_mu_plus_alpha_sigma_B is violated, + with batch dimension + + """ + + log_numerator_B = torch.logsumexp( + noise_count_BC.log() + log_pdf_noise_counts_BC + beta_B.unsqueeze(-1) * noise_count_BC, + dim=-1, + ) + log_denominator_B = torch.logsumexp( + log_pdf_noise_counts_BC + beta_B.unsqueeze(-1) * noise_count_BC, + dim=-1, + ) + return log_numerator_B - log_denominator_B - log_mu_plus_alpha_sigma_B + + @staticmethod + def _chunked_compute_regularized_posterior( + noise_count_posterior_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + log_constraint_violation_fcn: Callable[[torch.Tensor, torch.Tensor, + torch.Tensor, torch.Tensor], torch.Tensor], + log_target_M: torch.Tensor, + target_tolerance: float = 0.001, + device: str = 'cpu', + n_chunks: Optional[int] = None, + ) -> sp.coo_matrix: + """Go through posterior in chunks and compute regularized posterior, + using the defined targets""" + + # Compute using dense chunks, chunked on m-index. + if n_chunks is None: + dense_size_gb = (len(np.unique(noise_count_posterior_coo.row)) + * (noise_count_posterior_coo.shape[1] + + np.array(list(noise_offsets.values())).max())) * 4 / 1e9 # GB + n_chunks = max(1, int(dense_size_gb // 1)) # approx 1 GB each + + # Make the sparse matrix compact in the sense that it should use contiguous row values. + unique_rows, densifiable_coo_rows = np.unique(noise_count_posterior_coo.row, return_inverse=True) + densifiable_csr = sp.csr_matrix((noise_count_posterior_coo.data, + (densifiable_coo_rows, noise_count_posterior_coo.col)), + shape=[len(unique_rows), noise_count_posterior_coo.shape[1]]) + chunk_size = int(np.ceil(densifiable_csr.shape[0] / n_chunks)) + + m = [] + c = [] + log_prob_reg = [] + + for i in range(n_chunks): + # B index here represents a batch: the re-defined m-index + log_pdf_noise_counts_BC = torch.tensor( + log_prob_sparse_to_dense(densifiable_csr[(i * chunk_size):((i + 1) * chunk_size)]) + ).to(device) + noise_count_BC = (torch.arange(log_pdf_noise_counts_BC.shape[1]) + .to(log_pdf_noise_counts_BC.device) + .unsqueeze(0) + .expand(log_pdf_noise_counts_BC.shape)) + m_indices_for_chunk = unique_rows[(i * chunk_size):((i + 1) * chunk_size)] + noise_count_BC = noise_count_BC + (torch.tensor([noise_offsets.get(m, 0) + for m in m_indices_for_chunk], + dtype=torch.float) + .unsqueeze(-1) + .to(device)) + + # Parallel binary search for beta for each entry of count matrix + beta_B = torch_binary_search( + evaluate_outcome_given_value=lambda x: + log_constraint_violation_fcn( + beta_B=x, + log_pdf_noise_counts_BC=log_pdf_noise_counts_BC, + noise_count_BC=noise_count_BC, + log_mu_plus_alpha_sigma_B=log_target_M[(i * chunk_size):((i + 1) * chunk_size)], + ), + target_outcome=torch.zeros(noise_count_BC.shape[0]).to(device), + init_range=(torch.tensor([-100., 100.]) + .to(device) + .unsqueeze(0) + .expand((noise_count_BC.shape[0],) + (2,))), + target_tolerance=target_tolerance, + max_iterations=100, + ) + + # Generate regularized posteriors. + log_pdf_reg_BC = log_pdf_noise_counts_BC + beta_B.unsqueeze(-1) * noise_count_BC + log_pdf_reg_BC = log_pdf_reg_BC - torch.logsumexp(log_pdf_reg_BC, -1, keepdims=True) + + # Store sparse COO values in lists. + tensor_for_nonzeros = log_pdf_reg_BC.clone().exp() # probability + m_i, c_i, log_prob_reg_i = dense_to_sparse_op_torch( + log_pdf_reg_BC, + tensor_for_nonzeros=tensor_for_nonzeros, + ) + m_i = np.array([m_indices_for_chunk[j] for j in m_i]) # chunk m to actual m + + # Add sparse matrix values to lists. + try: + m.extend(m_i.tolist()) + c.extend(c_i.tolist()) + log_prob_reg.extend(log_prob_reg_i.tolist()) + except TypeError as e: + # edge case of a single value + m.append(m_i) + c.append(c_i) + log_prob_reg.append(log_prob_reg_i) + + reg_noise_count_posterior_coo = sp.coo_matrix((log_prob_reg, (m, c)), + shape=noise_count_posterior_coo.shape) + return reg_noise_count_posterior_coo + + @staticmethod + @torch.no_grad() + def regularize(noise_count_posterior_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + alpha: float, + device: str = 'cuda', + target_tolerance: float = 0.001, + n_chunks: Optional[int] = None, + **kwargs) -> sp.coo_matrix: + """Perform posterior regularization using approximate quantile-targeting. + + Args: + noise_count_posterior_coo: Noise count posterior log prob COO + noise_offsets: Offset noise counts per 'm' index + alpha: The tunable parameter of quantile-targeting posterior + regularization. The output distribution has a mean which is + input_mean + alpha * input_std (if possible) + device: Where to perform tensor operations: ['cuda', 'cpu'] + target_tolerance: Tolerance when searching using binary search + n_chunks: For testing only - the number of chunks used to + compute the result when iterating over the posterior + + Results: + reg_noise_count_posterior_coo: The regularized noise count + posterior data structure + + """ + logger.info(f'Regularizing noise count posterior using approximate quantile-targeting with alpha={alpha}') + + # Compute the expectation for the mean post-regularization. + log_target_dict = PRq._compute_log_target_dict( + noise_count_posterior_coo=noise_count_posterior_coo, + alpha=alpha, + ) + log_target_M = torch.tensor(list(log_target_dict.values())).to(device) + + reg_noise_count_posterior_coo = PRq._chunked_compute_regularized_posterior( + noise_count_posterior_coo=noise_count_posterior_coo, + noise_offsets=noise_offsets, + log_target_M=log_target_M, + log_constraint_violation_fcn=PRq._get_alpha_log_constraint_violation_given_beta, + device=device, + target_tolerance=target_tolerance, + n_chunks=n_chunks, + ) + + return reg_noise_count_posterior_coo + + +class PRmu(PosteriorRegularization): + r"""Approximate noise mean targeting: + + Overall (default): + E_reg[\sum_{n} \sum_{g} noise_counts_{ng}] = + E[\sum_{n} \sum_{g} noise_counts_{ng}] + nFPR * \sum_{n} \sum_{g} raw_counts_{ng} + + Per-gene: + E_reg[\sum_{n} noise_counts_{ng}] = + E[\sum_{n} noise_counts_{ng}] + nFPR * \sum_{n} raw_counts_{ng} + + """ + + @staticmethod + def name(): + return 'PRmu' + + @staticmethod + def _binary_search_for_posterior_regularization_factor( + noise_count_posterior_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + index_converter: 'IndexConverter', + target_removal: torch.Tensor, + shape: int, + target_tolerance: float = 100, + max_iterations: int = 20, + device: str = 'cpu', + ) -> torch.Tensor: + """Go through posterior and compute regularization factor(s), + using the defined targets""" + + def summarize_map_noise_counts(x: torch.Tensor, + per_gene: bool) -> torch.Tensor: + """Given a (subset of the) noise posterior, compute the MAP estimate + and summarize it either as the overall sum or per-gene. + """ + + # Regularize posterior. + regularized_noise_posterior_coo = PRmu._chunked_compute_regularized_posterior( + noise_count_posterior_coo=noise_count_posterior_coo, + noise_offsets=noise_offsets, + index_converter=index_converter, + beta=x, + device=device, + ) + + # Compute MAP. + estimator = MAP(index_converter=index_converter) + map_noise_csr = estimator.estimate_noise( + noise_log_prob_coo=regularized_noise_posterior_coo, + noise_offsets=noise_offsets, + device=device, + ) + + # Summarize removal. + if per_gene: + noise_counts = np.array(map_noise_csr.sum(axis=0)).squeeze() + else: + noise_counts = map_noise_csr.sum() + + return torch.tensor(noise_counts).to(device) + + # Perform binary search for beta. + per_gene = False + if target_removal.dim() > 0: + if len(target_removal) > 1: + per_gene = True + + beta = torch_binary_search( + evaluate_outcome_given_value=lambda x: + summarize_map_noise_counts(x=x, per_gene=per_gene), + target_outcome=target_removal, + init_range=(torch.tensor([-100., 200.]) + .to(device) + .unsqueeze(0) + .expand((shape,) + (2,))), + target_tolerance=target_tolerance, + max_iterations=max_iterations, + debug=True, + ) + return beta + + @staticmethod + def _chunked_compute_regularized_posterior( + noise_count_posterior_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + index_converter: 'IndexConverter', + beta: torch.Tensor, + device: str = 'cpu', + n_chunks: Optional[int] = None, + ) -> sp.coo_matrix: + """Go through posterior in chunks and compute regularized posterior, + using the defined targets""" + + # Compute using dense chunks, chunked on m-index. + if n_chunks is None: + dense_size_gb = (len(np.unique(noise_count_posterior_coo.row)) + * (noise_count_posterior_coo.shape[1] + + np.array(list(noise_offsets.values())).max())) * 4 / 1e9 # GB + n_chunks = max(1, int(dense_size_gb // 1)) # approx 1 GB each + + # Make the sparse matrix compact in the sense that it should use contiguous row values. + unique_rows, densifiable_coo_rows = np.unique(noise_count_posterior_coo.row, return_inverse=True) + densifiable_csr = sp.csr_matrix((noise_count_posterior_coo.data, + (densifiable_coo_rows, noise_count_posterior_coo.col)), + shape=[len(unique_rows), noise_count_posterior_coo.shape[1]]) + chunk_size = int(np.ceil(densifiable_csr.shape[0] / n_chunks)) + + m = [] + c = [] + log_prob_reg = [] + + for i in range(n_chunks): + # B index here represents a batch: the re-defined m-index + log_pdf_noise_counts_BC = torch.tensor( + log_prob_sparse_to_dense(densifiable_csr[(i * chunk_size):((i + 1) * chunk_size)]) + ).to(device) + noise_count_BC = (torch.arange(log_pdf_noise_counts_BC.shape[1]) + .to(log_pdf_noise_counts_BC.device) + .unsqueeze(0) + .expand(log_pdf_noise_counts_BC.shape)) + m_indices_for_chunk = unique_rows[(i * chunk_size):((i + 1) * chunk_size)] + noise_count_BC = noise_count_BC + (torch.tensor([noise_offsets.get(m, 0) + for m in m_indices_for_chunk], + dtype=torch.float) + .unsqueeze(-1) + .to(device)) + + # Get beta for this chunk. + if len(beta) == 1: + # posterior regularization factor is a single scalar + beta_B = beta + else: + # per-gene mode + n, g = index_converter.get_ng_indices(m_inds=m_indices_for_chunk) + beta_B = torch.tensor([beta[gene] for gene in g]) + + # Generate regularized posteriors. + log_pdf_reg_BC = log_pdf_noise_counts_BC + beta_B.unsqueeze(-1) * noise_count_BC + log_pdf_reg_BC = log_pdf_reg_BC - torch.logsumexp(log_pdf_reg_BC, -1, keepdims=True) + + # Store sparse COO values in lists. + tensor_for_nonzeros = log_pdf_reg_BC.clone().exp() # probability + # tensor_for_nonzeros.data[data == 0, :] = 0. # remove data = 0 + m_i, c_i, log_prob_reg_i = dense_to_sparse_op_torch( + log_pdf_reg_BC, + tensor_for_nonzeros=tensor_for_nonzeros, + ) + m_i = np.array([m_indices_for_chunk[j] for j in m_i]) # chunk m to actual m + + # Add sparse matrix values to lists. + try: + m.extend(m_i.tolist()) + c.extend(c_i.tolist()) + log_prob_reg.extend(log_prob_reg_i.tolist()) + except TypeError as e: + # edge case of a single value + m.append(m_i) + c.append(c_i) + log_prob_reg.append(log_prob_reg_i) + + reg_noise_count_posterior_coo = sp.coo_matrix((log_prob_reg, (m, c)), + shape=noise_count_posterior_coo.shape) + return reg_noise_count_posterior_coo + + @staticmethod + def _subset_posterior_by_cells(noise_count_posterior_coo: sp.coo_matrix, + index_converter: 'IndexConverter', + n_cells: int) -> sp.coo_matrix: + """Return a random slice of the full posterior with a specified number + of cells. + + NOTE: Assumes that all the entries in noise_count_posterior_coo are for + cell-containing droplets, and not empty droplets. + + Args: + noise_count_posterior_coo: The noise count posterior data structure + n_cells: The number of cells in the output subset + + Returns: + subset_coo: Posterior for a random subset of cells, in COO format + """ + + # Choose cells that will be included. + m = noise_count_posterior_coo.row + n, g = index_converter.get_ng_indices(m_inds=m) + unique_cell_inds = np.unique(n) + if n_cells > len(unique_cell_inds): + logger.debug(f'Limiting n_cells during PRmu regularizer binary search to {unique_cell_inds}') + n_cells = len(unique_cell_inds) + chosen_n_values = set(np.random.choice(unique_cell_inds, size=n_cells, replace=False)) + element_logic = [val in chosen_n_values for val in n] + + # Subset the posterior. + data_subset = noise_count_posterior_coo.data[element_logic] + row_subset = noise_count_posterior_coo.row[element_logic] + col_subset = noise_count_posterior_coo.col[element_logic] + return sp.coo_matrix((data_subset, (row_subset, col_subset)), + shape=noise_count_posterior_coo.shape) + + @staticmethod + @torch.no_grad() + def regularize(noise_count_posterior_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + index_converter: 'IndexConverter', + raw_count_matrix: sp.csr_matrix, + fpr: float, + per_gene: bool = False, + device: str = 'cuda', + target_tolerance: float = 0.5, + n_cells: int = 1000, + n_chunks: Optional[int] = None, + **kwargs) -> sp.coo_matrix: + """Perform posterior regularization using mean-targeting. + + Args: + noise_count_posterior_coo: Noise count posterior log prob COO + noise_offsets: Offset noise counts per 'm' index + index_converter: IndexConverter object from 'm' to (n, g) and back + raw_count_matrix: The raw count matrix + fpr: The tunable parameter of mean-targeting posterior + regularization. The output, summed over cells, has a removed + gene count distribution similar to what would be expected from + the noise model, plus this nominal false positive rate. + per_gene: True to find one posterior regularization factor for each + gene, False to find one overall scalar (behavior of v0.2.0) + device: Where to perform tensor operations: ['cuda', 'cpu'] + target_tolerance: Tolerance when searching using binary search. + In units of counts, so this really should not be less than 0.5 + n_cells: To save time, use only this many cells to estimate removal + n_chunks: For testing only - the number of chunks used to + compute the result when iterating over the posterior + + Results: + reg_noise_count_posterior_coo: The regularized noise count + posterior data structure + + """ + + logger.info('Regularizing noise count posterior using mean-targeting') + + # Use a subset of the data to find regularization factors, to reduce time. + logger.debug(f'Subsetting posterior to {n_cells} cells for this computation') + posterior_subset_coo = PRmu._subset_posterior_by_cells( + noise_count_posterior_coo=noise_count_posterior_coo, + index_converter=index_converter, + n_cells=n_cells, + ) + + # Compute target removal for MAP estimate using regularized posterior. + n, g = index_converter.get_ng_indices(m_inds=posterior_subset_coo.row) + included_cells = set(np.unique(n)) + excluded_barcode_inds = set(range(raw_count_matrix.shape[0])) - included_cells + raw_count_csr_for_cells = csr_set_rows_to_zero(csr=raw_count_matrix, + row_inds=excluded_barcode_inds) + # print(raw_count_csr_for_cells) + logger.debug('Computing target removal') + target_fun = compute_mean_target_removal_as_function( + noise_count_posterior_coo=posterior_subset_coo, + noise_offsets=noise_offsets, + index_converter=index_converter, + raw_count_csr_for_cells=raw_count_csr_for_cells, + n_cells=len(included_cells), + device=device, + per_gene=per_gene, + ) + target_removal = target_fun(fpr) * len(included_cells) + logger.debug(f'Target removal is {target_removal}') + + # Find the posterior regularization factor(s). + if per_gene: + logger.debug('Computing optimal posterior regularization factors for each gene') + shape = index_converter.total_n_genes + else: + logger.debug('Computing optimal posterior regularization factor') + shape = 1 + beta = PRmu._binary_search_for_posterior_regularization_factor( + noise_count_posterior_coo=posterior_subset_coo, + noise_offsets=noise_offsets, + index_converter=index_converter, + target_removal=target_removal, + device=device, + target_tolerance=target_tolerance, + shape=shape, + ) + logger.debug(f'Optimal posterior regularization factor\n{beta}') + + # Compute the posterior using the regularization factor(s). + logger.debug('Computing full regularized posterior') + regularized_noise_posterior_coo = PRmu._chunked_compute_regularized_posterior( + noise_count_posterior_coo=noise_count_posterior_coo, + noise_offsets=noise_offsets, + index_converter=index_converter, + beta=beta, + device=device, + ) + + return regularized_noise_posterior_coo + + +class IndexConverter: + + def __init__(self, total_n_cells: int, total_n_genes: int): + """Convert between (n, g) indices and flattened 'm' indices + + Args: + total_n_cells: Total rows in the full sparse matrix + total_n_genes: Total columns in the full sparse matrix + + """ + self.total_n_cells = total_n_cells + self.total_n_genes = total_n_genes + self.matrix_shape = (total_n_cells, total_n_genes) + + def __repr__(self): + return (f'IndexConverter with' + f'\n\ttotal_n_cells: {self.total_n_cells}' + f'\n\ttotal_n_genes: {self.total_n_genes}' + f'\n\tmatrix_shape: {self.matrix_shape}') + + def get_m_indices(self, cell_inds: np.ndarray, gene_inds: np.ndarray) -> np.ndarray: + """Given arrays of cell indices and gene indices, suitable for a sparse matrix, + convert them to 'm' index values. + """ + if not ((cell_inds >= 0) & (cell_inds < self.total_n_cells)).all(): + raise ValueError(f'Requested cell_inds out of range: ' + f'{cell_inds[(cell_inds < 0) | (cell_inds >= self.total_n_cells)]}') + if not ((gene_inds >= 0) & (gene_inds < self.total_n_genes)).all(): + raise ValueError(f'Requested gene_inds out of range: ' + f'{gene_inds[(gene_inds < 0) | (gene_inds >= self.total_n_genes)]}') + return cell_inds * self.total_n_genes + gene_inds + + def get_ng_indices(self, m_inds: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Given a list of 'm' index values, return two arrays: cell index values + and gene index values, suitable for a sparse matrix. + """ + if not ((m_inds >= 0) & (m_inds < self.total_n_cells * self.total_n_genes)).all(): + raise ValueError(f'Requested m_inds out of range: ' + f'{m_inds[(m_inds < 0) | (m_inds >= self.total_n_cells * self.total_n_genes)]}') + return np.divmod(m_inds, self.total_n_genes) + + +def compute_mean_target_removal_as_function(noise_count_posterior_coo: sp.coo_matrix, + noise_offsets: Dict[int, int], + index_converter: IndexConverter, + raw_count_csr_for_cells: sp.csr_matrix, + n_cells: int, + device: str, + per_gene: bool) -> Callable[[float], torch.Tensor]: + """Given the noise count posterior, return a function that computes target + removal (either overall or per-gene) as a function of FPR. + + NOTE: computes the value "per cell", i.e. dividing + by the number of cells, so that total removal can be computed by + multiplying this by the number of cells in question. + + Args: + noise_count_posterior_coo: Noise count posterior log prob COO + noise_offsets: Offset noise counts per 'm' index + index_converter: IndexConverter object from 'm' to (n, g) and back + raw_count_csr_for_cells: The input count matrix for only the cells + included in the posterior + n_cells: Number of cells included in the posterior, same number as in + raw_count_csr_for_cells + device: 'cpu' or 'cuda' + per_gene: True to come up with one target per gene + + Returns: + target_removal_scaled_per_cell: Noise count removal target + + """ + + # TODO: s1.h5 with FPR 0.99 only removes 50% of signal + + # Compute the expected noise using mean summarization. + estimator = Mean(index_converter=index_converter) + mean_noise_csr = estimator.estimate_noise( + noise_log_prob_coo=noise_count_posterior_coo, + noise_offsets=noise_offsets, + device=device, + ) + logger.debug(f'Total counts in raw matrix for cells = {raw_count_csr_for_cells.sum()}') + logger.debug(f'Total noise counts from mean noise estimator = {mean_noise_csr.sum()}') + + # Compute the target removal. + approx_signal_csr = raw_count_csr_for_cells - mean_noise_csr + logger.debug(f'Approximate signal has total counts = {approx_signal_csr.sum()}') + logger.debug(f'Number of cells = {n_cells}') + + def _target_fun(fpr: float) -> torch.Tensor: + """The function which gets returned""" + if per_gene: + target = np.array(mean_noise_csr.sum(axis=0)).squeeze() + target = target + fpr * np.array(approx_signal_csr.sum(axis=0)).squeeze() + else: + target = mean_noise_csr.sum() + target = target + fpr * approx_signal_csr.sum() + + # Return target scaled to be per-cell. + return torch.tensor(target / n_cells).to(device) + + return _target_fun + + +@torch.no_grad() +def torch_binary_search( + evaluate_outcome_given_value: Callable[[torch.Tensor], torch.Tensor], + target_outcome: torch.Tensor, + init_range: torch.Tensor, + target_tolerance: Optional[float] = 0.001, + max_iterations: int = consts.POSTERIOR_REG_SEARCH_MAX_ITER, + debug: bool = False, +) -> torch.Tensor: + """Perform a binary search, given a target and an evaluation function. + + NOTE: evaluate_outcome_given_value(value) should increase monotonically + with the input value. It is assumed that + consts.POSTERIOR_REG_MIN < output_value < consts.POSTERIOR_REG_MAX. + If this is not the case, the algorithm will produce an output close to one + of those endpoints, and target_tolerance will not be achieved. + Moreover, output_value must be positive (due to how we search for limits). + + Args: + evaluate_outcome_given_value: Function that takes a value as its + input and produces the outcome, which is the target we are + trying to control. Should increase monotonically with value. + target_outcome: Desired outcome value from evaluate_outcome_given_value(value). + init_range: Search range, for each value. + target_tolerance: Tolerated error in the target value. + max_iterations: A cutoff to ensure termination. Even if a tolerable + solution is not found, the algorithm will stop after this many + iterations and return the best answer so far. + debug: Print debugging messages. + + Returns: + value: Result of binary search. Same shape as init_value. + + """ + + logger.debug('Binary search commencing') + + assert (target_tolerance > 0), 'target_tolerance should be > 0.' + assert len(init_range.shape) > 1, 'init_range must be at least two-dimensional ' \ + '(last dimension contains lower and upper bounds)' + assert init_range.shape[-1] == 2, 'Last dimension of init_range should be 2: low and high' + + value_bracket = init_range.clone() + + # Binary search algorithm. + for i in range(max_iterations): + + logger.debug(f'Binary search limits [batch_dim=0, :]: ' + f'{value_bracket.reshape(-1, value_bracket.shape[-1])[0, :]}') + + # Current test value. + value = value_bracket.mean(dim=-1) + + # Calculate an expected false positive rate for this lam_mult value. + outcome = evaluate_outcome_given_value(value) + residual = target_outcome - outcome + + # Check on residual and update our bracket values. + stop_condition = (residual.abs() < target_tolerance).all() + if stop_condition: + break + else: + value_bracket[..., 0] = torch.where(outcome < target_outcome - target_tolerance, + value, + value_bracket[..., 0]) + value_bracket[..., 1] = torch.where(outcome > target_outcome + target_tolerance, + value, + value_bracket[..., 1]) + + # If we stopped due to iteration limit, take the average value. + if i == max_iterations: + value = value_bracket.mean(dim=-1) + logger.warning(f'Binary search target not achieved in {max_iterations} attempts. ' + f'Output is estimated to be {outcome.mean().item():.4f}') + + # Warn if we railed out at the limits of the search + if debug: + if (value - target_tolerance <= init_range[..., 0]).sum() > 0: + logger.debug(f'{(value - target_tolerance <= init_range[..., 0]).sum()} ' + f'entries in the binary search hit the lower limit') + logger.debug(value[value - target_tolerance <= init_range[..., 0]]) + if (value + target_tolerance >= init_range[..., 1]).sum() > 0: + logger.debug(f'{(value + target_tolerance >= init_range[..., 1]).sum()} ' + f'entries in the binary search hit the upper limit') + logger.debug(value[value + target_tolerance >= init_range[..., 1]]) + + return value + + +def restore_from_checkpoint(tarball_name: str, input_file: str) \ + -> Tuple['SingleCellRNACountsDataset', 'RemoveBackgroundPyroModel', Posterior]: + """Convenience function not used by the codebase""" + + d = load_checkpoint(filebase=None, tarball_name=tarball_name) + d.update(load_from_checkpoint(filebase=None, tarball_name=tarball_name, to_load=['posterior'])) + d['args'].input_file = input_file + + dataset_obj = get_dataset_obj(args=d['args']) + + posterior = Posterior( + dataset_obj=dataset_obj, + vi_model=d['model'], + ) + posterior.load(file=d['posterior_file']) + return dataset_obj, d['model'], posterior diff --git a/cellbender/remove_background/report.ipynb b/cellbender/remove_background/report.ipynb new file mode 100644 index 0000000..b091c1b --- /dev/null +++ b/cellbender/remove_background/report.ipynb @@ -0,0 +1,133 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CellBender `remove-background` report" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This output report from `cellbender remove-background` contains a summary of the run, including counts remaining, counts removed, further analyses, and any warnings or suggestions if the run seems to be abnormal.\n", + "\n", + "This HTML report is created from a jupyter notebook at \n", + "\n", + "`cellbender/cellbender/remove-background/report.ipynb`\n", + "\n", + "within the CellBender codebase. Feel free to run the notebook yourself and make any changes you see fit, or use it as a starting point for further analyses." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*The commentary in this report is generated using automated heuristics and best guesses based on hundreds of real datasets. If any of the automated commentary in this report seems incorrect for your dataset, please submit a question or an issue at our github repository https://github.com/broadinstitute/CellBender*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Cellarium Lab .. Methods Group .. Data Sciences Platform .. Broad Institute" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "-----------" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from cellbender.remove_background.report import generate_summary_plots" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Input and output files" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "(Modify this section if you run this notebook yourself.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Allows us to get the filenames without needing to rewrite this notebook\n", + "\n", + "import os\n", + "input_file = os.environ['INPUT_FILE']\n", + "output_file = os.environ['OUTPUT_FILE']\n", + "\n", + "# For the case of a simulated dataset where we have the ground truth\n", + "try:\n", + " truth_file = os.environ['TRUTH_FILE']\n", + "except KeyError:\n", + " truth_file = None\n", + "\n", + "print(f'Input file: {input_file}')\n", + "print(f'Output file: {output_file}')\n", + "if truth_file is not None:\n", + " print(f'Simulated truth file: {truth_file}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Report" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "generate_summary_plots(input_file=input_file, \n", + " output_file=output_file,\n", + " truth_file=truth_file)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/cellbender/remove_background/report.py b/cellbender/remove_background/report.py new file mode 100644 index 0000000..f1a34cb --- /dev/null +++ b/cellbender/remove_background/report.py @@ -0,0 +1,1888 @@ +"""Functions for creation of an HTML report that plots and explains output.""" + +from cellbender.remove_background.downstream import \ + load_anndata_from_input, \ + load_anndata_from_input_and_output, \ + _load_anndata_from_input_and_decontx +from cellbender.base_cli import get_version +from cellbender.remove_background import consts +import matplotlib.pyplot as plt +import numpy as np +import torch +import scipy.sparse as sp +import scipy.stats +from IPython.display import display, Markdown, HTML + +import subprocess +import datetime +import os +import logging +from typing import Dict, Optional + + +logger = logging.getLogger('cellbender') +warnings = [] +TIMEOUT = 1200 # twenty minutes should always be way more than enough + +# counteract an error when I run locally +# https://stackoverflow.com/questions/53014306/error-15-initializing-libiomp5-dylib-but-found-libiomp5-dylib-already-initial +os.environ['KMP_DUPLICATE_LIB_OK'] = 'True' + +run_notebook_str = lambda file: \ + f'jupyter nbconvert ' \ + f'--ExecutePreprocessor.timeout={TIMEOUT} ' \ + f'--to notebook ' \ + f'--allow-errors ' \ + f'--execute {file}' +to_html_str = lambda file, output: \ + f'jupyter nbconvert ' \ + f'--to html ' \ + f'--TemplateExporter.exclude_input=True ' \ + f'{file}' + + +def _run_notebook(file): + subprocess.run(f'cp {file} tmp.report.ipynb', shell=True) + subprocess.run(run_notebook_str(file='tmp.report.ipynb'), shell=True) + subprocess.run(f'rm tmp.report.ipynb', shell=True) + return 'tmp.report.nbconvert.ipynb' + + +def _to_html(file, output) -> str: + subprocess.run(to_html_str(file=file, output=output), shell=True) + subprocess.run(f'mv {file.replace(".ipynb", ".html")} {output}', shell=True) + subprocess.run(f'rm {file}', shell=True) + return output + + +def _postprocess_html(file: str, title: str): + with open(file, mode='r') as f: + html = f.read() + html = html.replace('tmp.report.nbconvert', + f'{title}') + with open(file, mode='w') as f: + f.write(html) + + +def run_notebook_make_html(file, output) -> str: + """Run Jupyter notebook to populate report and then convert to HTML. + + Args: + file: Notebook file + output: Output file. Should end in ".html" + + Returns: + output: Output file + + """ + assert output.endswith('.html'), 'Output HTML filename should end with .html' + html_file = _to_html(file=_run_notebook(file), output=output) + _postprocess_html( + file=html_file, + title=('CellBender: ' + os.path.basename(output).replace('_report.html', '')), + ) + return html_file + + +def generate_summary_plots(input_file: str, + output_file: str, + truth_file: Optional[Dict] = None, + dev_mode: bool = consts.EXTENDED_REPORT): + """Read in cellbender's output file and generate summary plots. + + Args: + input_file: Raw CellRanger + + """ + + global warnings + warnings = [] + + display(Markdown(f'### CellBender version {get_version()}')) + display(Markdown(str(datetime.datetime.now()).split('.')[0])) + display(Markdown(f'# {os.path.basename(output_file)}')) + + # load datasets, before and after CellBender + input_layer_key = 'raw' + if os.path.isdir(output_file): + adata = _load_anndata_from_input_and_decontx(input_file=input_file, + output_mtx_directory=output_file, + input_layer_key=input_layer_key, + truth_file=truth_file) + out_key = 'decontx' + else: + adata = load_anndata_from_input_and_output(input_file=input_file, + output_file=output_file, + analyzed_barcodes_only=True, + input_layer_key=input_layer_key, + truth_file=truth_file) + out_key = 'cellbender' + + # need to make any duplicate var indices unique (for pandas manipulations) + adata.var_names_make_unique() + + display(Markdown('## Loaded dataset')) + print(adata) + + # bit of pre-compute + cells = (adata.obs['cell_probability'] > consts.CELL_PROB_CUTOFF) + adata.var['n_removed'] = adata.var[f'n_{input_layer_key}'] - adata.var[f'n_{out_key}'] + adata.var['fraction_removed'] = adata.var['n_removed'] / (adata.var[f'n_{input_layer_key}'] + 1e-5) + adata.var['fraction_remaining'] = adata.var[f'n_{out_key}'] / (adata.var[f'n_{input_layer_key}'] + 1e-5) + adata.var[f'n_{input_layer_key}_cells'] = np.array(adata.layers[input_layer_key][cells].sum(axis=0)).squeeze() + adata.var[f'n_{out_key}_cells'] = np.array(adata.layers[out_key][cells].sum(axis=0)).squeeze() + adata.var['n_removed_cells'] = (adata.var[f'n_{input_layer_key}_cells'] + - adata.var[f'n_{out_key}_cells']) + adata.var['fraction_removed_cells'] = (adata.var['n_removed_cells'] + / (adata.var[f'n_{input_layer_key}_cells'] + 1e-5)) + adata.var['fraction_remaining_cells'] = (adata.var[f'n_{out_key}_cells'] + / (adata.var[f'n_{input_layer_key}_cells'] + 1e-5)) + + # this inline command is necessary after cellbender imports + plt.rcParams.update({'font.size': 12}) + + # input UMI curve + raw_full_adata = plot_input_umi_curve(input_file) + + # prove that remove-background is only subtracting counts, never adding + if out_key == 'cellbender': + assert (adata.layers[input_layer_key] < adata.layers[out_key]).sum() == 0, \ + "There is an entry in the output greater than the input" + else: + if (adata.layers[input_layer_key] < adata.layers[out_key]).sum() == 0: + display(Markdown('WARNING: There is an entry in the output greater than the input')) + + display(Markdown('## Examine how many counts were removed in total')) + try: + assess_overall_count_removal(adata, raw_full_adata=raw_full_adata, out_key=out_key) + except ValueError: + display(Markdown('Skipping assessment over overall count removal. Presumably ' + 'this is due to including the whole dataset in ' + '--total-droplets-included.')) + + # plot learning curve + if out_key == 'cellbender': + try: + assess_learning_curve(adata) + except Exception: + pass + else: + display(Markdown('Skipping learning curve assessment.')) + + # look at per-gene count removal + assess_count_removal_per_gene(adata, raw_full_adata=raw_full_adata, extended=dev_mode) + + if dev_mode: + display(Markdown('## Histograms of counts per cell for several genes')) + plot_gene_removal_histograms(adata, out_layer_key=out_key) + display(Markdown('Typically we see that some of the low-count cells have ' + 'their counts removed, since they were background noise.')) + + # plot UMI curve and cell probabilities + if out_key == 'cellbender': + display(Markdown('## Cell probabilities')) + display(Markdown('The inferred posterior probability ' + 'that each droplet is non-empty.')) + display(Markdown('*We sometimes write "non-empty" ' + 'instead of "cell" because dead cells and other cellular ' + 'debris can still lead to a "non-empty" droplet, which will ' + 'have a high posterior cell probability. But these ' + 'kinds of low-quality droplets should be removed during ' + 'cell QC to retain only high-quality cells for downstream ' + 'analyses.*')) + plot_counts_and_probs_per_cell(adata) + else: + display(Markdown('Skipping cell probability assessment.')) + + # concordance of data before and after + display(Markdown('## Concordance of data before and after `remove-background`')) + plot_validation_plots(adata, output_layer_key=out_key, extended=dev_mode) + + # PCA of gene expression + if out_key == 'cellbender': + display(Markdown('## PCA of encoded gene expression')) + plot_gene_expression_pca(adata, extended=dev_mode) + else: + display(Markdown('Skipping gene expression embedding assessment.')) + + # "mixed species" plots + mixed_species_plots(adata, input_layer_key=input_layer_key, output_layer_key=out_key) + + if dev_mode and (truth_file is not None): + + # accuracy plots ========================================== + + display(Markdown('# Comparison with truth data')) + + display(Markdown('## Removal per gene')) + + display(Markdown('Counts per gene are summed over cell-containing droplets')) + plt.figure(figsize=(10, 4)) + plt.subplot(1, 2, 1) + plt.plot(adata.var['n_truth'].values, adata.var[f'n_{out_key}_cells'].values, '.', color='k') + plt.plot([0, adata.var['n_truth'].max()], [0, adata.var['n_truth'].max()], + color='lightgray', alpha=0.5) + plt.xlabel('True counts per gene') + plt.ylabel(f'{out_key} counts per gene') + plt.subplot(1, 2, 2) + logic = (adata.var['n_truth'].values > 0) + plt.plot(adata.var['n_truth'].values[logic], + ((adata.var[f'n_{out_key}_cells'].values[logic] - adata.var['n_truth'].values[logic]) + / adata.var['n_truth'].values[logic]), + '.', color='k') + plt.plot([0, adata.var['n_truth'].max()], [0, 0], + color='lightgray', alpha=0.5) + plt.xlabel('True counts per gene') + plt.ylabel(f'{out_key}: residual count ratio per gene\n({out_key} - truth) / truth') + plt.tight_layout() + plt.show() + + adata.var['fraction_remaining_cells_truth'] = (adata.var[f'n_truth'] + / (adata.var[f'n_{input_layer_key}_cells'] + 1e-5)) + + plt.figure(figsize=(10, 4)) + plt.subplot(1, 2, 1) + plt.semilogx(adata.var[f'n_{input_layer_key}_cells'], + adata.var['fraction_remaining_cells'], 'k.', ms=1) + plt.ylim([-0.05, 1.05]) + plt.xlabel('Number of counts in raw data') + plt.ylabel('Fraction of counts remaining') + plt.title('Genes: removal of counts\nfrom (inferred) cell-containing droplets') + plt.subplot(1, 2, 2) + plt.semilogx(adata.var[f'n_{input_layer_key}_cells'], + adata.var['fraction_remaining_cells_truth'], 'k.', ms=1) + plt.ylim([-0.05, 1.05]) + plt.xlabel('Number of counts in raw data') + plt.ylabel('Truth: fraction of counts remaining') + plt.title('Genes: truth') + plt.tight_layout() + plt.show() + + plt.figure(figsize=(10, 4)) + plt.subplot(1, 2, 1) + plt.semilogx(adata.var[f'n_{input_layer_key}_cells'], + adata.var['fraction_remaining_cells'] - adata.var['fraction_remaining_cells_truth'], + 'k.', ms=1) + plt.ylim([-1.05, 1.05]) + plt.xlabel('Number of counts in raw data') + plt.ylabel('Residual fraction of counts remaining') + plt.title('Genes: residual') + plt.subplot(1, 2, 2) + plt.semilogx(adata.var[f'n_{input_layer_key}_cells'], + adata.var[f'n_{input_layer_key}_cells'] - adata.var['n_truth'], + 'k.', ms=1, label='raw') + plt.semilogx(adata.var[f'n_{input_layer_key}_cells'], + adata.var[f'n_{out_key}_cells'] - adata.var['n_truth'], + 'r.', ms=1, label=f'{out_key}') + plt.legend() + plt.xlabel('Number of counts in raw data') + plt.ylabel('Residual counts remaining') + plt.title('Genes: residual') + plt.tight_layout() + plt.show() + + display(Markdown('## Revisiting histograms of counts per cell')) + display(Markdown('Now showing the truth in addition to the `remove-background` ' + 'output.')) + plot_gene_removal_histograms(adata, plot_truth=True, out_layer_key=out_key) + + # plot z, and comparisons of learned to true gene expression + if out_key == 'cellbender': + cluster_and_compare_expression_to_truth(adata=adata) + else: + display(Markdown('Skipping gene expression embedding assessment.')) + + # gene expression as images: visualize changes + display(Markdown('## Visualization just for fun')) + display(Markdown('This is a strange but somewhat fun way to visualize what ' + 'is going on with the data for each cell. We look at one ' + 'cell at a time, and visualize gene expression as an ' + 'image, where pixels are ordered by their true expression ' + 'in the ambient RNA, where upper left is most expressed ' + 'in ambient. We plot the output gene expression in ' + 'blue/yellow, and then we look at three residuals (in red):' + '\n\n1. (raw - truth): what was supposed to be removed' + '\n2. (raw - posterior): what was actually removed' + '\n3. (truth - posterior): the residual, ' + 'where red means too little was removed and blue means too ' + 'much was removed.')) + try: + show_gene_expression_before_and_after(adata=adata, num=5) + except: + display(Markdown('WARNING: skipped showing gene expression as images, due to an error')) + + if dev_mode: + + # inference of latents + if out_key == 'cellbender': + compare_latents(adata) + else: + display(Markdown('Skipping gene expression embedding assessment.')) + + if truth_file is not None: + + # ROC curves + display(Markdown('## Quantification of performance')) + display(Markdown('Here we take a look at removal of noise counts from cell ' + 'containing droplets only.')) + true_fpr = cell_roc_count_roc( + output_csr=adata.layers[out_key], + input_csr=adata.layers[input_layer_key], + truth_csr=adata.layers['truth'], + cell_calls=(adata.obs['cell_probability'] > 0.5), + truth_cell_labels=adata.obs['truth_cell_label'], + ) + if type(adata.uns['target_false_positive_rate'][0]) == np.float64: + if true_fpr > adata.uns['target_false_positive_rate'][0]: + warnings.append('FPR exceeds target FPR.') + display(Markdown(f'WARNING: FPR of {true_fpr:.4f} exceeds target FPR of ' + f'{adata.uns["target_false_positive_rate"]}. Keep ' + f'in mind however that the target FPR is meant to ' + f'target false positives over and above some ' + f'basal level (dataset dependent), so the ' + f'measured FPR should exceed the target by some ' + f'amount.')) + + display(Markdown('# Summary of warnings:')) + if len(warnings) == 0: + display(Markdown('None.')) + else: + for warning in warnings: + display(Markdown(warning)) + + +def plot_input_umi_curve(inputfile): + adata = load_anndata_from_input(inputfile) + plt.loglog(sorted(np.array(adata.X.sum(axis=1)).squeeze(), reverse=True)) + plt.xlabel('Ranked Barcode ID') + plt.ylabel('UMI counts') + plt.title(f'UMI curve\nRaw input data: {os.path.basename(inputfile)}') + plt.show() + return adata + + +def assess_overall_count_removal(adata, raw_full_adata, input_layer_key='raw', out_key='cellbender'): + global warnings + cells = (adata.obs['cell_probability'] > 0.5) + initial_counts = adata.layers[input_layer_key][cells].sum() + removed_counts = initial_counts - adata.layers[out_key][cells].sum() + removed_percentage = removed_counts / initial_counts * 100 + print(f'removed {removed_counts:.0f} counts from non-empty droplets') + print(f'removed {removed_percentage:.2f}% of the counts in non-empty droplets') + + from scipy.stats import norm + + log_counts = np.log10(np.array(adata.layers[input_layer_key].sum(axis=1)).squeeze()) + empty_log_counts = np.array(raw_full_adata.X[ + [bc not in adata.obs_names + for bc in raw_full_adata.obs_names] + ].sum(axis=1)).squeeze() + empty_log_counts = np.log10(empty_log_counts[empty_log_counts > consts.LOW_UMI_CUTOFF]) + bins = np.linspace(empty_log_counts.min(), log_counts.max(), 100) + # binwidth = bins[1] - bins[0] + + # def plot_normal_fit(x, loc, scale, n, label): + # plt.plot(x, n * binwidth * norm.pdf(x=x, loc=loc, scale=scale), label=label) + + plt.hist(empty_log_counts.tolist() + log_counts[~cells].tolist(), + histtype='step', label='empty droplets', bins=bins) + plt.hist(log_counts[cells], histtype='step', label='non-empty droplets', bins=bins) + xx = np.linspace(plt.gca().get_xlim()[0], plt.gca().get_xlim()[-1], 100) + # if 'cell_size' in adata.obs.keys(): + # plt.hist(np.log10(adata.obs['cell_size'][cells]), + # histtype='step', label='inferred cell sizes', bins=bins) + # plot_normal_fit(x=xx, + # loc=np.log10(adata.obs['cell_size'][cells]).mean(), + # scale=np.log10(adata.obs['cell_size'][cells]).std(), + # n=cells.sum(), + # label='inferred cell sizes') + # if (('empty_droplet_size_lognormal_loc' in adata.uns.keys()) + # and ('empty_droplet_size_lognormal_scale' in adata.uns.keys())): + # plot_normal_fit(x=xx, + # loc=np.log10(np.exp(adata.uns['empty_droplet_size_lognormal_loc'])), + # scale=np.log10(np.exp(adata.uns['empty_droplet_size_lognormal_scale'])), + # n=(~cells).sum() + len(empty_log_counts), + # label='inferred empty sizes') + plt.ylabel('Number of droplets') + plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) + plt.ylim(bottom=1) + plt.yscale('log') + # x-axis log10 to regular number + plt.xticks(plt.gca().get_xticks(), + [f'{n:.0f}' for n in np.power(10, plt.gca().get_xticks())], rotation=90) + plt.xlim(left=empty_log_counts.min()) + plt.xlabel('UMI counts per droplet') + plt.show() + + estimated_ambient_per_droplet = np.exp(adata.uns['empty_droplet_size_lognormal_loc']).item() + expected_fraction_removed_from_cells = estimated_ambient_per_droplet * cells.sum() / initial_counts + + fpr = adata.uns['target_false_positive_rate'].item() # this is an np.ndarray with one element + cohort_mode = False + if type(fpr) != float: + cohort_mode = True + + print('Rough estimate of expectations based on nothing but the plot above:') + print(f'roughly {estimated_ambient_per_droplet * cells.sum():.0f} noise counts ' + f'should be in non-empty droplets') + print(f'that is approximately {expected_fraction_removed_from_cells * 100:.2f}% of ' + f'the counts in non-empty droplets') + + if not cohort_mode: + expected_percentage = (expected_fraction_removed_from_cells + fpr) * 100 + print(f'with a false positive rate [FPR] of {fpr * 100}%, we would expect to remove about ' + f'{expected_percentage:.2f}% of the counts in non-empty droplets') + else: + expected_percentage = expected_fraction_removed_from_cells * 100 + print(f'ran in cohort mode, so no false positive rate [FPR] target was set, ' + f'but we would still expect to remove about ' + f'{expected_percentage:.2f}% of the counts in non-empty droplets') + + display(Markdown('\n')) + + if np.abs(expected_percentage - removed_percentage) <= 0.5: + display(Markdown('It looks like the algorithm did a great job meeting that expectation.')) + elif np.abs(expected_percentage - removed_percentage) <= 1: + display(Markdown('It looks like the algorithm did a decent job meeting that expectation.')) + elif np.abs(expected_percentage - removed_percentage) <= 5: + if removed_percentage < expected_percentage: + display(Markdown('The algorithm removed a bit less than naive expectations ' + 'would indicate, but this is likely okay. If removal ' + 'seems insufficient, the FPR can be increased.')) + else: + display(Markdown('The algorithm removed a bit more than naive expectations ' + 'would indicate, but this is likely okay. Spot-check ' + 'removal of a few top-removed genes as a QC measure. ' + 'If less removal is desired, decrease the FPR.')) + elif removed_percentage - expected_percentage > 5: + display(Markdown('The algorithm seems to have removed more overall counts ' + 'than would be naively expected.')) + warnings.append('Algorithm removed more counts overall than naive expectations.', ) + elif expected_percentage - removed_percentage > 5: + display(Markdown('The algorithm seems to have removed fewer overall counts ' + 'than would be naively expected.')) + warnings.append('Algorithm removed fewer counts overall than naive expectations.', ) + + +def assess_learning_curve(adata, + spike_size: float = 0.5, + deviation_size: float = 0.25, + monotonicity_cutoff: float = 0.1): + global warnings + display(Markdown('## Assessing convergence of the algorithm')) + plot_learning_curve(adata) + if 'learning_curve_train_elbo' not in adata.uns.keys(): + return + display(Markdown( + '*The learning curve tells us about the progress of the algorithm in ' + 'inferring all the latent variables in our model. We want to see ' + 'the ELBO increasing as training epochs increase. Generally it is ' + 'desirable for the ELBO to converge at some high plateau, and be fairly ' + 'stable.*')) + display(Markdown( + '*What to watch out for:*' + )) + display(Markdown( + '*1. large downward spikes in the ELBO (of value more than a few hundred)*\n' + '*2. the test ELBO can be smaller than the train ELBO, but generally we ' + 'want to see both curves increasing and reaching a stable plateau. We ' + 'do not want the test ELBO to dip way back down at the end.*\n' + '*3. lack of convergence, where it looks like the ELBO would change ' + 'quite a bit if training went on for more epochs.*' + )) + + if adata.uns['learning_curve_train_epoch'][-1] < 50: + display(Markdown('Short run. Will not analyze the learning curve.')) + warnings.append(f'Short run of only {adata.uns["learning_curve_train_epoch"][-1]} epochs') + return + + train_elbo_min_max = np.percentile(adata.uns['learning_curve_train_elbo'], q=[5, 95]) + train_elbo_range = train_elbo_min_max.max() - train_elbo_min_max.min() + + # look only from epoch 45 onward for spikes in train ELBO + large_spikes_in_train = np.any((adata.uns['learning_curve_train_elbo'][46:] + - adata.uns['learning_curve_train_elbo'][45:-1]) + < -train_elbo_range * spike_size) + + second_half_train_elbo = (adata.uns['learning_curve_train_elbo'] + [(len(adata.uns['learning_curve_train_elbo']) // 2):]) + large_deviation_in_train = np.any(second_half_train_elbo + < np.median(second_half_train_elbo) + - train_elbo_range * deviation_size) + + half = len(adata.uns['learning_curve_train_elbo']) // 2 + threequarter = len(adata.uns['learning_curve_train_elbo']) * 3 // 4 + typical_end_variation = np.std(adata.uns['learning_curve_train_elbo'][half:threequarter]) + low_end_in_train = (adata.uns['learning_curve_train_elbo'][-1] + < adata.uns['learning_curve_train_elbo'].max() - 5 * typical_end_variation) + + # look only from epoch 45 onward for spikes in train ELBO + non_monotonicity = ((adata.uns['learning_curve_train_elbo'][46:] + - adata.uns['learning_curve_train_elbo'][45:-1]) + < -3 * typical_end_variation).sum() / len(adata.uns['learning_curve_train_elbo']) + non_monotonic = (non_monotonicity > monotonicity_cutoff) + + def windowed_cumsum(x, n=20): + return np.array([np.cumsum(x[i:(i + n)])[-1] for i in range(len(x) - n)]) + + windowsize = 20 + tracking_trace = windowed_cumsum(adata.uns['learning_curve_train_elbo'][1:] + - adata.uns['learning_curve_train_elbo'][:-1], + n=windowsize) + big_dip = -1 * (adata.uns['learning_curve_train_elbo'][-1] + - adata.uns['learning_curve_train_elbo'][5]) / 10 + backtracking = (tracking_trace.min() < big_dip) + backtracking_ind = np.argmin(tracking_trace) + windowsize + + halftest = len(adata.uns['learning_curve_test_elbo']) // 2 + threequartertest = len(adata.uns['learning_curve_test_elbo']) * 3 // 4 + typical_end_variation_test = np.std(adata.uns['learning_curve_test_elbo'][halftest:threequartertest]) + runaway_test = (adata.uns['learning_curve_test_elbo'][-1] + < adata.uns['learning_curve_test_elbo'].max() - 4 * typical_end_variation_test) + + non_convergence = (np.mean([adata.uns['learning_curve_train_elbo'][-1] + - adata.uns['learning_curve_train_elbo'][-2], + adata.uns['learning_curve_train_elbo'][-2] + - adata.uns['learning_curve_train_elbo'][-3]]) + > 2 * typical_end_variation) + + display(Markdown('**Automated assessment** --------')) + if large_spikes_in_train: + warnings.append('Large spikes in training ELBO.') + display(Markdown('- *WARNING*: Large spikes detected in the training ELBO.')) + if large_deviation_in_train: + warnings.append('Large deviation in training ELBO from max value late in learning.') + display(Markdown('- *WARNING*: The training ELBO deviates quite a bit from ' + 'the max value during the second half of training.')) + if low_end_in_train: + warnings.append('Large deviation in training ELBO from max value at end.') + display(Markdown('- The training ELBO deviates quite a bit from ' + 'the max value at the last epoch.')) + if non_monotonic: + warnings.append('Non-monotonic training ELBO.') + display(Markdown('- We typically expect to see the training ELBO increase almost ' + 'monotonically. This curve seems to have a lot more downward ' + 'motion than we like to see.')) + if backtracking: + warnings.append('Back-tracking in training ELBO.') + display(Markdown('- We typically expect to see the training ELBO increase almost ' + 'monotonically. This curve seems to have a concerted ' + f'period of motion in the wrong direction near epoch {backtracking_ind}. ' + f'If this is early in training, this is probably okay.')) + if runaway_test: + warnings.append('Final test ELBO is much lower than the max test ELBO.') + display(Markdown('- We hope to see the test ELBO follow the training ELBO, ' + 'increasing almost monotonically (though there will be ' + 'deviations, and that is expected). There may be a large ' + 'gap, and that is okay. However, this curve ' + 'ends with a low test ELBO compared to the max test ELBO ' + 'value during training. The output could be suboptimal.')) + if non_convergence: + warnings.append('Non-convergence of training ELBO.') + display(Markdown('- We typically expect to see the training ELBO come to a ' + 'stable plateau value near the end of training. Here ' + 'the training ELBO is still moving quite a bit.')) + + display(Markdown('**Summary**:')) + if large_spikes_in_train or large_deviation_in_train: + display(Markdown('This is unusual behavior, and a reduced --learning-rate ' + 'is indicated. Re-run with half the current learning ' + 'rate and compare the results.')) + elif low_end_in_train or non_monotonic or runaway_test: + display(Markdown('This is slightly unusual behavior, and a reduced ' + '--learning-rate might be indicated. Consider re-running ' + 'with half the current learning rate to compare the results.')) + elif non_convergence: + display(Markdown('This is slightly unusual behavior, and more training ' + '--epochs might be indicated. Consider re-running ' + 'for more epochs to compare the results.')) + else: + display(Markdown('This learning curve looks normal.')) + + +def plot_learning_curve(adata): + + if 'learning_curve_train_elbo' not in adata.uns.keys(): + print('No learning curve recorded!') + return + + def _mkplot(): + plt.plot(adata.uns['learning_curve_train_epoch'], + adata.uns['learning_curve_train_elbo'], label='train') + try: + plt.plot(adata.uns['learning_curve_test_epoch'], + adata.uns['learning_curve_test_elbo'], '.:', label='test') + plt.legend() + except Exception: + pass + plt.title('Learning curve') + plt.ylabel('ELBO') + plt.xlabel('Epoch') + + if len(adata.uns['learning_curve_train_elbo']) > 20: + + # two panels: zoom on the right-hand side + plt.figure(figsize=(10, 4)) + plt.subplot(1, 2, 1) + _mkplot() + plt.subplot(1, 2, 2) + _mkplot() + plt.title('Learning curve (zoomed in)') + low = np.percentile(adata.uns['learning_curve_train_elbo'], q=10) + if (len(adata.uns['learning_curve_train_elbo']) > 0) \ + and (len(adata.uns['learning_curve_test_elbo']) > 0): + high = max(adata.uns['learning_curve_train_elbo'].max(), + adata.uns['learning_curve_test_elbo'].max()) + else: + high = adata.uns['learning_curve_train_elbo'].max() + plt.ylim([low, high + (high - low) / 10]) + plt.tight_layout() + plt.show() + + else: + _mkplot() + plt.show() + + +def assess_count_removal_per_gene(adata, + raw_full_adata, + input_layer_key='raw', + r_squared_cutoff=0.5, + extended=True): + + global warnings + display(Markdown('## Examine count removal per gene')) + + # how well does it correlate with our expectation about the ambient RNA profile? + cells = (adata.obs['cell_probability'] > 0.5) + counts = np.array(raw_full_adata.X.sum(axis=1)).squeeze() + clims = [adata.obs[f'n_{input_layer_key}'][~cells].mean() / 2, + np.percentile(adata.obs[f'n_{input_layer_key}'][cells].values, q=2)] + # if all are called "cells" then clims[0] will be a nan + if np.isnan(clims[0]): + clims[0] = counts.min() + if 'approximate_ambient_profile' in adata.uns.keys(): + approximate_ambient_profile = adata.uns['approximate_ambient_profile'] + else: + empty_count_matrix = raw_full_adata[(counts > clims[0]) & (counts < clims[1])].X + if empty_count_matrix.shape[0] > 100: + approximate_ambient_profile = np.array(raw_full_adata[(counts > clims[0]) + & (counts < clims[1])].X.mean(axis=0)).squeeze() + else: + # a very rare edge case I've seen once + display(Markdown('Having some trouble finding the empty droplets via heuristics. ' + 'The "approximate background estimated from empty droplets" may be inaccurate.')) + approximate_ambient_profile = np.array(raw_full_adata[counts < clims[1]].X.mean(axis=0)).squeeze() + approximate_ambient_profile = approximate_ambient_profile / approximate_ambient_profile.sum() + y = adata.var['n_removed'] / adata.var['n_removed'].sum() + maxval = (approximate_ambient_profile / approximate_ambient_profile.sum()).max() + + def _plot_identity(maxval): + plt.plot([0, maxval], [0, maxval], 'lightgray') + + if extended: + + plt.figure(figsize=(12, 4)) + plt.subplot(1, 2, 1) + plt.plot(approximate_ambient_profile, adata.var['ambient_expression'], '.', ms=2) + _plot_identity(maxval) + plt.xlabel('Approximate background per gene\nestimated from empty droplets') + plt.ylabel('Inferred ambient profile') + plt.title('Genes: inferred ambient') + + plt.subplot(1, 2, 2) + plt.plot(approximate_ambient_profile, y, '.', ms=2) + _plot_identity(maxval) + plt.xlabel('Approximate background per gene\nestimated from empty droplets') + plt.ylabel('Removal per gene') + plt.title('Genes: removal') + plt.tight_layout() + plt.show() + + else: + + plt.plot(approximate_ambient_profile, y, '.', ms=2) + _plot_identity(maxval) + plt.xlabel('Approximate background per gene\nestimated from empty droplets') + plt.ylabel('Removal per gene') + plt.title('Genes: removal') + plt.tight_layout() + plt.show() + + cutoff = 1e-6 + logic = np.logical_not((approximate_ambient_profile < cutoff) | (y < cutoff)) + r_squared_result = scipy.stats.pearsonr(np.log(approximate_ambient_profile[logic]), + np.log(y[logic])) + if hasattr(r_squared_result, 'statistic'): + # scipy version 1.9.0+ + r_squared = r_squared_result.statistic + else: + r_squared = r_squared_result[0] + display(Markdown(f'Pearson correlation coefficient for the above is {r_squared:.4f}')) + if r_squared > r_squared_cutoff: + display(Markdown('This meets expectations.')) + else: + warnings.append('Per-gene removal does not closely match a naive estimate ' + 'of ambient RNA from empty droplets. Does it look like ' + 'CellBender correctly identified the empty droplets?') + display(Markdown('WARNING: This deviates from expectations, and may ' + 'indicate that the run did not go well')) + + percentile = 90 + genecount_lowlim = int(np.percentile(adata.var[f'n_{input_layer_key}'], q=percentile)) + display(Markdown('### Table of top genes removed\n\nRanked by fraction removed, ' + f'and excluding genes with fewer than {genecount_lowlim} ' + f'total raw counts ({percentile}th percentile)')) + df = adata.var[adata.var['cellbender_analyzed']] # exclude omitted features + df = df[[c for c in df.columns if (c != 'features_analyzed_inds')]] + display(HTML(df[df[f'n_{input_layer_key}'] > genecount_lowlim] + .sort_values(by='fraction_removed', ascending=False).head(10).to_html())) + + for g in adata.var[(adata.var[f'n_{input_layer_key}_cells'] > genecount_lowlim) + & (adata.var['fraction_removed'] > 0.8)].index: + warnings.append(f'Expression of gene {g} decreases quite a bit') + display(Markdown(f'**WARNING**: The expression of the highly-expressed ' + f'gene {g} decreases quite markedly after CellBender. ' + f'Check to ensure this makes sense!')) + + if extended: + + plt.figure(figsize=(12, 4)) + plt.subplot(1, 2, 1) + plt.semilogx(adata.var[f'n_{input_layer_key}'], + adata.var['fraction_remaining'], 'k.', ms=1) + plt.ylim([-0.05, 1.05]) + plt.xlabel('Number of counts in raw data') + plt.ylabel('Fraction of counts remaining') + plt.title('Genes: removal of counts\nfrom the entire dataset') + plt.subplot(1, 2, 2) + plt.semilogx(adata.var[f'n_{input_layer_key}_cells'], + adata.var['fraction_remaining_cells'], 'k.', ms=1) + plt.ylim([-0.05, 1.05]) + plt.xlabel('Number of counts in raw data') + plt.ylabel('Fraction of counts remaining') + plt.title('Genes: removal of counts\nfrom (inferred) cell-containing droplets') + plt.show() + + +def plot_counts_and_probs_per_cell(adata, input_layer_key='raw'): + + limit_to_features_analyzed = True + + if limit_to_features_analyzed: + var_logic = adata.var['cellbender_analyzed'] + else: + var_logic = ... + + in_counts = np.array(adata.layers[input_layer_key][:, var_logic].sum(axis=1)).squeeze() + # cellbender_counts = np.array(adata.layers['cellbender'][:, var_logic].sum(axis=1)).squeeze() + order = np.argsort(in_counts)[::-1] + # plt.semilogy(cellbender_counts[order], '.:', ms=3, color='lightgray', alpha=0.5, label='cellbender') + plt.semilogy(in_counts[order], 'k-', lw=1, label=input_layer_key) + plt.xlabel('Sorted barcode ID') + plt.ylabel('Unique UMI counts' + ('\n(for features analyzed by CellBender)' + if limit_to_features_analyzed else '')) + plt.legend(loc='lower left', title='UMI counts') + plt.gca().twinx() + plt.plot(adata.obs['cell_probability'][order].values, '.', ms=2, alpha=0.2, color='red') + plt.ylabel('Inferred cell probability', color='red') + plt.yticks([0, 0.25, 0.5, 0.75, 1.0], color='red') + plt.ylim([-0.05, 1.05]) + plt.show() + + +def plot_validation_plots(adata, input_layer_key='raw', + output_layer_key='cellbender', + extended=True): + + display(Markdown('*The intent is to change the input ' + 'data as little as possible while achieving noise removal. ' + 'These plots show general summary statistics about similarity ' + 'of the input and output data. We expect to see the data ' + 'lying close to a straight line (gray). There may be ' + 'outlier genes/features, which are often those highest-' + 'expressed in the ambient RNA.*')) + display(Markdown('The plots here show data ' + 'for inferred cell-containing droplets, and exclude the ' + 'empty droplets.')) + + cells = (adata.obs['cell_probability'] > 0.5) + + # counts per barcode + plt.figure(figsize=(9, 4)) + plt.subplot(1, 2, 1) + plt.loglog(adata.obs[f'n_{input_layer_key}'][cells].values, + adata.obs[f'n_{output_layer_key}'][cells].values, + '.', ms=3, alpha=0.8, rasterized=True) + minmax = [adata.obs[f'n_{input_layer_key}'][cells].min() / 10, + adata.obs[f'n_{input_layer_key}'][cells].max() * 10] + plt.loglog(minmax, minmax, lw=1, color='gray', alpha=0.5) + plt.xlabel('Input counts per barcode') + plt.ylabel(f'{output_layer_key} counts per barcode') + plt.title('Droplet count concordance\nin inferred cell-containing droplets') + plt.axis('equal') + + # counts per gene + plt.subplot(1, 2, 2) + plt.loglog(adata.var[f'n_{input_layer_key}_cells'], + adata.var[f'n_{output_layer_key}_cells'], + '.', ms=3, alpha=0.8, rasterized=True) + minmax = [adata.var[f'n_{input_layer_key}_cells'].min() / 10, + adata.var[f'n_{input_layer_key}_cells'].max() * 10] + plt.loglog(minmax, minmax, lw=1, color='gray', alpha=0.5) + plt.xlabel('Input counts per gene') + plt.ylabel(f'{output_layer_key} counts per gene') + plt.title('Gene count concordance\nin inferred cell-containing droplets') + plt.axis('equal') + plt.tight_layout() + plt.show() + + cells = (adata.obs['cell_probability'] >= 0.5) + + if extended: + + # Fano factor per barcode + plt.figure(figsize=(9, 4)) + plt.subplot(1, 2, 1) + plt.loglog(fano(adata.layers[input_layer_key][cells], axis=1), + fano(adata.layers[output_layer_key][cells], axis=1), '.', ms=1) + plt.loglog([1e0, 1e3], [1e0, 1e3], lw=1, color='gray', alpha=0.5) + plt.xlabel('Input Fano factor per droplet') + plt.ylabel(f'{output_layer_key} Fano factor per droplet') + plt.title('Droplet count variance\nin inferred cell-containing droplets') + plt.axis('equal') + + # Fano factor per gene + plt.subplot(1, 2, 2) + plt.loglog(fano(adata.layers[input_layer_key][cells], axis=0), + fano(adata.layers[output_layer_key][cells], axis=0), '.', ms=1) + plt.loglog([1e0, 1e3], [1e0, 1e3], lw=1, color='gray', alpha=0.5) + plt.xlabel('Input Fano factor per gene') + plt.ylabel(f'{output_layer_key} Fano factor per gene') + plt.title('Gene count variance\nin inferred cell-containing droplets') + plt.axis('equal') + plt.tight_layout() + plt.show() + + # histogram of per-cell cosine distances + # cells_in_data = latents['barcodes_analyzed_inds'][latents['cell_probability'] >= 0.5] + cosine_dist = [] + for bc in np.random.permutation(np.where(cells)[0])[:300]: + cosine_dist.append(cosine(np.array(adata.layers[input_layer_key][bc, :].todense()).squeeze(), + np.array(adata.layers[output_layer_key][bc, :].todense()).squeeze())) + cosine_dist = np.array(cosine_dist) + plt.hist(cosine_dist, bins=100) + plt.xlabel('Cosine distance between cell before and after') + plt.ylabel('Number of cells') + plt.title('Per-cell expression changes') + plt.show() + + print(f'cosine_dist.mean = {cosine_dist.mean():.4f}') + + display(Markdown('We want this cosine distance to be as small as it can be ' + '(though it cannot be zero, since we are removing counts). ' + 'There is no specific threshold value above which we are ' + 'concerned, but typically we see values below 0.05.')) + + +def plot_gene_removal_histograms(adata, input_layer_key='raw', plot_truth=False, out_layer_key='cellbender'): + order_gene = np.argsort(np.array(adata.var[f'n_{input_layer_key}']))[::-1] + bins = np.arange(50) - 0.5 + + plt.figure(figsize=(14, 6)) + for i, g_ind in enumerate([0, 5, 10, 20, 100, 1000]): + if g_ind >= len(order_gene): + break + plt.subplot(2, 3, i + 1) + plt.hist(np.array(adata.layers[input_layer_key][:, order_gene[g_ind]].todense()).squeeze(), + bins=bins, log=True, label=input_layer_key, histtype='step') + plt.hist(np.array(adata.layers[out_layer_key][:, order_gene[g_ind]].todense()).squeeze(), + bins=bins, log=True, label=out_layer_key, alpha=0.75, histtype='step') + if plot_truth: + plt.hist(np.array(adata.layers['truth'][:, order_gene[g_ind]].todense()).squeeze(), + bins=bins, log=True, label='truth', alpha=0.5, histtype='step') + plt.xlabel('counts per cell', fontsize=12) + plt.title(f'{adata.var_names[order_gene[g_ind]]}: rank {g_ind} in ambient', fontsize=12) + plt.ylabel('number of cells', fontsize=12) + plt.legend() + plt.tight_layout() + plt.show() + + +def show_gene_expression_before_and_after(adata, + input_layer_key: str = 'raw', + num: int = 10): + """Display gene expression as an image. + Show what was removed. + Show what should have been removed. + Show residual. + """ + + inds = np.where(adata.obs['cell_probability'] > 0.5)[0][:num] + + # ambient sort order + order = np.argsort(adata.var['truth_ambient_expression'])[::-1] + + for i in inds: + + raw = np.array(adata.layers[input_layer_key][i, :].todense(), dtype=float).squeeze() + + # size of images + nz_inds = np.where(raw[order] > 0)[0] + nrows = int(np.floor(np.sqrt(nz_inds.size)).item()) + imsize = (nrows, int(np.ceil(nz_inds.size / max(1, nrows)).item())) + + raw = np.resize(raw[order][nz_inds], imsize[0] * imsize[1]).reshape(imsize) + overflow = (imsize[0] * imsize[1]) - nz_inds.size + 1 + raw[-1, -overflow:] = 0. + post = np.array(adata.layers['cellbender'][i, :].todense(), dtype=float).squeeze() + post = np.resize(post[order][nz_inds], imsize[0] * imsize[1]).reshape(imsize) + post[-1, -overflow:] = 0. + + true_ambient = np.resize(adata.var['truth_ambient_expression'][order][nz_inds], + imsize[0] * imsize[1]).reshape(imsize) + true_ambient[-1, -overflow:] = 0. + + true = np.array(adata.layers['truth'][i, :].todense(), dtype=float).squeeze() + true = np.resize(true[order][nz_inds], imsize[0] * imsize[1]).reshape(imsize) + true[-1, -overflow:] = 0. + lim = max(np.log1p(post).max(), np.log1p(true).max(), np.log1p(raw).max()) + + plt.figure(figsize=(14, 3)) + + # plt.subplot(1, 4, 1) + # plt.imshow(np.log1p(raw), vmin=1, vmax=lim) + # plt.title(f'log raw [{np.expm1(lim):.1f}]') + # plt.xticks([]) + # plt.yticks([]) + + plt.subplot(1, 4, 1) + plt.imshow(np.log1p(post), vmin=1, vmax=lim) + plt.title(f'log posterior [{np.expm1(lim):.1f}]') + plt.xticks([]) + plt.yticks([]) + + plt.subplot(1, 4, 2) + dat = (raw - true) + minmax = max(-1 * dat.min(), dat.max()) + plt.imshow(dat, cmap='seismic', vmin=-1 * minmax, vmax=minmax) + plt.title(f'raw - truth [{minmax:.1f}]') + plt.xticks([]) + plt.yticks([]) + + plt.subplot(1, 4, 3) + dat = (raw - post) + # minmax = max(-1*dat.min(), dat.max()) + cmap = plt.get_cmap('seismic', 2 * minmax + 1) + plt.imshow(dat, cmap=cmap, vmin=-1 * minmax, vmax=minmax) + plt.title(f'raw - posterior [{minmax}]') + plt.xticks([]) + plt.yticks([]) + + plt.subplot(1, 4, 4) + dat = (true - post) + thisminmax = max(-1 * dat.min(), dat.max()) + minmax = max(thisminmax, minmax) + plt.imshow(-dat, cmap='seismic', vmin=-1 * minmax, vmax=minmax) + if minmax == dat.max(): + plt.title(f'truth - posterior [- {minmax:.1f}]') + else: + plt.title(f'truth - posterior [{minmax:.1f}]') + plt.xticks([]) + plt.yticks([]) + + plt.show() + + ambient_counts = raw - true + removed_counts = raw - post + removed_ambient = np.sum(np.minimum(removed_counts[ambient_counts > 0], ambient_counts[ambient_counts > 0])) + + print(f'Fraction of ambient counts removed: {removed_ambient / np.sum(ambient_counts):.2f}') + print(f'Ambient counts removed: {int(removed_ambient)}') + print(f'Remaining ambient counts: {- int(np.sum(dat[dat < 0]))}') + print(f'Erroneously subtracted real counts: {int(np.sum(dat[dat > 0]))}') + + +def plot_gene_expression_pca(adata, key='cellbender_embedding', + input_layer_key='raw', extended=True): + + cells = (adata.obs['cell_probability'] > 0.5) + adata.obsm['X_pca'] = pca_2d(adata.obsm[key]).detach().numpy() + + # plot z PCA colored by latent size + sizeorder = np.argsort(adata.obs['cell_size'][cells]) + s = plt.scatter(x=adata.obsm['X_pca'][:, 0][cells][sizeorder], + y=adata.obsm['X_pca'][:, 1][cells][sizeorder], + c=np.log10(adata.obs['cell_size'][cells][sizeorder]), + s=2, + cmap='brg', + alpha=0.5) + plt.title('Gene expression embedding\ncolored by log10 inferred cell size $d_n$') + plt.xlabel('PCA 0') + plt.ylabel('PCA 1') + plt.xticks([]) + plt.yticks([]) + plt.gcf().colorbar(s, pad=0.1, label='log10 cell size') + plt.show() + + display(Markdown('*We are not looking for anything ' + 'specific in the PCA plot ' + 'of the gene expression embedding, but often we see clusters ' + 'that correspond to different cell types. If you see only ' + 'a single large blob, then the dataset might contain only ' + 'one cell type, or perhaps there are few counts per ' + 'droplet.*')) + + if extended: + + # plot z PCA colored by a few features + def _pca_color(g: int, layer): + outcounts = np.array(adata.layers['cellbender'][:, adata.var_names == g].todense()).squeeze() + rawcounts = np.array(adata.layers[input_layer_key][:, adata.var_names == g].todense()).squeeze() + # cmax = 2 * (rawcounts - outcounts)[rawcounts > 0].mean() + cmax = np.percentile(rawcounts[rawcounts > 0], q=80) + if layer == 'cellbender': + counts = outcounts + else: + counts = rawcounts + order = np.argsort(counts[cells]) + + s = plt.scatter(x=adata.obsm['X_pca'][:, 0][cells][order], + y=adata.obsm['X_pca'][:, 1][cells][order], + c=counts[cells][order], + s=10, + vmin=0, + vmax=cmax, # min(20, max(1, cmax)), + cmap='Oranges', + alpha=0.25) + plt.title(f'{g}: {layer}') + plt.xlabel('PCA 0') + plt.ylabel('PCA 1') + plt.gcf().colorbar(s, pad=0.05, label='Counts (truncated)') + + percentile = 90 + genecount_lowlim = int(np.percentile(adata.var[f'n_{input_layer_key}'], q=percentile)) + if 'feature_type' in adata.var.keys(): + feature_logic = (adata.var['feature_type'] == 'Gene Expression') + else: + feature_logic = True + features = (adata.var[feature_logic + & (adata.var['n_cellbender'] > genecount_lowlim)] + .sort_values(by='fraction_removed', ascending=False) + .groupby('genome') + .head(2) + .index + .values + .tolist()) + if 'feature_type' in adata.var.keys(): + if (adata.var['feature_type'] != 'Gene Expression').sum() > 0: + features.extend(adata.var[(adata.var['feature_type'] != 'Gene Expression') + & (adata.var['n_cellbender'] > genecount_lowlim)] + .sort_values(by='fraction_removed', ascending=False) + .groupby('feature_type') + .head(2) + .index + .values + .tolist()) + + display(Markdown('### Visualization of a few features')) + display(Markdown('Focusing on a few top features which were removed the most.')) + + for g in features: + plt.figure(figsize=(11, 4)) + plt.subplot(1, 2, 1) + _pca_color(g, layer=input_layer_key) + plt.subplot(1, 2, 2) + _pca_color(g, layer='cellbender') + plt.tight_layout() + plt.show() + + display(Markdown('*We typically see selective ' + 'removal of some genes from ' + 'particular cell types. The genes above have been picked ' + 'randomly based on fraction_removed, and so might not be ' + 'the best genes for visualization. These sorts of plots ' + 'can be an interesting way to visualize what ' + '`remove-background` does.*')) + + +def cluster_cells(adata, embedding_key='cellbender_embedding', n=2): + """Run PCA (if not done) and use spectral clustering to label cells. + Returns: + cluster: np.ndarray of cluster labels as ints + """ + from sklearn.cluster import SpectralClustering + + cells = (adata.obs['cell_probability'] > 0.5) + + if 'X_pca' not in adata.obsm.keys(): + from sklearn.decomposition import PCA + z = adata.obsm[embedding_key] + adata.obsm['X_pca'] = PCA(n_components=20).fit_transform(z) + + if 'truth_cell_probability' in adata.obs.keys(): + # if truth exists, use it + n_cell_labels = np.array([k.startswith('truth_gene_expression_cell_label_') + for k in adata.var.keys()]).sum() + else: + n_cell_labels = n + spec = (SpectralClustering(n_clusters=n_cell_labels, random_state=0) + .fit_predict(adata.obsm[embedding_key][cells, :])) + cluster = np.zeros(adata.shape[0]) + cluster[cells] = spec + 1 # offset by 1 so that empty is 0 + return cluster.astype(int) + + +def cluster_and_compare_expression_to_truth(adata, embedding_key='cellbender_embedding'): + + cluster = cluster_cells(adata, embedding_key=embedding_key) + adata.obs['cluster'] = cluster + cells = (adata.obs['cell_probability'] > 0.5) + + display(Markdown('## Gene expression embedding, $z_n$')) + display(Markdown('Here we find cluster labels *de novo* using spectral clustering, ' + 'in order to compare learned profiles with the truth. There ' + 'is inherently an issue of non-identifiabiity, where we cannot ' + 'know the order of the true cluster labels')) + + # plot z PCA colored by spectral cluster + plt.figure(figsize=(4, 4)) + for k in np.unique(adata.obs['cluster'][cells]): + plt.plot(adata.obsm['X_pca'][adata.obs['cluster'] == k, 0], + adata.obsm['X_pca'][adata.obs['cluster'] == k, 1], + '.', ls='', ms=5, alpha=0.25) + plt.title('Gene expression embedding,\ncolored by spectral clustering') + plt.xlabel('PC 0') + plt.ylabel('PC 1') + plt.show() + + display(Markdown('## Agreement of per-celltype gene expression with truth')) + display(Markdown('We can now compare the gene expression profile of cells ' + 'from the same cluster (found *de novo*) before and after ' + 'CellBender. We expect to see the ambient profile match, ' + 'and we expect that each *de novo* cluster will match the ' + 'gene expression of one of the "truth" clusters, although ' + 'the label order may be swapped. Here are three ways to ' + 'visualize this same data.')) + + # load truth data into a matrix + true_chi = np.zeros((np.unique(cluster).size, adata.shape[1])) + true_chi[0, :] = adata.var['truth_ambient_expression'] + for i in range(1, np.unique(cluster).size): + true_chi[i, :] = adata.var[f'truth_gene_expression_cell_label_{i}'] + + # figure out the learned expression profiles chi + learned_chi = np.zeros((np.unique(cluster).size, true_chi.shape[1])) + learned_chi[0, :] = adata.var['ambient_expression'] + for k in adata.obs['cluster'].unique(): + if k == 0: + continue + # get chi from mean cell expression in that cluster + summmed_expression = np.array(adata.layers['cellbender'][adata.obs['cluster'] == k, :] + .sum(axis=0)).squeeze() + learned_chi[k, :] = summmed_expression / summmed_expression.sum() + + # compare learned expression to the underlying true expression + def _gridplot(adata, upper_lim=1e-1, scale='linear'): + plt.figure(figsize=(10, 9)) + n_clusters = adata.obs['cluster'].nunique() + for k in adata.obs['cluster'].unique(): + for j in adata.obs['cluster'].unique(): + plt.subplot(n_clusters, + n_clusters, + n_clusters * k + j + 1) + plt.plot([1e-7, 1e-1], [1e-7, 1e-1], color='black', lw=0.2) + plt.plot(true_chi[j, :], learned_chi[k, :], '.', ls='', ms=1, alpha=0.5) + plt.ylim([1e-6, upper_lim]) + plt.xlim([1e-6, upper_lim]) + plt.xscale(scale) + plt.yscale(scale) + if j == 0: + plt.ylabel(f'Learned {k if k > 0 else "ambient"}') + else: + plt.yticks([]) + if k == n_clusters - 1: + plt.xlabel(f'True {j if j > 0 else "ambient"}') + else: + plt.xticks([]) + plt.show() + + _gridplot(adata, scale='log') + _gridplot(adata, upper_lim=1e-2,) + + # same in the format of cosine distance + n_clusters = adata.obs['cluster'].nunique() + dist = np.zeros((n_clusters, n_clusters)) + for k in adata.obs['cluster'].unique(): + for j in adata.obs['cluster'].unique(): + u, v = true_chi[j, :], learned_chi[k, :] + # cosine distance + dist[k, j] = cosine(u, v) + + fig = plt.figure(figsize=(5, 5)) + if dist.max() > 0.9: + plt.imshow(dist, vmin=0, vmax=dist[dist < 0.9].max() + 0.1, + cmap=plt.cm.gray_r) + else: + plt.imshow(dist, vmin=0, + cmap=plt.cm.gray_r) + + for i in range(dist.shape[0]): + for j in range(dist.shape[1]): + plt.text(i - 0.3, j + 0.05, f'{dist[j, i]:.3f}', fontsize=14, color='gray') + + plt.xticks(range(n_clusters), ['BKG'] + [i + 1 for i in range(n_clusters - 1)]) + # plt.gca().xaxis.set_ticks_position('top') + # plt.gca().xaxis.set_label_position('top') + plt.yticks(range(n_clusters), ['BKG'] + [i + 1 for i in range(n_clusters - 1)]) + plt.ylim(plt.gca().get_xlim()[::-1]) + plt.xlabel('True') + plt.ylabel('Inferred') + plt.title('Cosine distance between\ntrue and inferred expression') + + from mpl_toolkits.axes_grid1 import make_axes_locatable + + divider = make_axes_locatable(plt.gca()) + cax = divider.append_axes("right", size="5%", pad=0.05) + plt.colorbar(cax=cax, format='%.2f') + + plt.tight_layout() + plt.show() + + return learned_chi + + +def cosine(u: np.ndarray, v: np.ndarray): + return 1 - np.dot(u, v) / (np.sqrt(np.dot(u, u)) * np.sqrt(np.dot(v, v)) + 1e-10) + + +def fano(count_matrix_csr, axis=0): + """Calculate Fano factor for count matrix, either per gene or per cell, + as specified by axis. + """ + + mean = np.array(count_matrix_csr.mean(axis=axis)).squeeze() + square_mean = np.array(count_matrix_csr.power(2).mean(axis=axis)).squeeze() + var = square_mean - np.power(mean, 2) + return var / (mean + 1e-10) + + +def plot_counts_per_gene(adata): + + order_gene = np.argsort(adata.var['n_cellbender'])[::-1] + plt.loglog(adata.var['n_cellbender'][order_gene]) + plt.loglog(adata.var['n_cellbender'][order_gene], ':', alpha=0.5) + plt.xlabel('Sorted gene ID') + plt.ylabel('Unique UMI counts') + plt.show() + + +def compare_latents(adata, input_layer_key='raw'): + """Compare inferred latents to truth""" + + truth_exists = ('truth_cell_probability' in adata.obs.keys()) + + display(Markdown('## Inference of latent variables (for experts)')) + + if not truth_exists: + display(Markdown('This section deals with particular details of the ' + 'latent variables in the `remove-background` model. ' + 'This analysis is provided for interested experts, but ' + 'is not necessary for judging the success of a run.')) + + display(Markdown('### Ambient gene expression profile')) + plt.figure(figsize=(12, 3)) + plt.plot(adata.var['ambient_expression'].values) + plt.ylabel('Fractional expression') + plt.xlabel('Gene') + plt.title('Inferred ambient profile') + plt.show() + + if truth_exists: + plt.figure(figsize=(12, 3)) + plt.plot(adata.var['ambient_expression'].values + - adata.var['truth_ambient_expression'].values) + plt.ylabel('Residual: inferred - true') + plt.xlabel('Gene') + plt.title('Inferred ambient profile') + plt.show() + + display(Markdown('### Swapping fraction, rho')) + + rho = adata.uns.get('swapping_fraction_dist_params', None) + if rho is None: + display(Markdown('Swapping fraction not inferred. `--model` may have ' + 'been "ambient" instead of "swapping" or "full"')) + else: + from scipy.stats import beta + nbins = 100 + xx = np.linspace(0, 1, nbins) + if truth_exists: + plt.hist(adata.obs['truth_swapping_fraction'].values, + bins=xx, label='Truth', alpha=0.5) + plt.plot(xx, (adata.shape[0] / nbins + * beta.pdf(xx, consts.RHO_ALPHA_PRIOR, consts.RHO_BETA_PRIOR)), + label='Prior', color='k') + plt.plot(xx, (adata.shape[0] / nbins + * beta.pdf(xx, rho[0], rho[1])), label='Inferred', color='r') + plt.title('Inferred distribution for swapping fraction') + plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) + plt.xlabel('Swapping fraction rho') + plt.ylabel('Number of droplets') + plt.show() + + if truth_exists: + + display(Markdown('### Droplet efficiency and cell size latents')) + display(Markdown('Including empty droplets')) + keys = ['droplet_efficiency', 'cell_size'] + + plt.figure(figsize=(5 * len(keys), 4)) + for i, key in enumerate(keys): + plt.subplot(1, len(keys), i + 1) + otherkey = 'droplet_efficiency' if (key == 'cell_size') else 'cell_size' + plt.scatter(adata.obs[f'truth_{key}'], adata.obs[key], + c=adata.obs[otherkey], s=5, cmap='coolwarm', alpha=0.5) + cbar = plt.colorbar() + cbar.set_label(otherkey.capitalize().replace('_', ' ')) + xx = [adata.obs[f'truth_{key}'].min(), adata.obs[f'truth_{key}'].max()] + plt.plot(xx, xx, color='lightgray') + plt.title(key.capitalize().replace('_', ' ')) + plt.xlabel('truth') + plt.ylabel('cellbender') + plt.axis('equal') + plt.tight_layout() + plt.show() + + display(Markdown('Cells only')) + cells = (adata.obs['cell_probability'] > 0.5) + plt.figure(figsize=(5 * len(keys), 4)) + for i, key in enumerate(keys): + plt.subplot(1, len(keys), i + 1) + for c in sorted(adata.obs['truth_cell_label'][cells].unique()): + ctype = (adata.obs['truth_cell_label'] == c) + plt.plot(adata.obs[f'truth_{key}'][cells & ctype], + adata.obs[key][cells & ctype], '.', + label=f'{c} ({ctype.sum()} cells)', alpha=0.5) + xx = [adata.obs[f'truth_{key}'][cells].min(), + adata.obs[f'truth_{key}'][cells].max()] + plt.plot(xx, xx, color='lightgray') + plt.title(key.capitalize().replace('_', ' ') + ': cells') + plt.xlabel('truth') + plt.ylabel('cellbender') + plt.axis('equal') + plt.legend(loc='center left', bbox_to_anchor=(1, 0.5), title='Cell type') + plt.tight_layout() + plt.show() + + display(Markdown(r'### Droplet efficiency $\epsilon$')) + if truth_exists: + display(Markdown('Comparison with the truth.')) + + order = np.argsort(adata.obs[f'n_{input_layer_key}'].values)[::-1] + plt.figure(figsize=(6, 4)) + plt.semilogy(adata.obs[f'n_{input_layer_key}'].values[order], 'k', lw=2) + plt.ylabel('UMI counts') + plt.xlabel('Droplet') + + plt.gca().twinx() + if truth_exists: + plt.plot(adata.obs['truth_droplet_efficiency'].values[order], + label='truth', alpha=0.5) + plt.plot(adata.obs['droplet_efficiency'].values[order], + 'r', label='inferred', alpha=0.5) + plt.ylabel(r'$\epsilon$', fontsize=18, color='r') + plt.legend() + plt.show() + + display(Markdown('Comparison with the prior.')) + from numpy.random import gamma + rh = consts.EPSILON_PRIOR + delta = 0.01 + xx = np.arange(0, 2.1, step=delta) + + if truth_exists: + cells = (adata.obs['truth_cell_probability'] == 1) + yy, _ = np.histogram( + gamma(adata.uns['truth_epsilon_param'], + scale=1. / adata.uns['truth_epsilon_param'], size=[50000]), + bins=xx, + ) + plt.step(xx[:-1], yy / np.sum(yy * delta), label='truth', color='r') + else: + cells = (adata.obs['cell_probability'] > 0.5) + + yy, _ = np.histogram(gamma(rh, scale=1. / rh, size=[50000]), bins=xx) + plt.step(xx[:-1], yy / np.sum(yy * delta), label='prior', color='k') + + yy, _ = np.histogram(adata.obs['droplet_efficiency'][cells], bins=xx) + plt.step(xx[:-1], yy / np.sum(yy * delta), alpha=0.5, label='posterior: cells') + + yy, _ = np.histogram(adata.obs['droplet_efficiency'][~cells], bins=xx) + plt.step(xx[:-1], yy / np.sum(yy * delta), alpha=0.5, label='posterior: empties') + + plt.ylabel('Probability density') + plt.xlabel(r'$\epsilon$', fontsize=18) + plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) + plt.show() + + display(Markdown('### Cell size $d$')) + if truth_exists: + display(Markdown('Comparison with the truth.')) + cell_order = np.argsort(adata.obs[f'n_{input_layer_key}'].values[cells])[::-1] + plt.figure(figsize=(6, 4)) + plt.semilogy(adata.obs[f'n_{input_layer_key}'].values[cells][cell_order], 'k', lw=2) + plt.ylabel('UMI counts') + plt.xlabel('Droplet') + + plt.gca().twinx() + if truth_exists: + plt.semilogy(adata.obs['truth_cell_size'].values[cells][cell_order], + label='truth', alpha=0.5) + plt.semilogy(adata.obs['cell_size'].values[cells][cell_order], + 'r', label='inferred', alpha=0.5) + plt.ylabel('$d$', fontsize=18, color='r') + plt.legend() + plt.show() + + display(Markdown(r'### The combination $d \epsilon$')) + display(Markdown('It is easier for the model to nail this down, since there ' + 'is less degeneracy.')) + + plt.figure(figsize=(6, 4)) + plt.semilogy(adata.obs[f'n_{input_layer_key}'].values[cells][cell_order], 'k', lw=2) + plt.ylabel('UMI counts') + plt.xlabel('Droplet') + + plt.gca().twinx() + if truth_exists: + plt.semilogy((adata.obs['truth_cell_size'].values[cells][cell_order] + * adata.obs['truth_droplet_efficiency'].values[cells][cell_order]), + label='truth', alpha=0.5) + plt.semilogy((adata.obs['cell_size'].values[cells][cell_order] + * adata.obs['droplet_efficiency'].values[cells][cell_order]), + 'r', label='inferred', alpha=0.5) + plt.ylabel(r'$d \epsilon$', fontsize=18, color='r') + plt.legend() + plt.show() + + display(Markdown(r'### Joint distributions of $d$ and $\epsilon$')) + if truth_exists: + display(Markdown('In true cells')) + else: + display(Markdown('In inferred cells')) + if truth_exists: + plt.figure(figsize=(9, 4)) + plt.subplot(1, 2, 1) + for c in sorted(adata.obs['truth_cell_label'][cells].unique()): + ctype = (adata.obs['truth_cell_label'] == c) + plt.semilogy(adata.obs['truth_droplet_efficiency'][ctype], + adata.obs['truth_cell_size'][ctype], + '.', alpha=0.5) + plt.ylabel('$d$', fontsize=18) + plt.xlabel(r'$\epsilon$', fontsize=18) + plt.title('Truth') + plt.subplot(1, 2, 2) + for c in sorted(adata.obs['truth_cell_label'][cells].unique()): + ctype = (adata.obs['truth_cell_label'] == c) + plt.semilogy(adata.obs['droplet_efficiency'][ctype], + adata.obs['cell_size'][ctype], + '.', label=f'{c} ({ctype.sum()} cells)', alpha=0.5) + plt.legend(loc='center left', bbox_to_anchor=(1, 0.5), title='Cell type') + else: + plt.figure(figsize=(4, 4)) + plt.semilogy(adata.obs['droplet_efficiency'][cells], + adata.obs['cell_size'][cells], + 'k.', ms=1, alpha=0.5) + plt.ylabel('$d$', fontsize=18) + plt.xlabel(r'$\epsilon$', fontsize=18) + plt.title('Inferred') + plt.tight_layout() + plt.show() + + +def mixed_species_plots(adata, input_layer_key='raw', output_layer_key='cellbender', + ngene_cutoff=5000, count_cutoff=500): + """Make mixed species plots if it seems appropriate""" + + # find a list of genes with expression exclusive to a cell type + if 'genome' not in adata.var.keys(): + return + + if ('feature_type' in adata.var.keys()) and ('Gene Expression' in adata.var['feature_type'].values): + genomes = adata.var[adata.var['feature_type'] == 'Gene Expression']['genome'].unique() + else: + genomes = adata.var['genome'].unique() + cells = (adata.obs['cell_probability'] > 0.5) + + if len(genomes) != 2: + return + + display(Markdown('## "Mixed species" analysis')) + display(Markdown('There are multiple genomes in the dataset, so we can ' + 'analyze these genes assuming they were from a "mixed ' + 'species" experiment, where we know that certain genes ' + 'come only from certain celltypes.')) + + for genome in genomes: + if 'feature_type' in adata.var.keys(): + if 'Gene Expression' in adata.var["feature_type"].values: + var_subset = adata.var[(adata.var["genome"] == genome) + & (adata.var["feature_type"] == "Gene Expression")] + else: + var_subset = adata.var[(adata.var["genome"] == genome)] + else: + var_subset = adata.var[(adata.var["genome"] == genome)] + print(f'Genome "{genome}" has {len(var_subset)} genes: ' + f'{", ".join(var_subset.index.values[:3])} ...') + + for i, genome1 in enumerate(genomes): + for genome2 in genomes[(i + 1):]: + plt.figure(figsize=(5, 5)) + for k in [input_layer_key, output_layer_key]: + x = np.array(adata.layers[k] + [:, (adata.var['genome'] == genome1)].sum(axis=1)).squeeze() + y = np.array(adata.layers[k] + [:, (adata.var['genome'] == genome2)].sum(axis=1)).squeeze() + plt.plot(x + 1, + y + 1, + '.', ms=2, label=k, alpha=0.3, + color='k' if (k == input_layer_key) else 'r') + x = x[cells] + y = y[cells] + contam_x = (y / (y + x + 1e-10))[x > (10 * y)] + contam_y = (x / (x + y + 1e-10))[y > (10 * x)] + contam = np.concatenate((contam_x, contam_y), axis=0) + display(Markdown(f'Mean "contamination" per droplet in {k} data: ' + f'{contam.mean() * 100:.2f} %')) + display(Markdown(f'Median "contamination" per droplet in {k} data: ' + f'{np.median(contam) * 100:.2f} %')) + plt.xscale('log') + plt.yscale('log') + plt.ylim(bottom=0.55) + plt.xlim(left=0.55) + plt.xlabel(f'{genome1} counts (+1)') + plt.ylabel(f'{genome2} counts (+1)') + plt.title(f'Counts per droplet: {output_layer_key}') + lgnd = plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) + _set_legend_dot_size(lgnd, size=10) + plt.show() + + +def _set_legend_dot_size(lgnd, size: int): + """Set dot size in a matplotlib legend. + Different versions of matplotlib seem to need different things. + Just give up and move on if it doesn't work. + I know this is a bit ridiculous but so is matplotlib. + """ + for h in lgnd.legendHandles: + worked = False + try: + h._legmarker.set_markersize(size) + worked = True + except AttributeError: + pass + if worked: + continue + try: + h.set_sizes([size]) + worked = True + except AttributeError: + pass + if worked: + continue + try: + h._sizes = [size] + worked = True + except AttributeError: + pass + if worked: + continue + try: + h.set_markersize(size) + worked = True + except AttributeError: + pass + + +def cell_roc_count_roc(output_csr: sp.csr_matrix, + input_csr: sp.csr_matrix, + truth_csr: sp.csr_matrix, + cell_calls: np.ndarray, + truth_cell_labels: np.ndarray = None) -> float: # 0 = empty, nonzero = cell + """Plot a ROC curve (point) for cell identification, and plot + a second for noise count identification. + + Returns: + fpr: Acutal false positive rate for counts in cells + + Note: + True positive = a noise count that has been removed + True negative = a real count that has not been removed + False positive = a real count that has been removed + False negative = a noise count that has not been removed + + | noise | real | + ------------------------------------ + removed | TP | FP | + -------------|----------o----------| + not removed | FN | TN | + ------------------------------------ + + For the cell identification case: + + True positive = a cell called a cell + True negative = an empty called empty + False positive = an empty called a cell + False negative = a cell called empty + + | cell | empty | + -------------------------------------- + called a cell | TP | FP | + ---------------|----------o----------| + called empty | FN | TN | + -------------------------------------- + + """ + + assert cell_calls.size == truth_csr.shape[0], "Cell calls must be number of barcodes." + + # find the real cells as things with counts in truth data + cell_truth = (np.array(truth_csr.sum(axis=1)).squeeze() > 0) + called_cell = (cell_calls != 0) + + display(Markdown('### Identification of non-empty droplets')) + display(Markdown('TPR = true positive rate\n\nFPR = false positive rate')) + + # cell breakdown + tpr = np.logical_and(cell_truth, called_cell).sum() / cell_truth.sum() + fpr = np.logical_and(np.logical_not(cell_truth), called_cell).sum() / called_cell.sum() + print(f'\nCell identification TPR = {tpr:.3f}') + print(f'Cell identification FPR = {fpr:.3f}') + + fig = plt.figure(figsize=(5, 5)) + plt.plot(fpr, tpr, 'o') + plt.ylabel('fraction of true cells called') + plt.xlabel('fraction of called cells that are really empty') + plt.ylim([-0.05, 1.05]) + plt.xlim([-0.05, 1.05]) + plt.title('Cell identification') + plt.show() + + # counts + # just work with cells + cell_inds = np.where(cell_truth)[0] + real_counts = truth_csr[cell_inds] + removed_counts = input_csr[cell_inds] - output_csr[cell_inds] + noise_counts = input_csr[cell_inds] - truth_csr[cell_inds] + + # count breakdown + tp = (np.array(noise_counts + .minimum(removed_counts) + .sum(axis=1)).squeeze()) + erroneously_removed_counts = removed_counts - noise_counts + erroneously_removed_counts[erroneously_removed_counts < 0] = 0 + fp = np.array(erroneously_removed_counts.sum(axis=1)).squeeze() + noise_remaining = noise_counts - removed_counts + noise_remaining[noise_remaining < 0] = 0 + fn = np.array(noise_remaining.sum(axis=1)).squeeze() + tn = np.array(real_counts.minimum(output_csr[cell_inds]).sum(axis=1)).squeeze() + + display(Markdown('Take a look at the statistics of count removal, with the ' + 'following definitions:\n\n- TP = true positive: a noise ' + 'count (correctly) removed\n- FP = false positive: a real ' + 'count (incorrectly) removed\n- TN = true negative: a real ' + 'count (correctly) not removed\n- FN = false negative: a ' + 'noise count (incorrectly) not removed')) + + print(f'max ambient counts in any matrix entry is {(input_csr - truth_csr).max()}') + + plt.figure(figsize=(8, 1)) + plt.hist(tp, bins=100) + plt.title('TP: noise counts removed per cell') + plt.show() + + plt.figure(figsize=(8, 1)) + plt.hist(fp, bins=100) + plt.title('FP: real counts removed per cell') + plt.show() + + plt.figure(figsize=(8, 1)) + plt.hist(tn, bins=100) + plt.title('TN: real counts not removed per cell') + plt.show() + + plt.figure(figsize=(8, 1)) + plt.hist(fn, bins=100) + plt.title('FN: noise counts not removed per cell') + plt.show() + + plot_roc_points(tp, fp, tn, fn, point_colors=np.array(truth_cell_labels)[cell_truth], + title='Background removal from cells', color='black', show=False) + plt.show() + + display(Markdown('### Summary statistics for noise removal')) + + print(f'Cell background removal TPR = {(tp / (tp + fn)).mean():.3f}' + f' +/- {(tp / (tp + fn)).std():.3f}') + print(f'Cell background removal FPR = {1 - (tn / (tn + fp)).mean():.3f}' + f' +/- {(tn / (tn + fp)).std():.3f}') + + display(Markdown('And finally one more way to quanitify the amount of noise remaining:')) + + contam_cell_raw = (np.array(noise_counts.sum(axis=1)).squeeze() + / np.array(real_counts.sum(axis=1)).squeeze()) + contam_cell_after = (np.array(noise_remaining.sum(axis=1)).squeeze() + / np.array(real_counts.sum(axis=1)).squeeze()) + print(f'Per cell contamination fraction raw = {contam_cell_raw.mean():.3f}' + f' +/- {contam_cell_raw.std():.3f}') + print(f'Per cell contamination fraction after = {contam_cell_after.mean():.3f}' + f' +/- {contam_cell_after.std():.3f}') + + return 1 - (tn / (tn + fp)).mean() # FPR + + +def plot_roc_points(tp: np.ndarray, + fp: np.ndarray, + tn: np.ndarray, + fn: np.ndarray, + point_colors: np.ndarray, + title: str = 'roc', + show: bool = True, + **kwargs): + """Plot points (and one summary point with error bars) on a ROC curve, + where the x-axis is FPR and the y-axis is TPR. + + Args: + tp: True positives, multiple measurements of the same value. + fp: False positives + tn: True negatives + fn: False negatives + point_colors: Color for each point + title: Plot title + show: True to show plot + kwargs: Passed to the plotter for the summary point with error bars + + """ + + fig = plt.figure(figsize=(5, 5)) + sensitivity = tp / (tp + fn) + specificity = tn / (tn + fp) + sens_mean = sensitivity.mean() + sens_stdv = sensitivity.std() + spec_mean = specificity.mean() + spec_stdv = specificity.std() + # individual points + if type(point_colors) == str: + point_colors = np.array([point_colors] * len(sensitivity)) + else: + assert len(point_colors) == len(sensitivity), 'point_colors is wrong length' + unique_colors = np.unique(point_colors) + for c in unique_colors: + plt.plot((1 - specificity)[point_colors == c], + sensitivity[point_colors == c], + '.', ms=1, alpha=0.5, label=c) + lgnd = plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) + _set_legend_dot_size(lgnd, size=15) + # midpoint + plt.plot(1 - spec_mean, sens_mean, 'o', **kwargs) + # errorbars + plt.plot([1 - (spec_mean - spec_stdv), + 1 - (spec_mean + spec_stdv)], + [sens_mean, sens_mean], **kwargs) + plt.plot([1 - spec_mean, 1 - spec_mean], + [sens_mean - sens_stdv, + sens_mean + sens_stdv], **kwargs) + plt.ylabel('sensitivity = TP / (TP + FN)\ntrue positive rate') + plt.xlabel('1 - specificity = 1 - TN / (TN + FP)\nfalse positive rate') + plt.ylim([-0.05, 1.05]) + plt.xlim([-0.05, 1.05]) + plt.title(title) + + if show: + plt.show() + else: + return fig + + +def pca_2d(mat: np.ndarray) -> torch.Tensor: + """Perform PCA using pytorch and return top 2 PCs + + Args: + mat: matrix where rows are observations and columns are features + + Returns: + out: matrix where rows are observations and columns are top 2 PCs + """ + + A = torch.as_tensor(mat).float() + U, S, V = torch.pca_lowrank(A) + return torch.matmul(A, V[:, :2]) + + +def plot_summary(loss: Dict[str, Dict[str, np.ndarray]], + umi_counts: np.ndarray, + p: np.ndarray, + z: np.ndarray): + """Output summary plot with three panels: training, cells, latent z.""" + + fig = plt.figure(figsize=(6, 18)) + + # Plot the train error. + plt.subplot(3, 1, 1) + try: + plt.plot(loss['train']['elbo'], '.--', label='Train') + + # Plot the test error, if there was held-out test data. + if 'test' in loss.keys(): + if len(loss['test']['epoch']) > 0: + plt.plot(loss['test']['epoch'], + loss['test']['elbo'], 'o:', label='Test') + plt.legend() + + ylim_low = max(loss['train']['elbo'][0], loss['train']['elbo'][-1] - 2000) + try: + ylim_high = max(max(loss['train']['elbo']), max(loss['test']['elbo'])) + except ValueError: + ylim_high = max(loss['train']['elbo']) + ylim_high = ylim_high + (ylim_high - ylim_low) / 20 + plt.gca().set_ylim([ylim_low, ylim_high]) + except: + logger.warning('Error plotting the learning curve. Skipping.') + pass + + plt.xlabel('Epoch') + plt.ylabel('ELBO') + plt.title('Progress of the training procedure') + + # Plot the barcodes used, along with the inferred + # cell probabilities. + plt.subplot(3, 1, 2) + count_order = np.argsort(umi_counts)[::-1] + plt.semilogy(umi_counts[count_order], color='black') + plt.ylabel('UMI counts') + plt.xlabel('Barcode index, sorted by UMI count') + if p is not None: # The case of a simple model. + plt.gca().twinx() + plt.plot(p[count_order], '.:', color='red', alpha=0.3, rasterized=True) + plt.ylabel('Cell probability', color='red') + plt.ylim([-0.05, 1.05]) + plt.title('Determination of which barcodes contain cells') + else: + plt.title('The subset of barcodes used for training') + + plt.subplot(3, 1, 3) + if p is None: + p = np.ones(z.shape[0]) + + # Do PCA on the latent encoding z. + z_pca = pca_2d(z[p >= consts.CELL_PROB_CUTOFF]) + + # Plot the latent encoding via PCA. + plt.plot(z_pca[:, 0], z_pca[:, 1], + '.', ms=3, color='black', alpha=0.3, rasterized=True) + plt.ylabel('PC 1') + plt.xlabel('PC 0') + plt.title('PCA of latent encoding of gene expression in cells') + + return fig diff --git a/cellbender/remove_background/run.py b/cellbender/remove_background/run.py new file mode 100644 index 0000000..7b777e4 --- /dev/null +++ b/cellbender/remove_background/run.py @@ -0,0 +1,779 @@ +"""Single run of remove-background, given input arguments.""" + +from cellbender.remove_background.model import RemoveBackgroundPyroModel +from cellbender.remove_background.data.dataset import get_dataset_obj, \ + SingleCellRNACountsDataset +from cellbender.remove_background.vae.decoder import Decoder +from cellbender.remove_background.vae.encoder \ + import EncodeZ, CompositeEncoder, EncodeNonZLatents +from cellbender.remove_background.data.dataprep import \ + prep_sparse_data_for_training as prep_data_for_training +from cellbender.remove_background.checkpoint import attempt_load_checkpoint, \ + save_checkpoint +import cellbender.remove_background.consts as consts +from cellbender.remove_background.data.dataprep import DataLoader +from cellbender.remove_background.train import run_training +from cellbender.remove_background.posterior import Posterior, PRq, PRmu, \ + compute_mean_target_removal_as_function, load_or_compute_posterior_and_save +from cellbender.remove_background.estimation import MultipleChoiceKnapsack, \ + MAP, Mean, SingleSample, ThresholdCDF +from cellbender.remove_background.exceptions import ElboException +from cellbender.remove_background.sparse_utils import csr_set_rows_to_zero +from cellbender.remove_background.data.io import write_matrix_to_cellranger_h5 +from cellbender.remove_background.report import run_notebook_make_html, plot_summary + +import pyro +from pyro.infer import SVI, JitTraceEnum_ELBO, JitTrace_ELBO, \ + TraceEnum_ELBO, Trace_ELBO +from pyro.optim import ClippedAdam +import numpy as np +import scipy.sparse as sp +import pandas as pd +import torch + +import os +import sys +import logging +import argparse +import traceback +import psutil +from datetime import datetime +from typing import Tuple, Optional, Dict, Union + +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt # This needs to be after matplotlib.use('Agg') + + +logger = logging.getLogger('cellbender') + + +def run_remove_background(args: argparse.Namespace) -> Posterior: + """The full script for the command line tool to remove background RNA. + + Args: + args: Inputs from the command line, already parsed using argparse. + + Note: Returns nothing, but writes output to a file(s) specified from + command line. + + """ + + # Handle initial random state. + pyro.util.set_rng_seed(consts.RANDOM_SEED) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(consts.RANDOM_SEED) + + # Load dataset, run inference, and write the output to a file. + + # Log the start time. + logger.info(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + logger.info("Running remove-background") + + # Run pytorch multithreaded if running on CPU: but this makes little difference in runtime. + if not args.use_cuda: + if args.n_threads is not None: + n_jobs = args.n_threads + else: + n_jobs = psutil.cpu_count(logical=True) + torch.set_num_threads(n_jobs) + logger.debug(f'Set pytorch to use {n_jobs} threads') + + # Load data from file and choose barcodes and genes to analyze. + try: + dataset_obj = get_dataset_obj(args=args) + + except OSError: + logger.error(f"OSError: Unable to open file {args.input_file}.") + logger.error(traceback.format_exc()) + sys.exit(1) + + # Instantiate latent variable model and run full inference procedure. + if args.model == 'naive': + inferred_model = None + else: + inferred_model, _, _, _ = run_inference(dataset_obj=dataset_obj, args=args) + inferred_model.eval() + + try: + + file_dir, file_base = os.path.split(args.output_file) + file_name = os.path.splitext(os.path.basename(file_base))[0] + + # Create the posterior and save it. + posterior = load_or_compute_posterior_and_save( + dataset_obj=dataset_obj, + inferred_model=inferred_model, + args=args, + ) + logger.info(datetime.now().strftime('%Y-%m-%d %H:%M:%S\n')) + + # Save output plots. + save_output_plots( + file_dir=file_dir, + file_name=file_name, + dataset_obj=dataset_obj, + inferred_model=inferred_model, + p=posterior.latents_map['p'], + z=posterior.latents_map['z'], + ) + + # Save cell barcodes in a CSV file. + analyzed_barcode_logic = (posterior.latents_map['p'] > consts.CELL_PROB_CUTOFF) + cell_barcodes = (dataset_obj.data['barcodes'] + [dataset_obj.analyzed_barcode_inds[analyzed_barcode_logic]]) + bc_file_name = os.path.join(file_dir, file_name + "_cell_barcodes.csv") + write_cell_barcodes_csv(bc_file_name=bc_file_name, cell_barcodes=cell_barcodes) + + # Compute estimates of denoised count matrix for each FPR and save them. + compute_output_denoised_counts_reports_metrics( + posterior=posterior, + args=args, + file_dir=file_dir, + file_name=file_name, + ) + + logger.info("Completed remove-background.") + logger.info(datetime.now().strftime('%Y-%m-%d %H:%M:%S\n')) + + return posterior + + # The exception allows user to end inference prematurely with CTRL-C. + except KeyboardInterrupt: + + # If partial output has been saved, delete it. + full_file = args.output_file + + # Name of the filtered (cells only) file. + file_dir, file_base = os.path.split(full_file) + file_name = os.path.splitext(os.path.basename(file_base))[0] + filtered_file = os.path.join(file_dir, file_name + "_filtered.h5") + + if os.path.exists(full_file): + os.remove(full_file) + + if os.path.exists(filtered_file): + os.remove(filtered_file) + + logger.info("Keyboard interrupt. Terminated without saving.\n") + + +def save_output_plots(file_dir: str, + file_name: str, + dataset_obj: SingleCellRNACountsDataset, + inferred_model: RemoveBackgroundPyroModel, + p: np.ndarray, + z: np.ndarray) -> bool: + """Save the UMI histogram and the three-panel output summary PDF""" + + try: + # File naming. + summary_fig_name = os.path.join(file_dir, file_name + ".pdf") + + # Three-panel output summary plot. + counts = np.array(dataset_obj.get_count_matrix().sum(axis=1)).squeeze() + fig = plot_summary(loss=inferred_model.loss, umi_counts=counts, p=p, z=z) + fig.savefig(summary_fig_name, bbox_inches='tight', format='pdf') + logger.info(f"Saved summary plots as {summary_fig_name}") + return True + + except Exception: + logger.warning("Unable to save all plots.") + logger.warning(traceback.format_exc()) + return False + + +def compute_output_denoised_counts_reports_metrics(posterior: Posterior, + args: argparse.Namespace, + file_dir: str, + file_name: str) -> bool: + """Handle the estimation of the output denoised count matrix, given a + posterior. Compute the estimate for all specified FPR values. Save each + as we go. Create reports and save, and aggregate metrics and save as we go. + + Args: + posterior: Posterior object + args: Argparser namespace with all parsed input args + file_dir: + file_name: + + Returns: + True iff all files write correctly + + """ + + # Ensure that the posterior distribution has been computed. + # TODO: when we make the properties non-private, then this should become obsolete + posterior.cell_noise_count_posterior_coo() + + # Choose output count matrix estimation method. + noise_target_fun = None + if args.estimator == 'map': + estimator = MAP + elif args.estimator == 'mean': + estimator = Mean + elif args.estimator == 'sample': + estimator = SingleSample + elif args.estimator == 'cdf': + estimator = ThresholdCDF + elif args.estimator == 'mckp': + estimator = MultipleChoiceKnapsack + + # Prep specific for MCKP: target estimation. + logger.info('Computing target noise counts per gene for MCKP estimator') + count_matrix = posterior.dataset_obj.data['matrix'] # all barcodes + cell_inds = posterior.dataset_obj.analyzed_barcode_inds[posterior.latents_map['p'] + > consts.CELL_PROB_CUTOFF] + empty_inds = set(range(count_matrix.shape[0])) - set(cell_inds) + cell_counts = csr_set_rows_to_zero(csr=count_matrix, row_inds=empty_inds) + + noise_target_fun_per_cell = compute_mean_target_removal_as_function( + noise_count_posterior_coo=posterior._noise_count_posterior_coo, + noise_offsets=posterior._noise_count_posterior_coo_offsets, + index_converter=posterior.index_converter, + raw_count_csr_for_cells=cell_counts, + n_cells=len(cell_inds), + device='cuda' if args.use_cuda else 'cpu', # TODO check this + per_gene=True, + ) + noise_target_fun = lambda x: noise_target_fun_per_cell(x) * len(cell_inds) + else: + raise ValueError(f'Input --estimator must be one of ' + f'["map", "mean", "sample", "cdf", "mckp"]') + + # Save denoised count matrix outputs (for each FPR if applicable). + success = True + for fpr in args.fpr: + + logger.debug(f'Working on FPR {fpr}') + + # Regularize posterior again at this FPR, if needed. + if args.posterior_regularization in ['PRmu', 'PRmu_gene']: + # TODO: currently this re-calculates the first FPR which is not necessary + posterior.regularize_posterior( + regularization=PRmu, + raw_count_matrix=posterior.dataset_obj.data['matrix'], + fpr=fpr, + per_gene=True if (args.posterior_regularization == 'PRmu_gene') else False, + device='cuda', + ) + else: + # Other posterior regularizations were already performed in + # posterior.load_or_compute_posterior_and_save() + pass + + # If using MCKP, recompute noise targets for this FPR. + if noise_target_fun is not None: + noise_targets = noise_target_fun(fpr).detach().cpu().numpy() + logger.debug(f'Computed noise targets for FPR {fpr}:\n{noise_targets}') + logger.info(f'Using MCKP noise targets computed for FPR {fpr}') + else: + noise_targets = None + + # Compute denoised counts. + logger.info(f'Computing denoised counts using {args.estimator} estimator') + denoised_counts = posterior.compute_denoised_counts( + estimator_constructor=estimator, + noise_targets_per_gene=noise_targets, + q=args.cdf_threshold_q, + alpha=args.prq_alpha, + device='cuda' if args.use_cuda else 'cpu', + use_multiple_processes=args.use_multiprocessing_estimation, + ) + + # Restore eliminated features in cells. + logger.debug('Restoring eliminated features in cells') + denoised_counts = posterior.dataset_obj.restore_eliminated_features_in_cells( + denoised_counts, + posterior.latents_map['p'], + ) + + # TODO: correct cell probabilities so that any zero-count droplet becomes "empty" + + # Save denoised count matrix. + name_suffix = (f'_FPR_{fpr}' if len(args.fpr) > 1 else '') + fpr_output_filename = os.path.join(file_dir, file_name + name_suffix + '.h5') + filtered_output_file = os.path.join(file_dir, file_name + name_suffix + '_filtered.h5') + + def _writer_helper(file, **kwargs) -> bool: + return write_denoised_count_matrix( + file=file, + denoised_count_matrix=denoised_counts, + posterior_regularization=args.posterior_regularization, + posterior_regularization_kwargs=posterior._noise_count_regularized_posterior_kwargs, + estimator=args.estimator, + estimator_kwargs=None if (args.cdf_threshold_q is None) else {'q': args.cdf_threshold_q}, + latents=posterior.latents_map, + dataset_obj=posterior.dataset_obj, + learning_curve=posterior.vi_model.loss, + fpr=fpr, + **kwargs, + ) + + # Full count matrix. + write_succeeded = _writer_helper(file=fpr_output_filename) + success = success and write_succeeded + + # Count matrix filtered to cells only. + analyzed_barcode_logic = (posterior.latents_map['p'] > consts.CELL_PROB_CUTOFF) + write_succeeded = _writer_helper( + file=filtered_output_file, + analyzed_barcode_logic=analyzed_barcode_logic, + barcode_inds=posterior.dataset_obj.analyzed_barcode_inds[analyzed_barcode_logic], + ) + success = success and write_succeeded + + # Compile and save metrics. + try: + df = collect_output_metrics( + dataset_obj=posterior.dataset_obj, + inferred_count_matrix=denoised_counts, + fpr=fpr, + cell_logic=(posterior.latents_map['p'] >= consts.CELL_PROB_CUTOFF), + loss=posterior.vi_model.loss, + ) + metrics_file_name = os.path.join(file_dir, file_name + name_suffix + '_metrics.csv') + df.to_csv(metrics_file_name, index=True, header=False, float_format='%.3f') + logger.info(f'Saved output metrics as {metrics_file_name}') + except Exception: + logger.warning("Unable to collect output metrics.") + logger.warning(traceback.format_exc()) + + # Create report. + try: + os.environ['INPUT_FILE'] = os.path.abspath(os.path.join(os.getcwd(), args.input_file)) + os.environ['OUTPUT_FILE'] = os.path.abspath(os.path.join(os.getcwd(), fpr_output_filename)) + if args.truth_file is not None: + os.environ['TRUTH_FILE'] = os.path.abspath(os.path.join(os.getcwd(), args.truth_file)) + html_report_file = os.path.join(file_dir, file_name + name_suffix + '_report.html') + run_notebook_make_html( + file=os.path.abspath(os.path.join(os.path.dirname(__file__), 'report.ipynb')), + output=html_report_file, + ) + logger.info(f'Succeeded in writing report to {html_report_file}') + + except Exception: + logger.warning("Unable to create report.") + logger.warning(traceback.format_exc()) + + return success + + +def write_denoised_count_matrix(file: str, + denoised_count_matrix: sp.csr_matrix, + posterior_regularization: Optional[str], + posterior_regularization_kwargs: Optional[Dict[str, float]], + estimator: str, + estimator_kwargs: Optional[Dict[str, float]], + latents: Dict[str, np.ndarray], + dataset_obj: SingleCellRNACountsDataset, + learning_curve: Dict[str, np.ndarray], # inferred_model.loss + fpr: float, + analyzed_barcode_logic: np.ndarray = ..., + barcode_inds: np.ndarray = ...) -> bool: + """Helper function for writing output h5 files""" + + z = latents['z'][analyzed_barcode_logic, :] + d = latents['d'][analyzed_barcode_logic] + p = latents['p'][analyzed_barcode_logic] + epsilon = latents['epsilon'][analyzed_barcode_logic] + + # Inferred ambient gene expression vector. + ambient_expression_trimmed = pyro.param('chi_ambient').detach().cpu().numpy() + + # Convert the indices from trimmed gene set to original gene indices. + ambient_expression = np.zeros(dataset_obj.data['matrix'].shape[1]) + ambient_expression[dataset_obj.analyzed_gene_inds] = ambient_expression_trimmed + + # Some summary statistics: + # Fraction of counts in each droplet that were removed. + raw_count_matrix = dataset_obj.data['matrix'][dataset_obj.analyzed_barcode_inds, :] # need all genes + raw_counts_droplet = np.array(raw_count_matrix.sum(axis=1)).squeeze() + out_counts_droplet = np.array(denoised_count_matrix[dataset_obj.analyzed_barcode_inds, :] + .sum(axis=1)).squeeze() + background_fraction = ((raw_counts_droplet - out_counts_droplet) / + (raw_counts_droplet + 0.001))[analyzed_barcode_logic] + + # Handle the optional rho parameters. + rho = None + if (('rho_alpha' in pyro.get_param_store().keys()) + and ('rho_beta' in pyro.get_param_store().keys())): + rho = np.array([pyro.param('rho_alpha').detach().cpu().numpy().item(), + pyro.param('rho_beta').detach().cpu().numpy().item()]) + + # Determine metadata fields. + # Wrap in lists to avoid scanpy loading bug + # which may already be fixed by https://github.com/scverse/scanpy/pull/2344 + metadata = {'learning_curve': learning_curve, + 'barcodes_analyzed': dataset_obj.data['barcodes'][dataset_obj.analyzed_barcode_inds], + 'barcodes_analyzed_inds': dataset_obj.analyzed_barcode_inds, + 'features_analyzed_inds': dataset_obj.analyzed_gene_inds, + 'fraction_data_used_for_testing': 1. - consts.TRAINING_FRACTION, + 'target_false_positive_rate': fpr} + for k in ['posterior_regularization', 'posterior_regularization_kwargs', + 'estimator', 'estimator_kwargs']: + val = locals().get(k) # give me the input variable with this name + if val is not None: + if type(val) != dict: + if type(val) != list: + val = [val] # wrap in a list, unless it's a dict + metadata.update({k: val}) + + # Write h5. + write_succeeded = write_matrix_to_cellranger_h5( + cellranger_version=3, # always write v3 format output + output_file=file, + gene_names=dataset_obj.data['gene_names'], + gene_ids=dataset_obj.data['gene_ids'], + feature_types=dataset_obj.data['feature_types'], + genomes=dataset_obj.data['genomes'], + barcodes=dataset_obj.data['barcodes'][barcode_inds], + count_matrix=denoised_count_matrix[barcode_inds, :], + local_latents={'barcode_indices_for_latents': dataset_obj.analyzed_barcode_inds, + 'gene_expression_encoding': z, + 'cell_size': d, + 'cell_probability': p, + 'droplet_efficiency': epsilon, + 'background_fraction': background_fraction}, + global_latents={'ambient_expression': ambient_expression, + 'empty_droplet_size_lognormal_loc': pyro.param('d_empty_loc').item(), + 'empty_droplet_size_lognormal_scale': pyro.param('d_empty_scale').item(), + 'cell_size_lognormal_std': pyro.param('d_cell_scale').item(), + 'swapping_fraction_dist_params': rho}, + metadata=metadata, + ) + return write_succeeded + + +def collect_output_metrics(dataset_obj: SingleCellRNACountsDataset, + inferred_count_matrix: sp.csr_matrix, + fpr: Union[float, str], + cell_logic, + loss) -> pd.DataFrame: + """Create a table with a few output metrics. The idea is for these to + potentially be used by people creating automated pipelines.""" + + # Compute some metrics + input_count_matrix = dataset_obj.data['matrix'][dataset_obj.analyzed_barcode_inds, :] + total_raw_counts = dataset_obj.data['matrix'].sum() + total_output_counts = inferred_count_matrix.sum() + total_counts_removed = total_raw_counts - total_output_counts + fraction_counts_removed = total_counts_removed / total_raw_counts + total_raw_counts_in_nonempty_droplets = input_count_matrix[cell_logic].sum() + total_counts_removed_from_nonempty_droplets = \ + total_raw_counts_in_nonempty_droplets - inferred_count_matrix.sum() + fraction_counts_removed_from_nonempty_droplets = \ + total_counts_removed_from_nonempty_droplets / total_raw_counts_in_nonempty_droplets + average_counts_removed_per_nonempty_droplet = \ + total_counts_removed_from_nonempty_droplets / cell_logic.sum() + expected_cells = dataset_obj.priors['expected_cells'] + found_cells = cell_logic.sum() + average_counts_per_cell = inferred_count_matrix.sum() / found_cells + ratio_of_found_cells_to_expected_cells = \ + None if (expected_cells is None) else (found_cells / expected_cells) + found_empties = len(dataset_obj.analyzed_barcode_inds) - found_cells + fraction_of_analyzed_droplets_that_are_nonempty = \ + found_cells / len(dataset_obj.analyzed_barcode_inds) + if len(loss['train']['elbo']) > 20: + # compare mean ELBO increase over last 3 steps to the typical end(ish) fluctuations + convergence_indicator = (np.mean(np.abs([(loss['train']['elbo'][i] + - loss['train']['elbo'][i - 1]) + for i in range(-3, -1)])) + / np.std(loss['train']['elbo'][-20:])) + else: + convergence_indicator = 'not enough training epochs to compute (requires more than 20)' + if len(loss['train']['elbo']) > 0: + overall_change_in_train_elbo = loss['train']['elbo'][-1] - loss['train']['elbo'][0] + else: + overall_change_in_train_elbo = 0 # zero epoch initialization + + all_metrics_dict = \ + {'total_raw_counts': total_raw_counts, + 'total_output_counts': total_output_counts, + 'total_counts_removed': total_counts_removed, + 'fraction_counts_removed': fraction_counts_removed, + 'total_raw_counts_in_cells': + total_raw_counts_in_nonempty_droplets, + 'total_counts_removed_from_cells': + total_counts_removed_from_nonempty_droplets, + 'fraction_counts_removed_from_cells': + fraction_counts_removed_from_nonempty_droplets, + 'average_counts_removed_per_cell': + average_counts_removed_per_nonempty_droplet, + 'target_fpr': fpr, + 'expected_cells': expected_cells, + 'found_cells': found_cells, + 'output_average_counts_per_cell': average_counts_per_cell, + 'ratio_of_found_cells_to_expected_cells': + ratio_of_found_cells_to_expected_cells, + 'found_empties': found_empties, + 'fraction_of_analyzed_droplets_that_are_nonempty': + fraction_of_analyzed_droplets_that_are_nonempty, + 'convergence_indicator': convergence_indicator, + 'overall_change_in_train_elbo': overall_change_in_train_elbo} + + return pd.DataFrame(data=all_metrics_dict, + index=['metric']).transpose() + + +def write_cell_barcodes_csv(bc_file_name: str, cell_barcodes: np.ndarray): + """Write the cell barcode CSV file. + + Args: + bc_file_name: Output CSV file + cell_barcodes: Array of the cell barcode names + + """ + + # Save barcodes determined to contain cells as _cell_barcodes.csv + try: + barcode_names = np.array([str(cell_barcodes[i], encoding='UTF-8') + for i in range(cell_barcodes.size)]) + except UnicodeDecodeError: + # necessary if barcodes are ints + barcode_names = cell_barcodes + except TypeError: + # necessary if barcodes are already decoded + barcode_names = cell_barcodes + np.savetxt(bc_file_name, barcode_names, delimiter=',', fmt='%s') + logger.info(f"Saved cell barcodes in {bc_file_name}") + + +def get_optimizer(n_batches: int, + batch_size: int, + epochs: int, + learning_rate: float, + constant_learning_rate: bool, + total_epochs_for_testing_only: Optional[int] = None) \ + -> Union[pyro.optim.PyroOptim, pyro.optim.lr_scheduler.PyroLRScheduler]: + """Get optimizer or learning rate scheduler (if using one)""" + + # Set up the optimizer. + optimizer = pyro.optim.clipped_adam.ClippedAdam # just ClippedAdam does not work + optimizer_args = {'lr': learning_rate, 'clip_norm': 10.} + + # Set up a learning rate scheduler. + if total_epochs_for_testing_only is not None: + total_steps = n_batches * total_epochs_for_testing_only + else: + total_steps = n_batches * epochs + scheduler_args = {'optimizer': optimizer, + 'max_lr': learning_rate * 10, + 'total_steps': total_steps, + 'optim_args': optimizer_args} + scheduler = pyro.optim.OneCycleLR(scheduler_args) + + # Constant learning rate overrides the above and uses no scheduler. + if constant_learning_rate: + logger.info('Using ClippedAdam --constant-learning-rate rather than ' + 'the OneCycleLR schedule. This is not usually recommended.') + scheduler = ClippedAdam(optimizer_args) + + return scheduler + + +def run_inference(dataset_obj: SingleCellRNACountsDataset, + args: argparse.Namespace, + output_checkpoint_tarball: str = consts.CHECKPOINT_FILE_NAME, + total_epochs_for_testing_only: Optional[int] = None)\ + -> Tuple[RemoveBackgroundPyroModel, pyro.optim.PyroOptim, DataLoader, DataLoader]: + """Run a full inference procedure, training a latent variable model. + + Args: + dataset_obj: Input data in the form of a SingleCellRNACountsDataset + object. + args: Input command line parsed arguments. + output_checkpoint_tarball: Intended checkpoint tarball filepath. + total_epochs_for_testing_only: Hack for testing code using LR scheduler + + Returns: + model: cellbender.model.RemoveBackgroundPyroModel that has had + inference run. + + """ + + # Get the checkpoint file base name with hash, which we stored in args. + checkpoint_filename = args.checkpoint_filename + + # Configure pyro options (skip validations to improve speed). + pyro.enable_validation(False) + pyro.distributions.enable_validation(False) + + # Set random seed, updating global state of python, numpy, and torch RNGs. + pyro.clear_param_store() + pyro.set_rng_seed(consts.RANDOM_SEED) + if args.use_cuda: + torch.cuda.manual_seed_all(consts.RANDOM_SEED) + + # Attempt to load from a previously-saved checkpoint. + ckpt = attempt_load_checkpoint(filebase=checkpoint_filename, + tarball_name=args.input_checkpoint_tarball, + force_device='cuda:0' if args.use_cuda else 'cpu') + ckpt_loaded = ckpt['loaded'] # True if a checkpoint was loaded successfully + + if ckpt_loaded: + + model = ckpt['model'] + scheduler = ckpt['optim'] + train_loader = ckpt['train_loader'] + test_loader = ckpt['test_loader'] + if hasattr(ckpt['args'], 'num_failed_attempts'): + # update this from the checkpoint file, if present + args.num_failed_attempts = ckpt['args'].num_failed_attempts + for obj in [model, scheduler, train_loader, test_loader, args]: + assert obj is not None, \ + f'Expected checkpoint to contain model, scheduler, train_loader, ' \ + f'test_loader, and args; but some are None:\n{ckpt}' + logger.info('Checkpoint loaded successfully.') + + else: + + logger.info('No checkpoint loaded.') + + # Get the trimmed count matrix (transformed if called for). + count_matrix = dataset_obj.get_count_matrix() + + # Set up the variational autoencoder: + + # Encoder. + encoder_z = EncodeZ(input_dim=count_matrix.shape[1], + hidden_dims=args.z_hidden_dims, + output_dim=args.z_dim, + use_batch_norm=False, + use_layer_norm=False, + input_transform='normalize') + + encoder_other = EncodeNonZLatents(n_genes=count_matrix.shape[1], + z_dim=args.z_dim, + log_count_crossover=dataset_obj.priors['log_counts_crossover'], + prior_log_cell_counts=np.log1p(dataset_obj.priors['cell_counts']), + empty_log_count_threshold=np.log1p(dataset_obj.empty_UMI_threshold), + prior_logit_cell_prob=dataset_obj.priors['cell_logit'], + input_transform='log_normalize') + + encoder = CompositeEncoder({'z': encoder_z, + 'other': encoder_other}) + + # Decoder. + decoder = Decoder(input_dim=args.z_dim, + hidden_dims=args.z_hidden_dims[::-1], + use_batch_norm=True, + use_layer_norm=False, + output_dim=count_matrix.shape[1]) + + # Set up the pyro model for variational inference. + model = RemoveBackgroundPyroModel(model_type=args.model, + encoder=encoder, + decoder=decoder, + dataset_obj_priors=dataset_obj.priors, + n_analyzed_genes=dataset_obj.analyzed_gene_inds.size, + n_droplets=dataset_obj.analyzed_barcode_inds.size, + analyzed_gene_names=dataset_obj.data['gene_names'][dataset_obj.analyzed_gene_inds], + empty_UMI_threshold=dataset_obj.empty_UMI_threshold, + log_counts_crossover=dataset_obj.priors['log_counts_crossover'], + use_cuda=args.use_cuda) + + # Load the dataset into DataLoaders. + frac = args.training_fraction # Fraction of barcodes to use for training + batch_size = int(min(consts.MAX_BATCH_SIZE, frac * dataset_obj.analyzed_barcode_inds.size / 2)) + + # Set up dataloaders. + train_loader, test_loader = \ + prep_data_for_training(dataset=count_matrix, + empty_drop_dataset=dataset_obj.get_count_matrix_empties(), + batch_size=batch_size, + training_fraction=frac, + fraction_empties=args.fraction_empties, + shuffle=True, + use_cuda=args.use_cuda) + + # Set up optimizer (optionally wrapped in a learning rate scheduler). + scheduler = get_optimizer( + n_batches=len(train_loader), + batch_size=train_loader.batch_size, + epochs=args.epochs, + learning_rate=args.learning_rate, + constant_learning_rate=args.constant_learning_rate, + total_epochs_for_testing_only=total_epochs_for_testing_only, + ) + + # Determine the loss function. + if args.use_jit: + + # Call guide() once as a warm-up. + # model.guide(torch.zeros([10, dataset_obj.analyzed_gene_inds.size]).to(model.device)) + + if args.model == "simple": + loss_function = JitTrace_ELBO() + else: + loss_function = JitTraceEnum_ELBO(max_plate_nesting=1, + strict_enumeration_warning=False) + else: + + if args.model == "simple": + loss_function = Trace_ELBO() + else: + loss_function = TraceEnum_ELBO(max_plate_nesting=1) + + # Set up the inference process. + svi = SVI(model.model, model.guide, scheduler, loss=loss_function) + + # Run training. + if args.epochs == 0: + logger.info("Zero epochs specified... will only initialize the model.") + model.guide(train_loader.__next__()) + train_loader.reset_ptr() + + # Even though it's not much of a checkpoint, we still need one for subsequent steps. + save_checkpoint(filebase=checkpoint_filename, + tarball_name=output_checkpoint_tarball, + args=args, + model_obj=model, + scheduler=svi.optim, + train_loader=train_loader, + test_loader=test_loader) + + else: + logger.info("Running inference...") + try: + run_training(model=model, + args=args, + svi=svi, + train_loader=train_loader, + test_loader=test_loader, + epochs=args.epochs, + test_freq=5, + output_filename=checkpoint_filename, + ckpt_tarball_name=output_checkpoint_tarball, + checkpoint_freq=args.checkpoint_min, + epoch_elbo_fail_fraction=args.epoch_elbo_fail_fraction, + final_elbo_fail_fraction=args.final_elbo_fail_fraction) + + except ElboException: + + logger.warning(traceback.format_exc()) + + # Keep track of number of failed attempts. + if not hasattr(args, 'num_failed_attempts'): + args.num_failed_attempts = 1 + else: + args.num_failed_attempts = args.num_failed_attempts + 1 + logger.debug(f'Training failed, and the number of failed attempts ' + f'on record is {args.num_failed_attempts}') + + # Retry training with reduced learning rate, if indicated by user. + logger.debug(f'Number of times to retry training is {args.num_training_tries}') + if args.num_failed_attempts < args.num_training_tries: + args.learning_rate = args.learning_rate * args.learning_rate_retry_mult + logger.info(f'Restarting training: attempt {args.num_failed_attempts + 1}, ' + f'learning_rate = {args.learning_rate}') + run_remove_background(args) # start from scratch + sys.exit(0) + else: + logger.info(f'No more attempts are specified by --num-training-tries. ' + f'Therefore the workflow will abort here.') + sys.exit(1) + + logger.info("Inference procedure complete.") + + return model, scheduler, train_loader, test_loader diff --git a/cellbender/remove_background/sparse_utils.py b/cellbender/remove_background/sparse_utils.py new file mode 100644 index 0000000..4a0f26f --- /dev/null +++ b/cellbender/remove_background/sparse_utils.py @@ -0,0 +1,101 @@ +"""Utility functions for working with scipy sparse matrices""" + +import torch +import numpy as np +import scipy.sparse as sp + +from typing import Optional, Tuple, Iterable + + +@torch.no_grad() +def dense_to_sparse_op_torch(t: torch.Tensor, + tensor_for_nonzeros: Optional[torch.Tensor] = None) \ + -> Tuple[np.ndarray, ...]: + """Converts dense matrix to sparse COO format tuple of numpy arrays (*indices, data) + + Args: + t: The Tensor + tensor_for_nonzeros: If this is not None, then this tensor will be used + to determine nonzero indices, while the values will come from t. + + Returns: + Tuple of + (nonzero_inds_dim0, nonzero_inds_dim1, ...) + (nonzero_values, ) + + """ + + if tensor_for_nonzeros is None: + tensor_for_nonzeros = t + + nonzero_inds_tuple = torch.nonzero(tensor_for_nonzeros, as_tuple=True) + nonzero_values = t[nonzero_inds_tuple].flatten() + + return tuple([ten.cpu().numpy() for ten in (nonzero_inds_tuple + (nonzero_values,))]) + + +def log_prob_sparse_to_dense(coo: sp.coo_matrix) -> np.ndarray: + """Densify a sparse log prob COO data structure. Same as coo_matrix.todense() + except it fills missing entries with -np.inf instead of 0, since 0 is a + very meaningful quantity for log prob. + """ + return todense_fill(coo=coo, fill_value=-np.inf) + + +def todense_fill(coo: sp.coo_matrix, fill_value: float) -> np.ndarray: + """Densify a sparse COO matrix. Same as coo_matrix.todense() + except it fills missing entries with fill_value instead of 0. + """ + dummy_value = np.nan if not np.isnan(fill_value) else np.inf + dummy_check = np.isnan if np.isnan(dummy_value) else np.isinf + coo = coo.copy().astype(float) + coo.data[coo.data == 0] = dummy_value + out = np.array(coo.todense()).squeeze() + out[out == 0] = fill_value + out[dummy_check(out)] = 0 + return out + + +def csr_set_rows_to_zero(csr: sp.csr_matrix, + row_inds: Iterable[int]) -> sp.csr_matrix: + """Set all nonzero elements in rows "row_inds" to zero. + Happens in-place, although output is returned as well. + + https://stackoverflow.com/questions/12129948/scipy-sparse-set-row-to-zeros + """ + + if not isinstance(csr, sp.csr_matrix): + try: + csr = csr.tocsr() + except Exception: + raise ValueError('Matrix given must be of CSR format.') + for row in row_inds: + csr.data[csr.indptr[row]:csr.indptr[row + 1]] = 0 + csr.eliminate_zeros() + return csr + + +def overwrite_matrix_with_columns_from_another(mat1: sp.csc_matrix, + mat2: sp.csc_matrix, + column_inds: np.ndarray) -> sp.csc_matrix: + """Given two sparse matrices of the same shape, replace columns that are not + in `column_inds` in `mat1` with the entries from `mat2`. + """ + column_inds = set(column_inds) + + mat1 = mat1.copy().tocsr() + mat2 = mat2.copy().tocsr() # failure to copy could overwrite actual count data + + # Zero out values in mat2 that are in the specified columns. + inds = np.where([i in column_inds for i in mat2.indices])[0] + mat2.data[inds] = 0 + mat2.eliminate_zeros() + + # Zero out values in mat1 that are not in the specified columns. + inds = np.where([i not in column_inds for i in mat1.indices])[0] + mat1.data[inds] = 0 + mat1.eliminate_zeros() + + # Put in the new values by addition. + output = mat1 + mat2 + + return output.tocsc() diff --git a/cellbender/remove_background/tests/benchmarking/README.md b/cellbender/remove_background/tests/benchmarking/README.md new file mode 100644 index 0000000..e52c142 --- /dev/null +++ b/cellbender/remove_background/tests/benchmarking/README.md @@ -0,0 +1,62 @@ +# Benchmarking CellBender versions and changes + +## Running benchmarking tests + +There is a benchmarking WDL in /cellbender/remove_background/tests/benchmarking called +`benchmark.wdl`. This can be run using a local installation of cromshell +(`conda install -c bioconda cromshell`) by running the python script in +`run_benchmark.py` as in + +```console +python run_benchmark.py \ + --git "v0.2.1" \ + --input "gs://broad-dsde-methods-sfleming/cellbender_test/pbmc8k_raw_gene_bc_matrices.h5" \ + --sample pbmc8k +``` + +The `--git` input can be a full commit sha or a tag or a branch. Anything that +can be pip installed via `pip install -y git+https://github.com/broadinstitute/CellBender.git@` + +(The caveat here is that older versions of CellBender cannot be installed from +github in this way, so this is a test that will work moving forward from +commit `cb2d209d5aedffe71e28947bc5b7859600aef64d`) + +### Datasets from the paper + +To re-run the datasets that were analyzed in the CellBender paper, plus +several other interesting benchmarks, try running + +```console +./run_usual_benchmarks.sh b9d2953c76c538d13549290bd986af4e6a1780d5 +``` + +You can check their status via `cromshell` using + +```console +cromshell list -u | tail -n 5 +``` + +## Validation of outputs + +Validation is comprised of a Terra notebook in the following workspace: +https://app.terra.bio/#workspaces/broad-firecloud-dsde-methods/sfleming_dev + +To prepare a data table for the Terra workspace (once all jobs +have completed): + +```console +python run_benchmark_result_tabulation.py \ + --workflows b9d2953c76c538d13549290bd986af4e6a1780d5 \ + --output samples.tsv +``` + +where, for example, `b9d2953c76c538d13549290bd986af4e6a1780d5` is +the git hash used to run `run_usual_benchmarks.sh`. (The script will `grep` +the local cromshell database for all runs that match. More than one search +term can be used.) + +Then manually upload the `samples.tsv` to Terra as a data table. + +Open the validation notebook and enter the git commit +`b9d2953c76c538d13549290bd986af4e6a1780d5` at the top. Then +run the entire notebook to produce plots. diff --git a/cellbender/remove_background/tests/benchmarking/benchmark.wdl b/cellbender/remove_background/tests/benchmarking/benchmark.wdl new file mode 100644 index 0000000..6a0781b --- /dev/null +++ b/cellbender/remove_background/tests/benchmarking/benchmark.wdl @@ -0,0 +1,29 @@ +version 1.0 + +import "cellbender_remove_background.wdl" as cellbender + +## Copyright Broad Institute, 2023 +## +## LICENSING : +## This script is released under the WDL source code license (BSD-3) +## (see LICENSE in https://github.com/openwdl/wdl). + + +workflow run_cellbender_benchmark { + input { + String? git_hash + } + + call cellbender.run_cellbender_remove_background_gpu as cb { + input: + dev_git_hash__=git_hash + + } + + output { + File log = cb.log + File summary_pdf = cb.pdf + Array[File] html_report_array = cb.report_array + Array[File] h5_array = cb.h5_array + } +} diff --git a/cellbender/remove_background/tests/benchmarking/benchmark_inputs.json b/cellbender/remove_background/tests/benchmarking/benchmark_inputs.json new file mode 100644 index 0000000..dea4a2e --- /dev/null +++ b/cellbender/remove_background/tests/benchmarking/benchmark_inputs.json @@ -0,0 +1,19 @@ +{ + "run_cellbender_benchmark.git_hash": "GIT_HASH", + "run_cellbender_benchmark.cb.input_file_unfiltered": "INPUT_GSURL", + "run_cellbender_benchmark.cb.sample_name": "SAMPLE_NAME", + "run_cellbender_benchmark.cb.truth_file": "TRUTH_GSURL", + "run_cellbender_benchmark.cb.exclude_feature_types": "Peaks", + "run_cellbender_benchmark.cb.expected_cells": null, + "run_cellbender_benchmark.cb.total_droplets_included": null, + "run_cellbender_benchmark.cb.low_count_threshold": null, + "run_cellbender_benchmark.cb.fpr": "FPR", + "run_cellbender_benchmark.cb.debug": false, + "run_cellbender_benchmark.cb.checkpoint_mins": 200, + "run_cellbender_benchmark.cb.checkpoint_file": null, + "run_cellbender_benchmark.cb.hardware_preemptible_tries": 0, + "run_cellbender_benchmark.cb.hardware_cpu_count": 8, + "run_cellbender_benchmark.cb.hardware_memory_GB": 64, + "run_cellbender_benchmark.cb.estimator_multiple_cpu": null, + "run_cellbender_benchmark.cb.docker_image": "us.gcr.io/broad-dsde-methods/cellbender:0.2.1" +} \ No newline at end of file diff --git a/cellbender/remove_background/tests/benchmarking/cuda_check_inputs.json b/cellbender/remove_background/tests/benchmarking/cuda_check_inputs.json new file mode 100644 index 0000000..b9a3c46 --- /dev/null +++ b/cellbender/remove_background/tests/benchmarking/cuda_check_inputs.json @@ -0,0 +1,4 @@ +{ + "check_pytorch_cuda_status.run_check_pytorch_cuda_status.docker_image": "us.gcr.io/broad-dsde-methods/cellbender:20230427" +} + diff --git a/cellbender/remove_background/tests/benchmarking/docker_image_check_cuda_status.wdl b/cellbender/remove_background/tests/benchmarking/docker_image_check_cuda_status.wdl new file mode 100644 index 0000000..8763bfe --- /dev/null +++ b/cellbender/remove_background/tests/benchmarking/docker_image_check_cuda_status.wdl @@ -0,0 +1,38 @@ +version 1.0 + +## Copyright Broad Institute, 2023 +## +## LICENSING : +## This script is released under the WDL source code license (BSD-3) +## (see LICENSE in https://github.com/openwdl/wdl). + +task run_check_pytorch_cuda_status { + input { + String docker_image + Int? hardware_boot_disk_size_GB = 20 + String? hardware_zones = "us-east1-d us-east1-c us-central1-a us-central1-c us-west1-b" + String? hardware_gpu_type = "nvidia-tesla-t4" + String? nvidia_driver_version = "470.82.01" # need >=465.19.01 for CUDA 11.3 + } + command { + set -e + python < argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Benchmark a specific commit of CellBender on a dataset " + "using cromshell.", + ) + + parser.add_argument('-g', '--git', + type=str, + required=False, + default=None, + dest='git_hash', + help='Specific github commit from the CellBender repo: ' + 'can be a tag, a branch, or a commit sha. Not ' + 'including the flag will run using the base ' + 'docker image specified in benchmark_inputs.json') + parser.add_argument('-i', '--input', + type=str, + required=True, + dest='input_file', + help='Input path (must be a gsURL, i.e. ' + '"gs://bucket/path/file.h5")') + parser.add_argument('-s', '--sample', + type=str, + required=True, + dest='sample', + help='Sample name: used to create output file names') + parser.add_argument('-t', '--truth', + type=str, + required=False, + default=None, + dest='truth_file', + help='Truth data path for simulated data (must be a ' + 'gsURL, i.e. "gs://bucket/path/file.h5")') + parser.add_argument('-f', '--fpr', + type=str, + required=False, + default="0.01", + dest='fpr', + help='FPR for CellBender') + parser.add_argument('-q', '--quiet', + action='store_true', + default=False, + dest='quiet', + help='Include this flag to avoid printing to stdout') + + return parser + + +def update_input_json(template_file: str, + substitutions: Dict[str, str], + tmpdir: tempfile.TemporaryDirectory, + verbose: bool = True) -> Tuple[str, str]: + """Create a new input json for a Cromwell run based on a template""" + + # read template into dict + with open(template_file, mode='r') as f: + template = json.load(f) + + # perform substitutions + for key, value in template.items(): + replacement = substitutions.get(value, '__absent') + template[key] = replacement if (replacement != '__absent') else value + + # write filled-in input json + json_inputs_file = os.path.join(tmpdir, 'inputs.json') + with open(json_inputs_file, mode='w') as f: + json.dump(template, f, indent=2) + + # write options json that disables call-caching + json_options_file = os.path.join(tmpdir, 'options.json') + options_dict = { + "write_to_cache": False, + "read_from_cache": False, + } + with open(json_options_file, mode='w') as f: + json.dump(options_dict, f, indent=2) + + if verbose: + print('Contents of inputs.json:') + subprocess.run(['cat', json_inputs_file]) + print('', end='\n') + + if verbose: + print('Contents of options.json:') + subprocess.run(['cat', json_options_file]) + print('', end='\n') + + return json_inputs_file, json_options_file + + +def cromshell_submit(wdl: str, + inputs: str, + options: str, + dependencies: List[str], + tmpdir: tempfile.TemporaryDirectory, + alias: Optional[str] = None, + verbose: bool = True) -> Tuple[str, str]: + """Submit job via cromshell and return the workflow-id and alias + + NOTE: the whole dependency zipping thing is way more difficult than it has + to be and took me hours. it is super fragile. but at least now I think I + can put this benchmark.wdl anywhere I want to. + """ + + # zip up dependencies + if verbose: + print('Zipping dependencies') + dependencies_zip = os.path.join(tmpdir, 'dependencies.zip') + dependencies_dir = tmpdir + for file in dependencies: + shutil.copy(file, dependencies_dir) + subprocess.run(['zip', '-j', dependencies_zip, + ' '.join([os.path.join(dependencies_dir, os.path.basename(f)) + for f in dependencies])]) + + # move WDL to tmpdir + tmp_wdl = os.path.join(tmpdir, os.path.basename(wdl)) + shutil.copy(wdl, tmp_wdl) + + submit_cmd = ['cromshell', 'submit', + tmp_wdl, + inputs, + options, + dependencies_zip] + + # submit job + if verbose: + print(f'Submitting WDL {tmp_wdl}') + current_path = os.getcwd() + os.chdir(tmpdir) + out = subprocess.run(submit_cmd) + os.chdir(current_path) + out.check_returncode() # error if this failed + + # get workflow-id + out = subprocess.run(['cromshell', 'status'], capture_output=True) + d = json.loads(out.stdout) + workflow_id = d.get('id') + + # alias job + if alias is not None: + # solve the issue where an alias cannot be made twice + hash = random.getrandbits(128) + alias = alias + '__runhash_' + str(hash)[:10] + subprocess.run(['cromshell', 'alias', '-1', alias]) + + return workflow_id, alias + + +if __name__ == '__main__': + + # handle input arguments + parser = get_parser() + args = parser.parse_args(sys.argv[1:]) + # args = validate_args(args) + + with tempfile.TemporaryDirectory() as tmpdir: + + # determine substitutions to be made in input json + substitutions = { + 'GIT_HASH': args.git_hash, + 'INPUT_GSURL': args.input_file, + 'SAMPLE_NAME': args.sample, + 'TRUTH_GSURL': args.truth_file, + 'FPR': args.fpr, + } + + # create the updated input json + inputs_json, options_json = update_input_json( + template_file='benchmark_inputs.json', + substitutions=substitutions, + tmpdir=tmpdir, + verbose=not args.quiet, + ) + + # get the path to the cellbender WDL + this_dir = os.path.dirname(os.path.abspath(__file__)) + cellbender_wdl_path = os.path.abspath(os.path.join( + this_dir, '..', '..', '..', '..', 'wdl', + 'cellbender_remove_background.wdl', + )) + + # run cromshell submit + if args.git_hash is not None: + alias = '_'.join(['cellbender', args.sample, args.git_hash]) + else: + alias = '_'.join(['cellbender', args.sample]) + workflow_id, alias = cromshell_submit( + wdl=os.path.join(this_dir, 'benchmark.wdl'), + inputs=inputs_json, + options=options_json, + dependencies=[cellbender_wdl_path], + alias=alias, + tmpdir=tmpdir, + ) + + # show workflow-id alias + if not args.quiet: + print('Sucessfully submitted job:') + if alias is not None: + print(alias) + print(workflow_id) + + sys.exit(0) diff --git a/cellbender/remove_background/tests/benchmarking/run_benchmark_result_tabulation.py b/cellbender/remove_background/tests/benchmarking/run_benchmark_result_tabulation.py new file mode 100644 index 0000000..ffc5e5a --- /dev/null +++ b/cellbender/remove_background/tests/benchmarking/run_benchmark_result_tabulation.py @@ -0,0 +1,146 @@ +"""Tabulate outputs from CellBender benchmarking as a samples.tsv for Terra +Example: + $ conda activate cromshell + $ python run_benchmark_result_tabulation.py \ + --workflows 2927346f4c513a217ac8ad076e494dd1adbf70e1 \ + --output "samples.tsv" +""" + +import pandas as pd +import os +import re +import sys +import subprocess +import argparse +from io import StringIO +from typing import Tuple, Dict, Optional, List, Union + + +def get_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Create a Terra-compatible data table TSV from cromshell " + "benchmarking outputs.", + ) + parser.add_argument('-w', '--workflows', + type=str, + required=True, + dest='grep', + help='grep to choose workflows from the cromshell database') + parser.add_argument('-n', '--note', + type=str, + required=True, + dest='note', + help='Annotation describing the git commit') + parser.add_argument('-o', '--output', + type=str, + required=True, + dest='output_file', + help='Output TSV file') + + return parser + + +def find_cromshell_workflow_ids_dates(grep: str) -> Tuple[List[str], List[str]]: + """Find workflows in the cromshell database matching some string""" + + date_col = 0 + workflow_id_col = 2 + + # use cromshell list to get the database, then comb through it + out = subprocess.run(['cromshell', 'list'], capture_output=True) + s = out.stdout.decode() + _RE_COMBINE_WHITESPACE = re.compile(r"(?a: +)") + s = _RE_COMBINE_WHITESPACE.sub(" ", s).strip() + greps = grep.split(' ') # separately match all search elements (space delimited) + selected_rows = [r for r in s.split('\n') if all([g in r for g in greps])] + + for r in selected_rows: + if ('Succeeded' not in r): + print(f'WARNING: Skipping\n{r}\nbecause the run has not completed') + selected_rows = [r for r in selected_rows if ('Succeeded' in r)] + + workflow_ids = [r.split(' ')[workflow_id_col] for r in selected_rows] + dates = [r.split(' ')[date_col] for r in selected_rows] + + return workflow_ids, dates + + +def get_cromshell_output_h5(workflow: str, grep: str = '_out.h5') -> Union[str, List[str]]: + """Use cromshell list-outputs to get the relevant file gsURL""" + + output = grep_from_command(['cromshell', 'list-outputs', workflow], grep=grep) + out = output[:-1].decode().split('\n') + if len(out) > 1: + return out + else: + return out[0] + + +def sample_name_from_h5(h5: str) -> str: + """Get sample name by parsing h5 file name""" + return os.path.basename(h5).replace('_out.h5', '') + + +def grep_from_command(command: List[str], grep: str) -> bytes: + """Pipe a command to grep and give the output""" + ps = subprocess.Popen(command, stdout=subprocess.PIPE) + output = subprocess.check_output(['grep', grep], stdin=ps.stdout) + ps.wait() + return output + + +def metadata_from_workflow_id(workflow: str) -> Tuple[str, str, Optional[str]]: + """Get other metadata for a workflow id: git hash, input file, truth file""" + + # git hash + output = grep_from_command(['cromshell', 'metadata', workflow], + grep='"git_hash":') + git_hash = output[17:-3].decode() + + # input file + output = grep_from_command(['cromshell', 'metadata', workflow], + grep='run_cellbender_benchmark.cb.input_file_unfiltered') + input_file = output[58:-3].decode() + + # truth file + output = grep_from_command(['cromshell', 'metadata', workflow], + grep='run_cellbender_benchmark.cb.truth_file') + if 'null' not in output.decode(): + truth_file = output[47:-3].decode() + else: + truth_file = None + + return git_hash, input_file, truth_file + + +if __name__ == '__main__': + + # handle input arguments + parser = get_parser() + args = parser.parse_args(sys.argv[1:]) + # args = validate_args(args) + + workflow_ids, dates = find_cromshell_workflow_ids_dates(args.grep) + output_h5s = [get_cromshell_output_h5(id) for id in workflow_ids] + samples = [sample_name_from_h5(h5) for h5 in output_h5s] + list_of_tuples = [metadata_from_workflow_id(id) for id in workflow_ids] + if len(list_of_tuples) > 0: + git_hashes, input_h5s, truth_h5s = zip(*list_of_tuples) + + run_ids = [sample + '_' + git for sample, git in zip(samples, git_hashes)] + + df = pd.DataFrame(data={'entity:sample_id': run_ids, + 'git_commit': git_hashes, + 'sample': samples, + 'output_h5': output_h5s, + 'input_h5': input_h5s, + 'truth_h5': truth_h5s, + 'cromwell_workflow_id': workflow_ids, + 'date_time': dates, + 'note': args.note}) + + df.to_csv(args.output_file, sep='\t', index=False) + else: + print('No submissions selected') + + sys.exit(0) diff --git a/cellbender/remove_background/tests/benchmarking/run_usual_benchmarks.sh b/cellbender/remove_background/tests/benchmarking/run_usual_benchmarks.sh new file mode 100755 index 0000000..98c50f7 --- /dev/null +++ b/cellbender/remove_background/tests/benchmarking/run_usual_benchmarks.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# Example run: +# $ ./run_usual_benchmarks.sh "v0.2.1" + +GIT_HASH=$1 + +# from the paper ========================== + +# 10x Genomics pbmc8k +python run_benchmark.py \ + --git $GIT_HASH \ + --input "gs://broad-dsde-methods-sfleming/cellbender_test/pbmc8k_raw_gene_bc_matrices.h5" \ + --sample "pbmc8k" + +# 10x Genomics hgmm12k +python run_benchmark.py \ + --git $GIT_HASH \ + --input "gs://broad-dsde-methods-sfleming/cellbender_test/hgmm_12k_raw_gene_bc_matrices.h5" \ + --sample "hgmm12k" + +# 10x Genomics pbmc5k CITE-seq +python run_benchmark.py \ + --git $GIT_HASH \ + --input "gs://broad-dsde-methods-sfleming/cellbender_test/5k_pbmc_protein_v3_nextgem_raw_feature_bc_matrix.h5" \ + --sample "pbmc5k" \ + --fpr "0.1" + +# Broad PCL rat6k +python run_benchmark.py \ + --git $GIT_HASH \ + --input "gs://broad-dsde-methods-sfleming/cellbender_test/PCL_rat_A_LA6_raw_feature_bc_matrix.h5" \ + --sample "rat6k" + +# Simulation s1 +python run_benchmark.py \ + --git $GIT_HASH \ + --input "gs://broad-dsde-methods-sfleming/cellbender_test/s1.h5" \ + --truth "gs://broad-dsde-methods-sfleming/cellbender_test/s1_truth.h5" \ + --sample "s1" + +# Simulation s4 +python run_benchmark.py \ + --git $GIT_HASH \ + --input "gs://broad-dsde-methods-sfleming/cellbender_test/s4.h5" \ + --truth "gs://broad-dsde-methods-sfleming/cellbender_test/s4_truth.h5" \ + --sample "s4" \ + --fpr "0.2" + +# Simulation s7, hgmm +python run_benchmark.py \ + --git $GIT_HASH \ + --input "gs://broad-dsde-methods-sfleming/cellbender_test/s7.h5" \ + --truth "gs://broad-dsde-methods-sfleming/cellbender_test/s7_truth.h5" \ + --sample "s7" + +# additional datasets ======================== + +# Broad PCL human PV dataset, hard time calling high-count cells in v0.2.0, wobbly learning curve +python run_benchmark.py \ + --git $GIT_HASH \ + --input "gs://broad-dsde-methods-sfleming/cellbender_test/PCL_human_PV_1817_ls.h5" \ + --sample "pv20k" + +python run_benchmark.py \ + --git $GIT_HASH \ + --input "gs://broad-dsde-methods-sfleming/cellbender_test/PCL_human_PV_1799_BPV.h5" \ + --sample "pv15k" + +# public data from Caroline Porter, high UMI count cells called empty +python run_benchmark.py \ + --git $GIT_HASH \ + --input "gs://broad-dsde-methods-sfleming/cellbender_test/cporter_20230331_N18_Epi_A.h5" \ + --sample "epi2k" diff --git a/cellbender/remove_background/tests/conftest.py b/cellbender/remove_background/tests/conftest.py new file mode 100644 index 0000000..8bafa2d --- /dev/null +++ b/cellbender/remove_background/tests/conftest.py @@ -0,0 +1,202 @@ +"""Test utility functions and session-scoped fixtures.""" + +import pytest +import scipy.sparse as sp +import numpy as np +import torch + +from cellbender.remove_background.data.extras.simulate import \ + generate_sample_dirichlet_dataset +from cellbender.remove_background.data.io import write_matrix_to_cellranger_h5 + +import shutil + + +USE_CUDA = torch.cuda.is_available() + + +def sparse_matrix_equal(mat1: sp.csc_matrix, + mat2: sp.csc_matrix): + """Fast assertion that sparse matrices are equal""" + return (mat1 != mat2).sum() == 0 + + +def tensors_equal(a: torch.Tensor, + b: torch.Tensor): + """Assertion that torch tensors are equal for each element""" + return (a == b).all() + + +def string_ndarray_equality(a: np.ndarray, b: np.ndarray) -> bool: + return (a.astype(str) == b.astype(str)).all() + + +class SavedFileH5: + def __init__(self, name, keys, version, shape, analyzed_shape=None, + local_keys=None, global_keys=None, meta_keys=None, barcodes_analyzed=None): + self.name = name + self.version = version + self.keys = keys + self.shape = shape + self.analyzed_shape = analyzed_shape + self.local_keys = local_keys + self.global_keys = global_keys + self.meta_keys = meta_keys + self.barcodes_analyzed = barcodes_analyzed + + def __repr__(self): + return f'Saved h5 file ({self.shape}) at {self.name} ' \ + f'(CellRanger v{self.version} with keys {self.keys})' + + +@pytest.fixture(scope='session') +def simulated_dataset(): + """Generate a small simulated dataset Dict once and make it visible to all tests""" + d = generate_sample_dirichlet_dataset( + n_droplets=100, + n_genes=1000, + chi_artificial_similarity=0.25, + cells_of_each_type=[10, 10, 10], + cell_mean_umi=[5000, 5000, 5000], + cell_lognormal_sigma=0.01, + empty_mean_umi=200, + empty_lognormal_sigma=0.01, + model_type='full', + dirichlet_alpha=0.05, + epsilon_param=20, + rho_alpha=4, + rho_beta=96, + random_seed=0, + ) + d['genomes'] = np.array(['simulated'] * d['gene_names'].size) + d['gene_ids'] = np.array(['ENSEMBLSIM000' + s for s in d['gene_names']]) + d['feature_types'] = np.array(['Gene Expression'] * (d['gene_names'].size - 50) + + ['Antibody Capture'] * 40 + + ['CRISPR Guide Capture'] * 5 + + ['Custom'] * 5) # a mix of types + d['matrix'] = (d['counts_true'] + d['counts_bkg']).tocsc() + return d + + +@pytest.fixture(scope='session') +def h5_v3_file(tmpdir_factory, simulated_dataset) -> 'SavedFileH5': + """Save an h5 file once and make it visible to all tests.""" + tmp_dir = tmpdir_factory.mktemp('data') + filename = tmp_dir.join('tmp_v3.h5') + cellranger_version = 3 + d = simulated_dataset + write_matrix_to_cellranger_h5( + output_file=filename, + gene_names=d['gene_names'], + gene_ids=d['gene_ids'], + feature_types=d['feature_types'], + genomes=d['genomes'], + barcodes=d['barcodes'], + count_matrix=d['matrix'], + cellranger_version=cellranger_version, + ) + yield SavedFileH5(name=filename, + keys=['gene_names', 'barcodes', 'genomes', 'gene_ids', 'feature_types'], + version=cellranger_version, + shape=d['matrix'].shape) + shutil.rmtree(str(tmp_dir)) + + +@pytest.fixture(scope='session') +def h5_v2_file(tmpdir_factory, simulated_dataset) -> 'SavedFileH5': + """Save an h5 file once and make it visible to all tests.""" + tmp_dir = tmpdir_factory.mktemp('data') + filename = tmp_dir.join('tmp_v2.h5') + cellranger_version = 2 + d = simulated_dataset + write_matrix_to_cellranger_h5( + output_file=filename, + gene_names=d['gene_names'], + gene_ids=d['gene_ids'], + # feature_types=d['feature_types'], + # genomes=d['genomes'], + barcodes=d['barcodes'], + count_matrix=d['matrix'], + cellranger_version=cellranger_version, + ) + yield SavedFileH5(name=filename, + keys=['gene_names', 'barcodes', 'gene_ids'], + version=cellranger_version, + shape=d['matrix'].shape) + shutil.rmtree(str(tmp_dir)) + + +@pytest.fixture(scope='session') +def h5_v2_file_missing_ids(tmpdir_factory, simulated_dataset) -> 'SavedFileH5': + """Save an h5 file once and make it visible to all tests.""" + tmp_dir = tmpdir_factory.mktemp('data') + filename = tmp_dir.join('tmp_v2.h5') + cellranger_version = 2 + d = simulated_dataset + write_matrix_to_cellranger_h5( + output_file=filename, + gene_names=d['gene_names'], + # gene_ids=d['gene_ids'], + # feature_types=d['feature_types'], + # genomes=d['genomes'], + barcodes=d['barcodes'], + count_matrix=d['matrix'], + cellranger_version=cellranger_version, + ) + yield SavedFileH5(name=filename, + keys=['gene_names', 'barcodes'], + version=cellranger_version, + shape=d['matrix'].shape) + shutil.rmtree(str(tmp_dir)) + + +@pytest.fixture(scope='session', params=[2, 3]) +def h5_file(request, h5_v2_file, h5_v3_file): + if request.param == 2: + return h5_v2_file + elif request.param == 3: + return h5_v3_file + else: + raise ValueError(f'Test error: requested v{request.param} h5 file') + + +@pytest.fixture(scope='session') +def h5_v3_file_post_inference(tmpdir_factory, simulated_dataset) -> 'SavedFileH5': + """Save an h5 file once and make it visible to all tests.""" + tmp_dir = tmpdir_factory.mktemp('data') + filename = tmp_dir.join('tmp_v3_inferred.h5') + cellranger_version = 3 + d = simulated_dataset + + barcodes_analyzed = 50 + + write_matrix_to_cellranger_h5( + output_file=filename, + gene_names=d['gene_names'], + gene_ids=d['gene_ids'], + feature_types=d['feature_types'], + genomes=d['genomes'], + barcodes=d['barcodes'], + count_matrix=d['matrix'], + cellranger_version=cellranger_version, + local_latents={'barcode_indices_for_latents': np.arange(0, barcodes_analyzed), + 'gene_expression_encoding': np.random.rand(barcodes_analyzed, 10), + 'cell_probability': np.random.rand(barcodes_analyzed), + 'd': np.random.rand(barcodes_analyzed) + 5.}, + global_latents={'global_var1': np.array([1, 2, 3])}, + metadata={'metadata1': np.array(0.9), + 'metadata2': {'key1': 'val1', 'key2': {'a': 'val2', 'b': 'val3'}}, + 'metadata3': None, + 'metadata4': 0.5, + 'metadata5': 'text'}, + ) + yield SavedFileH5(name=filename, + keys=['gene_names', 'barcodes', 'genomes', 'gene_ids', 'feature_types'], + version=cellranger_version, + shape=d['matrix'].shape, + analyzed_shape=(barcodes_analyzed, d['matrix'].shape[1]), + local_keys=['d', 'cell_probability', 'gene_expression_encoding'], + global_keys=['global_var1'], + meta_keys=['metadata1'], + barcodes_analyzed=barcodes_analyzed) + shutil.rmtree(str(tmp_dir)) diff --git a/cellbender/remove_background/tests/mckp_memory_profiling.py b/cellbender/remove_background/tests/mckp_memory_profiling.py new file mode 100644 index 0000000..fe6e831 --- /dev/null +++ b/cellbender/remove_background/tests/mckp_memory_profiling.py @@ -0,0 +1,146 @@ +"""Script to enable memory usage profiling via memory-profiler""" + +from cellbender.remove_background.estimation import MultipleChoiceKnapsack +from cellbender.remove_background.sparse_utils import csr_set_rows_to_zero +from cellbender.remove_background.checkpoint import load_from_checkpoint +from cellbender.remove_background.posterior import Posterior, \ + compute_mean_target_removal_as_function +from cellbender.remove_background.data.dataset import SingleCellRNACountsDataset +from cellbender.remove_background import consts + +import scipy.sparse as sp +import numpy as np +from memory_profiler import profile + +import argparse +import sys + + +def get_parser() -> argparse.ArgumentParser: + parser_ = argparse.ArgumentParser( + description="Run memory profiling on output count matrix generation. " + "NOTE that you have to decorate " + "MultipleChoiceKnapsack.estimate_noise() with memory_profiler's " + "@profile() decorator manually.", + ) + parser_.add_argument('-f', '--checkpoint-file', + type=str, + required=True, + dest='input_checkpoint_tarball', + help='Saved CellBender checkpoint file ckpt.tar.gz') + parser_.add_argument('-i', '--input', + type=str, + required=True, + dest='input_file', + help='Input data file') + return parser_ + + +def compute_noise_counts(posterior, + fpr: float, + estimator_constructor: 'EstimationMethod', + **kwargs) -> sp.csr_matrix: + """Probably the most important method: computation of the clean output count matrix. + + Args: + estimator_constructor: A noise count estimator class derived from + the EstimationMethod base class, and implementing the + .estimate_noise() method, which creates a point estimate of + noise. Pass in the constructor, not an object. + **kwargs: Keyword arguments for estimator_constructor().estimate_noise() + + Returns: + denoised_counts: Denoised output CSC sparse matrix (CSC for saving) + + """ + + # Only compute using defaults if the cache is empty. + if posterior._noise_count_regularized_posterior_coo is not None: + # Priority is taken by a regularized posterior, since presumably + # the user computed it for a reason. + posterior_coo = posterior._noise_count_regularized_posterior_coo + else: + # Use exact posterior if a regularized version is not computed. + posterior_coo = (posterior._noise_count_posterior_coo + if (posterior._noise_count_posterior_coo is not None) + else posterior.cell_noise_count_posterior_coo()) + + # Instantiate Estimator object. + estimator = estimator_constructor(index_converter=posterior.index_converter) + + # Compute point estimate of noise in cells. + noise_targets = get_noise_targets(posterior=posterior, fpr=fpr) + noise_csr = estimator.estimate_noise( + estimator=estimator, + noise_log_prob_coo=posterior_coo, + noise_offsets=posterior._noise_count_posterior_coo_offsets, + noise_targets_per_gene=noise_targets, + **kwargs, + ) + + return noise_csr + + +def get_noise_targets(posterior, fpr=0.01): + count_matrix = posterior.dataset_obj.data['matrix'] # all barcodes + cell_inds = posterior.dataset_obj.analyzed_barcode_inds[posterior.latents_map['p'] + > consts.CELL_PROB_CUTOFF] + non_cell_row_logic = np.array([i not in cell_inds + for i in range(count_matrix.shape[0])]) + cell_counts = csr_set_rows_to_zero(csr=count_matrix, row_logic=non_cell_row_logic) + + noise_target_fun_per_cell = compute_mean_target_removal_as_function( + noise_count_posterior_coo=posterior._noise_count_posterior_coo, + noise_offsets=posterior._noise_count_posterior_coo_offsets, + index_converter=posterior.index_converter, + raw_count_csr_for_cells=cell_counts, + n_cells=len(cell_inds), + device='cpu', + per_gene=True, + ) + noise_target_fun = lambda x: noise_target_fun_per_cell(x) * len(cell_inds) + noise_targets = noise_target_fun(fpr).detach().cpu().numpy() + return noise_targets + + +if __name__ == "__main__": + + # handle input arguments + parser = get_parser() + args = parser.parse_args(sys.argv[1:]) + + # load checkpoint + ckpt = load_from_checkpoint(tarball_name=args.input_checkpoint_tarball, + filebase=None, + to_load=['model', 'posterior', 'args'], + force_device='cpu') + + # load dataset + dataset_obj = \ + SingleCellRNACountsDataset(input_file=args.input_file, + expected_cell_count=ckpt['args'].expected_cell_count, + total_droplet_barcodes=ckpt['args'].total_droplets, + fraction_empties=ckpt['args'].fraction_empties, + model_name=ckpt['args'].model, + gene_blacklist=ckpt['args'].blacklisted_genes, + exclude_features=ckpt['args'].exclude_features, + low_count_threshold=ckpt['args'].low_count_threshold, + ambient_counts_in_cells_low_limit=ckpt['args'].ambient_counts_in_cells_low_limit, + fpr=ckpt['args'].fpr) + + # load posterior + posterior = Posterior( + dataset_obj=dataset_obj, + vi_model=ckpt['model'], + posterior_batch_size=ckpt['args'].posterior_batch_size, + debug=False, + ) + posterior.load(file=ckpt['posterior_file']) + + # run output count matrix generation + compute_noise_counts(posterior=posterior, + fpr=0.01, + estimator_constructor=MultipleChoiceKnapsack, + approx_gb=0.1) + + sys.exit(0) diff --git a/cellbender/remove_background/tests/miniwdl_check_wdl.sh b/cellbender/remove_background/tests/miniwdl_check_wdl.sh new file mode 100755 index 0000000..2da9ad8 --- /dev/null +++ b/cellbender/remove_background/tests/miniwdl_check_wdl.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euxo pipefail + +# runs from the root directory of the repo + +# find all WDL files +# WDL_FILES=$(find . -type f -name "*.wdl") +WDL_FILES=("./wdl/cellbender_remove_background.wdl") + +for WDL in ${WDL_FILES}; do + miniwdl check ${WDL} +done \ No newline at end of file diff --git a/cellbender/remove_background/tests/test.py b/cellbender/remove_background/tests/test.py deleted file mode 100644 index 5f72ec8..0000000 --- a/cellbender/remove_background/tests/test.py +++ /dev/null @@ -1,193 +0,0 @@ -import unittest -import os -import warnings - -import cellbender -import cellbender.remove_background.model -from cellbender.remove_background.train import run_inference -from cellbender.remove_background.data.extras.simulate import \ - simulate_dataset_with_ambient_rna -from cellbender.remove_background.data.dataset import \ - SingleCellRNACountsDataset, write_matrix_to_cellranger_h5, \ - get_matrix_from_cellranger_h5, get_matrix_from_anndata -import numpy as np -import anndata -import sys - - -class TestConsole(unittest.TestCase): - - def test_data_simulation_and_write_and_read(self): - """Run basic tests of data simulation and read and write functionality. - - Test the ability to read/write data to/from an HDF5 file using pytables. - This necessitates creating a small simulated dataset, writing it to - a temporary file, reading the file back in, and deleting the - temporary file. - - """ - - try: - - # This is here to suppress the numpy warning triggered by - # scipy.sparse. - warnings.simplefilter("ignore") - - # Generate a simulated dataset with ambient RNA. - n_cells = 100 - csr_barcode_gene_synthetic, _, chi, _ = \ - simulate_dataset_with_ambient_rna(n_cells=n_cells, - n_empty=3 * n_cells, - clusters=1, n_genes=1000, - d_cell=2000, d_empty=100, - ambient_different=False) - - # Generate some names for genes and barcodes. - gene_names = np.array([f'g_{i}' for i in - range(csr_barcode_gene_synthetic.shape[1])]) - barcode_names = \ - np.array([f'bc_{i}' for i in - range(csr_barcode_gene_synthetic.shape[0])]) - - # Save the data to a temporary cellranger h5 file. - temp_file_name = 'testfile.h5' - write_matrix_to_cellranger_h5(cellranger_version=3, - output_file=temp_file_name, - loss=None, - gene_names=gene_names, - barcodes=barcode_names, - inferred_count_matrix= - csr_barcode_gene_synthetic.tocsc(), - ambient_expression=chi[0, :]) - - # Save the data to a temporary AnnData file. - temp_h5ad_name = 'testfile.h5ad' - temp_adata = anndata.AnnData( - X=csr_barcode_gene_synthetic, - ) - temp_adata.var_names = gene_names - temp_adata.obs_names = barcode_names - temp_adata.write_h5ad(temp_h5ad_name) - - # Read the data back in. - reconstructed = get_matrix_from_cellranger_h5(temp_file_name) - new_matrix = reconstructed['matrix'] - - # Check that the data matches. - assert (csr_barcode_gene_synthetic.sum(axis=1) == - new_matrix.sum(axis=1)).all(), \ - "Saved and re-opened data is not accurate." - assert (csr_barcode_gene_synthetic.sum(axis=0) == - new_matrix.sum(axis=0)).all(), \ - "Saved and re-opened data is not accurate." - - # Remove the temporary file. - os.remove(temp_file_name) - - # Check that anndata matches. - reconstructed_h5ad = get_matrix_from_anndata(temp_h5ad_name) - matrix_h5ad = reconstructed_h5ad['matrix'] - assert (csr_barcode_gene_synthetic.sum(axis=1) == - matrix_h5ad.sum(axis=1)).all(), \ - "Saved and re-opened AnnData is not accurate." - assert (csr_barcode_gene_synthetic.sum(axis=0) == - matrix_h5ad.sum(axis=0)).all(), \ - "Saved and re-opened AnnData is not accurate." - - os.remove(temp_h5ad_name) - - return 1 - - except TestConsole.failureException: - - return 0 - - def test_inference(self): - """Run a basic tests doing inference on a synthetic dataset. - - Runs the inference procedure on CPU. - - """ - - try: - - n_cells = 100 - - # Generate a simulated dataset with ambient RNA. - csr_barcode_gene_synthetic, _, _, _ = \ - simulate_dataset_with_ambient_rna(n_cells=n_cells, - n_empty=3 * n_cells, - clusters=1, n_genes=1000, - d_cell=2000, d_empty=100, - ambient_different=False) - - # Fake some parsed command line inputs. - args = ObjectWithAttributes() - args.use_cuda = False - args.z_hidden_dims = [100] - args.d_hidden_dims = [10, 2] - args.p_hidden_dims = [100, 10] - args.z_dim = 10 - args.learning_rate = 0.001 - args.epochs = 10 - args.model = "full" - args.fraction_empties = 0.5 - args.use_jit = True - args.training_fraction = 0.9 - - args.expected_cell_count = n_cells - - # Wrap simulated count matrix in a Dataset object. - dataset_obj = SingleCellRNACountsDataset() - dataset_obj.data = \ - {'matrix': csr_barcode_gene_synthetic, - 'gene_names': - np.array([f'g{n}' for n in - range(csr_barcode_gene_synthetic.shape[1])]), - 'barcodes': - np.array([f'bc{n}' for n in - range(csr_barcode_gene_synthetic.shape[0])])} - dataset_obj.priors['n_cells'] = n_cells - dataset_obj._trim_dataset_for_analysis() - dataset_obj._estimate_priors() - - # Run inference on this simulated dataset. - inferred_model = run_inference(dataset_obj, args) - - # Get encodings from the trained model. - z, d, p = cellbender.remove_background.model.\ - get_encodings(inferred_model, dataset_obj) - - # Make the background-subtracted dataset. - inferred_count_matrix = cellbender.remove_background.model.\ - generate_maximum_a_posteriori_count_matrix(z, d, p, - inferred_model, - dataset_obj) - - # Get the inferred background RNA expression from the model. - ambient_expression = cellbender.remove_background.model.\ - get_ambient_expression_from_pyro_param_store() - - return 1 - - except TestConsole.failureException: - - return 0 - - -class ObjectWithAttributes(object): - """Exists only to populate the args data structure with attributes.""" - pass - - -def main(): - """Run unit tests.""" - - tester = TestConsole() - - passed_tests = 0 - - passed_tests += tester.test_data_simulation_and_write_and_read() - passed_tests += tester.test_inference() - - sys.stdout.write(f'Passed {passed_tests} of 2 tests.\n\n') diff --git a/cellbender/remove_background/tests/test_checkpoint.py b/cellbender/remove_background/tests/test_checkpoint.py new file mode 100644 index 0000000..59ccb9e --- /dev/null +++ b/cellbender/remove_background/tests/test_checkpoint.py @@ -0,0 +1,626 @@ +"""Tests for checkpointing functions.""" + +import pytest +import random +import numpy as np +import torch +from torch.distributions import constraints +import pyro +import pyro.optim as optim + +import cellbender +from cellbender.remove_background.checkpoint import make_tarball, unpack_tarball, \ + create_workflow_hashcode, save_checkpoint, load_checkpoint, \ + save_random_state, load_random_state +from cellbender.remove_background.vae.encoder import EncodeZ, EncodeNonZLatents, \ + CompositeEncoder +from cellbender.remove_background.vae.decoder import Decoder +from cellbender.remove_background.data.extras.simulate import \ + generate_sample_dirichlet_dataset, get_dataset_dict_as_anndata +from cellbender.remove_background.data.dataprep import prep_sparse_data_for_training +from cellbender.remove_background.model import RemoveBackgroundPyroModel +from cellbender.remove_background.data.dataset import SingleCellRNACountsDataset +from cellbender.remove_background.run import run_inference +from .conftest import USE_CUDA + +import os +import argparse +import shutil +import subprocess +from typing import List + + +class RandomState: + def __init__(self, use_cuda=False): + self.python = random.randint(0, 100000) + self.numpy = np.random.randint(0, 100000, size=1).item() + self.torch = torch.randint(low=0, high=100000, size=[1], device='cpu').item() + self.use_cuda = use_cuda + if self.use_cuda: + self.cuda = torch.randint(low=0, high=100000, size=[1], device='cuda').item() + + def __repr__(self): + if self.use_cuda: + return f'python {self.python}; numpy {self.numpy}; torch {self.torch}; torch_cuda {self.cuda}' + else: + return f'python {self.python}; numpy {self.numpy}; torch {self.torch}' + + +def test_create_workflow_hashcode(): + """Ensure workflow hashcodes are behaving as expected""" + + tmp_args1 = argparse.Namespace(epochs=100, expected_cells=1000, use_cuda=True) + tmp_args2 = argparse.Namespace(epochs=200, expected_cells=1000, use_cuda=True) + tmp_args3 = argparse.Namespace(epochs=100, expected_cells=500, use_cuda=True) + hashcode1 = create_workflow_hashcode(module_path=os.path.dirname(cellbender.__file__), + args=tmp_args1) + hashcode2 = create_workflow_hashcode(module_path=os.path.dirname(cellbender.__file__), + args=tmp_args2) + hashcode3 = create_workflow_hashcode(module_path=os.path.dirname(cellbender.__file__), + args=tmp_args3) + + # make sure it changes for different arguments + assert hashcode1 != hashcode3 + + # but that the "epochs" argument has no effect + assert hashcode1 == hashcode2 + + +def create_random_state_blank_slate(seed, use_cuda=USE_CUDA): + """Establish a base random state + https://pytorch.org/docs/stable/notes/randomness.html + """ + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + pyro.util.set_rng_seed(seed) + if use_cuda: + torch.cuda.manual_seed_all(seed) + + +def perturb_random_state(n, use_cuda=USE_CUDA): + """Perturb the base random state by drawing random numbers""" + for _ in range(n): + random.randint(0, 10) + np.random.randint(0, 10, 1) + torch.randn((1,), device='cpu') + if use_cuda: + torch.randn((1,), device='cuda') + + +@pytest.fixture(scope='function', params=[0, 1, 1234], ids=lambda x: f'seed{x}') +def random_state_blank_slate(request): + create_random_state_blank_slate(request.param) + return request.param + + +@pytest.fixture(scope='function', params=[0, 1, 10]) +def perturbed_random_state_dict(request, random_state_blank_slate): + perturb_random_state(request.param) + return {'state': RandomState(), + 'seed': random_state_blank_slate, + 'n': request.param} + + +@pytest.fixture(scope='function', params=[0], ids=lambda x: f'setseed{x}') +def random_state_blank_slate0(request): + create_random_state_blank_slate(request.param) + return request.param + + +@pytest.fixture(scope='function', params=[10], ids=lambda x: f'set{x}') +def perturbed_random_state0_dict(request, random_state_blank_slate0): + perturb_random_state(request.param) + return {'state': RandomState(), + 'seed': random_state_blank_slate0, + 'n': request.param} + + +def test_perturbedrandomstate_fixture_meets_expectations(perturbed_random_state0_dict, + perturbed_random_state_dict): + """Test the setup of these randomstate fixtures. + + The state0 fixture is one set of params. + The state fixture is a combinatorial set of params, only one of which matches + the state0 setup. + + We want to make sure that when we have fixtures set up the same way, then + randomness behaves the same (and different when set up differently). + """ + prs = perturbed_random_state_dict['state'] + params = (perturbed_random_state_dict['seed'], perturbed_random_state_dict['n']) + prs0 = perturbed_random_state0_dict['state'] + params0 = (perturbed_random_state0_dict['seed'], perturbed_random_state0_dict['n']) + + if params == params0: + # this is the only case in which we expect the two random states to be equal + assert str(prs0) == str(prs) + else: + assert str(prs0) != str(prs) + + +def test_that_randomstate_plus_perturb_gives_perturbedrandomstate( + perturbed_random_state0_dict): + """Make sure perturbation is working as intended""" + + # recreate and make sure we end up in the same place + # the "recipe" is in perturbed_random_state0_tuple[1] + create_random_state_blank_slate(perturbed_random_state0_dict['seed']) + perturb_random_state(perturbed_random_state0_dict['n']) + this_prs0 = RandomState() + + # check equality + prs0 = perturbed_random_state0_dict['state'] + assert str(prs0) == str(this_prs0) + + +@pytest.mark.parametrize('cuda', + [False, + pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, + reason='requires CUDA'))], + ids=lambda b: 'cuda' if b else 'cpu') +def test_save_and_load_random_state(tmpdir_factory, perturbed_random_state_dict, cuda): + """Test whether random states are being preserved correctly. + perturbed_random_state_dict is important since it initializes the state.""" + + # save the random states + filebase = tmpdir_factory.mktemp('random_states').join('tmp_checkpoint') + save_random_state(filebase=filebase) + + # see "what would have happened had we continued" + counterfactual = RandomState(use_cuda=cuda) + incorrect = RandomState(use_cuda=cuda) # a second draw + + # load the random states and check random number generators + load_random_state(filebase=filebase) + actual = RandomState(use_cuda=cuda) + + # check equality + assert str(counterfactual) == str(actual) + assert str(incorrect) != str(actual) + + +def new_train_loader(data: torch.Tensor, batch_size: int, shuffle: bool = True): + return torch.utils.data.DataLoader(data, batch_size=batch_size, shuffle=shuffle) + + +class PyroModel(torch.nn.Module): + + def __init__(self, dim=8, hidden_layer=4, z_dim=2): + super(PyroModel, self).__init__() + create_random_state_blank_slate(0) + pyro.clear_param_store() + self.encoder = EncodeZ(input_dim=dim, hidden_dims=[hidden_layer], output_dim=z_dim) + self.decoder = Decoder(input_dim=z_dim, hidden_dims=[hidden_layer], output_dim=dim) + self.z_dim = z_dim + self.use_cuda = torch.cuda.is_available() + self.normal = pyro.distributions.Normal + self.loss = [] + # self.to(device='cuda' if self.use_cuda else 'cpu') # CUDA not tested + + def model(self, x: torch.FloatTensor): + pyro.module("decoder", self.decoder, update_module_params=True) + with pyro.plate('plate', size=x.shape[0]): + z = pyro.sample('z', self.normal(loc=0., scale=1.).expand_by([x.shape[0], self.z_dim]).to_event(1)) + x_rec = self.decoder(z) + pyro.sample('obs', self.normal(loc=x_rec, scale=0.1).to_event(1), obs=x) + + def guide(self, x: torch.FloatTensor): + pyro.module("encoder", self.encoder, update_module_params=True) + with pyro.plate('plate', size=x.shape[0]): + enc = self.encoder(x) + pyro.sample('z', self.normal(loc=enc['loc'], scale=enc['scale']).to_event(1)) + + +def train_pyro(n_epochs: int, + data_loader: torch.utils.data.DataLoader, + svi: pyro.infer.SVI): + """Run training""" + loss_per_epoch = [] + for _ in range(n_epochs): + loss = 0 + norm = 0 + for data in data_loader: + loss += svi.step(data) + norm += data.size(0) + loss_per_epoch.append(loss / norm) + return loss_per_epoch + + +def _check_all_close(tensors1, tensors2) -> bool: + """For two lists of tensors, check that they are all close""" + assert len(tensors1) == len(tensors2), \ + 'Must pass in same number of tensors to check if they are equal' + equal = True + for t1, t2 in zip(tensors1, tensors2): + equal = equal and torch.allclose(t1, t2) + return equal + + +def _get_params(module: torch.nn.Module) -> List[torch.Tensor]: + return [p.data.clone() for p in module.parameters()] + + +@pytest.mark.parametrize('batch_size_n', [32, 128], ids=lambda n: f'batch{n}') +def test_save_and_load_pyro_checkpoint(tmpdir_factory, batch_size_n): + """Check and see if restarting from a checkpoint picks up in the same place + we left off. Use a dataloader. + """ + + filedir = tmpdir_factory.mktemp('ckpt') + filebase = filedir.join('ckpt') + dim = 8 + epochs = 3 + epochs2 = 3 + lr = 1e-2 + + # data and dataloader + dataset = torch.randn((128, dim)) + train_loader = new_train_loader(data=dataset, batch_size=batch_size_n) + + # create an ML model + initial_model = PyroModel(dim=dim) + + # set up the inference process + scheduler = optim.ClippedAdam({'lr': lr, 'clip_norm': 10.}) + svi = pyro.infer.SVI(initial_model.model, initial_model.guide, scheduler, loss=pyro.infer.Trace_ELBO()) + w1 = _get_params(initial_model.encoder) + + print('initial weight matrix =========================') + print('\n'.join([str(t) for t in w1])) + + # train in two parts: part 1 + initial_model.loss.extend(train_pyro(n_epochs=epochs, data_loader=train_loader, svi=svi)) + + print('no_ckpt trained round 1 (saved) ===============') + # print(initial_model.encoder.linears[0].weight.data) + print('\n'.join([str(t) for t in _get_params(initial_model.encoder)])) + + # save + save_successful = save_checkpoint(filebase=str(filebase), + args=argparse.Namespace(), + model_obj=initial_model, # TODO + scheduler=scheduler, + train_loader=train_loader, + tarball_name=str(filebase) + '.tar.gz') + assert save_successful, 'Failed to save checkpoint during test_save_and_load_checkpoint' + + # load from checkpoint + create_random_state_blank_slate(0) + pyro.clear_param_store() + ckpt = load_checkpoint(filebase=str(filebase), + tarball_name=str(filebase) + '.tar.gz', + force_device='cpu') + model_ckpt = ckpt['model'] + scheduler_ckpt = ckpt['optim'] + train_loader = ckpt['train_loader'] + s = ckpt['loaded'] + print('model_ckpt loaded =============================') + print('\n'.join([str(t) for t in _get_params(model_ckpt.encoder)])) + + matches = _check_all_close(_get_params(model_ckpt.encoder), + _get_params(initial_model.encoder)) + print(f'model_ckpt loaded matches data from (saved) trained round 1: {matches}') + + # clean up before most assertions... hokey now due to lack of fixture usage here + shutil.rmtree(str(filedir)) + + # and continue training + assert s is True, 'Checkpoint loading failed during test_save_and_load_checkpoint' + svi_ckpt = pyro.infer.SVI(model_ckpt.model, model_ckpt.guide, scheduler_ckpt, loss=pyro.infer.Trace_ELBO()) + rng_ckpt = RandomState() + guide_trace_ckpt = pyro.poutine.trace(svi_ckpt.guide).get_trace(x=dataset) + model_ckpt.loss.extend(train_pyro(n_epochs=epochs2, data_loader=train_loader, svi=svi_ckpt)) + + print('model_ckpt after round 2 ======================') + print('\n'.join([str(t) for t in _get_params(model_ckpt.encoder)])) + + # one-shot training straight through + model_one_shot = PyroModel(dim=dim) # resets random state + scheduler = optim.ClippedAdam({'lr': lr, 'clip_norm': 10.}) + train_loader = new_train_loader(data=dataset, batch_size=batch_size_n) + svi_one_shot = pyro.infer.SVI(model_one_shot.model, model_one_shot.guide, scheduler, loss=pyro.infer.Trace_ELBO()) + model_one_shot.loss.extend(train_pyro(n_epochs=epochs, data_loader=train_loader, svi=svi_one_shot)) + rng_one_shot = RandomState() + guide_trace_one_shot = pyro.poutine.trace(svi_one_shot.guide).get_trace(x=dataset) + model_one_shot.loss.extend(train_pyro(n_epochs=epochs2, data_loader=train_loader, svi=svi_one_shot)) + + assert str(rng_one_shot) == str(rng_ckpt), \ + 'Random states of the checkpointed and non-checkpointed versions at ' \ + 'the start of training round 2 do not match.' + + print('model_one_shot ================================') + print('\n'.join([str(t) for t in _get_params(model_one_shot.encoder)])) + + print(model_one_shot.loss) + print(model_ckpt.loss) + print([l1 == l2 for l1, l2 in zip(model_one_shot.loss, model_ckpt.loss)]) + + # training should be doing something to the initial weight matrix + assert (w1[0] != _get_params(model_one_shot.encoder)[0]).sum().item() > 0, \ + 'Training is not changing the weight matrix in test_save_and_load_checkpoint' + assert (w1[0] != _get_params(model_ckpt.encoder)[0]).sum().item() > 0, \ + 'Training is not changing the checkpointed weight matrix in test_save_and_load_checkpoint' + + # see if we end up where we should + assert _check_all_close(_get_params(model_one_shot.encoder), + _get_params(model_ckpt.encoder)), \ + 'Checkpointed restart does not agree with one-shot training' + + # check guide traces + print('checking guide trace nodes for agreement:') + for name in guide_trace_one_shot.nodes: + if (name != '_RETURN') and ('value' in guide_trace_one_shot.nodes[name].keys()): + print(name) + disagreement = (guide_trace_one_shot.nodes[name]['value'].data + != guide_trace_ckpt.nodes[name]['value'].data).sum() + print(f'number of values that disagree: {disagreement}') + assert disagreement == 0, \ + 'Guide traces disagree with and without checkpoint restart' + + +@pytest.mark.parametrize('cuda', + [False, + pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, + reason='requires CUDA'))], + ids=lambda b: 'cuda' if b else 'cpu') +@pytest.mark.parametrize('scheduler', + [False, True], + ids=lambda b: 'OneCycleLR' if b else 'Adam') +def test_save_and_load_cellbender_checkpoint(tmpdir_factory, cuda, scheduler): + """Check and see if restarting from a checkpoint picks up in the same place + we left off. Use our model and dataloader. + """ + + filedir = tmpdir_factory.mktemp('ckpt') + + epochs = 5 + epochs2 = 5 + + # data + n_genes = 2000 + dataset = generate_sample_dirichlet_dataset(n_genes=n_genes, cells_of_each_type=[100], + n_droplets=2000, model_type='ambient', + cell_mean_umi=[5000]) + adata = get_dataset_dict_as_anndata(dataset) + adata_file = os.path.join(filedir, 'sim.h5ad') + adata.write(adata_file) + dataset_obj = \ + SingleCellRNACountsDataset(input_file=adata_file, + expected_cell_count=100, + total_droplet_barcodes=1000, + fraction_empties=0.1, + model_name='ambient', + gene_blacklist=[], + exclude_features=[], + low_count_threshold=15, + fpr=[0.01]) + + # set up the inference process + args = argparse.Namespace() + args.output_file = os.path.join(filedir, 'out.h5') + args.z_dim = 10 + args.z_hidden_dims = [50] + args.model = 'ambient' + args.use_cuda = cuda + args.use_jit = False + args.learning_rate = 1e-3 + args.training_fraction = 0.9 + args.fraction_empties = 0.1 + args.checkpoint_filename = 'test01234_' + args.checkpoint_min = 5 + args.epoch_elbo_fail_fraction = None + args.final_elbo_fail_fraction = None + args.constant_learning_rate = not scheduler + args.debug = False + args.input_checkpoint_tarball = 'none' + + create_random_state_blank_slate(0) + pyro.clear_param_store() + args.epochs = -1 # I am hacking my way around an error induced by saving a checkpoint for 0 epoch runs + initial_model, scheduler, _, _ = run_inference(dataset_obj=dataset_obj, + args=args) + w1 = _get_params(initial_model.encoder['z']) + print('encoder structure ============') + print(initial_model.encoder['z']) + + print('initial weight matrix =========================') + print('\n'.join([str(t) for t in w1])) + + # train in two parts: part 1 + pyro.clear_param_store() + create_random_state_blank_slate(0) + args.epochs = epochs + initial_model, scheduler, train_loader, test_loader = \ + run_inference(dataset_obj=dataset_obj, args=args, + output_checkpoint_tarball='none', + total_epochs_for_testing_only=epochs + epochs2) + + print('no_ckpt trained round 1 (saved) ===============') + print('\n'.join([str(t) for t in _get_params(initial_model.encoder['z'])])) + # print(scheduler.get_state().keys()) + # print(list(scheduler.get_state().values())[0].keys()) + # print(list(list(scheduler.optim_objs.values())[0].optimizer.state_dict().values())[0].values()) + # assert 0 + + # save + hashcode = create_workflow_hashcode(module_path=os.path.dirname(cellbender.__file__), + args=args)[:10] + checkpoint_filename = os.path.basename(args.output_file).split('.')[0] + '_' + hashcode + args.checkpoint_filename = checkpoint_filename + filebase = filedir.join(checkpoint_filename) + save_successful = save_checkpoint(filebase=str(filebase), + args=args, + model_obj=initial_model, # TODO + scheduler=scheduler, + tarball_name=str(filebase) + '.tar.gz', + train_loader=train_loader, + test_loader=test_loader) + assert save_successful, 'Failed to save checkpoint during test_save_and_load_checkpoint' + assert os.path.exists(str(filebase) + '.tar.gz'), 'Checkpoint should exist but does not' + + # load from checkpoint (automatically) and run + pyro.clear_param_store() + create_random_state_blank_slate(0) + args.epochs = -1 + args.input_checkpoint_tarball = str(filebase) + '.tar.gz' + model_ckpt, scheduler, _, _ = run_inference(dataset_obj=dataset_obj, args=args, + output_checkpoint_tarball='none') + + print('model_ckpt loaded =============================') + print('\n'.join([str(t) for t in _get_params(model_ckpt.encoder['z'])])) + + matches = _check_all_close(_get_params(model_ckpt.encoder['z']), + _get_params(initial_model.encoder['z'])) + print(f'model_ckpt loaded matches data from (saved) trained round 1: {matches}') + + # and continue training + create_random_state_blank_slate(0) + pyro.clear_param_store() + args.epochs = epochs + epochs2 + model_ckpt, scheduler, _, _ = run_inference(dataset_obj=dataset_obj, args=args, + output_checkpoint_tarball='none') + + print('model_ckpt after round 2 ======================') + print('\n'.join([str(t) for t in _get_params(model_ckpt.encoder['z'])])) + + # clean up the temp directory to remove checkpoint before running the one-shot + shutil.rmtree(str(filedir)) + + # one-shot training straight through + pyro.clear_param_store() + create_random_state_blank_slate(0) + args.epochs = epochs + epochs2 + args.input_checkpoint_tarball = 'none' + model_one_shot, scheduler, _, _ = run_inference(dataset_obj=dataset_obj, args=args, + output_checkpoint_tarball='none') + + print('model_one_shot ================================') + print('\n'.join([str(t) for t in _get_params(model_one_shot.encoder['z'])])) + + print('loss for model_one_shot:') + print(model_one_shot.loss) + print('loss for model_ckpt:') + print(model_ckpt.loss) + print([l1 == l2 for l1, l2 in zip(model_one_shot.loss, model_ckpt.loss)]) + + # training should be doing something to the initial weight matrix + assert (w1[0] != _get_params(model_one_shot.encoder['z'])[0]).sum().item() > 0, \ + 'Training is not changing the weight matrix in test_save_and_load_checkpoint' + assert (w1[0] != _get_params(model_ckpt.encoder['z'])[0]).sum().item() > 0, \ + 'Training is not changing the checkpointed weight matrix in test_save_and_load_checkpoint' + + # see if we end up where we should + assert _check_all_close(_get_params(model_one_shot.encoder['z']), + _get_params(model_ckpt.encoder['z'])), \ + 'Checkpointed restart does not agree with one-shot training' + + # # check guide traces + # print('checking guide trace nodes for agreement:') + # for name in guide_trace_one_shot.nodes: + # if (name != '_RETURN') and ('value' in guide_trace_one_shot.nodes[name].keys()): + # print(name) + # disagreement = (guide_trace_one_shot.nodes[name]['value'].data + # != guide_trace_ckpt.nodes[name]['value'].data).sum() + # print(f'number of values that disagree: {disagreement}') + # assert disagreement == 0, \ + # 'Guide traces disagree with and without checkpoint restart' + + +@pytest.mark.parametrize( + "Optim, config", + [ + (optim.ClippedAdam, {"lr": 0.01}), + (optim.ExponentialLR, {"optimizer": torch.optim.SGD, + "optim_args": {"lr": 0.01}, + "gamma": 0.9}), + ], +) +def test_optimizer_checkpoint_restart(Optim, config, tmpdir_factory): + """This code has been copied from a version of a pyro test by Fritz Obermeyer""" + + tempdir = tmpdir_factory.mktemp('ckpt') + + def model(): + x_scale = pyro.param("x_scale", torch.tensor(1.0), + constraint=constraints.positive) + z = pyro.sample("z", pyro.distributions.Normal(0, 1)) + return pyro.sample("x", pyro.distributions.Normal(z, x_scale), obs=torch.tensor(0.1)) + + def guide(): + z_loc = pyro.param("z_loc", torch.tensor(0.0)) + z_scale = pyro.param( + "z_scale", torch.tensor(0.5), constraint=constraints.positive + ) + pyro.sample("z", pyro.distributions.Normal(z_loc, z_scale)) + + store = pyro.get_param_store() + + def get_snapshot(optimizer): + s = {k: v.data.clone() for k, v in store.items()} + if type(optimizer) == optim.lr_scheduler.PyroLRScheduler: + lr = list(optimizer.optim_objs.values())[0].get_last_lr()[0] + else: + lr = list(optimizer.optim_objs.values())[0].param_groups[0]['lr'] + s.update({'lr': f'{lr:.4f}'}) + return s + + # Try without a checkpoint. + expected = [] + store.clear() + pyro.set_rng_seed(20210811) + optimizer = Optim(config.copy()) + svi = pyro.infer.SVI(model, guide, optimizer, pyro.infer.Trace_ELBO()) + for _ in range(5 + 10): + svi.step() + expected.append(get_snapshot(optimizer)) + if type(optimizer) == optim.lr_scheduler.PyroLRScheduler: + svi.optim.step() + del svi, optimizer + + # Try with a checkpoint. + actual = [] + store.clear() + pyro.set_rng_seed(20210811) + optimizer = Optim(config.copy()) + svi = pyro.infer.SVI(model, guide, optimizer, pyro.infer.Trace_ELBO()) + for _ in range(5): + svi.step() + actual.append(get_snapshot(optimizer)) + if type(optimizer) == optim.lr_scheduler.PyroLRScheduler: + svi.optim.step() + + # checkpoint + optim_filename = os.path.join(tempdir, "optimizer_state.pt") + param_filename = os.path.join(tempdir, "param_store.pt") + optimizer.save(optim_filename) + store.save(param_filename) + del optimizer, svi + store.clear() + + # load from checkpoint + store.load(param_filename) + optimizer = Optim(config.copy()) + optimizer.load(optim_filename) + svi = pyro.infer.SVI(model, guide, optimizer, pyro.infer.Trace_ELBO()) + for _ in range(10): + svi.step() + actual.append(get_snapshot(optimizer)) + if type(optimizer) == optim.lr_scheduler.PyroLRScheduler: + svi.optim.step() + + # display learning rates and actual/expected values for z_loc + print(repr(optimizer)) + print('epoch\t\tlr\t\tactual\t\t\t\texpected') + print('-' * 100) + for i, (ac, ex) in enumerate(zip(actual, expected)): + print('\t\t'.join([f'{x}' for x in [i, ac['lr'], ac['z_loc'], ex['z_loc']]])) + if i == 4: + print('-' * 100) + + # ensure actual matches expected + for actual_t, expected_t in zip(actual, expected): + actual_t.pop('lr') + expected_t.pop('lr') + for actual_value, expected_value in zip(actual_t.values(), expected_t.values()): + assert torch.allclose(actual_value, expected_value) diff --git a/cellbender/remove_background/tests/test_dataprep.py b/cellbender/remove_background/tests/test_dataprep.py new file mode 100644 index 0000000..8fcb156 --- /dev/null +++ b/cellbender/remove_background/tests/test_dataprep.py @@ -0,0 +1,101 @@ +"""Test functions in dataprep.py""" + +import pytest +import scipy.sparse as sp +import numpy as np +import torch + +from cellbender.remove_background.data.dataprep import DataLoader +from cellbender.remove_background.sparse_utils import dense_to_sparse_op_torch + +from .conftest import sparse_matrix_equal, simulated_dataset + + +USE_CUDA = torch.cuda.is_available() + + +@pytest.mark.parametrize('cuda', + [False, + pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, + reason='requires CUDA'))], + ids=lambda b: 'cuda' if b else 'cpu') +def test_dataloader_sorting(simulated_dataset, cuda): + """test dataset.py _overwrite_matrix_with_columns_from_another()""" + + d = simulated_dataset + data_loader = DataLoader( + d['matrix'], + empty_drop_dataset=None, + batch_size=5, + fraction_empties=0., + shuffle=False, + use_cuda=cuda, + ) + sorted_data_loader = DataLoader( + d['matrix'], + empty_drop_dataset=None, + batch_size=5, + fraction_empties=0., + shuffle=False, + sort_by=lambda x: -1 * np.array(x.max(axis=1).todense()).squeeze(), + use_cuda=cuda, + ) + + # try to shuffle and sort at the same time, and expect a failure + with pytest.raises(AssertionError): + sorted_data_loader2 = DataLoader( + d['matrix'], + empty_drop_dataset=None, + batch_size=5, + fraction_empties=0., + shuffle=True, + sort_by=lambda x: -1 * np.array(x.max(axis=1).todense()).squeeze(), + use_cuda=cuda, + ) + + # this is copied from infer.BasePosterior._get_mean() which is not ideal + out = [] + for loader in [data_loader, sorted_data_loader]: + + barcodes = [] + genes = [] + counts = [] + ind = 0 + + for data in loader: + dense_counts = data # just make it the same! + + # Convert to sparse. + bcs_i_chunk, genes_i, counts_i = dense_to_sparse_op_torch(dense_counts) + + # Barcode index in the dataloader. + bcs_i = bcs_i_chunk + ind + + # Obtain the real barcode index after unsorting the dataloader. + bcs_i = loader.unsort_inds(bcs_i) + + # Add sparse matrix values to lists. + barcodes.append(bcs_i) + genes.append(genes_i) + counts.append(counts_i) + + # Increment barcode index counter. + ind += data.shape[0] # Same as data_loader.batch_size + + # Convert the lists to numpy arrays. + counts = np.concatenate(counts).astype(np.uint32) + barcodes = np.concatenate(barcodes).astype(np.uint32) + genes = np.concatenate(genes).astype(np.uint32) # uint16 is too small! + + print('counts') + print(counts) + print('barcodes') + print(barcodes) + print('genes') + print(genes) + + # Put the counts into a sparse csc_matrix. + out.append(sp.csc_matrix((counts, (barcodes, genes)), + shape=d['matrix'].shape)) + + assert sparse_matrix_equal(out[0], out[1]) diff --git a/cellbender/remove_background/tests/test_dataset.py b/cellbender/remove_background/tests/test_dataset.py new file mode 100644 index 0000000..08fdfef --- /dev/null +++ b/cellbender/remove_background/tests/test_dataset.py @@ -0,0 +1,40 @@ +"""Test functions in dataset.py""" + +import pytest +import scipy.sparse as sp +import numpy as np + +from .conftest import sparse_matrix_equal + + +@pytest.mark.skip +def test_heuristic_priors(): + pass + + +@pytest.mark.skip +def test_feature_type_exclusion(): + # TODO there seems to be an error + # TODO see https://github.com/broadinstitute/CellBender/issues/121#issuecomment-1443995082 + pass + + +@pytest.mark.skip +def test_barcode_trimming(): + pass + + +@pytest.mark.skip +def test_feature_trimming(): + pass + + +@pytest.mark.skip +def test_restore_eliminated_features_in_cells(): + pass + + +@pytest.mark.skip +def test_remove_zero_count_cells(): + """Functionality yet to be written too""" + pass diff --git a/cellbender/remove_background/tests/test_downstream.py b/cellbender/remove_background/tests/test_downstream.py new file mode 100644 index 0000000..bf57882 --- /dev/null +++ b/cellbender/remove_background/tests/test_downstream.py @@ -0,0 +1,119 @@ +"""Test functions in downstream.py and a few more.""" + +import pytest + +from cellbender.remove_background.downstream import anndata_from_h5, \ + load_anndata_from_input_and_output +from cellbender.remove_background.tests.conftest import SavedFileH5, \ + h5_file, h5_v3_file_post_inference + + +def convert(s): + """The h5 gets saved with different key names than the h5_file class""" + if s == 'gene_names': + return 'gene_name' + elif s == 'barcodes': + return 'barcode' + elif s == 'gene_ids': + return 'gene_id' + elif s == 'genomes': + return 'genome' + elif s == 'feature_types': + return 'feature_type' + return s + + +def test_anndata_from_h5(h5_file: SavedFileH5): + + # load AnnData and check its shape + adata = anndata_from_h5(file=h5_file.name) + assert h5_file.shape == adata.shape, 'Shape of loaded adata is not correct' + + # ensure everything that was supposed to be saved is loaded properly + indices = [adata.obs.index.name, adata.var.index.name] + all_columns = list(adata.obs.columns) + list(adata.var.columns) + indices + for key in h5_file.keys: + key = convert(key) + assert key in all_columns, f'Saved information "{key}" is missing from loaded adata' + + +@pytest.mark.parametrize('analyzed_bcs_only', [True, False]) +def test_anndata_from_inferred_h5(h5_v3_file_post_inference: SavedFileH5, analyzed_bcs_only): + """Make sure the extra stuff we save gets loaded by downstream loader. + TODO: tidy this up + """ + + adata = anndata_from_h5(file=h5_v3_file_post_inference.name, analyzed_barcodes_only=analyzed_bcs_only) + print(adata) + print(adata.obs.head()) + print(adata.var.head()) + + # check the shape of the loaded AnnData + if analyzed_bcs_only: + expected_shape = (h5_v3_file_post_inference.barcodes_analyzed, h5_v3_file_post_inference.shape[1]) + else: + expected_shape = h5_v3_file_post_inference.shape # all barcodes + assert adata.shape == expected_shape, 'Shape of loaded adata is not correct' + + # ensure everything that was supposed to be saved in .obs and .obsm and .var is loaded properly + indices = [adata.obs.index.name, adata.var.index.name] + all_columns = list(adata.obs.columns) + list(adata.obsm.keys()) + list(adata.var.columns) + indices + + relevant_keys = h5_v3_file_post_inference.keys + if analyzed_bcs_only: + relevant_keys = relevant_keys + h5_v3_file_post_inference.local_keys + + for key in relevant_keys: + print(key) + key = convert(key) + assert key in all_columns, f'Saved information "{key}" is missing from loaded adata' + + # ensure other things are also loading + extra_columns = list(adata.uns.keys()) + relevant_keys = h5_v3_file_post_inference.global_keys + h5_v3_file_post_inference.meta_keys + if not analyzed_bcs_only: + relevant_keys = relevant_keys + h5_v3_file_post_inference.local_keys + + for key in relevant_keys: + print(key) + assert key in extra_columns, \ + f'Saved .uns information "{key}" is missing from adata: {adata.uns.keys()}' + + +def test_load_anndata_from_input_and_output(h5_file, h5_v3_file_post_inference): + adata = load_anndata_from_input_and_output(input_file=h5_file.name, + output_file=h5_v3_file_post_inference.name, + gene_expression_encoding_key='gene_expression_encoding', + analyzed_barcodes_only=False) + print(adata) + assert h5_file.shape == adata.shape, \ + 'Shape of loaded adata is not correct when loading all barcodes' + for key in (h5_v3_file_post_inference.local_keys + + h5_v3_file_post_inference.global_keys + + h5_v3_file_post_inference.meta_keys): + assert key in adata.uns.keys(), f'Key {key} missing from adata.uns' + + adata2 = load_anndata_from_input_and_output(input_file=h5_file.name, + output_file=h5_v3_file_post_inference.name, + gene_expression_encoding_key='gene_expression_encoding', + analyzed_barcodes_only=True) + print(adata2) + assert h5_v3_file_post_inference.analyzed_shape == adata2.shape, \ + 'Shape of loaded adata is not correct when loading only analyzed barcodes' + for key in h5_v3_file_post_inference.local_keys: + if key == 'gene_expression_encoding': + assert key in adata2.obsm.keys(), f'Key {key} missing from adata.obsm' + else: + assert key in adata2.obs.keys(), f'Key {key} missing from adata.obs' + for key in h5_v3_file_post_inference.global_keys + h5_v3_file_post_inference.meta_keys: + assert key in adata2.uns.keys(), f'Key {key} missing from adata.uns' + + +@pytest.mark.skip(reason='TODO') +def test_scanpy_loading(): + pass + + +@pytest.mark.skip(reason='TODO') +def test_seurat_loading(): + pass diff --git a/cellbender/remove_background/tests/test_estimation.py b/cellbender/remove_background/tests/test_estimation.py new file mode 100644 index 0000000..528ab67 --- /dev/null +++ b/cellbender/remove_background/tests/test_estimation.py @@ -0,0 +1,358 @@ +"""Test functions in estimation.py""" +import pandas as pd +import pytest +import scipy.sparse as sp +import numpy as np +import torch + +from cellbender.remove_background.estimation import Mean, MAP, \ + SingleSample, ThresholdCDF, MultipleChoiceKnapsack, pandas_grouped_apply +from cellbender.remove_background.posterior import IndexConverter, \ + dense_to_sparse_op_torch, log_prob_sparse_to_dense + +from typing import Dict, Union + + +@pytest.fixture(scope='module') +def log_prob_coo_base() -> Dict[str, Union[sp.coo_matrix, np.ndarray, Dict[int, int]]]: + n = -np.inf + m = np.array( + [[0, n, n, n, n, n, n, n, n, n], # map 0, mean 0 + [n, 0, n, n, n, n, n, n, n, n], # map 1, mean 1 + [n, n, 0, n, n, n, n, n, n, n], # map 2, mean 2 + [-6, -2, np.log(1. - 2 * np.exp(-2) - np.exp(-6)), -2, n, n, n, n, n, n], + [-2.5, -1.5, -0.5, -3, np.log(1. - np.exp([-2.5, -1.5, -0.5, -3]).sum()), + n, n, n, n, n], + [-0.74, -1, -2, -4, np.log(1. - np.exp([-0.74, -1, -2, -4]).sum()), + n, n, n, n, n], + [-1, -0.74, -2, -4, np.log(1. - np.exp([-0.74, -1, -2, -4]).sum()), + n, n, n, n, n], + [-2, -1, -0.74, -4, np.log(1. - np.exp([-0.74, -1, -2, -4]).sum()), + n, n, n, n, n], + ] + ) + # make m sparse, i.e. zero probability entries are absent + rows, cols, vals = dense_to_sparse_op_torch(torch.tensor(m), tensor_for_nonzeros=torch.tensor(m).exp()) + # make it a bit more difficult by having an empty row at the beginning + rows = rows + 1 + shape = list(m.shape) + shape[0] = shape[0] + 1 + offset_dict = dict(zip(range(1, 9), [0] * 7 + [1])) # noise count offsets (last is 1) + + maps = np.argmax(m, axis=1) + maps = maps + np.array([offset_dict[m] for m in offset_dict.keys()]) + cdf_logic = (torch.logcumsumexp(torch.tensor(m), dim=-1) > np.log(0.5)) + cdfs = [np.where(a)[0][0] for a in cdf_logic] + cdfs = cdfs + np.array([offset_dict[m] for m in offset_dict.keys()]) + + return {'coo': sp.coo_matrix((vals, (rows, cols)), shape=shape), + 'offsets': offset_dict, # not all noise counts start at zero + 'maps': np.array([0] + maps.tolist()), + 'cdfs': np.array([0] + cdfs.tolist())} + + +@pytest.fixture(scope='module', params=['exact', 'filtered', 'unsorted']) +def log_prob_coo(request, log_prob_coo_base) \ + -> Dict[str, Union[sp.coo_matrix, np.ndarray, Dict[int, int]]]: + """When used as an input argument, this offers up a series of dicts that + can be used for tests""" + if request.param == 'exact': + return log_prob_coo_base + + elif request.param == 'filtered': + coo = log_prob_coo_base['coo'] + logic = (coo.data >= -6) + new_coo = sp.coo_matrix((coo.data[logic], (coo.row[logic], coo.col[logic])), + shape=coo.shape) + out = {'coo': new_coo} + out.update({k: v for k, v in log_prob_coo_base.items() if (k != 'coo')}) + return out + + elif request.param == 'unsorted': + coo = log_prob_coo_base['coo'] + order = np.random.permutation(np.arange(len(coo.data))) + new_coo = sp.coo_matrix((coo.data[order], (coo.row[order], coo.col[order])), + shape=coo.shape) + out = {'coo': new_coo} + out.update({k: v for k, v in log_prob_coo_base.items() if (k != 'coo')}) + return out + + else: + raise ValueError(f'Test writing error: requested "{request.param}" log_prob_coo') + + +@pytest.fixture(scope='module', params=['exact', 'filtered', 'unsorted']) +def mckp_log_prob_coo(request, log_prob_coo_base) \ + -> Dict[str, Union[sp.coo_matrix, np.ndarray, Dict[int, int]]]: + """When used as an input argument, this offers up a series of dicts that + can be used for tests. + + NOTE: separate for MCKP because we cannot include an empty 'm' because it + throws everything off (which gene is what, etc.) + """ + def _fix(v): + if type(v) == dict: + return {(k - 1): val for k, val in v.items()} + elif type(v) == sp.coo_matrix: + return _eliminate_row_zero(v) + else: + return v + + def _eliminate_row_zero(coo_: sp.coo_matrix) -> sp.coo_matrix: + row = coo_.row - 1 + shape = list(coo_.shape) + shape[0] = shape[0] - 1 + return sp.coo_matrix((coo_.data, (row, coo_.col)), shape=shape) + + if request.param == 'exact': + out = log_prob_coo_base + + elif request.param == 'filtered': + coo = log_prob_coo_base['coo'] + logic = (coo.data >= -6) + new_coo = sp.coo_matrix((coo.data[logic], (coo.row[logic], coo.col[logic])), + shape=coo.shape) + out = {'coo': new_coo} + out.update({k: v for k, v in log_prob_coo_base.items() if (k != 'coo')}) + + elif request.param == 'unsorted': + coo = log_prob_coo_base['coo'] + order = np.random.permutation(np.arange(len(coo.data))) + new_coo = sp.coo_matrix((coo.data[order], (coo.row[order], coo.col[order])), + shape=coo.shape) + out = {'coo': new_coo} + out.update({k: v for k, v in log_prob_coo_base.items() if (k != 'coo')}) + + else: + raise ValueError(f'Test writing error: requested "{request.param}" log_prob_coo') + + return {k: _fix(v) for k, v in out.items()} + + +def test_single_sample(log_prob_coo): + """Test the single sample estimator""" + + # the input + print(log_prob_coo) + print('input log probs') + dense = log_prob_sparse_to_dense(log_prob_coo['coo']) + print(dense) + + # with this shape converter, we get one row, where each value is one m + converter = IndexConverter(total_n_cells=1, + total_n_genes=log_prob_coo['coo'].shape[0]) + + # set up and estimate + estimator = SingleSample(index_converter=converter) + noise_csr = estimator.estimate_noise(noise_log_prob_coo=log_prob_coo['coo'], + noise_offsets=log_prob_coo['offsets']) + + # output + print('dense noise count estimate, per m') + out_per_m = np.array(noise_csr.todense()).squeeze() + print(out_per_m) + + # test + for i in log_prob_coo['offsets'].keys(): + print(f'testing "m" value {i}') + allowed_vals = np.arange(dense.shape[1])[dense[i, :] > -np.inf] + log_prob_coo['offsets'][i] + print('allowed values') + print(allowed_vals) + print('sample') + print(out_per_m[i]) + assert out_per_m[i] in allowed_vals, \ + f'sample {out_per_m[i]} is not allowed for {dense[i, :]}' + + +def test_mean(log_prob_coo): + """Test the mean estimator""" + + def _add_offsets_to_truth(truth: np.ndarray, offset_dict: Dict[int, int]): + return truth + np.array([offset_dict.get(m, 0) for m in range(len(truth))]) + + offset_dict = log_prob_coo['offsets'] + + # the input + print(log_prob_coo) + print('input log probs') + dense = log_prob_sparse_to_dense(log_prob_coo['coo']) + print(dense) + + # with this shape converter, we get one row, where each value is one m + converter = IndexConverter(total_n_cells=1, + total_n_genes=log_prob_coo['coo'].shape[0]) + + # set up and estimate + estimator = Mean(index_converter=converter) + noise_csr = estimator.estimate_noise(noise_log_prob_coo=log_prob_coo['coo'], + noise_offsets=offset_dict) + + # output + print('dense noise count estimate, per m') + out_per_m = np.array(noise_csr.todense()).squeeze() + print(out_per_m) + + # truth + brute_force = np.matmul(np.arange(dense.shape[1]), np.exp(dense).transpose()) + brute_force = _add_offsets_to_truth(truth=brute_force, offset_dict=offset_dict) + print('truth') + print(brute_force) + + # test + np.testing.assert_allclose(out_per_m, brute_force) + + +def test_map(log_prob_coo): + """Test the MAP estimator""" + + offset_dict = log_prob_coo['offsets'] + + # the input + print(log_prob_coo) + print('input log probs') + print(log_prob_sparse_to_dense(log_prob_coo['coo'])) + + # with this shape converter, we get one row, where each value is one m + converter = IndexConverter(total_n_cells=1, + total_n_genes=log_prob_coo['coo'].shape[0]) + + # set up and estimate + estimator = MAP(index_converter=converter) + noise_csr = estimator.estimate_noise(noise_log_prob_coo=log_prob_coo['coo'], + noise_offsets=offset_dict) + + # output + print('dense noise count estimate, per m') + out_per_m = np.array(noise_csr.todense()).squeeze() + print(out_per_m) + print('truth') + print(log_prob_coo['maps']) + + # test + np.testing.assert_array_equal(out_per_m, log_prob_coo['maps']) + + +def test_cdf(log_prob_coo): + """Test the estimator based on CDF thresholding""" + + offset_dict = log_prob_coo['offsets'] + + # the input + print(log_prob_coo) + print('input log probs') + print(log_prob_sparse_to_dense(log_prob_coo['coo'])) + + # with this shape converter, we get one row, where each value is one m + converter = IndexConverter(total_n_cells=1, + total_n_genes=log_prob_coo['coo'].shape[0]) + + # set up and estimate + estimator = ThresholdCDF(index_converter=converter) + noise_csr = estimator.estimate_noise(noise_log_prob_coo=log_prob_coo['coo'], + noise_offsets=offset_dict, + q=0.5) + + # output + print('dense noise count estimate, per m') + out_per_m = np.array(noise_csr.todense()).squeeze() + print(out_per_m) + print('truth') + print(log_prob_coo['cdfs']) + + # test + np.testing.assert_array_equal(out_per_m, log_prob_coo['cdfs']) + + +@pytest.mark.parametrize('n_chunks, parallel_compute', + ([1, False], + [2, False], + [2, True]), ids=['1chunk', '2chunks_1cpu', '2chunks_parallel']) +@pytest.mark.parametrize('n_cells, target, truth, truth_mat', + ([1, np.zeros(8), np.array([0, 1, 2, 0, 0, 0, 0, 1]), None], + [1, np.ones(8), np.array([0, 1, 2, 1, 1, 1, 1, 1]), None], + [1, np.ones(8) * 2, np.array([0, 1, 2, 2, 2, 2, 2, 2]), None], + [4, np.zeros(2), np.array([2, 2]), None], + [4, np.ones(2) * 4, np.array([4, 4]), np.array([[0, 1], [2, 2], [2, 0], [0, 1]])], + [4, np.ones(2) * 9, np.array([9, 9]), np.array([[0, 1], [2, 3], [4, 2], [3, 3]])]), + ids=['1_cell_target_0', + '1_cell_target_1', + '1_cell_target_2', + '4_cell_target_0', + '4_cell_target_4', + '4_cell_target_9']) +def test_mckp(mckp_log_prob_coo, n_cells, target, truth, truth_mat, n_chunks, parallel_compute): + """Test the multiple choice knapsack problem estimator""" + + offset_dict = mckp_log_prob_coo['offsets'] + + # the input + print('input log probs ===============================================') + print(log_prob_sparse_to_dense(mckp_log_prob_coo['coo'])) + + # set up and estimate# with this shape converter, we have 1 cell with 8 genes + converter = IndexConverter(total_n_cells=n_cells, + total_n_genes=mckp_log_prob_coo['coo'].shape[0] // n_cells) + estimator = MultipleChoiceKnapsack(index_converter=converter) + noise_csr = estimator.estimate_noise( + noise_log_prob_coo=mckp_log_prob_coo['coo'], + noise_offsets=offset_dict, + noise_targets_per_gene=target, + verbose=True, + n_chunks=n_chunks, + use_multiple_processes=parallel_compute, + ) + + assert noise_csr.shape == (converter.total_n_cells, converter.total_n_genes) + + # output + print('dense noise count estimate') + out_mat = np.array(noise_csr.todense()) + print(out_mat) + print('noise counts per gene') + out = out_mat.sum(axis=0) + print(out) + print('truth') + print(truth) + + # test + if truth_mat is not None: + np.testing.assert_array_equal(out_mat, truth_mat) + np.testing.assert_array_equal(out, truth) + + +def _firstval(df): + return df['log_prob'].iat[0] + + +def _meanval(df): + return df['log_prob'].mean() + + +@pytest.mark.parametrize('fun', (_firstval, _meanval), ids=['first_value', 'mean']) +def test_parallel_pandas_grouped_apply(fun): + """Test that the parallel apply gives the same thing as non-parallel""" + + df = pd.DataFrame(data={'m': [0, 0, 0, 1, 1, 1, 2, 2, 2], + 'c': [0, 1, 2] * 3, + 'log_prob': [1, 2, 3, 4, 5, 6, 7, 8, 9]}) + print('input data') + print(df) + + reg = pandas_grouped_apply( + coo=sp.coo_matrix((df['log_prob'], (df['m'], df['c'])), shape=[3, 3]), + fun=fun, + parallel=False, + ) + print('normal application of groupby apply') + print(reg) + + parallel = pandas_grouped_apply( + coo=sp.coo_matrix((df['log_prob'], (df['m'], df['c'])), shape=[3, 3]), + fun=fun, + parallel=True, + ) + print('parallel application of groupby apply') + print(parallel) + + np.testing.assert_array_equal(reg['m'], parallel['m']) + np.testing.assert_array_equal(reg['result'], parallel['result']) diff --git a/cellbender/remove_background/tests/test_infer.py b/cellbender/remove_background/tests/test_infer.py new file mode 100644 index 0000000..06feb67 --- /dev/null +++ b/cellbender/remove_background/tests/test_infer.py @@ -0,0 +1,301 @@ +# """Test functions in infer.py""" +# +# import pytest +# import scipy.sparse as sp +# import numpy as np +# import torch +# +# from cellbender.remove_background.data.dataprep import DataLoader +# from cellbender.remove_background.infer import BasePosterior, Posterior, \ +# binary_search, dense_to_sparse_op_torch, dense_to_sparse_op_numpy +# +# from .conftest import sparse_matrix_equal, simulated_dataset, tensors_equal +# +# +# USE_CUDA = torch.cuda.is_available() +# +# +# @pytest.mark.parametrize('cuda', +# [False, +# pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, +# reason='requires CUDA'))], +# ids=lambda b: 'cuda' if b else 'cpu') +# def test_dense_to_sparse_op_numpy(simulated_dataset, cuda): +# """test infer.py BasePosterior.dense_to_sparse_op_numpy()""" +# +# d = simulated_dataset +# data_loader = DataLoader( +# d['matrix'], +# empty_drop_dataset=None, +# batch_size=5, +# fraction_empties=0., +# shuffle=False, +# use_cuda=cuda, +# ) +# +# barcodes = [] +# genes = [] +# counts = [] +# ind = 0 +# +# for data in data_loader: +# dense_counts = data # just make it the same! +# +# # Convert to sparse. +# bcs_i_chunk, genes_i, counts_i = \ +# dense_to_sparse_op_numpy(dense_counts.detach().cpu().numpy()) +# +# # Barcode index in the dataloader. +# bcs_i = bcs_i_chunk + ind +# +# # Obtain the real barcode index after unsorting the dataloader. +# bcs_i = data_loader.unsort_inds(bcs_i) +# +# # Add sparse matrix values to lists. +# barcodes.append(bcs_i) +# genes.append(genes_i) +# counts.append(counts_i) +# +# # Increment barcode index counter. +# ind += data.shape[0] # Same as data_loader.batch_size +# +# # Convert the lists to numpy arrays. +# counts = np.array(np.concatenate(tuple(counts)), dtype=np.uint32) +# barcodes = np.array(np.concatenate(tuple(barcodes)), dtype=np.uint32) +# genes = np.array(np.concatenate(tuple(genes)), dtype=np.uint32) # uint16 is too small! +# +# # Put the counts into a sparse csc_matrix. +# out = sp.csc_matrix((counts, (barcodes, genes)), +# shape=d['matrix'].shape) +# +# assert sparse_matrix_equal(out, d['matrix']) +# +# +# @pytest.mark.parametrize('cuda', +# [False, +# pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, +# reason='requires CUDA'))], +# ids=lambda b: 'cuda' if b else 'cpu') +# def test_dense_to_sparse_op_torch(simulated_dataset, cuda): +# """test infer.py BasePosterior.dense_to_sparse_op_torch()""" +# +# d = simulated_dataset +# data_loader = DataLoader( +# d['matrix'], +# empty_drop_dataset=None, +# batch_size=5, +# fraction_empties=0., +# shuffle=False, +# use_cuda=cuda, +# ) +# +# barcodes = [] +# genes = [] +# counts = [] +# ind = 0 +# +# for data in data_loader: +# dense_counts = data # just make it the same! +# +# # Convert to sparse. +# bcs_i_chunk, genes_i, counts_i = \ +# dense_to_sparse_op_torch(dense_counts) +# +# # Barcode index in the dataloader. +# bcs_i = bcs_i_chunk + ind +# +# # Obtain the real barcode index after unsorting the dataloader. +# bcs_i = data_loader.unsort_inds(bcs_i) +# +# # Add sparse matrix values to lists. +# barcodes.append(bcs_i) +# genes.append(genes_i) +# counts.append(counts_i) +# +# # Increment barcode index counter. +# ind += data.shape[0] # Same as data_loader.batch_size +# +# # Convert the lists to numpy arrays. +# counts = np.concatenate(counts).astype(np.uint32) +# barcodes = np.concatenate(barcodes).astype(np.uint32) +# genes = np.concatenate(genes).astype(np.uint32) # uint16 is too small! +# +# # Put the counts into a sparse csc_matrix. +# out = sp.csc_matrix((counts, (barcodes, genes)), +# shape=d['matrix'].shape) +# +# assert sparse_matrix_equal(out, d['matrix']) +# +# +# def test_binary_search(): +# """Test the general binary search function.""" +# +# tol = 0.001 +# +# def fun1(x): +# return x - 1. +# +# out = binary_search(evaluate_outcome_given_value=fun1, +# target_outcome=torch.tensor([0.]), +# init_range=torch.tensor([[0., 10.]]), +# target_tolerance=tol) +# print('Single value binary search') +# print('Target value = [1.]') +# print(f'Output = {out}') +# assert ((out - torch.tensor([1.])).abs() <= tol).all(), \ +# 'Single input binary search failed' +# +# def fun2(x): +# x = x.clone() +# x[0] = x[0] - 1. +# x[1] = x[1] - 2. +# return x +# +# out = binary_search(evaluate_outcome_given_value=fun2, +# target_outcome=torch.tensor([0., 0.]), +# init_range=torch.tensor([[-10., 5.], [0., 10.]]), +# target_tolerance=tol) +# print('Two-value binary search') +# print('Target value = [1., 2.]') +# print(f'Output = {out}') +# assert ((out - torch.tensor([1., 2.])).abs() <= tol).all(), \ +# 'Two-argument input binary search failed' +# +# @pytest.mark.parametrize('cuda', +# [False, +# pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, +# reason='requires CUDA'))], +# ids=lambda b: 'cuda' if b else 'cpu') +# @pytest.mark.parametrize('fun', +# [Posterior._mckp_noise_given_log_prob_tensor, +# Posterior._mckp_noise_given_log_prob_tensor_fast], +# ids=['inchworm', 'sort']) +# def test_mckp_noise_given_log_prob_tensor(cuda, fun): +# """Test the bespoke inchworm algorithm for solving a discrete +# convex constrained optimization problem""" +# +# device = 'cuda' if cuda else 'cpu' +# +# poisson_noise_means = torch.tensor([[1.9160, 0.5520], +# [2.7160, 0.0840], +# [3.9080, 2.5280]]).to(device) +# log_p_ngc = (torch.distributions.Poisson(rate=poisson_noise_means.unsqueeze(-1)) +# .log_prob(torch.arange(10).to(device))) +# +# debug = True +# +# print('\nA couple normal test cases') +# out = fun( +# log_prob_noise_counts_NGC=log_p_ngc, +# offset_noise_counts_NG=torch.zeros_like(poisson_noise_means), +# data_NG=torch.ones_like(poisson_noise_means) * 10., +# target_G=torch.tensor([1., 3.]).to(device), +# debug=debug, +# ) +# assert tensors_equal(out[0], torch.tensor([[0., 0.], [0., 0.], [1., 3.]]).to(device)) +# assert tensors_equal(out[1], torch.tensor([0., 0.]).to(device)) +# +# out = fun( +# log_prob_noise_counts_NGC=log_p_ngc, +# offset_noise_counts_NG=torch.zeros_like(poisson_noise_means), +# data_NG=torch.ones_like(poisson_noise_means) * 10., +# target_G=torch.tensor([6., 6.]).to(device), +# debug=debug, +# ) +# assert tensors_equal(out[0], torch.tensor([[1., 1.], [2., 0.], [3., 5.]]).to(device)) +# assert tensors_equal(out[1], torch.tensor([0., 0.]).to(device)) +# +# print('\nEdge case of zero removal') +# out = fun( +# log_prob_noise_counts_NGC=log_p_ngc, +# offset_noise_counts_NG=torch.zeros_like(poisson_noise_means), +# data_NG=torch.ones_like(poisson_noise_means) * 10., +# target_G=torch.tensor([0., 0.]).to(device), +# debug=debug, +# ) +# assert tensors_equal(out[0], torch.tensor([[0., 0.], [0., 0.], [0., 0.]]).to(device)) +# assert tensors_equal(out[1], torch.tensor([0., 0.]).to(device)) +# +# print('\nEdge case of massive target') +# out = fun( +# log_prob_noise_counts_NGC=log_p_ngc, +# offset_noise_counts_NG=torch.zeros_like(poisson_noise_means), +# data_NG=torch.ones_like(poisson_noise_means) * 10., +# target_G=torch.tensor([100., 0.]).to(device), +# debug=debug, +# ) +# assert tensors_equal(out[0], torch.tensor([[9., 0.], [9., 0.], [9., 0.]]).to(device)) +# assert tensors_equal(out[1], torch.tensor([73., 0.]).to(device)) +# +# print('\nNonzero offset noise counts') +# out = fun( +# log_prob_noise_counts_NGC=log_p_ngc, +# offset_noise_counts_NG=torch.ones_like(poisson_noise_means), +# data_NG=torch.ones_like(poisson_noise_means) * 10., +# target_G=torch.tensor([3., 3.]).to(device), +# debug=debug, +# ) +# assert tensors_equal(out[0], torch.tensor([[1., 1.], [1., 1.], [1., 1.]]).to(device)) +# assert tensors_equal(out[1], torch.tensor([0., 0.]).to(device)) +# +# print('\nNonzero offset noise counts with zero target') +# out = fun( +# log_prob_noise_counts_NGC=log_p_ngc, +# offset_noise_counts_NG=torch.tensor([[1., 0.], [0., 0.], [0., 0.]]).to(device), +# data_NG=torch.ones_like(poisson_noise_means) * 10., +# target_G=torch.tensor([0., 0.]).to(device), +# debug=debug, +# ) +# assert tensors_equal(out[0], torch.tensor([[1., 0.], [0., 0.], [0., 0.]]).to(device)) +# assert tensors_equal(out[1], torch.tensor([-1., 0.]).to(device)) +# +# print('\nNonzero offset noise counts') +# out = fun( +# log_prob_noise_counts_NGC=log_p_ngc, +# offset_noise_counts_NG=torch.ones_like(poisson_noise_means), +# data_NG=torch.ones_like(poisson_noise_means) * 10., +# target_G=torch.tensor([6., 6.]).to(device), +# debug=debug, +# ) +# assert tensors_equal(out[0], torch.tensor([[1., 1.], [2., 1.], [3., 4.]]).to(device)) +# assert tensors_equal(out[1], torch.tensor([0., 0.]).to(device)) +# +# # # This cannot happen for a properly constructed log prob tensor: +# # print('Argmax > data') +# # out = fun( +# # log_prob_noise_counts_NGC=log_p_ngc, +# # offset_noise_counts_NG=torch.ones_like(poisson_noise_means), +# # data_NG=torch.ones_like(poisson_noise_means) * 1., +# # target_G=torch.tensor([6., 6.]).to(device), +# # debug=debug, +# # ) +# # assert tensors_equal(out[0], torch.tensor([[1., 1.], [1., 1.], [1., 1.]]).to(device)) +# # assert tensors_equal(out[1], torch.tensor([4., 4.]).to(device)) +# +# print('\nExtra genes that have no moves') +# poisson_noise_means = torch.tensor([[1.9160, 0.5520, 0.1, 0.1], +# [2.7160, 0.0840, 0.2, 0.3], +# [3.9080, 2.5280, 0.3, 0.2]]).to(device) +# data_NG = torch.tensor([[2, 0, 0, 0], +# [0, 0, 1, 0], +# [0, 0, 0, 0]]).float().to(device) +# log_p_ngc = (torch.distributions.Poisson(rate=poisson_noise_means.unsqueeze(-1)) +# .log_prob(torch.arange(10).to(device))) +# log_p_ngc = torch.where(torch.arange(10).to(device).unsqueeze(0).unsqueeze(0) > data_NG.unsqueeze(-1), +# torch.ones_like(log_p_ngc) * -np.inf, +# log_p_ngc) +# target_G = torch.tensor([2., 2., 2., 2.]).to(device) +# print('data') +# print(data_NG) +# print('log_prob') +# print(log_p_ngc) +# out = fun( +# log_prob_noise_counts_NGC=log_p_ngc, +# offset_noise_counts_NG=torch.zeros_like(poisson_noise_means), +# data_NG=data_NG, +# target_G=target_G, +# debug=debug, +# ) +# assert tensors_equal(out[0], data_NG) +# assert tensors_equal(out[1], target_G - data_NG.sum(dim=0)) +# diff --git a/cellbender/remove_background/tests/test_integration.py b/cellbender/remove_background/tests/test_integration.py new file mode 100644 index 0000000..624c59d --- /dev/null +++ b/cellbender/remove_background/tests/test_integration.py @@ -0,0 +1,52 @@ +"""Full run through on small simulated data""" + +from cellbender.remove_background.cli import CLI +from cellbender.base_cli import get_populated_argparser +from cellbender.remove_background.downstream import anndata_from_h5 +from cellbender.remove_background import consts +import numpy as np +import os +import pytest + +from .conftest import USE_CUDA + + +@pytest.mark.parametrize('cuda', + [False, + pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, + reason='requires CUDA'))], + ids=lambda b: 'cuda' if b else 'cpu') +def test_full_run(tmpdir_factory, h5_v3_file, cuda): + """Do a full run of the command line tool using a small simulated dataset""" + + tmp_dir = tmpdir_factory.mktemp('data') + filename = tmp_dir.join('out.h5') + + os.chdir(tmp_dir) # so checkpoint file goes to temp dir and gets removed + + # handle input arguments using the argparser + input_args = ['cellbender', 'remove-background', + '--input', str(h5_v3_file.name), + '--output', str(filename), + '--epochs', '5'] + if cuda: + input_args.append('--cuda') + args = get_populated_argparser().parse_args(input_args[1:]) + args = CLI.validate_args(args=args) + + # do a full run through + posterior = CLI.run(args=args) + + # do some checks + + # ensure the cell probabilities in the posterior object match the output file + p_for_analyzed_barcodes = posterior.latents_map['p'] + adata = anndata_from_h5(str(filename), analyzed_barcodes_only=True) + file_p_for_analyzed_barcodes = adata.obs['cell_probability'].values + np.testing.assert_array_equal(p_for_analyzed_barcodes, file_p_for_analyzed_barcodes) + + # ensure the cell barcodes are the same both ways + cell_barcodes = np.genfromtxt(str(filename)[:-3] + '_cell_barcodes.csv', dtype=str, delimiter='\n') + adata_cell_barcodes = adata.obs_names[adata.obs['cell_probability'] > consts.CELL_PROB_CUTOFF] + assert set(cell_barcodes) == set(adata_cell_barcodes), \ + 'Cell barcodes in h5 are different from those in CSV file' diff --git a/cellbender/remove_background/tests/test_io.py b/cellbender/remove_background/tests/test_io.py new file mode 100644 index 0000000..06d23ce --- /dev/null +++ b/cellbender/remove_background/tests/test_io.py @@ -0,0 +1,223 @@ +"""Test input reading and output writing functionality.""" + +import pytest +import numpy as np +from scipy.io import mmwrite +import scipy.sparse as sp + +from cellbender.remove_background.data.io import \ + load_data, get_matrix_from_cellranger_mtx, get_matrix_from_dropseq_dge, \ + get_matrix_from_bd_rhapsody, get_matrix_from_anndata, \ + detect_cellranger_version_h5, detect_cellranger_version_mtx, unravel_dict + +from cellbender.remove_background.tests.conftest import \ + sparse_matrix_equal, string_ndarray_equality, h5_v2_file, \ + h5_v2_file_missing_ids, h5_v3_file, h5_file + +from typing import List, Dict, Optional +import gzip +import shutil + + +def assert_loaded_matches_saved(d: Dict[str, np.ndarray], + loaded: Dict[str, np.ndarray], + keys: List[str], + cellranger_version: Optional[int] = None): + """Check if a loaded file's data matches the data that was saved. + + Args: + d: Dict of the data that was saved + loaded: Dict of the data that was loaded from file + keys: List of keys to the dicts that will be checked for equality + cellranger_version: In [2, 3] + """ + if 'cellranger_version' in loaded.keys(): + assert cellranger_version == loaded['cellranger_version'] + assert sparse_matrix_equal(loaded['matrix'], d['matrix']) + for key in keys: + if d[key] is None: + continue + assert loaded[key] is not None, \ + f'Loaded h5 key "{key}" was None, but data was saved: {d[key][:5]} ...' + assert string_ndarray_equality(d[key], loaded[key]), \ + f'Loaded h5 key "{key}" did not match saved data' + + +@pytest.mark.parametrize('filetype', ['h5_v2_file', 'h5_v2_file_missing_ids', 'h5_v3_file']) +def test_simulate_save_load_h5(simulated_dataset, + filetype, + h5_v2_file, + h5_v2_file_missing_ids, + h5_v3_file): + + # get information from fixture, since you cannot pass fixtures to parametrize + if filetype == 'h5_v2_file': + saved_h5 = h5_v2_file + elif filetype == 'h5_v2_file_missing_ids': + saved_h5 = h5_v2_file_missing_ids + elif filetype == 'h5_v3_file': + saved_h5 = h5_v3_file + + # load data from file, using auto-loading, as it would be run + loaded = load_data(input_file=saved_h5.name) + + # assert equality + assert_loaded_matches_saved( + d=simulated_dataset, + loaded=loaded, + keys=saved_h5.keys, + cellranger_version=saved_h5.version, + ) + + +def test_detect_cellranger_version_h5(h5_file): + v = detect_cellranger_version_h5(filename=h5_file.name) + true_version = h5_file.version + assert v == true_version + + +def gzip_file(file): + """gzip a file""" + with open(file, 'rb') as f_in, gzip.open(file + '.gz', 'wb') as f_out: + f_out.writelines(f_in) + + +def save_mtx(tmpdir_factory, simulated_dataset, version: int) -> str: + """Save data files in MTX format and return the directory path""" + dirname = tmpdir_factory.mktemp(f'mtx_v{version}') + + # barcodes and sparse matrix... seems the MTX matrix is transposed + mmwrite(dirname.join('matrix.mtx'), simulated_dataset['matrix'].transpose()) + np.savetxt(dirname.join('barcodes.tsv'), simulated_dataset['barcodes'], fmt='%s') + + # features and gzipping if v3 + features = np.concatenate((np.expand_dims(simulated_dataset['gene_ids'], axis=1), + np.expand_dims(simulated_dataset['gene_names'], axis=1), + np.expand_dims(simulated_dataset['feature_types'], axis=1)), axis=1) + + if version == 3: + np.savetxt(dirname.join('features.tsv'), features, fmt='%s', delimiter='\t') + gzip_file(dirname.join('matrix.mtx')) + gzip_file(dirname.join('barcodes.tsv')) + gzip_file(dirname.join('features.tsv')) + elif version == 2: + np.savetxt(dirname.join('genes.tsv'), features[:, :2], fmt='%s', delimiter='\t') + else: + raise ValueError(f'Test problem: version is {version}, but [2, 3] allowed') + + return dirname + + +@pytest.fixture(scope='session', params=[2, 3]) +def mtx_directory(request, tmpdir_factory, simulated_dataset): + dirname = save_mtx(tmpdir_factory=tmpdir_factory, + simulated_dataset=simulated_dataset, + version=request.param) + yield dirname + shutil.rmtree(str(dirname)) + + +def test_detect_cellranger_version_mtx(mtx_directory): + v = detect_cellranger_version_mtx(filedir=mtx_directory) + true_version = 2 if ('_v2' in str(mtx_directory)) else 3 + assert v == true_version + + +def test_load_mtx(simulated_dataset, mtx_directory): + + # use the correct loader function + loaded = get_matrix_from_cellranger_mtx(filedir=mtx_directory) + version = 3 if ('_v3' in str(mtx_directory)) else 2 + assert_loaded_matches_saved(d=simulated_dataset, + loaded=loaded, + keys=(['gene_ids', 'gene_names', 'barcodes'] + + (['feature_types'] if (version == 3) else [])), + cellranger_version=version) + + # use auto-loading, as it would be run + loaded = load_data(mtx_directory) + assert_loaded_matches_saved(d=simulated_dataset, + loaded=loaded, + keys=(['gene_ids', 'gene_names', 'barcodes'] + + (['feature_types'] if (version == 3) else [])), + cellranger_version=version) + + +def save_dge(tmpdir_factory, simulated_dataset, do_gzip) -> str: + """Save data files in DGE format and return the file path""" + sep = '\t' + name = 'dge.txt' + if do_gzip: + name = name + '.gz' + tmp_dir = tmpdir_factory.mktemp('dge') + filename = tmp_dir.join(name) + load_fcn = gzip.open if do_gzip else open + + def row_generator(mat: sp.csc_matrix) -> List[str]: + for i in range(mat.shape[1]): + yield np.array(mat[:, i].todense()).squeeze().astype(int).astype(str).tolist() + + with load_fcn(filename, 'wb') as f: + f.write(b'# some kind of header!\n') + f.write(sep.join(['GENE'] + simulated_dataset['barcodes'].astype(str).tolist()).encode() + b'\n') + for g, vals in zip(simulated_dataset['gene_names'], row_generator(simulated_dataset['matrix'])): + f.write(sep.join([g] + vals).encode() + b'\n') + + return filename, tmp_dir + + +@pytest.fixture(scope='session', params=[True, False], ids=lambda x: 'gzipped' if x else 'not') +def dge_file(request, tmpdir_factory, simulated_dataset): + filename, tmp_dir = save_dge(tmpdir_factory=tmpdir_factory, + simulated_dataset=simulated_dataset, + do_gzip=request.param) + yield filename + shutil.rmtree(str(tmp_dir)) + + +def test_load_dge(simulated_dataset, dge_file): + + # use the correct loader function + loaded = get_matrix_from_dropseq_dge(str(dge_file)) + assert_loaded_matches_saved(d=simulated_dataset, + loaded=loaded, + keys=['gene_names', 'barcodes']) + + # use auto-loading, as it would be run + loaded = load_data(str(dge_file)) + assert_loaded_matches_saved(d=simulated_dataset, + loaded=loaded, + keys=['gene_names', 'barcodes']) + + +@pytest.mark.skip +def test_load_bd(): + pass + + +@pytest.mark.skip +def test_load_anndata(): + pass + + +@pytest.mark.skip +def test_load_loom(): + pass + + +@pytest.mark.skip +def test_write_matrix_to_cellranger_h5(): + pass + + +@pytest.mark.skip +def test_write_denoised_count_matrix(): + # from run.py, but should probably be refactored to io.py + pass + + +def test_unravel_dict(): + key, value = 'pref', {'a': 1, 'b': {'c': 2, 'd': {'e': 3, 'f': 4}}} + answer = {'pref_a': 1, 'pref_b_c': 2, 'pref_b_d_e': 3, 'pref_b_d_f': 4} + d = unravel_dict(key, value) + assert d == answer, 'unravel_dict failed to produce correct output' diff --git a/cellbender/remove_background/tests/test_monitor.py b/cellbender/remove_background/tests/test_monitor.py new file mode 100644 index 0000000..6873507 --- /dev/null +++ b/cellbender/remove_background/tests/test_monitor.py @@ -0,0 +1,21 @@ +"""Tests for monitoring function.""" + +import pytest +import torch + +from cellbender.monitor import get_hardware_usage + +USE_CUDA = torch.cuda.is_available() + + +@pytest.mark.parametrize('cuda', + [False, + pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, + reason='requires CUDA'))], + ids=lambda b: 'cuda' if b else 'cpu') +def test_get_hardware_usage(cuda): + """Check and see if restarting from a checkpoint picks up in the same place + we left off. Use our model and dataloader. + """ + + print(get_hardware_usage(use_cuda=cuda)) diff --git a/cellbender/remove_background/tests/test_posterior.py b/cellbender/remove_background/tests/test_posterior.py new file mode 100644 index 0000000..e5382b3 --- /dev/null +++ b/cellbender/remove_background/tests/test_posterior.py @@ -0,0 +1,428 @@ +"""Test functions in posterior.py""" + +import pytest +import scipy.sparse as sp +import numpy as np +import torch + +from cellbender.remove_background.data.dataprep import DataLoader +from cellbender.remove_background.posterior import Posterior, torch_binary_search, \ + PRmu, PRq, IndexConverter, compute_mean_target_removal_as_function +from cellbender.remove_background.sparse_utils import dense_to_sparse_op_torch, \ + log_prob_sparse_to_dense, todense_fill +from cellbender.remove_background.estimation import Mean + +import warnings +from typing import Dict, Union + +from .conftest import sparse_matrix_equal, simulated_dataset, tensors_equal + + +USE_CUDA = torch.cuda.is_available() + + +# NOTE: issues caught +# - have a test that actually creates a posterior +# - using uint32 for barcode index in a COO caused integer overflow + + +@pytest.fixture(scope='module') +def log_prob_coo_base() -> Dict[str, Union[sp.coo_matrix, np.ndarray, Dict[int, int]]]: + n = -np.inf + m = np.array( + [[0, n, n, n, n, n, n, n], # map 0, mean 0 + [n, 0, n, n, n, n, n, n], # map 1, mean 1 + [-0.3, -1.5, np.log(1. - np.exp(np.array([-0.3, -1.5])).sum())] + [n] * 5, + [-3, -1.21, -0.7, -2, -4, np.log(1. - np.exp(np.array([-3, -1.21, -0.7, -2, -4])).sum())] + [n] * 2, + ] + ) + # make m sparse, i.e. zero probability entries are absent + rows, cols, vals = dense_to_sparse_op_torch(torch.tensor(m), tensor_for_nonzeros=torch.tensor(m).exp()) + # make it a bit more difficult by having an empty row at the beginning + rows = rows + 1 + shape = list(m.shape) + shape[0] = shape[0] + 1 + offset_dict = dict(zip(range(1, len(m) + 1), [0] * len(m) + [1])) # noise count offsets (last is 1) + return {'coo': sp.coo_matrix((vals, (rows, cols)), shape=shape), + 'offsets': offset_dict} + + +@pytest.fixture(scope='module', params=['sorted', 'unsorted']) +def log_prob_coo(request, log_prob_coo_base) \ + -> Dict[str, Union[sp.coo_matrix, np.ndarray, Dict[int, int]]]: + """When used as an input argument, this offers up a series of dicts that + can be used for tests""" + if request.param == 'sorted': + return log_prob_coo_base + + elif request.param == 'unsorted': + coo = log_prob_coo_base['coo'] + order = np.random.permutation(np.arange(len(coo.data))) + new_coo = sp.coo_matrix((coo.data[order], (coo.row[order], coo.col[order])), + shape=coo.shape) + out = {'coo': new_coo} + out.update({k: v for k, v in log_prob_coo_base.items() if (k != 'coo')}) + return out + + else: + raise ValueError(f'Test writing error: requested "{request.param}" log_prob_coo') + + +@pytest.mark.parametrize('alpha', [0, 1, 2], ids=lambda a: f'alpha{a}') +@pytest.mark.parametrize('n_chunks', [1, 2], ids=lambda n: f'{n}chunks') +@pytest.mark.parametrize('cuda', + [False, + pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, + reason='requires CUDA'))], + ids=lambda b: 'cuda' if b else 'cpu') +def test_PRq(log_prob_coo, alpha, n_chunks, cuda): + + target_tolerance = 0.001 + + print('input log_prob matrix, densified') + dense_input_log_prob = torch.tensor(log_prob_sparse_to_dense(log_prob_coo['coo'])).float() + print(dense_input_log_prob) + print('probability sums per row') + print(torch.logsumexp(dense_input_log_prob, dim=-1).exp()) + print('(row 0 is a missing row)') + + print('input mean noise counts per row') + counts = torch.arange(dense_input_log_prob.shape[-1]).float().unsqueeze(dim=0) + input_means = (dense_input_log_prob.exp() * counts).sum(dim=-1) + print(input_means) + + print('input std per row') + input_std = (dense_input_log_prob.exp() + * (counts - input_means.unsqueeze(dim=-1)).pow(2)).sum(dim=-1).sqrt() + print(input_std) + + print('\ntruth: expected means after regularization') + truth_means_after_regularization = input_means + alpha * input_std + print(truth_means_after_regularization) + print('and the log') + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="divide by zero encountered in log") + print(np.log(truth_means_after_regularization)) + + print('testing compute_log_target_dict()') + target_dict = PRq._compute_log_target_dict(noise_count_posterior_coo=log_prob_coo['coo'], + alpha=alpha) + print(target_dict) + for m in target_dict.keys(): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="divide by zero encountered in log") + np.testing.assert_almost_equal(target_dict[m], + np.log(truth_means_after_regularization[m])) + + print('targets are correct\n\n') + + print('means after regularization') + regularized_coo = PRq.regularize( + noise_count_posterior_coo=log_prob_coo['coo'], + noise_offsets=log_prob_coo['offsets'], + alpha=alpha, + device='cuda' if cuda else 'cpu', + target_tolerance=target_tolerance, + n_chunks=n_chunks, + ) + print('regularized posterior:') + dense_regularized_log_prob = torch.tensor(log_prob_sparse_to_dense(regularized_coo)).float() + print(dense_regularized_log_prob) + means_after_regularization = (dense_regularized_log_prob.exp() * counts).sum(dim=-1) + print('means after regularization:') + print(means_after_regularization) + + torch.testing.assert_close( + actual=truth_means_after_regularization, + expected=means_after_regularization, + rtol=target_tolerance, + atol=target_tolerance, + ) + + +@pytest.mark.parametrize('fpr', [0., 0.1, 1], ids=lambda a: f'fpr{a}') +@pytest.mark.parametrize('n_chunks', [1, 2], ids=lambda n: f'{n}chunks') +# @pytest.mark.parametrize('per_gene', [False, True], ids=lambda n: 'per_gene' if n else 'overall') +@pytest.mark.parametrize('per_gene', [False], ids=lambda n: 'per_gene' if n else 'overall') +@pytest.mark.parametrize('cuda', + [False, + pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, + reason='requires CUDA'))], + ids=lambda b: 'cuda' if b else 'cpu') +def test_PRmu(log_prob_coo, fpr, per_gene, n_chunks, cuda): + + target_tolerance = 0.5 + + index_converter = IndexConverter(total_n_cells=log_prob_coo['coo'].shape[0], total_n_genes=1) + print(index_converter) + + print('raw count matrix') + count_matrix = sp.csr_matrix(np.expand_dims(np.array([0, 0, 1, 2, 5]), axis=-1)) # reflecting filled in log_prob values + print(count_matrix) + + print('input log_prob matrix, densified') + dense_input_log_prob = torch.tensor(log_prob_sparse_to_dense(log_prob_coo['coo'])).float() + print(dense_input_log_prob) + print('probability sums per row') + print(torch.logsumexp(dense_input_log_prob, dim=-1).exp()) + print('(row 0 is a missing row)') + + estimator = Mean(index_converter=index_converter) + mean_noise_csr = estimator.estimate_noise( + noise_log_prob_coo=log_prob_coo['coo'], + noise_offsets=log_prob_coo['offsets'], + device='cuda' if cuda else 'cpu', + ) + print(f'Mean estimator removes {mean_noise_csr.sum()} counts total') + + print('testing compute_target_removal()') + n_cells = 4 # hard coded from the log_prob_coo + target_fun = compute_mean_target_removal_as_function( + noise_count_posterior_coo=log_prob_coo['coo'], + noise_offsets=log_prob_coo['offsets'], + raw_count_csr_for_cells=count_matrix, + n_cells=n_cells, + index_converter=index_converter, + device='cuda' if cuda else 'cpu', + per_gene=per_gene, + ) + targets = target_fun(fpr) + print(f'aiming to remove {targets} overall counts per cell') + print(f'so about {targets * n_cells} counts total') + + print('means after regularization') + regularized_coo = PRmu.regularize( + noise_count_posterior_coo=log_prob_coo['coo'], + noise_offsets=log_prob_coo['offsets'], + index_converter=index_converter, + raw_count_matrix=count_matrix, + fpr=fpr, + per_gene=per_gene, + device='cuda' if cuda else 'cpu', + target_tolerance=target_tolerance, + n_chunks=n_chunks, + ) + print('regularized posterior:') + dense_regularized_log_prob = torch.tensor(log_prob_sparse_to_dense(regularized_coo)).float() + print(dense_regularized_log_prob) + + print('MAP noise:') + map_noise = torch.argmax(dense_regularized_log_prob, dim=-1) + print(map_noise) + + if fpr == 0.: + torch.testing.assert_close( + actual=map_noise.sum().float(), + expected=torch.tensor(mean_noise_csr.sum()).float(), + rtol=1, + atol=1, + ) + elif fpr == 1.: + torch.testing.assert_close( + actual=map_noise.sum().float(), + expected=torch.tensor(count_matrix.sum()).float(), + rtol=1, + atol=1, + ) + else: + assert torch.tensor(mean_noise_csr.sum()).float() - 1 <= map_noise.sum().float(), \ + 'Noise estimate is less than Mean estimator' + assert torch.tensor(count_matrix.sum()).float() >= map_noise.sum().float(), \ + 'Noise estimate is greater than sum of counts... this should never happen' + + # TODO: this test is very weak... it's just hard to test it exactly... + # TODO: passing should mean the code will run, but not that it's quantitatively accurate + + +@pytest.mark.skip +def test_create_posterior(): + pass + + +def test_index_converter(): + index_converter = IndexConverter(total_n_cells=10, total_n_genes=5) + print(index_converter) + + # check basic conversion + n = np.array([0, 1, 2, 3]) + g = n.copy() + m = index_converter.get_m_indices(cell_inds=n, gene_inds=g) + print(f'm inds are {m}') + truth = 5 * n + g + print(f'expected {truth}') + np.testing.assert_equal(m, truth) + + # back and forth + n_star, g_star = index_converter.get_ng_indices(m_inds=m) + np.testing.assert_equal(n, n_star) + np.testing.assert_equal(g, g_star) + + # check on input validity checking + with pytest.raises(ValueError): + index_converter.get_m_indices(cell_inds=np.array([-1]), gene_inds=g) + with pytest.raises(ValueError): + index_converter.get_m_indices(cell_inds=np.array([10]), gene_inds=g) + with pytest.raises(ValueError): + index_converter.get_m_indices(cell_inds=n, gene_inds=np.array([-1])) + with pytest.raises(ValueError): + index_converter.get_m_indices(cell_inds=n, gene_inds=np.array([5])) + with pytest.raises(ValueError): + index_converter.get_ng_indices(m_inds=np.array([-1])) + with pytest.raises(ValueError): + index_converter.get_ng_indices(m_inds=np.array([10 * 5])) + + +@pytest.mark.skip +def test_estimation_array_to_csr(): + Posterior._estimation_array_to_csr() + pass + + +def test_torch_binary_search(): + """Test the general binary search function.""" + + tol = 0.001 + + def fun1(x): + return x - 1. + + out = torch_binary_search( + evaluate_outcome_given_value=fun1, + target_outcome=torch.tensor([0.]), + init_range=torch.tensor([[0., 10.]]), + target_tolerance=tol, + ) + print('Single value binary search') + print('Target value = [1.]') + print(f'Output = {out}') + assert ((out - torch.tensor([1.])).abs() <= tol).all(), \ + 'Single input binary search failed' + + def fun2(x): + x = x.clone() + x[0] = x[0] - 1. + x[1] = x[1] - 2. + return x + + out = torch_binary_search( + evaluate_outcome_given_value=fun2, + target_outcome=torch.tensor([0., 0.]), + init_range=torch.tensor([[-10., 5.], [0., 10.]]), + target_tolerance=tol, + ) + print('Two-value binary search') + print('Target value = [1., 2.]') + print(f'Output = {out}') + assert ((out - torch.tensor([1., 2.])).abs() <= tol).all(), \ + 'Two-argument input binary search failed' + + +@pytest.mark.parametrize('fpr', [0., 0.1, 0.5, 0.75, 1], ids=lambda a: f'fpr{a}') +@pytest.mark.parametrize('per_gene', [False], ids=lambda n: 'per_gene' if n else 'overall') +@pytest.mark.parametrize('cuda', + [False, + pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, + reason='requires CUDA'))], + ids=lambda b: 'cuda' if b else 'cpu') +def test_compute_mean_target_removal_as_function(log_prob_coo, fpr, per_gene, cuda): + """The target removal computation, very important for the MCKP output""" + + noise_count_posterior_coo = log_prob_coo['coo'] + noise_offsets = log_prob_coo['offsets'] + device = 'cuda' if cuda else 'cpu' + + print('log prob posterior coo') + print(noise_count_posterior_coo) + + index_converter = IndexConverter(total_n_cells=log_prob_coo['coo'].shape[0], + total_n_genes=1) + print(index_converter) + + print('raw count matrix') + count_matrix = sp.csr_matrix( + np.expand_dims(np.array([0, 0, 1, 2, 5]), axis=-1) + ) # reflecting filled in log_prob values + print(count_matrix) + + n_cells = log_prob_coo['coo'].shape[0] # hard coded from the log_prob_coo + + target_fun = compute_mean_target_removal_as_function( + noise_count_posterior_coo=noise_count_posterior_coo, + noise_offsets=noise_offsets, + index_converter=index_converter, + raw_count_csr_for_cells=count_matrix, + n_cells=n_cells, + device=device, + per_gene=per_gene, + ) + + target = (target_fun(fpr) * n_cells).item() + print(f'\nwith fpr={fpr:.2f}, target is: {target:.1g}') + + assert target >= 1, 'There is one noise count guaranteed from this test posterior' + if fpr == 1: + torch.testing.assert_close(target, float(count_matrix.sum())) + + # assert False + # TODO: this has not been tested out + + +@pytest.mark.parametrize('blank_noise_offsets', [False, True], ids=['', 'no_noise_offsets']) +def test_save_and_load(tmpdir_factory, blank_noise_offsets): + """Test that a round trip through save and load gives the same thing""" + + tmp_dir = tmpdir_factory.mktemp('posterior') + filename = tmp_dir.join('posterior.h5') + + m = 1000 + n = 20 + + posterior = Posterior(dataset_obj=None, vi_model=None) # blank + + posterior_coo = sp.random(m, n, density=0.1, format='coo', dtype=float) + posterior_coo2 = sp.random(m, n, density=0.08, format='coo', dtype=float) + if blank_noise_offsets: + noise_offsets = {} + else: + noise_offsets = dict(zip(np.random.randint(low=0, high=(m - 1), size=10), + np.random.randint(low=1, high=5, size=10))) + kwargs = {'a': 'b', 'c': 1} + kwargs2 = {'a': 'method', 'c': 1} + + # jam in fake values + posterior._noise_count_posterior_coo = posterior_coo + posterior._noise_count_posterior_coo_offsets = noise_offsets + posterior._noise_count_posterior_kwargs = kwargs + posterior._noise_count_regularized_posterior_coo = posterior_coo2 + posterior._noise_count_regularized_posterior_kwargs = kwargs2 + posterior._latents = {'p': np.random.randn(100), 'd': np.random.randn(100)} + posterior.index_converter = IndexConverter(total_n_cells=1000, total_n_genes=1000) + + # save + posterior.save(file=str(filename)) + + # load + posterior2 = Posterior(dataset_obj=None, vi_model=None) # blank + posterior2.load(file=str(filename)) + + # check + for attr in ['_noise_count_posterior_coo', '_noise_count_posterior_coo_offsets', + '_noise_count_posterior_kwargs', '_noise_count_regularized_posterior_coo', + '_noise_count_regularized_posterior_kwargs', '_latents']: + val1 = getattr(posterior, attr) + val2 = getattr(posterior2, attr) + print(f'{attr} ===================') + print('saved:') + print(val1) + print('loaded') + print(val2) + err_msg = f'Posterior attribute {attr} not preserved after saving and loading' + if type(val1) == sp.coo_matrix: + assert sparse_matrix_equal(val1, val2), err_msg + elif type(val1) == np.ndarray: + np.testing.assert_equal(val1, val2) + elif (type(val1) == dict) and (val1 != {}) and (type(list(val1.values())[0]) == np.ndarray): + for k in val1.keys(): + np.testing.assert_equal(val1[k], val2[k]) + else: + assert val1 == val2, err_msg diff --git a/cellbender/remove_background/tests/test_sparse_utils.py b/cellbender/remove_background/tests/test_sparse_utils.py new file mode 100644 index 0000000..01230da --- /dev/null +++ b/cellbender/remove_background/tests/test_sparse_utils.py @@ -0,0 +1,203 @@ +import pytest +import scipy.sparse as sp +import numpy as np +import torch + +from cellbender.remove_background.sparse_utils import todense_fill, \ + csr_set_rows_to_zero, dense_to_sparse_op_torch, log_prob_sparse_to_dense, \ + overwrite_matrix_with_columns_from_another +from cellbender.remove_background.data.dataprep import DataLoader +from .conftest import sparse_matrix_equal + + +USE_CUDA = torch.cuda.is_available() + + +@pytest.mark.parametrize('val', [0, 1, np.nan, np.inf, -np.inf]) +def test_todense_fill(val): + """Test densification of scipy sparse COO matrix with arbitrary fill value""" + + mat = np.array( + [[0, 0, 0, 0], + [1, 0, 0, 0], + [0, 1, 2, 0], + [1, 2, 3, 4]], + dtype=float, + ) + coo = sp.coo_matrix(mat) + + print('original') + print(mat) + print('sparse version') + print(coo) + + print(f'densified using {val}') + dense = todense_fill(coo=coo, fill_value=val) + print(dense) + brute_force = mat.copy() + brute_force[brute_force == 0] = val + np.testing.assert_array_equal(brute_force, dense) + + +@pytest.mark.parametrize('cuda', + [False, + pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, + reason='requires CUDA'))], + ids=lambda b: 'cuda' if b else 'cpu') +def test_dense_to_sparse_op_torch(simulated_dataset, cuda): + """test infer.py BasePosterior.dense_to_sparse_op_torch()""" + + d = simulated_dataset + data_loader = DataLoader( + d['matrix'], + empty_drop_dataset=None, + batch_size=5, + fraction_empties=0., + shuffle=False, + use_cuda=cuda, + ) + + barcodes = [] + genes = [] + counts = [] + ind = 0 + + for data in data_loader: + dense_counts = data # just make it the same! + + # Convert to sparse. + bcs_i_chunk, genes_i, counts_i = \ + dense_to_sparse_op_torch(dense_counts) + + # Barcode index in the dataloader. + bcs_i = bcs_i_chunk + ind + + # Obtain the real barcode index after unsorting the dataloader. + bcs_i = data_loader.unsort_inds(bcs_i) + + # Add sparse matrix values to lists. + barcodes.append(bcs_i) + genes.append(genes_i) + counts.append(counts_i) + + # Increment barcode index counter. + ind += data.shape[0] # Same as data_loader.batch_size + + # Convert the lists to numpy arrays. + counts = np.concatenate(counts).astype(np.uint32) + barcodes = np.concatenate(barcodes).astype(np.uint32) + genes = np.concatenate(genes).astype(np.uint32) # uint16 is too small! + + # Put the counts into a sparse csc_matrix. + out = sp.csc_matrix((counts, (barcodes, genes)), + shape=d['matrix'].shape) + + assert sparse_matrix_equal(out, d['matrix']) + + +def test_log_prob_sparse_to_dense(): + """Test densification of log prob sparse posteriors filling with -np.inf""" + + data = np.array([0, -1, -2, 0, -4, -2, 0]) + row = np.array([0, 0, 2, 2, 3, 3, 4]) + col = np.array([0, 1, 0, 1, 0, 1, 0]) + coo = sp.coo_matrix((data, (row, col)), shape=[5, 2]) + print(coo) + print('scipy .todense()') + print(coo.todense()) + print('log prob densification') + mat = log_prob_sparse_to_dense(coo=coo) + print(mat) + print('nan densification') + mat_nan = todense_fill(coo=coo, fill_value=np.nan) + print(mat_nan) + print('8 densification') + mat_8 = todense_fill(coo=coo, fill_value=8.) + print(mat_8) + + truth = lambda x: np.array( + [[ 0., -1.], + [ x, x ], + [-2., 0.], + [-4., -2.], + [ 0., x ]], + ) + print('truth with -np.inf') + print(truth(-np.inf)) + + np.testing.assert_array_equal(mat, truth(-np.inf)) + np.testing.assert_array_equal(mat_nan, truth(np.nan)) + np.testing.assert_array_equal(mat_8, truth(8.)) + + +@pytest.mark.parametrize('mat1, mat2, col_inds', + [(sp.csc_matrix([[1, 2], [3, 4]]), + sp.csc_matrix([[0, 0], [0, 0]]), + [0]), + (sp.csc_matrix([[1, 2], [3, 4], [5, 6]]), + sp.csc_matrix([[0, 0], [0, 0], [0, 0]]), + [1]), + (sp.csc_matrix(np.random.poisson(lam=2., size=(10, 10))), + sp.csc_matrix(np.random.poisson(lam=2., size=(10, 10))), + [0, 1, 2, 3])]) +def test_overwrite_matrix_with_columns_from_another(mat1: sp.csc_matrix, + mat2: sp.csc_matrix, + col_inds: np.ndarray): + """test overwrite_matrix_with_columns_from_another()""" + + out = overwrite_matrix_with_columns_from_another(mat1=mat1, mat2=mat2, column_inds=col_inds) + excluded_col_inds = [i for i in range(mat1.shape[1]) if i not in col_inds] + + print('col_inds') + print(col_inds) + print('mat1') + print(mat1.todense()) + print('mat2') + print(mat2.todense()) + print('out') + print(out.todense()) + + print('assertion') + print(out[:, excluded_col_inds].todense()) + print(mat2[:, excluded_col_inds].todense()) + + print('assertion') + print(out[:, col_inds].todense()) + print(mat1[:, col_inds].todense()) + + # excluded columns should be replaced with new values + assert sparse_matrix_equal(out[:, excluded_col_inds], mat2[:, excluded_col_inds]) + + # included columns should be left alone + assert sparse_matrix_equal(out[:, col_inds], mat1[:, col_inds]) + + +@pytest.mark.parametrize('mat, row_inds', + [(sp.csc_matrix([[1, 2], [3, 4]]), + [0]), + (sp.csc_matrix([[1, 2], [3, 4], [5, 6]]), + [1]), + (sp.csc_matrix(np.random.poisson(lam=2., size=(10, 10))), + [0, 1, 2, 3])]) +def test_csr_set_rows_to_zero(mat: sp.csr_matrix, row_inds: np.ndarray): + """test csr_set_rows_to_zero()""" + + out = csr_set_rows_to_zero(csr=mat, row_inds=row_inds) + other_row_inds = [i for i in range(mat.shape[0]) if i not in row_inds] + + print('row_inds') + print(row_inds) + print('mat') + print(mat.todense()) + print('out') + print(out.todense()) + + print('assertion') + print(out[other_row_inds, :].todense()) + print(mat[other_row_inds, :].todense()) + + # other rows should be left alone + assert sparse_matrix_equal(out[other_row_inds, :], mat[other_row_inds, :]) + + # specified rows should be all zero + assert out[row_inds, :].sum() == 0 diff --git a/cellbender/remove_background/tests/test_train.py b/cellbender/remove_background/tests/test_train.py new file mode 100644 index 0000000..66bf729 --- /dev/null +++ b/cellbender/remove_background/tests/test_train.py @@ -0,0 +1,135 @@ +"""Test functionality in train.py""" + +import torch +import pyro.distributions as dist +import pyro.infer.trace_elbo +import pytest +import scipy.sparse as sp +from pyro.infer.svi import SVI +import unittest.mock + +from cellbender.remove_background.run import get_optimizer +from cellbender.remove_background.data.dataprep import prep_sparse_data_for_training \ + as prep_data_for_training +from cellbender.remove_background.train import train_epoch, evaluate_epoch +from .conftest import USE_CUDA + + +@pytest.mark.parametrize('cuda', + [False, + pytest.param(True, marks=pytest.mark.skipif(not USE_CUDA, + reason='requires CUDA'))], + ids=lambda b: 'cuda' if b else 'cpu') +@pytest.mark.parametrize('dropped_minibatch', [False, True], ids=['', 'dropped_minibatch']) +def test_one_cycle_scheduler(dropped_minibatch, cuda): + + # if there is a minibatch so small that it's below consts.SMALLEST_ALLOWED_BATCH + # then the minibatch gets skipped. make sure this works with the scheduler. + + pyro.clear_param_store() + device = 'cuda' if cuda else 'cpu' + + n_cells = 3580 + n_empties = 50000 + n_genes = 100 + learning_rate = 1e-4 + batch_size = 512 + if dropped_minibatch: + n_cells += 2 + epochs = 50 + + count_matrix = sp.random(n_cells, n_genes, density=0.01, format='csr') + empty_matrix = sp.random(n_empties, n_genes, density=0.0001, format='csr') + + # Set up dataloaders. + train_loader, _ = \ + prep_data_for_training(dataset=count_matrix, + empty_drop_dataset=empty_matrix, + batch_size=batch_size, + training_fraction=1., + fraction_empties=0.3, + shuffle=True, + use_cuda=cuda) + + print(f'epochs = {epochs}') + print(f'len(train_loader), i.e. number of minibatches = {len(train_loader)}') + print(f'train_loader.batch_size = {train_loader.batch_size}') + print(f'train_loader.cell_batch_size = {train_loader.cell_batch_size}') + + # Set up optimizer. + scheduler = get_optimizer( + n_batches=len(train_loader), + batch_size=train_loader.batch_size, + epochs=epochs, + learning_rate=learning_rate, + constant_learning_rate=False, + total_epochs_for_testing_only=None, + ) + + # Set up SVI dummy. + def _model(x): + y_mean = pyro.param("y_mean", torch.zeros(1).to(device)) + pyro.sample("y_obs", dist.Normal(loc=y_mean, scale=1), obs=x.sum()) + + def _guide(x): + pass + + svi = SVI(model=_model, + guide=_guide, + optim=scheduler, + loss=pyro.infer.trace_elbo.Trace_ELBO()) + + # Looking for a RuntimeError raised by the scheduler trying to step too much + lr = [] + for _ in range(epochs): + train_epoch(svi=svi, train_loader=train_loader) + lr.append(list(svi.optim.optim_objs.values())[0].get_last_lr()[0]) + + print('learning rates at each epoch:') + print('\n'.join([f'[{i + 1:03d}] {rate:.2e}' for i, rate in enumerate(lr)])) + + # Ensure learning rates are numerically correct + # scheduler args include 'max_lr': learning_rate * 10 + max_lr = 10 * learning_rate + initial_lr = max_lr / 25 + final_lr = initial_lr / 1e4 + # https://pytorch.org/docs/stable/generated/torch.optim.lr_scheduler.OneCycleLR.html + assert lr[len(lr) // 2] > lr[0], 'Cosine LR schedule should have middle > start' + assert lr[len(lr) // 2] > lr[-1], 'Cosine LR schedule should have middle > end' + torch.testing.assert_close(lr[0], initial_lr + (max_lr - initial_lr) / epochs, rtol=1, atol=1e-7), \ + 'Starting learning rate for scheduler seems off' + torch.testing.assert_close(lr[-1], final_lr, rtol=1, atol=1e-7), \ + 'Final learning rate for scheduler seems off' + torch.testing.assert_close(max(lr), max_lr, rtol=1, atol=1e-7), \ + 'Max learning rate in scheduler seems off' + + # And one more step ought to raise the error + with pytest.raises(ValueError, + match=r"Tried to step .* times. The specified number " + r"of total steps is .*"): + svi.optim.step() + + +@pytest.mark.skip +def test_epoch_elbo_fail_restart(): + """Trigger failure and ensure a new attempt is made""" + pass + + +@pytest.mark.skip +def test_final_elbo_fail_restart(): + """Trigger failure and ensure a new attempt is made""" + pass + + +@pytest.mark.skip +def test_restart_matches_scratch(): + """Ensure that an auto-restart gives same ELBO as a start-from-scratch""" + # https://stackoverflow.com/questions/50964786/mock-exception-raised-in-function-using-pytest + pass + + +@pytest.mark.skip +def test_num_training_attempts(): + """Make sure the number of training attempts matches input arg""" + pass diff --git a/cellbender/remove_background/train.py b/cellbender/remove_background/train.py index 0f47f18..8d77808 100644 --- a/cellbender/remove_background/train.py +++ b/cellbender/remove_background/train.py @@ -1,35 +1,40 @@ """Helper functions for training.""" import pyro -import pyro.distributions as dist -from pyro.infer import SVI, JitTraceEnum_ELBO, JitTrace_ELBO, \ - TraceEnum_ELBO, Trace_ELBO -from pyro.optim import ClippedAdam from pyro.util import ignore_jit_warnings +from pyro.infer import SVI +import torch from cellbender.remove_background.model import RemoveBackgroundPyroModel -from cellbender.remove_background.vae.decoder import Decoder -from cellbender.remove_background.vae.encoder \ - import EncodeZ, CompositeEncoder, EncodeNonZLatents -from cellbender.remove_background.data.dataset import SingleCellRNACountsDataset -from cellbender.remove_background.data.dataprep import \ - prep_sparse_data_for_training as prep_data_for_training from cellbender.remove_background.data.dataprep import DataLoader -from cellbender.remove_background.exceptions import NanException +from cellbender.remove_background.exceptions import NanException, ElboException import cellbender.remove_background.consts as consts +from cellbender.remove_background.checkpoint import save_checkpoint +from cellbender.monitor import get_hardware_usage import numpy as np -import torch +import argparse from typing import Tuple, List, Optional import logging import time from datetime import datetime +import sys + + +logger = logging.getLogger('cellbender') + + +def is_scheduler(optim): + """Currently is_scheduler is on a pyro dev branch""" + if hasattr(optim, 'is_scheduler'): + return optim.is_scheduler() + else: + return type(optim) == pyro.optim.lr_scheduler.PyroLRScheduler def train_epoch(svi: SVI, - train_loader: DataLoader, - epoch: Optional[int] = None) -> float: + train_loader: DataLoader) -> float: """Train a single epoch. Args: @@ -53,12 +58,22 @@ def train_epoch(svi: SVI, # Perform gradient descent step and accumulate loss. epoch_loss += svi.step(x_cell_batch) - svi.optim.step(epoch=epoch) # for LR scheduling normalizer_train += x_cell_batch.size(0) + if is_scheduler(svi.optim): + svi.optim.step() # for LR scheduling + # Return epoch loss. total_epoch_loss_train = epoch_loss / normalizer_train + if is_scheduler(svi.optim): + try: + logger.debug(f'Learning rate scheduler: LR = ' + f'{list(svi.optim.optim_objs.values())[0].get_last_lr()[0]:.2e}') + except IndexError: + logger.debug('No values being optimized') + pass + return total_epoch_loss_train @@ -98,232 +113,185 @@ def evaluate_epoch(svi: pyro.infer.SVI, @ignore_jit_warnings() def run_training(model: RemoveBackgroundPyroModel, + args: argparse.Namespace, svi: pyro.infer.SVI, train_loader: DataLoader, test_loader: DataLoader, epochs: int, + output_filename: str, test_freq: int = 10, final_elbo_fail_fraction: float = None, - epoch_elbo_fail_fraction: float = None) -> Tuple[List[float], - List[float], bool]: + epoch_elbo_fail_fraction: float = None, + ckpt_tarball_name: str = consts.CHECKPOINT_FILE_NAME, + checkpoint_freq: int = 10) -> Tuple[List[float], List[float]]: """Run an entire course of training, evaluating on a tests set periodically. Args: model: The model, here in order to store train and tests loss. + args: Parsed arguments, which get saved to checkpoints. svi: The pyro object used for stochastic variational inference. train_loader: Dataloader for training set. test_loader: Dataloader for tests set. epochs: Number of epochs to run training. + output_filename: User-specified output file, used to construct + checkpoint filenames. test_freq: Test set loss is calculated every test_freq epochs of training. - final_elbo_fail_fraction: fail if final test ELBO >= best ELBO * (1+this value) - epoch_elbo_fail_fraction: fail if current test ELBO >= previous ELBO * (1+this value) + final_elbo_fail_fraction: Fail if final test ELBO >= + best ELBO * (1 + this value) + epoch_elbo_fail_fraction: Fail if current test ELBO >= + previous ELBO * (1 + this value) + ckpt_tarball_name: Name of saved tarball for checkpoint. + checkpoint_freq: Checkpoint after this many minutes Returns: - Tuple( - list of training ELBO for each epoch, - list of test ELBO for each epoch for which testing is done, - boolean: False indicates training failed - ) - """ + total_epoch_loss_train: The loss for this epoch of training, which + is -ELBO, normalized by the number of items in the training set. - logging.info("Running inference...") + """ # Initialize train and tests ELBO with empty lists. train_elbo = [] test_elbo = [] - succeeded = True + lr = [] + epoch_checkpoint_freq = 1000 # a large number... it will be recalculated + # Run training loop. Use try to allow for keyboard interrupt. try: - for epoch in range(1, epochs + 1): + + start_epoch = (1 if (model is None) or (len(model.loss['train']['epoch']) == 0) + else model.loss['train']['epoch'][-1] + 1) + + for epoch in range(start_epoch, epochs + 1): + + # In debug mode, log hardware load every epoch. + if args.debug: + # Don't spend time pinging usage stats if we will not use the log. + # TODO: use multiprocessing to sample these stats DURING training... + logger.debug('\n' + get_hardware_usage(use_cuda=model.use_cuda)) # Display duration of an epoch (use 2 to avoid initializations). - if epoch == 2: + if epoch == start_epoch + 1: t = time.time() + model.train() total_epoch_loss_train = train_epoch(svi, train_loader) train_elbo.append(-total_epoch_loss_train) - model.loss['train']['epoch'].append(epoch) - model.loss['train']['elbo'].append(-total_epoch_loss_train) - - if epoch == 2: - logging.info("[epoch %03d] average training loss: %.4f (%.1f seconds per epoch)" - % (epoch, total_epoch_loss_train, time.time() - t)) + try: + last_learning_rate = list(svi.optim.optim_objs.values())[0].get_last_lr()[0] + except AttributeError: + # not a scheduler + last_learning_rate = args.learning_rate + lr.append(last_learning_rate) + + if model is not None: + model.loss['train']['epoch'].append(epoch) + model.loss['train']['elbo'].append(-total_epoch_loss_train) + model.loss['learning_rate']['epoch'].append(epoch) + model.loss['learning_rate']['value'].append(last_learning_rate) + + if epoch == start_epoch + 1: + time_per_epoch = time.time() - t + logger.info("[epoch %03d] average training loss: %.4f (%.1f seconds per epoch)" + % (epoch, total_epoch_loss_train, time_per_epoch)) + epoch_checkpoint_freq = int(np.ceil(60 * checkpoint_freq / time_per_epoch)) + if (epoch_checkpoint_freq > 0) and (epoch_checkpoint_freq < epochs): + logger.info(f"Will checkpoint every {epoch_checkpoint_freq} epochs") + elif epoch_checkpoint_freq >= epochs: + logger.info(f"Will not checkpoint due to projected run " + f"completion in under {checkpoint_freq} min") else: - logging.info("[epoch %03d] average training loss: %.4f" - % (epoch, total_epoch_loss_train)) + logger.info("[epoch %03d] average training loss: %.4f" + % (epoch, total_epoch_loss_train)) # If there is no test data (training_fraction == 1.), skip test. - if len(test_loader) == 0: - continue - - # Every test_freq epochs, evaluate tests loss. - if epoch % test_freq == 0: - total_epoch_loss_test = evaluate_epoch(svi, test_loader) - test_elbo.append(-total_epoch_loss_test) - model.loss['test']['epoch'].append(epoch) - model.loss['test']['elbo'].append(-total_epoch_loss_test) - logging.info("[epoch %03d] average test loss: %.4f" - % (epoch, total_epoch_loss_test)) - if epoch_elbo_fail_fraction is not None and len(test_elbo) > 1 and \ - test_elbo[-1] < test_elbo[-2] and \ - (test_elbo[-2] - test_elbo[-1])/(test_elbo[-2] - train_elbo[0]) > epoch_elbo_fail_fraction: - logging.info( - "Training failed because this test loss (%.4f) exceeds previous test loss(%.4f) by >= %.2f%%, " - "relative to initial train loss %.4f" , - test_elbo[-1], test_elbo[-2], 100*epoch_elbo_fail_fraction, train_elbo[0]) - succeeded = False - break - - logging.info("Inference procedure complete.") - - if succeeded and final_elbo_fail_fraction is not None and len(test_elbo) > 1: + if (test_loader is not None) and (len(test_loader) > 0): + + # Every test_freq epochs, evaluate tests loss. + if epoch % test_freq == 0: + model.eval() + total_epoch_loss_test = evaluate_epoch(svi, test_loader) + test_elbo.append(-total_epoch_loss_test) + model.loss['test']['epoch'].append(epoch) + model.loss['test']['elbo'].append(-total_epoch_loss_test) + logger.info("[epoch %03d] average test loss: %.4f" + % (epoch, total_epoch_loss_test)) + + # Check whether test ELBO has spiked beyond specified conditions. + if (epoch_elbo_fail_fraction is not None) and (len(test_elbo) > 2): + current_diff = max(0., test_elbo[-2] - test_elbo[-1]) + overall_diff = np.abs(test_elbo[-2] - test_elbo[0]) + fractional_spike = current_diff / overall_diff + if fractional_spike > epoch_elbo_fail_fraction: + raise ElboException( + f'Training failed because test loss moved {current_diff:.2f} ' + f'in the wrong direction, and that is {fractional_spike:.2f} ' + f'of the total test ELBO change, > ' + f'specified epoch_elbo_fail_fraction {epoch_elbo_fail_fraction:.2f}' + ) + + # Checkpoint throughout and after final epoch. + if ((ckpt_tarball_name != 'none') + and (((checkpoint_freq > 0) and (epoch % epoch_checkpoint_freq == 0)) + or (epoch == epochs))): # checkpoint at final epoch + save_checkpoint(filebase=output_filename, + tarball_name=ckpt_tarball_name, + args=args, + model_obj=model, + scheduler=svi.optim, + train_loader=train_loader, + test_loader=test_loader) + + # Check on the final test ELBO to see if it meets criteria. + if final_elbo_fail_fraction is not None: best_test_elbo = max(test_elbo) - if test_elbo[-1] < best_test_elbo and \ - (best_test_elbo - test_elbo[-1])/(best_test_elbo - train_elbo[0]) > final_elbo_fail_fraction: - logging.info( - "Training failed because final test loss (%.4f) exceeds " - "best test loss(%.4f) by >= %.2f%%, relative to initial train loss %.4f", - test_elbo[-1], best_test_elbo, 100*final_elbo_fail_fraction, train_elbo[0]) - succeeded = False + if test_elbo[-1] < best_test_elbo: + final_best_diff = best_test_elbo - test_elbo[-1] + initial_best_diff = best_test_elbo - test_elbo[0] + if (final_best_diff / initial_best_diff) > final_elbo_fail_fraction: + raise ElboException( + f'Training failed because final test loss {test_elbo[-1]:.2f} ' + f'is not sufficiently close to best test loss {best_test_elbo:.2f}, ' + f'compared to the initial test loss {test_elbo[0]:.2f}. ' + f'Fractional difference is {final_best_diff / initial_best_diff:.2f}, ' + f'which is > specified final_elbo_fail_fraction {final_elbo_fail_fraction:.2f}' + ) # Exception allows program to continue after ending inference prematurely. except KeyboardInterrupt: - - logging.info("Inference procedure stopped by keyboard interrupt.") + logger.info("Inference procedure stopped by keyboard interrupt... " + "will save a checkpoint.") + save_checkpoint(filebase=output_filename, + tarball_name=ckpt_tarball_name, + args=args, + model_obj=model, + scheduler=svi.optim, + train_loader=train_loader, + test_loader=test_loader) + + except ElboException as e: + logger.info(e.message) + raise e # re-raise the exception to pass it back to caller function # Exception allows program to produce output when terminated by a NaN. except NanException as nan: - print(nan.message) - logging.info(f"Inference procedure terminated early due to a NaN value in: {nan.param}\n\n" - f"The suggested fix is to reduce the learning rate.\n\n") - succeeded = False - - if succeeded: - logging.info("Training succeeded") - logging.info(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + logger.info(f"Inference procedure terminated early due to a NaN value in: {nan.param}\n\n" + f"The suggested fix is to reduce the learning rate by a factor of two.\n\n") + sys.exit(1) - return train_elbo, test_elbo, succeeded + logger.info(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + # Check final ELBO meets conditions. + if (final_elbo_fail_fraction is not None) and (len(test_elbo) > 1): + best_test_elbo = max(test_elbo) + if -test_elbo[-1] >= -best_test_elbo * (1 + final_elbo_fail_fraction): + raise ElboException(f'Training failed because final test loss ({-test_elbo[-1]:.4f}) ' + f'exceeds best test loss ({-best_test_elbo:.4f}) by >= ' + f'{100 * final_elbo_fail_fraction:.1f}%') -def run_inference(dataset_obj: SingleCellRNACountsDataset, - args) -> RemoveBackgroundPyroModel: - """Run a full inference procedure, training a latent variable model. - - Args: - dataset_obj: Input data in the form of a SingleCellRNACountsDataset - object. - args: Input command line parsed arguments. + # Free up all the GPU memory we can once training is complete. + torch.cuda.empty_cache() - Returns: - model: cellbender.model.RemoveBackgroundPyroModel that has had - inference run. - - """ - - # Get the trimmed count matrix (transformed if called for). - count_matrix = dataset_obj.get_count_matrix() - - # Configure pyro options (skip validations to improve speed). - pyro.enable_validation(False) - pyro.distributions.enable_validation(False) - - # Load the dataset into DataLoaders. - frac = args.training_fraction # Fraction of barcodes to use for training - batch_size = int(min(300, frac * dataset_obj.analyzed_barcode_inds.size / 2)) - - for attempt in range(args.num_training_tries): - # set seed for every attempt for reproducibility - pyro.set_rng_seed(0) - pyro.clear_param_store() - - # Set up the variational autoencoder: - - # Encoder. - encoder_z = EncodeZ(input_dim=count_matrix.shape[1], - hidden_dims=args.z_hidden_dims, - output_dim=args.z_dim, - input_transform='normalize') - - encoder_other = EncodeNonZLatents(n_genes=count_matrix.shape[1], - z_dim=args.z_dim, - hidden_dims=consts.ENC_HIDDEN_DIMS, - log_count_crossover=dataset_obj.priors['log_counts_crossover'], - prior_log_cell_counts=np.log1p(dataset_obj.priors['cell_counts']), - input_transform='normalize') - - encoder = CompositeEncoder({'z': encoder_z, - 'other': encoder_other}) - - # Decoder. - decoder = Decoder(input_dim=args.z_dim, - hidden_dims=args.z_hidden_dims[::-1], - output_dim=count_matrix.shape[1]) - - # Set up the pyro model for variational inference. - model = RemoveBackgroundPyroModel(model_type=args.model, - encoder=encoder, - decoder=decoder, - dataset_obj=dataset_obj, - use_cuda=args.use_cuda) - - train_loader, test_loader = \ - prep_data_for_training(dataset=count_matrix, - empty_drop_dataset= - dataset_obj.get_count_matrix_empties(), - random_state=dataset_obj.random, - batch_size=batch_size, - training_fraction=frac, - fraction_empties=args.fraction_empties, - shuffle=True, - use_cuda=args.use_cuda) - - # Set up the optimizer. - optimizer = pyro.optim.clipped_adam.ClippedAdam - optimizer_args = {'lr': args.learning_rate, 'clip_norm': 10.} - - # Set up a learning rate scheduler. - minibatches_per_epoch = int(np.ceil(len(train_loader) / train_loader.batch_size).item()) - scheduler_args = {'optimizer': optimizer, - 'max_lr': args.learning_rate * 10, - 'steps_per_epoch': minibatches_per_epoch, - 'epochs': args.epochs, - 'optim_args': optimizer_args} - scheduler = pyro.optim.OneCycleLR(scheduler_args) - - # Determine the loss function. - if args.use_jit: - - # Call guide() once as a warm-up. - model.guide(torch.zeros([10, dataset_obj.analyzed_gene_inds.size]).to(model.device)) - - if args.model == "simple": - loss_function = JitTrace_ELBO() - else: - loss_function = JitTraceEnum_ELBO(max_plate_nesting=1, - strict_enumeration_warning=False) - else: - - if args.model == "simple": - loss_function = Trace_ELBO() - else: - loss_function = TraceEnum_ELBO(max_plate_nesting=1) - - # Set up the inference process. - svi = SVI(model.model, model.guide, scheduler, - loss=loss_function) - - # Run training. - train_elbo, test_elbo, succeeded = run_training(model, svi, train_loader, test_loader, - epochs=args.epochs, test_freq=5, - final_elbo_fail_fraction=args.final_elbo_fail_fraction, - epoch_elbo_fail_fraction=args.epoch_elbo_fail_fraction) - if succeeded or attempt+1 >= args.num_training_tries: - break - else: - args.learning_rate = args.learning_rate * args.learning_rate_retry_mult - logging.info("Learning failed. Retrying with learning-rate %.8f" % args.learning_rate) - - return model + return train_elbo, test_elbo diff --git a/cellbender/remove_background/vae/base.py b/cellbender/remove_background/vae/base.py new file mode 100644 index 0000000..2e8b93c --- /dev/null +++ b/cellbender/remove_background/vae/base.py @@ -0,0 +1,164 @@ +"""Base neural network architectures, for convenience""" + +import torch +from typing import Optional, List + + +class Exp(torch.nn.Module): + """Exponential activation function as a torch module""" + + def __init__(self, eps: float = 1e-5): + """Exponential activation function with numerical stabilization, useful + for outputs that must be > 0 + + NOTE: output = torch.exp(input) + eps + + Parameters + ---------- + eps: Numerical stability additive constant. + """ + super().__init__() + self.eps = eps + + def forward(self, x): + return torch.exp(x) + self.eps + + def __repr__(self): + return f"torch.exp() + {self.eps}" + + +class FullyConnectedLayer(torch.nn.Module): + """Neural network unit made of a fully connected linear layer, but + customizable including shapes, activations, batch norm, layer norm, and + dropout. + + Parameters + ---------- + input_dim: Number of features for input + output_dim: Number of features for output + activation: Activation function to be applied to each hidden layer + (default :py:class:`torch.nn.ReLU`) + use_batch_norm: True to apply batch normalization using + :py:class:`torch.nn.BatchNorm1d` with ``momentum=0.01``, ``eps=0.001`` + (default False) + use_layer_norm: True to apply layer normalization (after optional batch + normalization) using :py:class:`torch.nn.LayerNorm` with + ``elementwise_affine=False`` (default False) + dropout_rate: Dropout rate to use in :py:class:`torch.nn.Dropout` before + linear layer + + """ + + def __init__( + self, + input_dim: int, + output_dim: int, + activation: torch.nn.Module = torch.nn.ReLU, + use_batch_norm: bool = False, + use_layer_norm: bool = False, + dropout_rate: Optional[float] = None, + ): + super().__init__() + self.input_dim = input_dim + self.output_dim = output_dim + + # set up layers as a list of Linear modules with appropriate extras + modules = [] + if dropout_rate is not None: + modules.append(torch.nn.Dropout(p=dropout_rate)) + modules.append(torch.nn.Linear(in_features=input_dim, out_features=output_dim)) + if use_batch_norm: + modules.append( + torch.nn.BatchNorm1d(num_features=output_dim, momentum=0.01, eps=0.001) + ) + if use_layer_norm: + modules.append( + torch.nn.LayerNorm( + normalized_shape=output_dim, elementwise_affine=False + ) + ) + if activation is not None: + modules.append(activation) + + # concatenate Linear layers using Sequential + self.layer = torch.nn.Sequential(*modules) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.layer(x) + + +class FullyConnectedNetwork(torch.nn.Module): + """Neural network made of fully connected linear layers, + :py:class:`FullyConnectedLayer`. Architecture is customizable including + shapes, activations, batch norm, layer norm, and dropout. + + Parameters + ---------- + input_dim: Number of features for input + hidden_dims: List of hidden layer sizes, can be empty list [] + output_dim: Number of features for output + hidden_activation: Activation function to be applied to each hidden layer + (default :py:class:`torch.nn.ReLU`) + output_activation: Activation function to be applied to output (default None) + use_batch_norm: True to apply batch normalization using + :py:class:`torch.nn.BatchNorm1d` with ``momentum=0.01``, ``eps=0.001`` + (default False) + use_layer_norm: True to apply layer normalization (after optional batch + normalization) using :py:class:`torch.nn.LayerNorm` with + ``elementwise_affine=False`` (default False) + norm_output: True to apply normalization to output layer before output + activation (default False) + dropout_rate: Dropout rate to use in :py:class:`torch.nn.Dropout` for each + hidden layer (applied before each layer) + dropout_input: True to apply dropout before first layer (default False) + """ + + def __init__( + self, + input_dim: int, + hidden_dims: List[int], + output_dim: int, + hidden_activation: torch.nn.Module = torch.nn.ReLU(), + output_activation: Optional[torch.nn.Module] = None, + use_batch_norm: bool = False, + use_layer_norm: bool = False, + norm_output: bool = False, + dropout_rate: Optional[float] = None, + dropout_input: bool = False, + ): + super().__init__() + + if use_layer_norm and use_batch_norm: + raise UserWarning( + "You are trying to use both batch norm and layer " + "norm. That's probably too much norm." + ) + + # set up layers as a list of Linear modules with appropriate extras + dim_ins_and_outs = zip([input_dim] + hidden_dims, hidden_dims + [output_dim]) + n_layers = 1 + len(hidden_dims) + layers = [ + FullyConnectedLayer( + input_dim=i, + output_dim=j, + activation=hidden_activation + if (layer < n_layers - 1) + else output_activation, + use_batch_norm=use_batch_norm + if ((layer < n_layers - 1) or norm_output) + else False, + use_layer_norm=use_layer_norm + if ((layer < n_layers - 1) or norm_output) + else False, + dropout_rate=None + if ((layer == 0) and not dropout_input) + else dropout_rate, + ) + for layer, (i, j) in enumerate(dim_ins_and_outs) + ] + + # concatenate Linear layers using Sequential + self.network = torch.nn.Sequential(*layers) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.network(x) diff --git a/cellbender/remove_background/vae/decoder.py b/cellbender/remove_background/vae/decoder.py index 56b0a58..8d56eef 100644 --- a/cellbender/remove_background/vae/decoder.py +++ b/cellbender/remove_background/vae/decoder.py @@ -1,9 +1,8 @@ import torch -import torch.nn as nn -from typing import List +from cellbender.remove_background.vae.base import FullyConnectedNetwork -class Decoder(nn.Module): +class Decoder(FullyConnectedNetwork): """Decoder module transforms latent representation into gene expression. The number of input units is the dimension of the latent space and the @@ -20,6 +19,8 @@ class Decoder(nn.Module): expression will be embedded. hidden_dims: Size of each of the hidden layers. output_dim: Number of genes. The size of the output of this decoder. + use_layernorm: True to use LayerNorm after each hidden layer is + computed, before the activation log_output: Whether or not the output is in log space. Attributes: @@ -36,37 +37,10 @@ class Decoder(nn.Module): """ - def __init__(self, - input_dim: int, - hidden_dims: List[int], - output_dim: int, - log_output: bool = False): - super(Decoder, self).__init__() + def __init__(self, input_dim: int, **kwargs): + super().__init__(input_dim=input_dim, **kwargs) self.input_dim = input_dim - self.log_output = log_output - - # Set up the linear transformations used in fully-connected layers. - self.linears = nn.ModuleList([nn.Linear(input_dim, hidden_dims[0])]) - for i in range(1, len(hidden_dims)): # Second hidden layer onward - self.linears.append(nn.Linear(hidden_dims[i-1], hidden_dims[i])) - self.outlinear = nn.Linear(hidden_dims[-1], output_dim) - - # Set up the non-linear activations. - self.softplus = nn.Softplus() - if log_output: - self.softmax = nn.LogSoftmax(dim=-1) - else: - self.softmax = nn.Softmax(dim=-1) + self.softmax = torch.nn.Softmax(dim=-1) def forward(self, z: torch.Tensor) -> torch.Tensor: - # Define the forward computation to go from latent z to gene expression. - - # Compute the hidden layers. - hidden = self.softplus(self.linears[0](z)) - for i in range(1, len(self.linears)): # Second hidden layer onward - hidden = self.softplus(self.linears[i](hidden)) - - # Compute the output, which is on a simplex. - gene_exp = self.softmax(self.outlinear(hidden)) - - return gene_exp + return self.softmax(self.network(z)) diff --git a/cellbender/remove_background/vae/encoder.py b/cellbender/remove_background/vae/encoder.py index 6862cdf..909ada9 100644 --- a/cellbender/remove_background/vae/encoder.py +++ b/cellbender/remove_background/vae/encoder.py @@ -1,5 +1,9 @@ import torch import torch.nn as nn +import pyro +import numpy as np +from cellbender.remove_background.vae.base import FullyConnectedNetwork +from cellbender.remove_background import consts from typing import Dict, List, Optional @@ -23,6 +27,9 @@ def __init__(self, module_dict): super(CompositeEncoder, self).__init__(module_dict) self.module_dict = module_dict + def __call__(self, **kwargs): + return self.forward(**kwargs) + def forward(self, **kwargs) \ -> Dict[str, torch.Tensor]: @@ -47,7 +54,7 @@ def forward(self, **kwargs) \ return out -class EncodeZ(nn.Module): +class EncodeZ(FullyConnectedNetwork): """Encoder module transforms gene expression into latent representation. The number of input units is the total number of genes and the number of @@ -85,35 +92,34 @@ class EncodeZ(nn.Module): """ - def __init__(self, input_dim: int, hidden_dims: List[int], output_dim: int, - input_transform: str = None): - super(EncodeZ, self).__init__() + def __init__(self, + input_dim: int, + hidden_dims: List[int], + output_dim: int, + input_transform: str = None, + **kwargs): + assert len(hidden_dims) > 0, 'EncodeZ needs to have at least one hidden layer' + super(EncodeZ, self).__init__(input_dim=input_dim, + hidden_dims=hidden_dims[:-1], + output_dim=hidden_dims[-1], + hidden_activation=nn.Softplus(), + output_activation=nn.Softplus(), + norm_output=True, + **kwargs) + self.transform = input_transform self.input_dim = input_dim self.output_dim = output_dim - self.transform = input_transform - - # Set up the linear transformations used in fully-connected layers. - self.linears = nn.ModuleList([nn.Linear(input_dim, hidden_dims[0])]) - for i in range(1, len(hidden_dims)): # Second hidden layer onward - self.linears.append(nn.Linear(hidden_dims[i-1], hidden_dims[i])) self.loc_out = nn.Linear(hidden_dims[-1], output_dim) self.sig_out = nn.Linear(hidden_dims[-1], output_dim) - # Set up the non-linear activations. - self.softplus = nn.Softplus() - - def forward(self, x: torch.Tensor, **kwargs) -> Dict[str, torch.Tensor]: - # Define the forward computation to go from gene expression to latent - # representation. + def forward(self, x: torch.Tensor, **kwargs) -> torch.Tensor: # Transform input. x = x.reshape(-1, self.input_dim) - x = transform_input(x, self.transform) + x_ = transform_input(x, self.transform) - # Compute the hidden layers. - hidden = self.softplus(self.linears[0](x)) - for i in range(1, len(self.linears)): # Second hidden layer onward - hidden = self.softplus(self.linears[i](hidden)) + # Obtain last hidden layer. + hidden = self.network(x_) # Compute the outputs: loc is any real number, scale must be positive. loc = self.loc_out(hidden) @@ -122,6 +128,10 @@ def forward(self, x: torch.Tensor, **kwargs) -> Dict[str, torch.Tensor]: return {'loc': loc.squeeze(), 'scale': scale.squeeze()} +def _poisson_log_prob(lam, value): + return (lam.log() * value) - lam - (value + 1).lgamma() + + class EncodeNonZLatents(nn.Module): """Encoder module that transforms data into all latents except z. @@ -175,66 +185,59 @@ class EncodeNonZLatents(nn.Module): def __init__(self, n_genes: int, z_dim: int, - hidden_dims: List[int], log_count_crossover: float, # prior on log counts of smallest cell - prior_log_cell_counts: int, # prior on counts per cell + prior_log_cell_counts: float, # prior on counts per cell + empty_log_count_threshold: float, + prior_logit_cell_prob: float, input_transform: Optional[str] = None): super(EncodeNonZLatents, self).__init__() self.n_genes = n_genes self.z_dim = z_dim self.transform = input_transform - self.output_dim = 3 + self.output_dim = 1 # Values related to logit cell probability - self.INITIAL_WEIGHT_FOR_LOG_COUNTS = 2. - self.P_OUTPUT_SCALE = 1. + self.INITIAL_WEIGHT_FOR_LOG_COUNTS = 1. + self.P_OUTPUT_SCALE = 5. self.log_count_crossover = log_count_crossover + self.empty_log_count_threshold = empty_log_count_threshold self.prior_log_cell_counts = prior_log_cell_counts + self.prior_logit_cell_prob = prior_logit_cell_prob # Values related to epsilon - self.EPS_OUTPUT_SCALE = 0.05 # slows down learning for epsilon - self.EPS_OUTPUT_MEAN = 1. - - # Set up the linear transformations used in fully-connected layers. - - self.linears = nn.ModuleList([nn.Linear(3 + self.n_genes, - hidden_dims[0])]) - for i in range(1, len(hidden_dims)): # Second hidden layer onward - self.linears.append(nn.Linear(hidden_dims[i-1], hidden_dims[i])) - self.output = nn.Linear(hidden_dims[-1], self.output_dim) - - # Adjust initialization conditions to start with a reasonable output. - self._weight_init() + self.EPS_OUTPUT_SCALE = 0.2 # slows down learning for epsilon + self.EPS_OUTPUT_MEAN = 0.6931 # log(e - 1) # softplus(0.6931) = 1. + + # Set up network for inference of p + additional_features_p = 4 + self.layer1 = nn.Linear(additional_features_p + self.n_genes, 512) + self.batchnorm1 = nn.BatchNorm1d(num_features=512) + self.layer2 = nn.Linear(additional_features_p + 512, 512) + self.batchnorm2 = nn.BatchNorm1d(num_features=512) + self.layer3 = nn.Linear(additional_features_p + 512, 1) + + # Set up network for inference of epsilon + additional_features_eps = 4 + self.eps_network = nn.Sequential( + nn.Linear(additional_features_eps + self.n_genes, 512), + nn.BatchNorm1d(num_features=512), + nn.Softplus(), + nn.Linear(512, 512), + nn.BatchNorm1d(num_features=512), + nn.Softplus(), + nn.Linear(512, 1), + ) # Set up the non-linear activations. - self.nonlin = nn.Softplus() self.softplus = nn.Softplus() + self.dropout50 = nn.Dropout1d(p=0.5) # Set up the initial biases. self.offset = None # Set up the initial scaling for values of x. self.x_scaling = None - - # Set up initial values for overlap normalization. - self.overlap_mean = None - self.overlap_std = None - - def _weight_init(self): - """Initialize neural network weights""" - - # Initialize p to be a sigmoid function of UMI counts. - for linear in self.linears: - with torch.no_grad(): - linear.weight[0][0] = 1. - with torch.no_grad(): - self.output.weight[0][0] = self.INITIAL_WEIGHT_FOR_LOG_COUNTS - # Prevent a negative weight from starting something inverted. - self.output.weight[1][0] = torch.abs(self.output.weight[1][0]) - self.output.weight[2][0] = torch.abs(self.output.weight[2][0]) - - def _poisson_log_prob(self, lam, value): - return (lam.log() * value) - lam - (value + 1).lgamma() + self.batchnorm0 = nn.BatchNorm1d(num_features=self.n_genes) def forward(self, x: torch.Tensor, @@ -256,44 +259,55 @@ def forward(self, # Calculate the log of the number of nonzero genes. log_nnz = (x > 0).sum(dim=-1, keepdim=True).float().log1p() - # Calculate a similarity between expression and ambient. + # Calculate probability that log counts are consistent with d_empty. if chi_ambient is not None: - overlap = self._poisson_log_prob(lam=counts * chi_ambient.detach().unsqueeze(0), - value=x).sum(dim=-1, keepdim=True) - if self.overlap_mean is None: - self.overlap_mean = (overlap.max() + overlap.min()) / 2 - self.overlap_std = overlap.max() - overlap.min() - overlap = (overlap - self.overlap_mean) / self.overlap_std * 5 + # Gaussian log probability + overlap = -0.5 * ( + torch.clamp(log_sum - pyro.param("d_empty_loc").detach(), min=0.) + / 0.1 + ).pow(2) else: overlap = torch.zeros_like(counts) + # Calculate a dot product between expression and ambient, for epsilon. + if chi_ambient is not None: + x_ambient = pyro.param("d_empty_loc").exp().detach() * chi_ambient.detach().unsqueeze(0) + x_ambient_norm = x_ambient / torch.linalg.vector_norm(x_ambient, ord=2, dim=-1, keepdim=True) + eps_overlap = (x_ambient_norm * x).sum(dim=-1, keepdim=True) + else: + eps_overlap = torch.zeros_like(counts) + # Apply transformation to data. x = transform_input(x, self.transform) - # Calculate a scale factor (first time through) to control the input variance. - if self.x_scaling is None: - n_std_est = 10 - num = int(self.n_genes * 0.4) - std_estimates = torch.zeros([n_std_est]) - for i in range(n_std_est): - idx = torch.randperm(x.nelement()) - std_estimates[i] = x.view(-1)[idx][:num].std().item() - robust_std = std_estimates.median().item() - self.x_scaling = (1. / robust_std) / 100. # Get values on a level field - # Form a new input by concatenation. # Compute the hidden layers and the output. - x_in = torch.cat((log_sum, - log_nnz, - overlap, - x * self.x_scaling), - dim=-1) - - hidden = self.nonlin(self.linears[0](x_in)) - for i in range(1, len(self.linears)): # Second hidden layer onward - hidden = self.nonlin(self.linears[i](hidden)) - - out = self.output(hidden).squeeze(-1) + x_in = self.dropout50(self.batchnorm0(x)) + p_extra_features = torch.cat( + (log_sum, + log_nnz, + overlap, + torch.linalg.vector_norm(z.detach(), ord=2, dim=-1, keepdim=True)), + dim=-1, + ) + eps_extra_features = torch.cat( + (log_sum, + log_nnz, + eps_overlap, + torch.linalg.vector_norm(z.detach(), ord=2, dim=-1, keepdim=True)), + dim=-1, + ) + + def add_extra_features(y, features): + return torch.cat((features, y), dim=-1) + + # Do the forward pass for p + x_ = self.softplus(self.batchnorm1(self.layer1(add_extra_features(x_in, p_extra_features)))) + x_ = self.softplus(self.batchnorm2(self.layer2(add_extra_features(x_, p_extra_features)))) + p_out = self.layer3(add_extra_features(x_, p_extra_features)).squeeze() + + # Do the forward pass for epsilon + eps_out = self.eps_network(add_extra_features(x_in, eps_extra_features)).squeeze() if self.offset is None: @@ -301,39 +315,53 @@ def forward(self, # Heuristic for initialization of logit_cell_probability. cells = (log_sum > self.log_count_crossover).squeeze() - if (cells.sum() > 0) and ((~cells).sum() > 0): - cell_median = out[cells, 0].median().item() - empty_median = out[~cells, 0].median().item() - self.offset['logit_p'] = empty_median + (cell_median - empty_median) * 9. / 10 - else: - self.offset['logit_p'] = 0. - - # Heuristic for initialization of d. - self.offset['d'] = out[cells, 1].median().item() + assert cells.sum() > 4, "Fewer than 4 cells passed to encoder minibatch" + self.offset['logit_p'] = p_out.mean().item() # Heuristic for initialization of epsilon. - self.offset['epsilon'] = out[cells, 2].mean().item() + self.offset['epsilon'] = eps_out[cells].mean().item() + + p_y_logit = ( + (p_out - self.offset['logit_p']) + + ((log_sum.squeeze() - self.log_count_crossover).abs().pow(0.5) + * torch.sign(log_sum.squeeze() - self.log_count_crossover) + * 10.) + ) + + # Enforce high cell prob for known cells + beta = 50. # like a temperature for the sigmoid's sharpness + alpha = (beta * (log_sum - self.prior_log_cell_counts)).sigmoid().squeeze() + p_y_logit = (1. - alpha) * p_y_logit + alpha * consts.REG_LOGIT_MEAN - p_y_logit = ((out[:, 0] - self.offset['logit_p']) - * self.P_OUTPUT_SCALE).squeeze() - epsilon = self.softplus((out[:, 2] - self.offset['epsilon']).squeeze() - * self.EPS_OUTPUT_SCALE + self.EPS_OUTPUT_MEAN) + # Enforce low cell prob for known empties + alpha_empty = (beta * (log_sum - self.empty_log_count_threshold)).sigmoid().squeeze() + p_y_logit = alpha_empty * p_y_logit + (1. - alpha_empty) * (-1 * consts.REG_LOGIT_MEAN) + + # Constrain epsilon in (0.5, 2.5) with eps_out 0 mapping to epsilon 1 + # 1.0986122886681098 = log(3) + epsilon = 2. * (eps_out * self.EPS_OUTPUT_SCALE - 1.0986122886681098).sigmoid() + 0.5 + + d_empty = pyro.param("d_empty_loc").exp().detach() + + d_loc = self.softplus( + (self.softplus(counts.squeeze() / (epsilon + 1e-2) - d_empty) + 1e-10).log() + - self.log_count_crossover + ) + self.log_count_crossover + # d_loc = (d_loc + d_loc_est) / 2 return {'p_y': p_y_logit, - 'd_loc': self.softplus(out[:, 1] - self.offset['d'] - + self.softplus(log_sum.squeeze() - - self.log_count_crossover) - + self.log_count_crossover).squeeze(), + 'd_loc': d_loc, 'epsilon': epsilon} -def transform_input(x: torch.Tensor, transform: str) -> torch.Tensor: +def transform_input(x: torch.Tensor, transform: str, eps: float = 1e-5) -> torch.Tensor: """Transform input to encoder. Args: x: Input torch.Tensor transform: Specifies which transformation to perform. Must be one of - ['log', 'normalize']. + ['log', 'normalize', 'normalize_log', 'log_normalize']. + eps: Preclude nan values in case of an input x with zero counts for a cell Returns: Transformed input as a torch.Tensor of the same type and shape as x. @@ -348,7 +376,17 @@ def transform_input(x: torch.Tensor, transform: str) -> torch.Tensor: return x elif transform == 'normalize': - x = x / x.sum(dim=-1, keepdim=True) + x = x / (x.sum(dim=-1, keepdim=True) + eps) + return x + + elif transform == 'normalize_log': + x = x.log1p() + x = x / (x.sum(dim=-1, keepdim=True) + eps) + return x + + elif transform == 'log_normalize': + x = x / (x.sum(dim=-1, keepdim=True) + eps) + x = x.log1p() return x else: diff --git a/docker/Dockerfile b/docker/Dockerfile index 9b35e0f..1a37bc4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,40 +1,35 @@ # Start from nvidia-docker image with drivers pre-installed to use a GPU -FROM nvcr.io/nvidia/cuda:9.2-base-ubuntu16.04 +FROM nvcr.io/nvidia/cuda:11.7.1-base-ubuntu18.04 -LABEL maintainer="Stephen Fleming " +# Copy the local cellbender repo +ADD . /software/cellbender -# Install curl and sudo and git and miniconda and pytorch, cudatoolkit, pytables, and cellbender -RUN apt-get update && apt-get install -y --no-install-recommends \ - curl \ - ca-certificates \ - sudo \ - && sudo apt-get install -y --no-install-recommends \ - git \ - bzip2 \ +LABEL maintainer="Stephen Fleming " +ENV DOCKER=true \ + CONDA_AUTO_UPDATE_CONDA=false \ + CONDA_DIR="/opt/conda" \ + GCLOUD_DIR="/opt/gcloud" \ + GOOGLE_CLOUD_CLI_VERSION="397.0.0" \ + GIT_SHA=$GIT_SHA +ENV PATH="$CONDA_DIR/bin:$GCLOUD_DIR/google-cloud-sdk/bin:$PATH" + +RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates sudo \ + && apt-get clean \ && sudo rm -rf /var/lib/apt/lists/* \ - && curl -so ~/miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \ - && chmod +x ~/miniconda.sh \ - && ~/miniconda.sh -b -p /home/user/miniconda \ - && rm ~/miniconda.sh - -ENV PATH=/home/user/miniconda/bin:$PATH -ENV CONDA_AUTO_UPDATE_CONDA=false - -RUN conda install -y "pytorch>=1.9.0" torchvision cudatoolkit -c pytorch \ - && conda install -y -c anaconda pytables \ - && conda clean -ya - -ENV DOCKER='true' - -RUN git clone https://github.com/broadinstitute/CellBender.git cellbender \ - && yes | pip install -e cellbender \ +# get miniconda + && curl -so $HOME/miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-py37_23.1.0-1-Linux-x86_64.sh \ + && chmod +x $HOME/miniconda.sh \ + && $HOME/miniconda.sh -b -p $CONDA_DIR \ + && rm $HOME/miniconda.sh \ +# get gsutil + && mkdir -p $GCLOUD_DIR \ + && curl -so $HOME/google-cloud-cli.tar.gz https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-${GOOGLE_CLOUD_CLI_VERSION}-linux-x86_64.tar.gz \ + && tar -xzf $HOME/google-cloud-cli.tar.gz -C $GCLOUD_DIR \ + && .$GCLOUD_DIR/google-cloud-sdk/install.sh --usage-reporting false \ + && rm $HOME/google-cloud-cli.tar.gz \ +# get compiled crcmod for gsutil + && conda install -y -c conda-forge crcmod \ +# install cellbender and its dependencies + && yes | pip install -e /software/cellbender/ \ + && conda clean -yaf \ && sudo rm -rf ~/.cache/pip - -# google cloud SDK and gsutil -RUN curl -sSL https://sdk.cloud.google.com | bash -RUN conda install -y -c conda-forge google-api-python-client oauth2client google-auth \ - google-cloud-sdk google-auth-oauthlib \ - && conda clean -ya - -# Add cellbender command to PATH -ENV PATH="~/miniconda/bin:${PATH}" diff --git a/docker/DockerfileGit b/docker/DockerfileGit new file mode 100644 index 0000000..05bbde4 --- /dev/null +++ b/docker/DockerfileGit @@ -0,0 +1,35 @@ +# Start from nvidia-docker image with drivers pre-installed to use a GPU +FROM nvcr.io/nvidia/cuda:11.7.1-base-ubuntu18.04 + +# Specify a certain commit of CellBender via branch, tag, or sha +ARG GIT_SHA + +LABEL maintainer="Stephen Fleming " +ENV DOCKER=true \ + CONDA_AUTO_UPDATE_CONDA=false \ + CONDA_DIR="/opt/conda" \ + GCLOUD_DIR="/opt/gcloud" \ + GOOGLE_CLOUD_CLI_VERSION="397.0.0" \ + GIT_SHA=$GIT_SHA +ENV PATH="$CONDA_DIR/bin:$GCLOUD_DIR/google-cloud-sdk/bin:$PATH" + +RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates sudo git \ + && apt-get clean \ + && sudo rm -rf /var/lib/apt/lists/* \ +# get miniconda + && curl -so $HOME/miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-py37_23.1.0-1-Linux-x86_64.sh \ + && chmod +x $HOME/miniconda.sh \ + && $HOME/miniconda.sh -b -p $CONDA_DIR \ + && rm $HOME/miniconda.sh \ +# get gsutil + && mkdir -p $GCLOUD_DIR \ + && curl -so $HOME/google-cloud-cli.tar.gz https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-${GOOGLE_CLOUD_CLI_VERSION}-linux-x86_64.tar.gz \ + && tar -xzf $HOME/google-cloud-cli.tar.gz -C $GCLOUD_DIR \ + && .$GCLOUD_DIR/google-cloud-sdk/install.sh --usage-reporting false \ + && rm $HOME/google-cloud-cli.tar.gz \ +# get compiled crcmod for gsutil + && conda install -y -c conda-forge crcmod \ +# install cellbender and its dependencies + && yes | pip install --no-cache-dir -U git+https://github.com/broadinstitute/CellBender.git@$GIT_SHA \ + && conda clean -yaf \ + && sudo rm -rf ~/.cache/pip diff --git a/docs/source/_static/remove_background/PCL_rat_A_LA6_learning_curve.png b/docs/source/_static/remove_background/PCL_rat_A_LA6_learning_curve.png new file mode 100644 index 0000000000000000000000000000000000000000..4f66e009c04c33fce8ccc0860f64019e9f7f515c GIT binary patch literal 23433 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 E0Am;H3IG5A literal 0 HcmV?d00001 diff --git a/docs/source/_static/remove_background/bad_learning_curves.png b/docs/source/_static/remove_background/bad_learning_curves.png new file mode 100644 index 0000000000000000000000000000000000000000..356a8d1c6b45f2017c885017be80d8f424a60c7a GIT binary patch literal 214054 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�`evtVr{IhQm`L+50&2 zVigiag|Es>_uSNIHg%;rd^{4;*NqfLiacM-!ArTq`^@%-?bPfks|}=y$#wZpJPKg2 z_Sfgk7$x<2xVN|1p?>ysKxsXYN~Q59i}3IXrj^?xH5lu#%ald9|BE{bmk)@*MnQ=l z$;pioD-wOs#O|Lz4#RiY2gUD11DSsa)TGaZM4~(T@Kx(Ig@qlPUw0oMrO|ghmj}L% zNeE8-J#zTFPsiBg*bg=mpZ`P02OodA&f_)o^Je?Y_`2IG*zb7;4)IC}phdQ{626^~ z#8*!nBAE`F2ASob=ZhxUWVx7T&^>+lWX^Y*A*BPn;UBQf#jPpkYxzacM0w4a#Nq8Hy zt8Q|h8dU5byeT8`usYBB)3uyOw%xSjWWAr&M#?c`R&&Fduj#1Ax%Ko_ijEs7Qi3m) z;)wL53K=kp3WjU{oc0NW=34d!dAxba*OUz7&)?tC_KJPU9Qb~ggeThK>*%zn(U)m2 zv$%~?uJr7(dRhb=Hb<`i?^In>(}9E3J0d}}=!syLb1o5?w9#P}2(NIAa;b@_?UGh9 zGOZi&Gj8uT20Dt+`S=^vlP774^i4CcH9?;Wg_FkZRj74KAsTITJn1<}eo>-`^b4-q zTmWxx&bR0@%FqJ8KQh&Y+tR=O$Xc}J#RXov3WDtB*gSGND^>ni$mJnG_cfTavkR`8 zvhes=;@H-z_PSP>Am}^Hw76RnYMabdC!mE>Yw7=iK@c~e#YFpe%`0| z-E@Tq3<`eUdPv+XnufwNb|&y%w+C0%f~;VWKeP@!7#&VRW1*GIGUFL&_l{qNPfpYF zx>61)c%8Il(pL5HkER|;Trh{{(3J}x2yEe<>&FY3%ac`@QZfhQcls_}jd44Z2{$b+ z2^URrRtG=?ONz-^NSYsUkum%#n1u&ff*u7`g{F7H7=EAdI8LDYe1MNZPj5pIkiR7l z5I>P51?g)&ZrRNb@t;Ck1Ef@r!6(*tOk?&`oGDBR*xYYzQPtz}RTD+<=nTI0sip?N zH#|fNMFYcD-{+!z z%;}Y`p&eDE`|{afy{V?bx}=LF)f%topnMiRvHN19BYGpd?3C-P;l{Ky@d(k(vEgDx zRug?BMjyNy=E`LMSi~%4Z}mHPU)ffsM9XL|X!qykaKuqokgrYd{-L&Tp(;Z+L3Ns8 z{X&$2b4IMJLBZhX+)HN5$&^)OkH@!vqzY*AKoT-7^cyq}KjDiD=YQ!vc-+W^o~4t) z%uXp5?fsit#}&W*ot*3AvdXT60VmqxJo5G$^mI+|mzOhP5*ew{T;Zf`wE1qbgi`_= z$A#DsP=jLPV#DkD!|xAHp7-*I_9G>?6oN!H-Q!P+Kt?^)MG#1aNtByG-vABkCpYe( zxKZIUZ|($t(B(l&1Hmv)(A*7+E4d{B7)9UF$njVcycafZ0k}#TQ?+fZxOHj;VR#MN zmGRm}MxF6m6u8yvYS6^jgH}RP>S*ZUaM&T&iAu`RH+cb19qms4JMB=u$76jC4T<_O zv%18z~CFIiu#t388xbE<6Q%phPTF~Ng3 zZ|VNMN%M&aCE%H_JPI@fCa)|^DrnvxzMiq3)!g_&!uU}vPG+tdQk2Wlp0;LlvCh^v z5k&gE7+i_3QfTOrSCp~G0ho$4-T6_`E5xb3b^Af3zCiH$+k3dx^u@=NBcSQnb=zfqLhF(jR zEwtPgCNb~yUnlq3>T5l?i>LPBo|LXYbU6qVZ5_j;|!>QrNZG8-m_8x?3 zdbuUU6iyAMC9E|1)#HIuN(EU+O~ulM<@*-k>OUw@^U6%ug?XR zPHQgkF90~3-91I~k>IQ21xhkUl6TTwl`b!>l|l@z>rX$mPyBu{XlVCC(N7B7*Ihh5 zl=5?0BXie6N8yq8J=)zrWHa|fQbQ&uf?I8Ur^4~X33PQGMi$S$JA+lb3QES{aa#aj zL=%4U&uw(k%GSYy_UT}W91qCEi&BRQQeF}IvS;*Jr#RZ)vba04?pgGPV#1PHAdosv z27~q&&z&63BN|G&kIGMKrp@2tVo7(Cjf*8oE9K&oOEyqhpI;1CZbpusB|MuQE^RnM zJ-tM@Ij4xnvA^T{JX1co?q)gBd7oJ`8TaUPnURjefqc$?po6vjaBOAPp`E>u@zI#_ zf=H?^0Q&YAz~#zQdfH&_+CJJgwd`6U6T@#am8f=GZGp9MKVJENQ9(fvHRjix z!0F^JkMF9cF3G$8P{2BhN#O?Ug8{C^m*9KWeUM&9rsv=K zq2v&zG#kBZbtDlImM$RQjYg8=dL`+gEijPHJ$uaCe`K9rn@MUu>~$UDi_U!E0c&@J zHsZXGrN1A?kDJdu`=UWN@VERA4-gJ5mE%8NZ%sxs_KuCnWpgR2k9>Z-S=rf{jLnio zMTIFUD-XPT-L)Iu8Y>M`bDfl1A`CBa(QUhsQaIZ3qBXTZ(QQUy!WU}TL$(FX63>P` zMl5vx_0&un(6yt}=v=9%$Ptk~C)nOj8K~@O(X2ezWamDCf|Qlo%Umk?LqBf1efnpi zb!&b0%<_PdwDTAi&_V@|hb{|lcZsG;ew@<^S~Y;^87obT!i3nQI0ab%dQNS78G(DF1q<^Uyt}P$+a5A;v{b!O9i`~$;+OG$X z%~q=omGGqe^xzy<+w7UwM^8Q5ACY^hQ=SavR%CBU!xp|MsFG4fMJ!?b z;^>r=3^jzUk(W}4KglLBET~v($c240sW@316eg*BOw*=`2Itp;KTiL51?Y;xOYJ9qIy)b%wdv6o)oB$EX6^ME z>J@v8;o&b;L<)@ML_yUStEWChsG{l(ozXu@F$>m47hR{5nu#37_-WkkLtBsA&Ghsl z5)4^EHlKRCXeW$;yE#92BK12y*niq(S~6hX;${$%$W2G*cgd5Bo8azY1Ib%Y>;PR^ z!Vj(tAoT`4x#(f6fiu%Bi*+_@j!co@uCcAqgcct4zy6eiOiIJC)GAU_)y_A)xvh85 zEK;Q04YPrdBTIcDG$z!FYU`X*>KigKf581O0Rf`)D6(%Y6?TPgC$orW_X!5uf|wwozbO$K3$h2`Py4 z08M2@K*#>iv+}kfen(0)p1HC(q)Whwid-UY;NX@^*aCo7(At_-mj8mSAONxqd^#;s zU0J-j2bo$$(aPx*tY zS-SQ_(riXnf^|R1+;Hc(Z9g@%SWBW~XC^dF|L%9Dw5}l=JnOO}#HbTrO!Va!)7g`< z_@3FIJ8a#QkU7YrQ1#LN53*n;7SnOzFSdz2AKmF>cAw19*JnvavE6vBaOlSmL&{Co zkICLMwUVSxmSQlm`Q5aRMVwnko0$iOC(Fdw~s5_9k^6I>ry3R{4-1d>n8y%cIC3QG&`?Q z3GPMY?7WU}KL05b6cINUZ~HbnS=*k(xcTAry>A~bjsJ^J@O3W!`COyVTfJ3>q<+o4 zK9x#6)%jOLk&IY@k#=`eQ5I+i$7yswQ6k#*yP);*xVtxw=cd0utQ!L=QGiXE%7eAe zQmN#X@7x>QK?`_!)-?eSMuu8gY`8|%sT&;h6ypaOnhOi z-a2Evn$N1C2Un`!2tt^JX9k}sht}V-?5r^TK2URP!+6 zVZ;3&krhdjk9dM81f($F*ANFYmq-XoIytql~!8_}nm%+6DZ`?TR{TVt!nLDz`P<zSl&HPgB7F=R5tkD)0IO$(X*E+1s|+OCQm67e&-HEI9WkhmBxF?!6ijAYl&2RTHn`ZmswuVJYBb3J0=cYNl zt(e(QD+T-*SIT0_bXzHj#>3S~e^}e`BA|g|%f!m#L7EeNpG8*eXrz&U+IR791pB~p zXr`BtE5VpuFc1zHiBd*e`U&U1)G}d`$C1W;_fv1nq9C^tp1OC+fCD7PdF}W`Ar-GX zj>0ff7%9av(}0#9Jm)KW2=XwEi5;2@3jJi4B_EeVx=RvIBcew2*fT=3ub~* zOQS>~cXn< z69%SV#xw`@Lw|lbf_boyZc%b{ zVKRw!XZ?98u!I&O%#N3FQccIg^HW-u^m-vRzv#XW#nw*`Jx-Pd1#B_E-^!`1%tqct5 z=m&p;?z$A8iQtf9lzIsLBcgSsBmP^-Bflsjk#sU>d>6u!}5W+bfSZ{~x) z<&GauCj~*I#pb-q`{aL@<9);J0$JbR@9gmzrnsU^}$QCg_QS0sWlEJ=>PY| zZ4j!;=b=b!5sWr~FliLx$ttd+Vu-OAN3$3>J9EKSN=CH=RzSQG5rV>^`0qH2PXpZ| zeE)LRM?4k#4Yr^GUv$C!ZT)aPyd1>sqf{72rGFO6Qjx%V*G-dG9XBr?{wG^c0->*F zgzXI%MufTj!cnNB4`JaRG=m3z_i{_ckw7|dC_0oCTfPgS3}@JCku!ghWCmF|aZglV z!9m{cX?#Ke-C)?saRz(LgTQP;&-9c3;h!tjXQ4+K(zuk=jc2!|y_Jx+w)WrA(k&x5w@S$8F-;q<835) zI&7Ac#7GbwM#uyCpG77V76j;rL8u2xfi5LItEYVFK{=q`LuX*h|B@?Ts0*A|1ce=0 z4Bk_l1*P@)wTx_*>JegF|8-xmSM z-K4j@_14?Zb3byF^e@==_L~9L`t*>YX3roltyjI)!I&y@OhZ|FxYBs((BWBjbd6Yu zf@NTQNjl3n<3~?8XCkV`TgaSt62}=F2;KgGsLf0R0sJmm7+hmdpVCh*qv>=os$X%!#KS?}x;zafUuw-z*4MmjOl`XPsKB|y?gsg6g5CD|)JO#K|?M|fS zi?*Y&fwaos_T^Ue$F&b2A=hFCBA9*0F9=kz{$p?k%g4|H7D$D$^xY^eNa`F`q_F1u z$#z4ybpexH69Tl-llzS7QW2t_)5|0=V|^q4%JYGW(XNl+QgzGTY#Ld2Q*;z2-X3`A^A0B61@@?>9F}R_7F}5Eo@un)$2+PF3B+daL!WSZ+q)GUgwv4w)7xxRc{^4 zb|Io)`G7j63jnh^PoLhWBejr;T0jU_f}n!)M&88w8lNP58X;CUhP{&wof5s!|95N* z_tVQp14FRS{h+N6;*Z18_Q!Rbe^>B9K{%0ce*~&tue=g&p7%M>^L|c3NnZ)JYAvSk z^TENwX=dEf^(0Bz_XC+2eB_!_0xEDJIERWtGGRm?Mk@PW&8%k>a3+n|^~O%N9#xFE z@n~{8@Q1563q^Pzeften*x@T*Us{Jg4{@;kWtV96bf_hgKv!EU z;D^`P*l5_0aU0vEekvrNcyc=zPE5)ks_u};Y9sDMYj41ZW!-!c`dtOb|E9!B(qo8Q ztGY(Bn8aeH{qrc$G3Y1NvMMRP3XoiQ?2Nn^J1CU@C)zg`PN7?bz;V(3|E3TB1O9;o zjuAcQQ{fqPoIU+C1-hA8gs|@B@4aZE6Bb$YWDob&-cW@k1iApE+A2!K6y|$eUYrRk zYp);gngLsVA0G}lg1s2P-Y)@Av6El#nQ$Qn4jxaaw+|>ef2dZ%*4I5tp_dzaRr($=IBl^_JB4MzgVCWPaIdo z4jOFbNt6EE+xYQBXqk`@fAZC;364e$1fADIUOciH?iJ@i1f+%}^zXNTgB`o5i;@Z{ zA~02c%$&+Yn7Qfm-MmvCmaQ|93gT+a&_-uumXB-T}? zEm9OnmQJBRFY|ot!-N|-ZXucg1Lh?mhkMgj%mcsaJcn=T0R8axx!^{3A58L{)idNp zl?b2paHY8+2=!UR?dq-*(475W^M1P!sQY6shQ0gY-ZC(tju9}YD0;}Xj{gy`b!*W6 zu)I-k^5&kBo__E{3}jS*EgnlhGL~2bvcOV>b4>c~ONyIkh+!Ta0jP2nm;RZueo7Sy@p^UoODr}tE|89uBv^!r3^~+@Bm@vq$pKjh=^zxE z6%}eiwSU5W0QFE%VSl@C7>utLYv24Dq{%~0%mp4OA8Re&-&EmYWOg7R%eGt~S{+_I z!8OF9SZiwJ0zigi6&AzcJ7B6H!PJ;y;30n#hatT!1n4=_uJe6s7be}!;9dC-y+MRw zh@~2M=W#4+9x?24nsHAuWj?zxX$jk0Vhr9W;DZyW$HFCnBny8HVG-3rX8AyL)n}Ok zu_|kE+!+D*UX>UQ?JjxB#aY;|2_NP^E3%aEF;$Vs8hw`B0~MWzn`?h?g68$3oJ)A~51= zPgXuc0!CwLB|JoG(!=HkJjhFxzxRFY%51;V$uah+IaAB=Ho$j1iFN^+f7AiF{c7;$ ziGz0VQ8yphThwdfj`Z|Uw0bfyQhcDx>6I2m zY3>lVpp_j)NH91KD@U!rKz4k@-VHEvhiP~#k8On+Ho^#H(L8s18aM?wHw$F1$iY5i z2a*l^#~e_pk99Bkt7#4Wx}EXF5VO$n6)RlYD$Nrr(NrDE-%rYJ^p&p)*7dIeY?CYncG3Hq0BimWYngcpg4nc^)j# z_LN1st2AQUSFdS>&wwmoSM5%I8xv$iD%l13Sh!UQNd3XU9L_|_I6!c%?8SOE=p{Cu zfVtr=VbXKNem+EX#k>FQIUa1txCK3|2QJUI2H8{D#`o!S3U>Ll;dB5=4$U?z2ZQS` z3x!G$eGX(-5({01(!?3!rUi{fd(A$O*VW)zLX>fsiGF9kb#+)Of7j+Ct=`g!zY1S} z$Xh9LPG+uQf{ZRb9JEA9uuV+J77mz%C5?N8 zC3u{Obu%n3u^>{whS10QjQ>o)ecrL@vsBZKXO9^cfi47O$8fvXbdrB;f2}oVW!8tG zc40x~0Aa*X1Eyh?J7%F+(>DV;Lm5b}EOE*Zl%$MP!b)iEHRALtLJA5BJ%J7npg^^+ z6Gf~28kiJ|VgJj?-sju3LP8Iv#X6|<@HCk;x*4hM6AvQUzl*}+Ybqr$=<~e%d6_zZ z(@JO!eehqA7AWlRYfaKq@;#B=o9c*jSu7-PoT`#qCO^#t#h(X9Ni^&rsc?D8;0fD` z*3XH#qbQf6%7WnQ8NE2{^L1kC^b}bT#KTdXDKm?UB0+CR-iKi4BnqE_i&%djRkljr zrjkqu+C94XZ!i;{7Eg@GfH#2cdsa^Pxw;@mCMn%;%q9k1%>FK+2B^#xQ>{x+1N+(` zp)ay7$Fld_f`^Qu$`_ zEZvZK&r%Q@1&&{xz8e%;@{@wlcz%v(m%%>ZWR$P$XE<05IWK&;Wy>W~nmOjJlO7F@ zC;cvKKknPM#mhMI|Iu^}?0GcII<{@wwrw=F)v&Q`qm7+3wrw`H?Z$1?@NC}iIzJ$rXJ-fZ%sqfS zrts7t7wZRM@HTdGVmfbvvgHSF&hB?zrn+fJnR*UE5rd)!(ATY}g?QLDcGh(PMrSJD z4+mCL-EVMT2(f(II+1F*eiRPu_OYF@XHQ&1&5U|0{5Hq+bw)z?>lpEEU1RrB#}&al z>8ehIzHhNg`v&>mlp&+BYy;}Dr!IF4LC{MVEG-i`w_GJe$O-KJoS=q4lbmPF+ep!f zWcEaLReLUabY}-#$HY5Pt>{h2R&++VYCSAU(k4Cqn&4_s0fy8ZCLc(nL|8)gdLB4^ zpKeDDa(k}>-l2=66GpTgdvFV!U{3Nph=3h=5%wD`rVGc6Pv&pluUGXud)q!&f-yeI z(@<&5-UGuN>wd-nyY+?St-iXDcey77w0&IDoM#`|6|Ynu_Mmv8;90M)&_txT!MA(Q zzMN(Hoh`P}0pbt^!;#(7wn7L<3PnJ9Feo16NkS2vEyghlAuATU|ZTnsK<)_Mb zlOyUE*4v53@D~-Hi5@im!|{bkI?g)Uc-;V z6+=Hn`831%_9@%i-tcpWHq_o1LP)-dqIp!>r>cgZqsFDba7qr5^>tPWHLAU9kuJY# z(Kn`CY(GSB9VbJI!G&?cl3Jy%83*>U9^O{VSRtwup_T!)u>Lzyoq-3% z0N8EQA`^DmHenyIRLVXfRIxE*QQ3~-TJK68v()10+HqMfa%x^=gXlx6IMAOI_dBN- z=K9*1MQL?Y^38#)Q1R#93QgO4&w=cH)@&}maNBFS5U=C(V^k$=1$ zTS=^_0yiPVna}d8#z`z!*><>gt+Bt8!F2O8*=~UtWQfLL;%^qJ zK11NGcmI3qX~MJ+P9J<9rFjiptnZc{KDn9SvW5PR{ya}evAG}Cbp0w?PC;*gpOmh> z4@$D)2l}%Mp?{eUKujcYnZzqF7JQ1fXO zalxvP8fSfptR8Ve$?gQ3ZyN^L>0Z$!i=jinpTOgPRMDF-&c|>Yl_m*VKrw`X^pC_l|Vl?2ldwHyG5;5<_hKG2TigvVFa*pT?ngrg*|Z8&ME-F_Si>3eq`MzWYkhFcm6o{yNpgftotW|zP+Kb@-I7e*O)b0TYwfnhWaGiNpIKy zo(0NUSdRXuT>*s;5CfxPYApGk1WJ1YfD=AB>XN)??iWvpV|9g)>^p1zFh3LtGtzZm zQg z1I5!jjRwgfP8aF@mfqPnah8vE}%#M2BO7t9$kE)Ne|L+5XrYg zdk>Z{|aGGA`dczPz{2j3}yGmEkj=fb@r|Hxb zwGl>uK{IAi%?J1xC|LXE1NNN`7&K%YO3<~CD&fD=U%9!ya|ee+u7!s@HJC7(-;`;p z-Ku?HYIk=MVJlB9Of&%Ao|hL0@oxpqB3`~rOzIroChYhu<`Qr)4455fdK;uiU!^tlnA7-vEf=2DZn2msy z+ls?O13k^MXr4%W))#L<=sdpnU1*cUX>;f9^JDGL;1y^-D4$3vw&pvZHFr!k6ShOw z85B>>^mM0GuWeEE?feG7orul0Ltx@cZAdyQ{bUyN6eYLL$DZmj0HBxYJ{aH*zv^ z`*uWvp&XJ6`zs3TX1Z(7{|P`6v$Iq*`_FoL`2b^N?F8NTm+a4MFhY&u&KwPMA2RxD zwSUt;a#t}lF&TnSRayyxV%C3$(HE^=ik3U@gc5A_CT`Jc?!%xR3L8e=KVMORX6EU5 z)U%?~9$Qd7HiEQT(%hQQUU2%U!SChZ2Z2|*6!HUoy8Eg>|cotd2-gVEO9bn+dKg7C6NmNVz#EPYQTNFQk}zIhP41L3reyu~9X4VNZ0P zI1AlD2;qIY5F2<#{8PveOhm&sqiQ?*l0k^l3}DO~sCf!voL)Eq0k|+fA0>J5k(3oL zZfqL2%`leYo5rHoohI%&WYNOczIDI*G8<&qv)lp;Jx4mn{zs$LSexN9nknV}p+XFb zj064^cOqw#(SCQ99v>~F>g1_Z@ak2y^O42i!;}E>eh>itfnJJ!stDZmD|fJ?FZV;~ zRubYt6+T*zquRGr@L^moyIS`>dM8hBKg*POTM?bL-mGv--80+@0y#`o)~%S24zDmDLmy(wr_QTf!Q)8qa+ z-0Qz?!JNw4W|xy9LAO<-^MRROKKK+VieXN%m#+pnmjVkpR1|t=9(h1qofQw7Mp`lS zseUIZW=7(A>AUSBnrv7}{Qq76Cp#y|-Gnj~T<`$>k)JJSF7g)iE$STN8aQlfRZrV) z3*V;Gw_gg|UP(E;#J!K*xd|em9ASh4npz#jf%i{bSUw-4>tn!0N&ZvNah~5q1O@MP zU-coOnQqx{u>j}P#u+#Q5`kq2Z3gLmA7aj)KyQUjBD|zfN<$ht6!a+t^~Fy96jwDn zOTT*(UKcikB2o;?8;YN?=U zwI=sV-KLgilf5u_lhzHWJFBE2we8*cknHseMI#we8nQem4aKU9DiG07qBh^tFh$Sb zSMVy|-T%N#RgQy1=mb+^TuyEaMU#%(h0e)vivdL3V)xB0Xj?Gc1t>F?wlo(lML)sILzIEkRasyQ2N4qEKA85)^DK?$< zqnpGu4xsP~vZYiL?nH>w{F=Rj@3(9??dCBED)RfKDDdMCbzi9u{#=;`{CPTfa{jLA zAd|k?W~ja``Zi4#e44EQ1Mak;w5?H&R$l9TaL(EEliapcWee%eX3nm?%`Ur~anGIT z&9pur8vqs1ung963-jLZO0|3aLx^s&`P_cx`!g0_SKxJf>n~VhBpq-Rba2krA0Ho5 zLNxbTlaL*RB|IDyFe?h4t;A2c8JqgEaa}mh?-v_5MMp>dqs7Vj@r1q!Jo;JLqK}<8 zt*BfOB&3)O81qdOw&n12ScWU2+Kr_PoFdu7Hi~0L&jEC`yoh3J@6x!j^zVJ? z8UxFnI%_0CdJ{#8ke2b01;dx+Ap6ZYrwYvX!r`*A$5RX0`$g*xCNf>(7ZnK547Us& z=NpsQU8M8Bim6WY5;c#%4A(Cf`%_oDG@^$iU9EgE)zy_7Q>&4eHo%nF&Z`pvJ4}?2 zDmN_!1;@Xu4Z1U6sVE%AUv6EXvc*;y6LrXh%*NYkou{hyAaoF~WOkmfm2bqaH+SxN zuU-FE!5SutI58&Euup%IaMe5-{M%l_bF>##uno7OH!hwFa7JvpY^@8HAk1))Vq&wb zrn8W|oED0rq2{Or6g-4naLxdH!7Zjd^FNv}>JI{kwqA`Wh?X*{!b_qc8lFJP1rO`o~)acs7Di-yRA116n|4qPb)w6kIZHJZj)AYwb&9J%HBwt1&|4!qcd zF(**=pCEV2ie?}k1J_*jJ$sgZdNgH2e?f)1qx4~lu(<7Oe@0c^)-#83 zF@pnuX%!|-8ED4J>CEkA4@o!%x^X@5xe9XDl1!PQoS1S#0ImQ^HTmkM7*hWCI(WT? z0TuZCzW$P=Ql7Rm8#EM=gJp=C>8>_P1imh+24PMmN}DmhbE{B-Oa>+cC;ah502q)3 zOPi+i0+$sGceKM=erfuV$?!Y6kMllB-e4_;D`>YE!(B{&H-xrW>Bkb(tf5xar7%CG zkMXg>aUl(zM60ugA)ub;!~Fy!31B&)cMCz853At{6R|0G_-T|2e-hJdMmMr5Zl~kF zK8c~y#GV5)lma>?);Cb9C$Rh5z4m>1X?ZdAyblgWt=$!nmpv>v$gz`k2j!j5T&xVO z>SBmZ;BpvUErS?`fCCU6U95jr&8kxQ-k-L09m5uzSjnRKN>G>@bhQDyI}NeQ4hW5i zM%LqrZxIhvDT*@hv{`P~$ zy54Ez~(@*WXtD|a!D$L$XQO#+b zNwvSBr6@sCxc^?tW$a=x0UMHoclkby9=Xfy=rGC&bmzFJLnT)w3n8j*?+(@sUOp?9 zYdRmU^A7uMn_yWlOhB-K?y^fSkOK6T=$IXk_Zn6bKUj|LqMGu;u~`Cy=6R|~LW}sm zx!@U1F3>nDFCw+}`0{&4r9h@uLyt0dQ=~+TbJ&xz=Lrc5%KUqkjO^uk$77$;P_EAQjpidqR znEskSkJjgQr0DS$g>CuHZR(h#*L$86OU9Iyv2#M$qW=fQ>Gng+VN*IH3xMdfA&cME zMnIFSaS9NX#dzz8JZXVB;~j$w?DzLm00vvYz)=O zzDNi((2Vgv!>Cn=!0puG2ZKXV*XSlVyLFdrbaJ^9QkY=fopu@Nw^S@CVu_Uni)c4v zyN%HBA%V*_3{I`@`9XrsBZ5LR0H8{9YoPf6y860ceU$3DkVHhaoK=9Aw~=DEC5-a< z;H1`W)K;%T+0&%rejkGPrP=r2LWrk@i^c`IZU*fSlj$*6?YOts<|D|+wSzNj04I_R zoze4!t^QW5rf4?~civSS)5yAiL~QnV9O$KS%!2Q|VUXad13|kUeU3@{ubKfvD!y~K zkMSltT{E|)^qt!~(T;P#0Zm2qTC$sa_2;u14j5A9nYM}mUWVD|94h&kZ2kTNB&AFP zeqzp7am|LGMYF8@^`#&%st_gUNi0(;3{-p8_h>YV-B-}(J0 z+I85?%;9R%IfMCrqvb4+?bpc5n|;7`qEIEg%hNOhMjMWwf|E5g%XbkvNE(TrCWgm~+C>SQEhxArU9^TS%2D_m zTd9cM9=1*zfG|9WVH8_3>ftNrm-X{gNhk9QO4}2>CKmDB%(e3KIo)w$w?Spd?&P zK7!&br!{W~!!BroQSr~#fK_FIl=X}Z8J*+Jo^cUA`8**` z($}D2oEiKW zZ(X_VhwSe2IE2F}^IT~p80==dQdn3Bw&#RXBo#OC4_iOd=eoUg)Se^4_AAhG-aB|a zm8Ghq16ny8Jx58|=6d#Jv)z}sI+xr^p~)0{;5=6(vts`^`Cpsob{x-zCM(})d=&*d zA&j$_ZZNrHOUSrtnWAnb`Q^3MOwe@NGEra2FHH&^gmm5XXrK5hxXZBLd~aV=50Oj&AjK!#$; zV?a2zfc7hZ!Dv45pNT5lA{L#!UhsKi36nPAx`zeo+3$F8vK?-2&D!11;55;(v3E5Z zut`K9KM4cMB*yNLu>eATQkcw|x%~eY+e8k%+2>Y6ISAlC$HP zHV7+fMz%b5;yg5~#oR5SQke{*M@Fc+-X1ozo4;b}>gwGBj0w`Xv6Z4Nh!0XLIrCKf zt2+I~aZeui;hpIm%3(;Jt$cMqZ-a_wJY-mVJ?8ixp`(aB4Y&fA4CRUFB$0BFGT$vN zE{gONxF`72q3_!c`kLBCyf~vF0a!%v;* z{yXp2e8Q?~D7gvIv8f}J=#zO;Dj@!S^|!&Ql0wBw2~_1Be}PIL;A*ok?h(YkJf?BXjoON0$f`$5UTz8hl0Oz$Ey2vUasqAKu$ZB z3ZSD=!z3kld5Jt~+kn0JJ5g4-K1fk)5xeDp zqa#hyM6GLta#=EY=BiSjWg-G^*8!#q1djUz-gt4P`EpK+Jj3i@X~&$Ho$h-`q~@pl zlO_cV51PU}uXTY?cO1+ z1WT<%VdT%zMEz^l&d@=?KX@$09vI6M)en6)MMOjYGmEzaXkt&0Fi6Opjj0@NWMrW` z6#u&v;)9R`p@B`W6NGjMYsS^Rs4?&B1q7V}k`tYlo6+lFpMn{U6_D^#$S*q8t?o{&= zaD(T16?YO5w#S$wtV)ESi$uX?pYkm%q~Amecp5bGhB^gqX?676%qoXq^hj!^Ty z@9WvL%gx`~Nio?Ha9&x<)mSpNOJM!-#_hqm<>^ny14^DoQpIkp`R^7eF;-#-=|Q2}mF9X3%&`G^#e{5BwuLYU8cu4FC}I@<%gygnOl#{$k4mg#SgCts zD|_3-@KgXzBubp|5^~u+VE{n`yLHFgHtl5M@p>@F=!1``{mGO;yDopj{r>*GjL#J& z$*FkbWNh}&wPM80=;Pz5$KGhF4_)o!KK93YxU@q?3`V7^TrID#MyF?-LEm)SBbe{E zGC~bR2lJ9umTDf%R3Xb9XG`kXg&HU)&YUHYHW9OZVDppu``HTF#-`^>26@B%^gYL@I;!dx> zmal(}K7KcB=?s2z>eScn_U5dptV~MIjLyCwz(^p7YFfPN&PHA#kkawurBY4@sciP? zt0^`|fP;f`+T9hk+v>nf({GSKCb`^hl>YFwIrB6(H+SD)A}0qQhJ;-0aKm5U&`rw9 zS*k@vl_+O-%7P(-6Svnx0oVcAj@`s77OKg2L}}Hqep8fw8|spwJ!0aE5qbOvlOw+v zmBbp#5ZF3~moq0~s`(I#7@W#&us-7W)7~9j`C_xB7U;VAE?kA(POa<%9`)v@y5>UM z_HuIT$j{;-FHk%*X2CJ$$mH`0Bj|^(CvDL7W>Thu0fR76jg*>iITk3jiX3=I)@OjHOpMN!Kd@`%Sw>;e;x z_#Vw~15i9UAaa6_93|{7%~X(7B8+Q-rz%ZEEFA_W3L<3>g9;Tvh*^FcAOx{*xX(!V zRoA*4#u0%EEZl=CYAkal0!pYWDl+rU`~+?L(ex}eO&A$xXU{#CYn@s~NzYd=*Cq#6 zd3#$Ll`Qt_i_`W^L^*gdYY2v??elRIBXk!}Z_?LO!Ts7dU+`OVBMTu?Xt6+k6m>%f z5qs_i)C1+g8{ZWOZN@|#jcNaUHTbeO9~Vv{qi(H_kNTy^hn3FTjezT+4?i!chU9(6 zC^4n2N_?dKgzGCXbO=GGp>JS)o-FZudn@ky89vkJE|Ao0z7#rfyM6h8z$WdFj4O9U z`gRaPyX&@a&qeJx;l`Bf_noqO^_;#~Ae?EEvw7Qu6*m!ocF^QQb?qu>rekeElm5^p zK?SmKUmy#oKmwcTH(zwf^ttjYffSj7dUhiNY}`p#z~uFIOK?|`2;Dh5otq0@GX>^6 zE?`zZ=iB;WO`<2|*=7(8mj;KKl?HwKfYb2g5c~+hF-Fb2T-S~Od-e=gFa6F5pC%h; z$oS8#Wmo^s^n#pZyCBUk%=6_fXnyQ`d%1j?_UH+D6nX;Ld0ze)geLZ#{pVHUe)ltm zT&1#N)b-6=n(N$)pApIP&_$OepL-&eaqv5>TIWRs$&I(stg!IA=cXB(O6Df1Z(XA% zbHXn|t@SS(C*6P{ZEOYF&J^qpXWU(^gL}KtTt;fQ!C1;H;q-HpN5Sv@2#3|~E`rgT zWWW&u7kj2ybAU1<@QjBhz7-W}!W9$gY)K7@L}pn{O#3=q7{%-PT=e$b-CxgxiIa6Q zl%0Ha)S2+pdn2FYyQtezFe_xq@P{IFG~k!>MUrsE_7jgdOs2~a2);Uj`mPw^Us`HU zWM*k+r>cmL2Qx`L)CMJTVwq}2xx?py_(*;_N(tH7uU|yNB1P0io6OL(&PTJyzyKcV zO2rNmTrDwW{jVH3px{xpB`r{Tt&Z1!{h?mpnLd=A`8xdKQuX;v1|T*AKHj_&6sxQ3 zkNW8_u!q+}A_*BKni}FN3Lld}M*!nEKw)Yp!L&|A*ur{q#@P}r~W@MwgkwUSRN)X}Jt;E6Vw2p6L+ z+~?+SGvb6rOCbxbt*o|ZW{ty19Z?*-)0gyZPE?p2+XmxVCqSy;h9rKM2C~Rcc?Kzo z?Heb1>I+qZWYK4;%w0eRC_L0?8XgDV@}_4PcXJ$tJ3|FtN%*;U;X)+#vMH>>Fc`c2v(b1-rn3AF* z{{D7_J>>Y@2LDF-+I0TcamyKM$64@=+>(59b!I}xSx%~)i{fYdz zM-KAEPRj3F$FK4-wVa#D4s@G;XDIX1!L7vIHA>C6^{@N4We$k>6~mRn9v?^OH%EWv z`bKXfHdMS(L$kk>T{f$9D{g6q12#$;6%DkH%{B<@ZRpM&JCma!optl+>gwJx!$69| zgbz4%8sy+>Girto>^1i698Th-o%flw&J2dobDK$PWWBRDCkF|4S;Ao{U74k35rCx( z8tWqC*iY-SohK}KppNHrX}PDW?JvZbQ6hfELS0=Q!RkZYg<74(wqNHt_HLJ=z8$To z=s(_`O6~<&GDLlS*q#3FoJo9^FJ(GAS?)=#*NE2zACfS$MpLtIV4W0kOlbj6o zW|MKnVfHYlKv4DQ+S$8ULnu|e#?nr6Uv;_TWRBrANh3G>;`94ni#QmDyGq44Fb_B1 zS%4`w6S>I&EQMSoACbW`ScVff709Bx`p&uo^~kA0qff+sUH7mNJ^%J3*>Vu6lopYI zWJ*sY)=E?xg-7`UcLxo$qOzr{0Q9OFF3?T#{__42zGbqaC2Ret?r&5zG#x?gAKC6= zCC*zS5wEFRTGb`?k%oZ0Hg2iUTLj`>R90Iu?w==cwv0IUF-dmF}2Moc) zGSc=xBjT%Q=&Ui$wqO3WpS}8n&7>PT85;{4;0LZ)Db-}>PU^ngWa1GgpLfpF0KvWW zb%-+MFNq*N{ZVP87reMv^VzMoc|`RHm@?^0)D(M!}|AU>1|f}8rbsYjxUib z?cxHDmzBw%w?c(kO_d^)p{?l-0Oe#FGFsm9Mk5&9=hP{>J_~j!k+TCaylO`=^T8dG zYeU2lbEoY5#}{)GY;`Tf)m&tf>nG$P8?8Zsj~2D_^6^_1r4O`XR%{WQn&8AtX-et; zxJ|%G zQ5mJoKA7iEvEa@s+`n>V7@w0XqsGefZta4rxBs}z%`HiVg^4ZYt2`Q)KN>3v!4lq| zRXtz*H(du2$HsV`4NaQ0>#X<0y52|dpT=kW!(q{>3!9qMKQ`YR*t**OND3NzjS_nF z*$L@NaR~=N)V$57-2gWa5tIe*G2mii_(O8eS87Lz{Xd}nn05z2lK;n>-ma{uI$E7wZC<) zQViD2hp{0xbABr1LXqR-rW$!LXWEL6+;TTQfJ2>(zmy_8L4jFfs*~sVs@-h zTv~dLY13l26+D&IDh4^YD4TNAnD^1O(PGr`4ppVsibQYZkITL32@afbaZYEne0`~I zt+O9RV$ktLW$N|SNh2y;@7_RMF-q{*@xq^}){n}}#AD$30Bh5~^n7bxQ8Cbq`DnlP z&t?yJ$0dviO3gAINvO#S&rA(`&&^KzT9QAilWLB2ds#{KyYgZ)sLVe$p0XrvkvNn< z8I4C@uoC3T7y~OLu*(%c4)t?Z2BG8ECCj&c31G~?hB^XuyRCtJ%xnHf=5vKSX3`Y1 z_+%EG&0V{z06<}$Hp%K0VPs`%9@YG`4cm+|oeSk`ZQX>LV67C1Nbcg8Nc1%b z$-r_d$!9-9N#54)o>C{MeYQi%P%}DNe>Y88S2)8v z{Vp{XauhyFa-1sWWC^~%mQ$tZmfo-3X()&K%&#}v!v-14YR+?M<@Nz6B@6zAYzHva zv-AGo;k0qtI(Xntju-X&8qu;5m#dR-ZA}Ly484|b>eu8@2(my~TA5pOt9TM7MQI5M zJv%(q*^n9m`bfekv;VC;2RC;?cQqkYNa*)P_2N=13JF;w0Dz}$8b;ha_lnunfG(m8 zCI2PCoSbcSxR*3Wfm{U+{B+lPW}$4|LVfRGd;1t*4ERsa6GcM+W)gv-M66~V4C^kC z{jRz<{P)*D{WsnrRJDU=@Ah(nKS78pwyomj8>Lo%VZG2vv8KW~o&`h*@ZKze_iE#F zm!w|UHi|#%9?2j8riwUBq>eM&CAGU7v2v28&7A0*rmBJ)+dKg2PIKAc7jIKH7HL@; z*?=nA4_hf$3Mosyv_q0hog#}(Q(YPgcO4_EY$19xD{cG5lkPK}Lr^8dbj z6oV(0{0^(ai+UhcyiX4=HD?3VRznj_FQ-6jE>NDfr2FVoNwp6F3PWr>`Syr^;Dj z01y4|yWOSy(k8&KsNXf~Ei;~D$){8Co7!cR-I7O@dWGqFW0&ahgwHmHfH*rnX);$) zg0R9U;J66;INLC8zkN)MRz^#w1ZFzwB!Whp2Gn^J&?JJy8!j*IcVkD9YqH`qhHc7L zZ?L9a-k_UhLfj_3cuq2hbiCw%cS5ib_Wee`{VI@J%)M4LWWVzmOS67VY5GqgQL`s2#>k5%NtJ z6EplmU2jvI#`1?)*O@ZM;pYdW{O<$8nR!d%j#oPYiSJGGnrOS;3LKSx=Nf%5T33oYH@WDiV@KdkpSR;1r~!6637RFG2R z1BsYX7s$A7xV5*}ShOJfV(+2yjs71JZhlnNLRe1HIvdu>byNQKVa~H-wSXjr6_f9m zAvPK`b!cH#68L94zuM7JQB@};G z49(h`@iGy}p>@EK0*zpV==c?Kqg|M~y z);*8_4{G*-=d)~86^a|!$-m*;jX zNg^oQm=IDIALfqh0mT~?8L5<-UV%DCYnAf>t{_t~MxX>4;Vr`PBo5OI=L%Hp@OJh7 zSfz(mf|No}>j;N0TQPXx0P4blb4n4ddXAeT*JKqFPvObbLG0v-vi^tFSiAdIJPkY9 zc;XhVQ%(SKQ;?OUXNsngDkZ3_OG`k4b-A64vc%#V1Y!_&s>7&S4ENx7icJnP7KbcO z45Xj`o{R}?k2le=ZSU7o!Ef-X+`liIUJ`KwmtsT+7%%}H!zMUDj|`B0G_ib#ZnBcg(IlIlKha3c9$wubnAENuSLNV!Y^os zH%Yw$Q6PU!=ZBPg$8}QfLY?g*>`kK(|E&pfyOdnUey3{)hv|%@oF5s?^~jeGjHG-13&1T8yo#tu%U~z6y@1>wJuLWXK&xKDtO@J!-mcNkQMbh; z5)T+5kI|8YZyljR?yu|oyP=$Lz!N*eLKy~+Hgp$G9#PkzNfJ8>veV&a`-&`d@T9-xNITVdp8#_#XPM3D zt1?VR7JkJRxY-SPkTZ0?9riq4v)XeeaA4{IhO0MSuDi9LM(}xDkq%WfAl$azy?%eE zZM)v~VYQhr<^o?@T#Xd|IGXXiykAM+3v{uZ8L_SHcGvIk^Tc9~-L_qP^Fc@2#rs-fix?~d@*3PIS-j8CrU)y|Ge9rGO^ z%U2Aexea_iCFSBt_0?^7dDk@3eJlliLubc)hfHt69E|e82X~cwLO$=`C>~KzQRh~E z(q>cQI2JGJmu0+xra@Z{3qh>(g5z}LEM5?}ebXW);tM8|m2p6Fl}l97+WZXg}N>w)_^n#Hf@XFG~gpK#+@40z7 zp^1TowLX*O^8W9Ek(wLZuoZt88fo{3meKGBqhYL`LpNj$R(DKSkDs2Q739B^EKY%Q=-4M+)#<`xEC%eu>x?mg9M!DanlEsx2`h`q+6|#My$??w&NUWU+jK*=XFTo?}m(keAn( zjJ)7}{kO8}>V7Mus=f`!?kHgAa--tTv^cgFGfx25bT&`@MC|43+Tflo6Lvaqors!5 zPRauJM{P44mQgS`m%XN)YfHOXfe1^PSuR(s5-bA67D%52Mof6G}~7H_o5 zKq2FW;<8(=2Tdd5bCVWF?AmEOJR};^EdCtf8mt7}e#%HHJVpriUNVER^TsZ?cNE>} zq|z^0#z;)==x2Q*;2ALjyT^_$E^5UF1g%JUL8}FEMsTu!MNGlH$O=PEA5ZO$ku#?(4mODLj=oSv%7c;irR^xiJ*=&f0&4a9^iTeB!)iZTx8Udn!Ir*aqN*%u7Yt+048HHt~x9sC`< zjR^;8i?=OPP3(S}E!ZM&;qMM_1xL#9_Ap^0);oCl1#mLgkc^$o`6IJCK)9zt0-Ri6 zP1hed3ZE`d-~Qm*X6F}VfIQr2Zk< z6sHaQR+FbX6V(7sASeouxy=2(URzxJUbk@;nq>Y$1~_&~B8?=y_}-pGQ1*dXrRr;I znZ^Jx-bU`lEAif2)*C*JPLXP&dM5`x&GIgUT6C{9+D&{wj**c)P?D(^dbT~o$f7&{ z%5(0n(w!79f|SSucYmh=y#hkUr%pz9wmhSgN0va8{4bXMJ?Ok1$t@%kc(@rnT@!q? zt=_b)?FmLsV;U}IpBWlr-+0`0wZ`jm;Lc{E4oAk1USH?CSgi-g=XM-fP!^7ZR2B%b zt!byKn0X!~CK_n5-_T{+E^Nm!N|^yBj8U}k%W192inH9ug-WRg3+gmsfVf2v%|JrC zrx0MP`IB}X8vkf9*Qxm7RVs}-KWkM*J>i&jIVqAv~Sm}X36e)1yUVzxbYp*8jo=0h2L=!1pd%i zD&@uNIdg$3qdG~xW_?{c+X1ar?5!5>r$U4+5RYBXW7@V;K5&FFzY zP-Dp&T*b^($X8g>@-h+UMA^f_Hd-pvOkRUW>Gwy$2A4s=GQeqo z*HpH)K>mz1MOrs)Y9t`thhhnAu#Qhk?$qv5k_`gtf{|VgvF?evM+lmmy z=L2t`$5dHU`%y@Kf)w;UHzGPPRPdh3x90n-3Ij+?NP|IT#t~f6P*qO` zMNVif4X|H#-yXkh5PA%Ck?3*R4+MgOU*yr#RXSb!J(6)!LMjueHfY-9{wtt?%T|R@ zGxEFC*z`WPpH;_17XE{+Ab67PK2yJKbv&72e>HKvv(sbXeRfnv!#tV{bn+B>V4K{f z&$;8So3($uzo;5%c?>yMKD_Ar%D#S|oJ9=!!9P)7UpMgnawBy1`K#t+Q+~VcZldl* zBia~ZbKDOZ)xFx;>hO%wf;Kc>-fmV5%`)@DdaW5ET;zr)y)|6)l`@wV1^oMRsL2%6 z^v(`JH`2%Zr;CeS5HOk3Ay$Z+Nol?MQikkeejj}!1>6Cb6;HDE1&}2R=A`0|U7u^v zP%h?hmv!X)d)%$1hzPQ}iYg2ZuC<-!>T|WCzw>(pY?%F3U;=+0{9j9(P?Isl5)iap z8+jR#n7h&hKpM;OEX64oFZ>C-foSH6b1E=gbxoK;r92XX{EDT-7#=GqE9P*uClm{4 z53_HJDP_;grK8z(t{}~3_eRgonXAdh1L4CAB{cGW5wdXT#8HEfgVdjSdFK6i{;N>t z-BGmMIYzF z6!JQqt0WNiQIe58Qyp1&-BebN6is7GbgV#*!Xmlqj&`?dUA0)*{z6SotwPfhWZ!(6 z;zZw_U)Qv-)@=eWn35+^K##vAH9oYvg8y`=Q4Zb1r|P=im+YK zcb6M#*@>5Vk(7F0VBr8y&q4jP*PDD%91d%hN%;Bac}vwfp_dt}Tt6(prcAR=AVWxB zW{`zmVJ6Z7;AB%()V6whQ!x);w;!)0{M5+b|0Pg=+iG4ReB3QAjgU8Z@)b%C|F(H; zc1juv^ERr4xxiu4g6GNk8-o|{inWgaQrWuR{}El#)=Ed~4OgjlJFN}*@+81`t_$Y% z=pr2SX|d2h^szs#jJoX&@Ihc}a)^#O54ERvz%Ft@Z9wueTAociSs0u*Z<|pI06uGu zdx8TL@aU9hfBqpm%REBS6Ytv!7^aJmh{7F%fIdaSI7o-*24&Yq(QnHlr9OSyBnmt8|m(DkZ$QdbL0J- z_gg=7nCyV`r82#xh3!B|GoX zm|aJwmvnw7EXFey03+JwFtBs}_{u*qy=M?H)l{dyjkEDWgyO}+SLejPiWzw*>A1s? zgKjlNd~9_3ZQ#L)zuVEPqPnVX9ohh| zD8es3i>QRk*iB66dZ@xPZ(h6kPgY?u?`wM;3~A8__3ydpd|KlxV&(lmb;QN=+^|OU z_0^QLS%3%PKfI%1H^_QH7t!)~e^k5I@_@P}mFP#4dgrg{Tvg??e9t$K%12g(etvbU zP4@Fmk4ZB9cR^0#gt@~9O)jUkOOGp``^$=9UeujDT4d&-i@+}hks_+Ay_7%`eVr=o zga6prEei-%o!fWN`6hw#iPa)E=1+h`>rGyDATxd$n&Bi)kUIu~X)SMV8lzLa?e*SMDV0UK7Y+~n<$V9m8e@pG&s`fZ z=1Etvs6J`2D3zVJ3Ncq@*&l0;;1bQ#pUTOP$5RI}($S$UAo+ z{K$LL(=74@o(`}1#Omw9>R)>drS|&ZMC(o^l^U1o>5bn5JL5LP3?6~aO)fMOQ0jP2 z-|ermGP7OD0rMd%%5QOPCL&6E<)lH&7PxaS)wLjNGu|c?wrl0nBvBbq-<|c#{O)Pq ziDS)S7{YHIGlh&z9O-$6Qlr#*otf)<2eAHtTahjm8Rju)IS_3Rv2vSae5TCUSHyRB zGlj*hO(j1*N=rP;%{BQw$1^!oZGdOU!I+xJEO zx*Bg(>b6B66&;>Zqn$Z7%MwH*I^02L!Ex z^2fcKu(gkq4DZ|4TlcA;#R)N@6xS9zi5MUI%(Lpbn)l77RD3@Sd4o-~feDSi7MNoXY|Ug*7%;PUfo ztVgY~ZVX$L_Kqw8j_sH!o0im-dN$pC4UE@>QFr63@otz-&w*-+>|q&wHIkQ~G)jQ! z`jJe`cn|RaCP}Q^JI&Am8|H`y?!b=*+Q=4T>Z9#Q!)-&0P%A+lg0sa5XF zt5w+tRGsO97%3#_?!C7tHs?`y7+lSq-CA8XkQE7)C(OY2l}hWq;kz}o-iCNJIqY;^ zXx*&VU5$2Tj?m2-s1TI)DWmTtkFLU_HR-=SbKA?*Qru&#aaHFybTMEBK1G2Ze$jl? zmw(ICh2K+j6fU4vl%W{95$M>GQGmIa4Jj1OvJu^){S|8|WPScI497_ECuJV}HnnzJ z&F+RTDc+Bvd=lKNy@I$mnHKdZ}|lM@w0D2c9w#SL~diFU)i86#3Ppa z`SvnhUVkW13=Sj!xpubhnvK0|T90spK&Btvd1)#+-NjM{24*D-st7utJ7gmx?Vy=8 zy!fy7bUs80$@KAVM)2GWi2}4)*^5)-&ii>Wz^Ub3$MMq6F?$HfvDD2Hm*)~(Q$Buq z!+J^zcSL;q%j>gD2rCzZsEZ7Bw{rmzNdT{ok++Ot0s8sdj&4N3cD zg-c3-gNUXt`Glg&^xfWH#IOmoea=TTt?>$r3?=ytUXx*!!n)HhIYtt;-nnwvYwZ-4 zB}|htD~Bi^-cmu5h&KAUoo=G@o2J@)SA*DyPFk#n|LB;dtCKNo4toENb*pa)0+2w$ zY0x7+nr;CA1w3C50*nMGG$+l7b1Z^5_rkyMBaY*6O0UoyB2JYhtd2|zs;!~+Yt?T+ z<%ZN{7)%D|UVg`-Rar=w+%ZGSph&ZG0^~>eBm9r85ZqoyV#Kr`GP?%xA-$A>ffUIc z?661qV7}(OB{_>o7kZNAl7e?euorCX%<8b-iQ5^HcWM(#-yXBP8L03jpsK06c;OKv1VSYnWVDC}ybC31mdjF=X*>UX<+gOR^oY_Wwr7tvMv zC8(s*0$KJpdyH5^(7ZS{=NbUMR@O;ui zKt~dq*)T8S`);XovAtA#Jyt=!0&=ki$|;dD*Z&C1Yh$o$V%3xVMsqmVg)nmvGl<}n zmW=s7-kamSA%c-3x86#^?vZq~MG6FrJ%)qa#LP2OT7wc{qO+sYc?HfFaz7BkALm1D zDsN){yk$oAd=fp0qn60o@N3BZs~ik`3VuCVGsrRsRgT6|)S8qfv%jEAM_gq;aOJnL zCT?Ia5PNh}3F&Sq_)&VQ!+;a4@&!Vccp8Nx&|4O8Gzvxm^WlTY|3n=wya0nUfea3IoTXjXviWa8=YqNqkgka~J6Y~=E{3G~ zsy~`0$KNN(hkfrAvOAtiaKjQLpUqplArN|hP?^%xZ_1a*TzQLng|~=V(M-7@lwXzK z#CTEcLsz+D!SwZB`$$gIbYa1K&U z5S+;t)~s-cqmDE%+hHsy|2Q0k6s@3-D{1%`C)$T%$9*dt-%qg{+tZN*s~L=L*=e=AN7><~pUY!5X;a<;6z zR8{79duFBZq2W}T19C)KDvaELoEZ&t+Gcm({^@<5>?Om)QAM*5`-p6MsMGMyLO2IH z7o&D-0dcDbwRRE=CV;b*AX)f*smu#?8T(Yd(BAb-6 z;Yx~)0J@HVn7C2vW>%993|_FRL(gBgDru#4?ei) zwm_Z?M9Iv41g$nH%=2`c7YZ8Bm*@HfcNvg6}RzsW5H!Nlgt(J)0^(N$AV2rSEZ-X}T6s!o!N8(#qz_$*nziR@|vclkem^W_r$i84PV0n#JSMihc6v5IKy z1vmfAM6V?xjz~3wj%Z(9_;>yvzt&qf!!MLr(#k1}wVy{z=_o$P(>t>-bbpQ2i)6ub zB7pRCfYZss?Qh>$m2jY}^d5%Ag3?IW!1ts%RJD?HF-;?f3F|4d6H2E{q=G8bv(GUDtz~LnI*`>lP_{*`GXVwKav%tmqHNE-0{i zHzJ34*?(RTS1k{hA?fn>5e&Hb0dWVX$4Z$&-xChEwOu!;Jsx_`bwq4KsxwsHaw_zx zwgwAe=PuB<_hG)CZ+flso>7T#hJ0)UutqzzJAI{|7IyUI;P_#ah9&MYj}uaPM6v}L z)+;P?Px(4T=h=$@^bZc>p8e^Rli9k~CHA$IJ=(aFNdj`^H_()Ruac+i3~)nqnQIXt z!c0ebXKrdTATI0Ugvg8p`siKrN6$jvdG(%uZqF`=+}Vzd3@UiFl}Vc>#D<1J0-j>p z3fcEs7@&Ih)rvXB;%ym&X;!V`oz4ZL6iLdwWf`#UM8+Q<;4;c!)Y??#O5AZq<`5)@ zi5BOZag|xphw!*J^ErM!Vo5VBBgZ-axv3Mw5=9ysFY$|5^5=Tu6~hfYRX2_zs>QUn z>bGFr!cn!51rT^NXShjC9;SFnnNiDFXoN!Ae2o@#KPreMA{rqibQ$!g=V#70a>4U3 zVtV{+7jiU`h!@v+-&B>MD(}rlKUhs$aT7l;5wV_C@C*w{L8kX5`J?C(>TS0seNhO3 zRh(rv`ZTM3f%l_0jQdoYevn*@fY<2HJe@knTCYLqBc}3mx#0njXs4{#rnI ztCnB#4HlBrlK;Y8d(V&%OVFRnYy?5p$F8p^EYc2~+X^Nb?!KI7+vx%dgH>Kd4-Wgy zGAxTS^n;o)EX{lqmpU`i8{Mf`e%r6)b)P6)m}?WCh=!l$siU!pyPYY2omt@QC)jqd zRYV8-eD)#`_n9in*`7y?n=Ldt(@rE|O8Dvhn} zGL;5fgXn{kam;*$PF9xAUa1A4L9CnOrkGP zZLAO%0Qk4=&_o6&W>RPyf$&~zrznp--H~i+vlw=g_fB^d;mSK_vEkz<+=j|UuY^~P z*Be?veIL6;p8wt7@^dWWH$HDUh);0tq`z9e!HsCXFJx80QA-^iaSrxGx~351b=mxt zRZn3;m`2(FM-Qe58{3VU%IKM}0;_KeN<>y`;S4M>_BP9#%ipDkHO>d>%Zs2-oE)HQ zUrK*vASXJ;&KLD0CHzp$bhf<$LyX#s*@PF42Ni38_*pz-!^IOAmQ|-@XmJpH_w*Nb;q(}uXsGM$i1|4-F zeJx_~UO26{_>7X;z~(I=xL@561A5Amp`8rR!k5pOyf+(wC*Y#g<+nufJ0BD-vB&hg zkg4&BDxWxsIG``8iFBgmnRLEyOUU_fS@&xZzK1Dky%>ah27_gRTjLkX0o9^pTm-Uz z{)&30i;V)d1GdC8k1h>`8noHY34+JcGjiV94!|@P8 zgRRee9@B`#M6@wUWk{HQk3{?hFeG1NGd`if%6yoF3)BhYoIW6oI-kD`7#WgHKhj36 zut(f{q2bTQR-caO!&szGiqsH0%%4rX_4cg}MsoL{7*GWS$o$d2CF<#eCF&uw`AU|0j&6hv)|^n9^2uAd2V#mpNNI@4sYccolk)sA$y>r?8~Gioe-KdR$v#C$Zch@Ov1 z>!!AN1MpEyHvuR9CXKWHMjUW1j9yygYXoKG!r43fXoyb!@$xo5K~8yHOe7go@3bN; zdY>C?xPQe$e@A=PQ~=FmjL2b84pWy$c(3o}u7=#TcN@;+$UBjt2FG?smK15}Yj;L$ z2|}AUqPPdkq3C;46gVg%uIlY9Hml$9+S039^vleq=?g!f!2kG(nbw>Ry=Z4AFDK_| zHx!S+A;>?>2tQS#?!VaJBp^Q*YhA@#pshvP6+)6IM>Napg!`%1x2VrfC8M~j=FAF< z^_Ie$*I1|pzWZ490%=>s412B$EutN^HyL^>?qjVLL^3^5a$(TtOpdWU$$=lhnRSIV zBE4`qW@^q*Bt)*iGR!6=JhiePV_>FX&v!=B5A{|;jC1YFFE{SU{YPq|$yN_iAIEDU zIYen6y_q0u1a?R?!Fb~b?p5!iQ)YcVf|pL(Adz|`7P?S3sE-vMAHHP!IF!s~v+UOL zyL_WxPD?o?%w*b;Z#?;}rI=tmh9g)TkEMNa?{x{GT*Q7>cL_Sd=Jg z&uqSyTod^u0jVH(N>_G;rJ`J1#f+AE&fsC>4zj48q3BL z1Q!HDL**AQ=rsbqmoRkPlDT>|G<)D_eSLtp-*aT26^nzZs;WZTrqJMYqQAM=K~;#O z3stX>q>yA3Q9j#|kIO5s_w;ao*eGgw!HWc<{N4y7GFnt zv0T9L?(22Izzpy$g$a8p8qxtOA@eI*Ef=X{qdq*l+hM8afbI3yi)=8sdv*$G6o2?&e@^k`myjnNM$HJ(Pu6OWmbH%gp1KL!;j617Ky7H0 z7Hb>+s!KGof0Ju6hRp<}gx}rr)sLVPq-z9(gud&aX}o^B;5c9OnvPcB@^CSnPltSn zd);1&>vACuCPn$!b~0b-VtBnP#DpzTA>#MZ#bq~HxAx-&TSJY*_x~%6+d918CGnD1WFHZLel_+;ItcO4;Q1BoxiQ+q z+L~gvvOrOHmAHb0gor^4kdsH_>kx#pO%W9pwHBHEF`a|OwbnlGUY=Hq!LrGM*WK;$ z-qGa3xbbu<@vF-yCECDID6NsK#U)DWu+d|k>nxMLfkAp>Bh1gGTfW=o@$S&}rppC? z0_GgCb5+zA@A!3Bm#lsE4X12BOiZ_v9d{36Fqdn;C{E_g<&c;rK?EHDm;D!rYI-RW zlV&{v6xGnzWI`%4gr89Pgg!`c4!KRTK7XXVMu-agOXV3#M}*`MQre|K%)lZ_Me)mC z%%EKK%uqmANa~1T%*IH1`|+>`cc9lbY*;5;q}Yw_k>qB7CIke`?WQFDG*S5V`ylWEU-!J|jQHeEFFLE~ zB!}o^x1b~~w-&!ZrKs!bWSx@N%1R#lm25&#y`S^lN^!&SY3luc?$Ni(;R(%%P1O4n0iYxk=q4g^ z%*r}TmK+f=W{F*Gu^bZ5Y&5rFTGDc%)-b#|ts7lj^))ggEjQ3E&=*kpAAEa^sQm}I zid)zas<`uPxpIWoLI%LAhkw_FfFI9i**@@#j=#~BJ!amKOA;<;+Np+dRCGt+~mIAKJ@w{>6H|ZSoF9P7C#HdPZOt$ z;(Hdh$i*D7rR|D+dvWAHeb5zGX3;-oIai6SqV2|t5fi2JP*k%9O3oiz>taUR*1RDj z&b!h-1@<9a(L-*3FPD!M&sFd#YXGOkI8D>Fu@v4ctza3rOq##0Q4 z(}q`A;j3;OFe#?{EV3Cxa(T&x_>tP1kL%J)pEQhV&d69`@_n35}e*=Jz15 z5iUDhAjSaaVBSBiSV%$fZf%RNJDAO_VUWqelHxQzy zQc44G)BX>d8-h%UcxE6qbDuxY@qBao8EAYO6_9>#-1U3<kQ{Wn+yG*EGXS2P44Y@{?wQwb$SuEDD;7R@?Y0BiwdHvW@6B0c))(OsP)A7=TQ%pC);ac_3dvmy3VsUW_4G&rBOn0WsmltN0Zw?6agX3U1jL0jS^@{4J7TyCbNdeKB)O%<1SVb}~z7>Nog^l4y8xr@EwKw~8 zd0uk3K?kXU3{EPmBj;M98ht}IM~HT!wx^(RA~fz$!0@ATfobY5l$;M}K7*ci0&euPUMy9l zK8-IZ&*81q%w@0hvXZnNYO>P}#6LKBv^FSzL}(sjW^DBC$~$*sLHyCAv3~kBFCOg2 z`2|8*GPQ37gUpz#DAg= z9tl`Gjh!Dp7-w$&e!ps)s|s7^12M$9fDIE>m|_Us?#uW)zU*1Wmp}k=fNlS2VNm;s z3K_A$EiQhoLkfW%ctGmWUUFwMH`Xh&QU-!RME$cSedfELhyGfP1QBCNaDmE?7(G8U zhd$Ka;KpUouhg%Q0$0(37~qoewHnQg*Q%PsHwM4H2x}oAoXdzgqV&6fF1Vhs<7Cm) zwck5Y5fEJ}+n@q2b6|x*`arXzrj4xsYUh~)NgDzj?*ma#1WA~pfASrEIMr+@5n{98 z@p?DZ%hqIw+lX!m_yZ$^qOJD|+pZ3S1TIkZpK>JFE6xf&y;G~e6bwK3nUHG!wyow6 z)M1QggVsA`uB`(sR1bsP(=SsNY4795@0cH%n4J3)sn8A8d6dvM@j}dIQS#ZyWubu1 zJtcd$#_vT)Thvc-*G)tbe+9%2*ee?fp^qC-LAAJk!+o&1#W@pq zvKt2jqGtD}T#ZrnIZh^kzxV;1380Y={L2j}kch_I$7Uwsv=`2+c-A4@WODKNkj+7u zAEzPm#=+JP>P6Aq&+^@z06gY?BABabvH99~$YKNtNz_jT>jK|_K*=1Yyvy+}VPoH;UQ>8W zFR+58Quwl?29|0O{mm|nG0LtZWLAjigX>@b7T#60V`Wi7@HH7+4yfgRrw&XS@RHWT zEHM$})aT;i!^k0h&dCG)9l#&Mae^d@la0{gWfW`KUH@CMVQcnaE4USy08iNnwirsk zlWg2dzld>2K@$^<+nYQaB3eu&`T@FtY8;vMwiiTXe;?>X(935;f)h}_F?;?^7L#@l z>Y1RZ8&3zjj(Gn!LV1*3&I_<-%T6BGmqKoSdmUo+EU-HX%bLF{{QWj{-?Q`$hWIK$ zin8FPhyude?-RGWGRVq%inc7hl5lz`T#aL4PfSdC-PS8HEBBX1kgpV(5oXlk-E$c0 zYylERf{3@@8$f=x1H_Lc(wji>QeqG<#NauG$iJqnO_`cK+K`g$)T~@V0?|C?gpQ&X z!VvKsY`e=Z8zIOtVhae(G}U2J?@Td*#qYP4u0`o89!|q6D!Wk^7CGR78M0BarlA0# zsGk7oy^hcE)D_`Mq(yN_24it8a~6~B87{Q~vpvCz++f6}7GclI+NI?tJn=1$zSi&& zO?r6HfA+Jk!9JOrlCNh{1ngy!b~zt#|2mkOkMZMB7yJq3XCI*z;E%rE1NaAWopiE5 zw{sIO$l(Hdu75^dLDsnZ4MTERcXg~X90|JW$e7CQQlzK1Zh;R%7JBsn zZ$Op_7!{;z?~6zm2P-&>s5BvtH_edA66PHziVfQTRQsr8x34h#g8lSj_ou^v6=XWPYxYlN7v=l;lhFd6b*f-x%V^dTNCI zC3%}U*psRBGGfN@J7hQzMU8wI9jo0H%(iS^@+TP7gAgNgL8JH29F*iM(}8xsonNLL z%L~PwN@Oy5mg4pRf3}}8e|Q2~T8~k;YAe=kIM|YOJdz~9)3nDX^|(XM%Fdx0fI=Ow zWY(j`#&GWYzt;5b1!s>4}DV_3xNp*5kfaUb!JE?rvU#jr(?3SG)hW-)5y1n)`Nu`75yX_P6d% zQ)NLR1L_WPE+u%kiU_O;=_{dE8X=j(*8e5|gv0C$&VSgjOH8-@2l8~lwys=uI7QIq!;08-L!;$(?tzU0A0Cwybaw|`dpKwI z9^$VYW|sCERBtZ#AZ6EqVq&hCYf=$ae&L5k8Wfz!mRX(iV%x=aegFKGKtxu@vhly^ z`uB}sQHrA9ULYY$7CS+b%PTLXP08};S|7mQ`r+%b)Jhg)Zz7-mV5Ng z!G_J_Q*>BFOkX1tLqh}LI$jRuCM~&->)bNiwGP93_u2DzlStS`d;gn$rrq7$70j#k zFE8QIz4|!$q@X|lj88m75GwtDJMjwvWU;r>DA&9Y0!;ZR}evPH|YcF5SIr=BPoN_cYiRio0db;`ZV(XYS)0H5?U|4EgDkHlD4Qy?Gr6 zfB%zc)8m{H(|s<7;fI@AU{ef`B|p7}*pGjo4zF#hTLymrF0NK z_8;$f#LQBgWjf3n#8$a%F4Wn*cbKaKzRDs9yBu%pg|1n7+!7^Wh+QF5-fXalbj!N` zSrwC0P#E9jRg#yJR}I{=s(pWh$>k;J7s#oNG})YXjwk*;=zqekx3r-Wy2Zr}{eqQd zr}=WZB7AdmgA775l2Ug$M2Mfp$42SiqSE&EzNZUnUp2qJuG>m!Pqb-DMT_&TyCq(%!r>%n6zHY)s#1e5ffumt_z3~;W$H_&a)oQ56ox;a7W?GKk zhw-gb5m$@X`22c$Lj6qk`U|x-*s1cWN{dX)XNu3KIwq6_)H%8cz)rxYizXpDOMU!} zgvw4@GKks&sd= ztMGC-@cprLp}XU@wzea5Twr%6X7#{8rL*nDQLNEpqsOB=3kwS+ZSVZ1LltY)>o;?V zHSNeHY&*5n18*|lX2fATGsR&ss?q%{IFgTCK9SEONJZ3!9d>{Ei#VG%pfTRx>u-7Pt35@jKk+Cz1RjWyL~>kLhaD8 zv1e4&8#UiFZEW6OkZ}j(DddndGc!{l%<`(RBs@~Gv2p!VFd$ST*1^%n4vD;CQ!@bu z>tLN;^gJC(M3=DS8XVcNT_uy{4P53vh8l`@#Bg_5cC!|fuf7O#!lM(+9QH>Y;v*|o zrZ&Dk+xcntOQzHRW9*mp9O<56KH}}lpPVuK|#-v@EoMEpYS7@ zuO&|u3j;D94mKtlPWLxtxR1<@wq(kZX7+3l9Xq{IUVr9y#-@daNzXnU8hB)llz~x| z)2X;_bSqMIG;%UBjBLE{b(i|cbJaA8Ff&t@+M zpmi04F9CLN$>*&U$(WN?Vxd!M0npD72Qd z&M`Y}Ty~EP(lkGsZFiC5w{Ut18t`q6NWh4P(R@bk=u@PhNggLCz1h^ibR6#|b8{fE zM7>nE-Vvb5^!Y44r-u_7@_1QSId%7&S=y;BIPk8<%u39F2n!CaJ?(ZI9~u8~1js!B z9EU+N1(gm5Lz>CptLNADAQ^H=31oMN9!Ui)dEr2c7Bru}&yZXB&26T)f@Z2!vdntY ziLk+6)(8b?u(E~Jvv)$6Z(i@Czv$F~{22<%F2Wny!8jtHL`0Vw!4+o*20+DPYT)R+1G*N@Q~XARFlIVLgwS3iqD%|TR_A^`HSL%PH=94rQHooz48?s(W>(3dmsDFJL zF(G7md|Jln z@<&k`pxTc;JPjKw)0|=B9qo|8J_=-|yJ@t@B||i^?Z@h0K%U0>`DP>O?|3j|*ft@M z{^Qv@nl6-mi_j04-;%eb$K@>%^oxZo8hkjed|7NKn-L7PMm z{s46azvObHDII71tJ-q$&3v-k=fDPiajIvd({`J0c<8G7++-j2w4JX;#q@i^GV>`+ zYS{}jLpUWb7d=`CjSQ9N>r&kxRM|kR$Z$9zTRQOrEf&)20@WB9;7VJxzCX7)cXSr1 zKY07Yz8At;Si?yFIIPt@GOoUBA4P_SWPFe`O)OoHs+Y5EerYo&pH(Pu(yED2%4g*&1?Q9TwlIxCZ)aAD~7kg7p z{JkhSk?M)azJ$-0UrefKpI_|fBwFT=Vbr)>9@ZaJt5?x6$dXB?()Sx?L8xhh6_6j! z!s_8bHiQLT?@t%u6Uua8f1yJb#2tEkp@C20%59YGms*o)^rrh`QwVosi(0j*(gyE1 z%S4Hl_R)>YQFDRA?B3ei!9T7!M=mn+dM*4;Oq)TgWNS%##lrRGeX_mc)+?I|KOiEY zp!Q|`OpFiaSe-#djY^w#QC8y-{=oz`mDFap$?|Wfi-*0_%dVN;cdiFL{R-`8ex~#F zMK@SLwPl>d_FJXljN~M5zUFu3F=i`L*sZ@oS_F384c~afY z&DzI&?)LEe+nrsr;!%kfp1e8chESXP(?FX1g_IS{?V0tEN%ZOEv}U*4sAWPACCk%m*S_A_Tx)jM))umN zVmF2+BH@Q|nOvWqBxS9r8mw?z5nOfAj~qUC1vDJUQtB|&uCC1*g(^L|-%n&1d?a8x z>x|XjUVd~})*eX8uGhqY)`Sf#cZH@BCj2tW1M(qB<^Vu^=7`eBfT;#rWr#==Eqz)e&W+`v7 zf10VrxT^87;L`f2^>PJ64W0Vcu@OY0b+GZCz{r>H|2VjCq$7Y#UT(qC&9H}kZztYc zR#u15kQH#k!Kb|#y8cI=yQ_6p`yM$~<1h{P`4QcA`yGSWy}q~fmxBET#r2ZRMni>v zxwk_2h8w7;{rapx(Tmmk?cYD`4~|MkRX`EhJc&sAe!7~TqqjR<3f1Bp#dCf2-wbBm zsuga3#B3!o%lIH-7TYEcvs78!IxgA`H_0c5y7khMG;ms)6u-RI%SwjGmeLZyckshig3J&BED)y9K+O_M^wNs3?S`7j0wHwDk1FHlgj| z0Y8598M+~uE##Ej7=?}x+2j&Y7xxcM31RZ$JYU9&(Jb@N(H;9Irh{t{0=(5{_LMv8 zJ4^kDucgSI_>aHIcIoN_1@Fb-Nw(YVg?wtxlG{zLe5UBqIk5ai?`tm(=JHgPW0XOy z_)?`Y!4Fu+1=5KTAFB5%DwIA8&D2@!DHo|$blt9t1JI&aVz6%?J`KYry4#Z+abiZ$ z5*qiUU?6hXd-|a}|FhNiA&5?bI^OFa9#)}xc9Jrkyg^Wuk?{o=_l0wRP*hch!{c)H zN0p-+@9(Hp?)NvQ!_~U?&UBh(f{P6f3To|VoR1;($pMs(z4dQ_u0?c>1+VjT`vG(q z3$a*?LY)JhQrS}zW-cysibd)*LoQQ74Wuv5D~6L;$IbHQ3yfr`6-|H6=Sveg?Ahiv z-~Ihqq)}Zsw{AF?a^Ob7x&N8fVy<$iCH1g$lKw#S50jhyO8_YNU4g#MSP@NfG*K#C z$A^2;q@{QttY-67wA@tznN5U{eA|$P#N~O|t~nN6mfiZ^zMJU8)5`6N5AHkCZvK+3 z<$3}?n8>FM|DD>*w7$sLXfUDZ_OL+~61aDor;gRg=#Vt~R&6@^=~obk(xLHTx)|xG zY=Lt0^5uie(Oxq^X?2#H| z3TE``5^e2I>1uLIbLEmTcM2yeSxR#4PclT*D)j^LCeS%|UoX0z4l1QMkN*B%|F>^B z&t_eU#C}9T@}pU<#e9v)syDn^m0@trApBPFrQl$nL0*oD@&2sQUOOi5NYvv!*ej}* z$Ln@uI)8DH!0T53!E&iE(W+ho76IeOPquLzVv%?Mw35dYcOWixVyjng^qd(wGJXE1 z@J%UI*n5FCjV!<+1^FLrLW{2BY>?qvG=e;Vo{ zj7V6^<>1I+BU&ZDv^3W>5G!@L#8-zuFkRT{#pcjlrDYMalk|v1&0@})^^8lBE`D|> z>J+}U@}^&M&4O*ggZ*Nv;%CJ_MVbVX;>Yx?OIB7KGOuvcW?m&gksr{ zN@_vm3;~5utxI-Bvh}flva)!bG1x}z8C(kX9f(-OL*|_r{=C>BofL4JHv&L|hx4CU zzY-lmhCBy_g}6YucxXZR^qMqYowEOf#ZV3y0ubxq-{r~aKUKw#gVJ7|^H{1}z_ua5 zWUn#&!ZezoWe+kX|E>;BNYAc2W0oG>t9VswwL&&>?F#I@|DiaklOOh5C6l|sT#c`^ zgj}i>=Le{ksKApQbdbkF)?b^?YW3_0zms{A;N+V)IO;bm^HmAW*2!${JXr>uPaRq# zG=m6EADMoeM)bwU+dG7R6OW~^pZwFFpdMlq3_&2bHM0!OT9nl`^Y!Qx0_h+WtJaG> ziZqFU;C=Gu@F^&H>w+(IN*J63@187xFF)0@h{`^C%Q(U9UA}h&#>77+Uh64-{Udr< zr}Q6kr?VU}QN>+X`e(U0L5QKT?MwCrA8-Dbr05Q}Waq3sE*R=vh zok22U?jZjMT%P#r!J5gE{l)(*5_U0&o|W&^8MXhM`DAEdxe#}US*XAzztVd`y&RA! zqAoJ~Ur%FS{u-N^l=p180dFH8Itj-M@Lp%vQYutPuwwe}6=rYncIp=cq{h2l0PlF# zZf+gGo5tdx0H-WgejKI<*m^@n z6&VOTh9Wnl_Pg7iE-O>ZqKU=XdG+`IbT8y*s1E27aVM%L7RlxNZyZlQ@FxrXM{tm@ zX+!A_bXecN^pEBzxpLV4VUw=vm5MO2-h8?j@_~W#MBzQ(dOKvRzNvftVP-w|TQ!X# zU7PraCI9hZt`!oe*XwKj9!6<+m>j9`)yjXr_6;Zgb-3o^o9rrrU%EQ?VEd2iy9go3 z(sU+3&xPowDV0$2-)fb0GfMujmu{hLPkXYlp3DHhmmD&^P@`5i>;{k?dB%adnHCM^ z`=&ieWqN}2pBvLOv(@m z2Gv16U)`Aem*}6(_W+^PQiCYOWa@uv2&&^niMUFTCK!s`lMu{p917HB{2x^Sehttp zSLc1Ay<9{v0mkh4NStn8lAp+ie?O1K@jU*n7XqbEhKF4=_-bKE+F4A4wiYPD6gaWw zNX82vj9u753rue7`{)8#B{3&S-VsK1{$qfk`QV=9QIoHFw*Sa~;pKLa9bb`J_ExHg z7Cb!s?)a}z(GX&4?QAGpO~>k`ulEiRH-|Kj#ezV%34ZyzkJHQ6K7ws&PA)d=8Q`RF zu?oZCnBk*K1chkT@PJF_OtH|ApLL;@X=Y;Yj$DPGqrgJu#G0SofEgV80tSush{U-b zM%SANqNU`%6kzPBb;FulDPChEDNez+_dk);kw?Py*M9UnUcPoqGa#l)>iVMHZrpnm1k(q{oym$=1Po&Q>81M056`W@ueBk5mNu> z|M)K3XtVqGExn4CRnXd;G9Fva_OTX{3_S`%f=mp#9Ur z%OmYn7)|#VT+XZ6vG#L2zjor5??gLAI$d1DzFl7fgq`zsS}c^X=*3{UW1WSK&F91q zTc&_8e*E}}TUL1U@Zt3Ue(jwd zI4$?(R#xc}E}2jKw!ZRgkvLVh7?oxi`1EC@q_}AO{8yy&YuX-PV@MbH_x}l#b94bC zv^X}8<{YW`j1f|VNW6z-5lIOXxrd`zEoTXa=DYGCu1k$zVh-C9%z^TERv&VSI7r>j z_^D~AmF8c?g{4(=hfIp#X=M#1W1d*rsZY5xxz*>$B&xhaC0bv4eHhed5Zh+_A1 zmbI$xivh1F7y168wD)PXNQ<^AirUIy_vf54L;6IXGw}GWz|z&-@LTwyL(4(>AGm)i zOdBp4F^iwXECx1cvS4O)11^sB6<1$ZA*s-kSvyJEhbWXD!opL_}a|JeEps3^NOTKxH5T(13mX?-=p;Q!Uq@}w{T85U+p@yCTqz4A1bKsuA@BjaG*S)hApfb*! zIq!MTv!A{9^PHzbn!wa&b&R%a=3fYAuV0rVWFO9wiF%BClgDo6r@qJ*P529xHyUhW zAbI-HN#BJhrX;xbjD1zF-n|U8St|j++x7IjK7!)bb+ZHYlSpnL|*%s9WFS4_e^7>Tn%|d6DBIg$jG>Z^N+?ohPgxQj+NB)27FQVxPh+O2D*|DPz(e>8yazTj?Og{!AAI0ho7 zhDOQKG9NjW#5U~;P-_-NdmD#(9O$GUxskQQ%``0=XcZ77Z~W+8lk#D70;3e55q8F4Xn6#UMOe@H~n z>k%p6YjPFg9xJJ()+G&^!caG@k&e)PaC4%HFXV^5QuuDzIxSn~u6=%VV5j-6D3G53 z?c;S5U`8-|SB%3oYNf~NiRu0eaJF1AeYSnm`N>u^qfVo7 zts~!xm8U_`*38q*7zk zm{E$qDKQwphrj>8N4Lcfe)0G50qRc-;2O)c%7mjSiT|0%)Wv^(^&{72dOit$ZP^D` z1ppwOpG|JFDZ3TO_&V7fOVm%hV(xl3xnpUe%eUodV;yw4>%uq;hFO-C_w&tk&6e`B zhnj8Wi}zhSgqvpRn?%zV7foqt{SVgWA3Fb2^&TgXnQvn-gks$DsjF2ATW^F{(&e~b zhP=NpI69w6<+0slI+j;^4XqjsKo(!VG|uQUt8Pr4N7S92T@M;m`rGGuAbB?9v4z6! zrGWX3)P`o*Hd_Q3QDZ;FMskcOCacr}rs?1SDo2FErU&}Q)-00p-=_UmBCOS|u>^NC zBcEHvli%<*&mxocy%A>eb;6Q?6dtwUC+v~4h8QKvShGTt&M?8VKdtL$b!+77LHh&} zP_jh6OU{#2xqUvsk+%qNCMw3Nk*aEHF-bEob4r(CP67CvA&4_TWlogodkDF{181e( zq5B*?^xQU;ltPSN`L5eT@3!)#pW8V?(%b=9Om^k1J(hTao}db=Y^Pndt2NNtHZq;5 z&tJuda`xEkHaZ8|{XB=Tg5wo=Fxq|k=54Gp`^WO3q%Iksz~+Gd^5w?KVbeu1eF{W2 zN2^??N+ID?X8|T4<$byz9vmMYrmd2-J}W_q3uv(>o4)StJH3&SQSfSorvskCkwmh} znIX=$fVU5QI%X|r$0kPqo>E(}{q2Olc-(*}c0PZ&{fhYEy3wT;9NX`L`aV)moMcAY zx&2GYjWx9=f!8``cR*BWeax)aLNht@9g)Atu`|@%mrJUX+_UJ_?b`$}3n65Ja*3Lv zfVS;)W$_MVbK_Gh{?=CLE(#h+tq)S~ar;YVjP%Wy8^?~Mx}lsT+lr%V@$sRRKHk+f z%o)Fa8-MxKVu%sP@)kds2#6E?*TS*{Y%fO_H}j)Rw+3_`E4i&AEY`c+?}FwTA9GKw z+Z?I)*sWg&q~Smzd1)Ubt6sy-POnQ_gjC1A?sD}e>*Wm|K+s4Md;cY7&WDthS-q0g zGnJIfTmA#XPl9%{NxFa8t6q&1?PIw<%(8U@pnk028MQ!}ZRoYQBSr0SQKG>*%)@}a z1zrL~f8A;UWTO)6I)14n*zp~;&%{<>;hZI$X~_;zt)&E{l+}f`k%lT>C82$1Kph*$ zO%oClGI?Sk#oBTD$N^sD&v^&!>NU0@nLZFefav38M0qDdR;|GM@ntH}%>H{f{^VqR zQgC5!qzatgX~t_;1Vl-zu5T@Z$BR*!KB)dgzBMdqEYdodCg%4TSN($PtBT-2w4 z359v(=6u$1fN5W+u0(D*637+j69Ua{n7x-2XqQ9nlDeZkodzANO;arO&T`i|O z`jTgu(A8{HHR3MK9deyz$<64>DP5_?ZV@^u$y4I*N;{Hou@@Z*hrgp zKKZ>u=B`gt=?7ofdGuhNeG?NGE56Xia|MrGRu_@ViiZy$E^>KKMa1RSAR(`75Rk+a zp95`|tuX~4ydT@8j_VsQ=F2E~jN{xR$uig0hF9OdehoTzw(ByP9fwLgAgjkPu&@j- zDoSwwPZXy z&VbH|uR?IVM8D7t>1$Z5!#!>vCXmx%`|y((9x-Ru6dDBJL6dN$QEiK)9Cq`8;Ze(x z(fuE-R;*SS_mkY9&xIvhYr8m=uKO^--vKmM9(#=zF;z5u0*QR3QR*Eets*4z^!^i-A!}`fb&4R+|hrf$GT7!pljLuID zON6B%EfEDV{y_3-%gV~bN7$}UQ3?p4(4EQN)hVfGNtz`m+7%Ws)r)hCTP-?DNo3v| z?f6cM%w<%?^USsKUF;I3tP4-j0+v$TpXdZed+g)v=?|Qt1OxW?QKR85rtx_Q)@!6)UV`AVospK-0bK^~P zis}n}@@BjRy2Q6G_w08QwH^g-*vIpP~kyBlPRE5BI%9QoVT8OX}9~ z6}-+Iwoc|Tr_2pTB(qrd$YtVmws^?ekZpXtm$$pjrk45cp4Ipp8qb=cK#Xo@yKbFn zw=u%KmlZWqm2#=1b3z`@S_5km69b!SuX%rT{3P~M!#nyD z8}<+A<-eyfm)4i}Y`}j1Hr7AwTz6Jp9}{$(ds$_JoT>9(=m>>^IDQy}^72V7ssAl& zPiG-13H33k7@J<6aah<}e56GB!|Bz(T;D-(sDJ=;eLyG%&eB^*XFr@(CO?wvvD-La zG-+tPv)FCGb9@bdrm6S2^V)Fs$xdNuz34EuLa`2k*5+y&Za75df+C@OkVy3O^^Kz7 zh5Miy=-tWQ+KJD3IBK#u(Z_RGhGTjPzy?YrfkaYX_~7%bw|LeOF#U)^oC`j~bw@X8 zpVaXJxc8U5+@VtSMUWz%EePnj4R*Dbd95q#ApBv@l zshYj3VA2B=eSm4!m-|%=cY|3P+|hGZAJ<;1t*ot^ zJzYA4-#4EcKKLDw(y&96O-p|Cj+KKd3-tYdwP5{5tHR`K7!*{+7drD;x+&}Kqo<`p zIk|Ye+HNT%3o5jJO;tu_RP*NGCVMPvLl+|pzW8flHr=4;Fd=Tx2Of~ani4F!T!clt zOrjeB+38=asIijPl@ASbH-qe(Fx4XGt2Exi7Gb=06KTLfaSuIRs9R%4!f6@+@_vR2 zf6WTBlY{l#G25xtJeu$#7aYK87Bkv6E)~;OI!~$s9teavI>DnkKX{#+J(=f*gRCrW zd%La-Hs8t%{uNefgVtmWijVX+W6l)&ME#&pw!SK}YZwX$ZGYFvY#LA9^f6mjhGVHR z+G&T%-LXQaoho-UuRz@dK_&yWb&ZUYJrGQp#B3}$_Kwe2< zBwTc0{A7R9YC1tt_F>*i=hdizw1^^xVy@^SQ=| zxzFJ?Ph~}MU*4!c9NBJ7?7#3OyKd?UbCCCpVFI@$iuGC!LD|numOOxa%;iZ zfTgO&P|fBot%W{Gc(tPK#Na84ABXJ<9T=4fkz-Pj;SG_6=7YIQJt>Vu3P|&1>zn3S zXxJ8%^+9PoY?ICRajO1I_PqqNXZd0b%1Z}*qu*}2*{1vYLdD@Lme7`L?ld(uGGu*JzyVYCcpI$!CuqYhUvm_@Gpc2&&;fYNlDv79Wg_I5T-aCYcbBt4SHk?SZE75WBy7x30;KEUx;y|>V-WBu^LzpR&h_VR5> zrDKAf6(mam&LEeO+ga~s{LLnhUNc^AGq|&1hBgTtV6I?jshxF zp2f_!5j-KM-CsROojHsUjev5g;-|G;Mvmyb4VOuSRqc4)$A`_L=PFSBGp{#`ChYfb z^fDnjo*$=QrqOMroz)HdIpUW$z{+scHPp5)fQ?9QPQC4Qdp9k6J1AX290g z^65*0plt$hIdN}UtG<%12DwI>0b}Bj_V9RO%Vh>kc_3a?PW;y)H_Fo<5m3_^u@kQa z*Umm9AS4`f_XVCtEj^zvA}4=dd-HokMN-ZK0W-qHz@RP(fu0LEWMDH++u5>)v)V=0 zd98u{NEWFr>Gu2QC8YGTzHxpH^*)Kefc$0NEd%*=igg=dwU8D2b?8Sp|jhpS|Bl zkI$~34CT03-}vWhm6d*(qQ$G{d3=VR3gB1`K;CHkcvhJA?=7#FGE?0z+O8@uFW>7Z zEM*c9IR23AhSjz-EpPi-{=}q^{i}86@Eb@7UvgBV8l4oP^<%4heuR- zHYSXaW)@gu$uk&JYBcW}q~mzv?&z-ws$ScRg*GLa6?Toi zJS!^-qSlYS)cd`gLjZeo`>NUsDMIB~!YCdjSZgv#oR?3OcL;W-*(_%cW1Z$O`P8;G7MEe zOH~p{rD~;I`zyc2_DDeSUQ= zjpOtL=cSJ9{c`a3D7B5o9l`vdzl0fOAprk~0Ps|0!y}K}kBABuDOXFLA{cl5UtjDM zA2fFqM z_EU?=Fu;er5<(jmfxu|>fDw~y%ug#uZz6#m*~_9bmTq3G>v0vxx})^%9(OLdr6LHp z%q-X=8$7Z@_IP)Agv8G^8y>%#HM~RD*>+nS*o|@&lM9qz<@KPCCB7iXKfp>qG*LI+ zm7<;ko-?1xn&0omo9a-qSpUSyksm(y+vX3L9ca^a?5$S=Ne_{!u(*74+@POaPi8!> z*hQ462zr3MC5=3wD6upHmZI!F{}tIn;;+Ps89oRNI^$N8FcdC)d3sfre_8cd*I8(e zezB5kKzR$6?!tGeP>^^?g)lhriSjM~&Kh9rF*Bv=|XH1e9@@pnJ7@BZb( zQSj%rc6-^E!E%%WD-3@)=~Nk-_TBw8U`4f}81goQIx#kX)$i+~=(=Dsr-pwkCxV*-zL?BRD%fTYyZ)%6X{kA!5z=|kfE@kKYaM0N~; zX*W+|Vf5R%yCc3mP3wBNg4thla`jIMC_ULocjE1D@Nd|=<^Dxb<7p{1b5raCvv+HD zPYjM@f|kKKxtp2m8^_16Xhy+{`rS(4_$=0~98|e>^WE`|8N|Bhal`3AozBm9W#?X< zWJ}ZRhGnX`DT0#LWWUMM6k7ZadvCfAv?{ej-XS}#6*PQ+f( z4@Ap^YX)wPO7=LH>8nd%1&tPG!}%%a!$?|_T*CYa<4Fl2Q-1%D&(|51IX$^v?6yZ% z@gIILIw3eeX;1JQ_)X?WOPZ3Y(RL0^cZusf#0KCGK6%= zC9{(H!II5XR_p2hc+>f@5*`V^GRRT(UG08-7Hasm`)PY&zbiAGi8QRsnZjhUr?+?H zWWxpN8p`$5a#&4iy<`0%O9ohugtqF{uHD3ow2i4OQFssY#{COof%W3r~Vx|p&YJVJSC?u7b=PNDvgKmO|z58*R{_Kn?j!hp5nFdE}D6f}SG^``)Vp#g!c8hD?P3|o9urutD zKV|+JYKnM%9v+z^5!At*ZA3&nOm+4%l$iv zIoOU^rL@H+LK-QRKk^1op}@Adv=aBZg%9afA#!@A)}Ml@x+k!w>>{xE7LS}${q8bU!YJFglLp*V-jpZN~nCG}ooHM7;JwLn|j%AM0CTiLjiZ20;o zv9;E(@Q&!J*Ue7z?KT$=5wj(KS>y7yLKf8mQ#kMqCEu_n@&~z2UddOooY7JBEndE^ zv%L`*FvuyPzy%&IHa(dlyCjB`r%%_$BiA%n5lAPZaJzf8UTSw6${U52YLI-Q#vGh9M z*th%IDW_`Bl^?E|am$V)+%6P^Akd z6w=lJwwXnHgj&#J370&@Eah{*6UK?!%>jcKkmf7Jx=mtR^``>{!bm(hz)I$fC<-gU z6nqqMz-~EXtIOj#Ccov=S=Ku}KNY6dThV^=;nOt22wBm-oat*xk1kht07t9_P%28M zFTrDgXJgt!$rE|0~)3`uxPHCeGi zUG>@Q?sEV1Qdw+Oqk;^hrBmX4T&7GFe>d-;3<;Otq(e}`Jc%WyH*N*hEhK*hMWRP# zf7v*tG)sEg)ZL$Va%Xa+4~&VCC&@x(zjn;3HGbXZi|e~m+#1He!=UdC*Es2Xjc1abpz#E<90{yL&PBjklD`QBnZ@UqTSBDy=XN>mFGmGjJYWVCHhbX8ex6W zvp)?6K1&3cN=VhoAv!Zf*fTmc8juMLj39YuYtmz{|MLlQfXY^8$Woz^$_E}54!0dF z(h@HUN*h@r>>LZ71ez__jv;?DO~YDiYhHfe@;eqySzUak-94}vo6M|f3Dux~5K@fW z0Zv&jRpnFDUs6)m==D!1Slp}G-!Qy5(Wr^6^|DEk>_UW zg&vnE-Z^hrND zPe=K=M(Wx)HFI#R9#kX5jJrP!B@cD~TRUaVs9*4zUKL`I(Cbf_~Lr4S$XB-W(c zL3Zjq+B6BEeC&&~#WDeP-%RVT+9V7%KmMJW#g*vTG3AfS@8aT;6cVjp%mK8x025LI zWrc#q%~$ZqdFAoAIJpltDyldvM%a{wds2Od=UV(?;PvtT_b5tyq&fzTWdGv@AT6n^ zR7bckC=jgsjXGQ%DOJu;GH(V1GecDQ--@!3GMi53;g~1Rf|oA4G=sOmd*nbWsijj_>!?;3=~Ih<2BEZ;Z%N|1@jN>_#|l+x30bNdj|Tn*CB=lS zK1pH?@*0~-PQXaTC!AsqtwzeT0cn(9wLZ4ycPKh2d^nqX_Q?z3JBvY1QMybLUz_&8 z`UC;gjG5)Gr$+p+{cH7v_<@tW#Hk;Ik_+pwH|?p8;vDR$MeQL^xAG-4V;d6$)U*{6 zAvN}3iR6PD^DljuQUDzL%)TK)x=uTH+1;sD0_Ho{tHuqZnz+T2`Rr3};*)34r?|gr z4S!lP-x^-HWe?Ob@Hl<^*W4EVPXFoGpy1$4k-W1{ue|@ve|22(K?1}15>;-YgLw{H z?U8XUPue4j)Kyzub2zMUZ>q2qGW!w~{g`-kt)Zr>3hio-=?J8#PmUJC8;JjBwQ4R6 zOyjzDj2TiI#Wxtj^~pD%KP{d;2C4e9vkx!F48DDUbUJK!L%J$ULb-A*t>%H(94kGWqjh+! z$K-L`P+2PUDS2b8x|%|7+!9kYOEqVa4be%_dgyakv{0T#ucpqAcV)|c$C9=>H3w5; z=U~uC3=V}?0N}Iq&<^qO@q#~1q%hAfq->|lB?{)NG9!p&qa8ks6EFr%l(R^IG_~2j zAdFoAFejIfW|0+U^z~ZXX-(i0;Gf^eN(VNb+m0#1@mV$Wz&aRWJB^f=J5=qkPdM2H zH7iXRj6I3(<`EN4y^CDQVU zw+B(LEi#h(_LX$Y6)}3N`2n97)*IS_^G6Htd^LLQ*kZm}h+1j=PI)YgZpmHtMsf6X z)i^jesW-K&oNTM`w6}c^KGQw1#eB@mSWtOjp?yR&A@R@F?lA!i=8KuGX7Q=d%IBul z;P?vT9oB(iZYLNJvfyX{UD1~pMDv7#1Ssn8xyI*{Txks^S zZwN70N1ggugC`%tXWEv;7wG{xZ-3&_ogMe?y#DxfKMi5oT~)i95$tkyo5oc0+@ZPX&FK5G0T5r@n)Y>abzwb7H}Dc>;#aw^akA<|)yV{% z|JWVI9dr$_Gkmj!*HhZ?KRjrpqo<9XCA<@}Hv;N(YKv`vH^s;(-A$ON2=f#4$<8jF;mp`*gVq*Jrf7PaVxEQ1p$N#vfqZ;6ANXa(m|18@n?ZClg~( z2yR!@V0uGi>P2R?Eg}P0@CshoZU2@4b_7tvW+x1BH$>!>uBYqQukWtFM*Q*C8`f50 z%q%Rd+Y2;Mvo+zvTm>Q)`U`l32(b(kzz|{I-8fQ1kn3^)fZ>QffQozNgVR`ZB(bIz`=NrWll{%&$=SZ4o~7Qx z7ahe^$=geD$o++|rzPg6?;|ELpricM#rLze%a3cU+(s4>H?a8ZFWV+&^N4<|sCP{G za3&M$LG8I(TQlp^Nd|TLbsg-Z&~5*x1(mrj8^?W2Pb~k^bUTr_Cs3f11 z2k0+3yb)D^B7l8I9{*zDsu@r3aFKoE(4La0hNENoIq1|31&8=WM-vqsdq_cEd#-h&&%A72etW$3xB;LQv)(YdceN_u@F8 zuxOI-n&)NJN3)^?^c>Vl+iTg)_klrEZW4zrv@LLSEQz55f%|7^u;1DmW0r#AN2;rS-??9Iu5X}NPJ*r#d*oFxSt9hc)np)4Ae`HeLu zd-Rvvg5IEJ8v{3mFBp!!MzO(%()HL&#QXFDjlgk_(wN5i<9moSi{rd=Bb_Cl+LMNX zNzSx@;ZM*tiT8fDsPh;Y_#a&Nif8(#k&;1Io!2Y|&-UZ1yV=fz@hiY4_-}(t}C4A}L}>6YXKK5@ik!xAKWU z-NgI#XWY;?W|dqzaue#O;z1r|YKr6O$wD%H^-zN&`OjrfF!cmdAy-^b7$}h}=&}vU z^6nyBmqx2JWyePP8gk54x}eEnb`q(}9j%Cl2|r>of*T4W^Do``Z{m^$^!4=_Ur-zi zOiWWz3z9!EiGp9m4k_T^NWB|O>pi}(a1qo)k`|@NwcmHb!I2*RqL^#K9UPyWOtyhW zYuFTXmo24~t99bwfX2NXjfBlR*4hts1IwZfnS4S@&AQ99o;TLemkC1lZVgCDQgL#U z0Q;;P=q4{-lP*ebW@?(Z9w|SJct6Kd=_LalR_~YZ{k4(m#`U*#yR5Kf9UUz#4Hxqx z;&@^SNFC{FmTEr|DW-s;EDa27jasD)h2jW#T}?@jxWS;1+ns zB+|zXu4uvE_H@m&ojB*`=d_L?<+L0eTBDCQLhQGrdVo92BHhowK!d6E_+C_I7q@=w zy(iQ(aFR9nr%!XMZA@+sJ$+r`rp}d+cFNe2-i5ASJ2 z$bm*8RfwarFrr;i&dgR+`S`AsloY9`5AL-TqR*gF`a*E6AteXL(4B)K=l4!dt>tFD ze~{6PM5n*r;I7W5n$-}=9P8KEks;mJS?e)lx~pTEU#a|3H&Yst$r?eiV=)zGInh4N zZR@K8GfzweE1dxnNhD%JbbfMb?{(T=lc!hPBP`IVL&I37O6==$R>%)oBm%yUTZW>d zy7+Qsk3%IMDCW*ntt~GY zy^f>xT3R}w_-p2bho?p9xP~YTE&;Rp4mn>IMvvs3U33Q`t{y*@g5Z(I30`uq3m z_)7TuK`c#Q#(|%^d`B~|=NT9-ANV5t?~wI-jU!8zz(w8po~6s}7M0Zt+L%obDhagZ z)3eYpGh4{gcZ^e)J5PFij!c(mSEA4&%T4`p3sXs+gly%kOfsZ0HGSh_C9F$xEm@FN zsF9_qon4rwRhix7m^Nb=^x_^1znq%O_c8l-<^-^dhi(lSFC;U(ba`S3wsnmPap$xD z>uHy3?0*wX+lkb4#U8Eozef>{?~si8t;nIsnC>D%xgOK8ZxYgDi8+yB@dVblp-CS5 z@jUj#1CRJVuMh_vJ)^3jV&%=!Z2b zb3SfF3JVjm)#ODEb6YE43tC!k-M(FmzN=SH>bGk_d!TiHZY#V1UXW=Xipn;H$>uI}(!Ad_ z|0gLaNhQ~g%>8FwqIc?(jEwi^eZ+cA55k*2yu7Ye=x@~m*tM5j85!)qjn$vAb+%n` z{g4{YZuR-|W1`Y1dL?Q>K@z@4k5t&6-%ocz6#4vbGTpDy=u@v5-b878KM(j1q13T*1? zYGb<^k#~pU*ygSJ2(of>TSj`i(lg*FZ1?i}&-1kx>oiMK2TM*C*`y;u|r{(n@l#W9Hso$GJgZX`hwiidww= zPLS-?I`ZokU(pI!XlQV2ZNn2`<_`?L87g- zAJ1idR1CNvIUs#R6&!ldy{7M?7oMd2@2~JhMa}vy0;1}YMq7hwd zMk5NJ7%v#Bq`$i1b+BgfZn8YPd&OtBpYP34>nqZGq9LOpxb8op87i6nM4ZkHDP+eR z4Ktytw8AuxoR<4KQ1GTX(>9qldZfC&QjZDnu>W}19iFF}pE0DHB8a|LbiQN(zz8WZ zl;y}BlJ2k|>FzPZzfb(&hyAQb-|?A~&15-oT&W(9__Lp~1eMm~_tcQ7L0}6Hp?1MY z{$uWQM8$j7Xn?n|e&zbnNrqXWraS0bFR%w+jD4Ejq{_4s=iiD*3%~#f6 zjrS9H-n+Q+KD|Lqbh0<1{ib4BOFW4`pGp3s&pFDS!*yeKbvQe?8$FOB6b`a{(BYpo zpx#|YSd*3E7Jjv*-#C3^*y(1WHBW?+qD)I}s*gX&cIMA&odQBa#32yD1Rnb^4Q8!` z89FZJyIYMyw?PlI_H*x3yw1BgLPF}L26!O9Z2o+^G*Mw9Vs|;KJF3iW%RnzO>&_J_ zo`OuZPQ~xMnAb8OgSI6FA5Yp61Kj~i2A$~_?s|vCeLOs* zQ|O4K4Ti7m_Sej2z-Yq1du{sTHJNPA)G{?XY>&g|%##O|UmQ%BeE&0b-5mmnhQE(} z7Ir(1rP?CD3mp0A2I3Glx)OuU5tnX_5dYtB^;HIH_8He%Xa37-$p)fM>n@L4&eYI$ufU!d;iI}<~9vD3BO)Jivch2ej$U4y$;h$L$S9aQcSy>Jhps=j*l z<5X;aqCz7dW3M6`8&gMV)46{&IPn1;%7;{+_VDm1bX1(&1Aqs*lS7Ql?HsPqE;}GL%oCbBhc2M+P30&)gUZzXsXiI zgkLhm7tKSEt7S-5s*$HGCXCFMdybmxHVz1u(h_U%48GhB2mw)9O29%M&m{|$Vu zK*!zvNO&?gvf2Vh&dR{h^(VPhvp4}-qF+}Xo8;KLQ70p-sOb>9g4Eg)K`(@hYL-!O z7=Jc81uVf++qm!Fv#PcR#e}6q>b9P+3^tyhSnmsTniNzX%{i9b5%sVbi1W4&5OxN_6#SaD+Y+m?UO zz&);>8u4PRK??uwmP5BrlaC)29XWZ5PHnRz>Y#U_!!qB}@{13<^?31!o}NBlY1O+S z>L}PW)GS!D(3SNy#&+np#FRyaC|8@_d1?O1E#k5US+`Z4THjzNL(lBIV@K=qCgnnykYMP%pc)5-IG~dkxT>3~ zS9VM52REfFBppG=@*HU3lHY8z8kjq+bWu+NLhEw8_u*Pope>4eog`{*2LTKVb*}5Y zE*C$o$E#|PKDyR}KR%)?B8W7L-pH(vtf<(I9Yk63{@_ zg}xa0hEsL}3+9&U7;UxcK`ROOAhuir&i8t`%t+ewRns{J-eS7?!^lGragY3k=_=85 zgD3uLiw=PJHKfqly>S}3cIL5)$i>69nm29RILA<`Jk^j0H} zI%IK?>YAIPK=Q7mYW4+va`nSR#;vAOW33WB4|rLkp=PY2hH+K|L#khjmk6^~iTX5H z8(JD&wurOiU5yvNe*IGSh$_$P4K(U0R@bQB)w0TZ;pusrhh7x=mZ(m>#1(n(u`WHq ztCb|l6z-q1u!eE9xjY>)U=^}SobB);vBqx_aEdtcmq?<(A zOterHk74V%=>&~TN6s2Lake%ZmH_3BzTQYJ&1Y`6SL2bPa}<5p>yL~ND1kE^7eV$d$QUeH z4czSX$mx=%bJcJ;!r!&KE4ydF>EG(a#I355Zo@f86)&vX9rZEw(@nwbc~;-J4Q&ji zik&KlgVSLb*Db5UXEeG@9Khnl;!S&&ZqN`?{Y}xMdn&SNYr1AO$AmeC$NNTqG!PgQ zNcp2ydKc7k6}Xt1e7B~uNm5jOmWWKOO)u>E1{q_F*4(-H7>FHye`Y!y{?n%~NjnxP z!5*5%-jrlfnas_$#3SUok$3Y(vBc)XgYF_FJE6h+rD-}*%-ecWU#F<6K5NpER(}}oEW3^)wpMN9Jr{x2 zW~%yMAk#eq!3-ok4W56K9&GN_OX#Va3XNlEA`_~nS>7QS#lw?OUh0v=evDw9k@rgl z8X*X>KL-b5F9#s_@!IkJPre-wxSj9EK>>w&?N$%pcx_QCV1{s9JWs1_rRC+B+C8-$ zMUIbX#FJhGf-;(BWaHfAR2-XbPKj=%jpa@)IMzX2Qj-q!pwz7PbByyj+m@>0wOQ(k zdH!xN3e|3Ei&y*DbRlOJZg_gQZn4J_?V8*MJ$qAU^x$2 z0wyBzC>Msqt=U{Vx2>`Myt6TU>kmJE+$=cjA@Qn~WNw432L=Wn&DyV15;S#$k`QAW z^p5YWj@ELqb+pax#jE*u+RxMn3ZEX(@c(4eseETviNGi%7C8L|7NhrXFMJ9E&ytJ0 zRR($Cg%+j3(H9jub*`a0`0bP3xAp5tO2bbkGQ;!7Lj(BJ$Mni1%Oz!0&Q7O-A*iXzyo75J=?|0vxf`K)F!U>uG&rqCa;Ph&A~ zb_?$3w>_3|^+oDZFJk3b0;03ntntF69 zy~#a@#A2A{L7VK>&?EQ4a3gpc6Oo`P!<5_V2P=(2>gQ>5RXh?9NxCuni8NzT)79sd zHj~4HV?cKj2S9!vJ^vAJoo{Y6BGVt81zQHbeRW$Tpj*{3qF{Tpmrr@2jhL=ByZgID zzozeA-1ETBVD^Th#?Fsd&%tVkqMv>{*9iR5CRH7`;76*v$a8`j)yXb>Lq%o&n<{4T^-+xM|I>pA+*Od zUqhYzZh!ZTmNpLlW2VY*yhw$8sMyo3>H`Mqvi|(MYTJDYuJs3Tw)Ot^qRb+Nrl8XS ziyhsJdd+mDrO6KKi$THD@8u>d2dedlv4`)skhk`BJAY9S{$A{kVzLxt>MQ?_0Rqee za`Jmx^z?gApAY^qFlICC=zf$tx3YwH@!@6WAp^lqS{f7TK=k6wM7V`X5>TA{&PM?x zKZ^9a`2#zrdmrsP-CPKlN>UD$+lH0sqXe5puefa0!QKgZdg4@C!S9UQ%`{I|SoBD9 z%OXMjLEwEOJd(xZS&;7eDUQ4cuv@+{7vv~+r95%=$tFQ)9DDMeT2@Y}6VUuBRkCLC ziM$H7dV?>UNE0KX=1d9<029Ce;QnfOuU5&_o=12<58K}?Aoxj?b1=nUx}+TtOV}@3 zkQrn@#SskL32Y!KB+t_!tkKNo8*T@d9{leD4V< zR;kD_S}5w1=V_(`{G38#UfoxEuLke;+rMeEvXM+*JhdE+OY~Z#xGX52KYwXvzXTiP zUV^;(CyA6Sl;k%AUSJV-yqIOG7aCd>t4uH%LGu+(7avc&3a_9iUmwR8Wzz=T<(?S# z?V1c_bz_czv3s|E1OA;TYl+*(Y7Cd-n7#1kjeObJTBjAcN%FWNkYUgF^BG7roT4%$ zf(eO^H>ACe=5cX}If-B|Ft}hzhiA*h{hA%`{L>g28TnpVggnNN>1kNgaSXGefv_X8 zW5HSx0!biAi@PdEL9xIhPJF#Uol3}`1CS3~TyZa%4 z?uB+o(d8G-e}c-G{t!8bxaPPj$1wfi8j=A$2_$|(&EwX&Eclg`#Le*kyDypp~7Hxp2U5(Dat2dt8&VxWfO(7#) zN9fbs9F4)=;k9oODe2|xq+|*d38bVO^NSmw_bRzpgT70c*fw6Adv2V#@iiW!m`96@ zAj-(cjL=ALv!KGnM2LEM443Uso7`u)dF!;nzMp+RP%IC```t7$l9rcMRZ~)k=e%KJ z2)Rwbtnn&Uid+TO-_`dy$1G09X|kJ5U_{aMd-u&MT38r)XmA``;o3JJg9aV_Z&BX+ zJIo7%yXgiV8R=`>E;g>Nj{eEXYox5XNsj#s<(Aq4*W$uBSwrJZilWpGC>7z z9f%9`5@DfE4&7|i@$uTRvk{z<s6gvto?QuGiES(S~{NCIwv7#rA zk2m9g=%yqn-E@AS`(i8?-yiR(&-8c{XHtxs*0-YsLC)SilY93jXK~gl>8P2kgA%fs z)qbeVbI8=V&sav5oH=Iykb&+n>*z*{%D9~x`nJq=O1b=?pM_@M3>Hn^zb&~W&v+Xo z+<_u8ZXn$PUDb20*$<|rpR_LzpoNsvjc>@OiXcoQH@y$xMvZI^W3q%U5=&VlvzX-`D;oedkn^?H%UB7c9G$d!Ub~q93#l;m^}8FSgs#sGIz2%k@VYiHc%G1UY~3V*J)X>ixq=sm^8He>IQ6tLE)h z^S-0O=Y!e|FKif8sS=M)){k7!I?n4e#F&#qsoLh3pdZVF;5e3!K4PxXJ#vr)Fsaku zA^;Uf+?pp&iOKpybWMz_Jht(sgW)8?cUGurILt>G6&My9>wFoTp7Ttlj^R~xTy;EwL>rH$j^U6Loh)+oXZ|&}XpUqmW_By**BY4>N2Q4fo=2wnabPo+P$arE=@G zr&PX|%+ZZ0thgqoX(0lxAjTa6=srld7otcz$KSDq)|_36rXp9b5J|7rkf^ ztvh_X=LE}Ni&sz(U5I!z`}v=o$~qrb13VGWJjqO3$X3JQeFxNI-eSYj0bhutfs^aN z=Hj0|EqW(MXHM>4YLStn$}i7vnm9T>hCqIp70~9aBL4%^TdZ_z zv4FuQqyUP7kX~jKLuy)D0qWyIXIMOL|0Gv#L~KH`z)%{MFP)FjbhX|8sqMVuss7(S zUin5tNkVp+MabS1A+m+CjuEoQF|y7{va&L>NA^4+*~B5+vG?BRaO^$rH~Rg4_x-s4 zy>EZiqj1jod_M1Sywb<9Go=to}| z$Z~>JFC>iNovVtf^;BmFo!LPm$OTLR6_veTwSQgSYha>J(m?1wg5+1W&~}F{za`7% z|HifI3L_+J?4uK{t~QHd(IO^^th)U?$#0wc#mkpFTcXiqWTlSSm~vL5ZdMbUy{0Lo z@pNpJsO*{R!~09cYIk)c!>UG9UmQHmhk<`CL?~gND>QP4zn9r9%?9%G3^EcOpNI(RtU(qQb9!orOdBfkH~E|Qf0TA z*_k<2_AfukN(`^ zDSg89vWj}lH4}U0CWax78x!H#OM<``FwM76E6LaCONh;>WMDLIe2q&zI#$Hu!l_64 zn&%Ny5Ikzs>M#h4DF;`>OED6A{d%bbO9fY^N(3`wBGLUjm!zffv;(T&f5P0>w2-grR1bO&;=A3wi!g|%VQ-PWBFPO zqDc0M73*QSv7j zgL9%_wAxaOe^6+03thx06a%VG=LVtKI@sP1RC{_&xT8~BTR4wm=ooo1}`omGcK+Vb$;mZ#t|CJ`l!koh}N|K)K3lXR8p z??hz>6IBar4lNn423E|bwqu){%?Rg@&dwBqeNv*kloA>rYn-rklMg-B8fQ|S#+AiJ zYU1bHV`|%nBpqHRh&l+OiVP8d493cry%i4~v6C1J7iA}NCbC$vtSn=8@pS1uLnN*P z{P%AUZ{NiQF9ML7fa`4+=7rFB0!i*qa?V_k;3dl&wCU+4Sbx=Wo>!~VAL zjSk*F|0F5T{PAwY+gd^>Wc!n{at<4VL1{LrfE$oNDcrjuq%6C?k9Qx}{p1L_^>J-Q z#I=t@o%{(9bS(O^Bg#wM8g8iRLWMp|UfRKq1Z(n`JxF!5jP*BPLA$ickIkUx{G_R2 zZ^ll{7a&reX38PT%-6+n-NYqM#k@m9j_N;NHv4FQ7(^w7I9$R&dL52k^xte)%@mo+QjCwEPppZx)_ZdHt9 z>v+IpzpQ6RMJKR|9jrz!JFj-dnGTY4BqCO<$18_^U_A+bNsb97GpFp#`dv#I7%zY_ z*>^rQsK*Y-Kcr<21k>2|Apld#wIQwepR8L|_(&gc;|=j6#@ zY?9?tKflBLlXQjXPyXKw?I9Qi}B4PAKZ+;T7C`Tayy7^wVUx$VMv z?e65Nlj82GZm5*QVceh5dOyU>Zx2tPSu>gn#W*=o{vSPnO| zSoz}4&HjWi9|QSQLw>fNM<%@Rt?m%u7ckC+bKN^EMcU}KDlK(#xoi_d$!Loj z%X+L@6!^jHAK1tb-H0BJl*Y+Ffge(!6J$j3RMwU2-Uo@Q849X0gm)nV^G6%4?0dG& z!8SjNkl=~r(U%Y5uk%B3IXH#_h4v-ey6B{PXH>jiKczXT%I-qj$vxwX}Mb_?=A1uIN-aBwg${KO5n^b(hGn^HmOD?PoWc`P;sm@x}w z{AH!@p~k3wu<<$TJ`xtzHXkborDUYq$%ly)@w>@+%raMii>{?r1Cw|>GSV$MO}~Bu zj1jqN1Pb!5@V>(16d&^Xo!Dz?jKfmBzPi!C-Y72i}=mKsS-GkFhESMm=m z;($-)?)8yiHu(ZD$fxG((~sFYz(OX{}#p8W$e(1fYS zp0Om=Ew^~=jVAjGubmyC)l5T8VBZw`5;6S*_J^AL67Y~z8eeBIfy1`pv-Yph5Ao#= zukq6qTn^gEu!~(O&TK$-QRirv9|i;-YSXL&l$SScq@#UJHGSlDb6A=No=d$7$Z-;P zv>jMIRKyKl1LMFF*r|*YU#khErdez&VRX-R#E#pzFzetXKBU(hP__OimYG_`C%n9p z5-0Tpy4OUg#uRaIz*HpKT%-eh1lCebp5n38-Eq}M-z9-L4b1+=UJ*(}L(p-JI+XWo zz4&w~{PZ$0#80)i!9SZ700URUwmSi~K@r3Phyb;-%x38#}c!l{{SEFD~1;-}I6gvVQ_9<0=;;Qfao^hM-^xCAVuwxN!&&$6m+h&!1y9!}eM@7C?NgzJ1FS)#BV2bAQ_Q;O7ozZ{6(F zC-{DH(DjYdaD8;jV|;W;DE8Zvw+0(SGZ5GIPDMvd?_*-E$2AbQb+0doEqKEpouceL z5+)&euHs75A(|T2tE(O@{L#}hhgUcI5MBx|Ui@z8yLyX)R~+Hvk}Ms(a`fTm57QaO zHprR?5IjTDdSs-$y27l}(hcf;2|p|*`vkzQ($|FV1HbkM{1g@^?0lu~Q%+q z12i>F`_*j8L=FBqa&tdt3zkHh5aE&RkRXt6`B0GgMqjpuG>GOq5fHJ@bGfF^lxpm? zwvbnrM=A&!gK0dAif)43ct3!YXY$8t#_24NmJ!wDzfFNf)AuNS?#&Kll#iJNSr#ot zcgk4(0VeUqsP#3;e1m^iDGV!kgma{vr|Iz1as(G_bvzYp_133{G$%)!sO3G~s!s>o z$j)q|o5CE6GO^L2tiY6qbj17Xz)w>O)%8vA034~d-!(@Gog45DW0Dv5#Jv^pYogRP z<-p4)x0%j-yEjLJhEJE(z%Pq2Z@?4U*xS8`s;^dK!KdOK%}&P; z3z!-z5|h|VwjqWz5C1GFY7IijNv8BnqUSs^5?ydvHQ%A0KB$p{9p;tvE<8}}d%qzN z^}sv}6jM>ls||^*-#y%)n4W+;&46z6*&WI?y6*PaVdCJqHcox{*?|a`gT48nSRk$( z5Szd3I4}Vv?#6{J#>jf1j2e7(ROVi5N^sr_%kY0Rb^$@4`n*x(F#66rxgo_ z_2N4t@uX|xdajW-|pV$JX>D#{wF-DgwedvdAZ8% zl?se?wXW_4c{&5IFq@}Ut-UEwnwyaPxP zjw8E{NSU!>$V6j->89~E%M93*A52LJJ04eP#aH@(UWcm-D84V9*@>_^b8z-X6H$11 zWx+gj;8=0(BZr@6U6epj^>0^nX64EYAQQPEwvd}MkfbZpd;6B!cJmCALiXNmm+9+A zKRWjYEFth?;&GQHDtdZ`@6J7k%78BWWjU0cZW;-30L6n)jMFSdKZS+t=+*NMt}7@g zG&MCn^Ja6^R1h#38WGAIita~f`LGFa6)dvq7TyQxcC?|^W8(hl^2%9uM$3cMR0BkJ zJ8P^VZv2M0pFB76yI8QL5-t(d!S`uDwp3rJusvI?XUA~6Sf>O~}AV%@eKFj!wrs4~m? zWLfdvH`Q)M7sdHJk&Rv)?ez-yDc5;qslzi#;Abt0GR z$9*64>uX;L$@QCES+UxQW8et)nAR!*KB)MAED3?8IzDt zUO(SttKOl1a9KC&L;9>%V(UHArq*)Z`B7PURU@r_<eMrnW&_vAm1{s}50WY01;@5?y&Z_RPT=2v;X$jRS`UQ_5RCcSNI!%B z|34%M@XT|@@b4eW(a3*&g1x($ZJe@7NG+b)R32wC7G4c44q>VTB~ zCiu@!1Pq$S7zZjBw8VqgWNre9Vm6C5{FGFb@?x)gCMOp^I%0=YbgNv_L?c7Gx~^rZ z-6Yt>ctx;i)YgvY->YPyrH!q}u{pbcfR4lup&wh5m|7!PlioJ#+v@^&^ju}k$E(rCpBT0 zNZt7*vpi^jq@h7tOc%V3_B;)s4%Esrb#|09wzaXh|Lwkw`IbfXt7Jj$@ZweaKY|{~ zFJ6zH-<)?w!0HPG3)zO@-?qUjUjO6UTSKW{go!ppVdL(2mpwe9wmO6cE1kI_Ss_7H zt~*JN?;pABEPD_lrK8-_K#aL}kb@|IPw3Wb`#neE-s-Ayb;2j#qF=rlm2WasXas5+ zG2Ql}dEw?JY&LL1GMIYosZA$9sXr_@=f}s#o8eoJoOS-quiK#-Q7$r;7w#=$0vcPx z^hFok*SH*K9Q_xZ^T%j0*#4PfZ(|8DHG{qip|K7~d`id`q`*wHy;xwl=e%up^b=zO z4nuz)y#z1$domf>Ysut_SKPinL-?lL;A-{$4n?lkAw^}S@TLV3bneE%?y3*man)nC zTyRk6cWo_ohGJr^HAR2ULIAl4Zkf-^o}M0#^=-c>(4n!M9Vd~HN%3q|(rfL{d!z3A zIQU}RWZLV4XG2JgcRoPhMg#nUmV@IuUs&kIa#T8ca(WpbetL<2*1U;4l`c=QU6P|=SYR`C<#TVY(U!i& zSOHFkLcGf9!jK8j@5XQvQ3-+U)q7Vc;1j+O_0vy{)b0TdQRf%ZBzu0d+otNLz`#*A z?)pxnB$i99$!#6E&|PWDB`om%j=~E{zG3~WJ0QGhd)o9E_=-MY>B<>-_3fCHl)dr( z39sn}{rgM6eD^{3h$(r-eFsH9_w=U-uB~l1#5gSd0AL}K7qlRH1He%{jRw$n?iTnV zw|%>)r}|gQ9hbfwmHq4p3X0;jBo{rh5fm2}S4vZnbU57R8-HdXTj;|2cg@iP;$e4x zo4X&-$Uvq^Q@5$qVVIB%-{Z%GeAbM5ps@F~e3gc;sj*rs^keHYy0w9y|0y05gP(c= zgpVxL9@?6idbmIXf55;%1~3KRF9CHwAFH-8qVu&2#2L`kLN51AIuejM?7^xyg$HNt zq?uvoXmcT_hlj`e-XDbI-0#0GbpO!D;Za(|%v=3L>L%ziX6&2x=aP~0U6arrb+7>N z1EwHz(Vzdr+Q;(am5SB-mUO2n@*}@6JD~o4M!srKPoKRQCr6i-b#)r-t>&9Ra-J6% z9r1w)tUaf`z`J>15*n9c2nSU~arKIy(cm9WBz)Gt4KJt9zz|5xG@X1~?9a zFglQekCV)Ihe~jIa*B@M>7=q84#3{j^whfMT0(HDWh^lr`E?7XQ$aH2gX}v7+Z?SR zv+P)8ZO`|$?RCjAySad0z>DWx{-(g`PW2}AVg+rvA=7>9=1mDHW8(y;C+1H5naVRk zq4vSl{z@|CDB(@HnURz{S2TicwF27;=m7G5&o2|{v4%C zkki<56*tLYsUNe`YpNRfYah`4>wrhh%pFuliDXbif0Rf7)uQhsI(&CRdMnLeYNT0{ z;VtE*zg^I|qEf*C*|)`Lf~jI2(sLsCVeJygaq$JI?HUM|$VazW#Ui@p49JdpkOO$Bf5?ELZ1Sq!rWOhed$t$U~y_G^rkOgX7~ zE*M>6r(3ECmN!5@^!3KmZ2B?LV6mXdq$J@Hnb3SW}Y@E|meLZj17l1%?FPdbp4H8r>5F(u(M4b&kH7rdTy_e;`vc)6O}MAYs_ zwtY$VP+Ljaj-=&aPzK}FGtP0SBrZN)e#NiKeW(7Of}}Q|*To21d3Zj;IHoT?>V3`+ zPh)s3HTBf9xJhEU%O)0W0bh$x#d)vT%v*0@0#3)vd%GG|eYV_yT~~jjm>RaH&u!6r z4eT+009WwUg@xE-?h2v07(S>X!B-aeuD2tWPmzZ*m|Exy3x`G#4LgnRaIpl~8Ah~G ztUhJX`3Lm$^|PXC+1g}~wuC@HnAA1FrB>8LwkQ>*i8df9v9 zmSozEIYQ@$p%3Y3X;t~zwJsu@+!#4hS7F-k;1S8kaI5cbTMmGrd%A9abI)}kUz`bz zDPfll%^#l{LOt zW%bYg`Yx^q*S>hGM>M~5{Ct}zU8TIit>mUUeEsjZF4JP_lRB8Nylu5z%JRpm#b$T> z|28p?Fq7+Mp#6Sy5&9rMF>9zV#e&N^`-)IM`yMT{RO-A-G`QB!=Jt_*LBDw68-C{h zhwAFj6a3rnXdkSm)X?4CLymOk5a;E4 ze&^hK*SdcU46|P5sZZ{`pRjkT@H<^^>F17du-K{%8Cmd@P@xo$Z|@1O#mU?*sgH zj!=PtG{`XUC3yA{xZBYR<^b57&kz7`(3z{^Gw+GCVK5nAZg@q`Wey~mS``%$zOoA4;|hAwIqVxEIc%7Uyjl5vwp zgVW^0iPN0~sG0X8*cT5nb!@R>MKFr@0z6m|a^)G0~zM*!< z7Nf2k)<_pm8x}%$r@o}CWBiBP2zvZ%l_?yj`Do&58ct@yHIkL-w1Yrh>0mVY@%q~} zE!E-(k|k`-+U~Y|RWFDb5!IbXxc1Xz33y|jVl6~@Q)ud3Rr}G?>{Z@-AV7$`4Tjb` zGPio)EfRgH->xt}v+XB+CHJ}+HSeQ69L!=sFD3GEThpy9$<>JaVn_Aa@q7s{y_9g(3k zJ9oE(HH&VR-VS@0Za6J{MrfF+)K51H9$06UZiXmFM62{~vvV<&AH>GF}a1SF? z38u7g*E+0*{>&~j)>7kP^g6>aXWYq0k1;UHh%9X$fsOCc7BHnS`MRVd>BGFAQEfU6 zWlEI9?KPQ(b5oL8inp{nm3tU1AziwpU!62JlSPMPm(WKI4lhrKN<5o-pT0~}`2jKo z(m`Vm?IA^&BbZ9t>44?NT1x)UXe_$bm>=NUh;50!TKDEVLib*plb4_TLkWx4QZ{M6 z!btxinqFJ)q)os*W@oyM4e7;7C~%Kvb#wxCr;)xym&u_Wz(J@W2uhE;?j9_R_&*Da z2ORBbYdrkT%N!vK9dP#>&?jkB36XAbOZ2nmRqmoo)Eh zZFt-{=(X2*s&BZ97lu1#9mS>iq3hEqV)~Q+@06cYl0~OaRrnGd;@dMG7Kcg&51xGM zh?HWo3eT51P)10Q&4${9mtc9~G;T&*xS3!bMu$p8co&^f@3Oyk!$2;DF%lj(g-YQu zV5_3HMF>T?M|u#{4aP*(QqEs8JzrGy4i`ebF-JROshM7Tp8ooOZ<%2gQAv7D>)sFS z)`EPovhnT9B%APhWk?RX8%9evv2#zWsIZ+UBTAnI7o}50;7(FUd&y@~Esfok@I1{} z*tdJihSwf^DDh(+4*b`;vvvEWlxs$>t2MDvc1U^D(j9sb`hm$oZ+Exu7X%$L-JpeMQ6@@@v@*Z+Z1*4j#V!JkBaI}$^$|0Mgek1 zk%Y@e)zpem3lD9r{WL2Sa6k;l%ZZ_x~1VvaEO2z7aFI`!eORS+lO7Li69 z!Ob&Dg`QCNP)5(f!SLO+;@bH=i-V;m3aafk6Cylf;wR$bi3te_n#_U`#WZARGH*||GkOY#!Kbh6}>_6aD93Ny+-}i2>Z*Eji(vOInm$8Y zr9Q)5F$2GO8^JDn!P=lzVQNs}g}KH%Jy};%T4gtj3bE&b`0?~+yXA4j$oO~aw!^)= z+}#_ZyF+~I#gwqik85b*Lc4uj>k%6;u0nfet*9v_$YyA0KJu)#%c!5GvYUn+rvp^! zhvm`;eSFA&m@j0}40U_@I45UFB%`DyXaPjV z%ElH@V%Xq#dAPD!5((@MEFjKOeysAFnEJcE(0zwcg~>aN`c7-{AH%e_k_dZTjt%~{ zfIz{nck263O8=xxuw$*qYnF+ELyW?roxhVn6>4=|p%mpZwF&>;aoQEDAh*lG`S!GZ z2DYkOv}&@7SVJtWHiUMi3;iE3VrEl6^el8Xe5eqKS^xJA*e8nk2AZ+wzjoV%2@hk) z412k_K5Y&0oqwV>6MQTeTp^H~5c9WVMCeJlT@Aw%iHY8fAt64s{x(*``8SP*j$kws zKV>=RPn{)v+WSLYTcoROuX?z+IQV8{6+K+a=E1Bf7~&k7bwp}jO%bsjxT^b9-AlV{ z%}dUqcZ^wAs~5R1Mq3h$7YP@P)I3JZjYeoRa0keu-?II*2*0WTJmI&$ntwaI+9-De zqg*=v+X;^(KY#pp1Ol-JZd$}6A<1oQY^*QL)T|t1(aopo0FJPaD+BuRajUHS#pk%# zh8}@@ldT~YY?nw=WHfpWGrx)T0R-FmTTa`?la@aLmp!DvU!|^7#h zEPS%}c$3ClvS@tfoi{G~_;zSDuuN(9Pc*aLM2j`$j35v=@x0S9Gm?uG2euiPU|Lqhm zf-x+vu`5L4b#B=o-X(blVN1?5%0xk1l(T)iKij6yr3a`}(_&?xDt$9IRuu}A>=tZ< zF+5njg&Hw6(RUUOp`ULgg%yT*R^zf8@E)xsm4g)(znxCYxWx!}QGl?}{x*{T!|}pn zVGDyJ;i?ErGnKE?hk;WhNC9x(@Kun=-C431IES6Y%MT#LIS1I-M`*KtL zFJN=0BX<<-e*|*mC#F0vV-Go2N)T%1#EZ`C2$Bqo!ZjTt`4X#HM>F*_5-{H~Q)7OP z4O@Qt$%wrW0-6Q!q(PkHtyBs{;GBj<8B9!&G&Rh9xdR)UKUWN|JjYpw80W&G!K-)# z#03Su+tKKKJPU-|*6E?lJx}FGB|JQx`gs=2seX-#i>5T>5IyeLVv*`dvkT8Zi@(0& zbi;=4i2{R%;^w#X(vMYJ;B0)#`m1BeJSUG}VN=~b2sUZpHU)JhJW{)=hJUp7qsLpo zKLs@(#k!$yq80k>gbVQ>?9iLJhV@7x$jIu6CEGig{h=85TR@CXs@i0z%@MVn;L1{V ztugMRIY<8bK|>pW`-h3&d?!O8x*y-r;FdS1?YDzQKuuxfevRQgx>@pg2T4#kg;2_hVZCV~|8(C=rRpm>;oo zR8)qYBSnKSexW+9w7#$3eX$bqx2YjuztPoO;*Iresh6Q9`{KTWN71dPd!%opIzs6V z;x#n8oR$yFuX{>P-$vR_fCaG`B5RllufK;D33r8C{?~9UkSrh@mTB12j$6>CL5edJ z-_GFeapEm(j?DHgxdSNbf%dM15ODWnjA4+5pE`mA!EdIr7{!>^LbTNAArgp9WVzdI zeMZG^(DRkrNI_L~$iXCTeiFp zHAnhKY_EHG_%IOt%p?vMD0*(L>M!A}vcwXQkczFBOd{R6q^r)P+s-2`X3sDr-jLI7 zuUFjjb|^)rcyVozt=a$H$fzXnCe{-Dp~>4r*~q)QY}pRU)QhAS$-D&IV3qw97r&E(SW6I) zJ1kOvwY8_)pdkwN)u2F#6=m+pQnnUrl7Iki5pKG;IuoM>IUs&t-8{T1lWJ&&^8X%! zc~i-qt(Tr`*PQs3fV#rG_tD)^l3)ZOKpf%n-YpHXAN z@84|K=X?FTvz3X$pYTBhRD34K>%E$qGcr@qp_H)x4K_Z$@Y-dcdz0WK(?xhM(Z0y&fB=#MWmA66}}Bo2qiRiR+37@`qUwhJV!^ugaq3yY^9k%&2pk#P3HC){R`Wo zXksC!O_e1dl#P+c2BneL(HkXYWrv1Em2~OUdMD%~Dyps+HR;~$cq1iCOCFcO_?scg zgTpC(yQAI8;p0{uX8Ws+coCbqXP)P~TeZUln)<(G%Ph|#*W(@M@24CsOEIZ(AF35{ zKhoU5ag-Y(p4{`j=H|zbEsxn=Tir)9&pka)P$!91?@NE8k^tX`Hzs}w31WnMo=xiL z#4S5?GY+JIlPP(%?Uy`|lb#WkLlWP=^Z&*5+ZxX$r>5?ntG0TMt6ls`+mu$8twZja zA}D&aQ8U|wvH zMM<@Kf%vl;Xic_)qGEozp@G2wquW9&G~HWHP7a$mB=F|OE0RH69r;|sZQsC5Qds!s z&CQyTFQl5Cf0!p_NN+ROuBd7Djq~?fU-;m%S7=%oSwxZxZbsceXAe$`g*}o+8np;n zGD;ju>HOklKmMh9DDgl5IAE$ySo8EGtl~p6?|0c$vrEr$S?LeHeks2lA-g8kWB4x& zKei`G)Un6lxUKKJ4XIl>T{-pkE2f5ne?ZvW)@0}M=w3Cwk{P|Pw zY;3XMQ?CFwvevF>Wm5!oJm4nzp^`IM%QsPdr~J()N{aaU%jLeNKTztU?k^bJ#r8sW zh}ec|0eRuC0%#(E)p6WrCz;$t*D~%r>H1=wi22?k^)z{y1uB*=bP~iT1V=;-Vm1p<*=6qEOaQ0<~CeW6Zij&eLd z@c9vH`Tj017H?Ae_HOWbimxMe4&lD8**!4imG5%S@#S#vq;5o|X#Jawfe(!Y89X3t z_p6Yqf&;U*z$?pfR%t`u3^@>CnLR4H;0uIgGj0OcM}T(eD{t2+V*Y7}$2lrh3*|c# z-CC&ZCQ|1_7;>Vl+awvX#K<=5@#hCjfMy{4h&$!%68)j9Vl^}i1qz&%bHX7M@|Dq5 zCr#QFe_YdS=VSR0M%3HFK>lA2gwExJ_gvg=4(Dsj#O6l}ZEEE65{AM>}kKUgA5v z%J(JGRPAGX;3Z+7`Um8*MI$L86 zV0Z=8f3$0!j@?!M;dk>=-}((z7#E^TMH$f%qv>e_Cj@R4d+*SYBzptK0@C@Gt+_y# zbayrA`iT9t`);8P2ofTGH4=l=SKt2)6i8biI)Um|9@4|hjZ7X45x06Lj9#SDzZvN^ z75&}>dE~qOZ3QSic7zu74f$SI(_C?EEL@C6?e=qH@1m2j!#YIDG@czrv!+N+5byp= zoJpycl5@fOLo11BPj1!1@3Lvh@zFm?08R^uPOqzlZP^9ie{=4M=Zz>Xv|-tFokbLN zyVFDd@)iUN<$?3|Biyed0%fcd<`6${v{H)$#HE!-a-*`T0zKhXaa zxP-9RKPWvYE`cIfCYR|=x}~@VE71hIeq}$Ex?a+`^Ix@}8xEOS;|*_W7NIbpJa1;H zx$DwGodG2*Qy0yhxG&DKwRi__i&R`f+4cugpX+oEh5_D{Zk(Zf7pkn3-@-oUyFckr z!Atj)_YI~DEeQ?iem$hHCBp+-6Ce(X?)GnFDhDG7ABFq7=^U_UWrQai=?`W$K9bAr za8eq^RNYF1r}JP7Bhmyw?#IVuUUja;=tHcm$h2$tGp@ z6ER{uP+dn-r&|^D5XEFf4bYSt~>++01>lo9jr1I~-J;BnMLmA3w4vt;QAwPGxI z??YdQ{S_XIYNbVcBhONUL%~pk_G|uAVD7?c5;FJcDW>rZ+;VZJ%(A2@O@y$-gSo*EH7b zf4$F91{FG`!8uq2QF5GL+nS7gE%P>VA#qQyG?|Z2Lp{P|FZFk^n`%r0K&r9nWa1;= z6YHpdZZuPa-Jzy^PS@5 z{z+|txoMj8aGregah>0)tbu^vQ_6W}YObs|=63ODrfKF}RP+gn^To%R$_`t*YxsFj z08u2>4Th3qK0bD-Pq)uoeOy^n!D|UvQ=F4Yk28!2nt&wp3Iz|-6reAFzpotYSmtP^ zoRE55LDfLCnDsDg#&G(DE^-ho;EkUV^yRg1Y4?p=do2md;9!4y?}WJ)spVslcXfB0 zJL|pJt+`yfF|5^+UPvhF`wwpcJl{0z7jd9vyf#61tUSlMLhcJf|yQLXwwV^$J;M}<1RTgYG5Zs|Vc-BWe~ zm?I`i{uj1p7~R2*NSkK2A50$#`ER1ZLRA|-BY z%B~iUw_8n7Nl_Q=$*Sy9e zXz`K$-5PZ3iS}CzqwfV`Yf${iRBgF%MJtu)G@DKW>fDh@@yYI1dQF4PzUHfL1*iMVxFLN`E#2anh zH-2tA<=kR4*A;rA_!@Y$B2Hl8#VF4>m$PP)=7R&PfwQu!nC%OxJ?+cw{cXfuY~s1r zGz>Fjl2B~8v0)QR<776t7d<_r9`>0(PX{z_M-7Q02M8n$z>Dfma3 z&(|Hsh~uQ}z7y)h4Y_k#jY24>FA9Age$KAxd%J2RDQfyT^BS{(yq0ZVAz|Nl zD{LiDvP=Og+GDP5+d$WBx5n)Up$m@DQun1TDs9}s{HKXrev>hpy}DYf@&^s1KF7{y&nh!hp}DU+H3u!- zp1HqDB?iJq{akKHEqI(o7fbWQ*W`HWgViGc-kY;kR!-OQuTdV@5~di5wNOoxAbkAb zyvQFdEfOSW>*=!#!#B6Casm5hCI|SEb(Z5HqIKZ=uq2PHA;HUas+T6Sm4bT*lYv&^R^y2_iButP>8U$a%M zo!+v-`|E z_u}B?1*Rl!k>jUo4bQGhk*pu%*H5%I`j?zq5fVGK`_1{!yGF(6#B-kXTEUYFa%9W! z^RVOFBu_8e%DWARJ$f0 zTjcm*y805Y-%|y3x5H1YiERuqYMv|`i`bp6_nQn_rQH~?{H@%h6t$J4e_5ZJvzG_| zys~z7aeAF4uZ|&<9c$^xU%T?sR_Npf^X5Ui-R6s!Ysc9aR z^ibC1(i4LOzr4hen-HS$>Z+p%2xKq-*T`!uVQ*wU97r+F$=0$E9n{RzzX@rBRkto9 z+PSj0!?NLgIh$hE{}oI3;CUc?%WZ=2=Z?~wku*JE#!fX118bE;I9JGcAx){kI*;Fzw@DrAy)pI3G??n8o(OcPW6SxtjX>Zs)57Mv z5H{ILmJ902!Pqu}!%LZWL5nxt`|k>9k-SZDn8yMrB-DShc_3CcW2AUu<%YKRaWO8pz2_KvFs&6K+SL7`;+5ljrl&qYP!UF@oyY5 z-8U^a&GXyosk7(B{@?>BCgH*ZvYIMdscpG^N-t{7!o?Npj{(5jx#`C@C&MRmR;6f% zo5^;xi+@Cj)2*Sz|O6gye|xci5dH-jlEh=x5hs-xIXnpop0nM z+kTv@`r>u5=vKFAt6i-3MnM7BZOMZ(S=7d|c8e4sOzQ4hcpm`j&h~}}R)fT=aVu|) zU#wmPNxU@>*^YIy+-S4lY+;;}Hjz(jS)@nzR`b1&8&_#icgtKC#V|_%oko_;Z5#h+ za`OVef`7h|JDbhQDD{r?=D*IzKw~0gB)vRDis34h55EPU_~u zHRH&V{zuyE5D)1=$Wea%t#xayXJe4Hp{g+(@LKO@%C2=>c;^U=l4`?Th&-X+iJ62k@O z@wWiDq1DuUd#-=JH;)7F7ib9wc-*!iywmN;?=3eQ0-Bnd&pzBHDJ#~0|LI8mfS_~x zCU@J|6>pT6);O%RP#vu|?imO-5-cwXh4st1aGoik^^aH7nY&ZliZ!`SKF^8T)iyKH z11N#bORj>ifUv{m)wh0wx^Py2+qr8gytJJs15)a~6x3w0PfUxr2+Kk9;)V)p`8QCV z$KJwQ@7DXB{ovk@(4=|qTsX;n0aM_}J%boZU~*%W(Ta(AzIgLo$X?9yT;^TW^KA9+PKo5UUh(X8lepCNhPJIlrGOcX^Ei20 zTSHf`wZgC_dS5#nx*8vJ(Q@a!T9RK%@YGa*;-P22gY+-D=+T zmRhKUZ5NHt)iPz#VoZBn?m42tM!pW!C~F{;1DiaK0s)PkGSPI6dD+kuZ~}Nq_K}CG zsS@R8-H`=*`gTomsDqYtpWAC8>lr~MB_-WzOG-sWMM4I#9{_YL{Hc%Puk+R`0|N%t z+)B$s{an>l-M`M^ijM`7eTV zCNFTg@uS{R(*@CN-z*(djlFp6*6oC*3g;KeBcsxK?)62<+$jK#S!pr&5ilW492^nH zbpH?g9Oe`f0z_+xiHXG)uA%tx z$(%?VC_u$_ohh2TX_J9u{#cZqnfPdd7*?GHUF_u_K-`tZHA_r5==Nt;{ZY8I3f)Mi z<-h3MqD$l&;Yi;YG8CtA;_h5vqg-Ox=h3 z4ud)$CZBvEUBK4jr`#%D*w}3IeahCuD&=&>y>PkB17dp>kRV*g&enD@ANhlZ^3`;m zy|i9!I?41tbaLMsba!?_Mz}}U4}j^Xiji#mY=64WhM6YHEjSvXo+F9OfQsZw0c`M6 z;YXlMn&c09qw6!BL-bu#d7|~-aP`?JGCyF+IyMzo-+V#&}7%{8P+R6zCt8POg+NOT_Hze7hflsA{$vy?-Pgrhl&~#1LZI8=MOLX zOZ+HW2wp$KG7aAhzM8Mz&3eq^UJ zdeNkvdwUL%{sst&M6J7{(Sfor+f3zbtmC-_l{ybx&w$iA+ymz&#A3=NJTwSh0DT(6 z?vg?c=bN8=xDfWG9%Si)p#c!ipvCw%cO@9GOpj-Ik=U@{MiRUHG}e2tPLstYgQ>m2 zjs6H6JV#uhbE$VVP`f)#GgJn1^TaNvO~!NIUw}cr>C&;a+JDF&ts@TNi8=Vh{#{}q zm}UIZ)>wIF)zVdfjkBuc2ZsRSfXl%@(hu=*$+$A=L8JjhXi$knH4f;Atks-4?gdm! zOL9Vwld_Tdva1j}Juhey!3ipSc z-mU~DtFHOJh|E$8;riS4mD%_!K5_fJi0Oth=k;i;`dbxpDiG0c z3N^@T1;|j|njZ37C|NYM*E#3+9vItC0XBJvc}-}_;Xzi?U~@s#{Nl4&-|5D>hokG8 zFFZfKr~0G5FARWx@JTZm`r%?Kv|>B0_pLf-HqY}9YGt7VXqM^sot`~3PHOBWBKUQL zG)CR5qc2N_n;BSg{2isb*FC6d%+fH@Fle4zYJRsijo<5AU2{Pd)&w#H^xG6l)F~4( zFF4cfj5*QHa3>iie%0!J6Ef?2XVsl;zgI_buf@ef>r+Y5wszz8Hd|BU3m1nCw_qoY z6Pj^a`U@|Vuck%#2?PYxLC=3W2uo3x))R8(>n7Ei*$!sLkvwI{=cV&mhT{*AD* zu}7uZ4lLt?oo<(|hCBi8MEITrTEKLC42sGGU?H**(ybXLen87{HYKv`Kn%kI{ zNf4@ldnjxlBb1X7*?#O#k?!rFiBo&^()#H}+^6EU;HLVUmA&eL=2P>BJ}dM`MnPC7 z=h*@L=4l8TKtIVrXr?*I{8Co8yWgVOIRA7RlFQc13RWKr7uqsP4oJlmLjR}g5`qpV zpQ);rT|(mUa1`s;+U_nk#Q(bPDB*CBNbTtA`WYKbF>;Y_Uyt{wb%WN~v3;xQAWQZ<7rS=l{fj6nd=Fq`vZ3eU98EGVIonxZ3EO_{Q ziB(aI9nMGhXL0I-s{hH>)TS8V$z2IVMjJ?z^eMwAqM)Y9WI&|UyCw!C!Et~qS`*j0 znq@b(&IHf`0L<6ebc|tO?E4psSt9xN-dnSOY!$mJERpB{*?Mai|t+9wbZC@*In^)5W#6GaYD(X6|6n=CQUGgXTIHo z;_1u`s{cG;&`?SWHHae0*&1!Hz0!F)dawUO&kU8Fm{onDQ?2lnAGb^TQNyrR!_0{m zAV)DNy=Y}yMX!huN(6K(3}oUYmoE-=b}1}hToe9oz?W1zl+K_4ezK!o@}X5q7KQ>O z)xS83H*!AQSsJTkWPtfSB2&Nkd$eR#(jA1lIms2MdtdhiTf?(m%Y?vmiqf?-@YX94Gp0jb!(&SY-{g&Y{2CpXeA-G{Z0rvPDB(;@XA| zwn;wL4hK#PR3iC3A6=(o*h+%wON8gMiU6%uu_^}lm(U(SH)O&|ins9Xtp=*gDXe_r zX7L#FoqsO%-{L=i@-4e)q;(sjDzRP7r=yc(%dp`E9OEo?lvfu0tbho}?4bns+Y$WS zJ~+PWtEkv>{#W)QQ|0v%GGZ|5-D@VE+^uJ}Tzy6No+$0vHMr3WDW()t1WzrvmzzB5 zXdIxND*B@a6=5x5EjWC)6#dCtRb)=(oTzJD^EwxwRQ!Nz#rLqTuubyB5; zZj?|G(J3FZH7l*SI0i-BOWtGrljGUa_ij3Ea2F#FCq{9N}u$cmtO-V^qgkX1MXYgJ0uw zW|s`034<$p4h7z_F4vGZLVPf)Az$?7OLomzc<_B|(eR#lvur7V@p4a=6wSsMp56Y7 zbqBbH8H=I_^@vA$E+EQ;2#9GIc_EJPr)WA)s5NbQ|o(dHfX9IEc@42%AM@}Vow2cC02r);Bjiaah6X3)!53W zow$m*0%`8K>PyJKL>gJ!UHb%s?u=(ddcb`eEI1T^L{IvT}ljhN*W(4WEb%a(3$i2om)_s za~6iCr;SCpY43dkX#H0y#7~G6Y%k5T^|$~RNjRus+}vOuE2zP7{kB=+grjYfKEW~N zvqo4c@bC?w2f3J7X;3IBr%p>9p&x+S4+S+VKFFAWs{bQf7WUHCf51Q4CjGOC!_{&qqL^5fuvSaW6QG9ti@U4pP zq4}8v$LtXM0jtjkJ)oGv_(~29L^zI01?LYa?6(F^aw>+2@M?{|7GVEmT^WV|xvLZ) zSONMEy$k;?xbGAAH|pdPN1)4@t2!-W13D=QTdz-)fZBoEsXozFof9N?7CsHqtrkxr z9(@G>Wb?~LYAOjav}+o4Vs(U%)LEoT@GNs4CJcyWNc`d&Dre(NbvTwDk`1TZnU62@* zX5g|1D!x8C;;mXap#zloJ5Z@B;y5+tTK8jr9Jk0XH4^!9O8TGIF#Nu$H!MC1L}-ub z7t7jfNXjxk~v3D*lmSkyQe^Mi6vPm_F}eENNx z=@2)4hjBNXYHtrVxOhW8r3p+?-y|GRYQJ)2;$9yWs69Ne(1Ew)n`TZZ&!FVBTXqnYX zvJp(4BW(`zdAJiOGZXjz-P!FQG{!kw5TWQhe0;PEJYcn{s_(@Q2kRnLImD`ZyD-@h zg#OE=frn-WgLab!x;Fwan>FcX8RA9^7e6wc*6Z5BZjp^$f1jAWGnsq#o81=D>83lY zH1E=;Zj?^@uVKUviba^#=Q1~y6mdFl+QpefzVK^zkl(`nqAp2NlE^=jv-My~*7@mo zsFXCWoDl&KmK3-102kVuNqnzh|Ksqi7&^i&T2x<1uDjR`A5e>uu`dvU^FBKqk7k_J zV=U95nAcMF46pqZ;uCZq9`gO3h`po+70CfAA>33Zu%63ipTRDU%rLuOgG;`{nW^rn z+^YfC<4n)8i1AJN{8}yg8;GcQ)id|WEL|EpHw`CvfKboWVa%>xL`|x-7fVqW#(?pz zJJO#cDV$aWORs3+Wk|qXv&_Mlu~}-u*s9lf&UAC;XPSU}xE84mXWKbG<5r9wG>n56 za0O}!w2$-y3YnSXFN-3?yLz}ymUus{{%FLa1X6$3Xa-b$EptLM&uNRh53D-K+~N8s zK9oj2jWGlx#7@!iW;8>L&$DEzxgHUy zPU^YjFj;sYPz9{0kjq%{iF9 z#&;v}jD;pghNMZYR-nv(AJ$TVfdXh=GQ7!YO8-U-B7wBC#oeU(QJ9Zkm~UQ5!@N!4 z>fi0%TiX6_Chhmu!`8zNlN*dU(~E2SuKr@y-rmdYbtY{#lW6wYeVV__Xbs%LfzU6f ztS*8^v)F%v!AM2mJEPw885MOf&+)S<(8MhAj`iW*6`kqq5Fc0+@gG1c$cj&jl%y9`Cywx5_8EkG2)ZVH!doj z5e*T9`O!Dmf?HP{CXo|y1;EkY*kKP`FJSk@dEp?>l2a#u-IQVHz(P zxh(0 z6?}cW^K*t8+t4J?!j9LtYPzIRpxT>X1_?rqvD78fnCSxdjYM!B&wJ3;dSRb1`{C8L zmGSP%R)d#9_tDLK@PrJLYuX;ntr(ygsBf&ufD0Y&9-9eCCTyKC3wbyI)YBqcYx!twkL!$`qV9aNe~xH)4#bzTP2T*LdQo9a_lw6me_(dXtz& zu=tv;3$Cy8+DlFBVDv9WA71U$!cqf1VVY(T5E$5Ez!;dyCX4ifCzV||x#f$yCep?Z z+~;!&mBn5_SuY}7w|l;p z55+<>5Lg~S><}N}AiUe3NevQr8dssr8X~EzP(Z|0n21aop;u}edugz7c#|mjkF5@Y z*HAkA;YV|k=M={B1ZJL(O3T*kWZ}{Klq7Hq?AKfdgA^FPpTtcW*P3;Zb^kY@uj>z1 zQ0E-ens*^f4_^G>*D^85lUMH)*liz|ofGxEt=*#SoV7n|I422+4NFT~afGGJdwT^= z9Fx3){48dMfAFoj8fl<@K5ywmhxsA~xIhDmQq`D6LkhF!+V zmK82YtB*83Tt<(GH8f}Lb}M8EUDW=gQU!*`HT^M^Z>@l`3X@?k8lBAGwUpM|5UN~R z4ZflEOh+SNwsRN!hL@2j9x;fAn;bI3yYHf^4h3J zvDRg-79ZQ#;Qopi3FmCV=vW$7HV%j#-z^*qBUGQ4CNXNDC_(k9qBTJCiTq$Gp4R!4 zK0wnTGV8^{?B}2Qn>8<1$@94v1wt(GmYA265>-_uXdoO-autfE)*-{VO~dyHDA zOwN4wCZICDegYi2JII?Uh*9D>KNKzb4;}X?QQ%4!W&?YB>N|W;eoUm^ zDzXD%huOkXjhy^UY`W8Xpx+eo_QRN+ni%<}2hao=UX${%k-^1SXvNU^M7t=k3O?E_ z27A{N?lYLZ==;u^8PDjDG*P7E~I~@%|A(q3#i zw{Dhbbd;FidsGm3EJIdqC7}zp;T}SuGs|(H^#JZMcEp)Uf&Zqe1zRl@-Xe)`cI+ zdpfoA=W?#J4y);z$KLvLRoUHoxi5W#0rh0OFE+zDNgS2nq*b77S!fE$jf{!tA$}S2 zKXd~kDGoNA2%&WBs$Vt(4{SpC?p7jsT}_-GkKvW;U`4L{Y#M?f9p9QGPW|aCk*e1J zVJ`Ig;?Xt2g>WDpXgB`{4~9VeNc=vUgbO_}gm_|nHJWxO>cRPo76zw&`3osh00QoE zunP%S)fc|k@FozRfG&B9X8b+Hl7#&%4GyTEu(Rgr_@V6b!$X5dplg%yh2XLSxuuHQ zA+!H{xX`fs-aOuo7K9kx4UG+1?6yBiLzcbL&xu%uI_> zoci9In3~qE1h}>%*-j4wPulIF5e#@Gmc4n5q0irle|dhMuB7`Co;dbyQSe zzyD!qlo%1|8iS5ex?@nJr8^uH=?3W%7#b7=l@KXulvZL81*8$Fp$4V9Tlnqqd!OgI zcm38~cP;*a#X4u7ea?=aRvq=|?ii$KZ+@%G6Fqism;bHU$Ehdk({3~lJ$Lr4(Is9fO|#sF@p=Zc z3r)kP;!VLMQpUc@K;WJx{Puu`vJ{kd)b6f^_J*$bdk^UwaSu(7^dHtk_!izf?P z^*69$Z^s=QJzDg zdJd#VF4Zs6ZQRIxn*v+a;y&I}yK}UU#xM3jBw;5i;?=MRId49OpryJMcKaSe zU8dr0#TR7UI5IAp;9(^<&85be7a^_k+yeyZk-ZenL%OUKb0=oEJ9hWqgA`slU2-by z<4$Mt_4u-PJIVS+cNGzrR>?MGi5~|*I5kLo8ve*EmfU{yaNWVXOE7Ba0H!*yy?@J~ zyxF^Pv4mzQFfQ=j6a-@R#7y$5$r^jKtpoR|L}gz#?Yr3k;gHPca1owpmH$7}CZZ;b z83{ke<(m4~Ft4HmWq1#+J{sr#vC^fs;&E%Txi_fAmZ4FYw%m{IM1#^sA54Z0ld~b> z8Jt}B7~U@o01$)=sLor^`*+#{qMc05$6DR<(caKwG1r@0NrjU>8b{*&qh}`5za_8| zArErzzCxDehJ`oW1pddbAIVd%Kl9uxF-w}QHhsIh_4-je`n92BVwsyiWE4yD=G$BD z#|qkIcadS(ar6Z22R@VZMq2`T^OYkS*9bY@pW3n}*VQ{j7ZT}_m$zzAjX<={XV(`{ z4FuYFlK}>n%#P0UTVn=WO0)7? z(^oWe__HxY@GwQR#!y*v0VbXpn7KWLF!wuK;#(nx4`f|S;0rJ0^~+hOWC#?yLfI;H zSJ!q@C;mTj5|4dOkirwtdP5ei`x`u63!5TPX+QhQd8cEIw-p~XHidtQWcryye z!9OExShr9pv#3DE!+Z9d%LYNXm>I}CtEuYPw2_Q1KDG6DueKsa&FmaphhG{S$As1>V=s!`ka zj)T=Sp9$Kz4T-0)VQtkK#h@qQOsBGk1c#%#K~$pJ<(rJFACR|Kc;wa&Ma&#Zo}Sl%;fbsiu(*dGz8^oO2#CYWC_F2b|xdB~81whZAwE3R4Y z#eQhQCxW?`!xy3=%W2R7!b-i+AFCm10SOEW``F~i-S?zYOcgu2^hiSvUGQwVdQM8+ z=)~>(VvQV3xraT^hKWlKih%t7^ZSfEE;;o?(O;@6DxuZN-pwZ*HD@eCV;QrDF3*mx zrhASKF1nn4(F{L0ndg7Z!#^x4Qown{z2>)X3AytjxG5OljQs4%5tFxtO3M?3EarGj z-kXg&(fsutg3mEf^y!#cm6Wts_EORA)qbzn{A}y;7VyG7RK+y>SXSy+4Tq^*)O>#{ zt&4n}F2qnFUr+649v~czxeLgWcS9t0l$uhZcxkM^N zr2Ty3PK~sghUwBP;-A&&`rq^>hSS-P6s=NhqkDppBueTc?N?(ur^ZBP{}h2Ccptqx zmJ%twhOXz%%6cr_?VT0Xd+(Ij6|a8K!B6-}WPmX8GLX$J)2|NGOs?*=XPP~Le3PgQ zj%Pv8y}Ftt$&+uA2d1As({p31aBN;@5^;j#Gr@BkTQ~7zTW_g-CSVTBudO&J36yP& zxuuTpl;n~%VApeoMY>?O&?olhffbGK7`lC+m0h`g~Z!R2*#%5(6PE!Xy1WuUYRfY z)+CXmcNe$yHQo;r{BB%wf7Fp1!7zS|AuhSw z&BsT?4qqYk4`ZRMcYDx#HL`1XlqY=J+7M%iQCyN}T{sXpPosF*LKWI=S#wE7#)EP1 z4Mca|z(6tTJdQ9oID-&dF+ z_BiXIlnXDZ=cuJLG_={$xBMrl_v8xQ;?c*6tyP6eGga0D5%?7qV21%Xn@m*Lb8=9w zUb0vyXK~Y4JHyLy@#x@cA^$U(SQ(i%$=z`KAkMwQ9NY7*oAOD)#Y5gKJ-i$ek}2#_ zV`uMyH>ai~T8ocbB{OVvzK7w-zY5+KpWO&Y-seNj@ftc!&d$*PO~=(s}3QOMXzU74{=$B@63vPClg- zF>Ncn0r?b`ptJrG=hXn^FDmxQ9CPb(w{_n!w!HEVy?&qdOmJ}^_i(%LEzbI!Vbu9* zVodd;VGl4|v6h0NJMY>p<#*qw*!Ndc7<81?Pbx3A7brQNfAy}F?;U4~E0U0_)Nw)p zJEJ2GE;pjj1zSA+AnT+vZ10x3Or7)Kz>Ck5h$SDxiyJz;JRZ=&F;CP65H~3T{tRLuaBPGkca^n1XQqOOKvSEDv6jA-mboYl|m+`QP6 zqHyA6BiT^}!g~%ranklFLZAHAA04Vi?LS0~Sm==Izip$qc%N}N+MWS3-0fOIm1%Icz|6;Yn4yy9GBy!PziTE|2^Q9H$@p;8M+xP zSUu6VwK5J%vA@npLl}kX%xDZtT^A<5+gse=k~W&LH(jO7HGEh27O4ff`ZmU5p{nU;g@}DP(9f*(P(O;O zT+m0Uj<5qq!U(rg=;%3zWAFA-esB@_S~#YMQf<10VYSN4wWxVOK@1F+`x@GbqGfNZ z!`=&VK0|yl|J=H6zrh0{3}oPoZWphSQ98Wm9HW1_UN~@JRzEj0eCn0pH{5Qg=}H6m zHSCf-s|He@$($gZ5~|NYc<-6;R>?Gdy`#XhZ>;789zRP&OW{>Y?`ib3brkfTfF$gt z=A#!*t~iPn9*=;Fjb>$=wJw`v_9P+jisuK|#nGy~W+@GmwVX$tkEI6W)3QO}n$}8R z!Bt73Rf=vbA|u7#R+yBs5-c8gp!81YIOu5q-9lLsxS@AWeb4#1T_xvHtX$g}$(y%# zRcrWO(=b~R=2+E9}dPI%_z1MB{etN{k^Y( zYlway<=}A%)9g2o>KqVmrFEU4q8pHreRn?m3#8e27a zr)SO=SAV40DS_-E4^{_UAlQ&)VTA-b-nZ`v<1U#!^7gd>I{`%)L1|@mn47{I8PmH= zjQM)8w_tUs&f7QwB1=t*)EO7ReSI`VY|s^1+*Cn4SDus6 zI)*}D@xx9|JGIQ5+xo6Y?3D?U3iO62ak+{sgt2oG>f91FznN-}FZ@6R4S+_D-j@}^ zWIy6piVCY1y>yb>A{C-e*H`HYBaz;0 zXm^m89iUJlY{vT-GLC<$x84b4Q@}#-yLERYP}_*l?1F#Im>7g%XVj)Tg#T$2jG-Uo zbf#e>t#OHtRDT=2)V(8Z&p{u>^UMO!0#?qwlsQJoO0^uZXK zk?I0Tys)ZJ{{i+fL69dW7~pSCI1W853p=u)PR5hbtP#w|aNyoz-lRtQkTroN1xRc; zg`oEEMhFJSsz2d4W76L&A*af7oaiXD71p!ZKQ#)!p*>x%Lam%w2XUjFK36R*^KGDn zvOJ4I0c)!}Kh=v~g1ihKFSifWWX4_w%mU$mYVwwh1fTqOCzl&m6>|E4n~FCi z3(W-Ec?(|<95cfjWvhMjZC)7NVeQB7$B$za>0)+ZnaBlD08bQUnS_kViMy24gZZ%( z2Dafq{Co^qd>=#|aL07`L*WHz`WFxSSJT6-1U27gFP`d#xWvJ`SSm18`uvrE<9NR=iN^W`sh1jc0;NkbcXQ(X1my4!D(p^TxQ7+(z`7wR1&Dw}^1OUyR{L3wxAxSE|Aahe=ISM} zHCmrntx@6^kaT!5<<}1nM9c+3zwbA`j8UmHJm!KLZ#3L3MwzX91idITt&nL2Z9$yC zEqEp)w*#*@$i1E5st>}yL1q8Lk9+ue&DRX9-)I?KG9~cj8k6sQ2p70(m9~LEJPhp* z?Ffk;OO2VPOV&g7TaW}suYdnep9vualSbC z=EHjwqYI)|iSUIWq=VJMMr5_&7P4`GvlO82t}c9_e54nhTj1mDqYA#}<%VTFz5Udu>Eu8vjuO+QOP z7_YL;eDd{sjK<0()nB|d9kg%&-C$t5mz@JE1qTs-wxZKDE)Il`ZCL%G9z)<$WI+Q?#cq#=&#JkEG|)Kqrq0 zu*L8c!)rK=fYdB_y;q?81Iy<3OR_N&!-Rm_vTyfssV8xDq=HDD|CINzj3DwfC4OtI zg3sl-*J_DhSSE%B(0;pG4t&F~OG0lT$<)U8sLy^#Y_)1===0~B#9td#9v}F$Kej*L z5z~1yt7f&IumHmLv}=Y0raVUpC4(kceORXHR~Rb)*AU7n{nFdaJmKSM!X_o=nyKJ= zVK?AmrmL?}WRQDPGudGC(|&t;KFs~5_uZ(`5OZ`~*K&UxTmt6Ky5)kcK;vWYRxA&` zjUFiz_>QiS!NX*0zzW*gen?Ya3jZl7_= zw6o_sxmsL(p-Eh%_HHd3srKF{EwB)9NsWPX-5a3o&@nO^o7A`44hs*zuc>+KR(r)A zJ|6uNB>*0f_Fw0YTg_B4a;2Vesk%egFj;ds?f68n5-B@*a{Z^M$1|PvbmCV0Jek(w zny$`Hc?7+X6&_{O`Ps-fdL=hE`02rTO`tHfL8!OTs6fmh_pMT%Kh|d1h;r76a3;-7 zk3F(*fm#nj2fWgs5BBOz@$9rJ?aSQV-IvBbE7CHsoZ|26l}T z%?~=a!tM~d%4=6n8K@4wRdB^iZCFz=FI>>)8GgraR-aT~U%yZuX@4*4B8U^jJ}0nl zQ8Ef!QDWKcH?Mz!49IXa&WbJljZ53k5P$+GJ<@#p9En!YE|8XirSC`Hef2EqSNr5 z84@p^JtK5(n7f|N_o-7xR*SjI(JyEaj*zO!m zxa@ZER&4!>71S{?i2lfC7hOsTZWN};TPo%;e!dpbJ0y2}GON@b%#iNrw!_n z4&VAe(Pl+b@=KEq9`Wk{LbtQKOYp#am~t-7K(OFBNTxUO1(d;`u|4gdPu z=gnx&EG!TW+Mb^s=jG+G!*1NXX&q-*Y+vmC>*S(*uH%pInd_*r$btVMq3yMov+hj5 zIIondLIK6Tp$1q-Zaj-3)Mt@;tJ|~fRUqDmT{&dWIouAU&d1>62L0sx$nszcdF6kJ z6#WvvcXwYG7Eby4717nzl`i97s~KGY`2`qy_EVNdCJ*)lA1iPJT~Ds=6;M6;wLA_6 zs?G3JV#vQO@B3euklZ`s`E@z8*_LglLA8d4F^5PyqWcNhMe)7CVEO1iulnoPFY)C( z%#f(_=XFu=$I_4dOMxeQ0FnC(;I{H1MQGs06G{LEVw)4YsC4?*PU-_2+K<1r@$mq@ z&t}e$Dn>0qqK{pC6EBZHZ0ByNK%t&gz6@9n7Tg^)K|`W`drw52Cyh4YEK=U3@SBY2 z^|~1gQ1o{H#ryZ~H;UeX$@O4VMNLgA$FKMxKW|dM22G@QNO-6c$DUgmM_%`aJ!;@O zJPdFFZLVU_bv^UtANbr6oo?r^r?*7{_Dqld5CfP~ktA^b>o{ zbL85dEm1x#O8)T3HQ{Q?FK=Wx_8BQCrn*&8p8@oW00)3aKOq(RK!zrv1@fn`wT4Ym zk25%cIUjQ)egN`bk)$0&Yd%-I`&>h^X7vM*EWRT(GsCYNaGsPNh?Z6*sSFKl?2Wm) z#eJf1$qv0tIgbEAXE|H+wjnzYgP+#NhE~-g6JTD-&%dV30crUk-dYKpEiD@JAO#YU zPm72oP6pb*0#44c0wq1IC~uv#m7B>?-cphC$qp}?AwSZx$3#->e+#p~Lb}Wg+PN%& zBfj2dUD4{~ajo!y(F%;`=Y7YgLXT*@MFzop5|b!iskfeb*szx3d}d=~2-I5HMnu=n zSYoPXPh7OtBvosy!5~P=glPxpgGDIA%8kwkQ}8Qu02W-r)_F_$7#=K^ zBj)L7&MU(+{8L!*5<58bK($8nACnkzx*~ZOp%_XR$`o=t``5BObPEM9_({JjvL)-< zv^WXq*;q=PvHX}*h>3}>&R)Q0pZr;rv`j0~Z=8Yk_N8vrWv`K=Oswb60G)JtuM7{! z>1|@sMgoR?L0GPye<9Qa&ki8Oi{n252_Ah_-ph~HXhvNRjg{f7KdJLL_cnXyDl|HW zmbdJ^1!KcU&Z9g5IO6F`g#>c15z+u&{|L^#nq{hFi|(7*NgowKT~y`1?uD6Fk*F$3 zJ8qIFpm(1ede7c_(IlQGux}K3_^_wO7B9=vI8aL)d}XGCVb!ZZCTyk zgB>mnexwTo$T_weL1}T+cHr}Lfb0dnS`BflU=O{rvzr7gXhoQeGU|6^btj34pyP_-) zJu3(e*1C#$fWgxi7H(fE;PZu9VIZgF;@=O{03PV?m^qXF?=rKMnmCg4>##*eWI1F6 zKkX<(>`N6}|0v)huYV^g=hM2=esn-xM=qiKNd35F(}Dkznno+zoLCINuN7bGKKl%5_$eHDF8O-Me@6Lg@0!eH<~t zrjz<163GC6NN5?juRkmSP!HB%N5tvml9JFz+rPi}^(`ddW1X!TwLor6HXxLhp|rFS z-Sfq(WfOc*w-^=rzxmoKiX_TCbb+E?^8rCHvGX?bt}Hb-%m<}{LS@9W@EtOTz#r2I zQ9T8|pfth!IjQsnA)6rkfz9w#^v0XRxy&=cR`VV&{zwY+S^#eqOFJL+_NP2Qv25b`z6>8b8V`}!+ zC(_$OEomDa_D1?s&Rw}XQwM^Jq3zch5(V2 zyCB_)51-Rnl3;!KiD=;j#FQ}_4Dycz>L}xMojSS~+Zg-z%FEq9H*N5LHaD=T6Ki&; z(`^Y&>kB;&4sW1qt*vl69_e@TJ?Hq`6qq&DHwFH!72s8Z19A9oHl z+tXc<>YFBAR7}VMCgi(#`2F^Dsd)IkL>`BmA7kx_TfsEK1MX}BcRn`a; zF#Y*@fnezYHVOxVq`m0iN&gigXEa6mZ52z5jZ2fmsDSgQ5XEB;v>TeAzECApkI0uZj?<<9vtkN$#j1z89@4;9M<^uCLYU{I;WorQyfrnE* zhWgJPIM@Ov6P zlaz|Db)&ZT2%|aYlROcPjV161%*T*AZ4-Q@SBfwN{q-&eX`gLqhF9Gm z#h5O8awqNI*?T7d5negNy6^ ziiwoZ#@BV{8m$HWJDof{IAXK?gcQpc!`@Ig{F6VfY-iKJ`-kN5Oo(CSV{Yxu>n)|LgiV>@5xA=3RR;!=1-M$h+&YxxJZEb$t%| z8&hu{b-z5W2`YB-+v_fC+-nn9nDQnzjsPWBk$R~>^*Z`;clD2_kd;iUoHUUPCoUoW z_jRuqTuvefh6Zqi^7SS>^4?wzNwqTuBJgBtH&1G_i5+i+Pz%1?QtIYrx*ztIjY{15 z8o&|F>lvb1%e(;ALb20QDm^~>B(TAT_9fsluCR-j-$o<{sadHx+yu7O=bw&}|= za^yt!&Vs?HrUyn=9>^OM{k7T_00PP0(IaTnikYqa`P@QV9 z=f8cMxL~vAXUyimzxXb;yJd`sea}wQFaDlx!a84!S#JEYu4Sj(8@#LeVOYUcU(1zW z?%H&yi~M6MMsK3YK+<1+Cl?si8aNQR5+4vG;!zD7Jc(-A^^xjOR8wAP$mkR74LL$E>v!&IsE+ObUZw*P<%M0Zk{ zLK5TQr!o_T1Tvv=49J*}Vb~6z>-DXdg&l|XSO~I*qu>7z(GjS<6lqU6+8p8q6L{6F+&>^t!V3RO zF;&)U@&A79i4iDIhyZclAx7r%*yFfuO_T}6_)U1F<`esB|IsnC|-=^Pp|5ZcNGLj!C1!)UH=#ubk3GQgDhZR;^>ICFIINwr2nW1J@7j;p`SymE&-9O{PG0y#(8 z-?G3RXc5(c?`wgxqP9<1E0D$iW(TC)QUURkcX?kl%$^F>X|c#ZRnd(ZzAwUC==6)aQ5&O@s= zY^#M}HpcVt`#>34x~e%c6L>C_6>=KgKr3u520v@oeLG`wVQE?f_jc6|~aTmdHR@D-BwDT2wx8go!vHEe=XFZLden zDLM|kiPrFa3XJ7~QR^0xb7imo&Clqv^P*;O_uBI6FXkd_l@;|{RsyWk5e^;0a)uRc zGYYP*^k;5q!4*Y%Ot&Pdp8SsPggOd(AaCL`;1_H9zdIb1+mk)%_%Pj(1Yo+~niz9AnZ|7~Po&)8PXNL7QSNUrXpKPTWyvhKTb4 z&l(pAK($zuosaGeg8Et@t^_1B7$9_%fm`)5x|3UWJ{MurP$;dBD0jwa4c zdk9j+c1pp1w3V1`B6;7gyZnB_w8n-ZKGmwKZf&OzJU%9xj5m)j7o(0fPu|$YJ&zy$ zBAy&cIRQ^Vmmh&LdRU&+l~$WH?`c(eTcwkBrMuYS#mcv*PYRs$W)1)YMK z0<)_r4*COsijD;ze*8XxeS9(Cv>jnSxI||$5r`LVUXwSzI{MrKooB*`B?*a;J-r4w z^lg?K8j`=~n<_^dFfo~3*kFkkX76|K7ubp!t;O>XVaQrH&dN62mRz94&M%zv^#-tA z2PqHrro^(<{Pj)vjG$BaAAh+TT*HpfMU93y&eRdVLRitoU4g)%VyB`XQ6IK2{@gIb7!`q9l35@0pDY+? zC8V|`DA#Fgdr0(%P}qwJi1{y+{hJp~*81@uzOLmdGZ5aNX^HHbxCw2zS`VFY)%)S^ zdpQnX!GwGm+Beqj6JDsYiClfVUJ<<1f9f_Mdh}J~pS#TaH@BaxCAJ;heIGfmd-2EN zed81~xjSSrG&T-i(iF32!AgScQCRzP_VrGu@_+6D{~R7Ql%f*!X8 z_Sw}P{)nP^lQu#GdK&gmPvHZi;#QCuP}IHSVH#k4kJE+7&zfnbE>!UKXAA!22o`hZ zNS0;e*<;zuZ`P(sq?UdyjzR0>3J;RMYvIpkv7ITQX?y=Pyq=YvhS)%ISGlr7ddno4 zq`FSM?GxU|wAkp0BW?LV(T$9*X`Detlpl!$OvdBXN(GK!# z|BNY@+)Yp%cixcr_aQ{pnE0}2aJoy7x)c-s_lm&y@AJm~o{|Nupa1jl#$pCkCph_V zaB^Q86gsF(SP=ozqPHgE8cL-Ib@iw*Qq*S(EcRtX;5;)N$FKbc*4Bv03GMSoiC<`xOI}Eg)0gNM=Fvi&v9h$m?CT{ zP&TohRj?~RuV!n2+7(HsaQ(Nqt@BS1zBJKSd@6#?SQYXR)EVd#reur2AW2-MzQ zO7^85q79PgA?N5ti_r}-Pr?(cQR$Y;^)PhyJ#ZTua2qtB`d*z}U%vU?&o?#DD()CV zuWKFWOJPK2w#@y8NZKLJc!4_@YP6|;Zi;0<_HbtHNF#e-6%KR+hm1(oc(_!^G5WoH zkT&RE=)by$934=nl*mupfE57tN~j1DiU*MPTau2S^Kbi!($4ty~E7I1W(Bb01sE;}AF zy_M37?@XNxhyA*mY>alrdTAZEgj&XSHe~J4LthR2WRnNog#W9XXZbHBcm%gnnn)*! z=OBlWG)AO1$X8pCu;N(|-43-RSY$wY`*+EGqg;7P-wCOOEDFyqg1v7Y%djluk gf#RlruDoCiH~7FaOQ^~R&OF7tudInESF{TIe?Oh$W&i*H literal 0 HcmV?d00001 diff --git a/docs/source/_static/theme_overrides.css b/docs/source/_static/theme_overrides.css deleted file mode 100644 index cc6ce10..0000000 --- a/docs/source/_static/theme_overrides.css +++ /dev/null @@ -1,15 +0,0 @@ -# https://rackerlabs.github.io/docs-rackspace/tools/rtd-tables.html - -/* override table width restrictions */ -@media screen and (min-width: 767px) { - - .wy-table-responsive table td { - /* !important prevents the common CSS stylesheets from overriding - this as on RTD they are loaded after this stylesheet */ - white-space: normal !important; - } - - .wy-table-responsive { - overflow: visible !important; - } -} \ No newline at end of file diff --git a/docs/source/changelog/index.rst b/docs/source/changelog/index.rst new file mode 100644 index 0000000..534b1dd --- /dev/null +++ b/docs/source/changelog/index.rst @@ -0,0 +1,85 @@ +.. _changelog: + +Changelog +========= + +CellBender has been publicly available since 2019, and there have been some +significant changes over the years. Major changes and updates are denoted by +a change in the (first or) second digit of the version number. For example, +major changes were made between version 0.1.0 and 0.2.0. Small changes, +edge case bug fixes, speedups, and small new features might bump up the last +digit of the version number. For example, the difference between 0.2.1 and 0.2.0 +represents this kind of small change. + +Version 0.1.0 +------------- + +This was the initial release. The output count matrix was constructed via +imputation, so that there were no explicit guarantees that CellBender would +only subtract counts and never add. + +This version has been deprecated, and we do not recommend using it any longer. + +- Imputes the "denoised" count matrix using a variational autoencoder + +Version 0.2.0 +------------- + +A significant overhaul of the model and the output generation procedure were +undertaken to explicitly guarantee that CellBender only subtracts counts and +never adds. The output is not constructed by imputation or smoothing, and +CellBender intentionally tries to modify the raw data as little as possible in +order to achieve denoising. A nominal false positive rate is approximately +controlled at the level of the entire dataset, to prevent removal of too much +signal. + +- Uses a variational autoencoder as a prior + +- Computes the "denoised" count matrix using a MAP estimate and posterior regularization + + - CellBender never adds counts + +Version 0.3.0 +------------- + +Further significant changes were made to the way the output count matrix gets +computed. We compute a budget of noise counts for each gene individually, and +partition those noise counts in a way that optimizes the posterior probability +of our model. This involves solving an auxiliary optimization problem. + +The input arguments ``--expected-cells`` and ``--total-droplets-included``, which +have always been required for the tool to work reliably, are now much more +optional. Extensive testing has shown that these arguments can be left out the +vast majority of the time, making things easier on users. + +A great amount of effort has been put into refinements which reduce the failure +rate on difficult samples. + +Output data loading functions in python have been distributed with the CellBender +codebase in order to facilitate downstream data loading and use in +`scanpy `_. + +In addition, the code produces checkpoints at timed intervals, and these +checkpoints can be used to deterministically pick up where the code left off. +This is extremely advantageous in cloud compute settings, and was built with +`Terra `_ in mind. The checkpointing functionality, +together with the re-written WDL workflow, allow the workflow to be run +seamlessly on preemptible GPU instances, which are a fraction of the cost of +non-preemptible machines. We also hope these checkpoints make it easier to run +a workflow using Google Colab on a GPU for free. + +- Produces checkpoint files, and the WDL can run seamlessly on preemptible GPUs + +- Computes the "denoised" count matrix by solving an auxiliary optimization problem + + - We demonstrate that this approach is superior to v0.2.0 in the publication + +- Several tweaks to the inference procedure lead to speedups and better performance + on benchmarks + +- The tool produces an output report in HTML format with plots, analysis, + commentary, warnings, and recommendations + +- A ``metrics.csv`` output file is produced for those interested in running + hundreds of samples in automated pipelines. This file can be parsed to look for + indications that a sample may need to be re-run. diff --git a/docs/source/citation/index.rst b/docs/source/citation/index.rst index d55e2e3..65b8beb 100644 --- a/docs/source/citation/index.rst +++ b/docs/source/citation/index.rst @@ -3,10 +3,13 @@ Citation ======== -If you use CellBender in your research (and we hope you will!), please consider citing our papers: +If you use CellBender in your research (and we hope you will!), please consider +citing our paper: -`remove-background `_: +Stephen J Fleming, Mark D Chaffin, Alessandro Arduini, Amer-Denis Akkad, Eric Banks, +John C Marioni, Anthony A Philippakis, Patrick T Ellinor, and Mehrtash Babadi. +Unsupervised removal of systematic background noise from droplet-based single-cell +experiments using CellBender. *Nature Methods* (in press), 2023. -Stephen J Fleming, John C Marioni, and Mehrtash Babadi. CellBender remove-background: a deep -generative model for unsupervised removal of background noise from scRNA-seq datasets. -bioRxiv 791699; doi: https://doi.org/10.1101/791699 +Preprint `available at bioRxiv `_. +https://doi.org/10.1101/791699 diff --git a/docs/source/conf.py b/docs/source/conf.py index f41e45d..64a3214 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,6 +12,9 @@ # import os import sys +from cellbender.base_cli import get_version + + dir_, _ = os.path.split(__file__) root_dir = os.path.abspath(os.path.join(dir_, '..', '..')) sys.path.insert(0, root_dir) @@ -23,8 +26,8 @@ author = 'Stephen Fleming, Mehrtash Babadi' # The full version, including alpha/beta/rc tags -version = '' -release = '' +version = get_version() +release = get_version() # -- General configuration --------------------------------------------------- diff --git a/docs/source/contributing/index.rst b/docs/source/contributing/index.rst index 2986dbd..7d89ef9 100644 --- a/docs/source/contributing/index.rst +++ b/docs/source/contributing/index.rst @@ -3,10 +3,12 @@ Contributing ============ -We aspire to make CellBender an easy-to-use, robust, and accurate software package for the bioinformatics community. -While we test and improve CellBender together with our research collaborators, your feedback is -invaluable to us and allow us to steer CellBender in the direction that you find most useful in your research. If you +We aspire to make CellBender an easy-to-use, robust, and accurate software package +for the bioinformatics community. While we test and improve CellBender together +with our research collaborators, your feedback is invaluable to us and allows us +to steer CellBender in the direction that you find most useful in your research. If you have an interesting idea or suggestion, please do not hesitate to reach out to us. -If you encounter a bug, please file a detailed github `issue `_ +If you encounter a bug, please file a detailed github +`issue `_ and we will get back to you as soon as possible. diff --git a/docs/source/getting_started/index.rst b/docs/source/getting_started/index.rst deleted file mode 100644 index a9bf9b9..0000000 --- a/docs/source/getting_started/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. _getting started: - -Getting Started -=============== - -This section contains quick start tutorials for different CellBender modules. - -.. toctree:: - :maxdepth: 1 - - remove_background/index \ No newline at end of file diff --git a/docs/source/getting_started/remove_background/index.rst b/docs/source/getting_started/remove_background/index.rst deleted file mode 100644 index 90de53f..0000000 --- a/docs/source/getting_started/remove_background/index.rst +++ /dev/null @@ -1,138 +0,0 @@ -.. _remove background tutorial: - -remove-background -================= - -In this tutorial, we will run ``remove-background`` on a small dataset derived from the 10x Genomics -``pbmc4k`` scRNA-seq dataset (v2 Chemistry, CellRanger 2.1.0). - -As a first step, we download the full dataset and generate a smaller `trimmed` copy by selecting 500 barcodes -with high UMI count (likely non-empty) and an additional 50,000 barcodes with small UMI count (likely empty). Note -that the trimming step is performed in order to allow us go through this tutorial in a matter of minutes on a -typical CPU. Processing the full untrimmed dataset requires a CUDA-enabled GPU (e.g. NVIDIA Testla K80) -and takes about 30 minutes to finish. - -We have created a python script to download and trim the dataset. Navigate to ``examples/remove_background/`` -under your CellBender installation root directory and run the following command in the console: - -.. code-block:: console - - $ python generate_tiny_10x_pbmc.py - -After successful completion of the script, you should have a new directory named ``tiny_raw_gene_bc_matrices`` -containing ``GRCh38/matrix.mtx``, ``GRCh38/genes.tsv``, and ``GRCh38/barcodes.tsv``. - -Run remove-background ---------------------- - -We proceed to run ``remove-background`` on the trimmed dataset using the following command: - -.. code-block:: console - - $ cellbender remove-background \ - --input ./tiny_raw_gene_bc_matrices/GRCh38 \ - --output ./tiny_10x_pbmc.h5 \ - --expected-cells 500 \ - --total-droplets-included 5000 - -Again, here we leave out the ``--cuda`` flag solely for the purposes of being able to run this -command on a CPU. But a GPU is highly recommended for real datasets. - -The computation will finish within a minute or two (after ~ 150 epochs). The tool outputs the following files: - -* ``tiny_10x_pbmc.h5``: An HDF5 file containing a detailed output of the inference procedure, including the - normalized abundance of ambient transcripts, contamination fraction of each droplet, a low-dimensional - embedding of the background-corrected gene expression, and the background-corrected counts matrix (in CSC sparse - format). Please refer to the full documentation for a detailed description of these and other fields. - -* ``tiny_10x_pbmc_filtered.h5``: Same as above, though, only including droplets with a posterior cell probability - exceeding 0.5. - -* ``tiny_10x_pbmc_cell_barcodes.csv``: The list of barcodes with a posterior cell probability exceeding 0.5. - -* ``tiny_10x_pbmc.pdf``: A PDF summary of the results showing (1) the evolution of the loss function during training, - (2) a ranked-ordered total UMI plot along with posterior cell probabilities, and (3) a two-dimensional PCA - scatter plot of the latent embedding of the expressions in cell-containing droplets. Notice the rapid drop in - the cell probability after UMI rank ~ 500. - -Finally, try running the tool with ``--expected-cells 100`` and ``--expected-cells 1000``. You should find that -the output remains virtually the same. - -Use output count matrix in downstream analyses ----------------------------------------------- - -The count matrix that ``remove-background`` generates can be easily loaded and used for downstream analyses in -`scanpy `_ and `Seurat `_. - -To load the filtered count matrix (containing only cells) into scanpy: - -.. code-block:: python - - # import scanpy - import scanpy as sc - - # load the data - adata = sc.read_10x_h5('tiny_10x_pbmc_filtered.h5', genome='background_removed') - -To load the filtered count matrix (containing only cells) into Seurat: - -.. code-block:: rd - - # load Seurat (version 3) library - library(Seurat) - - # load data from the filtered h5 file - data.file <- 'tiny_10x_pbmc_filtered.h5' - data.data <- Read10X_h5(filename = data.file, use.names = TRUE) - - # create Seurat object - obj <- CreateSeuratObject(counts = data.data) - -Use latent gene expression in downstream analyses -------------------------------------------------- - -To load the latent representation of gene expression `z` computed by ``remove-background`` using a python script: - -.. code-block:: python - - import tables - import numpy as np - - z = [] - with tables.open_file('tiny_10x_pbmc_filtered.h5') as f: - print(f) # display the structure of the h5 file - z = f.root.background_removed.latent_gene_encoding.read() # read latents - -At this point, the variable ``z`` contains the latent encoding of gene expression, where rows are cells and -columns are dimensions of the latent variable. This data can be saved in CSV format with the following command: - -.. code-block:: python - - np.savetxt('tiny_10x_pbmc_latent_gene_expression.csv', z, delimiter=',') - -This latent representation of gene expression can be loaded into a Seurat object ``obj`` by doing the following: - -.. code-block:: rd - - # load the latent representation from cellbender - latent <- read.csv('tiny_10x_pbmc_latent_gene_expression.csv', header = FALSE) - latent <- t(data.matrix(latent)) - rownames(x = latent) <- paste0("CB", 1:20) - colnames(x = latent) <- colnames(data.data) - - # store latent as a new dimensionality reduction called 'cellbender' - obj[["cellbender"]] <- CreateDimReducObject(embeddings = t(latent), - key = "CB_", - assay = DefaultAssay(obj)) - -Or the variable ``z`` (from above) can be used directly in a scanpy ``anndata`` object. The code snippet below -demonstrates loading the latent ``z`` and using it to do Louvain clustering: - -.. code-block:: python - - # load the latent representation into a new slot called 'X_cellbender' - adata.obsm['X_cellbender'] = z - - # perform louvain clustering using the cellbender latents and cosine distance - sc.pp.neighbors(adata, use_rep='X_cellbender', metric='cosine') - sc.pp.louvain(adata) diff --git a/docs/source/help_and_reference/index.rst b/docs/source/help_and_reference/index.rst deleted file mode 100644 index 1f52c73..0000000 --- a/docs/source/help_and_reference/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _help and reference: - -Help & Reference -================ - -.. toctree:: - :maxdepth: 1 - - remove_background/index - license/index diff --git a/docs/source/help_and_reference/remove_background/index.rst b/docs/source/help_and_reference/remove_background/index.rst deleted file mode 100644 index 6c0b585..0000000 --- a/docs/source/help_and_reference/remove_background/index.rst +++ /dev/null @@ -1,81 +0,0 @@ -.. _remove background reference: - -remove-background -================= - -Command Line Options --------------------- - -.. argparse:: - :module: cellbender.base_cli - :func: get_populated_argparser - :prog: cellbender - :path: remove-background - -WDL Workflow Options --------------------- - -A `WDL `_ script is available as `cellbender_remove_background.wdl -`_, -and it can be used to run ``cellbender remove-background`` from -`Terra `_ or from your own -`Cromwell `_ instance. The WDL is designed to -make use of Tesla K80 GPUs on Google Cloud architecture. - -In addition to the above command line options, the workflow has its own set of -input options, which are described in detail -`here `_. - -.. _remove background reference troubleshooting: - -Troubleshooting ---------------- - -* The learning curve in the output PDF has large downward spikes, or looks super wobbly. - - * This could indicate instabilities during training that should be addressed. The solution - is typically to reduce the ``--learning-rate`` by a factor of two. - -* The following warning is emitted in the log file: "Warning: few empty droplets identified. - Low UMI cutoff may be too high. Check the UMI decay curve, and decrease the - ``--low-count-threshold`` parameter if necessary." - - * This warning indicates that no "surely empty" droplets were identified in the analysis. - This means that the "empty droplet plateau" could not be identified. The most likely - explanation is that the level of background RNA is extremely low, and that the value - of ``--low-count-threshold`` exceeds this level. This would result in the empty - droplet plateau being excluded from the analysis, which is not advisable. This can be - corrected by decreasing ``--low-count-threshold`` from its default of 15 to a value like 5. - - -* There are too many cells called. - - * Are there? ``remove-background`` equates "cell probability" with "the probability that - a given droplet is not empty." These non-empty droplets might not all contain healthy - cells with high counts. Nevertheless, the posterior probability that they are not empty - is greater than 0.5. The recommended procedure - would be to filter cells based on other criteria downstream. Certainly filter for percent - mitochondrial reads. Potentially filter for number of genes expressed as well, if - this does not lead to complete loss of a low-expressing cell type. - * Experiment with increasing ``--total-droplets-included``. - * Experiment with increasing or decreasing ``--empty-drop-training-fraction``. - * As a last resort, try decreasing ``--expected-cells`` by quite a bit. - - -* There are too few cells called. - - * Try estimating ``--expected-cells`` from the UMI curve rather than a priori, and - increase the number if necessary. - * Experiment with increasing or decreasing ``--total-droplets-included``. - - -* The PCA plot of latent gene expression shows no clusters or structure. - - * Has training converged? Training should proceed for at least 150 epochs. Check to - make sure that the ELBO has nearly reached a plateau, indicating that training is - complete. Try increasing ``--epochs`` to 300 and see if the plot changes. - * This is not necessarily a bad thing, although it indicates that cells in the experiment - had a continuum of expression, or that there was only one cell type. If this is - known to be false, some sort of QC failure with the experiment would be suspected. - Perform a downstream clustering analysis with and without ``cellbender remove-background`` - and compare the two. diff --git a/docs/source/index.rst b/docs/source/index.rst index ce71b41..5e23cc4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -4,11 +4,23 @@ Welcome to CellBender's documentation! .. image:: /_static/design/logo_250_185.png :height: 185 px -*CellBender* is a software package for eliminating technical artifacts from high-throughput single-cell RNA sequencing (scRNA-seq) data. +*CellBender* is a software package for eliminating technical artifacts from high-throughput +single-cell RNA sequencing (scRNA-seq) data. For a brief survey of the package and its current capabilities, you can read -the :ref:`Introduction `. For more comprehensive instructions -and quick-start tutorials, see the :ref:`Getting Started ` section. +the :ref:`Introduction `. + +For an explanation of when and how and why to use CellBender in your analysis +pipeline, along with example code, how to choose parameter settings, and +tips on quality control, read the :ref:`Usage ` section. + +For a step-by-step tutorial on a simplified example dataset, see the +:ref:`Quick start tutorial ` section. + +For details on input arguments and the output file format, +see the :ref:`Reference ` section. + +For troubleshooting and an FAQ, see the :ref:`Troubleshooting ` section. .. toctree:: :maxdepth: 1 @@ -16,7 +28,10 @@ and quick-start tutorials, see the :ref:`Getting Started ` sect introduction/index installation/index usage/index - getting_started/index - help_and_reference/index + tutorial/index + reference/index + troubleshooting/index contributing/index citation/index + reference/license/index + changelog/index diff --git a/docs/source/installation/index.rst b/docs/source/installation/index.rst index 86beb63..4defef3 100644 --- a/docs/source/installation/index.rst +++ b/docs/source/installation/index.rst @@ -3,36 +3,80 @@ Installation ============ -Manual Installation -------------------- +Via pip +------- -The recommended installation is as follows. Create a conda environment and activate it: +Python packages can be conveniently installed from the Python Package Index (PyPI) +using `pip install `_. +CellBender is `available on PyPI `_ +and can be installed via .. code-block:: console - $ conda create -n CellBender python=3.7 - $ conda activate CellBender + $ pip install cellbender + +If your machine has a GPU with appropriate drivers installed, it should be +automatically detected, and the appropriate version of PyTorch with CUDA support +should automatically be downloaded as a CellBender dependency. + +We recommend installing CellBender in its own +`conda environment `_. +This allows for easier installation and prevents conflicts with any other python +packages you may have installed. + +.. code-block:: console + + $ conda create -n cellbender python=3.7 + $ conda activate cellbender + (cellbender) $ pip install cellbender + + +Installation from source +------------------------ + +Create a conda environment and activate it: + +.. code-block:: console + + $ conda create -n cellbender python=3.7 + $ conda activate cellbender Install the `pytables `_ module: .. code-block:: console - (CellBender) $ conda install -c anaconda pytables + (cellbender) $ conda install -c anaconda pytables -Install `pytorch `_ (shown below for CPU; if you have a CUDA-ready GPU, please skip -this part and follow `these instructions `_ instead): +Install `pytorch `_ via +`these instructions `_: .. code-block:: console - (CellBender) $ conda install pytorch torchvision -c pytorch + (cellbender) $ pip install torch + +and ensure that your installation is appropriate for your hardware (i.e. that +the relevant CUDA drivers get installed and that ``torch.cuda.is_available()`` +returns ``True`` if you have a GPU available. -Clone this repository and install CellBender: +Clone this repository and install CellBender (in editable ``-e`` mode): .. code-block:: console - (CellBender) $ git clone https://github.com/broadinstitute/CellBender.git - (CellBender) $ pip install -e CellBender + (cellbender) $ git clone https://github.com/broadinstitute/CellBender.git + (cellbender) $ pip install -e CellBender + +Install a specific commit directly from GitHub +---------------------------------------------- + +This can be achieved via + +.. code-block:: console + + (cellbender) $ pip install --no-cache-dir -U git+https://github.com/broadinstitute/CellBender.git@ + +where ```` must be replaced by any reference to a particular git commit, +such as a tag, a branch name, or a commit sha. Docker Image ------------ @@ -43,12 +87,16 @@ A GPU-enabled docker image is available from the Google Container Registry (GCR) Older versions are available at the same location, for example as -``us.gcr.io/broad-dsde-methods/cellbender:0.1.0`` +``us.gcr.io/broad-dsde-methods/cellbender:0.2.0`` Terra Workflow -------------- -For `Terra `_ users, the following workflow is publicly available: +For `Terra `_ users (or any other users of WDL workflows), +the following WDL workflow is publicly available: * `cellbender/remove-background `_ + +Some documentation for the WDL is available at the above link, and some is visible +`on github `_. diff --git a/docs/source/introduction/index.rst b/docs/source/introduction/index.rst index aef82c4..7bcf04a 100644 --- a/docs/source/introduction/index.rst +++ b/docs/source/introduction/index.rst @@ -1,29 +1,30 @@ .. _introduction: What is CellBender? -===================== +=================== -CellBender is a software package for eliminating technical artifacts from high-throughput single-cell RNA sequencing -(scRNA-seq) data. +CellBender is a software package for eliminating technical artifacts from high-throughput single-cell omics data, +including scRNA-seq, snRNA-seq, and CITE-seq. Scope and Purpose ----------------- -Despite the recent progress in improving, optimizing and standardizing scRNA-seq protocols, the complexity of -scRNA-seq experiments leaves room for systematic biases and background noise in the raw observations. These nuisances +Despite the recent progress in improving, optimizing and standardizing droplet-based single-cell omics protocols +like scRNA-seq, the complexity of these experiments leaves room for systematic biases and background noise in +the raw observations. These nuisances can be traced back to undesirable enzymatic processes that produce spurious library fragments, contamination by exogeneous or endogenous ambient transcripts, impurity of barcode beads, and barcode swapping during amplification and/or sequencing. The main purpose of CellBender is to take raw gene-by-cell count matrices and molecule-level information produced -by 3rd party pipelines (e.g. CellRanger, Alevin), to model and remove systematic biases and background noise, and -to produce improved estimates of gene expression. +by 3rd party pipelines (e.g. CellRanger, Alevin, DropSeq, etc.), to model and remove systematic biases and +background noise, and to produce improved estimates of gene expression. As such, CellBender relies on an external tool for primary processing of the raw data obtained from the sequencer (e.g. BCL or FASTQ files). These basic processing steps lie outside of the scope of CellBender and include (pseudo-)alignment and annotation of reads, barcode error correction, and generation of raw gene-by-cell count matrices. Upcoming modules of CellBender will further utilize molecule-level information (e.g. observed reads -per molecule, transcript equivalent classes, etc.). +per molecule, transcript equivalence classes, etc.). Modules ------- @@ -31,6 +32,5 @@ Modules The current version of CellBender contains the following modules. More modules will be added in the future: * ``remove-background``: This module removes counts due to ambient RNA molecules and random barcode swapping from - (raw) UMI-based scRNA-seq gene-by-cell count matrices. At the moment, only the count matrices produced by the - CellRanger `count` pipeline is supported. Support for additional tools and protocols will be added in the future. + (raw) UMI-based scRNA-seq gene-by-cell count matrices. Several file formats for count matrices are supported. A quick-start tutorial can be found :ref:`here `. diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst new file mode 100644 index 0000000..ead0ce7 --- /dev/null +++ b/docs/source/reference/index.rst @@ -0,0 +1,188 @@ +.. _reference: + +Reference +========= + +.. _remove background reference: + +remove-background +----------------- + +Command Line Options +~~~~~~~~~~~~~~~~~~~~ + +.. argparse:: + :module: cellbender.base_cli + :func: get_populated_argparser + :prog: cellbender + :path: remove-background + +WDL Workflow Options +~~~~~~~~~~~~~~~~~~~~ + +A `WDL `_ script is available as `cellbender_remove_background.wdl +`_, +and it can be used to run ``cellbender remove-background`` from +`Terra `_ or from your own +`Cromwell `_ instance. The WDL is designed to +make use of an Nvidia Tesla T4 GPU on Google Cloud architecture. + +As of v0.3.0, the WDL uses preemptible instances that are a fraction of the cost, +and uses automatic restarting from checkpoints so that work is not lost. + +In addition to the above command line options, the workflow has its own set of +input options, which are described in detail +`here `_. + +.. _h5-file-format: + +Output h5 file format +~~~~~~~~~~~~~~~~~~~~~ + +An h5 output file (this one is from the :ref:`tutorial `) +can be examined in detail using PyTables in python: + +.. code-block:: python + + # import PyTables + import tables + + # open the file and take a look at its contents + with tables.open_file('tiny_output.h5', 'r') as f: + print(f) + +.. code-block:: console + + /home/jupyter/tiny_output.h5 (File) 'CellBender remove-background output' + Last modif.: 'Wed May 4 19:46:16 2022' + Object Tree: + / (RootGroup) 'CellBender remove-background output' + /droplet_latents (Group) 'Latent variables per droplet' + /droplet_latents/background_fraction (CArray(1000,), shuffle, zlib(1)) '' + /droplet_latents/barcode_indices_for_latents (CArray(1000,), shuffle, zlib(1)) '' + /droplet_latents/cell_probability (CArray(1000,), shuffle, zlib(1)) '' + /droplet_latents/cell_size (CArray(1000,), shuffle, zlib(1)) '' + /droplet_latents/droplet_efficiency (CArray(1000,), shuffle, zlib(1)) '' + /droplet_latents/gene_expression_encoding (CArray(1000, 100), shuffle, zlib(1)) '' + /global_latents (Group) 'Global latent variables' + /global_latents/ambient_expression (Array(100,)) '' + /global_latents/cell_size_lognormal_std (Array()) '' + /global_latents/empty_droplet_size_lognormal_loc (Array()) '' + /global_latents/empty_droplet_size_lognormal_scale (Array()) '' + /global_latents/posterior_regularization_lambda (Array()) '' + /global_latents/swapping_fraction_dist_params (Array(2,)) '' + /global_latents/target_false_positive_rate (Array()) '' + /matrix (Group) 'Counts after background correction' + /matrix/barcodes (CArray(50500,), zlib(1)) '' + /matrix/data (CArray(44477,), shuffle, zlib(1)) '' + /matrix/indices (CArray(44477,), shuffle, zlib(1)) '' + /matrix/indptr (CArray(50501,), shuffle, zlib(1)) '' + /matrix/shape (CArray(2,), shuffle, zlib(1)) '' + /metadata (Group) 'Metadata' + /metadata/barcodes_analyzed (Array(1000,)) '' + /metadata/barcodes_analyzed_inds (Array(1000,)) '' + /metadata/features_analyzed_inds (Array(100,)) '' + /metadata/fraction_data_used_for_testing (Array()) '' + /metadata/test_elbo (Array(30,)) '' + /metadata/test_epoch (Array(30,)) '' + /metadata/train_elbo (Array(150,)) '' + /metadata/train_epoch (Array(150,)) '' + /matrix/features (Group) 'Genes and other features measured' + /matrix/features/feature_type (CArray(100,), shuffle, zlib(1)) '' + /matrix/features/genome (CArray(100,), shuffle, zlib(1)) '' + /matrix/features/id (CArray(100,), shuffle, zlib(1)) '' + /matrix/features/name (CArray(100,), shuffle, zlib(1)) '' + +Any of these bits of data can be accessed directly with PyTables, or by using +python functions from ``cellbender.remove_background.downstream`` such as +``anndata_from_h5()`` that load the metadata as well as the count matrix into +`AnnData format `_ +(compatible with analysis in `scanpy `_). + +The names of variables in the h5 file are meant to explain their contents. +All data in the ``/matrix`` group is formatted as `CellRanger v3 h5 data +`_. + +The other root groups in the h5 file ``[droplet_latents, global_latents, metadata]`` +contain detailed information inferred by CellBender as part of its latent +variable model of the data generative process (details in our paper). +The ``/droplet_latents`` are variables that have a value inferred for each +droplet, such as ``cell_probability`` and ``cell_size``. The ``/global_latents`` are +variables that have a fixed value for the whole experiment, such as +``ambient_expression``, which is the inferred profile of ambient RNA in the +sample (normalized so it sums to one). Finally, ``/metadata`` includes other things +such as the learning curve (``train_elbo`` and ``train_epoch``) and which +features were analyzed during the run (``features_analyzed_inds``, as +integer indices that index ``/matrix/features``). + + +.. _loading-outputs: + +Loading and using outputs +~~~~~~~~~~~~~~~~~~~~~~~~~ + +As mentioned in the :ref:`tutorial `, output h5 files can +be opened natively by ``scanpy`` +(`scanpy.read_10x_h5() `_) +and (with some effort) by ``Seurat`` (see :ref:`here `), though +there is extra ``cellbender``-specific information in the output files that +``scanpy`` and ``Seurat`` will not load automatically. + +Additional loading functionality is distrubuted as part of the ``cellbender`` +package and is described below. + +Suggested use cases: + +- Load just the CellBender output: ``anndata_from_h5()`` +- Load the raw input together with the CellBender output (this is great for + downstream analysis, because it allows you to plot the effect of ``cellbender``, + and you can always look back at the raw data): ``load_anndata_from_input_and_output()`` +- Power users who want to run ``cellbender`` at multiple FPR values and make + comparisons: ``load_anndata_from_input_and_outputs()`` + +Example: + +.. code-block:: python + + from cellbender.remove_background.downstream import load_anndata_from_input_and_output + + adata = load_anndata_from_input_and_output( + input_file='my_raw_10x_file.h5', + output_file='my_cellbender_output_file.h5', + input_layer_key='raw', # this will be the raw data layer + ) + adata + +.. code-block:: console + + AnnData object with n_obs × n_vars = 6983 × 37468 + obs: 'background_fraction', 'cell_probability', 'cell_size', 'droplet_efficiency', + 'n_raw', 'n_cellbender' + var: 'ambient_expression', 'feature_type', 'genome', 'gene_id', 'cellbender_analyzed', + 'n_raw', 'n_cellbender' + uns: 'cell_size_lognormal_std', 'empty_droplet_size_lognormal_loc', + 'empty_droplet_size_lognormal_scale', 'swapping_fraction_dist_params', + 'target_false_positive_rate', 'features_analyzed_inds', + 'fraction_data_used_for_testing', 'test_elbo', 'test_epoch', + 'train_elbo', 'train_epoch' + obsm: 'cellbender_embedding' + layers: 'raw', 'cellbender' + +.. automodule:: cellbender.remove_background.downstream + :members: + +The posterior +~~~~~~~~~~~~~ + +There is an additional output file called ``posterior.h5`` which contains the +full posterior. The structure of this data is a sparse matrix whose shape is +[count matrix entries, number of potential noise counts]. Typically the number +of potential noise counts in any entry of the count matrix is truncated at 50. +The values are log probabilities of the specified number of noise counts in the +given count matrix entry. + +The information contained in the posterior can be used to +quantitatively answer questions such as "What is the probability that the +number of viral gene counts in this cell is nonzero?" For help with these kinds +of computations, please open a +`github issue `_. diff --git a/docs/source/help_and_reference/license/index.rst b/docs/source/reference/license/index.rst similarity index 100% rename from docs/source/help_and_reference/license/index.rst rename to docs/source/reference/license/index.rst diff --git a/docs/source/troubleshooting/index.rst b/docs/source/troubleshooting/index.rst new file mode 100644 index 0000000..893f7bb --- /dev/null +++ b/docs/source/troubleshooting/index.rst @@ -0,0 +1,426 @@ +.. _troubleshooting: + +Troubleshooting +=============== + +.. _remove background troubleshooting: + +remove-background +----------------- + +Feel free to check the +`issues on github `_, +where there are several answered questions +you can search through. If you don't see an answer there or below, please consider +opening a new issue on github to ask your question. + +FAQ +~~~ + +* :ref:`What are the "best practices" for running the tool? ` + +* :ref:`What are the "best practices" for incorporating CellBender into my analysis pipeline? ` + +* :ref:`How do I load and use the output? ` + +* :ref:`What are the details of the output h5 file format? ` + +* :ref:`Do I really need a GPU to run this? ` + +* :ref:`How do I determine the right "--fpr"? ` + +* :ref:`How do I determine the right "--expected-cells"? ` + +* :ref:`How do I determine the right "--total-droplets-included"? ` + +* :ref:`Does CellBender work with CITE-seq data? Or other non-"Gene Expression" features? ` + +* :ref:`Could I run CellBender using only "Gene Expression" features and ignore other features? ` + +* :ref:`Could I run CellBender using only "Antibody Capture" features and not Gene Expression? ` + +* :ref:`Where can I find the ambient RNA profile inferred by CellBender? ` + +* :ref:`The code completed, but how do I know if it "worked"? / How do I know when I need to re-run with different parameters? ` + +* :ref:`It seems like CellBender called too many cells ` + +* :ref:`It seems like CellBender called too few cells ` + +* :ref:`The PCA plot of latent gene expression shows no clusters or structure ` + +* :ref:`The learning curve looks weird. / What is the learning curve supposed to look like? ` + +* :ref:`What is the "metrics.csv" output for, and how do I interpret the metrics? ` + +* :ref:`My HTML report summary (at the bottom) said there were some warnings. What should I do about that? ` + +* :ref:`The tool failed to produce an HTML report ` + +* :ref:`I ran the WDL in Terra and the job "Failed" with PAPI error code 9 ` + +* :ref:`How much does it cost, per sample, to run the WDL in Terra? ` + +* :ref:`I am getting a GPU-out-of-memory error (process "Killed") ` + +* :ref:`I got a "nan" error and the tool crashed ` + +* :ref:`There was an error! ` + + +Answers / Troubleshooting Tips +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _a1: + +* What are the "best practices" for running the tool? + + * See the :ref:`recommended best practices ` + +.. _a2: + +* What are the "best practices" for incorporating CellBender into my analysis pipeline? + + * See the :ref:`proposed pipeline ` + +.. _a22: + +* How do I load and use the output? + + * The data can easily be loaded as an ``AnnData`` object in python, to be used in + ``scanpy``, or it can also be loaded by Seurat, see the + :ref:`example here ` + * We recommend loading the input and output data (together) using the function + ``cellbender.remove_background.downstream.load_anndata_from_input_and_output()`` + from the CellBender package, since that will create an ``AnnData`` with + separate layers for the raw data and the CellBender output data. This is + quite handy for downstream work. There are several simple data-loading + functions in ``cellbender.remove_background.downstream`` that might be + useful. + + .. code-block:: python + + from cellbender.remove_background.downstream import load_anndata_from_input_and_output + + adata = load_anndata_from_input_and_output(input_file='tiny_raw_feature_bc_matrix.h5ad', + output_file='tiny_output.h5') + adata + + .. code-block:: console + + AnnData object with n_obs × n_vars = 1000 × 100 + obs: 'background_fraction', 'cell_probability', 'cell_size', 'droplet_efficiency', 'n_cellranger', 'n_cellbender' + var: 'ambient_expression', 'features_analyzed_inds', 'feature_type', 'genome', 'gene_id', 'n_cellranger', 'n_cellbender' + uns: 'cell_size_lognormal_std', 'empty_droplet_size_lognormal_loc', 'empty_droplet_size_lognormal_scale', 'posterior_regularization_lambda', 'swapping_fraction_dist_params', 'target_false_positive_rate', 'fraction_data_used_for_testing', 'test_elbo', 'test_epoch', 'train_elbo', 'train_epoch' + obsm: 'cellbender_embedding' + layers: 'cellranger', 'cellbender' + +.. _a23: + +* What are the details of the output h5 file format? + + * :ref:`See here ` + +.. _a3: + +* Do I really need a GPU to run this? + + * It's not absolutely necessary, but the code takes a long time to run on a full + dataset on a CPU. + * While running on a GPU might seem like an insurmountable obstacle for those without + a GPU handy, consider trying out our + `workflow on Terra `_, + which runs on a GPU on Google Cloud at the click of a button. + * Others have successfully run on Google Colab notebooks for free. Since CellBender + produces checkpoint files during training (``ckpt.tar.gz``), you can even pick up + where you left off if you get preempted. You just need to put the ``ckpt.tar.gz`` + file in the directory where the CellBender command is being invoked (or + specify the file using the argument ``--checkpoint``), and CellBender will + automatically pick up where it left off when the same command is re-run. + * If you really want to use a CPU only, then consider things that will speed up the + run, like using fewer ``--total-droplets-included``, and increasing the threshold + ``--projected-ambient-count-threshold`` so that fewer features are analyzed, + and maybe decreasing ``--empty-drop-training-fraction``, so that each minibatch + has fewer empty droplets. + +.. _a4: + +* How do I determine the right ``--fpr``? + + * See the :ref:`recommended best practices ` + +.. _a5: + +* How do I determine the right ``--expected-cells``? + + * See the :ref:`recommended best practices ` + +.. _a6: + +* How do I determine the right ``--total-droplets-included``? + + * See the :ref:`recommended best practices ` + +.. _a7: + +* Does CellBender work with CITE-seq data? Or other non-``Gene Expression`` features? + + * Absolutely, `as shown here `_ + and `in our paper `_. + The results for ``Antibody Capture`` data look even better than + for gene expression, due to the higher ambient background for that modality. + * ATAC data is a bit more tricky. CellBender will run, though it takes a long + time with 200k+ Peak features. You can use the argument + ``--projected-ambient-count-threshold 2`` to tell CellBender to ignore all + features which are not estimated to have at least two noise counts in cells. + This can greatly speed things up. Feel free to experiment with that value. + Anecdotally it seems that ATAC data is less noisy than gene expression data + to begin with, so some users opt to have CellBender ignore the ATAC features + using the input argument ``--exclude-feature-types Peaks``. There is nothing + wrong with doing this. + +.. _a8: + +* Could I run CellBender using only ``Gene Expression`` features and ignore other features? + + * If you want to, you can (though it works great with ``Antibody Capture`` data): + just use the ``--exclude-feature-types`` input parameter. + +.. _a24: + +* Could I run CellBender using only ``Antibody Capture`` features and not ``Gene Expression``? + + * No, it is not a good idea to exclude ``Gene Expression`` features for the following + reason: the ``Gene Expression`` features form the basis of a good prior on "cell type", + which the method heavily relies on to denoise. Other features, without ``Gene Expression``, + are probably too sparse to cluster similar cells together and form a good prior for + "which cells are similar to which others". + +.. _a9: + +* Where can I find the ambient RNA profile inferred by CellBender? + + * This is present in the output h5 file as the field called + ``/global_latents/ambient_expression`` (:ref:`see here `). + If you use the dataloader from ``cellbender.remove_background.downstream`` + (:ref:`see here `), then the ambient expression profile will be loaded + into the AnnData object as ``adata.var['ambient_expression']`` + * (Though it is referred to here as "ambient RNA", all features are included, + not just Gene Expression.) + +.. _a10: + +* The code completed, but how do I know if it "worked"? / How do I know when I + need to re-run with different parameters? + + * The vast majority of runs using a nice dataset will work just fine. If your + dataset might not be so "nice", then we recommend taking a look at the output + ``_report.html``, which will have a few diagnostics and will issue warnings + and recommendations as appropirate. + * In general, if the learning curve (ELBO versus epoch) has huge spikes, or if + if does not converge near the end but rather dips back down, then you may + need to consider re-running with a lower ``--learning-rate``. The solution + is typically to reduce the ``--learning-rate`` by a factor of two. + * In certain cases, it may be obvious that CellBender has failed to call cells + accurately. In these cases, it may be necessary to do a bit of experimentation + with ``--expected-cells`` and ``--total-droplets-included`` to try to guide + CellBender toward a more reasonable solution. It has been our observation + that such cases are relatively rare. Try looking at the UMI curve and picking + a value for ``--expected-cells`` where you know that all the droplets + preceding that number are surely cells. + +.. _a11: + +* It seems like CellBender called too many cells + + * Did it? ``remove-background`` equates "cell probability" with "the probability that + a given droplet is not empty." These non-empty droplets might not all contain healthy + cells with high counts. Nevertheless, the posterior probability that they are not empty + is greater than 0.5. The recommended procedure + would be to filter cells based on other criteria downstream. Certainly filter for percent + mitochondrial reads. Potentially filter for number of genes expressed as well, if + this does not lead to complete loss of a low-expressing cell type. + * Experiment with increasing ``--total-droplets-included``. + * Experiment with increasing or decreasing ``--empty-drop-training-fraction``. + * As a last resort, try decreasing ``--expected-cells`` by quite a bit. + +.. _a12: + +* It seems like CellBender called too few cells + + * If CellBender seems to have missed cells, or if you get a "No cells found!" + error, then try increasing ``--expected-cells``, and also ensure that your value + for ``--total-droplets-included`` is large enough that all droplets after + this value on the UMI curve are "surely empty". + +.. _a25: + +* The PCA plot of latent gene expression shows no clusters or structure + + * Has training converged? Training should proceed for at least 150 epochs. Check to + make sure that the ELBO has nearly reached a plateau, indicating that training is + complete. Try increasing ``--epochs`` to 300 and see if the plot changes. + * This is not necessarily a bad thing, although it indicates that cells in the experiment + had a continuum of expression, or that there was only one cell type. If this is + known to be false, some sort of QC failure with the experiment would be suspected. + Perform a downstream clustering analysis with and without ``cellbender remove-background`` + and compare the two. + +.. _a13: + +* The learning curve looks weird. / What is the learning curve supposed to + look like? + + * The "learning curve", a.k.a. the plot of ELBO (evidence lower bound) verus + training epoch, is a record of the progress of inferring all the latent + variables in the CellBender model, based on data. This learning happens via + gradient descent. Typically, the ELBO changes gradually, increasing and + approaching some rather stable value by the end of training. Ideally, the + ELBO increases monotonically. + + * If the learning curve either starts decreasing, or has a large downward bump + or a downward spike or spikes, then something may have gone a bit "off the + rails" during training. We would be concerned that, for example, the inference + procedure got thrown off into some local minimum that is sub-optimal. If you + see a learning curve that looks strange, then try to re-run with half the + ``--learning-rate`` and see if it results in a more "canonical" learning curve. + If it does, use that output. + + * Examples: 2 fine learning curves (panels on right are zoomed-in y-axis) + + * .. image:: /_static/remove_background/PCL_rat_A_LA6_learning_curve.png + :width: 600 px + + + * .. image:: /_static/remove_background/simulated_s6_learning_curve.png + :width: 600 px + + * Examples: 2 bad learning curves + + * .. image:: /_static/remove_background/bad_learning_curves.png + :width: 700 px + +.. _a14: + +* What is the ``metrics.csv`` output for, and how do I interpret the metrics? + + * This is a bit of a niche output, and most people can ignore it if they want + to. The idea here is to enable automated analysis pipelines to make decisions + about whether to re-run CellBender with different parameters. Several of the + output metrics contained here are also contained in the HTML report (though + not all). But, importantly, this CSV file is easy to parse programmatically, + so that a pipeline can make automatic decisions. All metrics are scalar + quantities, and the intent was to name them so they are self-explanatory. + The contents are: + + 1. ``total_raw_counts``: Sum of all input count matrix entries + 2. ``total_output_counts``: Sum of all output count matrix entries + 3. ``total_counts_removed``: 1 minus 2 + 4. ``fraction_counts_removed``: 3 divided by 1 + 5. ``total_raw_counts_in_cells``: Same as 1, but calculated only in CellBender- + determined non-empty droplets + 6. ``total_counts_removed_from_cells``: 5 minus 2 (since only cells have + nonzero counts in the output) + 7. ``fraction_counts_removed_from_cells``: 6 divided by 5 + 8. ``average_counts_removed_per_cell``: 6 divided by the number of CellBender- + determined non-empty droplets + 9. ``target_fpr``: The input ``--fpr`` value + 10. ``expected_cells``: The input ``--expected-cells`` value, blank if not + provided. + 11. ``found_cells``: The number of CellBender- + determined non-empty droplets + 12. ``output_average_counts_per_cell``: 2 divided by 11 + 13. ``ratio_of_found_cells_to_expected_cells``: 11 divided by 10 + 14. ``found_empties``: The number of empty droplets, as determined by + CellBender. This number plus 11 equals the input + ``--total-droplets-included`` (or the value used by default) + 15. ``fraction_of_analyzed_droplets_that_are_nonempty``: 11 divided by the + input ``--total-droplets-included`` + 16. ``convergence_indicator``: The mean absolute difference between successive + values of the train ELBO for the last 3 epochs, divided by the standard + deviation of the train ELBO over the last 20 epochs. A smaller number + indicates better convergence. It's typical to see values of 0.25 or 0.35. + Large values might indicate a failure to converge. (Not many people have + yet used this in practice, so we might learn more about recommendations + in future.) + 17. ``overall_change_in_train_elbo``: The change in ELBO from first to last + epoch. + + * The most useful values for automated decision-making are probably 13, 15, + and 16. + +.. _a15: + +* My HTML report summary (at the bottom) said there were some warnings. What + should I do about that? + + * If the warning comes with a recommendation to re-run with different settings, + then that is worth a try. + * Some warnings do not need further action, and simply reflect peculiarities + of the specific dataset. + +.. _a16: + +* The tool failed to produce an HTML report + + * Please report the error as a github issue. The report-making process, since + it makes use of Jupyter notebooks, is a bit of a fragile process. These + reports are new in v0.3.0, and there has been less testing. + +.. _a17: + +* I ran the WDL in Terra and the job ``Failed`` with PAPI error code 9 + + * Typically this is a so-called "transient" error, meaning that it was a random + occurrance, and the job may succeed if run again without any changes. + However, it is worth checking the log and checking "Job Manager" to see if + there was a more specific error message. + +.. _a18: + +* How much does it cost, per sample, to run the WDL in Terra? + + * It depends on the size of the dataset, but $0.30 is pretty typical, as of + the pricing used by Google Cloud in July 2022. + +.. _a19: + +* I am getting a GPU-out-of-memory error (process ``Killed``) + + * Please report the issue on github, but there are a few things you can try + to reduce memory usage. Typically memory usage is highest during posterior + sampling. Try setting ``--posterior-batch-size`` to 64, instead of its + default value of 128. (Make sure to restart from the checkpoint file to + avoid re-running inference. This will happen automatically if you re-run + in the same folder as the ``ckpt.tar.gz`` file.) + * If you can, try running on an Nvidia Tesla T4 GPU, which has more RAM than + some other options. + * Currently, CellBender only makes use of 1 GPU, so extra GPUs will not help. + +.. _a20: + +* I got a ``nan`` error and the tool crashed + + * This is real bad. Definitely report this issue on github. You may be asked + to re-run the tool using the ``--debug`` flag, to get more error messages + for reporting. + +.. _a21: + +* There was an error! + + * Please report the issue on github. You may be asked + to re-run the tool using the ``--debug`` flag, to get more error messages + for reporting. + + * The following warning is emitted in the log file: "Warning: few empty droplets identified. + Low UMI cutoff may be too high. Check the UMI decay curve, and decrease the + ``--low-count-threshold`` parameter if necessary." + + * This warning indicates that no "surely empty" droplets were identified in the analysis. + This means that the "empty droplet plateau" could not be identified. The most likely + explanation is that the level of background RNA is extremely low, and that the value + of ``--low-count-threshold`` exceeds this level. This would result in the empty + droplet plateau being excluded from the analysis, which is not advisable. This can be + corrected by decreasing ``--low-count-threshold`` from its default of 15 to a value like 5. + diff --git a/docs/source/tutorial/index.rst b/docs/source/tutorial/index.rst new file mode 100644 index 0000000..c26316e --- /dev/null +++ b/docs/source/tutorial/index.rst @@ -0,0 +1,224 @@ +.. _quick start tutorial: + +Quick-start tutorial +==================== + +This section contains quick start tutorials for different CellBender modules. + +.. _remove background tutorial: + +remove-background +----------------- + +In this tutorial, we will run ``remove-background`` on a small dataset derived from the 10x Genomics +``heart10k`` snRNA-seq `dataset +`_ +(v3 Chemistry, CellRanger 3.0.2). + +As a first step, we download the full dataset and generate a smaller `trimmed` copy by selecting 500 barcodes +with high UMI count (likely non-empty) and an additional 50,000 barcodes with small UMI count (likely empty). +We also trim to keep only the top 100 most highly-expressed genes. Note +that the trimming step is performed in order to allow us go through this tutorial in a minute on a +typical CPU. Processing the full untrimmed dataset requires a CUDA-enabled GPU (e.g. NVIDIA Testla T4) +and takes about 30 minutes to finish. + +(Please note that trimming is NOT part of the recommended workflow, and is only for +the purposes of a quick demo run. When you run ``remove-background`` on real data, +do not do any preprocessing or feature selection or barcode selection first.) + +We have created a python script to download and trim the dataset. Navigate to ``examples/remove_background/`` +under your CellBender installation root directory and run the following command in the console: + +.. code-block:: console + + $ python generate_tiny_10x_dataset.py + +After successful completion of the script, you should have a new file named +``tiny_raw_feature_bc_matrix.h5ad``. + +Run remove-background +~~~~~~~~~~~~~~~~~~~~~ + +We proceed to run ``remove-background`` on the trimmed dataset using the following command: + +.. code-block:: console + + (cellbender) $ cellbender remove-background \ + --input tiny_raw_feature_bc_matrix.h5ad \ + --output tiny_output.h5 \ + --expected-cells 500 \ + --total-droplets-included 2000 + +Again, here we leave out the ``--cuda`` flag solely for the purposes of being able to run this +command on a CPU. But a GPU is highly recommended for real datasets. + +The computation will finish within a minute or two (after 150 epochs). The tool outputs the following files: + +* ``tiny_output.h5``: An HDF5 file containing a detailed output of the inference procedure, including the + normalized abundance of ambient transcripts, contamination fraction of each droplet, a low-dimensional + embedding of the background-corrected gene expression, and the background-corrected counts matrix (in CSC sparse + format). Please refer to the full documentation for a detailed description of these and other fields. + +* ``tiny_output_filtered.h5``: Same as above, though, only including droplets with a posterior cell probability + exceeding 0.5. + +* ``tiny_output_cell_barcodes.csv``: The list of barcodes with a posterior cell probability exceeding 0.5. + +* ``tiny_output.pdf``: A PDF summary of the results showing (1) the evolution of the loss function during training, + (2) a ranked-ordered total UMI plot along with posterior cell probabilities, and (3) a two-dimensional PCA + scatter plot of the latent embedding of the expressions in cell-containing droplets. Notice the rapid drop in + the cell probability after UMI rank ~ 500. + +* ``tiny_output_umi_counts.pdf``: A PDF showing the UMI counts per droplet as a histogram, annotated + with what CellBender thinks is empty versus cells. This describes CellBender's prior. This is mainly + a diagnostic if something seems to be going wrong with CellBender's automatic prior determination. + +* ``tiny_output.log``: Log file. + +* ``tiny_output_metrics.csv``: Output metrics, most of which have very descriptive names. This file is not + used by most users, but the idea is that it can be incorporated into automated pipelines which could re-run + CellBender automatically (with different parameters) if something goes wrong. + +* ``tiny_output_report.html``: An HTML report which points out a few things about the run and + highlights differences between the output and the input. Issues warnings if there are any + aspects of the run that look anomalous, and makes suggestions. + +Finally, try running the tool with ``--expected-cells 100`` and ``--expected-cells 1000``. You should find that +the output remains virtually the same. + +.. _downstream-example: + +Use output count matrix in downstream analyses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The count matrix that ``remove-background`` generates can be easily loaded and used for downstream analyses in +`scanpy `_ and `Seurat `_. + +You can load the filtered count matrix (containing only cells) into scanpy: + +.. code-block:: python + + # import scanpy + import scanpy as sc + + # load the data + adata = sc.read_10x_h5('tiny_output_filtered.h5') + +This will yield ``adata`` like this + +.. code-block:: console + + AnnData object with n_obs × n_vars = 531 × 100 + var: 'gene_ids', 'feature_types', 'genome' + +The CellBender output counts are in ``adata.X``. + +However, this does not include any of the CellBender metadata when loading +the file. To include the metadata, but still load an ``AnnData`` object +that ``scanpy`` can operate on, try some of the functions from +``cellbender.remove_background.downstream`` (see :ref:`here `) + +.. code-block:: python + + # import function + from cellbender.remove_background.downstream import anndata_from_h5 + + # load the data + adata = anndata_from_h5('tiny_output.h5') + +This yields an ``adata`` with all the cell barcodes which were analyzed by +CellBender (all the ``--total-droplets-included``), along with all the +metadata and latent variables inferred by CellBender: + +.. code-block:: console + + AnnData object with n_obs × n_vars = 1000 × 100 + obs: 'background_fraction', 'cell_probability', 'cell_size', 'droplet_efficiency' + var: 'ambient_expression', 'features_analyzed_inds', 'feature_type', 'genome', 'gene_id' + uns: 'cell_size_lognormal_std', 'empty_droplet_size_lognormal_loc', 'empty_droplet_size_lognormal_scale', 'posterior_regularization_lambda', 'swapping_fraction_dist_params', 'target_false_positive_rate', 'fraction_data_used_for_testing', 'test_elbo', 'test_epoch', 'train_elbo', 'train_epoch' + obsm: 'gene_expression_encoding' + +(If you want to load both the +raw data and the CellBender data into one AnnData object, which is very useful, +try the ``load_anndata_from_input_and_output()`` function in +``cellbender.remove_background.downstream``, see :ref:`here `) + +You can access the latent gene expression embedding learned by CellBender in +``adata.obsm['gene_expression_encoding']``, the inferred ambient RNA profile +is in ``adata.var['ambient_expression']``, and the inferred cell probabilties are +in ``adata.obs['cell_probability']``. + +You can limit this ``adata`` to CellBender cell calls very easily: + +.. code-block:: python + + adata[adata.obs['cell_probability'] > 0.5] + +.. code-block:: console + + View of AnnData object with n_obs × n_vars = 531 × 100 + obs: 'background_fraction', 'cell_probability', 'cell_size', 'droplet_efficiency' + var: 'ambient_expression', 'features_analyzed_inds', 'feature_type', 'genome', 'gene_id' + uns: 'cell_size_lognormal_std', 'empty_droplet_size_lognormal_loc', 'empty_droplet_size_lognormal_scale', 'posterior_regularization_lambda', 'swapping_fraction_dist_params', 'target_false_positive_rate', 'fraction_data_used_for_testing', 'test_elbo', 'test_epoch', 'train_elbo', 'train_epoch' + obsm: 'gene_expression_encoding' + +How to use the latent gene expression downstream +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After loading data using the ``anndata_from_h5()`` function as shown above, +we can compute nearest neighbors +in scanpy, using the CellBender latent representation of cells, and make a UMAP and do clustering: + +.. code-block:: python + + # compute a UMAP and do clustering using the cellbender latent gene expression embedding + sc.pp.neighbors(adata, use_rep='gene_expression_encoding', metric='euclidean', method='umap') + sc.pp.umap(adata) + sc.pp.leiden(adata) + +.. _open-in-seurat: + +Seurat +~~~~~~ + +Seurat 4.0.2 uses a dataloader ``Read10X_h5()`` which is not currently compatible with +the CellBender output file format. Hopefully Seurat will update its dataloader to +ignore extra information in the future, but in the interim, we can use a `super +handy utility from PyTables +`_ to strip the +extra CellBender information out of the output file so that Seurat can load it. + +From a python environment in which PyTables is installed, do the following at +the command line: + +.. code-block:: console + + $ ptrepack --complevel 5 tiny_output_filtered.h5:/matrix tiny_output_filtered_seurat.h5:/matrix + +(The flag ``--complevel 5`` ensures that the file size does not increase.) + +The file ``tiny_output_filtered_seurat.h5`` is now formatted *exactly* like +a CellRanger v3 h5 file, so Seurat can load it: + +.. code-block:: Rd + + # load data from the filtered h5 file + data.file <- 'tiny_output_filtered_seurat.h5' + data.data <- Read10X_h5(filename = data.file, use.names = TRUE) + + # create Seurat object + obj <- CreateSeuratObject(counts = data.data) + obj + +.. code-block:: console + + An object of class Seurat + 100 features across 531 samples within 1 assay + Active assay: RNA (100 features, 0 variable features) + +Of course, this will not load any metadata from CellBender, so if that is desired, +it would have to be accessed and added to the object another way. + +Another option for loading data into Seurat would be third party packages like +`scCustomize from Samuel Marsh +`_. diff --git a/docs/source/usage/index.rst b/docs/source/usage/index.rst index 295ad11..bb0d5e9 100644 --- a/docs/source/usage/index.rst +++ b/docs/source/usage/index.rst @@ -9,8 +9,8 @@ remove-background Use case ~~~~~~~~ -``remove-background`` is used to remove ambient / background RNA from a count matrix produced by -`10x Genomics' CellRanger pipeline +``remove-background`` is used to remove ambient / background RNA from a count matrix, +such as one produced by the `10x Genomics' CellRanger pipeline `_. The output of ``cellranger count`` produces a raw .h5 file that is used as the input for ``remove-background``. @@ -21,6 +21,8 @@ analysis using Seurat, scanpy, your own custom analysis, etc. The output of ``remove-background`` includes a new .h5 count matrix, with background RNA removed, that can directly be used in downstream analysis in Seurat or scanpy as if it were the raw dataset. +.. _proposed-pipeline: + Proposed pipeline ~~~~~~~~~~~~~~~~~ @@ -28,6 +30,9 @@ Proposed pipeline #. Run ``cellbender remove-background`` #. Perform per-cell quality control checks, and filter out dead / dying cells, as appropriate for your experiment +#. Perform all subsequent analyses using the CellBender count matrix. (It is useful + to also load the raw data: keep it as a layer in an ``anndata`` object, for + example, see :ref:`here `) A few caveats and hints: @@ -61,7 +66,7 @@ Run ``remove-background`` on the dataset using the following command .. code-block:: console - (CellBender) $ cellbender remove-background \ + (cellbender) $ cellbender remove-background \ --input raw_feature_bc_matrix.h5 \ --output output.h5 \ --cuda \ @@ -85,15 +90,22 @@ This command will produce five output files: output for convenient use in certain downstream applications. * ``output.pdf``: PDF file that provides a standard graphical summary of the inference procedure. * ``output.log``: Log file produced by the ``cellbender remove-background`` run. +* ``output_metrics.csv``: Metrics describing the run, potentially to be used to flag + problematic runs when using CellBender as part of a large-scale automated pipeline. +* ``output_report.html``: HTML report including plots and commentary, along with any + warnings or suggestions for improved parameter settings. +* ``ckpt.tar.gz``: Checkpoint file which contains the trained model and the full posterior. Quality control checks ~~~~~~~~~~~~~~~~~~~~~~ * Check the log file for any warnings. -* Check lines 8 - 11 in the log file. Ensure that the automatically-determined priors +* Check lines 11 -18 in the log file. Ensure that the automatically-determined priors for cell counts and empty droplet counts match your expectation from the UMI curve. Ensure that the numbers of "probable cells", "additional barcodes", and "empty droplets" are all nonzero and look reasonable. +* Look at the HTML report and note any warnings it gives. The report will give advice + for re-running the tool if appropriate. * Examine the PDF output. * Look at the upper plot to check whether @@ -108,7 +120,7 @@ Quality control checks * Check the middle plot to see which droplets have been called as cells. A converged inference procedure should result in the vast majority of cell probabilities being very close to either zero or one. If the cell calls look problematic, check - the :ref:`help documentation `. + the :ref:`help documentation `. Keep in mind that ``remove-background`` will output a high cell probability for any droplet that is unlikely to be drawn from the ambient background. This can result in a large number @@ -125,16 +137,19 @@ Quality control checks difficulties in calling which droplets contain cells.) * Create some validation plots of various analyses with and without - ``cellbender remove-background``. One convenient way to do this is in ``scanpy`` - and storing the raw count matrix and the background-removed count matrix as - separate `"layers" `_. + ``cellbender remove-background``. One convenient way to do this is in ``scanpy``, + storing the raw count matrix and the background-removed count matrix as + separate `"layers" `_. - * UMAPs with and without (on the same set of cell barcodes) - * Marker gene dotplots and violin plots (you should see less background) + * UMAPs with and without CellBender (on the same set of cell barcodes) + * Marker gene dotplots and violin plots before and after CellBender + (you should see less background noise) * Directly subtract the output count matrix from the input count matrix and take a close look at what was removed. +.. _best-practices: + Recommended best practices ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -142,6 +157,11 @@ The default settings are good for getting started with a clean and simple datase the publicly available `PBMC dataset from 10x Genomics `_. +As of v0.3.0, users will typically not need to set values for ``--expected-cells`` +or ``--total-droplets-included``, as CellBender will choose reasonable values +based on your dataset. If something goes wrong with these defaults, then you can +try to input these arguments manually. + Considerations for setting parameters: * ``--epochs``: 150 is typically a good choice. Look for a reasonably-converged ELBO value @@ -163,8 +183,13 @@ Considerations for setting parameters: * ``--cuda``: Include this flag. The code is meant to be run on a GPU. * ``--learning-rate``: The default value of 1e-4 is typically fine, but this value can be adjusted if problems arise during quality-control checks of the learning curve (as above). -* ``--fpr``: A value of 0.01 is generally quite good, but you can generate a few output - count matrices and compare them by choosing a few values: 0.01 0.05 0.1 +* ``--fpr``: A value of 0.01 is the default, and represents a fairly conservative + setting, which is appropriate for most analyses. + In order to examine a single dataset at a time and remove more noise (at the + expense of some signal), choose larger values such as 0.05 or 0.1. Bear in mind + that the value 1 represents removal of (nearly) every count in the dataset, signal and + noise. You can generate multiple output count matrices in the same run by + choosing several values: 0.0 0.01 0.05 0.1 .. image:: /_static/remove_background/UMI_curve_defs.png :width: 250 px diff --git a/examples/remove_background/.gitignore b/examples/remove_background/.gitignore index f631f06..ad6bf5a 100644 --- a/examples/remove_background/.gitignore +++ b/examples/remove_background/.gitignore @@ -1 +1,7 @@ -tiny_raw_gene_bc_matrices +*.h5 +*.h5ad +ckpt.tar.gz +*.log +*.csv +*.pdf +*.html \ No newline at end of file diff --git a/examples/remove_background/generate_tiny_10x_dataset.py b/examples/remove_background/generate_tiny_10x_dataset.py new file mode 100755 index 0000000..fdbc955 --- /dev/null +++ b/examples/remove_background/generate_tiny_10x_dataset.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python + +import sys +import os +import numpy as np +from cellbender.remove_background.downstream import load_anndata_from_input + +dataset_name = "heart10k (CellRanger 3.0.0, v3 Chemistry)" +dataset_url = "https://cf.10xgenomics.com/samples/cell-exp/3.0.0/heart_10k_v3/heart_10k_v3_raw_feature_bc_matrix.h5" +dataset_local_filename = "heart10k_raw_feature_bc_matrix.h5" +expected_cells = 7000 +start_of_empties = 10000 +num_cell_barcodes_to_keep = 500 +num_empty_barcodes_to_keep = 50_000 +low_count_threshold = 30 +num_genes_to_keep = 100 +random_seed = 1984 +rng = np.random.RandomState(random_seed) + + +if not os.path.exists(dataset_local_filename): + + print(f"Downloading {dataset_name}...") + try: + os.system(f'wget -O {dataset_local_filename} {dataset_url}') + + except Exception: + print(f"10x Genomics website is preventing an automatic download. Please visit " + f"https://www.10xgenomics.com/resources/datasets/10-k-heart-cells-from-an-e-18-mouse-v-3-chemistry-3-standard-3-0-0 " + f"in a browser and manually download 'Gene / cell matrix HDF5 (raw)' into " + f"this folder and re-run this script.") + sys.exit() + +print("Loading the dataset...") +adata = load_anndata_from_input(dataset_local_filename) +num_raw_genes = adata.shape[1] +print(f"Raw dataset size {adata.shape}") + +print(f"Trimming {dataset_name}...") + +# select 'num_genes_to_keep' highly expressed genes +total_gene_expression = np.array(adata.X.sum(axis=0)).squeeze() +genes_to_keep_indices = np.argsort(total_gene_expression)[::-1][:num_genes_to_keep] + +# slice dataset on genes +adata = adata[:, genes_to_keep_indices].copy() + +# find cells and empties +umi_per_barcode = np.array(adata.X.sum(axis=1)).squeeze() +umi_sorted_barcode_indices = np.argsort(umi_per_barcode)[::-1] +cell_indices = umi_sorted_barcode_indices[:expected_cells] +last_barcode = (umi_per_barcode > low_count_threshold).sum() +empty_indices = umi_sorted_barcode_indices[start_of_empties:last_barcode] + +# putative list of barcodes to keep +cell_barcodes_to_keep_indices = np.asarray(cell_indices)[ + rng.permutation(len(cell_indices))[:num_cell_barcodes_to_keep]].tolist() +empty_barcodes_to_keep_indices = np.asarray(empty_indices)[ + rng.permutation(len(empty_indices))[:num_empty_barcodes_to_keep]].tolist() +barcodes_to_keep_indices = cell_barcodes_to_keep_indices + empty_barcodes_to_keep_indices + +# slice dataset on barcodes +adata = adata[barcodes_to_keep_indices].copy() + +# compensate for lost counts (due to the reduced number of genes) +adata.X = adata.X * int((num_raw_genes / num_genes_to_keep) ** 0.25) + +print(f"Number of genes in the trimmed dataset: {len(genes_to_keep_indices)}") +print(f"Number of barcodes in the trimmed dataset: {len(barcodes_to_keep_indices)}") +print(f"Expected number of cells in the trimmed dataset: {num_cell_barcodes_to_keep}") + +print(adata) + +# save +output_file = 'tiny_raw_feature_bc_matrix.h5ad' +print(f"Saving the trimmed dataset as {output_file} ...") +adata.write(output_file) + +print("Done!") diff --git a/examples/remove_background/generate_tiny_10x_pbmc.py b/examples/remove_background/generate_tiny_10x_pbmc.py deleted file mode 100755 index fff6fdb..0000000 --- a/examples/remove_background/generate_tiny_10x_pbmc.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python - -import urllib.request -import sys -import tarfile -import os -import numpy as np -from scipy.io import mmread, mmwrite -import pandas as pd -import operator -import shutil - -dataset_name = "pbmc4k (CellRanger 2.1.0, v2 Chemistry)" -dataset_url = "http://cf.10xgenomics.com/samples/cell-exp/2.1.0/pbmc4k/pbmc4k_raw_gene_bc_matrices.tar.gz" -dataset_local_filename = "pbmc4k_raw_gene_bc_matrices.tar.gz" -expected_cells = 4000 -num_cell_barcodes_to_keep = 500 -num_empty_barcodes_to_keep = 50_000 -num_genes_to_keep = 100 -random_seed = 1984 -rng = np.random.RandomState(random_seed) - -print(f"Downloading {dataset_name}...") -try: - urllib.request.urlretrieve(dataset_url, dataset_local_filename) -except IOError: - print(f"Could not retrieve {dataset_name} -- cannot continue!") - sys.exit() - -print(f"Extracting {dataset_name}...") -tar = tarfile.open(dataset_local_filename, "r:gz") -tar.extractall() -tar.close() - -extracted_dataset_local_path = os.path.join(os.curdir, "raw_gene_bc_matrices") -matrix_mtx_path = os.path.join(extracted_dataset_local_path, "GRCh38", "matrix.mtx") -genes_tsv_path = os.path.join(extracted_dataset_local_path, "GRCh38", "genes.tsv") -barcodes_tsv_path = os.path.join(extracted_dataset_local_path, "GRCh38", "barcodes.tsv") - -print("Loading the gene expression matrix...") -matrix_mtx = mmread(matrix_mtx_path) -num_raw_genes = matrix_mtx.shape[0] -num_raw_barcodes = matrix_mtx.shape[1] - -print("Loading genes and barcodes...") -genes_df = pd.read_csv(genes_tsv_path, delimiter="\t", header=None) -barcodes_df = pd.read_csv(barcodes_tsv_path, delimiter="\t", header=None) - -print(f"Trimming {dataset_name}...") -# (naive) indices of expected cells -umi_per_barcode = np.asarray(np.sum(matrix_mtx, 0)).flatten() -total_gene_expression = np.asarray(np.sum(matrix_mtx, 1)).flatten() -umi_sorted_barcode_indices = list( - map(operator.itemgetter(0), - sorted(enumerate(umi_per_barcode), key=operator.itemgetter(1), reverse=True))) -cell_indices = umi_sorted_barcode_indices[:expected_cells] -empty_indices = umi_sorted_barcode_indices[expected_cells:] - -# (naive) filter counts to non-empty droplets -cell_counts_csr = matrix_mtx.tocsc()[:, cell_indices].tocsr() - -# select 'num_genes_to_keep' highly expressed genes -genes_to_keep_indices = sorted(range(num_raw_genes), - key=lambda x: total_gene_expression[x], reverse=True)[:num_genes_to_keep] - -# putative list of barcodes to keep -cell_barcodes_to_keep_indices = np.asarray(cell_indices)[ - rng.permutation(len(cell_indices))[:num_cell_barcodes_to_keep]].tolist() -empty_barcodes_to_keep_indices = np.asarray(empty_indices)[ - rng.permutation(len(empty_indices))[:num_empty_barcodes_to_keep]].tolist() -barcodes_to_keep_indices = cell_barcodes_to_keep_indices + empty_barcodes_to_keep_indices - -# remove barcodes with zero expression on kept genes -trimmed_counts_matrix = matrix_mtx.tocsr()[genes_to_keep_indices, :].tocsc()[:, barcodes_to_keep_indices] -umi_per_putatively_kept_barcodes = np.asarray(np.sum(trimmed_counts_matrix, 0)).flatten() -barcodes_to_keep_indices = np.asarray(barcodes_to_keep_indices)[umi_per_putatively_kept_barcodes > 0] -barcodes_to_keep_indices = barcodes_to_keep_indices.tolist() - -# slice the raw dataset -tiny_matrix_mtx = matrix_mtx.tocsr()[genes_to_keep_indices, :].tocsc()[:, barcodes_to_keep_indices].tocoo() -tiny_genes_df = genes_df.loc[genes_to_keep_indices] -tiny_barcodes_df = barcodes_df.loc[barcodes_to_keep_indices] - -# compensate for lost counts (due to the reduced number of genes) -tiny_matrix_mtx = tiny_matrix_mtx * int((num_raw_genes / num_genes_to_keep) ** 0.25) - -print(f"Number of genes in the trimmed dataset: {len(genes_to_keep_indices)}") -print(f"Number of barcodes in the trimmed dataset: {len(barcodes_to_keep_indices)}") -print(f"Expected number of cells in the trimmed dataset: {num_cell_barcodes_to_keep}") - -# save -print("Saving the trimmed dataset...") -output_path = os.path.join(os.curdir, 'tiny_raw_gene_bc_matrices', 'GRCh38') -os.makedirs(output_path, exist_ok=True) -mmwrite(os.path.join(output_path, "matrix.mtx"), tiny_matrix_mtx) -tiny_genes_df.to_csv(os.path.join(output_path, "genes.tsv"), sep='\t', header=None, index=False) -tiny_barcodes_df.to_csv(os.path.join(output_path, "barcodes.tsv"), sep='\t', header=None, index=False) - -print("Cleaning up...") -shutil.rmtree(extracted_dataset_local_path, ignore_errors=True) -os.remove(dataset_local_filename) - -print("Done!") diff --git a/readthedocs.yml b/readthedocs.yml index 91ace26..6e320be 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -3,6 +3,6 @@ version: 2 python: version: 3.7 install: - - requirements: REQUIREMENTS-RTD.txt + - requirements: requirements-rtd.txt - method: pip path: . diff --git a/REQUIREMENTS-RTD.txt b/requirements-rtd.txt similarity index 100% rename from REQUIREMENTS-RTD.txt rename to requirements-rtd.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3585b6a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +numpy +scipy +tables +pandas +pyro-ppl>=1.8.4 +torch +matplotlib +anndata>=0.7 +loompy +ipython +jupyter +jupyter_contrib_nbextensions +notebook<7.0.0 +nbconvert<7.0.0 +psutil diff --git a/setup.py b/setup.py index 9078221..106fa57 100644 --- a/setup.py +++ b/setup.py @@ -2,35 +2,58 @@ import os import setuptools +import codecs +from typing import List -def readme(): +def read(rel_path): + here = os.path.abspath(os.path.dirname(__file__)) + with codecs.open(os.path.join(here, rel_path), 'r') as fp: + return fp.read() + + +def get_version() -> str: + for line in read('cellbender/__init__.py').splitlines(): + if line.startswith('__version__'): + delim = '"' if '"' in line else "'" + return line.split(delim)[1] + else: + raise RuntimeError("Unable to find version string.") + + +def readme() -> str: with open('README.rst') as f: return f.read() -def get_requirements_filename(): +def _readlines(filename, filebase=''): + with open(os.path.join(filebase, filename)) as f: + lines = f.readlines() + return lines + + +def get_requirements() -> List[str]: + requirements = _readlines('requirements.txt') if 'READTHEDOCS' in os.environ: - return "REQUIREMENTS-RTD.txt" - elif 'DOCKER' in os.environ: - return "REQUIREMENTS-DOCKER.txt" - else: - return "REQUIREMENTS.txt" + requirements.extend(get_rtd_requirements()) + return requirements -install_requires = [ - line.rstrip() for line in open(os.path.join(os.path.dirname(__file__), get_requirements_filename())) -] +def get_rtd_requirements() -> List[str]: + requirements = _readlines('requirements-rtd.txt') + return requirements + setuptools.setup( name='cellbender', - version='0.2.2', + version=get_version(), description='A software package for eliminating technical artifacts from ' 'high-throughput single-cell RNA sequencing (scRNA-seq) data', long_description=readme(), + long_description_content_type='text/x-rst', classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Science/Research' + 'Development Status :: 4 - Beta', + 'Intended Audience :: Science/Research', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 3.7', 'Topic :: Scientific/Engineering :: Bio-Informatics', @@ -40,10 +63,15 @@ def get_requirements_filename(): author='Stephen Fleming, Mehrtash Babadi', license='BSD (3-Clause)', packages=setuptools.find_packages(), - install_requires=install_requires, + install_requires=get_requirements(), + extras_require={ + "dev": ["pytest", "scikit-learn"], + "docs": get_rtd_requirements(), + }, entry_points={ 'console_scripts': ['cellbender=cellbender.base_cli:main'], }, include_package_data=True, + package_data={'': ['*.ipynb']}, # include the report template zip_safe=False ) diff --git a/wdl/README.rst b/wdl/README.rst index 2232567..b82743c 100644 --- a/wdl/README.rst +++ b/wdl/README.rst @@ -8,7 +8,7 @@ enable CellBender commands to be run on cloud computing architecture. ``remove-background`` --------------------- -The workflow that runs ``cellbender remove-background`` on a (Tesla K80) GPU on a +The workflow that runs ``cellbender remove-background`` on a (Tesla T4) GPU on a Google Cloud virtual machine is located at ``wdl/cellbender_remove_background.wdl``. Method inputs: @@ -20,9 +20,8 @@ refer to the `documentation Required: -* ``input_10x_h5_file_or_mtx_directory``: Path to raw count matrix output by 10x - ``cellranger count`` as either a .h5 file or as the directory that contains - ``matrix.mtx(.gz)``. In the `Terra `_ +* ``input_file_unfiltered``: Path to raw count matrix such as an .h5 file from + ``cellranger count``, or as a .h5ad or a DGE-format .csv. In the `Terra `_ data model, this could be ``this.h5_file``, where "h5_file" is the column of the data model that contains the path to the raw count matrix h5. Alternatively, this could be a Google bucket path as a String (e.g. @@ -45,40 +44,31 @@ Optional and recommended: be called as either cell or empty droplet by the inference procedure. Any droplets not included in the ``total_droplets_included`` largest UMI-count droplets will be treated as surely empty. +* ``output_bucket_base_directory``: Google bucket path (gsURL) to a directory where + you want outputs to be copied. Within that directory, a new folder will appear + called `sample_name`, and all outputs will go there. Note that your data will + then be in two locations in the Cloud: here and wherever Cromwell has its + execution directory (if using Terra, this is in your workspace's Google bucket). Optional: * ``learning_rate``: The learning rate used during inference, as a Float (e.g. ``1e-4``). * ``epochs``: Number of epochs to train during inference, as an Int (e.g. 150). -* ``model``: One of {"full", "ambient", "swapping", "simple"}. This specifies how - the count data should be modeled. "full" specifies an ambient RNA plus chimera - formation model, while "ambient" specifies a model with only ambient RNA, and - "swapping" specifies a model with only chimera formation. "simple" should not - be used in this context. * ``low_count_threshold``: An Int that specifies a number of unique UMIs per droplet (e.g. 15). Droplets with total unique UMI count below ``low_count_threshold`` will be entirely excluded from the analysis. They are assumed not even to be empty droplets, but some barcode error artifacts that are not useful for inference. -* ``blacklist_genes``: A whitespace-delimited String of integers - (e.g. "523 10021 10022 33693 33694") that specifies genes that should be completely - excluded from analysis. Counts of these genes are set to zero in the output count matrix. - Genes are specified by the integer that indexes them in the count matrix. - -Optional but discouraged: - -[There should not be any need to change these parameters from their default values.] - -* ``z_dim``: Dimension of the latent gene expression space, as an Int. Use a smaller - value (e.g. 20) for slightly more imputation, and a larger value (e.g. 200) for - less imputation. -* ``z_layers``: Architecture of the neural network autoencoder for the latent representation - of gene expression. ``z_layers`` specifies the size of each hidden layer. - Input as a whitespace-delimited String of integers, (e.g. "1000"). - Only use one hidden layer. [Two hidden layers could be specified with "500 100" for - example, but only one hidden layer should be used.] -* ``empty_drop_training_fraction``: Specifies what fraction of the data in each - minibatch should come from surely empty droplets, as a Float (e.g. 0.3). +* ``projected_ambient_count_threshold``: The larger this number, the fewer genes + (features) are analyzed, and the faster the tool will run. Default is 0.1. The + value represents the expected number of ambient counts (summed over all cells) + that we would estimate based on naive assumptions. Features with fewer expected + counts than this threshold will be ignored, and the output for those features will + be identical to the input. + +Other input parameters are explained `in the documentation +`_, +but are only useful in rare cases. Optional runtime specifications: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -87,8 +77,9 @@ Software: * ``docker_image``: Name of the docker image which will be used to run the ``cellbender remove-background`` command, as a String that references an image - either on the Google Container Registry (e.g. "us.gcr.io/broad-dsde-methods/cellbender:latest") - or Dockerhub (e.g. "dockerhub_username/image_name"). Images should be 3GB or smaller. + with CellBender installed (e.g. "us.gcr.io/broad-dsde-methods/cellbender:0.3.0"). + Note that this WDL may not be compatible with other versions of CellBender due + to changes in input arguments. Hardware: @@ -100,23 +91,40 @@ Hardware: * ``hardware_disk_size_GB``: Specify the size of the disk attached to the VM, as an Int in units of GB. * ``hardware_preemptible_tries``: Specify the number of preemptible runs to attempt, - as an Int. Preemptible runs are 1/3 to 1/4 the cost. If the run gets pre-empted + as an Int. Preemptible runs are 1/3 to 1/4 the cost. If the run gets preempted ``hardware_preemptible_tries`` times, a final non-preemptible run is carried out. + Work is not lost during preemption because the workflow uses + `checkpointing `_ + to pick up (near) where it left off. Outputs: ~~~~~~~~ -``cellbender remove-background`` outputs five files, and each of these output files is +``cellbender remove-background`` outputs several files, and each of these files is included as an output of the workflow. -If multiple FPR values are specified, then separate ``h5`` and - -* ``h5_array``: Array of output count matrix files, with background RNA removed. -* ``output_directory``: Same as the workflow input parameter, useful for populating - a column of the Terra data model. -* ``csv``: CSV file containing all the droplet barcodes which were determined to have +If multiple FPR values are specified, then separate ``.h5`` and ``report.html`` +``metrics.csv`` files will be produced, one for each FPR. + +* ``h5_array``: Array of output count matrix files (one for each FPR), with + background RNA removed. +* ``html_report_array``: Array of HTML output reports (one for each FPR) +* ``metrics_csv_array``: Array of CSV files that include output metrics (one for + each FPR) +* ``output_directory``: If the input `output_base_directory` was blank, this + will be black too. Same as the workflow input parameter, but with the + `sample_name` subdirectory added: useful for populating + a column of the Terra data model. All outputs are copied here. +* ``cell_barcodes_csv``: CSV file containing all the droplet barcodes which were determined to have a > 50% posterior probability of containing cells. Barcodes are written in plain text. This information is also contained in each of the above outputs, but is included as a separate output for convenient use in certain downstream applications. -* ``pdf``: PDF file that provides a standard graphical summary of the inference procedure. +* ``summary_pdf``: PDF file that provides a quick graphical summary of the run. * ``log``: Log file produced by the ``cellbender remove-background`` run. + + +Cost: +~~~~~ + +The cost to run a single sample in v0.3.0 on a preemptible Nvidia Tesla T4 GPU +on Google Cloud hovers somewhere in the ballpark of diff --git a/wdl/cellbender_remove_background.wdl b/wdl/cellbender_remove_background.wdl index 014bd88..4efb8fa 100644 --- a/wdl/cellbender_remove_background.wdl +++ b/wdl/cellbender_remove_background.wdl @@ -1,4 +1,6 @@ -## Copyright Broad Institute, 2020 +version 1.0 + +## Copyright Broad Institute, 2022 ## ## LICENSING : ## This script is released under the WDL source code license (BSD-3) @@ -6,93 +8,234 @@ task run_cellbender_remove_background_gpu { - # File-related inputs - String sample_name - File input_10x_h5_file_or_mtx_directory - - # Outputs - String output_directory # Google bucket path - - # Docker image for cellbender remove-background version - String? docker_image = "us.gcr.io/broad-dsde-methods/cellbender:0.2.0" - - # Method configuration inputs - Int? expected_cells - Int? total_droplets_included - String? model - Int? low_count_threshold - String? fpr # in quotes: floats separated by whitespace: the output false positive rate(s) - Int? epochs - Int? z_dim - String? z_layers # in quotes: integers separated by whitespace - Float? empty_drop_training_fraction - String? blacklist_genes # in quotes: integers separated by whitespace - Float? learning_rate - Boolean? exclude_antibody_capture = false - - # Hardware-related inputs - String? hardware_zones = "us-east1-d us-east1-c us-central1-a us-central1-c us-west1-b" - Int? hardware_disk_size_GB = 50 - Int? hardware_boot_disk_size_GB = 20 - Int? hardware_preemptible_tries = 0 - Int? hardware_cpu_count = 4 - Int? hardware_memory_GB = 15 - String? hardware_gpu_type = "nvidia-tesla-t4" - - command { - cellbender remove-background \ - --input "${input_10x_h5_file_or_mtx_directory}" \ - --output "${sample_name}_out.h5" \ - --cuda \ - ${"--expected-cells " + expected_cells} \ - ${"--total-droplets-included " + total_droplets_included} \ - ${"--fpr " + fpr} \ - ${"--model " + model} \ - ${"--low-count-threshold " + low_count_threshold} \ - ${"--epochs " + epochs} \ - ${"--z-dim " + z_dim} \ - ${"--z-layers " + z_layers} \ - ${"--empty-drop-training-fraction " + empty_drop_training_fraction} \ - ${"--blacklist-genes " + blacklist_genes} \ - ${"--learning-rate " + learning_rate} \ - ${true="--exclude-antibody-capture" false=" " exclude_antibody_capture} - - gsutil -m cp ${sample_name}_out* ${output_directory}/${sample_name}/ - } - - output { - File log = "${sample_name}_out.log" - File pdf = "${sample_name}_out.pdf" - File csv = "${sample_name}_out_cell_barcodes.csv" - Array[File] h5_array = glob("${sample_name}_out*.h5") # v2 creates a number of outputs depending on "fpr" - String output_dir = "${output_directory}/${sample_name}" - } - - runtime { - docker: "${docker_image}" - bootDiskSizeGb: hardware_boot_disk_size_GB - disks: "local-disk ${hardware_disk_size_GB} HDD" - memory: "${hardware_memory_GB}G" - cpu: hardware_cpu_count - zones: "${hardware_zones}" - gpuCount: 1 - gpuType: "${hardware_gpu_type}" - preemptible: hardware_preemptible_tries - maxRetries: 0 - } + input { + + # File-related inputs + String sample_name + File input_file_unfiltered # all barcodes, raw data + File? barcodes_file # for MTX and NPZ formats, the bacode information is in a separate file + File? genes_file # for MTX and NPZ formats, the gene information is in a separate file + File? checkpoint_file # start from a saved checkpoint + File? truth_file # only for developers using simulated data + + # Outputs + String? output_bucket_base_directory # Google bucket path + + # Docker image with CellBender + String? docker_image = "us.gcr.io/broad-dsde-methods/cellbender:0.3.0" + + # Used by developers for testing non-dockerized versions of CellBender + String? dev_git_hash__ # leave blank to run CellBender normally + + # Method configuration inputs + Int? expected_cells + Int? total_droplets_included + Float? force_cell_umi_prior + Float? force_empty_umi_prior + String? model + Int? low_count_threshold + String? fpr # in quotes: floats separated by whitespace: the output false positive rate(s) + Int? epochs + Int? z_dim + String? z_layers # in quotes: integers separated by whitespace + Float? empty_drop_training_fraction + Float? learning_rate + String? exclude_feature_types # in quotes: strings separated by whitespace + String? ignore_features # in quotes: integers separated by whitespace + Float? projected_ambient_count_threshold + Float? checkpoint_mins + Float? final_elbo_fail_fraction + Float? epoch_elbo_fail_fraction + Int? num_training_tries + Float? learning_rate_retry_mult + Int? posterior_batch_size + Boolean? estimator_multiple_cpu + Boolean? constant_learning_rate + Boolean? debug + + # Hardware-related inputs + String? hardware_zones = "us-east1-d us-east1-c us-central1-a us-central1-c us-west1-b" + Int? hardware_disk_size_GB = 50 + Int? hardware_boot_disk_size_GB = 20 + Int? hardware_preemptible_tries = 2 + Int? hardware_cpu_count = 4 + Int? hardware_memory_GB = 32 + String? hardware_gpu_type = "nvidia-tesla-t4" + String? nvidia_driver_version = "470.82.01" # need >=465.19.01 for CUDA 11.3 + + } + + # For development only: install a non dockerized version of CellBender + Boolean install_from_git = (if defined(dev_git_hash__) then true else false) + + # Compute the output bucket directory for this sample: output_bucket_base_directory/sample_name/ + String output_bucket_directory = (if defined(output_bucket_base_directory) + then sub(select_first([output_bucket_base_directory]), "/+$", "") + "/${sample_name}/" + else "") + + command { + + set -e # fail the workflow if there is an error + + # install a specific commit of cellbender from github if called for (-- developers only) + if [[ ~{install_from_git} == true ]]; then + echo "Uninstalling pre-installed cellbender" + yes | pip uninstall cellbender + echo "Installing cellbender from github" + # this more succinct version is broken in some older versions of cellbender + echo "pip install --no-cache-dir -U git+https://github.com/broadinstitute/CellBender.git@~{dev_git_hash__}" + # yes | pip install --no-cache-dir -U git+https://github.com/broadinstitute/CellBender.git@~{dev_git_hash__} + # this should always work + git clone -q https://github.com/broadinstitute/CellBender.git /cromwell_root/CellBender + cd /cromwell_root/CellBender + git checkout -q ~{dev_git_hash__} + yes | pip install --no-cache-dir -U -e /cromwell_root/CellBender + pip list + cd /cromwell_root + fi + + # put the barcodes_file in the right place, if it is provided + if [[ ! -z "~{barcodes_file}" ]]; then + dir=$(dirname ~{input_file_unfiltered}) + if [[ "~{input_file_unfiltered}" == *.npz ]]; then + name="row_index.npy" + elif [[ "~{barcodes_file}" == *.gz ]]; then + name="barcodes.tsv.gz" + else + name="barcodes.tsv" + fi + echo "Moving barcodes file to "$dir"/"$name + echo "mv ~{barcodes_file} "$dir"/"$name + [ -f $dir/$name ] || mv ~{barcodes_file} $dir/$name + fi + + # put the genes_file in the right place, if it is provided + if [[ ! -z "~{genes_file}" ]]; then + dir=$(dirname ~{input_file_unfiltered}) + if [[ "~{input_file_unfiltered}" == *.npz ]]; then + name="col_index.npy" + elif [[ "~{genes_file}" == *.gz ]]; then + name="features.tsv.gz" + else + name="genes.tsv" + fi + echo "Moving genes file to "$dir"/"$name + echo "mv ~{genes_file} "$dir"/"$name + [ -f $dir/$name ] || mv ~{genes_file} $dir/$name + fi + + cellbender remove-background \ + --input "~{input_file_unfiltered}" \ + --output "~{sample_name}_out.h5" \ + --cuda \ + ~{"--checkpoint " + checkpoint_file} \ + ~{"--expected-cells " + expected_cells} \ + ~{"--total-droplets-included " + total_droplets_included} \ + ~{"--fpr " + fpr} \ + ~{"--model " + model} \ + ~{"--low-count-threshold " + low_count_threshold} \ + ~{"--epochs " + epochs} \ + ~{"--force-cell-umi-prior " + force_cell_umi_prior} \ + ~{"--force-empty-umi-prior " + force_empty_umi_prior} \ + ~{"--z-dim " + z_dim} \ + ~{"--z-layers " + z_layers} \ + ~{"--empty-drop-training-fraction " + empty_drop_training_fraction} \ + ~{"--exclude-feature-types " + exclude_feature_types} \ + ~{"--ignore-features " + ignore_features} \ + ~{"--projected-ambient-count-threshold " + projected_ambient_count_threshold} \ + ~{"--learning-rate " + learning_rate} \ + ~{"--checkpoint-mins " + checkpoint_mins} \ + ~{"--final-elbo-fail-fraction " + final_elbo_fail_fraction} \ + ~{"--epoch-elbo-fail-fraction " + epoch_elbo_fail_fraction} \ + ~{"--num-training-tries " + num_training_tries} \ + ~{"--learning-rate-retry-mult " + learning_rate_retry_mult} \ + ~{"--posterior-batch-size " + posterior_batch_size} \ + ~{true="--estimator-multiple-cpu " false=" " estimator_multiple_cpu} \ + ~{true="--constant-learning-rate " false=" " constant_learning_rate} \ + ~{true="--debug " false=" " debug} \ + ~{"--truth " + truth_file} + + # copy outputs to google bucket if output_bucket_base_directory is supplied + if [[ ! -z "~{output_bucket_directory}" ]]; then + echo "Copying output data to ~{output_bucket_directory} using gsutil cp" + gsutil -m cp ~{sample_name}_out* ~{output_bucket_directory} + fi + + } + + output { + File log = "${sample_name}_out.log" + File pdf = "${sample_name}_out.pdf" + File cell_csv = "${sample_name}_out_cell_barcodes.csv" + Array[File] metrics_array = glob("${sample_name}_out*_metrics.csv") # a number of outputs depending on "fpr" + Array[File] report_array = glob("${sample_name}_out*_report.html") # a number of outputs depending on "fpr" + Array[File] h5_array = glob("${sample_name}_out*.h5") # a number of outputs depending on "fpr" + String output_dir = "${output_bucket_directory}" + File ckpt_file = "ckpt.tar.gz" + } + + runtime { + docker: "${docker_image}" + bootDiskSizeGb: hardware_boot_disk_size_GB + disks: "local-disk ${hardware_disk_size_GB} HDD" + memory: "${hardware_memory_GB}G" + cpu: hardware_cpu_count + zones: "${hardware_zones}" + gpuCount: 1 + gpuType: "${hardware_gpu_type}" + nvidiaDriverVersion: "${nvidia_driver_version}" + preemptible: hardware_preemptible_tries + checkpointFile: "ckpt.tar.gz" + maxRetries: 0 # can be used in case of a PAPI error code 2 failure to install GPU drivers + } + meta { + author: "Stephen Fleming" + email: "sfleming@broadinstitute.org" + description: "WDL that runs CellBender remove-background on a GPU on Google Cloud hardware. See the [CellBender GitHub repo](https://github.com/broadinstitute/CellBender) and [read the documentation](https://cellbender.readthedocs.io/en/v0.3.0/reference/index.html#command-line-options) for more information." + } + + parameter_meta { + sample_name : + {help: "Name which will be prepended to output files and which will be used to construct the output google bucket file path, if output_bucket_base_directory is supplied, as output_bucket_base_directory/sample_name/"} + input_file_unfiltered : + {help: "Input file. This must be raw data that includes all barcodes. See http://cellbender.readthedocs.io for more information on file formats, but importantly, this must be one file, and cannot be a pointer to a directory that contains MTX and TSV files."} + barcodes_file : + {help: "Supply this only if your input_file_unfiltered is a sparse NPZ matrix from Optimus lacking metadata."} + genes_file : + {help: "Supply this only if your input_file_unfiltered is a sparse NPZ matrix from Optimus lacking metadata."} + output_bucket_base_directory : + {help: "Optional google bucket gsURL. If provided, the workflow will create a subfolder called sample_name in this directory and copy outputs there. (Note that the output data would then exist in two places.) Helpful for organization."} + docker_image : + {help: "CellBender docker image. Not all CellBender docker image tags will be compatible with this WDL.", + suggestions: ["us.gcr.io/broad-dsde-methods/cellbender:0.3.0"]} + checkpoint_file : + {help: "Optional gsURL for a checkpoint file created using a previous run ('ckpt.tar.gz') on the same dataset (using the same CellBender docker image). This can be used to create a new output with a different --fpr without re-doing the training run."} + truth_file : + {help: "Optional file only used by CellBender developers or those trying to benchmark CellBender remove-background on simulated data. Normally, this input would not be supplied."} + hardware_preemptible_tries : + {help: "If nonzero, CellBender will be run on a preemptible instance, at a lower cost. If preempted, your run will not start from scratch, but will start from a checkpoint that is saved by CellBender and recovered by Cromwell. For example, if hardware_preemptible_tries is 2, your run will attempt twice using preemptible instances, and if the job is preempted both times before completing, it will finish on a non-preemptible machine. The cost savings is significant. The potential drawback is that preemption wastes time."} + checkpoint_mins : + {help: "Time in minutes between creation of checkpoint files. Bear in mind that Cromwell copies checkpoints to a bucket every ten minutes."} + hardware_gpu_type : + {help: "Specify the type of GPU that should be used. Ensure that the selected hardware_zones have the GPU available.", + suggestions: ["nvidia-tesla-t4", "nvidia-tesla-k80"]} + } } workflow cellbender_remove_background { - call run_cellbender_remove_background_gpu + call run_cellbender_remove_background_gpu - output { - File log = run_cellbender_remove_background_gpu.log - File pdf = run_cellbender_remove_background_gpu.pdf - File csv = run_cellbender_remove_background_gpu.csv - Array[File] h5_array = run_cellbender_remove_background_gpu.h5_array - String output_directory = run_cellbender_remove_background_gpu.output_dir - } + output { + File log = run_cellbender_remove_background_gpu.log + File summary_pdf = run_cellbender_remove_background_gpu.pdf + File cell_barcodes_csv = run_cellbender_remove_background_gpu.cell_csv + Array[File] metrics_csv_array = run_cellbender_remove_background_gpu.metrics_array + Array[File] html_report_array = run_cellbender_remove_background_gpu.report_array + Array[File] h5_array = run_cellbender_remove_background_gpu.h5_array + String output_directory = run_cellbender_remove_background_gpu.output_dir + File checkpoint_file = run_cellbender_remove_background_gpu.ckpt_file + } } From 02dd763fdb3b740a100cf3251cca3525ed9d134a Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Mon, 7 Aug 2023 17:29:31 -0400 Subject: [PATCH 02/19] Add Nature Methods citation and update docs --- README.rst | 2 +- .../_static/remove_background/v0.2.2_hgmm.png | Bin 0 -> 203431 bytes .../_static/remove_background/v0.3.0_hgmm.png | Bin 0 -> 187301 bytes .../remove_background/v0.3.0_rc_hgmm.png | Bin 0 -> 212594 bytes docs/source/changelog/index.rst | 54 ++++++++++++++++++ docs/source/citation/index.rst | 5 +- docs/source/contributing/index.rst | 6 ++ docs/source/introduction/index.rst | 4 +- docs/source/troubleshooting/index.rst | 8 +-- docs/source/usage/index.rst | 35 ++++++++---- 10 files changed, 92 insertions(+), 22 deletions(-) create mode 100644 docs/source/_static/remove_background/v0.2.2_hgmm.png create mode 100644 docs/source/_static/remove_background/v0.3.0_hgmm.png create mode 100644 docs/source/_static/remove_background/v0.3.0_rc_hgmm.png diff --git a/README.rst b/README.rst index ea39e2c..d5895c7 100644 --- a/README.rst +++ b/README.rst @@ -149,6 +149,6 @@ Stephen J Fleming, Mark D Chaffin, Alessandro Arduini, Amer-Denis Akkad, Eric Banks, John C Marioni, Anthony A Phillipakis, Patrick T Ellinor, and Mehrtash Babadi. Unsupervised removal of systematic background noise from droplet-based single-cell experiments using CellBender. -`Nature Methods` (in press), 2023. +`Nature Methods`, 2023. https://doi.org/10.1038/s41592-023-01943-7 See also `our preprint on bioRxiv `_. diff --git a/docs/source/_static/remove_background/v0.2.2_hgmm.png b/docs/source/_static/remove_background/v0.2.2_hgmm.png new file mode 100644 index 0000000000000000000000000000000000000000..62a76adfca631a9ce43daa3c5d76b25f304c3e9e GIT binary patch literal 203431 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

-#V!w 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

    aI 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≪Ug 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

    kPwTaGKkHgmcU~}GPB#QEE z9=Kaur0~W45@?{K3V)-L5?-;O0w1NMRAKq;uH={`enTMptx@-{B%*~^HoirLWbbdQ z*Au0z*J+fEyA-jQgQq=yTw0%v(tV>o+xo4fm zrA$FIcYJzU(W{|JdyWC4`BiOx_Tz(zi#)`u^Hu~R=h_lUSMsR5cmMR1M4-e;V^)86 z#t_IVmt(O=%3<^mKf_9Il3G~*X@$XErMp?(sUt!Q)lF4=#IAgHy5jSty|d`9A;(> zYLq}wH}5ZQy#QNt^`#J&XS~MgO)TJc^up_BSsBGa~Rl1^%oBGv)O{LW)XC$sEq-!{KBCi5iLzSQGiNm8a<%$G_R~RAlPkd+Q-3Mq2-gVvS~)6NXhjpo5?hLsgAdi4#z76TIK!mBnd#zSf$LZg++CrP~kiwc{K8 z?@zZRBqkwj?meyAp&%W9`40-%b+Vl-FMsw?W2sZ(&DIjY?e6aiU!nn05q{*_xC9Qy zJ6Zp7a5pFP?m6E&_u@sXq&F%5!fHpQ3&+ki9w&`1kd^tQ#Kj#Ii@?FjHKJ}Ma20{K zwSy6r-d!wD;M(FQ*w;$+pCiS zf|uawGRDfR>=<$9Jz+`4`?DzfYyM>_^=Z}dj88nCQR!QKp?hs8cgeTrN;TLF!sy6P6S9(_EVAD?0ok7 zNlXe+Oli$oRFI0SU&8)w`3*$e65&6EGnH8iJ*&R5*PEpt!!VJR5Z z{?Y{$)g!$HYEx8Vr~r{J9zk2d+NHQ9gLxp6XT-QH<3PFCUSsJnrlr+yi@sm@S{|x6XJbvyR%Fn-Bo62zPTg2pS+ArV#$4!PaC&h|or66Xcq1|YvBTm-1#F)|MAB~Y<&E=+!ZWS`oC z{dL#s3bv@7-?R{0P#1M%H`2OQ+TZN@o3p$yq-a4X<8~FCl^ER~PoPXERxDmw;9)}~ zBG7JEn)}#Gl-Ym;S^aqhL18Em+96>9Jq8ZEE-QNHnnz3&Df-JplRz-Jukbl-rt3c5 zcMtXl?afa7kNLWc$9rxJ3QDb{q~Jv(0Kn|+!g(-d9NSglk}Jrz&$v{>Cp>6TWG`E> zzVlX>7n<^e6{Y(C@&6QL$HOD`4AxTH zcIi`A_@;vXer2LS-3((h=6J7$iKy7{i|6zH3CaDiXBMGopuXL>2% zCWZWYQvxTGZ{!cm4C?&@c%n(YvnCecdVf|0D5o5U4e1)K8C;iFS4|_k!L`;K-oCyw zG;fgw8(&06xfmR3xC4UTG$)R{uTkdcRF^?_$|o+n{W4oR@abh0+Ld*zS{kMXnKZc+ zuXw!nA}6<6$kpNzz_DPeHHfPGT}?Uh=+d%ttxI*S+O^c>3Y05mIMY=2=b9O*ucUnK zOqsN%Y^+X>fAI4`%PxF;01?9{dyd)eoBFh?H;Uv(uA`t6(XU z)D6#=9?KmFZp9ZI)rNw}21GN^muTv-2cL;!XDu?gEwBBNp`>5Q_Yn5Qz|Nt&DQSb~ zU$rcKAJBZ;hka|7w2-5V7t9y@jZnGhjeQzNSq^Z}jx^i{MUM)Qw?WH?t4a^=;Yj2^ zl4JDvAL#DBK`;B@e2_~D*7Wt`+4g_Adgrx>9i?r;*QfjW6GlWlE;g%w{1x&Qb6J03 z(<<40TXH+y8mS5yd}1p(KqdYW#-mu##dP;eSmwrn`$hTCM`;muOhTUa$E%hJzpxJ%vn&{Pm}%JR(aPuwe`*Fmhs`mdFHEfva=j*t7h? zrut^guCt^P3sCLqtn@PDjf7Wwr|U(4qH`G zUa3!)Ia4-w;+v>0MxvBK>6hn-zSmW4kDM$7$jjUI8S*ak&VKa`8o?K84EnESx_`Q5 z?0y=!TqN2f1qAv&$72CD#j0ID=mfK65K#zfmK(jNitpy~u!kmB8dyL*05&&D5azqQ ziUPfrr3IUY*>NkZ<#%4nvBb29Plbj}!fzEBoa=JXHE7M?|i1P{`>oRYUEM{5NIbir9N_z?|@ zxpMOISkNDAtfx<4J#W5BNvJhAhyt&9AP*LaSw9uu=%#!Us3v1NEMlTY5d3@VOrO#7 z7zPeu_+?Osc+ATvG&~l+PECnlEVYAw4;CeZmufK~#p17~E|Wt`$(@=B9In@wM{A(3 zb;Zi@IpsSb#Co#cm+8cT3YkHu^?wh9W^!M{_O zUdCc#8neu5Ox|J;5e@2>a}W#y{uoCPgf6hmJ<- zE~$pfmBlC>{2RA(g{hk2y1`3oquyB$E8lSi6;9WSO6ib3fwTv&yh0OGV7Xbv~S1 z{lI;8o@~%X(C+nkCO)q137@q+m6(JClUg;i<0v(Tp`IR8BqSq=n_s`OJuTGK)KB}S z3h(aLGR4ot%_tX|;%uDTu2Bqzk}pmin~D^dnq2J;XCk9)REu)-nUf4?lK*VLwg6Ly zQ+%1Jakct$M(C7yxOi|HNIXE$S0bYQ*{Z@qsnv=6eD^EC3uY?DL*HaKPRrZr<|%Uw zGNdze9+3RHBBG)*W?DJQ9w%n};c9-kcW#Y)SGllSz2v5T+>5id)FBZO8>QP zYcWdrp`2?}BIKi`I$dfa664V#*a3g)?IRBt7fQ6+;@)k-!rh`Uybvds;c=c9m8AFz znDOnj0G6sP(>uvBl*`@+@$Gp+?JtAv=t}on%azuv`1g^gOD>6k<5HwfHb(rx>6{^l z&18m@uK3b;NCO8o@ATvh5GlU(#T1@wH0rpV_eRs!0^NwW@658?90jI|pc4DTCJpzy ziL#rNY$Mf*!CP%R1Z-G!IPkLZZglx7k}JLQ_63ynyF$pv=8woy+Mldn4(>h-id$mTN*XTAvi6yE{5&#`@~Pg99}+z9d%|f3VYM&_l|NKhfj~ zR8s6cwWpg!-cVbZBGng2Vq-WL~2{IS>Jvduo<;oxAV zwic5rXsUR|y@eMVhHkCb1ByOVD*eI53WE-^GZhs5ZPp*Y)@O~5|BDZIgXo!#Fd&=5 zkv54$U^dV>@YC`x*}Ye+?_OLokq{T4%*;e?Cj;*hSeI05!bGO&Rm(CeGERRtIwv!# z3L(g9iT0#?hYR-2gK1fBZ|_KU;dGfUd61PO{^)W!lbsC9YiT$W(Gj@wJSk=5Te>f5 zZP-tdY}r`XbLG+Cc)d)$nK)(qsp8GL+K)Yb#2-v@j(D3dJ~}+ydm=8m-bf0ms2GZF zZ(~xy-rZI}$hZymfK@qTy>aSRK2W-TXdj z`0yRB{dF#Q)}`ffuv{Nz8-e=uD_|#nadPzR@5Rk!dT8X~yru42vn5eb$re=_o0^;J z>cfN>cCf+mZL(0O;1J88;vpWJgjQK^bHxl*tXl075ar0{Mn*)yq&Q!g>E{W0`I^s{ zkT5fk4s_J2&sDkKUu_PP@I#t?8M^e%8{?jqCmcI7zbPa;NTV z&nIv-Y_>;d20CWC=4ct%&J|@N0x`MXQEi6W8lxPlezCb8&jn58i0}w?^>xpICj+=O zpx60|K@P_4%bz?4lenfp{Vt?_&dVROEBF;06c5;LuXsOM9{Hf`Hh(QmGZpcx&ld-L z%FXspn3hmrlB6cfH?B3j9U>)t@rqoLd;E?BLFv-L3AUx`(i9=M?@s{eScc=B?~g;s zzJ_Y`P^@%eXHqR7drTwld%7{M6bSpWT-IuwJ)(DNALvcBt|sl;N%E4=z-_arlS zecFgDl2+HsTb{Ds`TbeEd2`|iS57Dh|J??VUo81^%tl1y>H8eUqkL22=h1BQ0|b=h zj~unMwY7P9Tgs0&Ya0NE5Ec+j;C33O(tTgGpANO~TF*2}?a$#ywRd#D@;WLgxP3}3 zvN(wufOrwU)Sr7lnE^IGh-Zgr5Wo)E%o2oB?;EZ+aq&Wf;UJW6hvZaKS5KjmjaXLb z?@<@AJ(%<5sL<*uXW|AlsNU{?nVr?H|0iIb?#2R4$mU@B1;hdYOWcvcjn4PKB@udibATVEu9>79ys-ebhb;$Ddhz83 zEj3(imFn0ZE*9n#jBXW5Km675aU1yH>SVrD`;LNDfhLlp>o-~k+q?@s%M z>eP~CGn*{e5TMgtr3-C4*!wXiAJAi_pXcrE{VJ*g#j%cAUDUEPv>Mma<*Zi^+B)mR zCn{9MA1D6kc3T^Wn`w3B{&073y<5DS9I!LvIbb)+_-y|#i{KYo6_rggT|GVQm?#Kv zVqEpCye++@2)STe?kZSpB~haJMxxPzL)eN4um}>I9-2a94FEQcy#7gA0|ys-J4uO3 zzyGCY#AKoB)y*|`Pv67lY-Ofqc+O%YObQMk2U$=-;hOOV?d20l#RGJuthD)vl8=r* z9QfLAAL}PC>b~ab?O#Rq;${HgrxMVWbYL^Eknj!8f~_pJc}vxes%H1MxwOHXKE~~+|$lVW6%Z(54kyBb<2=N z(^R^#(zk$P2wVZ%@;lk$v0vGpt8!AprlUN*@JMF5r8X(yqq%DkLp3%ahydC!GRVuj z`vWOs^|paeJj8Ja;docq`@j^gtZeS88sv~i2)a-7{F%)+Oh$ViMU4fZ2|Ko;4m*8J zMAGo?Wvm&A*;C~?5UMkn6(P9K`glRSfExOEBw2MJN2cUfF2(ZY@)#GhGOdpGjEo4R zMq)yK!AVm=AS8fVW~>eRl7i#s>R5@OFQ`FBYg9iQ1ky(!XxYaTR7|LPo7*<)A2}V# zRK3+1`T@LQt++cH5daMW*zN0lm`{~}`DjtLc4#U41oi~&v6P~`+8Mk{?m@!u^D}qt zUxvN_+|X=D)S=py8bvh%@!`mnB4&PVKi4FWn3LwM=Gg=_LLD82q-+mfnH)47j8%*iMuB?rhw_l z)3_u5=FmcXd3O(Ua82P2NkZ4Vk35P>?}GY3yYnd{03e^OS^62#Az2Xq)KV)FFwJ7Z zM_#j{Pe6W*YT{&6IuRkpWX^j@OKVH&wRn6tK*oB?%F0C~H?sPFN`rZVV=B9UHdfUb zG=I(b|LDTfNN~q$AmLFJxg_!h3>3*ls_Zd^O7J z$pRjW^?Gx_v)3?MAq`9()MC6p3&Y#|!L{k`pdJ|F_F+$midMs3ww0Anj?BAql8cMQw6qI3A)rl1j>=qK2F~4Sk4}6>9v;^} zbLRM1A4Gt04GauSOm&+syF?tPp7?DQy=pa`avAxG&5x8c0!V-0m%#-HItUY)T zr#>=AuQnvrob{WcKHYv5fIxt0jE5ALQ3O+%OAUWrp+cWIl0Wu%A_jlwsE?s|{rW8x z)tv5ZNRKYUlP7FhmN=26+8qJj#4fLaO6S%3X0cMi>t5^tBWQU1+w&a_&^}j%ec(s0 zT?rwg?m34Yg*`{dB6tq*ENux=L|P*w-On}#ek)4WTifH~o0aX+trUukj+-gT9s{8#jaya#=0R10L?FPQHeQEx2cl1 zVxwYWczXH}J-(VZq|#=LCWS=HO*NhL>?xO5x10C%I$Et=+iG9<4_A?dhlf+rg0Dcf zmH+~6dxFK}!_r1zN@AX?EA{;>?(Mq+wpSBHbeg^7xV4xeD|p%Sb0_t0>qF?s15 z{6ryFG8!ZZm~=dY+!hxr>4{7{q|)tnC$S;3#kz?UD(Bw?ape#%TnX@7zB>ji#KcFu z7N~WMUDm%;Ewf>Q2)JDwE)EtbX}`ggm6iQko%G_$&E>LdC|Ubmv_A)dmLbz?8+DS{ z6PxtVG+;0BDtYyp&Gy8Zlu zd_!UHj1u=In>E})0hRi@LEFJo*PAm$55BbdvQKu>jC*rvU|JfKAG(;wYpt_qEG;36 zt?qDa4L4;Oir)VI+l@JMDv6(W6O7MJ2e?0-+a>PGFWVdm{0MBfhiP?%3oAnn|Go_1}5V zA3uJr);=CVo-e3y&bhD*ZhaZscz(;2t6cX`jd&ucON$*Jodv&or{Cs-0xC$65oQ`v z67wZm{QWATzA(OOBJ@Xmh&3sBXux$*F|5h z&c--hcbRsK0H-q_D4FMEkn`}6u~ewT5lJo`8czkZuYz)k%SjvB((PZ*qWQiyvX%Z< zY;(Ec`|$!Kky44AprUe0))lhPQ{sO3{jSq?r}D(ywGhnddS7hu_5F>)l6x^N!8@nZ zDGhhi&(=V&$9mC`>gr%&aAl>g_2k(z*HgDBKexj#$@T;?oO0 zW=-46>|DBfy3E^sjLlD=Qi4~OLel==9}~QAmPy2O4GT^b7N4OJwKeY2e2PdJ_QUz&d-Lrkb!WQAA>qh!^vRo z8kO7C)n-8liUf^I52xQB~+4$DJGq`Uw2nDti)6>&DAOUwhkp%CY)mJN@ zk<#f3Aqnx{?yPitB&U|g>vu8%o)#sb0jY9TvE8@nRF>I(%URcEdevNbA19S*1QYZy zoI4)+@zUZ-iI#Eu#P9!b0Za%f!8>C@+9zfE3LivL zRfVkUMa0MA5@5gz!L8!~%h;>}gmni3s)f3({?{y}tNI_* z0WAZPzj4>(q|xNq80G^k88RsLNF}kmt!)gnw(^yJ>DMoBY-n(@IlMogBa8nw=+*f9 zK?6W%|KQ@<(j53WZoQoVY>iE99HI|a4yQRIRjApSa)2#)z6VoYFO5C{4ue|vWPkq_ za0a3Cp=|7|a_jX*Q3Eei&=3ax5=%cohT^)~Aqo@BYsR*w8?m??vae<$fXJt` z$yBYbEMXsBM)U&L!{v|AdMXH=9vzVZwII(Z4l`Jp6thC#s)Ht1fL=%TW$R9W;&Z8J zv4WgjRQ;O(`B7Q;W}*D>-1T*_7ts#FFPC08e=RkuwAqmrFcgAk zJy%qUA`mznMHMjY>uCK2K>y-sj}3zNLDx~$Z7`rxgJ}Vsd(dO37@Du-UT=hL*`G3E zg5OV@F1Ig&W(T9Bg;la%0)yjB&ZV*5d0LL z*QYDy$F7>{WdUfE3E>pBL5$DVto(H-vD<0dz}Mq*R8vQXANOSB`-m;u0pKO3dc7s0 z3hJ961iB`o@X9D*8m(IOTHL6Vd;blAj~?mZ(-B4s_U{))2nrLVxQLsxgB$+GyZx_} zWxHA;^VA?vcc(_jon-qlG&KWSl$JV2)+7ewwpzZ?%%mNS)M=ig0onlkDxY*Vo4we9 zm$TQV|FJlE2La`UmZOlcSr@FOo1^Xe-xe*$wE6Ke`Ai`N2F4-WO^(km=MP_z$Z5hnfDHo-n~I$%HQk&mM!Ng=*YG^icE& zjN1Y9f6v|XtJKHxPljBAZ%3sdH`RQPgq0O@WCtw7&(jEM@)!HNhNB%BsijQk6Zz!^ zgNjq5LnSW!fdc?ht#{_1`KEE65F#*L6ps(;$ECh=O0GfA)*1Nv9?oGZA!8qkX)+ zyxP>!XbR9_VcNX90kBjby_i?nDsZ@2!LsN_B_T&~Fn#R#_d?%kIDa^wTkw0j^ng13 zBIr5FfzZ6IoeiwVX+Z(-G@1d*NU~o*OtVh?+AB0afdhG?`q!7$ZZ&+8TDM zOmaA0(7+L-&4=XCVo%0Y2-}867+NS2zqr!;81SOLb@w#!kfT zfKG_nCg*rMEwjd;uEM5YI>S0KQLL?_qoW}i&#XWR^Oy(RuKXnA6Z!DPWU}uzHc3B; zhI|pTCm?ID`9uHG|ss5#%lAJ>M==LwwyAjzr(n{{Tk*=Q{3!6E4IX+oFoOtZm z6FAWm(Wh?@42#W&zG>{wp*)rbA%@U)}q5h`J%6)lhAvz>Ty-w}OdbN&{xnM&` z?B;S^N~tQWpcE}pRFMBB&%A&#{98+rZ_Ilqj7T2kfzm*JL)Y5iDg`lxy!0Z_aauW6 zZq2$!lw{krkn>(4+9*p;j5OCOB2qaH9!d}ZX~t_Cr^4D=$G&TzFU?e?xHMg}wOj0e z9Dv<-W@9^xO%M^8v$X}+}U>v>_p1P<)nT-0z<=0vV^1bsfxs-Ic0UJDTSXD&T;(*Bz@UEbc7JywMFlt`n? z#H=6{NeaQovLo|ANXSI9E0(1=Ckr;1?lToXA?AE=kVpFl=(5u`#mtGEwl%?k_WaS5 zCrsz>(NgD4)og7R6%=4vifU4(%8T;za?b17i>myq8QZh2U*abdS{ofCI&w>o_kB(l zkX?C#03CGtY<4EBn2C5S7^8Y^lj=-KzEG!v7F+cV`E5`()Zx?tuMn zXsFNDYw#y(`<=Xkf`8{%G9hlKV=rf0?AN*ZVc(EfQP%(^r3=Ft`X4I`i{VUh(ViZlW*8j&+!k*l zRrNiHui;@}zAi4#(~pgg-dF2cgVuH#(MPK{OVMXc4DC6^xOdy+Z>l+~{ZXLdcAxnj zHQL0Dsm!v}yq{nD429?l^NLrvCEhqKw6kV(=<+i)D)M6o7#qXftG8%-{-R+i{XVJv z!a$H!L9^t3VY|%8%5gU8J&Q%}Gr{6u*7~v0M&(qNY6C&CYgA zjWO92GID&``MI!>9h=mg<)gD@tFs$w*ki`D_<1(D4oS#-BCjPSQI4c?jSbI!vA*Zy z2eY24R_h|juRd-6JfmA5xb#_XNDT6??yeT!xY^oTM{E4ICqzuDqK3A3_l49}Npcxe zFh;sHrIJ{vZ_S=SL1$+R72Cc=it0VOyrgEx)v7}QuNxLKUBFdT%$nCpr3DJKy`0#f2m3IOS9HppOPhnesSxq zIz*JviI$e;;^GRElJD*K;09#omm0;z5Uzq}V7}r9PIcNP80%pF<^STusbCP!N1cHi zQo~FPky0fxkFdOO=GxfUz?k{|-t1aysHqX+rI2fFGe>*HoN+fNfV6rVHwL|R9L0Lk zY)Q=|+6dB@r!UAK)PDtICB^;wX@9S(qWViQMo~qTrn3$lM0e8)TO_NUrp zB(V&Yvp)y)>(BN?vt&aFI7pqsl0)%@vt*?cIXl!H4;Oh>9pB&m^?ZX*P5ro_N#$}E z!P-7MLqQ1%2?3wKU!7mGdvbhYzJ$t{pF>VGcyd9NBz0QyVh4cfBn3bJn~EH23y$4Q ztI6V&tQ4NP@NU%Gw$P6l4`N0}MuNh^)$X28sVQ$;K*^{l;?mx5aZELA6a)@@5kIKB zq+VGW4%Q5>^D_*@p>tA@Q~w-kN4G=<=FVNxNl&2SRQ{odGu+J`B{R&&=#qRE$ zi9*AiG5vuA6zJ@^kgT@0Y8@%IYLy8I(!+cst=J9GLOonddRM5Rd%$!}joG=~{r&xX zGx%dG}0H<$Zeh%GBTyU!pZC6u}Fs2H+D6?*}}?#nf+;x zBUfopWvcs+A_bXb2?@=6H;m{7TO256|zah#{7oih6r81XMioL%%S2wc8uM*7}Rq z!%{Asa(iHi8|5Koj{@)D(22#*DsmgL5P=R=;38QdL$FDl|XuR52S7_A}}UWOr^yk2^Cn_r6+G z0;4HB^a{S+yYxI+aYf76SPArpYKwsRL074CI{W_d9)cwC_LJT3x0jlm8_4)ZkB1hV z$szlTMZVcE1qJnmx+Q<~Z7FK-j zDlAg{N~5EqAdk2>$38(_1qDf4Tj&h~wV@taGD8RJ_Nccx9400vPKojEA2zDTP<#Bl zOTE#=?>1i;URC~+`@8O#i$vaD@)b!~$i~_VXR#nK+_E)(irN415m!U$4xzB1sEKIH zr8cRs(H%vzIhn7DQdp;_XB|(;noC5Kk}sHLmt@ib?AvugG!*1yDU|qw!TX6i(n{|0 zXGp)^x;2DV{*;tNbh|tIs!I-iQB{$O{}7oC!?Y|CWt^9fxzs$@Xdo>nCMIHG5zEj? z+9@<&9t!M8)$^p}vZp3SjJ*<3pFx$U8wm1v5BuM~eFLDGLZ%O=WP1zPc&USiCo%NC z9~RQ=*Q=LwH+2m*bx%wUCnoZwiS1wc_++HjYr;p^o!>28lyadk`fdFP?4txc(^3nw==TkRLP9p8st>10 zGS)#89xxxK*mnpsVPR!`X5OM-s$8Hr`&fgO_13XV^d^$b5XJbGXVi+nh0l`wRY~S6 zgV4*3tSnGM5vcX=80_t>bGzG#8F=8b_#5hrrbC)el!MG}Ss>YdFJ`JUVY~9fTRfHl zoXuF4(as}GCg^XiIzzLvC?6HTPk7*pnwo%iU1f!rxd6Z1uAKAzCduT)B#cLh6z#{? zk7;UvzRKlA!ZGs=Qy+pW*}lQ;ynT>$wOLvP0gFXSh36;c<(&4uSk?wwDQ3$##UnfGr9QKT&-Z6$r!lmCa36q3T@i3>(wC?TD!H`4_CLNOA8B=|0ZGg>+D4q7ez00%Fg~Ks*S) zA4&`({f55j>OO(s;oj=}9#&)jCbo*5tX_3gSW_--ASt}WRAp|BFpFnZi_kvY#-(^Nn zLt(w10Jl5sZ}7-ZI*3e}YtfIzo0x7;1x z0wj&|!bFgpztXKuKjx)o?D|ynCLVCj#9RA|s{C@|($Yg+T~vFM^jAJCsdEAba9X(P zbuW$TU)CZm`T1}Tbaf#@X~aBY;^L9=CLk7p!V(i{b5VMIO2b#)5EJ9HY!JJHCDi14 z1KR?`FH{)7#&{vKwZ^a~hVsKeLmF~8{$trE*q@c9WNC>L6GQ3)W(}Mwj^7Lo4{mH3 z@uz8+s>-kM@euCr z?E3^sNMa#y^jQOgU~pdsGJZhxNCxemx1k1Zt(alz_7@_#(rpKgo&PK z?Ko3>Wo0Flm?tfQI-bqkjH8uQ9AO8+J@m@hxVf0%?17NbdUR@f_AtM>>sBwt>~*gY z(1*fV2COKVkrC^Se!bFZ`Z+Hj@7jw!Wm#F$UIc!A{uAUtNOL(Q#NK$bXDk~I!q@C# zZ1$@OFi8F&O8f!11o_k-f0CEKT&Rai9C|Qh^ecV>$M0Z17>~2v2W#9Ukk$Y|8U`{c zC=dwx-(y~_UBEF&g_wB6xEBpdO%Ad`lPO{Oi@C#4Oj%u7kUx`1L8(2J)2#Wy5`e)}tBZ{( z;v}rB6CVNQ6CGU{kZ0t!*yYQ_5Q)rn&jCNA#M@NmNIs&u3hx9fG(SW2jGHXi&(Ot9u{ zg>rLqL9s`pqob>{DJa-gL0#PULF%J-R8XM!h1b+GOUolPowRZ*t303(ZDUhVScgi; z*rlD&QyO?@9*;O`LlBUfosFL$&wOPu))|Q?O#)X<)R%l z`C|OdpW%{{f%x}_bM=gL+}w2B3EAMVi=k6C(bpf8OV;?3kw0W$q!JLYv(m!P?~97f zNOU!OJ*G&kp@GxF*u2N?w)Z7}U#VF2!d^tgNa%45%y-+8aqGZ&s%lkNRC+I1ht1`g zE9vR#A`tQ5xxFU^SzC6}lfQlE`#etPe+)lrt$H>5fKBR6{rWY*YP8<|{QR8s1J~oZ z#WGoRchK_tBPh2;z=6Q$;GnR-#Rp7Lk2n=xdA-|u?}YJGo>5*OEzJosQdxP~D@h6T z6d zXJT5X^5u`q?d54tBw($GRN0pa!%kP(?p~kmnyb~`9_yL#^TR#bQ>)^~Cp9`dpYC>9 zTh|S_cCp*<*bOGwtAS{=S~n0`xW`r-X7^5wBOw54xOr7bUT^9fG1xiiL3 zSXC|qhIu8}*V%D@v5Nkx4G}v+Ou?3QDa2xIa@6VO+|*{=({}_dARutYq^hau9#Hdz zFE594#$)yyeNob7-Z<6O(c|8^Ew_|qiXRFK0OD?HYH7GodTpKE#j7;~y@^V_HA74) zg)w8VCt2pNH(oM4o}#Ht@jnxo1*88zzTPsfs-W!~U4$s5lpr7?(jZ79Qc6j8cXvp4 zC?Vb5-60*4N_Tg6cjp9W@=k7^<`SO_Db zZ$n>Kcnb)Y?C|i^(A2OV6W#Lc+BUt#(|sQm z@A!byHlXqTdNhu4XQF7P%9eT{8{x&9TMm}<-Mk18LWsd>r7b=N`d>o|;7}x#N!z?N zGPc;1WjszxzSJi%Qz1$$n+YsMhUVt9?ay66o-XKxaP9B!yI$AS)#)1;uz&mn3%0$} z=pw#5Iv7sc@|@ht)Tac8-tBeJ6mcR|#OHS%Og31E&~QG|16j;EMHTz90IDz#tYS)Z&$m#E+>Le$Tk)PRq#!q z&i;P$DK6FxVrWuPMTGvJ{RJrEG;hxLWoP-2Ins^AMCB|=KnC);(}35n)>bl!kXTj=^IJAzPdU#)3h)!Fwq->-+d}2mDoMs zFbA{P7O=q)G{VSGV6ufJS5_VnAp7Tk&_=|l5=?bWZ#UN)L}Nx!iUjWNvRYsY1RWC@`=Aiy7i1-A zC<2*RMvLb|!@!(3G7hAKDMf|#WWzIBBh};B&n5Q zBF5BNaQ_U_SI#C0r%ITU75u<3v^(6YPaZ{fJsF53@;go{ZcBSpT2f`xXxVY??@QD& zpB%rhO0G)sa`Fmh)KF*I#wqL8j@C3Q-TgP>h@n<<;o?iW2|anyd0q@;ZDbwS_X5nJ zc!pTh8M7U3c5akeLR(QvZKqT!1OZk8wV(WubcY>o1`e_p@(ozo@IK~y2%Fownak-D zcovlu7ZnQ^X^k36Gw1IZPiB&R2tX&yA2~KY?%6sUuWJt)O#L#nYq5?Xt{)ikzXoOu6`{1F``lg+AP1y(mn+wcj=dQ8&jPaBcm8zC$H~U_A0wFb8_0= z1^Ne;O_x<{2NJEr;VTAR%etB|7tQ8}#LEW#nEtU}M~kAFE$gC5hi4pX8=sGtk0odJ za!=W=DB-&aX@-9*tgEdn2BM;=of0owM2m|QS`Qh0ACqXnPLG*QQ681CK)k?&bTKo1 ze1%cnT`y-i6;~kB1uVvvm=bKnP-Z$$zWx|)_}M%Txg0}&k+~F$cI%EcUsaxtW>; zFYj9zdpNG-H&?R$u#g9JtEV+#x+7k)xc_RNpY!QW&buwXY3{`9)`^&pXKgQAM>%Yv zP&DW0+@KPN_DWo7q6UOi1Sqxmd#f59o_Bd4j5~_pG$X#6* z%JS;oHPKOCCULMhFi>O+O!LXG%l=_%_O_ZZ3B*BpEz)e9Szi{?7X%|b1r2h5ROPeJ z`4%^NJY#@oD$C0Bg_ja~EfM&3yF|Z!*(fg!Dfrswh%_sa0ENhF+F!h6?Zym=yo2v` zSlERS#EXEO_yrW15Bi3=l~J}1mjoIM^k#Eh8w&31Fn;dzhfyoIUztFKizHUGouCt&Atc<3cPkW zXLH^!e5GTTsBwpi8g$9>*6kR0`e9jm8X5I9J%BdZ$wF!o0lj`qY-DTocc#arq5^MZ zn8nqj6|jCsO`QP4^_(9%c7Fye^p& zuRJ~Nf{(_1HNom@Q(iw^=hPoqrB)YvxR5G!viNqu2!8hRjpX1~WqNBf4!!2re_*v& zCVakd~Hmslv$6)aB^bJE@`BXwz$ObdUroPyk^8 z0X!?RFywtlgJX9W6dCHxd5)EpeK9rN$4oyWIzmpcz|<-9I(Q0kWqCM>0-W$O|EzR@y4+83I$q7lIn1QFS8n_y35@_!I1x|#CN-XjbRQUMRl%L=Gmi5x8LJBVj z{TYM(zN&yLXGY^23w>bIFzg8i`|0*n00oEpk*Z_@7iTl@CT zCYYUtU4UWC;~CkqlyCGC?CcbzW$|tl?Mzt@uXb6?HxMpv zfp#PreboW;YygGJW$2&kFQPU~P^`_?yJdu@plybgPRLiS+Ku3FUwyW5^gunmsEDX-Sfs`hzFtIyd zo^bLjJ>4<8-~ZU3kkw+{yw+?-K2E?#*RUD5{sH^{2fL47(TtShXksvazJLZxG`P`O zKLU$1p!;TGhzkLZRrk02pk}R&xd*B!KCScD^#Z_hF%mU+s!|&Swu5FH3IsMJ2UF!S zmBAt@Y)5T~kY=FuqFAWS#vJeA6px|Dbcob$owISpbhO*i)EXm(8Sw;i`F zd&LoNk2($@x<4jfDj{#AosgEFy?eK8E^WoI@b{@4#@2}+rsMcA9{uuA1UdA}Xr=wZ z5)~WlF^X^QZ&Io)o_YZ5Lq1LiO?_kG_t&-$CFmFXFdoIx=hI^*nWF^YJ{}MIweR}- z`y2Gfw>VA?PG06U8fFd%ir-DrHx@MA1fLALC%c=;o%xj;V9wWX%@x=;-MgZqq9}?L zCf3)pS}f#4Mwu7Sk_k0Q_2#B4feHb}10dwEh+u;KH$Zb68N1-`v@n8XWva{%*Ou(uB{`)%kq0TCdUfY{8w-?PeTcr&~%&0B^r^ zI?|rlrv-XmHmlo@pl-kCILe8}o`1jtw_6Of3JMBJ0^BZh^*AHimWvPc4F*OuxBOp< zC=`O=rTZ;LgEw4(j#uxZdD0Z^Dlhe~{P_d0KYjWnOsR?b?*+YG@-F>%y3#skdTqHS zG4@!30;6NOXF;ghDdKVw;i;*M-#rn?>K&~q=t6(1NAX>+#SrvFf3O%C&owr#HeZON zH`~5@`hqIIz7QJAgK5OAF~#pLqN%YM7y#QB ztN(VozxCc2NCAtxin4NVGN-}NL;y{1>udL2(~;Sz$S4?(uud%_d_T9BPllb;ZaL!uRe;==#Q{;c>K$!!GOl~?{q!|c<^grvij@8aX4P@c8wVD z;H^BCPDE^f{=?g1rlz6Raj!d}<*{njF5ePk=FaDsDK0Az0|up0LX;$#Om34ELi0qA zILteu(kWBN4v))~Ui_VSw=@?PrOi}y+92~ zl6Ncs#7dQ<_@e>4Z;I^5HhqHOOTcX)r6(t6kbngIq|tb~kvevQ zRVmiv)X4nX{@&_*R%15cGWu%>9;qFUZ;qh}D}Uaqo1(0&sn%E&xhZ65aQovOdTC3H z^+use)0az;<|H$s4_}?tVM|wV^j`2KVK-D^LrhFeg2%z|;2e!H>sjP2X;kV=yH~sT zrdnGgV?)x##(^(eTPWI5;{`QoX$XH5hQ8;|+FCY!PdNJ*<_>8TV4*W~LO>w#=piBk zkSUuzXYd+$GwE-SgXrT4FwFVSKo%KP1Rk42nQtFSg5YO+dfM7`Mk@lCm@@R!6@?or zjDRGO%>ScX+y1))xXoW4Z?m#;yvtwn|GAD;AsJ;?#G1d@_g0-;_vhS?jY_Z3^p=|` zO2&GY=%?!Y9jN6ka3w)&KEIzhKEy#l)PJt)@qB*$3)Wj}(GJ1uE$Zsz^|{WzE>Zf~K9 zo0%Di%#C_uhpwJnz0lT9*VYCtHMgr_Qpca!rq>r|Hs3be9SzNW$_s=7{o-?ig8fU{#Fxs8c+MHf~p{(;j;{nF=1x;6WyCCTF{GjRO8}MjH z4*IRQpYQG!r_H<24$s&h)PHqYc!tUe+z5ble+@q&9KayW3kw?aQWWF{51kpSK{HYG*;9Q_@P)tsW|j_Qig-yHl)nX;eQNAL zts@Q_Uauo14wmLslt~zmgo2&wg2pEvow*dXZC;YU&8F&W>+`KXKLC&D{_swOBaZ)J zV1|ZMzo+W$?|a#htf^;2*_RsnQCdbuqpiKsKOiK?^$GxQ0+=JSqfFF{Q|TK=CZ=a0+g}cFN=TYAv_*ekZz`s|ORabA+UK93f zpDYz(`3rtVLP|DY?(Po*vwDNM+S`8S7XC6^1dqet_TIXe zR{=3No}O>GT@j#%nKHeRY{Kc8@4=t-h3xDP?q(WY5iO7Sy}bcuWNB-69U8sQ`R7K* z?Ymc9dW~*9IVQjWVIUZdC%%OC$RsTI4?_Q*L#(9hlL3Q)6u4&pm(*&`Hi0R2Dcr8t zEJQ?{oDJDN^PQTxQWPvK@_z*!E{+aE?G4{ug@zxq5G{%E@K_27$zY1`CarI;vvdy( zfc0TMt@6wA&BZ_p*Ij71w6c6Z!ubzxWDtfEWiOe#k~U~q#Jc>`Yh#Ks8OU8o$w<#E z?d=anI>yGBHRtQCzkK;qVh{GLvkJ&KupY@(b~ZMF0RfB#Lm=MC!pj>FXu7((#^vQ* z6jJE4v=uM+R=x(?g#W%2VfhP)Q+@G_Umbju-rHGO9mg>m00P+p;Bebr5-`fh2-%C2 zc5-#W>&uJ)LXdi+|BchqA5Zs+tJf95{O3abJw4@Bz^SRdEJasZ`F4$v9~iDpUXj)HUVWUpg+^~sdt;wG2zfj1Cs z+2lS(Bm5sHjmWI9u*h#9GOL8JK5u^Y>=gVbOG zL^wCv2!WZ=>)w?U6kA<)td3UXbDZ7O1=uvIo7=miO(i+?Iz|bY9S`q6l=z0Yy8?k? z(+71IKouyMRCFbn!%s^C3F#5`Hbrmqaz!tf12%6iPEJnDRh!;myiN=cKi=7&GxdmC zaEV9^2d#7dvnd0efh4Y^%kw>h#>V5|Cq$z}z_ktZ>H&K@Z>u72#3B0n@*4e-5yH$6 zTTe}luA6D_tcwo%2p(NS+~fz4q~_)A&0KnTxWi2ns|z4nsB;H?qPf6(*7X%`1rWuG!X28fLR>3+o`W`w&`>|8+bm!{rR4du(NH^*v9+hhrh?) zrbFM~1e=o*KYTr}p1w$DpAD-&wV6%BHBv zBrVS0DW4#cu~Cq~L)T{;O-cQQcS7R#q1S8mj#N|_Z$5#QYIIjiR8&u_vd#VAg3V<7 zJ@gFucblnkxQwb`T3l@GVku!-24Poq#4&0gMdRq`=wfOr;6nxCF?TmNhw@|qUY`j* zzSMY-f8Z&|O_`rp|3@|{0nC(zB9)lJ?%mtsq4aY%OU+)RR z8w8Id4W@@5@$vE3Gxv7{q;8O)xHtkNV{I*tx*@kYRiKbi7aAQ;7DM~iOELkhg&0rR zRa6>LP4{+vd9<~|1GS}T*=lR~zDW9raS>=*8GK}6d9<*a?T7_A!Y3(!F98z9cXx9{ zD&DGxudHbN^zkX0c9<9Z}J8v7GCf?iR3+l29=m1JB@hWSJSEnmqOO_2>I@Guft*Q8CtpH(dne_+Zm1 zEGmu=CIbWgw%_~Yv>WSiBhW+aSR1j%o;8++%m4Ps%T%#xzPb7HAXS)@6r#mE_;h09 zpZ{gFkIoXH*0j{hM{qM^kKR=bI7zMN^#5y{S zsDUU656qxfyjbjhMrFP=ObUfYCZa$p)n=SltIq&8BSE9Vir)#w{v*IiA3xp7%1S?2 zdmn2n>zsl3tYU$)(^J6l0^~r^>q8jG_U3Hr>-mRD)$#eG?i;% z^|M_LOnjGvf$u`wzkZe0UIwg;Cq)iR2TiqvcIDN`&yk`Rb12HU7MvHPWxImsb{9xV z1Im^`#Z|IUj|j;!=(YTSUPy$r`?Ry8qbrnX7?dd%OcW8zPyjl&k-Jx#mKKiZ57_ma z7l6%U;Yn7UGXAn0*=ys2dUBIP7)1eeYta*3Y0^ae#!B6rrR@c9O(2X`x_x~X6a-RL zv|)PV*vb`3k~!2M2$``w?MM3t(V8MD4YuV^1 z*NeUC;b)>>LQ2q{ZroHLg{meJ@N<(G?p$oe$K!zv07H(Mq48k^kL~>K^K(87tOq)| zc9Mip&z~=vTrb31P0@b3KRSxMzUfkPe$EB%XdY}PX+V?tNTVh9_I-rGKw8`5CuGHx z`R~~Y3JC^PEF2u1%0B-7e!~r*Av=dulvWlM7WxJS%~e?b{hg_MfIe@8yEUyGewDs3 z*QR$hoaG9Nshd+vTid?=e!FirrnW_oOoX|ik#SHRSz20>ZNtIB^3bZqGaEdI3Y8nK z%S&~NiV8oPxV}U?++5;aQW+l`bG^T1+t32<0el#Lzrx*{xhP1Ld;gy1Wj^wwVZ(&G z@@)8V_1%s8OjA=>i>QbgCt!R@aNl9QKE2pGWY^EmZY?;BOGz2Xm3r4MVY4)Mb7AKH zP{L?XwKqwmehZHsz^OBR+PaDb)F!+?;n>b**`*akM2Iml(SLutTNk5>J6>)>M*j_^ zHz4smZvB>rKv!M7xTK(9WhV7rrlBhMxqU-J!(Et8M6KD%c4$+C09dp?R{z#Kc3Gf6Q(#5y9V#=E-1e|7;axAu3r};qO97 z=rcFgMrWIv=rLv^En_{SeE?xSj znr6T`)iXz9@4KI`h)6U}nr@{pbAA>j(HtRSIzDhMAb_L~BE0vCHIm9wcDhCS4s-F$ ze;8e;nXnk&za~>c_+%b#*;`C(v_teIp;4klxgj1@`iO{2Mhh7RA<;k8k$Cbu9A-at zr}5liz7uAdRQ>5bOU#UvRrEAPmx`H-HrOD{ck1o4PvHvi!_GOr^h_DgNWw!wQ65lZ z@oQ=qpa7sOw=GZz!1-3dwkK#Mb) z`?Iy65#6*OArEG=0$i>d_ohRXl9l z!U6(T9hSDavD1NDX10%yY(^BzzUk@qT!KL!#nfW#e=Ba=-UbR{PTo^{ByxO(5>+{v zVc=ocAP~5C+t%O@=OLEB;uQ3%)Ovze>Fo0#<-5&q71eM<=cv#(D=SzJyiaPwT2(E3 zGfR9e0|PM@aL(-PD%8|at^u9240pxJmSVHp=_=xdh8Zs>l$n{$>2yp&DU?`yHQ(gQ zi^aF~$1|6Lo)-sje`TWhv|%EnqWnug9Z}WP)Euf&1zkQ8d(KOm0#Q-k5oZ*>r#h3OoA$|5BOQ-aT>8LmrC5MD{ArzkR zJ&R}=3WX^ced36I7p`^o7WRGKRoFvT*Ti>+(2P*}D1OLuf9e$#{j&PquWVQvN)4sN2q@Y6Z()o%st*hY`O+43 z)SZ(H%F2F7X$+jOIxK#Mo*^P>k4~g;*$M~t=|5J?Qgw&xWB1^1G+-Z_o&8NKiSARi zQBexIAQc4);=$ctBkgI96tJ&eS0cXDe+3;QBSVa{TwrWTPLiy|CSlaMKfICpw$tJc zO-@Y2TD#as|I3{l;S=&LfS1 zsfQk+S{aYl`1>j3HCID#dU*)C*Vx@x>5pmW)Pv2nU{gsmx zW&)?nCYnxZ@ao#Wt;>mJ4aEVLI7=oIcy^WNSg#!%4lCe2Zf}2F9L!s$7+*J@Ba;(d zT|Fc#A8*Ue4rlc-@{o~|!!?hasQ|GXETq4_4xYs@>E_{aW1h?XHy~Ink;x1XIz8JQ zX*OL}B}0S&7Q4f~`{tcGi+ZEWW8G=XoF6msE!!S9y)LW3a;>9P zkF-Y``1(`Y*mi-AnGqCGQx501Q$YA%$;`yb!3h#Kf!*Tur+Zr=M_k0km_5)T*7{NL z7d!p`O^+C0TmY8@3gOL3FN$U+7H(Vk;*;_U2Jxqe=O6elOAXkYK9&8G`?S5T5K zn?*ak{D>J*Ns@QD(BN9VG2seKOgDcy54=gv+Pj)v-xd7FdJxP<_Bxm-c0D{KqL|=r zeMW=}G?6=hN^szskF$D!8Oqx3YO4~K%@zif8!iY&TbPOqK}sl!MxD#mRZmRp()nMK z9uI9Y+>YHyw1%pyXdx`-`>#)!&ek<93kvdUcI0FQzdk2WF12ba`lSLm8J&wQ1@a|t zSnUcQXGL+A(Xl7V6U{<`YGyP!Wp()sEL_RImHIpC0Sh0}RF`~v^;s`#%Df!fuU@-LhT=00X&Ar?E~JTY!9T%f<=FhEhTZKnpL?50gxoU31Gw zm(KSG82iZ~&$C9i8^5IGqCWw%@&26iR5XNyJ`cL15U{+z zA0f3$ngvCQfNMj2h9nlyD}oom3#4M>;?1v)zFr=Eb$pthn*NS>jo`;DV0QvFYg2v3 zqC`--{D2`mgdnq+iQ6l2Vifu$8Dc=`Z+2>Mi_-nr0FYr zqsAM>FUieUq)14}em|0v8{cai69-8U4yD_)@HNj7tp+%}#KXgX!Yp}s31;+stZ!Qd z%l0rtqlkG@ojCm|P1ZZ(y;~!{m1BW_JaaD7*QYltUqcG7?V{>4WDtHZ-1Z&61UmF2USYX+#MYxSOX0Y#Aga{8asK$H@6;>)V8OD6h) zEIoxOelmuB)iH1tZEZ>dT9r?Zs7A+-UgIkKy9_ECno|2;7E_;(z+}2TSXflp$`u0i zHQ=REc0@tg`wK-gT<^0{cp&I`6XC?{^QBb)<(H@UnQMGR5IEKsGpt)Wxw8K~x0|V3@k>#cE^wBYnM+- z$_J^M|AUeOrAkpee?F&+6W3Yy`|K@Pd9-R`VtQdQ367{p$jE>!@bU<5A{gt2p=xdT z`4HI90!%I#_Hiz9OIgo`|Ctv~%0UD`(H+?-$sa>#UB+h_K&J?Ol4bFEPGGD*`~>&n_J*fPE6djul zo*XZ0i>D)qkjs_`FhM%@U>|W2$)BIK%cqWmVf?75*=JgY(4k&2v2q4R?+Omr^;0lK z%XCG?lzvD`O8j^hL#vU@=FIXqM;|PZPftw?GHV)P(i8EqK%X^K$gj0f$oM3AFw~pQ zHx~#a-{FG@1pzX$x5o)jpvHLijm;x&wzKXEi$!+tpP3l%I!ynOX-*FK&cG`3H5jyj zQK`({Z^GSw*)J5@ZkE+XS60g76sQ{}A`CTIt&BptilgUz2 zYK1CO%9YDB0yRTjx~I?aVh`6*B^q`gOA-J!w*rpr(9(sGx$w7GDy6+C&Py`*@|L0% z{eK^<$np&W6nKt{i;YJXcR4M-`-1lmaFUitVLLh068+*vr z&F9+?gMpQXtbL%6`s#@HrYe1H2p@QD@2x zEOPqVi<8(x-f(BXzVX2b^bw5KU7+OSVKG&-Ff^pt68xtu2iyzX|7Ak_y8y*+^Z)g? zdK^ty$p8Oe{|8J5zk2-hHbBYzpMO4nPl^5df4Dy2hpWBQ?LIsiN;8v>ERWFiw>|98 zzyI`j;@V|p9d`q5)HU!7k78` z1!h4GfFwK~^YK0Ws2Dk2Z-NPcd21zg@Kl=kCa~H9-7R<^0cyt!Zf+ z<6&U6$|%Vy)|jpdr_WkZmPU^f^FB+f`d{=FtVo_fEv0qqklbCkYbsv5CJiVBpFci* z^53@shxLE*U)!HY)gj>36-*Rv&Hau^Zsdx}Ha9l@q~9AG`}BoFJg2XJ`k*0KV`J-I z4^k1p_}Bo>jw6ZD@3C*dRt>a)m>C&!@(Kz}3}fTsLI_y*H;0HpzbhPR#$c49|D?U6 zNxjY{FCrqMs*1&1fc@|WCS43Pn7=Q)0B);eU0p&VA_>A7xOjNydplPk3esq6i>BJh z&`>aK2-r>DP^GG8yg>k z^O*2OZ$-E&9&6Q30mk8=fR-PZ$HR*U6)o5>a{z*%#Y)L~U&f#zFi3%*@;pj9x^JC> zL;?csOt!Y5q5p7o65#Le&8Mia@Jsv33Ry%XzOF7f*xY_%zbBf;9SEHg>biyoCm%b_ z0D}Vq!qCuI%4i$dK!eEE#Ty=|x#RW+;N96-*`*H~0tzh(A=hq95<9>Gl&gSb&ZZ|R zG$B>V&+nzHKoG1)dirvoN)>oBUKJG;8Ihi`KHI-OL0NihvwL?N1380+t$n-TEhl1b zXqxBBiW`>za0(JO=mFJc0(9pqcISRioZ>RV!ofLK`FUwQDwqEa6=51i>HyI!40nzI zmb*i^wUKxG+-3wxv9YiAqou;aXc8>%fwg3t(bnD`O>SIN7eM>?|s zuy8Lh&p@{UjT2}s?F%ZOb%8F9wAC@F&>Bmy68hYZSN(#5P#5-llh~`%!dgb${^6ga zOX})zQ&SC27sj>Yt6dKk2mLF?!V8cG_u6=EH9zpPOc2cSdPkJSj}b1bO`=Q3s88pK zdlNsW$P<@FrxQ~YE}JeDiS1veFkN|Yb9Z+%ey3({EixRtakCXN@;mV)d1f#1hU7uwFLtM!|a-?i_0bo!oZ*J-@m{12I(}l z3tqdI4Zj8XP4E*A55KXoIqv^G+Zx%JDrb_vVoOejotG-qmPqEY*wUqTkz^#`Z()2Sc)$Scb${9jtTEd49J+ZKsb1JK zS*gj*eFg>65yZWIpx~l^v`B`2nf*ohDHNgPEHg5yX@-yYYV-7fySvvkHTg}FxP`GX z6)ly&Ga7zM>?;@Q-r3pP!S8U^8v{F$<&)(W8ei0tI|}1i#>c1OfE~uBl%iy6dOi|_ zAst?>sx~$@ih_zNCwn6-ECiU6^767=67F`~Wb4yGiz(3`L_`Hac*#=}3-yzAwFWl= z$R*@8U|+Ed^>#Y}Q7JI?FgA`mm{0m>sr`KdRz3>z9tL_#OuDzb`(D(ur>BQgf8aSH zGMtCEHRqgx8!;)3oRSp!$pClQ&=A;2qab+xmIs~2bZ_JVjd~K;MJ7G}yrW-Q?PLwB zt5np~B?t%t0$)=az7bs=t*p4;(z~=i3$C-6Z=}a(vs(3Cvexg%GdHH1D>o3cJpFJ@Nq1G`U)^M2Dz0`T0k-sp0PY{Ooqk(EC-y^Nfx_DQS6guwPP<#Wi3CCE z<3$<+Y6a=((uwINCMMRI5iLI2F6xsP{B3||&g37zxp+7fp~n#S^Tq48W|w+xNLt+k zu}id?OoS%QD;5P4zc9EhAK&NC-@mmcbM?5wRu5dZ{*M<$yWLN)uMD2Rx3vHu|1p5& ziq-!Z5%?_a!v`=G_IfR|+3&}gBWvi=zH%GcN!|Cm?dlX2*kv)Fe=m>&4sy<~?2Wel zoQk^O4JjFnM_}Y>H8PGRPV6>Ekg00DO&(?l`?w1VVEi$!t*zU2KmQv1%3ND}_`5$4 z{X=Lsi$q`Rz~lx+%17d0VTDI_`hvKQSM1}UZXg2|)aYSLMM_~|x_`T;g*#WkiWp{k zTYn3DKwmc9{wbIDXyS%WM+U#eZ?`5<(+VUoTVq4@E_Y3H_J?&rre1e1(Ut*wz;o&X22 zfn@fZ!zIs{59;#ra?Fg}h9WEqUOnB@+a=`&K>T=e%H@i+gO9i0(F%D46$RD2GV9$O z0+s;iOXxi@G*3NzeSOE3Yv!YB$FOIS zVN3RfKJ5sQp{bGE$$I5}^Y?m*sIgzi4A8IA%BaXtG*t{J90LGy4=U@UUudcC|?%%qmW6VN_EMB%>ylkyclilOw&IH*F6gfA|QDm?)hd z+|GBW+S?Nc2Hp{A^*(p?7W`>fl`!KMLH-1*{59*oo9TP#c4=qlYU%dLW>ZF4neoBF z4Wt>~(9m_bq*2F~lmWf7f} zBo$=VV#a1?IjqMEn+p`4$Qm>}f6u~FrAti8HU+fMo6Z-4u>;15%#fmz%2!}vHP`F_k@W6_aQ+>|;1eE`24;A6V7#LqevjJotI|ndo3-7NkTk@#``-d%_{;t=DJyD^% ziEMtq1AXc~`9wWhc4SRYkJANt3`q7x3KVCWrcc!>&<%_0_felcQBs?gq=Fkh`~UtQfji);gyl; zy>pk9mBmX)fSTL&>qCpc%0_}89_~W;EHr$%7p$%5Ru51!fhGp}UQ0QNM3$c39-F*p z=-v_+Wir)tqdCrDRb+TL8UC8;(ReUA`VxKrsZ60-`Ls=|FJR}--fR$(ecmpw7&S;> zHJd6d&Ii3xZ2OD2BHq?(3 zvOD$oaweF+Zo6gWs}N$Hy1J2?Hb74>{44w2!|h+oX{O-W`ozR@=2J|*#|K{Aeedw_ znI36>td5x)XOllmRaHqa0W~G{6OY3~I_OuGQPM%LZjB#FlDe7#4mz&UG|L=FNc?vqz5$eVLbCNk# zQkGsGR_V5J>ghUgANCaGZLOT_+tRA2;H#)4lpXuK-}^N>+~3wBcPZQM^ROgkEOTuIdD}^1GDIOZC#dDIDDb7zZLe;!YOvxjnw%EE_mR9p*+W7hL$BT&l9;d7KE)V?ECncVbtl#VDLdD8%lg0_) ztZ&3QeS`&FQ-wv*16Ba&zJglt4X3_n3YW0WC*DAD?r(<>Wds`G!Pe5!BAsRDFD=(= z3Mm_er2fIt;j-5TYy;NfvNPJ>INCTJ4(HPO0iYfo7M8V%;@=;-H`ep*aMABMrb3A_ ze>XflyzkH!GInKgM|Iy!0(@TAz+5gxIIY9nq)&VHOQ1`7#U>40kY4B7ThwZikpa|V zrrLDrs;x4ph=%Q=)eFAF(Yixfu_Bp(mxotoq`!Z^I;sGG;Oysrsw(InIMz3o8Pp0i zxWSRb!&#UZ0Gl1L-XZKao#PWh9f$p(_g@Rr^18~?gNpXbb0nJ1;w{+vrG=E@(m^6- zR>jRl%BQiT*aInsd=`*5GW6q#{P=jj>&-AOh7}T01R2|FAoPsJej}l_H&@xy*JqX( zHTGF;E}4O0^=x!>IG)4tct9j(ZEX$FF!NQCc)u$kA7vIVT0i6#78c5JKP;~@jZL=l ziK{1z8~BcFNlOm{XuhJV^qku*F~bGS-yq;8DJe`Of>L#Tlqd$JgWVUt^7yBG`WhKA8~8;LuVWJ_g30lJv@`%4eSi zFul>r`}u-!oaNhd z==CSlToO+h=;ENm=JK2-vlKgeRcJ`1numRDYfYVj0YXDZr`6ni-7~1wuAR;ay#*VE zbnQ11@Z=;Up=%$RuFv(v^Z}1kf~CwcXt}qqk90@b274&v4*6R|c)03s{k}+{2q^W* zTQZhx%$bvye+_idm-(W9y)x6sw%vF>0E^FRLB^Qs{nMvxAXA(wpMo-r)A<~j>bwAE zQO-y2K9E-VA?M1sNj0xFzrkg0MU z2S=&M%M+XrV<7d!Ei(4-!W+2`4na{-d3$TSyQ^z#VqyUa85sqop73VzwD#odiyMc& zi{*Km9qruo%nI!_BpNX>dORBP4;M>+YeE-8XA@r^B(cLZcek%EU!4@zIwvM2f_7KS zv|o#6Mo3jtTB%XwRA)zC-ZE4C#%l?gktI&2!DJ30Hn&>`VdL$yMa_kkj)-u=6V(^{ z<3+zLaA?xcQXPBm^#REu97SL{0O#k6E57*#e8&rO>F$WZ1XlLLa3`mJHs_@uRnhDI z8g5`=_YL&ruwUZ2KG{&PnEg06+w1ltrTEvcLETG01Z3>cnV+p)ULFOGz_-B!J>C7! zpEt;<%A1?%nQ@ILiVtn{pC#^W31ysp+}^RL=2(_@;Byp1y(lNgfrCro5W+Q~2ZSp* z5t;IwKv7;-H4sea-Sa)I+9Kupri7f5lH;e&uaw^zo`31No9eFXB-k!~{rc4<^IF9N z5d!;JYt9Y4S193;;Z&5g!ouRupCbha`Ngrja}~IN)?F;UPX5j|q_cmIUTZyXMDVxDvM-fOMBw&?|iHBJ|reWEw;KACx|>Ke-rt}`Nusb&Fo!|wv7i6 zFz4h1DR;ftJJ)PajX4TX{43paO+I+MvkK2`ipIvHz|d*P_B?=chv;arjiO#PG+y0g z6}wtJ5d|Io(}Ug&1bB@Hs_boHB4U>M-WSXHSLeqpH0C!YyRBp-pUUsYp z>5}$I!hSz4e(emuN-8$Iy1Z1G3_d#qJqi8is_NS8U9t2s^7n?R2;w9>U&G@pzIkm9 zrna6iOu4>rP#xLac|~}}6fso+L`&N-8$%AH#}YoVjr3bYX&+M0MmkEik^oTMiUFey z`7B#jwE~aNMH(dgdiy6QQJ$+f*tm7(gg@Ph7(7-dCs4T9xYhX;*ZybCNFhNX)i(eU zq&1ei!QjW_uCA>KRaH%mMMbJd$Hb`eP#7ASzeYMb zz5SZ%>elA423PJEFY1b&92?Tzb9dbEDR?h`HdNc=iTh$zc^=x>*r;?dBtKqiq2o^- zOiX+}=>u1=zQMZcBYb<21brbGXwsOdq&q1pN^5-SvbEa!{m0JDUIsZ^g0QNRLGs3p zI)1_*zvCt3Ly!$rxCn1EL>kozg@@Ys`H92M*4N3abrHRaDJ#_ay1J^4rDiP|8N=*zLP8XbvO^t6$rvblvbayJ`L$Q=diJh6Yy>kizKnA8|NHh~PwBV)D;Q{< zYp2Uz2r$(wYZQ*em{bI@7#bU=D(S9T{{~=kn5$4i-|O@b?N%!95pr+ey+BS^+Y@aa zFP)y6oL)C)WJs%8Ho4H95SE4Y5!}CzuJY!FEt?S$wP!XhefcuWPz>ND0PIR!U5t#( z)&Q?;WVY1C$_mRMxPtY_y37wJzjtP_%Pd=muAMwE)_NAvxmuaBU0F4@TcTAA4x3s2 zXJ^~S;UT6l$9yfgPFSB!EQ4H@&A~-QJN(3k$#uXonb%r-Hqfdy2W*KJ zufUMAuF7ofH~{4RT5j;%HokS9P@?S$^VzS53#Muie<95;0D3R?4JAdzgOUl0U~8(( zDo^U}?H2OwkJJHjWMoNs{9CWc7#J4bSciqpu}vT|&eB<;$lB#RPAmAEez^^n<&|VG zF{vU7B&u7N%94(GP4)C5pWB2@<9SelPe0l~HgKPx)3espy3DX?T;NT5+m&<~?V1^E zuuF->$y|+D`$(@m6?I1WyIU~4?unsM_CWX_3!kP+IFZNXK6=EO-XJLtbSqc^MpdED zQ+Jn{HO^Qfh!QXtCpSao(P$1K9ED&+Lfc;L5M4Dl2S?rk9u`Yz%PnbZAAmb{psQC( zN>sKTdMiF9uxopGcvMwaKhrb#^7ANFXl|k9$Kw2qwtDhg-Nd4_>G8tYG4n4?K_aMNfI3>9Q!XKg^Hj1>F2s&aCIP|*VhG-;-< zi`zz)mhLPvDUmAX>yB614*2aR%!H&E*>5=Hg+)ffLBkp7Yy#&%OG|sc^|QCP*DVo9 z{Xs>hn~^)l;W++tWc7IDSH7YJ`G&(7Jv+PSb5%7B;W|q7YmROh;Xy)s^kI+Egr z+!qo!`;Ic~MG^i*x5sQ*s`zRHYE>4$AX)Uu*f8LTB|OtbIzNHa;js z_!(SYM=zxYnmF-eKKJiZ@<<4 zz8I|!hwV3&diHD=6%{);Tl$KKis1P(1E2&yKtPI_M+UxGWg8ws2&^bOIn{J^N4h7I zSH@%`K!?XvY@IEGSdV*VTtnajG%;0N5kxBL`=Xb}sD*hu8-DaIzWIY{Yr)jLsK`Lm z=I*w(ASR|hD;3ajU;iWJAuX-E2e$8PoL|32PYDo1KL%LrD`7~NC7Qvdz65sgG;v5a zgL%v>O3_n5#YKfzhCnDFIKW#cfGi|9WCP|8lt18rqg2t?x6ifRy=I&3Pr#T%2{j*a zJ7r<9x)RH}vYVjm(NUQ$4mFd;{H&I&sN&+dxHx`oX}5y1LQvla&4uErsr4_DX6BB8 zpxA0&!FV_)m9@68wpwD`RC^PIwM@#&$0yhQdQjBWHFlmPB3(>@jxL{zg9GFm!NIuz zbf@xRKmD!*v-asM1VMdgr>~*n#(qLfI6Q35qS~hvl)&NPI3Y|Rcu-OiT72^p@ZG1t z3kM|K_*wGx5s5FeQ?3|~H{tHA`-;g%9_;*i!CL6uN$9IeBKpQA(%zBK$L6&qt}`!{Zh<+tsnEf6IqgDihFCLTE-qy{GdxpAla?kQ>@t`p zqTXwF*BT8F{C6um+r;q+Sc?w*B>H8lx3S+_sXU*2-umjev8qbV#H47<p8RksQ%r3#STL#(JAk7_!?B^2fqEz?!jqB;Bc;gud6_?aA%R zq%<@&W9%E_bD7bU$4i4hLp^3-V83X<&{l3PhaHLq2gfHQU_1_;wzkCW-VgQl(Thf( z;LmezWC1QnLp{Gde?ayhVmXl-Z<`5uef^~O@2vv5FqlIuMxGrsnClnjfP<@w_wHeL zw>utQ`GQ?EjeWywv_})(?;R6PVy5v-hk{};OlrV=h!^gC=pLy#}k0egj>n#JVk;-_ZJF&}}@!z{A3ant{z2>^YnIuix|2gfPnR#8@U*#m6r;c7XP% z=;*hLDvx=}{nAtME?uu60AtGi-ubSBcDWO*YOO3%9mZX(I3&?yFggIZ5YK=0ujcPL z=fJ~(_r)`OeEiTS_*)AXmuVdo-Dn%NACbb}?$U8bYS$}Zk2heiJI>%v5J*FT+BaPa zAo+GqPS#FNEES(sI4G+XEO}t2GyWFRY?}7Q)po~GhUHRM^`MIU^ohy0u_|5={S%SX z#$wn|TKC<_ImZ``20B$1_$J`@?8+wm#T&;SXHFS6`-(+E#qm&zfv1eg(5aXWcO)d} z-S6L^e7oHS8h@n3>`)DGhl_W=DR|9`N($*k`k`z6NeTJl{ryMVZZo~pjF%pD{;=xx zMB9_i?1*8*K5zkq-rg_Nii!*G^_Bvu#>&z|zvH4HL9(^A?ben7$L95)TU&TzLE0b2 zYA;i7WkXn<^mb;==ndsTD9oxc!e1Qs{&}PRPgai$XXJSz9HWr$*As0Nu{Bp*m(xA& z$=ggpK_T~jZpU1!EGQ)TAoA#8rpGQCz1Q2F^AAo(BGT_7eD(|=*IUc$Z1ryLs>;h1 zq2=Y}VA`po_i;$QpP{d>ZcuwTjvYb?NLPK9pW;R@009^U@G5<>rB*XJX<5&C7w+)) z9-qxOgV1}AqXGzDkq@iEIZze;W;2T$!*O~FpFPvVCVac{Q>)tRrM*aT zX_QMFH*?>PxoIFYr;HbQtAusK(-ZrcMk>eiVia_(Hr^oRB+kw@`7DP_E1uZ22fvY& z1UTvXx;4PeHl3Rk3#1TZRJT0l%*+8+h!w=f z9k^^fM@`f~9_8n;<>%2KACi#l9PHHLf2-ST>jlywcq72A>s;r7-aeG&3W#?A)r|G0 zAQ$R6#Q#e0Y4;9bKD-4-=w)Q}ax&>u#0*i1=75YNfY|=+`0t2gPY6GtGX!;JT#n~g)?=oz$XkDkMUr8avnvo$!Bqz zE$!dC;Wm6s6B#LuqlKw<{~n&Z{BqX||8Ubq@UtalaZKwtT-qhw1>ON7Nj=}k2 z-@&xxkn6*{sab+Hh^iutXPu5(qsZf_;kG7$q@+^RgH&zG+ZVPXA|l@C%39|+wOc-N zJjPY_CoX_JsMU?hPLI}@F)gT4OTm=Xz3EC30M6st#l{dwPD!5EYVSja?C&6%cy-GE zJ3s?!Vja-D-5@Mr@+)~8cXD#_n}53d z%5+VSGYi>$hTk9J<4M1fgtoi_OS53kME3CKZJ#`;QbzWV@87qLe8d6Mh>-v2$yZf< zX-w^dy1ckYQk$^LNhkqtv)rqfbO9D$K<62G$`P&>g*WFv}ZNmX)>G{^_oFqp9=Z;`UTa$OZG^ z`Gk5_jqtdsy%FsE2)*G%^Qmb1+-mP4I2iQqds3(bUS~iH3XZO3V>T%xiP>p@S(G$> zVn9cA#ts?vD-wv!=g+>L*xQiBkNKQpX&H%MS;}}9Y)&1o2n?O+P=%D`H$<3GcXV&nqX(?UH&d)!5 z{0H5s`h;1EBu}gthm0Tk2Oq-Lg>_?QFbFEp>iB zq+%C{ClW@rr7+lWnvS{G*oV?B%K3XuUDZ!|f7H6v7IMNGGrrM65|fiegfV_bkJ-K? zSib&H+Y^z>vlGP9U9> zpS@rG-TahZ5Wlnd9z)r`=v;9(8Y`$LLjuxv=kyq58OFIM5=f6WYH*yehmr@TnV8sw zpu+?V0Dok1{D3S~)Oi{Jk|4I~8|qJ>Cdga(zR`I50s}Vb?2&(dw&MM{G1St2{18%d zvd^CnC>hDAUvXy^+h3f$-}TaT=J8Dq+CAE6qt0vrkpaqN5R)R?TywY`*df;%`FM(xCJz!Ey(r2f0Ctc`+tdXLcbUs+)0{f%mcw_|y# z_knlb7>aNu(buCId$4#NQ{D7nXZ^q)~eu8kE;=E&zwe@Z;50StU4x*#0hdGv-^eQY6Jnk)A^5yUYfv@#oR|qkQgEgo%UiE^cY=L?eFj3!@n1IdIq9eLsJ77M&IIO zl3P^bwLWN^@1Vs4Y3Yf@#=&W9YN7$~e0)4FFPECCCSjwy^|zYi%9S2fe|G^B9i9E3 zx`=^dF72yhR8kvvSs5>w6S~F24pbb5I!61es%lfErog!tpMq!GD1obgpw=1C%@;l^ zObSXd^guSl%{^1xSj2TW*Q8Fto7egp4>C#|zwUe+2?8Qhs4RChnyJ%aUYgH!Z-;el z?V;~30iC08;Joxiiz91S7YJ6^7I>tHcbaM90tLnNYPR0_#_-MVFWt&<#9Eosto7g> zcAC8(=qP!)wk5`A*-B+&wYa)!J(` zyt+ay{NVY)J2XVWXSq9jNbPFxyEa|(wxA#=2%^{}Yg0Lgb^9EU5-1evf>vVLbM@`~ zlgvtM9s-=rljMZr@?vD)k7fWVAUYrEJ}|C)skl+QF*ZkVFY;Z~ik2mC1HCBcHY{1K zE8;ufdq9COZ+_)`={FOP!2nXz8ERc_mXQgEU)?dYsfEs$^d|u)fb?27Ek+)$ z^Y)H3W=NCIPR7lSJVf08{HU*X$!_V>Cvt}LAxpDpax!n}MRj zXEH3a*x}(mJgC0%0L;}x3WmU+`!Cqq%9?K&n4KM50uDxPSrF~!lf*LzwWgxZf4dr= zDk>-J6#yaF(_5F?<0vsN5>91;EpQp5) za!)6yQW=Quiqk)6(^UkV(h4E5!&NdblY|75M5>gfrv4h{^Aoe4zms>(|3jqzd@$rqn56CN!nNk$+n}Cm9+bX`V22%uR;SLXSrRFw)T#`Zd?P1Ew zLsH_=TW)9lWU~om%Rve>_C=CSHo(llxWV^kZ@Ym6>Ws{krfy`Sh7^B@hFHeA>qaCOcQNg^J6T?87;>{b8$g0`d4RhgEV>I0uKS{bRn;}#p4ol6 zQ{Sy)lMPxCEiFV0(!E1{&PxQncbQpPsr)uaMP2s-|4xZ8F)<+}T~&@1_Vo77oHx&} zT&Zx~n?I;sfy3dzutY&O@xzD}Fhk?3;+QWlHZV>}-;?v>Kc8=2jwi+m`BYX``)Z^TG~iS)9+*xA}mOrl1f z!Z^6N2&e?d-HTRAj2GM%YJt25ydWsp`Ts*Wn88BhWWI(VVkk3}1%Ma}7DEKT&<@Es2v}E-idm#YE z!ch>W4gA`88|oX23X4amq5-~3@3|ftHb?&YZG07`W^0m)l3GngjjOl-06N`_. - - CellBender never adds counts Version 0.3.0 ------------- @@ -84,6 +88,37 @@ a workflow using Google Colab on a GPU for free. hundreds of samples in automated pipelines. This file can be parsed to look for indications that a sample may need to be re-run. + +Version 0.2.0 +------------- + +A significant overhaul of the model and the output generation procedure were +undertaken to explicitly guarantee that CellBender only subtracts counts and +never adds. The output is not constructed by imputation or smoothing, and +CellBender intentionally tries to modify the raw data as little as possible in +order to achieve denoising. A nominal false positive rate is approximately +controlled at the level of the entire dataset, to prevent removal of too much +signal. + +- Uses a variational autoencoder as a prior + +- Computes the "denoised" count matrix using a MAP estimate and posterior regularization + + - CellBender never adds counts + + +Version 0.1.0 +------------- + +This was the initial release. The output count matrix was constructed via +imputation, so that there were no explicit guarantees that CellBender would +only subtract counts and never add. + +This version has been deprecated, and we do not recommend using it any longer. + +- Imputes the "denoised" count matrix using a variational autoencoder + + Human-mouse mixture benchmark ----------------------------- @@ -137,3 +172,14 @@ v0.3.0 .. image:: /_static/remove_background/v0.3.0_hgmm.png :width: 750 px + +This represents a real improvement over the results published in the paper. + +v0.3.2 +~~~~~~ + +.. image:: /_static/remove_background/v0.3.2_hgmm.png + :width: 750 px + +This appears identical to v0.3.0, as the changes were intended to fix bugs and +reduce memory footprint. diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst index ead0ce7..2f47629 100644 --- a/docs/source/reference/index.rst +++ b/docs/source/reference/index.rst @@ -185,4 +185,5 @@ The information contained in the posterior can be used to quantitatively answer questions such as "What is the probability that the number of viral gene counts in this cell is nonzero?" For help with these kinds of computations, please open a -`github issue `_. +`github issue `_, or see +the `semi-worked example on the github issue here `_. From 1198d8f0b6b56bfa82707cc3a593e7ee28e7034f Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Mon, 24 Jun 2024 12:58:37 -0400 Subject: [PATCH 18/19] Update wdl for benchmarking compatibility --- wdl/cellbender_remove_background.wdl | 1 + 1 file changed, 1 insertion(+) diff --git a/wdl/cellbender_remove_background.wdl b/wdl/cellbender_remove_background.wdl index f7507ec..d3a964b 100644 --- a/wdl/cellbender_remove_background.wdl +++ b/wdl/cellbender_remove_background.wdl @@ -90,6 +90,7 @@ task run_cellbender_remove_background_gpu { git clone -q https://github.com/broadinstitute/CellBender.git /cromwell_root/CellBender cd /cromwell_root/CellBender git checkout -q ~{dev_git_hash__} + yes | pip install -U pip setuptools yes | pip install --no-cache-dir -U -e /cromwell_root/CellBender pip list cd /cromwell_root From d7d228b491858054fac7e4780f575bc366bc9165 Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Mon, 24 Jun 2024 13:00:20 -0400 Subject: [PATCH 19/19] Bump version to v0.3.2 --- cellbender/VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cellbender/VERSION.txt b/cellbender/VERSION.txt index 6833eaa..9fc80f9 100644 --- a/cellbender/VERSION.txt +++ b/cellbender/VERSION.txt @@ -1 +1 @@ -0.3.1.dev0 \ No newline at end of file +0.3.2 \ No newline at end of file

    *f$(vqyo!uStaO-78DJ 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 yW7ZGZVi6q+JDdBs!xq|};zbY>5P`_ +is a great dataset for benchmarking noise removal methods. + +Figure legend: + +a. Log-log plot of counts of human genes versus mouse genes in each cell. + Each cell is a dot. Gray dots are the raw data, same in each figure. Green + dots are the CellBender output at FPR 0.01. Marginal histograms are shown + above and to the right. +b. Quantification of cross-species contamination per cell. Blue boxes show + human genes in mouse cells, and orange boxes show mouse genes in human cells. +c. Quantification of overall count removal per gene. Each dot is a gene, + summed over all cells. X-axis is the raw data. Y-axis is the fraction of + counts remaining after CellBender noise removal. This is not a comparison + with truth, but rather a look to see if high-count or low-count genes + are being treated differently with regard to noise removal. The red line + guides the eye for what would be a constant level of removal. + +This benchmark is not shown for v0.1.0, as it is deprecated. + +v0.2.2 +~~~~~~ + +.. image:: /_static/remove_background/v0.2.2_hgmm.png + :width: 750 px + +Note that the very small dip in panel c for genes with raw counts > 1e5 +indicates some over-removal of highly expressed genes. (Some of the mass of +black dots is dipping below the red line.) + +Nature Methods publication +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The code run in the paper is technically v0.3.0_rc, a "release candidate". +The results are shown in the paper in Figure 5a, and reproduced here in a +comparable format. + +.. image:: /_static/remove_background/v0.3.0_rc_hgmm.png + :width: 750 px + +Note that the seeming performance regression came with +a lot of extra guarantees about the quality of the output. The dip visible +in panel c in v0.2.2 has disappeared. + +v0.3.0 +~~~~~~ + +.. image:: /_static/remove_background/v0.3.0_hgmm.png + :width: 750 px diff --git a/docs/source/citation/index.rst b/docs/source/citation/index.rst index 65b8beb..f6f1ed3 100644 --- a/docs/source/citation/index.rst +++ b/docs/source/citation/index.rst @@ -9,7 +9,6 @@ citing our paper: Stephen J Fleming, Mark D Chaffin, Alessandro Arduini, Amer-Denis Akkad, Eric Banks, John C Marioni, Anthony A Philippakis, Patrick T Ellinor, and Mehrtash Babadi. Unsupervised removal of systematic background noise from droplet-based single-cell -experiments using CellBender. *Nature Methods* (in press), 2023. +experiments using CellBender. *Nature Methods*, 2023. https://doi.org/10.1038/s41592-023-01943-7 -Preprint `available at bioRxiv `_. -https://doi.org/10.1101/791699 +Preprint `available on bioRxiv `_. diff --git a/docs/source/contributing/index.rst b/docs/source/contributing/index.rst index 7d89ef9..682cf7f 100644 --- a/docs/source/contributing/index.rst +++ b/docs/source/contributing/index.rst @@ -9,6 +9,12 @@ with our research collaborators, your feedback is invaluable to us and allows us to steer CellBender in the direction that you find most useful in your research. If you have an interesting idea or suggestion, please do not hesitate to reach out to us. +A github issue is often the right place to start a conversation about a new feature +or a bug fix. +From there, once we collectively agree on a sense of how to proceed, the repository +can be forked, changes made (using the ``dev`` branch), and a pull request (PR) +can be created. PRs are to target the ``dev`` branch. + If you encounter a bug, please file a detailed github `issue `_ and we will get back to you as soon as possible. diff --git a/docs/source/introduction/index.rst b/docs/source/introduction/index.rst index 7bcf04a..77b5525 100644 --- a/docs/source/introduction/index.rst +++ b/docs/source/introduction/index.rst @@ -13,11 +13,11 @@ Despite the recent progress in improving, optimizing and standardizing droplet-b like scRNA-seq, the complexity of these experiments leaves room for systematic biases and background noise in the raw observations. These nuisances can be traced back to undesirable enzymatic processes that produce spurious library fragments, contamination by -exogeneous or endogenous ambient transcripts, impurity of barcode beads, and barcode swapping during amplification +exogenous or endogenous ambient transcripts, impurity of barcode beads, and barcode swapping during amplification and/or sequencing. The main purpose of CellBender is to take raw gene-by-cell count matrices and molecule-level information produced -by 3rd party pipelines (e.g. CellRanger, Alevin, DropSeq, etc.), to model and remove systematic biases and +by 3rd party pipelines (e.g. CellRanger, Alevin, DropSeq, StarSolo, etc.), to model and remove systematic biases and background noise, and to produce improved estimates of gene expression. As such, CellBender relies on an external tool for primary processing of the raw data obtained from the diff --git a/docs/source/troubleshooting/index.rst b/docs/source/troubleshooting/index.rst index 893f7bb..54c1350 100644 --- a/docs/source/troubleshooting/index.rst +++ b/docs/source/troubleshooting/index.rst @@ -177,7 +177,8 @@ Answers / Troubleshooting Tips Anecdotally it seems that ATAC data is less noisy than gene expression data to begin with, so some users opt to have CellBender ignore the ATAC features using the input argument ``--exclude-feature-types Peaks``. There is nothing - wrong with doing this. + wrong with doing this. The CellBender output file will still contain the ATAC + Peak features, but they will be identical to the raw input file. .. _a8: @@ -421,6 +422,5 @@ Answers / Troubleshooting Tips This means that the "empty droplet plateau" could not be identified. The most likely explanation is that the level of background RNA is extremely low, and that the value of ``--low-count-threshold`` exceeds this level. This would result in the empty - droplet plateau being excluded from the analysis, which is not advisable. This can be - corrected by decreasing ``--low-count-threshold`` from its default of 15 to a value like 5. - + droplet plateau being excluded from the analysis, which is not advisable. This could + possibly be corrected by decreasing ``--low-count-threshold`` to a value like 1. diff --git a/docs/source/usage/index.rst b/docs/source/usage/index.rst index bb0d5e9..ddcaebf 100644 --- a/docs/source/usage/index.rst +++ b/docs/source/usage/index.rst @@ -27,7 +27,12 @@ Proposed pipeline ~~~~~~~~~~~~~~~~~ #. Run ``cellranger count`` or some other quantification tool to obtain a count matrix -#. Run ``cellbender remove-background`` +#. Run ``cellbender remove-background`` using the command + +.. code-block:: console + + cellbender remove-background --cuda --input input_file.h5 --output output_file.h5 + #. Perform per-cell quality control checks, and filter out dead / dying cells, as appropriate for your experiment #. Perform all subsequent analyses using the CellBender count matrix. (It is useful @@ -67,18 +72,16 @@ Run ``remove-background`` on the dataset using the following command .. code-block:: console (cellbender) $ cellbender remove-background \ - --input raw_feature_bc_matrix.h5 \ - --output output.h5 \ --cuda \ - --expected-cells 5000 \ - --total-droplets-included 20000 \ - --fpr 0.01 \ - --epochs 150 + --input raw_feature_bc_matrix.h5 \ + --output output.h5 (The output filename "output.h5" can be replaced with a filename of choice.) -This command will produce five output files: +This command will produce nine output files: +* ``output_report.html``: HTML report including plots and commentary, along with any + warnings or suggestions for improved parameter settings. * ``output.h5``: Full count matrix as an h5 file, with background RNA removed. This file contains all the original droplet barcodes. * ``output_filtered.h5``: Filtered count matrix as an h5 file, with background RNA removed. @@ -92,15 +95,23 @@ This command will produce five output files: * ``output.log``: Log file produced by the ``cellbender remove-background`` run. * ``output_metrics.csv``: Metrics describing the run, potentially to be used to flag problematic runs when using CellBender as part of a large-scale automated pipeline. -* ``output_report.html``: HTML report including plots and commentary, along with any - warnings or suggestions for improved parameter settings. * ``ckpt.tar.gz``: Checkpoint file which contains the trained model and the full posterior. +* ``output_posterior.h5``: The full posterior probability of noise counts. This is + not normally used downstream. + +If you are interested in saving space and you do not need to re-run cellbender, +only the ``output_report.html`` and the ``output.h5`` need to be stored. The +``ckpt.tar.gz`` in particular is a large file which can be deleted to save disk +storage space. (However, if you keep this checkpoint file, it can be used to +create a new output count matrix with a different ``--fpr``, without +needing to re-run the lengthy training process. Simply run the command again +with a different ``--fpr`` and specify ``--checkpoint ckpt.tar.gz``.) Quality control checks ~~~~~~~~~~~~~~~~~~~~~~ -* Check the log file for any warnings. -* Check lines 11 -18 in the log file. Ensure that the automatically-determined priors +* Check the log file for any errors or warnings. +* Check lines 13-21 in the log file. Ensure that the automatically-determined priors for cell counts and empty droplet counts match your expectation from the UMI curve. Ensure that the numbers of "probable cells", "additional barcodes", and "empty droplets" are all nonzero and look reasonable. From e3588886003177df498561107deed9eb86cc4fb9 Mon Sep 17 00:00:00 2001 From: kshakir Date: Tue, 31 Oct 2023 10:44:51 -0400 Subject: [PATCH 03/19] Add WDL input to set number of retries. (#247) --- .../tests/benchmarking/docker_image_check_cuda_status.wdl | 5 ++++- wdl/cellbender_remove_background.wdl | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cellbender/remove_background/tests/benchmarking/docker_image_check_cuda_status.wdl b/cellbender/remove_background/tests/benchmarking/docker_image_check_cuda_status.wdl index 8763bfe..91ae277 100644 --- a/cellbender/remove_background/tests/benchmarking/docker_image_check_cuda_status.wdl +++ b/cellbender/remove_background/tests/benchmarking/docker_image_check_cuda_status.wdl @@ -12,6 +12,8 @@ task run_check_pytorch_cuda_status { Int? hardware_boot_disk_size_GB = 20 String? hardware_zones = "us-east1-d us-east1-c us-central1-a us-central1-c us-west1-b" String? hardware_gpu_type = "nvidia-tesla-t4" + Int? hardware_premptible_tries = 2 + Int? hardware_max_retries = 0 String? nvidia_driver_version = "470.82.01" # need >=465.19.01 for CUDA 11.3 } command { @@ -28,7 +30,8 @@ task run_check_pytorch_cuda_status { gpuCount: 1 gpuType: "${hardware_gpu_type}" nvidiaDriverVersion: "${nvidia_driver_version}" - maxRetries: 0 + preemptible: hardware_premptible_tries + maxRetries: hardware_max_retries } } diff --git a/wdl/cellbender_remove_background.wdl b/wdl/cellbender_remove_background.wdl index 4efb8fa..6f97463 100644 --- a/wdl/cellbender_remove_background.wdl +++ b/wdl/cellbender_remove_background.wdl @@ -58,6 +58,7 @@ task run_cellbender_remove_background_gpu { Int? hardware_disk_size_GB = 50 Int? hardware_boot_disk_size_GB = 20 Int? hardware_preemptible_tries = 2 + Int? hardware_max_retries = 0 Int? hardware_cpu_count = 4 Int? hardware_memory_GB = 32 String? hardware_gpu_type = "nvidia-tesla-t4" @@ -186,7 +187,7 @@ task run_cellbender_remove_background_gpu { nvidiaDriverVersion: "${nvidia_driver_version}" preemptible: hardware_preemptible_tries checkpointFile: "ckpt.tar.gz" - maxRetries: 0 # can be used in case of a PAPI error code 2 failure to install GPU drivers + maxRetries: hardware_max_retries # can be used in case of a PAPI error code 2 failure to install GPU drivers } meta { author: "Stephen Fleming" @@ -214,6 +215,8 @@ task run_cellbender_remove_background_gpu { {help: "Optional file only used by CellBender developers or those trying to benchmark CellBender remove-background on simulated data. Normally, this input would not be supplied."} hardware_preemptible_tries : {help: "If nonzero, CellBender will be run on a preemptible instance, at a lower cost. If preempted, your run will not start from scratch, but will start from a checkpoint that is saved by CellBender and recovered by Cromwell. For example, if hardware_preemptible_tries is 2, your run will attempt twice using preemptible instances, and if the job is preempted both times before completing, it will finish on a non-preemptible machine. The cost savings is significant. The potential drawback is that preemption wastes time."} + hardware_max_retries : + {help: "If nonzero when CellBender exits without success it will be retried. If one also sets the memory_retry_multiplier workflow option, and the exit happens to be detected as an out of memory error, then the retry will also increase the memory allocated to the next run. The potential benefit is that one can start CellBender with less memory, and memory will be increased only when needed. The potential drawback is that the job will be retried even if the error is not a memory error."} checkpoint_mins : {help: "Time in minutes between creation of checkpoint files. Bear in mind that Cromwell copies checkpoints to a bucket every ten minutes."} hardware_gpu_type : From 7a834a41aa121dce3f32ff428facfa8c7f269985 Mon Sep 17 00:00:00 2001 From: alecw Date: Tue, 31 Oct 2023 10:45:41 -0400 Subject: [PATCH 04/19] Move hash computation so that it is recomputed on retry, and now-invalid checkpoint is not loaded. (#258) If number of tries is exhausted, and ELBO tests are still failing, allow to complete anyway (using checkpoint) so that outputs are produced, but exit(1). --- cellbender/remove_background/cli.py | 11 ----------- cellbender/remove_background/run.py | 25 ++++++++++++++++++++++++- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/cellbender/remove_background/cli.py b/cellbender/remove_background/cli.py index ad49c01..29f7928 100644 --- a/cellbender/remove_background/cli.py +++ b/cellbender/remove_background/cli.py @@ -207,17 +207,6 @@ def setup_and_logging(args): + ' '.join(['cellbender', 'remove-background'] + sys.argv[2:])) logger.info("CellBender " + get_version()) - # Set up checkpointing by creating a unique workflow hash. - hashcode = create_workflow_hashcode( - module_path=os.path.dirname(cellbender.__file__), - args_to_remove=(['output_file', 'fpr', 'input_checkpoint_tarball', 'debug', - 'posterior_batch_size', 'checkpoint_min', 'truth_file', - 'posterior_regularization', 'cdf_threshold_q', 'prq_alpha', - 'estimator', 'use_multiprocessing_estimation', 'cpu_threads'] - + (['epochs'] if args.constant_learning_rate else [])), - args=args)[:10] - args.checkpoint_filename = hashcode # store this in args - logger.info(f'(Workflow hash {hashcode})') return args, file_handler diff --git a/cellbender/remove_background/run.py b/cellbender/remove_background/run.py index 7b777e4..42cf5b4 100644 --- a/cellbender/remove_background/run.py +++ b/cellbender/remove_background/run.py @@ -1,5 +1,6 @@ """Single run of remove-background, given input arguments.""" +import cellbender from cellbender.remove_background.model import RemoveBackgroundPyroModel from cellbender.remove_background.data.dataset import get_dataset_obj, \ SingleCellRNACountsDataset @@ -21,6 +22,7 @@ from cellbender.remove_background.sparse_utils import csr_set_rows_to_zero from cellbender.remove_background.data.io import write_matrix_to_cellranger_h5 from cellbender.remove_background.report import run_notebook_make_html, plot_summary +from cellbender.remove_background.checkpoint import create_workflow_hashcode import pyro from pyro.infer import SVI, JitTraceEnum_ELBO, JitTrace_ELBO, \ @@ -59,6 +61,22 @@ def run_remove_background(args: argparse.Namespace) -> Posterior: """ + # Set up checkpointing by creating a unique workflow hash. + hashcode = create_workflow_hashcode( + module_path=os.path.dirname(cellbender.__file__), + args_to_remove=(['output_file', 'fpr', 'input_checkpoint_tarball', 'debug', + 'posterior_batch_size', 'checkpoint_min', 'truth_file', + 'posterior_regularization', 'cdf_threshold_q', 'prq_alpha', + 'estimator', 'use_multiprocessing_estimation', 'cpu_threads', + # The following settings do not affect the results, and can change when retrying, + # so remove them. + 'epoch_elbo_fail_fraction', 'final_elbo_fail_fraction', + 'num_failed_attempts', 'checkpoint_filename'] + + (['epochs'] if args.constant_learning_rate else [])), + args=args)[:10] + args.checkpoint_filename = hashcode # store this in args + logger.info(f'(Workflow hash {hashcode})') + # Handle initial random state. pyro.util.set_rng_seed(consts.RANDOM_SEED) if torch.cuda.is_available(): @@ -771,7 +789,12 @@ def run_inference(dataset_obj: SingleCellRNACountsDataset, sys.exit(0) else: logger.info(f'No more attempts are specified by --num-training-tries. ' - f'Therefore the workflow will abort here.') + f'Therefore the workflow will run once more without ELBO restrictions.') + args.epoch_elbo_fail_fraction = None + args.final_elbo_fail_fraction = None + run_remove_background(args) # start from scratch + # non-zero exit status in order to draw user's attention to the fact that ELBO tests + # were never satisfied. sys.exit(1) logger.info("Inference procedure complete.") From cf71148b952e8f6ed087e56a413c44c61b3c4768 Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Tue, 31 Oct 2023 10:46:16 -0400 Subject: [PATCH 05/19] Bug fix for WDL using MTX input (#246) --- wdl/cellbender_remove_background.wdl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/wdl/cellbender_remove_background.wdl b/wdl/cellbender_remove_background.wdl index 6f97463..f7507ec 100644 --- a/wdl/cellbender_remove_background.wdl +++ b/wdl/cellbender_remove_background.wdl @@ -125,8 +125,15 @@ task run_cellbender_remove_background_gpu { [ -f $dir/$name ] || mv ~{genes_file} $dir/$name fi + # use the directory as the input in the case of an MTX file + if [[ "~{input_file_unfiltered}" == *.mtx* ]]; then + input=$(dirname ~{input_file_unfiltered}) + else + input=~{input_file_unfiltered} + fi + cellbender remove-background \ - --input "~{input_file_unfiltered}" \ + --input $input \ --output "~{sample_name}_out.h5" \ --cuda \ ~{"--checkpoint " + checkpoint_file} \ From 322971d81eb8ae745601515aa15227cc7af6d951 Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Tue, 31 Oct 2023 10:47:02 -0400 Subject: [PATCH 06/19] Memory-efficient posterior generation (#263) --- cellbender/remove_background/estimation.py | 45 +++----------- cellbender/remove_background/posterior.py | 61 ++++++++++--------- cellbender/remove_background/sparse_utils.py | 6 +- .../remove_background/tests/test_dataprep.py | 6 +- .../tests/test_sparse_utils.py | 6 +- 5 files changed, 50 insertions(+), 74 deletions(-) diff --git a/cellbender/remove_background/estimation.py b/cellbender/remove_background/estimation.py index 2443724..371a7fa 100644 --- a/cellbender/remove_background/estimation.py +++ b/cellbender/remove_background/estimation.py @@ -463,11 +463,9 @@ def estimate_noise(self, if use_multiple_processes: logger.info('Dividing dataset into chunks of genes') - chunk_logic_list = list( - self._gene_chunk_iterator( - noise_log_prob_coo=noise_log_prob_coo, - n_chunks=n_chunks, - ) + chunk_logic_list = self._gene_chunk_iterator( + noise_log_prob_coo=noise_log_prob_coo, + n_chunks=n_chunks, ) logger.info('Computing the output in asynchronous chunks in parallel...') @@ -538,10 +536,9 @@ def estimate_noise(self, def _gene_chunk_iterator(self, noise_log_prob_coo: sp.coo_matrix, n_chunks: int) \ - -> Generator[np.ndarray, None, None]: - """Yields chunks of the posterior that can be treated as independent, - from the standpoint of MCKP count estimation. That is, they contain all - matrix entries for any genes they include. + -> List[np.ndarray]: + """Return a list of logical (size m) arrays used to select gene chunks + on which to compute the MCKP estimate. These chunks are independent. Args: noise_log_prob_coo: Full noise log prob posterior COO @@ -551,36 +548,14 @@ def _gene_chunk_iterator(self, Logical array which indexes elements of coo posterior for the chunk """ - # TODO this generator is way too slow - - # approximate number of entries in a chunk - # approx_chunk_entries = (noise_log_prob_coo.data.size - 1) // n_chunks - # get gene annotations _, genes = self.index_converter.get_ng_indices(m_inds=noise_log_prob_coo.row) genes_series = pd.Series(genes) - # things we need to keep track of for each chunk - # current_chunk_genes = [] - # entry_logic = np.zeros(noise_log_prob_coo.data.size, dtype=bool) - - # TODO eliminate for loop to speed this up - # take the list of genes from the coo, sort it, and divide it evenly - # somehow break ties for genes overlapping boundaries of divisions - sorted_genes = np.sort(genes) - gene_arrays = np.array_split(sorted_genes, n_chunks) - last_gene_set = {} - for gene_array in gene_arrays: - gene_set = set(gene_array) - gene_set = gene_set.difference(last_gene_set) # only the new stuff - # if there is a second chunk, make sure there is a gene unique to it - if (n_chunks > 1) and (len(gene_set) == len(set(genes))): # all genes in first set - # this mainly exists for tests - gene_set = gene_set - {gene_arrays[-1][-1]} - last_gene_set = gene_set - entry_logic = genes_series.isin(gene_set).values - if sum(entry_logic) > 0: - yield entry_logic + gene_chunk_arrays = np.array_split(np.arange(self.index_converter.total_n_genes), n_chunks) + + gene_logic_arrays = [genes_series.isin(x).values for x in gene_chunk_arrays] + return gene_logic_arrays def _chunk_estimate_noise(self, noise_log_prob_coo: sp.coo_matrix, diff --git a/cellbender/remove_background/posterior.py b/cellbender/remove_background/posterior.py index 3758702..6aaeefa 100644 --- a/cellbender/remove_background/posterior.py +++ b/cellbender/remove_background/posterior.py @@ -451,7 +451,7 @@ def _get_cell_noise_count_posterior_coo( f'accurate for your dataset.') raise RuntimeError('Zero cells found!') - dataloader_index_to_analyzed_bc_index = np.where(cell_logic)[0] + dataloader_index_to_analyzed_bc_index = torch.where(torch.tensor(cell_logic))[0] cell_data_loader = DataLoader( count_matrix[cell_logic], empty_drop_dataset=None, @@ -468,6 +468,12 @@ def _get_cell_noise_count_posterior_coo( log_probs = [] ind = 0 n_minibatches = len(cell_data_loader) + analyzed_gene_inds = torch.tensor(self.analyzed_gene_inds.copy()) + if analyzed_bcs_only: + barcode_inds = torch.tensor(self.dataset_obj.analyzed_barcode_inds.copy()) + else: + barcode_inds = torch.tensor(self.barcode_inds.copy()) + nonzero_noise_offset_dict = {} logger.info('Computing posterior noise count probabilities in mini-batches.') @@ -505,46 +511,43 @@ def _get_cell_noise_count_posterior_coo( ) # Get the original gene index from gene index in the trimmed dataset. - genes_i = self.analyzed_gene_inds[genes_i_analyzed] + genes_i = analyzed_gene_inds[genes_i_analyzed.cpu()] # Barcode index in the dataloader. - bcs_i = bcs_i_chunk + ind + bcs_i = (bcs_i_chunk + ind).cpu() # Obtain the real barcode index since we only use cells. bcs_i = dataloader_index_to_analyzed_bc_index[bcs_i] # Translate chunk barcode inds to overall inds. - if analyzed_bcs_only: - bcs_i = self.dataset_obj.analyzed_barcode_inds[bcs_i] - else: - bcs_i = self.barcode_inds[bcs_i] + bcs_i = barcode_inds[bcs_i] # Add sparse matrix values to lists. - try: - bcs.extend(bcs_i.tolist()) - genes.extend(genes_i.tolist()) - c.extend(c_i.tolist()) - log_probs.extend(log_prob_i.tolist()) - c_offset.extend(noise_count_offset_NG[bcs_i_chunk, genes_i_analyzed] - .detach().cpu().numpy()) - except TypeError as e: - # edge case of a single value - bcs.append(bcs_i) - genes.append(genes_i) - c.append(c_i) - log_probs.append(log_prob_i) - c_offset.append(noise_count_offset_NG[bcs_i_chunk, genes_i_analyzed] - .detach().cpu().numpy()) + bcs.append(bcs_i.detach()) + genes.append(genes_i.detach()) + c.append(c_i.detach().cpu()) + log_probs.append(log_prob_i.detach().cpu()) + + # Update offset dict with any nonzeros. + nonzero_offset_inds, nonzero_noise_count_offsets = dense_to_sparse_op_torch( + noise_count_offset_NG[bcs_i_chunk, genes_i_analyzed].detach().flatten(), + ) + m_i = self.index_converter.get_m_indices(cell_inds=bcs_i, gene_inds=genes_i) + + nonzero_noise_offset_dict.update( + dict(zip(m_i[nonzero_offset_inds.detach().cpu()].tolist(), + nonzero_noise_count_offsets.detach().cpu().tolist())) + ) + c_offset.append(noise_count_offset_NG[bcs_i_chunk, genes_i_analyzed].detach().cpu()) # Increment barcode index counter. ind += data.shape[0] # Same as data_loader.batch_size - # Convert the lists to numpy arrays. - log_probs = np.array(log_probs, dtype=float) - c = np.array(c, dtype=np.uint32) - barcodes = np.array(bcs, dtype=np.uint64) # uint32 is too small! - genes = np.array(genes, dtype=np.uint64) # use same as above for IndexConverter - noise_count_offsets = np.array(c_offset, dtype=np.uint32) + # Concatenate lists. + log_probs = torch.cat(log_probs) + c = torch.cat(c) + barcodes = torch.cat(bcs) + genes = torch.cat(genes) # Translate (barcode, gene) inds to 'm' format index. m = self.index_converter.get_m_indices(cell_inds=barcodes, gene_inds=genes) @@ -554,8 +557,6 @@ def _get_cell_noise_count_posterior_coo( (log_probs, (m, c)), shape=[np.prod(self.count_matrix_shape), n_counts_max], ) - noise_offset_dict = dict(zip(m, noise_count_offsets)) - nonzero_noise_offset_dict = {k: v for k, v in noise_offset_dict.items() if (v > 0)} self._noise_count_posterior_coo_offsets = nonzero_noise_offset_dict return self._noise_count_posterior_coo diff --git a/cellbender/remove_background/sparse_utils.py b/cellbender/remove_background/sparse_utils.py index 4a0f26f..ca31329 100644 --- a/cellbender/remove_background/sparse_utils.py +++ b/cellbender/remove_background/sparse_utils.py @@ -10,7 +10,7 @@ @torch.no_grad() def dense_to_sparse_op_torch(t: torch.Tensor, tensor_for_nonzeros: Optional[torch.Tensor] = None) \ - -> Tuple[np.ndarray, ...]: + -> Tuple[torch.Tensor, ...]: """Converts dense matrix to sparse COO format tuple of numpy arrays (*indices, data) Args: @@ -28,9 +28,9 @@ def dense_to_sparse_op_torch(t: torch.Tensor, tensor_for_nonzeros = t nonzero_inds_tuple = torch.nonzero(tensor_for_nonzeros, as_tuple=True) - nonzero_values = t[nonzero_inds_tuple].flatten() + nonzero_values = t[nonzero_inds_tuple].flatten().clone() - return tuple([ten.cpu().numpy() for ten in (nonzero_inds_tuple + (nonzero_values,))]) + return nonzero_inds_tuple + (nonzero_values,) def log_prob_sparse_to_dense(coo: sp.coo_matrix) -> np.ndarray: diff --git a/cellbender/remove_background/tests/test_dataprep.py b/cellbender/remove_background/tests/test_dataprep.py index 8fcb156..5d11380 100644 --- a/cellbender/remove_background/tests/test_dataprep.py +++ b/cellbender/remove_background/tests/test_dataprep.py @@ -75,9 +75,9 @@ def test_dataloader_sorting(simulated_dataset, cuda): bcs_i = loader.unsort_inds(bcs_i) # Add sparse matrix values to lists. - barcodes.append(bcs_i) - genes.append(genes_i) - counts.append(counts_i) + barcodes.append(bcs_i.detach().cpu()) + genes.append(genes_i.detach().cpu()) + counts.append(counts_i.detach().cpu()) # Increment barcode index counter. ind += data.shape[0] # Same as data_loader.batch_size diff --git a/cellbender/remove_background/tests/test_sparse_utils.py b/cellbender/remove_background/tests/test_sparse_utils.py index 01230da..2f2e13e 100644 --- a/cellbender/remove_background/tests/test_sparse_utils.py +++ b/cellbender/remove_background/tests/test_sparse_utils.py @@ -76,9 +76,9 @@ def test_dense_to_sparse_op_torch(simulated_dataset, cuda): bcs_i = data_loader.unsort_inds(bcs_i) # Add sparse matrix values to lists. - barcodes.append(bcs_i) - genes.append(genes_i) - counts.append(counts_i) + barcodes.append(bcs_i.detach().cpu()) + genes.append(genes_i.detach().cpu()) + counts.append(counts_i.detach().cpu()) # Increment barcode index counter. ind += data.shape[0] # Same as data_loader.batch_size From 12b47582d0eafc698994d6f24f7289449df44568 Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Tue, 31 Oct 2023 10:47:36 -0400 Subject: [PATCH 07/19] Fix posterior and estimator integer overflow bugs on Windows (#259) --- .github/workflows/run_pytest.yml | 6 ++-- cellbender/remove_background/checkpoint.py | 2 +- cellbender/remove_background/estimation.py | 10 +++--- cellbender/remove_background/posterior.py | 4 +-- .../remove_background/tests/conftest.py | 24 +++++++++++-- .../tests/test_estimation.py | 23 ++++++++++++ .../remove_background/tests/test_posterior.py | 35 ++++++++++++++----- 7 files changed, 83 insertions(+), 21 deletions(-) diff --git a/.github/workflows/run_pytest.yml b/.github/workflows/run_pytest.yml index 37e652b..7c8c662 100644 --- a/.github/workflows/run_pytest.yml +++ b/.github/workflows/run_pytest.yml @@ -1,4 +1,4 @@ -# Run cellbender's tests +# Run cellbender test suite name: 'pytest' @@ -7,11 +7,13 @@ on: pull_request jobs: build: - runs-on: 'ubuntu-latest' strategy: matrix: + os: ['ubuntu-latest', 'windows-latest'] python-version: ['3.7'] + runs-on: ${{ matrix.os }} + steps: - name: 'Checkout repo' uses: actions/checkout@v3 diff --git a/cellbender/remove_background/checkpoint.py b/cellbender/remove_background/checkpoint.py index 5b0903d..206737b 100644 --- a/cellbender/remove_background/checkpoint.py +++ b/cellbender/remove_background/checkpoint.py @@ -297,7 +297,7 @@ def make_tarball(files: List[str], tarball_name: str) -> bool: for file in files: # without arcname, unpacking results in unpredictable file locations! tar.add(file, arcname=os.path.basename(file)) - os.rename(tarball_name + '.tmp', tarball_name) + os.replace(tarball_name + '.tmp', tarball_name) return True diff --git a/cellbender/remove_background/estimation.py b/cellbender/remove_background/estimation.py index 371a7fa..b4866d2 100644 --- a/cellbender/remove_background/estimation.py +++ b/cellbender/remove_background/estimation.py @@ -218,7 +218,7 @@ def _estimation_array_to_csr(index_converter, data: np.ndarray, m: np.ndarray, noise_offsets: Optional[Dict[int, int]], - dtype=np.int64) -> sp.csr_matrix: + dtype=np.int) -> sp.csr_matrix: """Say you have point estimates for each count matrix element (data) and you have the 'm'-indices for each value (m). This returns a CSR matrix that has the shape of the count matrix, where duplicate entries have @@ -229,7 +229,7 @@ def _estimation_array_to_csr(index_converter, a flat format, indexed by 'm'. m: Array of the same length as data, where each entry is an m-index. noise_offsets: Noise count offset values keyed by 'm'. - dtype: Data type for sparse matrix. Int32 is too small for 'm' indices. + dtype: Data type for values of sparse matrix Results: noise_csr: Noise point estimate, as a CSR sparse matrix. @@ -238,7 +238,7 @@ def _estimation_array_to_csr(index_converter, row, col = index_converter.get_ng_indices(m_inds=m) if noise_offsets is not None: data = data + np.array([noise_offsets.get(i, 0) for i in m]) - coo = sp.coo_matrix((data.astype(dtype), (row.astype(dtype), col.astype(dtype))), + coo = sp.coo_matrix((data.astype(dtype), (row.astype(np.uint64), col.astype(np.uint8))), shape=index_converter.matrix_shape, dtype=dtype) coo.sum_duplicates() return coo.tocsr() @@ -785,7 +785,7 @@ def apply_function_dense_chunks(noise_log_prob_coo: sp.coo_matrix, """ array_length = len(np.unique(noise_log_prob_coo.row)) - m = np.zeros(array_length) + m = np.zeros(array_length, dtype=np.uint64) out = np.zeros(array_length) a = 0 @@ -804,7 +804,7 @@ def apply_function_dense_chunks(noise_log_prob_coo: sp.coo_matrix, out[a:(a + len_s)] = s.detach().cpu().numpy() a = a + len_s - return {'m': m.astype(int), 'result': out} + return {'m': m, 'result': out} def pandas_grouped_apply(coo: sp.coo_matrix, diff --git a/cellbender/remove_background/posterior.py b/cellbender/remove_background/posterior.py index 6aaeefa..c421713 100644 --- a/cellbender/remove_background/posterior.py +++ b/cellbender/remove_background/posterior.py @@ -555,7 +555,7 @@ def _get_cell_noise_count_posterior_coo( # Put the counts into a sparse csr_matrix. self._noise_count_posterior_coo = sp.coo_matrix( (log_probs, (m, c)), - shape=[np.prod(self.count_matrix_shape), n_counts_max], + shape=[np.prod(self.count_matrix_shape, dtype=np.uint64), n_counts_max], ) self._noise_count_posterior_coo_offsets = nonzero_noise_offset_dict return self._noise_count_posterior_coo @@ -1528,7 +1528,7 @@ def get_m_indices(self, cell_inds: np.ndarray, gene_inds: np.ndarray) -> np.ndar if not ((gene_inds >= 0) & (gene_inds < self.total_n_genes)).all(): raise ValueError(f'Requested gene_inds out of range: ' f'{gene_inds[(gene_inds < 0) | (gene_inds >= self.total_n_genes)]}') - return cell_inds * self.total_n_genes + gene_inds + return cell_inds.astype(np.uint64) * self.total_n_genes + gene_inds.astype(np.uint64) def get_ng_indices(self, m_inds: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Given a list of 'm' index values, return two arrays: cell index values diff --git a/cellbender/remove_background/tests/conftest.py b/cellbender/remove_background/tests/conftest.py index 8bafa2d..6a43010 100644 --- a/cellbender/remove_background/tests/conftest.py +++ b/cellbender/remove_background/tests/conftest.py @@ -15,12 +15,32 @@ USE_CUDA = torch.cuda.is_available() -def sparse_matrix_equal(mat1: sp.csc_matrix, - mat2: sp.csc_matrix): +def sparse_matrix_equal(mat1, mat2): """Fast assertion that sparse matrices are equal""" + if type(mat1) == sp.coo_matrix: + return coo_equal(mat1, mat2) + elif (type(mat1) == sp.csc_matrix) or (type(mat1) == sp.csr_matrix): + return csc_or_csr_equal(mat1, mat2) + else: + raise ValueError(f"Error with test functions: sparse_matrix_equal() was called with a {type(mat1)}") + + +def csc_or_csr_equal(mat1: sp.csc_matrix, + mat2: sp.csc_matrix): + """Fast assertion that CSC sparse matrices are equal""" return (mat1 != mat2).sum() == 0 +def coo_equal(mat1: sp.csc_matrix, + mat2: sp.csc_matrix): + """Fast assertion that COO sparse matrices are equal""" + return ( + ((mat1.data != mat2.data).sum() == 0) + and ((mat1.row != mat2.row).sum() == 0) + and ((mat1.col != mat2.col).sum() == 0) + ) + + def tensors_equal(a: torch.Tensor, b: torch.Tensor): """Assertion that torch tensors are equal for each element""" diff --git a/cellbender/remove_background/tests/test_estimation.py b/cellbender/remove_background/tests/test_estimation.py index 528ab67..c1cb931 100644 --- a/cellbender/remove_background/tests/test_estimation.py +++ b/cellbender/remove_background/tests/test_estimation.py @@ -81,6 +81,29 @@ def log_prob_coo(request, log_prob_coo_base) \ raise ValueError(f'Test writing error: requested "{request.param}" log_prob_coo') +def test_mean_massive_m(log_prob_coo): + """Sets up a posterior COO with massive m values that are > max(int32). + Will trigger github issue #252 if a bug exists, no assertion necessary. + """ + + coo = log_prob_coo['coo'] + greater_than_max_int32 = 2200000000 + new_row = coo.row.astype(np.int64) + greater_than_max_int32 + new_shape = (coo.shape[0] + greater_than_max_int32, coo.shape[1]) + new_coo = sp.coo_matrix((coo.data, (new_row, coo.col)), + shape=new_shape) + offset_dict = {k + greater_than_max_int32: v for k, v in log_prob_coo['offsets'].items()} + + # this is just a shim + converter = IndexConverter(total_n_cells=2, + total_n_genes=new_coo.shape[0]) + + # set up and estimate + estimator = Mean(index_converter=converter) + noise_csr = estimator.estimate_noise(noise_log_prob_coo=new_coo, + noise_offsets=offset_dict) + + @pytest.fixture(scope='module', params=['exact', 'filtered', 'unsorted']) def mckp_log_prob_coo(request, log_prob_coo_base) \ -> Dict[str, Union[sp.coo_matrix, np.ndarray, Dict[int, int]]]: diff --git a/cellbender/remove_background/tests/test_posterior.py b/cellbender/remove_background/tests/test_posterior.py index e5382b3..04b766e 100644 --- a/cellbender/remove_background/tests/test_posterior.py +++ b/cellbender/remove_background/tests/test_posterior.py @@ -368,23 +368,35 @@ def test_compute_mean_target_removal_as_function(log_prob_coo, fpr, per_gene, cu @pytest.mark.parametrize('blank_noise_offsets', [False, True], ids=['', 'no_noise_offsets']) -def test_save_and_load(tmpdir_factory, blank_noise_offsets): +@pytest.mark.parametrize('m', [1000, 2200000000], ids=['small', 'big']) +def test_save_and_load(tmpdir_factory, blank_noise_offsets, m): """Test that a round trip through save and load gives the same thing""" tmp_dir = tmpdir_factory.mktemp('posterior') filename = tmp_dir.join('posterior.h5') - m = 1000 n = 20 + num_nonzeros = 1000 posterior = Posterior(dataset_obj=None, vi_model=None) # blank - posterior_coo = sp.random(m, n, density=0.1, format='coo', dtype=float) - posterior_coo2 = sp.random(m, n, density=0.08, format='coo', dtype=float) + # old way that cannot handle large m + # posterior_coo = sp.random(m, n, density=0.1, format='coo', dtype=float) + # posterior_coo2 = sp.random(m, n, density=0.08, format='coo', dtype=float) + + m_array = np.random.randint(low=0, high=m, size=num_nonzeros - 1, dtype=np.uint64) + m_array = np.concatenate([m_array, np.array(m - 1, dtype=np.uint64)], axis=None) + n_array = np.random.randint(low=0, high=n, size=num_nonzeros) + val_array = np.random.rand(num_nonzeros) * -10 + val_array2 = np.random.rand(num_nonzeros) * -5 + + posterior_coo = sp.coo_matrix((val_array, (m_array, n_array)), shape=(m, n)) + posterior_coo2 = sp.coo_matrix((val_array2, (m_array, n_array)), shape=(m, n)) + if blank_noise_offsets: noise_offsets = {} else: - noise_offsets = dict(zip(np.random.randint(low=0, high=(m - 1), size=10), + noise_offsets = dict(zip(np.random.randint(low=0, high=(m - 1), size=10, dtype=np.uint64), np.random.randint(low=1, high=5, size=10))) kwargs = {'a': 'b', 'c': 1} kwargs2 = {'a': 'method', 'c': 1} @@ -396,7 +408,7 @@ def test_save_and_load(tmpdir_factory, blank_noise_offsets): posterior._noise_count_regularized_posterior_coo = posterior_coo2 posterior._noise_count_regularized_posterior_kwargs = kwargs2 posterior._latents = {'p': np.random.randn(100), 'd': np.random.randn(100)} - posterior.index_converter = IndexConverter(total_n_cells=1000, total_n_genes=1000) + posterior.index_converter = IndexConverter(total_n_cells=max(1000, m // 1000 + 1), total_n_genes=1000) # save posterior.save(file=str(filename)) @@ -406,9 +418,14 @@ def test_save_and_load(tmpdir_factory, blank_noise_offsets): posterior2.load(file=str(filename)) # check - for attr in ['_noise_count_posterior_coo', '_noise_count_posterior_coo_offsets', - '_noise_count_posterior_kwargs', '_noise_count_regularized_posterior_coo', - '_noise_count_regularized_posterior_kwargs', '_latents']: + for attr in [ + '_noise_count_posterior_coo', + '_noise_count_posterior_coo_offsets', + '_noise_count_posterior_kwargs', + '_noise_count_regularized_posterior_coo', + '_noise_count_regularized_posterior_kwargs', + '_latents' + ]: val1 = getattr(posterior, attr) val2 = getattr(posterior2, attr) print(f'{attr} ===================') From be360063c9f59e5fedcaac86134fb928bd0d4be4 Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Tue, 31 Oct 2023 10:48:29 -0400 Subject: [PATCH 08/19] Move from setup.py to pyproject.toml (#240) --- MANIFEST.in | 5 ++- build_docker_release.sh | 2 +- cellbender/VERSION.txt | 1 + cellbender/__init__.py | 4 ++- cellbender/base_cli.py | 7 +--- pyproject.toml | 45 ++++++++++++++++++++++++ requirements-dev.txt | 2 ++ setup.py | 77 ----------------------------------------- 8 files changed, 57 insertions(+), 86 deletions(-) create mode 100644 cellbender/VERSION.txt create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt delete mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in index 66e89a1..679690c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,7 @@ include README.rst include LICENSE include requirements.txt -include requirements-rtd.txt \ No newline at end of file +include requirements-rtd.txt +include requirements-dev.txt +include cellbender/VERSION.txt +include cellbender/remove_background/report.ipynb \ No newline at end of file diff --git a/build_docker_release.sh b/build_docker_release.sh index ff0511f..532bffe 100755 --- a/build_docker_release.sh +++ b/build_docker_release.sh @@ -1,6 +1,6 @@ #!/bin/bash -tag=$(cat cellbender/__init__.py | sed -e 's?__version__ = ??' | sed "s/^'\(.*\)'$/\1/") +tag=$(cat cellbender/VERSION.txt) release=v${tag} docker build \ diff --git a/cellbender/VERSION.txt b/cellbender/VERSION.txt new file mode 100644 index 0000000..9325c3c --- /dev/null +++ b/cellbender/VERSION.txt @@ -0,0 +1 @@ +0.3.0 \ No newline at end of file diff --git a/cellbender/__init__.py b/cellbender/__init__.py index 0404d81..9fdd33d 100644 --- a/cellbender/__init__.py +++ b/cellbender/__init__.py @@ -1 +1,3 @@ -__version__ = '0.3.0' +from .base_cli import get_version + +__version__ = get_version() diff --git a/cellbender/base_cli.py b/cellbender/base_cli.py index bf5a71b..fca8eb7 100644 --- a/cellbender/base_cli.py +++ b/cellbender/base_cli.py @@ -23,12 +23,7 @@ def read(rel_path): def get_version() -> str: - for line in read('__init__.py').splitlines(): - if line.startswith('__version__'): - delim = '"' if '"' in line else "'" - return line.split(delim)[1] - else: - raise RuntimeError("Unable to find version string.") + return read('VERSION.txt').splitlines()[0] class AbstractCLI(ABC): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6a0d36d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,45 @@ +[build-system] +requires = ["setuptools>=61.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "cellbender" +authors = [{name = "Stephen Fleming"}, {name = "Mehrtash Babadi"}] +license = {text = "BSD (3-Clause)"} +description = "A software package for eliminating technical artifacts from high-throughput single-cell RNA sequencing (scRNA-seq) data" +keywords = ["scRNA-seq", "bioinformatics"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3.7", + "Topic :: Scientific/Engineering :: Bio-Informatics", +] +dynamic = ["version", "dependencies", "optional-dependencies"] + +[project.readme] +file = "README.rst" +content-type = "text/x-rst" + +[project.urls] +Homepage = "http://github.com/broadinstitute/CellBender" +Documentation = "http://cellbender.readthedocs.io" + +[project.scripts] +cellbender = "cellbender.base_cli:main" + +[tool.setuptools] +zip-safe = false +include-package-data = true + +[tool.setuptools.packages] +find = {namespaces = false} + +[tool.setuptools.dynamic] +version = {attr = "cellbender.__version__"} +dependencies = {file = ["requirements.txt"]} +optional-dependencies.dev = {file = ["requirements-dev.txt"]} +optional-dependencies.docs = {file = ["requirements-rtd.txt"]} + +[tool.setuptools.package-data] +"*" = ["cellbender.remove_background.report.ipynb", "cellbender.VERSION.txt"] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..3257b82 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest +scikit-learn diff --git a/setup.py b/setup.py deleted file mode 100644 index 106fa57..0000000 --- a/setup.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python - -import os -import setuptools -import codecs -from typing import List - - -def read(rel_path): - here = os.path.abspath(os.path.dirname(__file__)) - with codecs.open(os.path.join(here, rel_path), 'r') as fp: - return fp.read() - - -def get_version() -> str: - for line in read('cellbender/__init__.py').splitlines(): - if line.startswith('__version__'): - delim = '"' if '"' in line else "'" - return line.split(delim)[1] - else: - raise RuntimeError("Unable to find version string.") - - -def readme() -> str: - with open('README.rst') as f: - return f.read() - - -def _readlines(filename, filebase=''): - with open(os.path.join(filebase, filename)) as f: - lines = f.readlines() - return lines - - -def get_requirements() -> List[str]: - requirements = _readlines('requirements.txt') - if 'READTHEDOCS' in os.environ: - requirements.extend(get_rtd_requirements()) - return requirements - - -def get_rtd_requirements() -> List[str]: - requirements = _readlines('requirements-rtd.txt') - return requirements - - -setuptools.setup( - name='cellbender', - version=get_version(), - description='A software package for eliminating technical artifacts from ' - 'high-throughput single-cell RNA sequencing (scRNA-seq) data', - long_description=readme(), - long_description_content_type='text/x-rst', - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python :: 3.7', - 'Topic :: Scientific/Engineering :: Bio-Informatics', - ], - keywords='scRNA-seq bioinformatics', - url='http://github.com/broadinstitute/CellBender', - author='Stephen Fleming, Mehrtash Babadi', - license='BSD (3-Clause)', - packages=setuptools.find_packages(), - install_requires=get_requirements(), - extras_require={ - "dev": ["pytest", "scikit-learn"], - "docs": get_rtd_requirements(), - }, - entry_points={ - 'console_scripts': ['cellbender=cellbender.base_cli:main'], - }, - include_package_data=True, - package_data={'': ['*.ipynb']}, # include the report template - zip_safe=False -) From 084194b4612623976cb5a46cd1b47f4afbe6fdec Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Tue, 31 Oct 2023 11:55:37 -0400 Subject: [PATCH 09/19] Fix bugs with report generation across platforms (#302) --- cellbender/remove_background/posterior.py | 11 ++++++++-- cellbender/remove_background/report.py | 25 ++++++++++++++--------- cellbender/remove_background/run.py | 1 + 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/cellbender/remove_background/posterior.py b/cellbender/remove_background/posterior.py index c421713..d99e533 100644 --- a/cellbender/remove_background/posterior.py +++ b/cellbender/remove_background/posterior.py @@ -1518,7 +1518,9 @@ def __repr__(self): f'\n\ttotal_n_genes: {self.total_n_genes}' f'\n\tmatrix_shape: {self.matrix_shape}') - def get_m_indices(self, cell_inds: np.ndarray, gene_inds: np.ndarray) -> np.ndarray: + def get_m_indices(self, + cell_inds: Union[np.ndarray, torch.Tensor], + gene_inds: Union[np.ndarray, torch.Tensor]) -> Union[np.ndarray, torch.Tensor]: """Given arrays of cell indices and gene indices, suitable for a sparse matrix, convert them to 'm' index values. """ @@ -1528,7 +1530,12 @@ def get_m_indices(self, cell_inds: np.ndarray, gene_inds: np.ndarray) -> np.ndar if not ((gene_inds >= 0) & (gene_inds < self.total_n_genes)).all(): raise ValueError(f'Requested gene_inds out of range: ' f'{gene_inds[(gene_inds < 0) | (gene_inds >= self.total_n_genes)]}') - return cell_inds.astype(np.uint64) * self.total_n_genes + gene_inds.astype(np.uint64) + if type(cell_inds) == np.ndarray: + return cell_inds.astype(np.uint64) * self.total_n_genes + gene_inds.astype(np.uint64) + elif type(cell_inds) == torch.Tensor: + return cell_inds.type(torch.int64) * self.total_n_genes + gene_inds.type(torch.int64) + else: + raise ValueError('IndexConverter.get_m_indices received cell_inds of unkown object type') def get_ng_indices(self, m_inds: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Given a list of 'm' index values, return two arrays: cell index values diff --git a/cellbender/remove_background/report.py b/cellbender/remove_background/report.py index f1a34cb..dfb5f90 100644 --- a/cellbender/remove_background/report.py +++ b/cellbender/remove_background/report.py @@ -16,6 +16,7 @@ import subprocess import datetime import os +import shutil import logging from typing import Dict, Optional @@ -42,26 +43,30 @@ def _run_notebook(file): - subprocess.run(f'cp {file} tmp.report.ipynb', shell=True) + shutil.copy(file, 'tmp.report.ipynb') subprocess.run(run_notebook_str(file='tmp.report.ipynb'), shell=True) - subprocess.run(f'rm tmp.report.ipynb', shell=True) + os.remove('tmp.report.ipynb') return 'tmp.report.nbconvert.ipynb' def _to_html(file, output) -> str: subprocess.run(to_html_str(file=file, output=output), shell=True) - subprocess.run(f'mv {file.replace(".ipynb", ".html")} {output}', shell=True) - subprocess.run(f'rm {file}', shell=True) + os.replace(file.replace(".ipynb", ".html"), output) + os.remove(file) return output def _postprocess_html(file: str, title: str): - with open(file, mode='r') as f: - html = f.read() - html = html.replace('tmp.report.nbconvert', - f'{title}') - with open(file, mode='w') as f: - f.write(html) + try: + with open(file, mode='r', encoding="utf8", errors="surrogateescape") as f: + html = f.read() + html = html.replace('tmp.report.nbconvert', + f'{title}') + with open(file, mode='w', encoding="utf8", errors="surrogateescape") as f: + f.write(html) + except: + logger.warning('Failed to overwrite default HTML report title. ' + 'This is purely aesthetic and does not affect output.') def run_notebook_make_html(file, output) -> str: diff --git a/cellbender/remove_background/run.py b/cellbender/remove_background/run.py index 42cf5b4..ca2a592 100644 --- a/cellbender/remove_background/run.py +++ b/cellbender/remove_background/run.py @@ -43,6 +43,7 @@ from typing import Tuple, Optional, Dict, Union import matplotlib +import matplotlib.backends.backend_pdf # issue #287 matplotlib.use('Agg') import matplotlib.pyplot as plt # This needs to be after matplotlib.use('Agg') From 97c1d408eb7bea18efded99777230cc473fce4cf Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Wed, 10 Apr 2024 22:38:53 -0400 Subject: [PATCH 10/19] Fix major bug in v0.3.1: negative counts (#347) * issue #348 lxml_html_clean dependency * Fix #306: noise count integer overflow * Introduce --force-use-checkpoint for redoing v0.3.1 runs * Add a warning to the README about v0.3.1 --- .github/workflows/miniwdl_check.yml | 7 +++- .github/workflows/run_packaging_check.yml | 4 ++- .github/workflows/run_pytest.yml | 4 ++- .gitignore | 1 + README.rst | 26 ++++++++++++++ cellbender/remove_background/argparser.py | 11 +++++- cellbender/remove_background/checkpoint.py | 36 ++++++++++++------- cellbender/remove_background/estimation.py | 10 ++++-- cellbender/remove_background/posterior.py | 6 ++-- cellbender/remove_background/run.py | 6 +++- .../tests/test_checkpoint.py | 1 + .../tests/test_estimation.py | 36 +++++++++++++++++-- .../tests/test_integration.py | 3 ++ requirements.txt | 1 + 14 files changed, 126 insertions(+), 26 deletions(-) diff --git a/.github/workflows/miniwdl_check.yml b/.github/workflows/miniwdl_check.yml index 8a8de2c..34d1a4c 100644 --- a/.github/workflows/miniwdl_check.yml +++ b/.github/workflows/miniwdl_check.yml @@ -1,7 +1,12 @@ name: 'validate WDL' -on: [pull_request] + +on: + pull_request: + branches: [ master, dev ] + env: MINIWDL_VERSION: 1.8.0 + jobs: miniwdl-check: runs-on: ubuntu-latest diff --git a/.github/workflows/run_packaging_check.yml b/.github/workflows/run_packaging_check.yml index 8a9fc1c..6144cbe 100644 --- a/.github/workflows/run_packaging_check.yml +++ b/.github/workflows/run_packaging_check.yml @@ -2,7 +2,9 @@ name: 'packaging' -on: pull_request +on: + pull_request: + branches: [ master, dev ] jobs: build: diff --git a/.github/workflows/run_pytest.yml b/.github/workflows/run_pytest.yml index 7c8c662..b6a02ae 100644 --- a/.github/workflows/run_pytest.yml +++ b/.github/workflows/run_pytest.yml @@ -2,7 +2,9 @@ name: 'pytest' -on: pull_request +on: + pull_request: + branches: [ master, dev ] jobs: build: diff --git a/.gitignore b/.gitignore index 80b6c0c..4d5b686 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ dist/ *.csv *.npz *.tar.gz +data/ \ No newline at end of file diff --git a/README.rst b/README.rst index d5895c7..f32bdef 100644 --- a/README.rst +++ b/README.rst @@ -36,6 +36,32 @@ The current release contains the following modules. More modules will be added i Please refer to `the documentation `_ for a quick start tutorial. + WARNING: + + The release tagged v0.3.1 included a bug which caused output count matrices to be incorrect. `The bug + `_, + introduced in `#303 `_, compromised output denoised count matrices + (due to an integer overflow) and would often show up as negative entries in the output count matrices. The bug also existed on + the master branch until `#347 `_. + + For now, we recommend using either v0.3.0 or the master branch (after #347) until v0.3.2 is released, and then using v0.3.2. + + Outputs generated with v0.3.1 (or the master branch between #303 and #347) can be salvaged by making use of the + checkpoint file, which is not compromised. The following command will re-run the (inexpensive, CPU-only) + estimation of the output count matrix using the saved posterior in the checkpoint file. Note the use of the new + input argument ``--force-use-checkpoint`` which will allow use of a checkpoint file produced by a different CellBender version: + + .. code-block:: console + + (cellbender) $ cellbender remove-background \ + --input my_raw_count_matrix_file.h5 \ + --output my_cellbender_output_file.h5 \ + --checkpoint path/to/ckpt.tar.gz \ + --force-use-checkpoint + + where ``path/to/ckpt.tar.gz`` is the path to the checkpoint file generated by the original run. Ensure that you pair up the right + ``--input`` with the right ``--checkpoint``. + Installation and Usage ---------------------- diff --git a/cellbender/remove_background/argparser.py b/cellbender/remove_background/argparser.py index d53aca3..aebe670 100644 --- a/cellbender/remove_background/argparser.py +++ b/cellbender/remove_background/argparser.py @@ -48,10 +48,19 @@ def add_subparser_args(subparsers: argparse) -> argparse: subparser.add_argument("--checkpoint", nargs=None, type=str, dest='input_checkpoint_tarball', required=False, default=consts.CHECKPOINT_FILE_NAME, - help="Checkpoint tarball produced by the same version " + help="Checkpoint tarball produced by v0.3.0+ " "of CellBender remove-background. If present, " "and the workflow hashes match, training will " "restart from this checkpoint.") + subparser.add_argument("--force-use-checkpoint", + dest='force_use_checkpoint', action="store_true", + help="Normally, checkpoints can only be used if the CellBender " + "code and certain input args match exactly. This flag allows you " + "to bypass this requirement. An example use would be to create a new output " + "using a checkpoint from a run of v0.3.1, a redacted version with " + "faulty output count matrices. If you use this flag, " + "ensure that the input file and the checkpoint match, because " + "CellBender will not check.") subparser.add_argument("--expected-cells", nargs=None, type=int, default=None, dest="expected_cell_count", diff --git a/cellbender/remove_background/checkpoint.py b/cellbender/remove_background/checkpoint.py index 206737b..44ed12e 100644 --- a/cellbender/remove_background/checkpoint.py +++ b/cellbender/remove_background/checkpoint.py @@ -154,7 +154,8 @@ def save_checkpoint(filebase: str, def load_checkpoint(filebase: Optional[str], tarball_name: str = consts.CHECKPOINT_FILE_NAME, - force_device: Optional[str] = None)\ + force_device: Optional[str] = None, + force_use_checkpoint: bool = False)\ -> Dict[str, Union['RemoveBackgroundPyroModel', pyro.optim.PyroOptim, DataLoader, bool]]: """Load checkpoint and prepare a RemoveBackgroundPyroModel and optimizer.""" @@ -163,6 +164,7 @@ def load_checkpoint(filebase: Optional[str], tarball_name=tarball_name, to_load=['model', 'optim', 'param_store', 'dataloader', 'args', 'random_state'], force_device=force_device, + force_use_checkpoint=force_use_checkpoint, ) out.update({'loaded': True}) logger.info(f'Loaded partially-trained checkpoint from {tarball_name}') @@ -172,7 +174,8 @@ def load_checkpoint(filebase: Optional[str], def load_from_checkpoint(filebase: Optional[str], tarball_name: str = consts.CHECKPOINT_FILE_NAME, to_load: List[str] = ['model'], - force_device: Optional[str] = None) -> Dict: + force_device: Optional[str] = None, + force_use_checkpoint: bool = False) -> Dict: """Load specific files from a checkpoint tarball.""" load_kwargs = {} @@ -192,19 +195,24 @@ def load_from_checkpoint(filebase: Optional[str], else: # no tarball loaded, so do not continue trying to load files raise FileNotFoundError - - # See if files have a hash matching input filebase. - if filebase is not None: + + # If posterior is present, do not require run hash to match: will pick up + # after training and run estimation from existing posterior. + # This smoothly allows re-runs (including for problematic v0.3.1) + logger.debug(f'force_use_checkpoint: {force_use_checkpoint}') + if force_use_checkpoint or (filebase is None): + filebase = (glob.glob(os.path.join(tmp_dir, '*_model.torch'))[0] + .replace('_model.torch', '')) + logger.debug(f'Accepting any file hash, so loading {filebase}*') + + else: + # See if files have a hash matching input filebase. basename = os.path.basename(filebase) filebase = os.path.join(tmp_dir, basename) logger.debug(f'Looking for files with base name matching {filebase}*') if not os.path.exists(filebase + '_model.torch'): logger.info('Workflow hash does not match that of checkpoint.') - raise ValueError('Workflow hash does not match that of checkpoint.') - else: - filebase = (glob.glob(os.path.join(tmp_dir, '*_model.torch'))[0] - .replace('_model.torch', '')) - logger.debug(f'Accepting any file hash, so loading {filebase}*') + raise ValueError('Workflow hash does not match that of checkpoint.') out = {} @@ -265,9 +273,10 @@ def load_from_checkpoint(filebase: Optional[str], return out -def attempt_load_checkpoint(filebase: str, +def attempt_load_checkpoint(filebase: Optional[str], tarball_name: str = consts.CHECKPOINT_FILE_NAME, - force_device: Optional[str] = None)\ + force_device: Optional[str] = None, + force_use_checkpoint: bool = False)\ -> Dict[str, Union['RemoveBackgroundPyroModel', pyro.optim.PyroOptim, DataLoader, bool]]: """Load checkpoint and prepare a RemoveBackgroundPyroModel and optimizer, or return the inputs if loading fails.""" @@ -276,7 +285,8 @@ def attempt_load_checkpoint(filebase: str, logger.debug('Attempting to load checkpoint from ' + tarball_name) return load_checkpoint(filebase=filebase, tarball_name=tarball_name, - force_device=force_device) + force_device=force_device, + force_use_checkpoint=force_use_checkpoint) except FileNotFoundError: logger.debug('No tarball found') diff --git a/cellbender/remove_background/estimation.py b/cellbender/remove_background/estimation.py index b4866d2..777bf03 100644 --- a/cellbender/remove_background/estimation.py +++ b/cellbender/remove_background/estimation.py @@ -21,6 +21,10 @@ logger = logging.getLogger('cellbender') +N_CELLS_DATATYPE = np.int32 +N_GENES_DATATYPE = np.int32 +COUNT_DATATYPE = np.int32 + class EstimationMethod(ABC): """Base class for estimation of noise counts, given a posterior.""" @@ -52,7 +56,7 @@ def _estimation_array_to_csr(self, data: np.ndarray, m: np.ndarray, noise_offsets: Optional[Dict[int, int]], - dtype=np.int64) -> sp.csr_matrix: + dtype=COUNT_DATATYPE) -> sp.csr_matrix: """Say you have point estimates for each count matrix element (data) and you have the 'm'-indices for each value (m). This returns a CSR matrix that has the shape of the count matrix, where duplicate entries have @@ -218,7 +222,7 @@ def _estimation_array_to_csr(index_converter, data: np.ndarray, m: np.ndarray, noise_offsets: Optional[Dict[int, int]], - dtype=np.int) -> sp.csr_matrix: + dtype=COUNT_DATATYPE) -> sp.csr_matrix: """Say you have point estimates for each count matrix element (data) and you have the 'm'-indices for each value (m). This returns a CSR matrix that has the shape of the count matrix, where duplicate entries have @@ -238,7 +242,7 @@ def _estimation_array_to_csr(index_converter, row, col = index_converter.get_ng_indices(m_inds=m) if noise_offsets is not None: data = data + np.array([noise_offsets.get(i, 0) for i in m]) - coo = sp.coo_matrix((data.astype(dtype), (row.astype(np.uint64), col.astype(np.uint8))), + coo = sp.coo_matrix((data.astype(dtype), (row.astype(N_CELLS_DATATYPE), col.astype(N_GENES_DATATYPE))), shape=index_converter.matrix_shape, dtype=dtype) coo.sum_duplicates() return coo.tocsr() diff --git a/cellbender/remove_background/posterior.py b/cellbender/remove_background/posterior.py index d99e533..5bdd890 100644 --- a/cellbender/remove_background/posterior.py +++ b/cellbender/remove_background/posterior.py @@ -107,13 +107,15 @@ def _do_posterior_regularization(posterior: Posterior): try: ckpt_posterior = load_from_checkpoint(tarball_name=args.input_checkpoint_tarball, filebase=args.checkpoint_filename, - to_load=['posterior']) + to_load=['posterior'], + force_use_checkpoint=args.force_use_checkpoint) except ValueError: # input checkpoint tarball was not a match for this workflow # but we still may have saved a new tarball ckpt_posterior = load_from_checkpoint(tarball_name=consts.CHECKPOINT_FILE_NAME, filebase=args.checkpoint_filename, - to_load=['posterior']) + to_load=['posterior'], + force_use_checkpoint=args.force_use_checkpoint) if os.path.exists(ckpt_posterior.get('posterior_file', 'does_not_exist')): # Load posterior if it was saved in the checkpoint. posterior.load(file=ckpt_posterior['posterior_file']) diff --git a/cellbender/remove_background/run.py b/cellbender/remove_background/run.py index ca2a592..7ac466b 100644 --- a/cellbender/remove_background/run.py +++ b/cellbender/remove_background/run.py @@ -307,6 +307,9 @@ def compute_output_denoised_counts_reports_metrics(posterior: Posterior, posterior.latents_map['p'], ) + # Failsafe to ensure no negative counts. + assert np.all(denoised_counts.data >= 0), 'Negative count matrix entries in output' + # TODO: correct cell probabilities so that any zero-count droplet becomes "empty" # Save denoised count matrix. @@ -627,7 +630,8 @@ def run_inference(dataset_obj: SingleCellRNACountsDataset, # Attempt to load from a previously-saved checkpoint. ckpt = attempt_load_checkpoint(filebase=checkpoint_filename, tarball_name=args.input_checkpoint_tarball, - force_device='cuda:0' if args.use_cuda else 'cpu') + force_device='cuda:0' if args.use_cuda else 'cpu', + force_use_checkpoint=args.force_use_checkpoint) ckpt_loaded = ckpt['loaded'] # True if a checkpoint was loaded successfully if ckpt_loaded: diff --git a/cellbender/remove_background/tests/test_checkpoint.py b/cellbender/remove_background/tests/test_checkpoint.py index 59ccb9e..0dbaa22 100644 --- a/cellbender/remove_background/tests/test_checkpoint.py +++ b/cellbender/remove_background/tests/test_checkpoint.py @@ -414,6 +414,7 @@ def test_save_and_load_cellbender_checkpoint(tmpdir_factory, cuda, scheduler): args.constant_learning_rate = not scheduler args.debug = False args.input_checkpoint_tarball = 'none' + args.force_use_checkpoint = False create_random_state_blank_slate(0) pyro.clear_param_store() diff --git a/cellbender/remove_background/tests/test_estimation.py b/cellbender/remove_background/tests/test_estimation.py index c1cb931..2cca641 100644 --- a/cellbender/remove_background/tests/test_estimation.py +++ b/cellbender/remove_background/tests/test_estimation.py @@ -6,9 +6,10 @@ import torch from cellbender.remove_background.estimation import Mean, MAP, \ - SingleSample, ThresholdCDF, MultipleChoiceKnapsack, pandas_grouped_apply + SingleSample, ThresholdCDF, MultipleChoiceKnapsack, pandas_grouped_apply, _estimation_array_to_csr, COUNT_DATATYPE from cellbender.remove_background.posterior import IndexConverter, \ dense_to_sparse_op_torch, log_prob_sparse_to_dense +from cellbender.remove_background.tests.conftest import sparse_matrix_equal from typing import Dict, Union @@ -92,11 +93,15 @@ def test_mean_massive_m(log_prob_coo): new_shape = (coo.shape[0] + greater_than_max_int32, coo.shape[1]) new_coo = sp.coo_matrix((coo.data, (new_row, coo.col)), shape=new_shape) + print(f'original COO shape: {coo.shape}') + print(f'new COO shape: {new_coo.shape}') + print(f'new row minimum value: {new_coo.row.min()}') + print(f'new row maximum value: {new_coo.row.max()}') offset_dict = {k + greater_than_max_int32: v for k, v in log_prob_coo['offsets'].items()} # this is just a shim - converter = IndexConverter(total_n_cells=2, - total_n_genes=new_coo.shape[0]) + converter = IndexConverter(total_n_cells=new_coo.shape[0], + total_n_genes=new_coo.shape[1]) # set up and estimate estimator = Mean(index_converter=converter) @@ -379,3 +384,28 @@ def test_parallel_pandas_grouped_apply(fun): np.testing.assert_array_equal(reg['m'], parallel['m']) np.testing.assert_array_equal(reg['result'], parallel['result']) + + +def test_estimation_array_to_csr(): + + larger_than_uint16 = 2**16 + 1 + + converter = IndexConverter(total_n_cells=larger_than_uint16, + total_n_genes=larger_than_uint16) + m = larger_than_uint16 + np.arange(-10, 10) + data = np.random.rand(len(m)) * -10 + noise_offsets = None + + output_csr = _estimation_array_to_csr(index_converter=converter, data=data, m=m, noise_offsets=noise_offsets, dtype=COUNT_DATATYPE) + + # reimplementation here with totally permissive datatypes + cell_and_gene_dtype = np.float64 + row, col = converter.get_ng_indices(m_inds=m) + if noise_offsets is not None: + data = data + np.array([noise_offsets.get(i, 0) for i in m]) + coo = sp.coo_matrix((data.astype(COUNT_DATATYPE), (row.astype(cell_and_gene_dtype), col.astype(cell_and_gene_dtype))), + shape=converter.matrix_shape, dtype=COUNT_DATATYPE) + coo.sum_duplicates() + truth_csr = coo.tocsr() + + assert sparse_matrix_equal(output_csr, truth_csr) diff --git a/cellbender/remove_background/tests/test_integration.py b/cellbender/remove_background/tests/test_integration.py index 624c59d..078e9f2 100644 --- a/cellbender/remove_background/tests/test_integration.py +++ b/cellbender/remove_background/tests/test_integration.py @@ -50,3 +50,6 @@ def test_full_run(tmpdir_factory, h5_v3_file, cuda): adata_cell_barcodes = adata.obs_names[adata.obs['cell_probability'] > consts.CELL_PROB_CUTOFF] assert set(cell_barcodes) == set(adata_cell_barcodes), \ 'Cell barcodes in h5 are different from those in CSV file' + + # ensure there are no negative count matrix entries in the output + assert np.all(adata.X.data >= 0), 'Negative count matrix entries in output' diff --git a/requirements.txt b/requirements.txt index 3585b6a..9b68a3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,5 @@ jupyter jupyter_contrib_nbextensions notebook<7.0.0 nbconvert<7.0.0 +lxml_html_clean psutil From 578c214e1afe9d737c4920c4296c63fcb98d4258 Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Fri, 19 Apr 2024 20:55:35 +0000 Subject: [PATCH 11/19] Tweak readme appearance --- README.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index f32bdef..76add55 100644 --- a/README.rst +++ b/README.rst @@ -1,23 +1,25 @@ CellBender ========== -.. image:: https://img.shields.io/github/license/broadinstitute/CellBender?color=white +|badge1| |badge2| |badge3| |badge4| |badge5| + +.. |badge1| image:: https://img.shields.io/github/license/broadinstitute/CellBender?color=white :target: LICENSE :alt: License -.. image:: https://readthedocs.org/projects/cellbender/badge/?version=latest +.. |badge2| image:: https://readthedocs.org/projects/cellbender/badge/?version=latest :target: https://cellbender.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status -.. image:: https://img.shields.io/pypi/v/CellBender.svg +.. |badge3| image:: https://img.shields.io/pypi/v/CellBender.svg :target: https://pypi.org/project/CellBender :alt: PyPI -.. image:: https://static.pepy.tech/personalized-badge/cellbender?period=total&units=international_system&left_color=grey&right_color=blue&left_text=pypi%20downloads +.. |badge4| image:: https://static.pepy.tech/personalized-badge/cellbender?period=total&units=international_system&left_color=grey&right_color=blue&left_text=pypi%20downloads :target: https://pepy.tech/project/CellBender :alt: Downloads -.. image:: https://img.shields.io/github/stars/broadinstitute/CellBender?color=yellow&logoColor=yellow) +.. |badge5| image:: https://img.shields.io/github/stars/broadinstitute/CellBender?color=yellow&logoColor=yellow) :target: https://github.com/broadinstitute/CellBender/stargazers :alt: Stars From 80514f06aea41795486ee0caea07a41b67498f90 Mon Sep 17 00:00:00 2001 From: Jura Pintar Date: Mon, 22 Apr 2024 15:43:11 -0400 Subject: [PATCH 12/19] Add newer Cell Ranger feature types (#339) In Cell Ranger 7+, there is a longer list of features one can specify in `cellranger multi`. These are added here to the list of allowed features. --- cellbender/remove_background/cli.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cellbender/remove_background/cli.py b/cellbender/remove_background/cli.py index 29f7928..fee0479 100644 --- a/cellbender/remove_background/cli.py +++ b/cellbender/remove_background/cli.py @@ -117,14 +117,22 @@ def validate_args(args) -> argparse.Namespace: args.fpr = fpr_list_correct_dtypes # Ensure that "exclude_features" specifies allowed features. - # As of CellRanger 6.0, the possible features are: + # As of CellRanger 7.2, the possible features are: # Gene Expression # Antibody Capture # CRISPR Guide Capture # Custom # Peaks + # Multiplexing Capture + # VDJ + # VDJ-T + # VDJ-T-GD + # VDJ-B + # Antigen Capture allowed_features = ['Gene Expression', 'Antibody Capture', - 'CRISPR Guide Capture', 'Custom', 'Peaks'] + 'CRISPR Guide Capture', 'Custom', 'Peaks', + 'Multiplexing Capture', 'VDJ', 'VDJ-T', + 'VDJ-T-GD', 'VDJ-B', 'Antigen Capture'] for feature in args.exclude_features: if feature not in allowed_features: sys.stdout.write(f"Specified '{feature}' using --exclude-feature-types, " From 72e6a3fa405c917a1e2faba971235eed037ef74f Mon Sep 17 00:00:00 2001 From: alecw Date: Mon, 22 Apr 2024 15:58:20 -0400 Subject: [PATCH 13/19] Fix test_elbo bug when retrying with more memory from checkpoint (#345) Keep test_elbo as part of model, and refer to it there from the retry code --- cellbender/remove_background/train.py | 30 +++++++++++++-------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/cellbender/remove_background/train.py b/cellbender/remove_background/train.py index 8d77808..bf7bed6 100644 --- a/cellbender/remove_background/train.py +++ b/cellbender/remove_background/train.py @@ -152,7 +152,6 @@ def run_training(model: RemoveBackgroundPyroModel, # Initialize train and tests ELBO with empty lists. train_elbo = [] - test_elbo = [] lr = [] epoch_checkpoint_freq = 1000 # a large number... it will be recalculated @@ -212,16 +211,15 @@ def run_training(model: RemoveBackgroundPyroModel, if epoch % test_freq == 0: model.eval() total_epoch_loss_test = evaluate_epoch(svi, test_loader) - test_elbo.append(-total_epoch_loss_test) model.loss['test']['epoch'].append(epoch) model.loss['test']['elbo'].append(-total_epoch_loss_test) logger.info("[epoch %03d] average test loss: %.4f" % (epoch, total_epoch_loss_test)) # Check whether test ELBO has spiked beyond specified conditions. - if (epoch_elbo_fail_fraction is not None) and (len(test_elbo) > 2): - current_diff = max(0., test_elbo[-2] - test_elbo[-1]) - overall_diff = np.abs(test_elbo[-2] - test_elbo[0]) + if (epoch_elbo_fail_fraction is not None) and (len(model.loss['test']['elbo']) > 2): + current_diff = max(0., model.loss['test']['elbo'][-2] - model.loss['test']['elbo'][-1]) + overall_diff = np.abs(model.loss['test']['elbo'][-2] - model.loss['test']['elbo'][0]) fractional_spike = current_diff / overall_diff if fractional_spike > epoch_elbo_fail_fraction: raise ElboException( @@ -245,15 +243,15 @@ def run_training(model: RemoveBackgroundPyroModel, # Check on the final test ELBO to see if it meets criteria. if final_elbo_fail_fraction is not None: - best_test_elbo = max(test_elbo) - if test_elbo[-1] < best_test_elbo: - final_best_diff = best_test_elbo - test_elbo[-1] - initial_best_diff = best_test_elbo - test_elbo[0] + best_test_elbo = max(model.loss['test']['elbo']) + if model.loss['test']['elbo'][-1] < best_test_elbo: + final_best_diff = best_test_elbo - model.loss['test']['elbo'][-1] + initial_best_diff = best_test_elbo - model.loss['test']['elbo'][0] if (final_best_diff / initial_best_diff) > final_elbo_fail_fraction: raise ElboException( - f'Training failed because final test loss {test_elbo[-1]:.2f} ' + f"Training failed because final test loss {model.loss['test']['elbo'][-1]:.2f} " f'is not sufficiently close to best test loss {best_test_elbo:.2f}, ' - f'compared to the initial test loss {test_elbo[0]:.2f}. ' + f"compared to the initial test loss {model.loss['test']['elbo'][0]:.2f}. " f'Fractional difference is {final_best_diff / initial_best_diff:.2f}, ' f'which is > specified final_elbo_fail_fraction {final_elbo_fail_fraction:.2f}' ) @@ -284,14 +282,14 @@ def run_training(model: RemoveBackgroundPyroModel, logger.info(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) # Check final ELBO meets conditions. - if (final_elbo_fail_fraction is not None) and (len(test_elbo) > 1): - best_test_elbo = max(test_elbo) - if -test_elbo[-1] >= -best_test_elbo * (1 + final_elbo_fail_fraction): - raise ElboException(f'Training failed because final test loss ({-test_elbo[-1]:.4f}) ' + if (final_elbo_fail_fraction is not None) and (len(model.loss['test']['elbo']) > 1): + best_test_elbo = max(model.loss['test']['elbo']) + if -model.loss['test']['elbo'][-1] >= -best_test_elbo * (1 + final_elbo_fail_fraction): + raise ElboException(f"Training failed because final test loss ({-model.loss['test']['elbo'][-1]:.4f}) " f'exceeds best test loss ({-best_test_elbo:.4f}) by >= ' f'{100 * final_elbo_fail_fraction:.1f}%') # Free up all the GPU memory we can once training is complete. torch.cuda.empty_cache() - return train_elbo, test_elbo + return train_elbo, model.loss['test']['elbo'] From a71e18362be9dbe45d553c843e786edce2edeb6c Mon Sep 17 00:00:00 2001 From: alecw Date: Tue, 23 Apr 2024 09:53:21 -0400 Subject: [PATCH 14/19] Retry: prevent ZeroDivisionError if initial test ELBO is the best test ELBO (#312) --- cellbender/remove_background/train.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cellbender/remove_background/train.py b/cellbender/remove_background/train.py index bf7bed6..961e06e 100644 --- a/cellbender/remove_background/train.py +++ b/cellbender/remove_background/train.py @@ -247,7 +247,12 @@ def run_training(model: RemoveBackgroundPyroModel, if model.loss['test']['elbo'][-1] < best_test_elbo: final_best_diff = best_test_elbo - model.loss['test']['elbo'][-1] initial_best_diff = best_test_elbo - model.loss['test']['elbo'][0] - if (final_best_diff / initial_best_diff) > final_elbo_fail_fraction: + if initial_best_diff == 0: + raise ElboException( + f"Training failed because there was no improvement from the initial test loss {model.loss['test']['elbo'][0]:.2f}. " + f"Final test loss was {model.loss['test']['elbo'][-1]}" + ) + elif (final_best_diff / initial_best_diff) > final_elbo_fail_fraction: raise ElboException( f"Training failed because final test loss {model.loss['test']['elbo'][-1]:.2f} " f'is not sufficiently close to best test loss {best_test_elbo:.2f}, ' From 2c3dbe88c660f5e05d979d38c8e99259695b5286 Mon Sep 17 00:00:00 2001 From: aawdeh Date: Mon, 24 Jun 2024 10:50:31 -0400 Subject: [PATCH 15/19] Azurize CellBender to run on ToA (#367) CPU-only version of WDL --- wdl/cellbender_remove_background_azure.wdl | 250 +++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 wdl/cellbender_remove_background_azure.wdl diff --git a/wdl/cellbender_remove_background_azure.wdl b/wdl/cellbender_remove_background_azure.wdl new file mode 100644 index 0000000..503a465 --- /dev/null +++ b/wdl/cellbender_remove_background_azure.wdl @@ -0,0 +1,250 @@ +version 1.0 + +## Copyright Broad Institute, 2022 +## +## LICENSING : +## This script is released under the WDL source code license (BSD-3) +## (see LICENSE in https://github.com/openwdl/wdl). + +task run_cellbender_remove_background_gpu { + + input { + + # File-related inputs + String sample_name + File input_file_unfiltered # all barcodes, raw data + File? barcodes_file # for MTX and NPZ formats, the bacode information is in a separate file + File? genes_file # for MTX and NPZ formats, the gene information is in a separate file + File? checkpoint_file # start from a saved checkpoint + File? truth_file # only for developers using simulated data + + # Outputs + String? output_bucket_base_directory # Google bucket path + + # Docker image with CellBender + String? docker_image = "us.gcr.io/broad-dsde-methods/cellbender:0.3.0" + + # Used by developers for testing non-dockerized versions of CellBender + String? dev_git_hash__ # leave blank to run CellBender normally + + # Method configuration inputs + Int? expected_cells + Int? total_droplets_included + Float? force_cell_umi_prior + Float? force_empty_umi_prior + String? model + Int? low_count_threshold + String? fpr # in quotes: floats separated by whitespace: the output false positive rate(s) + Int? epochs + Int? z_dim + String? z_layers # in quotes: integers separated by whitespace + Float? empty_drop_training_fraction + Float? learning_rate + String? exclude_feature_types # in quotes: strings separated by whitespace + String? ignore_features # in quotes: integers separated by whitespace + Float? projected_ambient_count_threshold + Float? checkpoint_mins + Float? final_elbo_fail_fraction + Float? epoch_elbo_fail_fraction + Int? num_training_tries + Float? learning_rate_retry_mult + Int? posterior_batch_size + Boolean? estimator_multiple_cpu + Boolean? constant_learning_rate + Boolean? debug + + # Hardware-related inputs + String? hardware_zones = "us-east1-d us-east1-c us-central1-a us-central1-c us-west1-b" + Int? hardware_disk_size_GB = 50 + Int? hardware_boot_disk_size_GB = 20 + Int? hardware_preemptible_tries = 0 + Int? hardware_max_retries = 0 + Int? hardware_cpu_count = 4 + Int? hardware_memory_GB = 32 + String? hardware_gpu_type = "nvidia-tesla-t4" + String? nvidia_driver_version = "470.82.01" # need >=465.19.01 for CUDA 11.3 + + } + + # For development only: install a non dockerized version of CellBender + Boolean install_from_git = (if defined(dev_git_hash__) then true else false) + + # Compute the output bucket directory for this sample: output_bucket_base_directory/sample_name/ + String output_bucket_directory = (if defined(output_bucket_base_directory) + then sub(select_first([output_bucket_base_directory]), "/+$", "") + "/${sample_name}/" + else "") + + command { + + set -e # fail the workflow if there is an error + set -x + + # install a specific commit of cellbender from github if called for (-- developers only) + if [[ ~{install_from_git} == true ]]; then + echo "Uninstalling pre-installed cellbender" + yes | pip uninstall cellbender + echo "Installing cellbender from github" + # this more succinct version is broken in some older versions of cellbender + echo "pip install --no-cache-dir -U git+https://github.com/broadinstitute/CellBender.git@~{dev_git_hash__}" + # yes | pip install --no-cache-dir -U git+https://github.com/broadinstitute/CellBender.git@~{dev_git_hash__} + # this should always work + git clone -q https://github.com/broadinstitute/CellBender.git /cromwell_root/CellBender + cd /cromwell_root/CellBender + git checkout -q ~{dev_git_hash__} + yes | pip install --no-cache-dir -U -e /cromwell_root/CellBender + pip list + cd /cromwell_root + fi + + # put the barcodes_file in the right place, if it is provided + if [[ ! -z "~{barcodes_file}" ]]; then + dir=$(dirname ~{input_file_unfiltered}) + if [[ "~{input_file_unfiltered}" == *.npz ]]; then + name="row_index.npy" + elif [[ "~{barcodes_file}" == *.gz ]]; then + name="barcodes.tsv.gz" + else + name="barcodes.tsv" + fi + echo "Moving barcodes file to "$dir"/"$name + echo "mv ~{barcodes_file} "$dir"/"$name + [ -f $dir/$name ] || mv ~{barcodes_file} $dir/$name + fi + + # put the genes_file in the right place, if it is provided + if [[ ! -z "~{genes_file}" ]]; then + dir=$(dirname ~{input_file_unfiltered}) + if [[ "~{input_file_unfiltered}" == *.npz ]]; then + name="col_index.npy" + elif [[ "~{genes_file}" == *.gz ]]; then + name="features.tsv.gz" + else + name="genes.tsv" + fi + echo "Moving genes file to "$dir"/"$name + echo "mv ~{genes_file} "$dir"/"$name + [ -f $dir/$name ] || mv ~{genes_file} $dir/$name + fi + + # use the directory as the input in the case of an MTX file + if [[ "~{input_file_unfiltered}" == *.mtx* ]]; then + input=$(dirname ~{input_file_unfiltered}) + else + input=~{input_file_unfiltered} + fi + + cellbender remove-background \ + --input $input \ + --output "~{sample_name}_out.h5" \ + ~{"--checkpoint " + checkpoint_file} \ + ~{"--expected-cells " + expected_cells} \ + ~{"--total-droplets-included " + total_droplets_included} \ + ~{"--fpr " + fpr} \ + ~{"--model " + model} \ + ~{"--low-count-threshold " + low_count_threshold} \ + ~{"--epochs " + epochs} \ + ~{"--force-cell-umi-prior " + force_cell_umi_prior} \ + ~{"--force-empty-umi-prior " + force_empty_umi_prior} \ + ~{"--z-dim " + z_dim} \ + ~{"--z-layers " + z_layers} \ + ~{"--empty-drop-training-fraction " + empty_drop_training_fraction} \ + ~{"--exclude-feature-types " + exclude_feature_types} \ + ~{"--ignore-features " + ignore_features} \ + ~{"--projected-ambient-count-threshold " + projected_ambient_count_threshold} \ + ~{"--learning-rate " + learning_rate} \ + ~{"--checkpoint-mins " + checkpoint_mins} \ + ~{"--final-elbo-fail-fraction " + final_elbo_fail_fraction} \ + ~{"--epoch-elbo-fail-fraction " + epoch_elbo_fail_fraction} \ + ~{"--num-training-tries " + num_training_tries} \ + ~{"--learning-rate-retry-mult " + learning_rate_retry_mult} \ + ~{"--posterior-batch-size " + posterior_batch_size} \ + ~{true="--estimator-multiple-cpu " false=" " estimator_multiple_cpu} \ + ~{true="--constant-learning-rate " false=" " constant_learning_rate} \ + ~{true="--debug " false=" " debug} \ + ~{"--truth " + truth_file} + + # copy outputs to google bucket if output_bucket_base_directory is supplied + echo ~{output_bucket_directory} + # commenting out b/c cant use gsutil in azure + if [[ ! -z "~{output_bucket_directory}" ]]; then + echo "Copying output data to ~{output_bucket_directory} using gsutil cp" + gsutil -m cp ~{sample_name}_out* ~{output_bucket_directory} + fi + + } + + output { + File log = "${sample_name}_out.log" + File pdf = "${sample_name}_out.pdf" + File cell_csv = "${sample_name}_out_cell_barcodes.csv" + Array[File] metrics_array = glob("${sample_name}_out*_metrics.csv") # a number of outputs depending on "fpr" + Array[File] report_array = glob("${sample_name}_out*_report.html") # a number of outputs depending on "fpr" + Array[File] h5_array = glob("${sample_name}_out*.h5") # a number of outputs depending on "fpr" + String output_dir = "${output_bucket_directory}" + File ckpt_file = "ckpt.tar.gz" + } + + runtime { + docker: "${docker_image}" + bootDiskSizeGb: hardware_boot_disk_size_GB + disks: "local-disk ${hardware_disk_size_GB} HDD" + memory: "${hardware_memory_GB}G" + cpu: hardware_cpu_count + zones: "${hardware_zones}" + preemptible: hardware_preemptible_tries + checkpointFile: "ckpt.tar.gz" + maxRetries: hardware_max_retries # can be used in case of a PAPI error code 2 failure to install GPU drivers + } + meta { + author: "Stephen Fleming" + email: "sfleming@broadinstitute.org" + description: "WDL that runs CellBender remove-background on a GPU on Google Cloud hardware. See the [CellBender GitHub repo](https://github.com/broadinstitute/CellBender) and [read the documentation](https://cellbender.readthedocs.io/en/v0.3.0/reference/index.html#command-line-options) for more information." + } + + parameter_meta { + sample_name : + {help: "Name which will be prepended to output files and which will be used to construct the output google bucket file path, if output_bucket_base_directory is supplied, as output_bucket_base_directory/sample_name/"} + input_file_unfiltered : + {help: "Input file. This must be raw data that includes all barcodes. See http://cellbender.readthedocs.io for more information on file formats, but importantly, this must be one file, and cannot be a pointer to a directory that contains MTX and TSV files."} + barcodes_file : + {help: "Supply this only if your input_file_unfiltered is a sparse NPZ matrix from Optimus lacking metadata."} + genes_file : + {help: "Supply this only if your input_file_unfiltered is a sparse NPZ matrix from Optimus lacking metadata."} + output_bucket_base_directory : + {help: "Optional google bucket gsURL. If provided, the workflow will create a subfolder called sample_name in this directory and copy outputs there. (Note that the output data would then exist in two places.) Helpful for organization."} + docker_image : + {help: "CellBender docker image. Not all CellBender docker image tags will be compatible with this WDL.", + suggestions: ["us.gcr.io/broad-dsde-methods/cellbender:0.3.0"]} + checkpoint_file : + {help: "Optional gsURL for a checkpoint file created using a previous run ('ckpt.tar.gz') on the same dataset (using the same CellBender docker image). This can be used to create a new output with a different --fpr without re-doing the training run."} + truth_file : + {help: "Optional file only used by CellBender developers or those trying to benchmark CellBender remove-background on simulated data. Normally, this input would not be supplied."} + hardware_preemptible_tries : + {help: "If nonzero, CellBender will be run on a preemptible instance, at a lower cost. If preempted, your run will not start from scratch, but will start from a checkpoint that is saved by CellBender and recovered by Cromwell. For example, if hardware_preemptible_tries is 2, your run will attempt twice using preemptible instances, and if the job is preempted both times before completing, it will finish on a non-preemptible machine. The cost savings is significant. The potential drawback is that preemption wastes time."} + hardware_max_retries : + {help: "If nonzero when CellBender exits without success it will be retried. If one also sets the memory_retry_multiplier workflow option, and the exit happens to be detected as an out of memory error, then the retry will also increase the memory allocated to the next run. The potential benefit is that one can start CellBender with less memory, and memory will be increased only when needed. The potential drawback is that the job will be retried even if the error is not a memory error."} + checkpoint_mins : + {help: "Time in minutes between creation of checkpoint files. Bear in mind that Cromwell copies checkpoints to a bucket every ten minutes."} + hardware_gpu_type : + {help: "Specify the type of GPU that should be used. Ensure that the selected hardware_zones have the GPU available.", + suggestions: ["nvidia-tesla-t4", "nvidia-tesla-k80"]} + } + +} + +workflow cellbender_remove_background { + + call run_cellbender_remove_background_gpu + + output { + File log = run_cellbender_remove_background_gpu.log + File summary_pdf = run_cellbender_remove_background_gpu.pdf + File cell_barcodes_csv = run_cellbender_remove_background_gpu.cell_csv + Array[File] metrics_csv_array = run_cellbender_remove_background_gpu.metrics_array + Array[File] html_report_array = run_cellbender_remove_background_gpu.report_array + Array[File] h5_array = run_cellbender_remove_background_gpu.h5_array + String output_directory = run_cellbender_remove_background_gpu.output_dir + File checkpoint_file = run_cellbender_remove_background_gpu.ckpt_file + } + +} From 569b7b1bf77e622706bed2d1a6d25eb9d76e402c Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Mon, 24 Jun 2024 12:57:29 -0400 Subject: [PATCH 16/19] Updates to benchmarking scripts --- .../remove_background/tests/benchmarking/run_benchmark.py | 2 ++ .../tests/benchmarking/run_benchmark_result_tabulation.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cellbender/remove_background/tests/benchmarking/run_benchmark.py b/cellbender/remove_background/tests/benchmarking/run_benchmark.py index efb8520..ab31ca3 100644 --- a/cellbender/remove_background/tests/benchmarking/run_benchmark.py +++ b/cellbender/remove_background/tests/benchmarking/run_benchmark.py @@ -141,7 +141,9 @@ def cromshell_submit(wdl: str, submit_cmd = ['cromshell', 'submit', tmp_wdl, inputs, + '--options-json', options, + '--dependencies-zip', dependencies_zip] # submit job diff --git a/cellbender/remove_background/tests/benchmarking/run_benchmark_result_tabulation.py b/cellbender/remove_background/tests/benchmarking/run_benchmark_result_tabulation.py index ffc5e5a..e5c997c 100644 --- a/cellbender/remove_background/tests/benchmarking/run_benchmark_result_tabulation.py +++ b/cellbender/remove_background/tests/benchmarking/run_benchmark_result_tabulation.py @@ -69,7 +69,7 @@ def get_cromshell_output_h5(workflow: str, grep: str = '_out.h5') -> Union[str, """Use cromshell list-outputs to get the relevant file gsURL""" output = grep_from_command(['cromshell', 'list-outputs', workflow], grep=grep) - out = output[:-1].decode().split('\n') + out = output.decode().lstrip('run_cellbender_benchmark.h5_array: ').rstrip('\n').split('\n') if len(out) > 1: return out else: @@ -95,18 +95,18 @@ def metadata_from_workflow_id(workflow: str) -> Tuple[str, str, Optional[str]]: # git hash output = grep_from_command(['cromshell', 'metadata', workflow], grep='"git_hash":') - git_hash = output[17:-3].decode() + git_hash = output.decode().split('"git_hash": ')[-1].lstrip('"').split('"')[0] # input file output = grep_from_command(['cromshell', 'metadata', workflow], grep='run_cellbender_benchmark.cb.input_file_unfiltered') - input_file = output[58:-3].decode() + input_file = 'gs://' + output.decode().split('gs://')[-1].split('"')[0] # truth file output = grep_from_command(['cromshell', 'metadata', workflow], grep='run_cellbender_benchmark.cb.truth_file') if 'null' not in output.decode(): - truth_file = output[47:-3].decode() + truth_file = 'gs://' + output.decode().split('gs://')[-1].split('"')[0] else: truth_file = None From 31130fd9fe5405c891d4c1efa25aeab9860a5d5e Mon Sep 17 00:00:00 2001 From: Stephen Fleming Date: Mon, 24 Jun 2024 12:58:19 -0400 Subject: [PATCH 17/19] Update documentation in anticipation of v0.3.2 --- .../_static/remove_background/v0.3.2_hgmm.png | Bin 0 -> 189077 bytes docs/source/changelog/index.rst | 82 ++++++++++++++---- docs/source/reference/index.rst | 3 +- 3 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 docs/source/_static/remove_background/v0.3.2_hgmm.png diff --git a/docs/source/_static/remove_background/v0.3.2_hgmm.png b/docs/source/_static/remove_background/v0.3.2_hgmm.png new file mode 100644 index 0000000000000000000000000000000000000000..a9ca57fb350324f0ccdb8c8ec1d84ac5f7c480fd GIT binary patch literal 189077 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≺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ν;3dEo!4VE*baqwnLdCmFC|z8bz!BiP<5*ek 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 z zo9Coow++UEwUUYWh^j5Y99y%A^70QfJ3aaz?2wm8SA}8<5(|||VZFnZ_Ffi((Uc}5 zxoK=J9|jj5e&ix@-A}HR*$|NvM^(l=0K3sLqlf#1S03({hmoMpdUhRxyZBZ-Mp!}Mej{0ur&yilP z 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Ú%O1H_)9Ccd#2M2PVPte}s#rED*l(rfCVW)j&ZZN| ztan{W_|p@B%V|}hm7=fmRZ7lRlKe+0yc(1O5E^I9{@w@wAaAT)Ggf(_&21W{lI&e*_${} zB_EJdRD58{KNH0aMfe3s(6&o0H>cgp5Mu7Ddpn8ak~9<-TpFxNb~iT=s! zit^@k>kHthnGRyD%W4oxO8V8~&!mtaw3 zye(~L9}`osJvw9Jlld2)fsRbPXa6gw586vNjX0_hpO15o%c22l*;uI{y?jL1_$86ujB& zx!7NDJ#ar>x=QqIpPXCc$^=jevx8Aq=CMJ&9pO;a{$%k)2o?qI+gsPU*zU)>o5Ce{ zWH4>I1c;c%{rvf2F=zE?3B+$BC5u06ubROj@wz`bOyy{O9Fqi)jjpb>b?R@W*O`?{bhf(f zKex^V?>09w)yo*@-r}Gj-kQ09T&=~qnrC>7eD2qO7mnN8?~@GXB_td@_|nx&mFSLf zaSC8bCGe)VsdgNEB94m7=_dkSqAh&PCGo@k-!-T9WV6#n5Tt4bGyz)N+?hU?+~{Z!gKoobcU;ya z(T6sR3F;cN$E1{bJ20=waQ`(kBgv=%I(N~06?uHtg4z}%K#`5Omxv*5P(|kC;4ock zx=606(T~@_Uj%k+fq8*x#NjXPx#IUf%B84OckFRaoH+iUiCycZp~DHbSt}giez3Bz zy!WJKjnE!mvyVp=*S18xv99r32ii1rj<;i(ZQ7M@?6`@-rr+r@Mh$L#T-1rtI97{f zVBLU?qKU?SF{;u86c-}$R8Hq&%C?2ZKl9-k=LZwWeuXGne-A>ibO5k>e`h)b;y=pf zYT_6TU`}^H_NndR>FLemc2pHdZ8n@yXME?15$tI^U%SuwsB^RbXSm(2CylsTD{8sb z^ZZCHSKV&*I}9}I(n*Xsi>>t7s1)--)XsaSu;yb7LV?9ii5w0>*rF3A|Dwrs+8niF zhTIKPckduQ2czlbffj)A2DX%*;rc#O_JkNiupA zFHl4~wZY+93|PelqY=*oE)FBs0iVEO@ zm*9hpsv3Oc&WFCS7AW}q!?RC3`!1BxHaK2>11{`0ph&>R#^wTKFN(YFMUI+{C`7?~ z5G6LP49yB2?4V?FT!!_wUPicl4tA=ONKH%;!R?N+CD}N4k4Cq~H=F6&_51q7e{f5g zNTzfw+W2&C`^;ktC!tMXd9qdY^z3X-k9ItQ#r9HI`QTnFMhYMkT}JN+eoCIYZDOYT zGGEB2FWe`6=VkG&aeV0SV+37*{D*0~% z`7{YQ@M)6rgWJE#+s)R&$jHc~T8@P#3e5&c zV;^zXPi`o;W|Xd8xO#sg&2ypY{GEW40Jz_5@)K-t6QvNmUlZ)oB}y?{*uoEeCYwYc z67a@`ynXB!R3ZO=eBj>({!^4GAEe}e0pb+Irmc&I!;ZDGLg=y}@MA1Efh@>+TOo?^ zrvBJaBUiu33)rL6+*xxNouLVD{^zi*VI9pEmHLA24^i*DI&kUY=}zxOC!9Scc(jx; zAZMO(Dase&;s;^wK|U{BJp&KeEZM(N()E%6kBh6;iET9kBPxz z2X<_CE@L_3gq%R{_glSu{p$}VA(TdpdXKMia&q@qD}KRjFwbatX?G#3z5vEgpDt7I z|I1Mk>zVuTtD$YilK>L%(S+3sb(h zLq=F00Ej`pO22{?izJv0j}Dh+Q;)aMFY