From b8828721b3008f3db97872b4c69a0956f18f2c67 Mon Sep 17 00:00:00 2001 From: scverse-bot <108668866+scverse-bot@users.noreply.github.com> Date: Wed, 24 Dec 2025 10:02:29 +0000 Subject: [PATCH] Automated template update to v0.7.0 --- .codecov.yaml | 17 + .cruft.json | 43 + .editorconfig | 15 + .github/ISSUE_TEMPLATE/bug_report.yml | 93 ++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.yml | 11 + .github/workflows/build.yaml | 26 + .github/workflows/release.yaml | 27 + .github/workflows/test.yaml | 103 ++ .gitignore | 33 +- .pre-commit-config.yaml | 38 + .readthedocs.yaml | 16 + .vscode/extensions.json | 18 + .vscode/launch.json | 33 + .vscode/settings.json | 18 + biome.jsonc | 17 + .../__init__.py => docs/_static/.gitkeep | 0 docs/_static/css/custom.css | 4 + docs/_templates/.gitkeep | 0 docs/_templates/autosummary/class.rst | 61 + docs/changelog.md | 3 + docs/conf.py | 136 ++ docs/contributing.md | 330 +++++ docs/extensions/typed_returns.py | 32 + examples/krumsiek11.ipynb | 220 --- examples/moignard15.ipynb | 189 --- examples/paul15.ipynb | 203 --- examples/toggleswitch.ipynb | 177 --- pyproject.toml | 154 ++ scanpy/__init__.py | 246 ---- scanpy/__main__.py | 114 -- scanpy/compat/matplotlib.py | 10 - scanpy/compat/urllib_request.py | 6 - scanpy/exs/__init__.py | 182 --- scanpy/exs/builtin.py | 293 ---- scanpy/exs/user.py | 84 -- scanpy/plotting.py | 1047 -------------- scanpy/settings.py | 372 ----- scanpy/tools/__init__.py | 30 - scanpy/tools/diffmap.py | 126 -- scanpy/tools/difftest.py | 236 --- scanpy/tools/dpt.py | 1272 ----------------- scanpy/tools/pca.py | 94 -- scanpy/tools/preprocess.py | 305 ---- scanpy/tools/sim.py | 1185 --------------- scanpy/tools/tsne.py | 266 ---- scanpy/utils.py | 1188 --------------- scripts/RData_to_other_formats.R | 117 -- scripts/diffmap.py | 17 - scripts/scanpy.py | 16 - setup.py | 30 - sim/krumsiek11.txt | 54 - sim/krumsiek11_params.txt | 8 - sim/toggleswitch.txt | 16 - sim/toggleswitch_params.txt | 8 - 55 files changed, 1215 insertions(+), 8129 deletions(-) create mode 100644 .codecov.yaml create mode 100644 .cruft.json create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 biome.jsonc rename scanpy/compat/__init__.py => docs/_static/.gitkeep (100%) create mode 100644 docs/_static/css/custom.css create mode 100644 docs/_templates/.gitkeep create mode 100644 docs/_templates/autosummary/class.rst create mode 100644 docs/changelog.md create mode 100644 docs/conf.py create mode 100644 docs/contributing.md create mode 100644 docs/extensions/typed_returns.py delete mode 100644 examples/krumsiek11.ipynb delete mode 100644 examples/moignard15.ipynb delete mode 100644 examples/paul15.ipynb delete mode 100644 examples/toggleswitch.ipynb create mode 100644 pyproject.toml delete mode 100644 scanpy/__init__.py delete mode 100755 scanpy/__main__.py delete mode 100644 scanpy/compat/matplotlib.py delete mode 100644 scanpy/compat/urllib_request.py delete mode 100644 scanpy/exs/__init__.py delete mode 100644 scanpy/exs/builtin.py delete mode 100644 scanpy/exs/user.py delete mode 100755 scanpy/plotting.py delete mode 100644 scanpy/settings.py delete mode 100644 scanpy/tools/__init__.py delete mode 100644 scanpy/tools/diffmap.py delete mode 100644 scanpy/tools/difftest.py delete mode 100644 scanpy/tools/dpt.py delete mode 100644 scanpy/tools/pca.py delete mode 100644 scanpy/tools/preprocess.py delete mode 100644 scanpy/tools/sim.py delete mode 100644 scanpy/tools/tsne.py delete mode 100644 scanpy/utils.py delete mode 100644 scripts/RData_to_other_formats.R delete mode 100755 scripts/diffmap.py delete mode 100755 scripts/scanpy.py delete mode 100644 setup.py delete mode 100644 sim/krumsiek11.txt delete mode 100644 sim/krumsiek11_params.txt delete mode 100644 sim/toggleswitch.txt delete mode 100644 sim/toggleswitch_params.txt diff --git a/.codecov.yaml b/.codecov.yaml new file mode 100644 index 0000000000..d0c0e29176 --- /dev/null +++ b/.codecov.yaml @@ -0,0 +1,17 @@ +# Based on pydata/xarray +codecov: + require_ci_to_pass: no + +coverage: + status: + project: + default: + # Require 1% coverage, i.e., always succeed + target: 1 + patch: false + changes: false + +comment: + layout: diff, flags, files + behavior: once + require_base: no diff --git a/.cruft.json b/.cruft.json new file mode 100644 index 0000000000..f5de3f802b --- /dev/null +++ b/.cruft.json @@ -0,0 +1,43 @@ +{ + "template": "https://github.com/scverse/cookiecutter-scverse", + "commit": "6ff5b92b5d44ea6d8a88e47538475718d467db95", + "checkout": "v0.7.0", + "context": { + "cookiecutter": { + "project_name": "scanpy", + "package_name": "scanpy", + "project_description": "Single-Cell Analysis in Python.", + "author_full_name": "Philipp Angerer", + "author_email": "philipp.angerer@helmholtz-munich.de", + "github_user": "scverse", + "github_repo": "scanpy", + "license": "BSD 3-Clause License", + "ide_integration": true, + "_copy_without_render": [ + ".github/workflows/build.yaml", + ".github/workflows/test.yaml", + "docs/_templates/autosummary/**.rst" + ], + "_exclude_on_template_update": [ + "CHANGELOG.md", + "LICENSE", + "README.md", + "docs/api.md", + "docs/index.md", + "docs/notebooks/example.ipynb", + "docs/references.bib", + "docs/references.md", + "src/**", + "tests/**" + ], + "_render_devdocs": false, + "_jinja2_env_vars": { + "lstrip_blocks": true, + "trim_blocks": true + }, + "_template": "https://github.com/scverse/cookiecutter-scverse", + "_commit": "6ff5b92b5d44ea6d8a88e47538475718d467db95" + } + }, + "directory": null +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..66678e378b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{*.{yml,yaml,toml},.cruft.json}] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..6104b9e6f4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,93 @@ +name: Bug report +description: Report something that is broken or incorrect +type: Bug +body: + - type: markdown + attributes: + value: | + **Note**: Please read [this guide](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports) + detailing how to provide the necessary information for us to reproduce your bug. In brief: + * Please provide exact steps how to reproduce the bug in a clean Python environment. + * In case it's not clear what's causing this bug, please provide the data or the data generation procedure. + * Replicate problems on public datasets or share data subsets when full sharing isn't possible. + + - type: textarea + id: report + attributes: + label: Report + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + id: versions + attributes: + label: Versions + description: | + Which version of packages. + + Please install `session-info2`, run the following command in a notebook, + click the “Copy as Markdown” button, then paste the results into the text box below. + + ```python + In[1]: import session_info2; session_info2.session_info(dependencies=True) + ``` + + Alternatively, run this in a console: + + ```python + >>> import session_info2; print(session_info2.session_info(dependencies=True)._repr_mimebundle_()["text/markdown"]) + ``` + render: python + placeholder: | + anndata 0.11.3 + ---- ---- + charset-normalizer 3.4.1 + coverage 7.7.0 + psutil 7.0.0 + dask 2024.7.1 + jaraco.context 5.3.0 + numcodecs 0.15.1 + jaraco.functools 4.0.1 + Jinja2 3.1.6 + sphinxcontrib-jsmath 1.0.1 + sphinxcontrib-htmlhelp 2.1.0 + toolz 1.0.0 + session-info2 0.1.2 + PyYAML 6.0.2 + llvmlite 0.44.0 + scipy 1.15.2 + pandas 2.2.3 + sphinxcontrib-devhelp 2.0.0 + h5py 3.13.0 + tblib 3.0.0 + setuptools-scm 8.2.0 + more-itertools 10.3.0 + msgpack 1.1.0 + sparse 0.15.5 + wrapt 1.17.2 + jaraco.collections 5.1.0 + numba 0.61.0 + pyarrow 19.0.1 + pytz 2025.1 + MarkupSafe 3.0.2 + crc32c 2.7.1 + sphinxcontrib-qthelp 2.0.0 + sphinxcontrib-serializinghtml 2.0.0 + zarr 2.18.4 + asciitree 0.3.3 + six 1.17.0 + sphinxcontrib-applehelp 2.0.0 + numpy 2.1.3 + cloudpickle 3.1.1 + sphinxcontrib-bibtex 2.6.3 + natsort 8.4.0 + jaraco.text 3.12.1 + setuptools 76.1.0 + Deprecated 1.2.18 + packaging 24.2 + python-dateutil 2.9.0.post0 + ---- ---- + Python 3.13.2 | packaged by conda-forge | (main, Feb 17 2025, 14:10:22) [GCC 13.3.0] + OS Linux-6.11.0-109019-tuxedo-x86_64-with-glibc2.39 + Updated 2025-03-18 15:47 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..5b62547f9a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Scverse Community Forum + url: https://discourse.scverse.org/ + about: If you have questions about “How to do X”, please ask them here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..04c83f7be5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,11 @@ +name: Feature request +description: Propose a new feature for scanpy +type: Enhancement +body: + - type: textarea + id: description + attributes: + label: Description of feature + description: Please describe your suggestion for a new feature. It might help to describe a problem or use case, plus any alternatives that you have considered. + validations: + required: true diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000000..c6ecc2f254 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,26 @@ +name: Check Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + filter: blob:none + fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Build package + run: uv build + - name: Check package + run: uvx twine check --strict dist/*.whl diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000000..8c6988d50c --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,27 @@ +name: Release + +on: + release: + types: [published] + +# Use "trusted publishing", see https://docs.pypi.org/trusted-publishers/ +jobs: + release: + name: Upload release to PyPI + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/scanpy + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + steps: + - uses: actions/checkout@v5 + with: + filter: blob:none + fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Build package + run: uv build + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000000..6bf473b64e --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,103 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "0 5 1,15 * *" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Get the test environment from hatch as defined in pyproject.toml. + # This ensures that the pyproject.toml is the single point of truth for test definitions and the same tests are + # run locally and on continuous integration. + # Check [[tool.hatch.envs.hatch-test.matrix]] in pyproject.toml and https://hatch.pypa.io/latest/environment/ for + # more details. + get-environments: + runs-on: ubuntu-latest + outputs: + envs: ${{ steps.get-envs.outputs.envs }} + steps: + - uses: actions/checkout@v5 + with: + filter: blob:none + fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Get test environments + id: get-envs + run: | + ENVS_JSON=$(uvx hatch env show --json | jq -c 'to_entries + | map( + select(.key | startswith("hatch-test")) + | { + name: .key, + label: (if (.key | contains("pre")) then .key + " (PRE-RELEASE DEPENDENCIES)" else .key end), + python: .value.python + } + )') + echo "envs=${ENVS_JSON}" | tee $GITHUB_OUTPUT + + # Run tests through hatch. Spawns a separate runner for each environment defined in the hatch matrix obtained above. + test: + needs: get-environments + permissions: + id-token: write # for codecov OIDC + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + env: ${{ fromJSON(needs.get-environments.outputs.envs) }} + + name: ${{ matrix.env.label }} + runs-on: ${{ matrix.os }} + continue-on-error: ${{ contains(matrix.env.name, 'pre') }} # make "all-green" pass even if pre-release job fails + + steps: + - uses: actions/checkout@v5 + with: + filter: blob:none + fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: ${{ matrix.env.python }} + - name: create hatch environment + run: uvx hatch env create ${{ matrix.env.name }} + - name: run tests using hatch + env: + MPLBACKEND: agg + PLATFORM: ${{ matrix.os }} + DISPLAY: :42 + run: uvx hatch run ${{ matrix.env.name }}:run-cov -v --color=yes -n auto + - name: generate coverage report + run: | + # See https://coverage.readthedocs.io/en/latest/config.html#run-patch + test -f .coverage || uvx hatch run ${{ matrix.env.name }}:cov-combine + uvx hatch run ${{ matrix.env.name }}:cov-report # report visibly + uvx hatch run ${{ matrix.env.name }}:coverage xml # create report for upload + - name: Upload coverage + uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true + use_oidc: true + + # Check that all tests defined above pass. This makes it easy to set a single "required" test in branch + # protection instead of having to update it frequently. See https://github.com/re-actors/alls-green#why. + check: + name: Tests pass in all hatch environments + if: always() + needs: + - get-environments + - test + runs-on: ubuntu-latest + steps: + - uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.gitignore b/.gitignore index fbee57b54b..bd24e4e004 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,21 @@ -# Scanpy -data/ -figs/ -*/figs/ -write/ - -# always-ignore extensions +# Temp files +.DS_Store *~ +buck-out/ -# Python / Byte-compiled / optimized / DLL +# Compiled files +.venv/ __pycache__/ -*.py[cod] +.*cache/ -# OS or Editor files and folders -.DS_Store -Thumbs.db -.ipynb_checkpoints/ -.directory -/.idea/ - -# always-ignore directories +# Distribution / packaging /dist/ -/build/ +# Tests and coverage +/data/ +/node_modules/ +/.coverage* + +# docs +/docs/generated/ +/docs/_build/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..27d8a954d4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +fail_fast: false +default_language_version: + python: python3 +default_stages: + - pre-commit + - pre-push +minimum_pre_commit_version: 2.16.0 +repos: + - repo: https://github.com/biomejs/pre-commit + rev: v2.3.10 + hooks: + - id: biome-format + exclude: ^\.cruft\.json$ # inconsistent indentation with cruft - file never to be modified manually. + - repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.11.1 + hooks: + - id: pyproject-fmt + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.10 + hooks: + - id: ruff-check + types_or: [python, pyi, jupyter] + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + types_or: [python, pyi, jupyter] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: detect-private-key + - id: check-ast + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=lf] + - id: trailing-whitespace + - id: check-case-conflict + # Check that there are no merge conflicts (could be generated by template sync) + - id: check-merge-conflict + args: [--assume-in-merge] diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..6c28477161 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +# https://docs.readthedocs.io/en/stable/config-file/v2.html +version: 2 +build: + os: ubuntu-24.04 + tools: + python: "3.13" + nodejs: latest + jobs: + create_environment: + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + build: + html: + - uvx hatch run docs:build + - mv docs/_build $READTHEDOCS_OUTPUT diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..caaeb4f732 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,18 @@ +{ + "recommendations": [ + // GitHub integration + "github.vscode-github-actions", + "github.vscode-pull-request-github", + // Language support + "ms-python.python", + "ms-python.vscode-pylance", + "ms-toolsai.jupyter", + "tamasfe.even-better-toml", + // Dependency management + "ninoseki.vscode-mogami", + // Linting and formatting + "editorconfig.editorconfig", + "charliermarsh.ruff", + "biomejs.biome", + ], +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..36d1874618 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Build Documentation", + "type": "debugpy", + "request": "launch", + "module": "sphinx", + "args": ["-M", "html", ".", "_build"], + "cwd": "${workspaceFolder}/docs", + "console": "internalConsole", + "justMyCode": false, + }, + { + "name": "Python: Debug Test", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "purpose": ["debug-test"], + "console": "internalConsole", + "justMyCode": false, + "env": { + "PYTEST_ADDOPTS": "--color=yes", + }, + "presentation": { + "hidden": true, + }, + }, + ], +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..e034b91f7a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "[python][json][jsonc]": { + "editor.formatOnSave": true, + }, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "always", + "source.organizeImports": "always", + }, + }, + "[json][jsonc]": { + "editor.defaultFormatter": "biomejs.biome", + }, + "python.analysis.typeCheckingMode": "basic", + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": ["-vv", "--color=yes"], +} diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 0000000000..9f8f2208c4 --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,17 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", + "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, + "formatter": { "useEditorconfig": true }, + "overrides": [ + { + "includes": ["./.vscode/*.json", "**/*.jsonc"], + "json": { + "formatter": { "trailingCommas": "all" }, + "parser": { + "allowComments": true, + "allowTrailingCommas": true, + }, + }, + }, + ], +} diff --git a/scanpy/compat/__init__.py b/docs/_static/.gitkeep similarity index 100% rename from scanpy/compat/__init__.py rename to docs/_static/.gitkeep diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 0000000000..b8c8d47fa0 --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,4 @@ +/* Reduce the font size in data frames - See https://github.com/scverse/cookiecutter-scverse/issues/193 */ +div.cell_output table.dataframe { + font-size: 0.8em; +} diff --git a/docs/_templates/.gitkeep b/docs/_templates/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst new file mode 100644 index 0000000000..7b4a0cf87d --- /dev/null +++ b/docs/_templates/autosummary/class.rst @@ -0,0 +1,61 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. add toctree option to make autodoc generate the pages + +.. autoclass:: {{ objname }} + +{% block attributes %} +{% if attributes %} +Attributes table +~~~~~~~~~~~~~~~~ + +.. autosummary:: +{% for item in attributes %} + ~{{ name }}.{{ item }} +{%- endfor %} +{% endif %} +{% endblock %} + +{% block methods %} +{% if methods %} +Methods table +~~~~~~~~~~~~~ + +.. autosummary:: +{% for item in methods %} + {%- if item != '__init__' %} + ~{{ name }}.{{ item }} + {%- endif -%} +{%- endfor %} +{% endif %} +{% endblock %} + +{% block attributes_documentation %} +{% if attributes %} +Attributes +~~~~~~~~~~ + +{% for item in attributes %} + +.. autoattribute:: {{ [objname, item] | join(".") }} +{%- endfor %} + +{% endif %} +{% endblock %} + +{% block methods_documentation %} +{% if methods %} +Methods +~~~~~~~ + +{% for item in methods %} +{%- if item != '__init__' %} + +.. automethod:: {{ [objname, item] | join(".") }} +{%- endif -%} +{%- endfor %} + +{% endif %} +{% endblock %} diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000000..d9e79ba648 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,3 @@ +```{include} ../CHANGELOG.md + +``` diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000000..42a5ddc558 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,136 @@ +# Configuration file for the Sphinx documentation builder. + +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- +import shutil +import sys +from datetime import datetime +from importlib.metadata import metadata +from pathlib import Path + +from sphinxcontrib import katex + +HERE = Path(__file__).parent +sys.path.insert(0, str(HERE / "extensions")) + + +# -- Project information ----------------------------------------------------- + +# NOTE: If you installed your project in editable mode, this might be stale. +# If this is the case, reinstall it to refresh the metadata +info = metadata("scanpy") +project = info["Name"] +author = info["Author"] +copyright = f"{datetime.now():%Y}, {author}." +version = info["Version"] +urls = dict(pu.split(", ") for pu in info.get_all("Project-URL")) +repository_url = urls["Source"] + +# The full version, including alpha/beta/rc tags +release = info["Version"] + +bibtex_bibfiles = ["references.bib"] +templates_path = ["_templates"] +nitpicky = True # Warn about broken links +needs_sphinx = "4.0" + +html_context = { + "display_github": True, # Integrate GitHub + "github_user": "scverse", + "github_repo": project, + "github_version": "main", + "conf_py_path": "/docs/", +} + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. +# They can be extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + "myst_nb", + "sphinx_copybutton", + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", + "sphinxcontrib.bibtex", + "sphinxcontrib.katex", + "sphinx_autodoc_typehints", + "sphinx_tabs.tabs", + "IPython.sphinxext.ipython_console_highlighting", + "sphinxext.opengraph", + *[p.stem for p in (HERE / "extensions").glob("*.py")], +] + +autosummary_generate = True +autodoc_member_order = "groupwise" +default_role = "literal" +napoleon_google_docstring = False +napoleon_numpy_docstring = True +napoleon_include_init_with_doc = False +napoleon_use_rtype = True # having a separate entry generally helps readability +napoleon_use_param = True +myst_heading_anchors = 6 # create anchors for h1-h6 +myst_enable_extensions = [ + "amsmath", + "colon_fence", + "deflist", + "dollarmath", + "html_image", + "html_admonition", +] +myst_url_schemes = ("http", "https", "mailto") +nb_output_stderr = "remove" +nb_execution_mode = "off" +nb_merge_streams = True +typehints_defaults = "braces" + +source_suffix = { + ".rst": "restructuredtext", + ".ipynb": "myst-nb", + ".myst": "myst-nb", +} + +intersphinx_mapping = { + # TODO: replace `3.13` with `3` once ReadTheDocs supports building with Python 3.14 + "python": ("https://docs.python.org/3.13", None), + "anndata": ("https://anndata.readthedocs.io/en/stable/", None), + "scanpy": ("https://scanpy.readthedocs.io/en/stable/", None), + "numpy": ("https://numpy.org/doc/stable/", None), +} + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_book_theme" +html_static_path = ["_static"] +html_css_files = ["css/custom.css"] + +html_title = project + +html_theme_options = { + "repository_url": repository_url, + "use_repository_button": True, + "path_to_docs": "docs/", + "navigation_with_keys": False, +} + +pygments_style = "default" +katex_prerender = shutil.which(katex.NODEJS_BINARY) is not None + +nitpick_ignore = [ + # If building the documentation fails because of a missing link that is outside your control, + # you can add an exception to this list. + # ("py:class", "igraph.Graph"), +] diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000000..b172fd1d09 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,330 @@ +# Contributing guide + +This document aims at summarizing the most important information for getting you started on contributing to this project. +We assume that you are already familiar with git and with making pull requests on GitHub. + +For more extensive tutorials, that also cover the absolute basics, +please refer to other resources such as the [pyopensci tutorials][], +the [scientific Python tutorials][], or the [scanpy developer guide][]. + +[pyopensci tutorials]: https://www.pyopensci.org/learn.html +[scientific Python tutorials]: https://learn.scientific-python.org/development/tutorials/ +[scanpy developer guide]: https://scanpy.readthedocs.io/en/latest/dev/index.html + +:::{tip} The *hatch* project manager + +We highly recommend to familiarize yourself with [`hatch`][hatch]. +Hatch is a Python project manager that + +- manages virtual environments, separately for development, testing and building the documentation. + Separating the environments is useful to avoid dependency conflicts. +- allows to run tests locally in different environments (e.g. different python versions) +- allows to run tasks defined in `pyproject.toml`, e.g. to build documentation. + +While the project is setup with `hatch` in mind, +it is still possible to use different tools to manage dependencies, such as `uv` or `pip`. + +::: + +[hatch]: https://hatch.pypa.io/latest/ + +## Installing dev dependencies + +In addition to the packages needed to _use_ this package, +you need additional python packages to [run tests](#writing-tests) and [build the documentation](#docs-building). + +:::::{tabs} +::::{group-tab} Hatch + +On the command line, you typically interact with hatch through its command line interface (CLI). +Running one of the following commands will automatically resolve the environments for testing and +building the documentation in the background: + +```bash +hatch test # defined in the table [tool.hatch.envs.hatch-test] in pyproject.toml +hatch run docs:build # defined in the table [tool.hatch.envs.docs] +``` + +When using an IDE such as VS Code, +you’ll have to point the editor at the paths to the virtual environments manually. +The environment you typically want to use as your main development environment is the `hatch-test` +environment with the latest Python version. + +To get a list of all environments for your projects, run + +```bash +hatch env show -i +``` + +This will list “Standalone” environments and a table of “Matrix” environments like the following: + +``` ++------------+---------+--------------------------+----------+---------------------------------+-------------+ +| Name | Type | Envs | Features | Dependencies | Scripts | ++------------+---------+--------------------------+----------+---------------------------------+-------------+ +| hatch-test | virtual | hatch-test.py3.11-stable | dev | coverage-enable-subprocess==1.0 | cov-combine | +| | | hatch-test.py3.14-stable | test | coverage[toml]~=7.4 | cov-report | +| | | hatch-test.py3.14-pre | | pytest-mock~=3.12 | run | +| | | | | pytest-randomly~=3.15 | run-cov | +| | | | | pytest-rerunfailures~=14.0 | | +| | | | | pytest-xdist[psutil]~=3.5 | | +| | | | | pytest~=8.1 | | ++------------+---------+--------------------------+----------+---------------------------------+-------------+ +``` + +From the `Envs` column, select the environment name you want to use for development. +In this example, it would be `hatch-test.py3.14-stable`. + +Next, create the environment with + +```bash +hatch env create hatch-test.py3.14-stable +``` + +Then, obtain the path to the environment using + +```bash +hatch env find hatch-test.py3.14-stable +``` + +In case you are using VScode, now open the command palette (Ctrl+Shift+P) and search for `Python: Select Interpreter`. +Choose `Enter Interpreter Path` and paste the path to the virtual environment from above. + +In this future, this may become easier through a hatch vscode extension. + +:::: + +::::{group-tab} uv + +A popular choice for managing virtual environments is [uv][]. +The main disadvantage compared to hatch is that it supports only a single environment per project at a time, +which requires you to mix the dependencies for running tests and building docs. +This can have undesired side-effects, +such as requiring to install a lower version of a library your project depends on, +only because an outdated sphinx plugin pins an older version. + +To initalize a virtual environment in the `.venv` directory of your project, simply run + +```bash +uv sync --all-extras +``` + +The `.venv` directory is typically automatically discovered by IDEs such as VS Code. + +:::: + +::::{group-tab} Pip + +Pip is nowadays mostly superseded by environment manager such as [hatch][]. +However, for the sake of completeness, and since it’s ubiquitously available, +we describe how you can manage environments manually using `pip`: + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -e ".[dev,test,doc]" +``` + +The `.venv` directory is typically automatically discovered by IDEs such as VS Code. + +:::: +::::: + +[hatch environments]: https://hatch.pypa.io/latest/tutorials/environment/basic-usage/ +[uv]: https://docs.astral.sh/uv/ + +## Code-style + +This package uses [pre-commit][] to enforce consistent code-styles. +On every commit, pre-commit checks will either automatically fix issues with the code, or raise an error message. + +To enable pre-commit locally, simply run + +```bash +pre-commit install +``` + +in the root of the repository. +Pre-commit will automatically download all dependencies when it is run for the first time. + +Alternatively, you can rely on the [pre-commit.ci][] service enabled on GitHub. +If you didn’t run `pre-commit` before pushing changes to GitHub it will automatically commit fixes to your pull request, or show an error message. + +If pre-commit.ci added a commit on a branch you still have been working on locally, simply use + +```bash +git pull --rebase +``` + +to integrate the changes into yours. +While the [pre-commit.ci][] is useful, we strongly encourage installing and running pre-commit locally first to understand its usage. + +Finally, most editors have an _autoformat on save_ feature. +Consider enabling this option for [ruff][ruff-editors] and [biome][biome-editors]. + +[pre-commit]: https://pre-commit.com/ +[pre-commit.ci]: https://pre-commit.ci/ +[ruff-editors]: https://docs.astral.sh/ruff/integrations/ +[biome-editors]: https://biomejs.dev/guides/integrate-in-editor/ + +(writing-tests)= + +## Writing tests + +This package uses [pytest][] for automated testing. +Please write {doc}`scanpy:dev/testing` for every function added to the package. + +Most IDEs integrate with pytest and provide a GUI to run tests. +Just point yours to one of the environments returned by + +```bash +hatch env create hatch-test # create test environments for all supported versions +hatch env find hatch-test # list all possible test environment paths +``` + +Alternatively, you can run all tests from the command line by executing + +:::::{tabs} +::::{group-tab} Hatch + +```bash +hatch test # test with the highest supported Python version +# or +hatch test --all # test with all supported Python versions +``` + +:::: + +::::{group-tab} uv + +```bash +uv run pytest +``` + +:::: + +::::{group-tab} Pip + +```bash +source .venv/bin/activate +pytest +``` + +:::: +::::: + +in the root of the repository. + +[pytest]: https://docs.pytest.org/ + +### Continuous integration + +Continuous integration via GitHub actions will automatically run the tests on all pull requests and test +against the minimum and maximum supported Python version. + +Additionally, there’s a CI job that tests against pre-releases of all dependencies (if there are any). +The purpose of this check is to detect incompatibilities of new package versions early on and +gives you time to fix the issue or reach out to the developers of the dependency before the package +is released to a wider audience. + +The CI job is defined in `.github/workflows/test.yaml`, +however the single point of truth for CI jobs is the Hatch test matrix defined in `pyproject.toml`. +This means that local testing via hatch and remote testing on CI tests against the same python versions and uses the same environments. + +## Publishing a release + +### Updating the version number + +Before making a release, you need to update the version number in the `pyproject.toml` file. +Please adhere to [Semantic Versioning][semver], in brief + +> Given a version number MAJOR.MINOR.PATCH, increment the: +> +> 1. MAJOR version when you make incompatible API changes, +> 2. MINOR version when you add functionality in a backwards compatible manner, and +> 3. PATCH version when you make backwards compatible bug fixes. +> +> Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. + +Once you are done, commit and push your changes and navigate to the "Releases" page of this project on GitHub. +Specify `vX.X.X` as a tag name and create a release. +For more information, see [managing GitHub releases][]. +This will automatically create a git tag and trigger a Github workflow that creates a release on [PyPI][]. + +[semver]: https://semver.org/ +[managing GitHub releases]: https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository +[pypi]: https://pypi.org/ + +## Writing documentation + +Please write documentation for new or changed features and use-cases. +This project uses [sphinx][] with the following features: + +- The [myst][] extension allows to write documentation in markdown/Markedly Structured Text +- [Numpy-style docstrings][numpydoc] (through the [napoloen][numpydoc-napoleon] extension). +- Jupyter notebooks as tutorials through [myst-nb][] (See [Tutorials with myst-nb](#tutorials-with-myst-nb-and-jupyter-notebooks)) +- [sphinx-autodoc-typehints][], to automatically reference annotated input and output types +- Citations (like {cite:p}`Virshup_2023`) can be included with [sphinxcontrib-bibtex](https://sphinxcontrib-bibtex.readthedocs.io/) + +See scanpy’s {doc}`scanpy:dev/documentation` for more information on how to write your own. + +[sphinx]: https://www.sphinx-doc.org/en/master/ +[myst]: https://myst-parser.readthedocs.io/en/latest/intro.html +[myst-nb]: https://myst-nb.readthedocs.io/en/latest/ +[numpydoc-napoleon]: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html +[numpydoc]: https://numpydoc.readthedocs.io/en/latest/format.html +[sphinx-autodoc-typehints]: https://github.com/tox-dev/sphinx-autodoc-typehints + +### Tutorials with myst-nb and jupyter notebooks + +The documentation is set-up to render jupyter notebooks stored in the `docs/notebooks` directory using [myst-nb][]. +Currently, only notebooks in `.ipynb` format are supported that will be included with both their input and output cells. +It is your responsibility to update and re-run the notebook whenever necessary. + +If you are interested in automatically running notebooks as part of the continuous integration, +please check out [this feature request][issue-render-notebooks] in the `cookiecutter-scverse` repository. + +[issue-render-notebooks]: https://github.com/scverse/cookiecutter-scverse/issues/40 + +#### Hints + +- If you refer to objects from other packages, please add an entry to `intersphinx_mapping` in `docs/conf.py`. + Only if you do so can sphinx automatically create a link to the external documentation. +- If building the documentation fails because of a missing link that is outside your control, + you can add an entry to the `nitpick_ignore` list in `docs/conf.py` + +(docs-building)= + +### Building the docs locally + +:::::{tabs} +::::{group-tab} Hatch + +```bash +hatch run docs:build +hatch run docs:open +``` + +:::: + +::::{group-tab} uv + +```bash +cd docs +uv run sphinx-build -M html . _build -W +(xdg-)open _build/html/index.html +``` + +:::: + +::::{group-tab} Pip + +```bash +source .venv/bin/activate +cd docs +sphinx-build -M html . _build -W +(xdg-)open _build/html/index.html +``` + +:::: +::::: diff --git a/docs/extensions/typed_returns.py b/docs/extensions/typed_returns.py new file mode 100644 index 0000000000..0fbffefe3a --- /dev/null +++ b/docs/extensions/typed_returns.py @@ -0,0 +1,32 @@ +# code from https://github.com/theislab/scanpy/blob/master/docs/extensions/typed_returns.py +# with some minor adjustment +from __future__ import annotations + +import re +from collections.abc import Generator, Iterable + +from sphinx.application import Sphinx +from sphinx.ext.napoleon import NumpyDocstring + + +def _process_return(lines: Iterable[str]) -> Generator[str, None, None]: + for line in lines: + if m := re.fullmatch(r"(?P\w+)\s+:\s+(?P[\w.]+)", line): + yield f"-{m['param']} (:class:`~{m['type']}`)" + else: + yield line + + +def _parse_returns_section(self: NumpyDocstring, section: str) -> list[str]: + lines_raw = self._dedent(self._consume_to_next_section()) + if lines_raw[0] == ":": + del lines_raw[0] + lines = self._format_block(":returns: ", list(_process_return(lines_raw))) + if lines and lines[-1]: + lines.append("") + return lines + + +def setup(app: Sphinx): + """Set app.""" + NumpyDocstring._parse_returns_section = _parse_returns_section diff --git a/examples/krumsiek11.ipynb b/examples/krumsiek11.ipynb deleted file mode 100644 index 5a8fb29c8a..0000000000 --- a/examples/krumsiek11.ipynb +++ /dev/null @@ -1,220 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Simulate and Analyze Model of Krumsiek et al. (2011)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here, we are going to simulate some data using a literature-curated boolean gene regulatory network, which is believed to describe myeloid differentiation (Krumsiek et al., 2011). Using `sc.sim`, the boolean model is translated into a stochastic differential equation (Wittmann et al., 2009). Simulations result in branching time series of gene expression, where each branch corresponds to a certain cell fate of common myeloid progenitors (megakaryocytes, erythrocytes, granulocytes and monocytes)." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "from sys import path\n", - "path.insert(0,'..')\n", - "import scanpy as sc\n", - "\n", - "# set very low png resolution, to decrease storage space\n", - "sc.sett.dpi(30)\n", - "# show some output\n", - "sc.sett.verbosity = 1" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "reading params file ../sim/krumsiek11_params.txt\n", - "writing to directory ../write/krumsiek11_sim\n", - "restart 0 new branch\n", - "restart 1 new branch\n", - "restart 2 no new branch\n", - "restart 3 no new branch\n", - "restart 4 no new branch\n", - "restart 5 no new branch\n", - "restart 6 no new branch\n", - "restart 7 no new branch\n", - "restart 8 no new branch\n", - "restart 9 new branch\n", - "restart 10 no new branch\n", - "restart 11 no new branch\n", - "restart 12 no new branch\n", - "restart 13 no new branch\n", - "restart 14 no new branch\n", - "restart 15 no new branch\n", - "restart 16 no new branch\n", - "restart 17 no new branch\n", - "restart 18 no new branch\n", - "restart 19 no new branch\n", - "restart 20 no new branch\n", - "restart 21 no new branch\n", - "restart 22 no new branch\n", - "restart 23 no new branch\n", - "restart 24 no new branch\n", - "restart 25 no new branch\n", - "restart 26 no new branch\n", - "restart 27 no new branch\n", - "restart 28 no new branch\n", - "restart 29 no new branch\n", - "restart 30 no new branch\n", - "restart 31 no new branch\n", - "restart 32 no new branch\n", - "restart 33 no new branch\n", - "restart 34 no new branch\n", - "restart 35 no new branch\n", - "restart 36 no new branch\n", - "restart 37 no new branch\n", - "restart 38 no new branch\n", - "restart 39 no new branch\n", - "restart 40 no new branch\n", - "restart 41 no new branch\n", - "restart 42 no new branch\n", - "restart 43 no new branch\n", - "restart 44 no new branch\n", - "restart 45 no new branch\n", - "restart 46 no new branch\n", - "restart 47 no new branch\n", - "restart 48 new branch\n", - "reading file ../write/krumsiek11_sim/sim_000000.h5\n", - "subsampled to 640 of 640 data points\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# let us now attack the true model by Krumsiek et al. (2011)\n", - "params = sc.read_params('../sim/krumsiek11_params.txt')\n", - "# pass params as keyword arguments\n", - "ddata = sc.sim(**params)\n", - "sc.plot(ddata)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "computing Diffusion Map with method \"local\"\n", - "0:00:26.001 - computed distance matrix with metric = sqeuclidean\n", - "0:00:00.006 - determined k = 5 nearest neighbors of each point\n", - "0:00:00.017 - computed W (weight matrix) with \"knn\" = False\n", - "0:00:00.002 - computed K (anisotropic kernel)\n", - "0:00:00.005 - computed Ktilde (normalized anistropic kernel)\n", - "0:00:00.626 - computed Ktilde's eigenvalues:\n", - "[ 1. 0.99993706307426 0.99991247662991 0.99982497190721\n", - " 0.99976772986157 0.99931303501302 0.9989748635937 0.99866417673645\n", - " 0.99823433744463 0.99806438090514]\n", - "perform Diffusion Pseudotime Analysis\n", - "0:00:00.108 - computed M matrix\n", - "0:00:00.284 - computed Ddiff distance matrix\n", - "detect 2 branchings\n", - "tip points [118 306 627] = [third start end]\n", - "tip points [128 114 241] = [third start end]\n", - "0:00:00.105 - finished branching detection\n" - ] - }, - { - "data": { - "image/png": "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\nRU5ErkJggg==\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\nvE6b1gAAAABJRU5ErkJggg==\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\ngg==\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "ddata['xroot'] = ddata['X'][0]\n", - "ddpt = sc.dpt(ddata, num_branchings=2)\n", - "sc.plot(ddpt, ddata, layout='3d', legendloc='upper left')" - ] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python [conda env:py27]", - "language": "python", - "name": "conda-env-py27-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/moignard15.ipynb b/examples/moignard15.ipynb deleted file mode 100644 index 1abc1d8858..0000000000 --- a/examples/moignard15.ipynb +++ /dev/null @@ -1,189 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "# Analysis of data of Moignard et al. (2015)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This has been published in Haghverdi et al., Nature Methods 13, 845 (2016) - there, a Matlab implementation was used." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "from sys import path\n", - "path.insert(0,'..')\n", - "import numpy as np\n", - "import scanpy as sc\n", - "\n", - "# set very low png resolution, to decrease storage space\n", - "sc.sett.dpi(30)\n", - "# show some output\n", - "sc.sett.verbosity = 1" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "def moignard15_raw():\n", - " \"\"\" \n", - " 1. Filter out a few genes. \n", - " 2. Choose 'root cell'. \n", - " 3. Define groupnames by inspecting cellnames. \n", - " \"\"\"\n", - " filename = 'data/moignard15/nbt.3154-S3.xlsx'\n", - " url = 'http://www.nature.com/nbt/journal/v33/n3/extref/nbt.3154-S3.xlsx'\n", - " ddata = sc.read(filename, sheet='dCt_values.txt', backup_url=url)\n", - " X = ddata['X'] # data matrix \n", - " genenames = ddata['colnames']\n", - " cellnames = ddata['rownames']\n", - " # filter genes \n", - " # filter out the 4th column (Eif2b1), the 31nd (Mrpl19), the 36th \n", - " # (Polr2a) and the 45th (last,UBC), as done by Haghverdi et al. (2016) \n", - " genes = np.r_[np.arange(0,4),np.arange(5,31),np.arange(32,36),np.arange(37,45)]\n", - " # print('selected', len(genes), 'genes')\n", - " ddata['X'] = X[:, genes] # filter data matrix \n", - " ddata['colnames'] = genenames[genes] # filter genenames \n", - " # choose root cell as in Haghverdi et al. (2016) \n", - " ddata['xroot'] = ddata['X'][532] # note that in Matlab/R, counting starts at 1 \n", - " # defne groupnames and groupnames_n \n", - " # coloring according to Moignard et al. (2015) experimental cell groups \n", - " groupnames = np.array(['HF', 'NP', 'PS', '4SG', '4SFG'])\n", - " groupnames_n = [] # a list with n entries (one for each sample) \n", - " for name in cellnames:\n", - " for groupname in groupnames:\n", - " if name.startswith(groupname):\n", - " groupnames_n.append(groupname)\n", - " ddata['groupnames_n'] = groupnames_n\n", - " ddata['groupnames'] = groupnames\n", - " # custom colors for each group \n", - " groupcolors = np.array(['#D7A83E', '#7AAE5D', '#497ABC', '#AF353A', '#765099'])\n", - " ddata['groupcolors'] = groupcolors\n", - " return ddata" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "reading file ../data/moignard15/nbt.3154-S3.h5\n", - "subsampled to 787 of 3934 data points\n", - "computing Diffusion Map with method \"local\"\n", - "0:00:00.527 - computed distance matrix with metric = sqeuclidean\n", - "0:00:00.012 - determined k = 5 nearest neighbors of each point\n", - "0:00:00.049 - computed W (weight matrix) with \"knn\" = False\n", - "0:00:00.005 - computed K (anisotropic kernel)\n", - "0:00:00.015 - computed Ktilde (normalized anistropic kernel)\n", - "0:00:00.154 - computed Ktilde's eigenvalues:\n", - "[ 1. 0.98827710168267 0.95478007177799 0.82124547396644\n", - " 0.77235915772496 0.7461081691426 0.66023847141885 0.62801311818493\n", - " 0.61945974353947 0.61397984513027]\n", - "perform Diffusion Pseudotime Analysis\n", - "0:00:00.102 - computed M matrix\n", - "0:00:00.374 - computed Ddiff distance matrix\n", - "detect 1 branchings\n", - "tip points [675 284 498] = [third start end]\n", - "0:00:00.117 - finished branching detection\n" - ] - } - ], - "source": [ - "# load data dictionary\n", - "ddata = moignard15_raw() # ddata = sc.example('moignard15_raw')\n", - "ddata = sc.subsample(ddata, 5)\n", - "# perform DPT analysis\n", - "ddpt = sc.dpt(ddata)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAIcAAABuCAYAAAANrABAAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAAEnQAABJ0BfDRroQAADOZJREFUeJztnX1wVNUVwH9n802IyEegEEERsRAsI05hDFqEWrT1Y4it\nVYpY0xmcVvuB1TotWKzjqK1FRUWdabVt0BnFKmPBllZqKUUrFMYRlEkriHwumhBIJJjwlT39474N\ny2NX9uPtvrfJ/c1kdvflvnPPe++8+8497957RFWxWOJRmO6OIlIJjAIOeaeOxWdKgf+p6l7IwDiA\nUfX19aurq6uTKtzU1ATAwIEDM6gyM9mrVq1i8uTJnslftWoVQFoyTyXbC1KV3dDQQF1d3SQgY+M4\nVF1dzfjx45MqHA6HAaiqqsqgysxkh8PhpPVNRn50ezoyTyXbC9KU3fUkkHR9DhEZv27dunWZnBhL\nsFi/fj0TJkyYoKrrAUJ+K2QJLkk/VkTkCuAsoK+q3g/mmRYOh7PSJGaDuro66uvrPZP37Zkz+eD1\n15k5eDCRSAREUEAARMBplcX5U0BEiDjbVQSJbbmd7yERItHPmLIh1RPkaySSsGzfIUOYsWwZoYKC\npI4lHA53+ShRkjYOVV3uGMgZye7T3TnU2kp7YyN7Gxv9VuUk9m/YQEdLC+UDBqQtI5WWY5RjIJOi\n2wYOHJg3rQZAbW2tp/JmzJoFs2Z5LtcPqqqq2LNnzwnbUumtnC8i1wAtnmqVQ7y+iN3BKD6LVB4r\ni7OpiCV49KjeSl1dnefyvJYZJHqUcVhS44THioicCZQBg1X1n/6olD2sz5Eabp/jRszLl3bAGkeO\n5QUN92OlP7AbKPdBF0vA6DIOETkXWItpMXa5C4rIrSLyMxH5RQ718xTrkKZG7GNlNDAGKAF2xin7\nCtAIzI9uyLfwuae03Aodi0D6+62JJ3xm+FxVlzqtx2jMa4Dlrv0/AX4JPJZlPbOGpz5Cnyeo/fpE\nKOrlncyA4XZIm1V1ftyS8HtgB1ALPA49PHweClF7/Uzv5PlMMuHzSU7roao6N/Yfqjo9y/pZAoa7\nt/IysAd4ywddso51SFPDbRwjgcOYcRuWHo77sfI5zIjyLT7oknVsECw13MbxODACOOqDLlnHGkdq\nuB8rdwB9Ae/HyVvyDrdxHAL6AafHKywi5SJS7/Ro8g7rkKaG+7FyhM/uqVQB70R/5FOEtKIBDrbC\nogbvZE45AsOKvZPnJ8kMML4Q88o+Asx1/Q9V3SwiNVnTMIuMCMGmS2sJYQ7O/RkdHR5yPsXZL17Z\nEFAEXDmtlhEluTyK3OI2jkedz2Mi0l9V98XZp2ssfT5FSDeMAkZ57EBWdx+HNF6E1O1zfNP5vAH4\nSTwhqvqsqm72Xj1L0HAbxy5gDeat7Ee5Vye7WIc0NdyPlReBmzFh9M7cq2MJEm7juBYzGuwKVf21\nD/pkFRsESw23cRwEmoDePuiSdaxxpIbb52gCHsCOIbVwsnFUAk8BrT7oknWsQ5oa7sdKIfAmcI67\noIhcBQwHilT1EUgtQrp69TaWLPkvIkokYrapQiikRCJCKASRiFJYGKKzM4KICUdFVygwZSESIamy\nIiaUJaKoCpMmnZX62elBnDJCqqoPO19XxNl/oqrOFZFfpVP5VVc9T1vbsXR29YQnn3ybJUusz5EK\nqcyyj17ZtCKkBw7clUJV+UF3Mo5kIqSfxdsiMhszjNDSA0jaOFR1qao+pqoL06koHA53rW7nNcnK\nTtd5TCTfC4c0COclEZksNVna0JD8+++os+NuurwgWdnNzc2sX7/eM/nNzc0Aack8lWwvSFW2cz1L\no78zWWrSrmDc/ThhBeO0jcPS/bGLt1gSkonPkTTxAmgZyrsV6IOZZ7MDM3rtbszotSLgaVXdnmEd\n3wUGYQaFeSZfRL4MfBHzSPZcdxG5FhgGnJep/Fy1HBOdXo5Xo9pfAR4EbgIWAv8BxgLvAg9h3i6n\njYhcinmF0JkF+V8FPgW2ZkE2mDhUJXBdpvJzZRwnBdAyJDrj/3aOD/nUmO+RDOVfDlRj7jav5fdX\n1SeBO7MgG2CMqs5xyUxLfk4cUhGZhpliGUk3TuKStxjTZH6MmUoRAe7FvFFuB+pVdUeGdQwDrgSG\neClfRK7GPA7LgWKvdReRm4EKL+Tb3oolITZTkyUWm6nJK/k2U1NibKYmm6kpPjZTU/fDZmqyJE1K\nxmFn2Z8srzuPIU215Yg7y94SLA63tzO/Xz8OppBBKqM0XpDfs+wh/+etqCoHw2EONTRwYN069q5Z\nwycbNnC0pYUjHR0cxSzJdACTS65XZWVG9aXTW8nLWfYQXOM4smcP++fNo+3VVzm2dy/HMGHNTo6v\nvxV9/xD7O/o9ekGkvJyhU6dSfffdVI4bl5IOp1yH1AkZjwI2J3pzp6rPOmVtN8UDjjyxgL0/vP2E\nRdikuBj69qW8pobySZMoq6mh18iRlPXP7VLa7pZjFuZt5Hjg/pxqkgO8Th0adUYzkRn6yuUMWPgw\nJT+43RulPMRtHL0wIdTkkpFaMqbw3T9TOG6k32rExW0c84EvAf/2QZesE0if45rbIBTMe9FtHHdh\nBqJcQJw1wfKdQBpHUXBXnHMbx2bgVT8UsQQPdxDsC5hRUJf7oEvWsRHS1Ii3eMtOvBvOZ8ljuoxD\nRAows+u7rWEE0ucIMLEtx1RgIib4Vkb8ZRjyGmscqRHrc4zF9FLKMJkTLDngXRbwCVv9ViMuXcbh\nrB44B/gj0H2Sl8UQRId0LD+mT0DvRXdv5UbMbKl5PujSI3mA+bTR5rcacXH3VlqcFKKf90WbLBNM\nn+NcKqjwQI73uI1jnYjcB/zLD2WyTdCM42808hHDPdLGe2LTlf8SM79SgWm+adSD+Ll+yFY6/FYj\nIbEO6RzgPVWdB2zyT6Xs4bdDuqsdFjZGOG3nEXodaGJnZAh/YYKnOnmJ+7FSJiL3YCYqd0tUoaUd\n1rTApnbY3g7bDkHjYdjfaSaTdmDyp3YWuiKCsWcrBDTDacDyldBS6IzWipaJvmgtwkxfLnD+VwCU\nQNXBXuzoPQDpSvsTPNzGER1hmndR0kgEVmyD+RthYyvsA3N0IcwFKQAG1bJoGeZixR559Hv0IsYh\n5BQrBPoIDCuCxotr2f6pU6DT7FuMWTikqgDGVMCYMqgph/EVUF4clVTs/AUbt3GscbZlukZEVjnY\nDlN+C+/sN3c3RRw/39G7s9Bs7hOCCyrhyiHwtam1DK+AQo+WrOkcV0u4A4Z105Xi3adpivN5Zq4V\nORWtbfCdp+BPW4ESTBy3ECpKYMZ5MPciGNYvtzoVhLqvYcDJxvEWJm3oMh90OYmXVsD0+yBShBm8\nWAJVQ2D7gvTu/iCOIQ0y7lP8LcxIsEuBe3KujcOH22HEZEzWl15w02XwmzuhpBtnYgwibuNQjNPt\nazKeG27spOCI0PRGiH59vZMbtCBY0HEbx/PAFcALPujSxe7tHbz2XCn9+no7z9saR2q4z/7FmC7+\njFwq0dZ2iGeeeZ/Jk1cgsordu7cwZUowR2T3JNwtRx/MYmPN7oKZJuNZvHgjs2f/HTNXNxoRKuJ4\n39N8r6w8nZ07LyYU8j44ZB3SxCQzkboUYwDvx9k/o2Q8t9yynNbWCFBEeXkJY8dWcvXVZzN9+jkM\nHXoahV4FHyyecdLUBFW9V0RuiVM2o2Q8LS1z0tPQQ6zPkZhTTqQGakRkNDBARB5Q1diJTXmfjMca\nR2q4c7zNTlRQVZdmXx1LkMjZmmBByEhkMzWlhs3UlIF8m6kpAXYF426JzdRkSQ67DqklITZTU+I6\nbKamdJVIEZup6Tg2U5MLm6npODZT0wmV2ExNsXJtpiZL/mN7K5aEWOOwJKTbG4eITEuUAkREvu+B\n/EtE5LJ4dTr+RcaIyE1+pDEJ1AgbEbkD07PZgoljDMIMdn4ZuB4YCrwE3AasxThcpcDDwI8wDuoW\nTHdtNTAA45yVOmNU9gHPxThkXZ67iLwG1AP7MctffQqsBC7EePoFmGkbD2JG5j/q1Hk6sFRE5jj7\nHAUGO3qd78Q1NgGTHZ1CmMQGg4DeqnqfU//3nOPpDYSB0ZgpIlOd72udgVbRY7wOWKmqi9I41UkR\ntJbjPUzvowC4DONd98f0GhZgDANgI2YQ9BbMchEjMEGlduBs4GNV/YMj6wNgA2Z0WykmKISIVAOx\nbw7XquoLGMNowFykEsxFjV7wqPdeANQAL2IGZQtQrqqPY3o30ToBDqvq74Ddqvo0ptc2FTMlt5+z\nUB/AGc7+ZU49i4AJwCOYtWEHxjnGrBkGBM84zsGMXx0G/ANzgT4CXsPENK6HrowTxHwexRhKMbCN\n4xdRHXlTMK1OO+YiA1wEvBFT9yUichvm7h6OaWXOxBhfCbALY5Q/xazX+iamhfqGU0+7c/dvj6lT\nY3SM1Wklpqv5sap2Otv3OK1bi/O7E/grMBtzozS5jjG6X9bIi66sMzptKubCPqSq+7JQh3vkW48n\nL4zD4g9Be6xYAsT/ATOmxdvhFip6AAAAAElFTkSuQmCC\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "ddpt['groupnames'] = ['trunk', 'undecided/endothelial', 'endothelial', 'erythrocytes']\n", - "sc.plot(ddpt, ddata, legendloc='lower left')" - ] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python [conda env:py27]", - "language": "python", - "name": "conda-env-py27-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.13" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/paul15.ipynb b/examples/paul15.ipynb deleted file mode 100644 index f891fea64f..0000000000 --- a/examples/paul15.ipynb +++ /dev/null @@ -1,203 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "# Analysis of data of Paul et al. (2015)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This is based on a more extensive R tutorial by M. Büttner available from https://github.com/theislab/scAnalysisTutorial." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "from sys import path\n", - "path.insert(0,'..')\n", - "import scanpy as sc\n", - "import numpy as np\n", - "\n", - "# set very low png resolution, to decrease storage space\n", - "sc.sett.dpi(30)\n", - "# show some output\n", - "sc.sett.verbosity = 1" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Get the data." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "def paul15_raw():\n", - " filename = 'data/paul15/paul15.h5'\n", - " url = 'http://falexwolf.de/data/paul15.h5'\n", - " ddata = sc.read(filename, 'data.debatched', backup_url=url)\n", - " # the data has to be transposed (in the hdf5 and R files, each row \n", - " # corresponds to one gene, we use the opposite convention) \n", - " ddata = sc.transpose_ddata(ddata)\n", - " # define local variables to manipulate \n", - " X = ddata['X']\n", - " genenames = ddata['colnames']\n", - " # cluster assocations identified by Paul et al. \n", - " # groupnames_n = sc.read(filename,'cluster.id')['X'] \n", - " infogenenames = sc.read(filename, 'info.genes_strings')['X']\n", - " # print('the first 10 informative gene names are', infogenenames[:10])\n", - " # just keep the first of the equivalent names for each gene \n", - " genenames = np.array([gn.split(';')[0] for gn in genenames])\n", - " # print('the first 10 trunkated gene names are', genenames[:10])\n", - " # mask array for the informative genes \n", - " infogenes_idcs = np.array([(True if gn in infogenenames else False)\n", - " for gn in genenames])\n", - " # restrict data array to the 3451 informative genes \n", - " X = X[:, infogenes_idcs]\n", - " genenames = genenames[infogenes_idcs]\n", - " # print('after selecting info genes, the first 10 gene names are',\n", - " # genenames[:10])\n", - " # write to dict \n", - " ddata['X'] = X\n", - " ddata['colnames'] = genenames\n", - " # set root cell as in Haghverdi et al. (2016) \n", - " ddata['xroot'] = X[840] # note that in Matlab/R, counting starts at 1 \n", - " return ddata" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "reading sheet data.debatched from file ../data/paul15/paul15.h5\n", - "reading sheet info.genes_strings from file ../data/paul15/paul15.h5\n", - "computing Diffusion Map with method \"local\"\n", - "0:00:17.432 - computed distance matrix with metric = sqeuclidean\n", - "0:00:00.101 - determined k = 20 nearest neighbors of each point\n", - "0:00:00.426 - computed W (weight matrix) with \"knn\" = True\n", - "0:00:00.048 - computed K (anisotropic kernel)\n", - "0:00:00.096 - computed Ktilde (normalized anistropic kernel)\n", - "0:00:00.998 - computed Ktilde's eigenvalues:\n", - "[ 1. 0.99284239124065 0.96918877360182 0.94440982539501\n", - " 0.93200680698376 0.8945161846998 0.87862932487111 0.85561150822895\n", - " 0.84358415344857 0.82316095171039]\n", - "perform Diffusion Pseudotime Analysis\n", - "0:00:00.985 - computed M matrix\n", - "0:00:11.296 - computed Ddiff distance matrix\n", - "detect 1 branchings\n", - "tip points [1687 877 2156] = [third start end]\n", - "0:00:00.699 - finished branching detection\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "ddata = paul15_raw()\n", - "ddata = sc.subsample(ddata, 1)\n", - "ddata = sc.preprocess(ddata, 'log')\n", - "\n", - "# perform DPT analysis\n", - "ddpt = sc.dpt(ddata, k=20, knn=True)\n", - "ddpt['groupnames'] = ['','GMP','','MEP']\n", - "sc.plot(ddpt, ddata)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "testing groups ['GMP' 'MEP'] with ids [1 3]\n" - ] - }, - { - "data": { - "image/png": "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\nJDZwAAAAAElFTkSuQmCC\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# perform differential gene expression test\n", - "ddifftest = sc.difftest(ddpt, ddata, groupnames=['GMP','MEP'], log=False)\n", - "sc.plot(ddifftest,ddata)" - ] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python [conda env:py27]", - "language": "python", - "name": "conda-env-py27-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.13" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/toggleswitch.ipynb b/examples/toggleswitch.ipynb deleted file mode 100644 index 1783ac0558..0000000000 --- a/examples/toggleswitch.ipynb +++ /dev/null @@ -1,177 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Simulate and Analyze Toggleswitch" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "A simple toggle switch of mutually suppressing genes, as in Nature 403, 339-342 (2000)." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "from sys import path\n", - "path.insert(0,'..')\n", - "import scanpy as sc\n", - "\n", - "# set very low png resolution, to decrease storage space\n", - "sc.sett.dpi(30)\n", - "# show some output\n", - "sc.sett.verbosity = 1" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "reading params file ../sim/toggleswitch_params.txt\n", - "writing to directory ../write/toggleswitch_sim\n", - "restart 0 new branch\n", - "restart 1 no new branch\n", - "restart 2 new branch\n", - "reading file ../write/toggleswitch_sim/sim_000000.h5\n", - "subsampled to 200 of 200 data points\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\nhuHCY4ejYfw/TYekOqfzdf8AAAAASUVORK5CYII=\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# let us first simulate an extremely simplet model, \n", - "# a 'toggleswitch'\n", - "params = sc.read_params('../sim/toggleswitch_params.txt')\n", - "# pass params as keyword arguments\n", - "ddata = sc.sim(**params)\n", - "sc.plot(ddata)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "computing Diffusion Map with method \"local\"\n", - "0:00:00.646 - computed distance matrix with metric = sqeuclidean\n", - "0:00:00.003 - determined k = 5 nearest neighbors of each point\n", - "0:00:00.002 - computed W (weight matrix) with \"knn\" = False\n", - "0:00:00.000 - computed K (anisotropic kernel)\n", - "0:00:00.000 - computed Ktilde (normalized anistropic kernel)\n", - "0:00:00.050 - computed Ktilde's eigenvalues:\n", - "[ 1. 0.99966960531601 0.99937767123908 0.99840708794327\n", - " 0.99736038384816 0.99571525283041 0.99335501079534 0.99130297192981\n", - " 0.98945796011601 0.98622767233945]\n", - "perform Diffusion Pseudotime Analysis\n", - "0:00:00.009 - computed M matrix\n", - "0:00:00.008 - computed Ddiff distance matrix\n", - "detect 1 branchings\n", - "tip points [ 2 95 189] = [third start end]\n", - "0:00:00.046 - finished branching detection\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAIsAAABuCAYAAAAXkODOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAAEnQAABJ0BfDRroQAADwFJREFUeJztnXt0VdWdxz+/PCAJIA95GuVRBSUqYxnDEhDFTguzGEYi\nZbRjbZLWaZ1xavExXRZqq9VqZ1R0lIKzpq1iOy3QohTraq0uFR9QSJag8lASBXmEl4k8bALkcX/z\nxz433BxuyL3n3HvOzXV/1rrr3Jyzz29/T87v7rP3Pnv/tqgqFksi5Hk9UUQGARcAx1MnxxIyBcD7\nqvpxvIOenQW4/NFHH3128uTJPkyc5ODBgwAMHjw46XNXr17N1KlT27d+bPnRFs0/FbZSrS0R1qxZ\nw2233TYbWBnvuB9nOTF69GhKS0t9mDhJXV0dAMXFxZ7OLS0t7bD1asuPtmj+qbCVam2J4Djeic6O\ni9c6i4iUVlVVVaXKWSzhU11dzYQJEyaoanW84zlBC7J0XxJ+DInIDGAk0F9V7wdTbNXV1aWsSPVK\nZWUlS5Ysad+GwaHt2ykvL+dH3/gGqgo5zu9QFQEQQSMRJCcHjURQEUQVEYGY9Oqkj4A5HmMHESKq\n5ADq7BsxZQoDx4zxrb+urq69/tMZCTuLqv7RcZiz/QrLRjYvW8a+DRt4dft2FBDouBVpv8HtOA6D\niHEwx3HE+bvdyZxjotruZFHb42+8kavuuy+Qa0ymZLnAcZgrovsGDx4ceqkCUFZW1mEbBlPmz+eu\nkpJQNfihuLiYvXv3njZNMq2hS0TkGuCQL1VpIBOcJRPyTzfJPIaWpVOIJfPJitZQZWVlh23YOrKV\nrHAWSzB0eAyJyAigEBimqq+GIyl5bJ0lGNx1lq9hXiY1AdZZPOrIVtyPoTOBPUCvELRYMpx2ZxGR\nMcA6TImy251QRG4Wke+JyN0B6ksIW8ENhtjH0FjgQqAnsCtO2pXAAeCh6I5M6e4H4Oh34divYd8K\nnM5wTB8nMd+j+9V1LN6+2L/jnetKn9Mf+DtflxAmSXX3q+oqp3QZi/lP/NGV9gjwE+CxFOv0TVlZ\nGfS5mrJrJ8DgWdH+9ZgUGuceSwLHcN7J0PX5OTmUlT2fluvLFDoMURCRr6vqU3ETiiwDdgJ1qvq4\nHaKQfXQ1RMHdGrrCKV1UVefHHlDVr6RLpKV74G4NrQD2AmtD0OIZW8ENBrezjMYMqxsZvBRLpuN+\nDA3FjNivDUGLZ2ynXDC4neVx4FygJQQtnrHOEgzux9AdQH8gNfMULFmF21mOAwOAfvESi0gvEVni\ntJgyBlvBDQa3szRjWkLrOklfDGyM/hHtwQ2TVUfhD0dh1s5QZXR7vAzYvgwzRCECzHcnVtUaEZmY\nMoUpoDgPLplRxiWF8HlbZ0kr7h7c6c7XVuBtVW045QSRckzJ09f24GYXyU4y+ydn+1XgP+KdoKq/\nVNWaFGq0dBPczrIb+AvmrfO+4OV4w1Zwg8FdZ1kOfBPT7d8WvBxLJuMuWeZgZhzOUNX3QtDjCdsp\nFwzukuWvwEGgdwhaPGOdJRjcJctB4AHsGFxLHNzOMghYDBwOQYtnbAU3GNyPoTzgTeA8d0IRmQmM\nAvJV9RFI7Rjcmpp6GhqOkZMDkQiIKCI5TtABRRVyc4W2NnVGTCqQQ+/e+b7ztnjowVXVBc7XF+Ok\nnaSq80XkP1OkrwNlZcs5cKDRhJcg+slxolBEw1AAaPvwWlXo27eQBQtmOTZsnSWdJBNFodXZtnf5\npjLkxtat/+7bRtg3K+z8/ZBIyI1k5jq/JSJzMcMuLZ9BEnYWVV2lqo+p6kJn16CunnHJUFdX5/kN\ntruC68dWPBK1l0gFNyxtieDcz0GdHfcT2rRnbW0t1dVx3zklTdTxuioK41FfX091dXX71o8tP9qi\n+afCVqq1JUJtbS2YSYZx8RPa1EbYzj5OG2Hbs7NYPnvYYD6WhEm6ziIignkrvR74FVAO5AMvqep6\nLyLidfh5tDMHGI95NApm1N88TbL4FJFewCLgSeAqx84PMaMH84GfqepHHuz9HPg2sAH4H+D2ZO2J\nyM1AX8wcr51+tDm2zsC8PB7Yla6ESxYRuUVElgK/AXo4xvKAFlX9MeCnk2GS08ryO6tgPCbSQyOw\nEOPQ4z3YKQbeBi6PsTMOeBd4GPN2Pll7GzFRKg5gHHm0R3srgf8CKlKgbSXwIDAxEV3JRKtcCCwU\nkT6O0ZHAlZjxusRsvXBKh59HFqvqHhFpxpQKsbEyEiZmrHFPOsZPiH5P6lpj7H0IPAX8PaYy6cVe\nNJrF7SnQFrV1E8ZBTqsr6QquiIwCbnUy+l/gXzBhxVaralVSxk7anIVxvkhMP44XOzdh3pj3BIow\nF3uvqiY9kMsZa1wNXB+1g3kj3wQsUdWk5hM49t4HrnHsPYB5fCRlLyaaxX7MtB3P2mJsNWDmi51W\nl20NWRLGrmRmicWuZBaUNruSWefYlcw60ZEKW6nWlgh2JTNLwtiVzCwpIylnsVEUEtORrSRbsmRc\nFIVMYVk3HiUHKV72DjIzigKEP2+o8cMPmXTDDRTk+WkvZD5e6ixpGYPrh7CcRdva2D5tGjVTppB3\n/Hig+UciEVobG4kcSs3CcsXFxV02v91LyAzHdLTVdPbmUlV/6aT9zDaDIrt20lr2RRr3NZB3bgmj\nnnySvtOnd31isqjC5o1Qtwvef4/mVb+n9aOdHD/aRMuJCBGg8MYb6fdYQEHPVbX9g3nHcDvw/dj9\n8T5AaVVVlWYCFRUVHbbpJtLYqM2LH9PmqnUaiURO0ZESFt2j+vm+qiX9VMcNUb10lLaWf1mbn/2t\ntuzambp8YqiqqlKgVDu55+6HbBGmyzc3GFftRkQicKgO1q9C1q4g/6Yn4Jyx6cmrpRl2rYelr8P5\n49p35xLujXE7y0PAFGBNCFo8E0idZdMLsPh6yOkF510Og0d2qsMXqrByLky8rIOjZAJuZ/k+ZuDQ\neOLElMtU0uosbSeg5v/g8DuwcDf06NOlDl+8Ohf2vgHfesO/rRTjdpYa4A9hCMlIWg7BWzfDB29C\n0UjISXPTONIGTTvgW69CUf/05uUBd9P5YmC68+k2pKUHt/F12DEX2nbAdZth9huQV5iQDk/s/h5U\nnwkXlUJRp/O8QiVeMJ9d+B/e2G2JcALqv4gc3or0GAGTqkHSW61UInDGfmTgRigclda8/BC7RmIu\nJnpChG7mLKmssxzmIpr6FcK59TByQ1KO4iX/j6ngIMM5VqQZ7SjQsWT5EjAJM3i6kPhhNzISv86i\nKLtYRD1LGcotDMj7ji8difIp79JIhLNYS4/84Z7yDJJYZxmHaQVtwqwMkvXsZwtv8wRtNFBEM6X8\nnEI+F0jeO3mNGhYzgR/Sg8x3FIh5DKnqg8A84LfADaEp8kCyFdzd7OB3LOJlHqcf5zOObzOVFfRm\nLLmdzwtPWEdXrOE5XuE5LmYefbnQc35BE2/F+DXAbOAHwctJHxGUI/yVJppZwTKG0psZ3Ef/EFbL\nOYsxnMvfMJQRgeftB7ezHFKzZO/5oajxSFd1lnqO8TY7+D1vkosyg2lM52/TpqMznmEzL7GeMyng\nfr6a8vzTjdtZqkTkx8BrYYjxSjxn2U0jt1BNK8fJp4UiclhEOf0oSLuOeGzmU6rYz51cwygGpE1D\nOml3FhH5CWaG20FgFvBSWKJSwVAKuYuLOYcihnD6zrR0s73tBLfm1FIuJd3WUaBjBXcesElVfwBs\nDk9S8lRUVPJpG5RXVPLBCdh4DKqPCRfrgEAdpbKykqY22NMM7xyDe480M7SpnitbD3FnpIRyzgpM\nSzpwP4YKReQezDzmbsG7R2H5EXh2Kxw/As/tNDO6e+TB6rNhbI9gdCzcB8s/gWdqTD9eLtAnP5eN\n5wxgWF52TKJwO0t0xG7G9+Aeb4EHt8IjB+CbV5fx0IXwfHkZc9rnHUigeib2hptnl/HwBbTH6c22\nYUFuZ/mLsy/Z+COBsv0wjF8KfQqh9loYVGQqlnNmhzfC/tI+cGll9x7h3xVuZ7nK2WZsB8Cuj+GJ\nF+C9r8CwzHuLn9W4H6Zrga3AXSFo6ZJnXobJd8ALG6ElJuKKnWQWDG5n+WfgCkwkoIyioQFeXg33\nXgubHoHhA8NW9NnD7SyKeevs/QVJGnj++VbOL1G2vQNfn3nq8bAnmbl1ZC3acXrHhcB3gXGdTQfQ\nAKeCLFjwiQ4ZskcHDjyqL77Ymta8LMlPBbkcE5/jekxAulBQVa6++i02bMjnpz8dxcyZvSgoyI6+\niu6M+w70xQT6r3cnFJGZTnjT26P7Ujkxftu2eu6/fy3Fxb+gf/+naWj4lC1bSpgz54wuHcVWcP3j\nZWJ8ASZ48bY4adO6ONXs2ctpamrj7runMnXq2YwZ033foWQrp0wFUdV7ReTf4qRN6+JUW7Z4X5zK\nVnD942Vxqoki8iPgIhF5wHUsYxenss4SDO41Eud2llBVV6VfjiWT8dPEsCuZdaIjFbYSxa5kliR2\nJTO7kpkleOxKZpbUYLtFLQljVzKLb8euZGZXMksYu5KZXcksMdSuZGZXMkvSll3JzK5kZvGKbQ1Z\nEsY6iyVhst5ZRGRWZ0veiIj3cREnbVwpItPi5Ski/+jXvmOvIhOW7cmoZSxE5A5My6gW05cwBOiN\n6de5DjgH+B2mgr0O04QvABYA38FU+moxTb7XMX0HfYACZ4xOA/CrmEpbe21fRP4MLAE+wYRLawRe\nAS7DtA5ygX6YZus9wH87efYDVonIPOecFmCYo+sSEfkCZu74VEdTDnA0em1OtwMi8q/O9fQG6oCx\nwHOY8G1jgXXOwLPoNV4LvKKqT3v4V3si00qWTZgafi4wDVMjPxP4B+BRjKMAvAMsxfzTXsOENbvA\nSf85YL+qPuXY+gDTZ7INcwMLAUSkBDNHKso6VV2KcZStmJvWE3OTow4QbQ3kYvomlmP6nQTopaqP\nA2fF5AlwQlV/AexR1Z9hWn1fAo4BA5zAjwBnO+cXOvk8DUwAHsHEJh4c5xoDcxTIPGc5DzP+dzjw\nMuaG7QP+jOlXuA5T8rj7dlowjtMD2MHJm6qOvaswpVIT5qYDTAZiw1hfKSK3Yn79ozCl0AiMM/YE\ndmOc9E5MvOA3MSXYl518mpzS4aOYPDVGY6ymVzBN/P0xzfq9TukXXROmDfgTMBfzwznousakuwP8\n0i2aziIyFvNrHAY8rKoNacjjAVXtNiHow6BbOIslM8i0x5Alg/l/ANTidA0nu7wAAAAASUVORK5C\nYII=\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOgAAABzCAYAAACSCE74AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAAEnQAABJ0BfDRroQAAELxJREFUeJztnX+QVeV5xz9PhEUgKtfKUrpKTSSh6pLppi5WQ2PunZE2\n0YCOmWwnmIDttBgVEKXNgD+Iv8AEEgM7TUzTRGJiA5M0IQ2jY2Zyo4bp0F3bbQuSxjUJKAt2QVdp\ng4LBp3+859w9e/b+vufc8+O+n5mde8+5577nuc/e733e93mf8x5RVSwWSzx5R9QGWCyW0liBWiwx\npm6BishUEdkqIu91tu8Qkc+IyIrgzLNYWpsJDby3AxjwbH8DeB24E0BEpgPzgRPAkQbOY7G0EtOB\nScAuVT1St0BV9XkRucyz6xRwD/CAsz1/3bp13+/u7qa9vb1+c0NieHgYIJa2Qbzti7NtkGz7hoeH\nGRwcZNWqVR8E6heogwIzROQsYCPwFLAQeBQ40d3dzVVXXdXgKWpn6dKlbN26teQ2wNDQEAAdHR1N\ntKx6gravmA/qpdV8FzSV7Ovv7wd4Exrr4qKqj3o2P+R7+Uhcf8Egvv88lzjbF2fbIF32pTKLe801\n15TdbkWsD5KJFWiLYH2QTBodg1osljrYsWMHBw4cYNasWVx77bUlj0tlBF26dGnZ7VbE+qD5HD16\nnMcf/1XR15577jlWrlzJvn37yraRSoFaLHHgxIlTvPrqm0VfmzDBdF5FpGwbqRSoHYOOx/qg+XR0\nnMH1119U9LXZs2fT29vLxRdfXLYNCetqFhHp7uvr6+vu7g6lfYslrfT39zNv3rx5qtqfyghqsaSF\nIIvlPykiy0XkhuDMqw+bJBqP9UEyaSSC+ovlL1TVXmCOu2N4eLhQ1mSxWCozNDRUqNWFBgSqqs8D\nr3l2/dZ5fLveNoPCJonGY30QL44dO8bChQsrHhdksfyvReRm4Bfui+3t7ZHURVqBjicNPsjlthee\n5/M9EVpSHXuH9vLt3d/mweseHPfa4cOHmTt37rj9HR0dHDp0qLAdZLF8fyNtWSyVqCTKXG47+XxP\n4TFq5syYw+0Lbi/+2pw5TJkypWIbqczi2iQR5DblyG3KFbbT4gM3iuZy2wt/LnESJ8DECROZfsb0\nkq9XKlIAW4ubGlwx5lfnxzymDVd87qMrUv/+JLB27dqKx6QygrbaGDS3KTdGkK5YvRE0rT7I53sK\nkRPGjlNTgaqG8gd09/X1qSVashuzUZtQNdnstjGP/uetQl9fnwLdqprOCGoZJb86PyaSxhnvGNKN\nhN7nrUgqBdpKSSK/+IqJMb86nxgfFBtLtrJIUynQVsKfDEpDcqjYeDJJyZ8gSWUWN+1JIm+UrFaQ\nSfJBEjOyYZHKCJp2geZX52uOlEn2Qat2b6Gxq1mudq5eWeVs/5WI3Coiq91jbLF8uKShO+tSToSt\nFEkDK5YHLldz9Yq7+O0U4CxGi+ZZs+aZBpqvn7QmifzVQbUQdx+0kghrYYxAReQhEdkgIuureK8r\nRHdJhmmqeg9wjntAW/YndP7tnwZjqaWurm0SyGR6ozYhNnR0dIy5JYQ/STQIfL3Ktv5NRFYCb4jI\nJcCbzp3NCvH5K9d/hSiWPEnjGNRfylcrcfVBLredkZHlUZsRW8asSSQidwECoKr3NtSwXZPIUoE4\nFbbHiXJrEh0FJgI2s2MJHSvOyvgFOlFV78KINDAyizvJLO4MssmypC1JFESpXpx84L9MzFIa/xh0\nqojcDxwP8iQjj+0NsrmWwDvmTFtiyEbO6ikIVEQ+ALzq/IWzWG6TSEOSKGhRJtEHlrERdB9wCJiM\nmdNMLGkQaNBYHySTwhhUVUeATwE9wJ8HfSI75qiPpFwqVg127Fk7/iTRmcApwlg68yNfbVqiKC1J\nIv9KCY0QtQ/cKRU7/qwNf5JoM2YMOjnoE6Ut0dEM0uQzK8z68Av0BkxUnQqsHn/4KCJyNfAuYIKq\nPiQiHwfOBU6p6uYwjK0WOwYdT5x8YAsUqqeRLq6/WD4HvAX80j0gqqtZkizQRgriyxEnH1hxlsZ/\nNYs/gn4JEwVfqKItf7H8JFXtFZHNwM5Sb8os7rTzomVIU7fW0jj+CPppTCS8uYr3+ovlnxaRW/BE\n0GK3fhh5bG/oyaKkJonCzNhG6QObua2eSlezKCYyTq3UkKr+0Lfr2WqNsBG0OGmNnrZLWz/+CPoE\nZvy5LewTh/mrmuQxaFhE6QMbQevHL9CrMePHN8M+8cDcG0NrO6kCDbOLG5UPbMa2Mfxd3J8D8zBd\n3efDPHHXnofDbD5xBFmUEBesOBvHH0EzmLnN3w37xGH+45KUJGp0pYRqicIHVpyN4xfoy6r6WeB/\nm3HyZl4jGlfSFjVd7LgzGPwCvUxE7gE6q1w4rCHCmnJJ6hg0TJrtAxs9g8Ev0J8D9wFDqlr55oUB\nEMaUS9IE2owrVprpAxs9g8Mv0GGMSJ9sphGt3tVNWzfXRs/g8Av0PcAfAYsqvdGzsvxtnn0bRGRB\nrUYE3dVNSpKomdd6NtMHNoIGh1+ghzFlfoNVvNctlp8O4FzNMqaGt5ZieVtdlA7s1EpjVCqWPxN4\nxXmsROEWDyLSBsx3Nt8N/LhWw9wIGoRQkzIGbWbXtlk+sOIMFv/C1XcCfwLsUtX7yr5RZBFwPkbM\nT6jqsyLyQeB0Vf2xXbi6PGkrTPDeEdvSGN6Fq8dEUFW9v9pGihTLo6oN3y0ps7iTrq72VH15/aRN\nnGCFGRaxuz/oyGN7G/7yxjlJFJU44+QDS/X47262QkTWi8hNURkEpruU1qmXtEVOsKv1hYk/SXQK\nOAC0RWBLgXy+h1yu/mgT1yRRs+puixGWD2zWNlz8SaJbgCuAZ5wplPobDihJlMbxmsVSjnJ3N9sP\n7MFc1RIpuU050216fBmZxZ2J7kKFtRBY1CT5f5IYVLXwB9yK6d62effX8wd09/X1aVBkN2arPnbJ\nkiVlt5vJtBXTIju3lyB9kM1u02nTtgTWnmUsfX19CnSr6rgx6IeBGZgLtptSLF8LSRvv5DblGNk8\nErUZgWHnOpuPv4v718BTwN8335QKPL6scPuICX8xtWyXMcokkWtX3MbOjfogk+m1t26IAH8E/Uvg\nNeAS4IHmm1Ma88UwX45cbjvshxzFRRCFQP2CjJM4oXof+KOkuz0ysjwcwyxl8Qt0CnB6kf3jKHLr\nhzsw9blvqOoWGC2W96+N2yijv+I95Dbl+Ome3WRfesT3WnPIrMzQdV5XQZD51fnYRc9acP2XyfSa\nii4bMZtKpWL5jcCVwNNVtHW5qq4VkQed7W8ArwN3BmFoNZhf92VoPj/atczBrv03cvJXo2O/tova\nOLnvZDDn9HWtvWNMV5hxFGdb26XMn3+bM8c8PkoODAyPEaSNmPHAPw/6NPAvmCtTfqqqd5d8o8i9\nqnq3iGxQ1TUi0g78DfCAqr4WVbF8blOOXV8eYP75Dxe+jLNmPcGLnS+acSwwMPM+Rh7bWxDbwEsD\ndJ3XNea5V2R+UcZRgJWYeclMDj97uJBo806R2CgZL0oWywP/pKpbRGQ5ldfGdW/9cFxEujHR9ylg\nIfBo4FZXSX51nh2zd7DlhS1kFt9H10fa+e66C7j0RZNkAmDAKco/fJezPQxdZrn9LiC/uWeMeNOQ\niT2n8xwT4fNOV9yKMhH4I+jHgPdhlj35P1X9Ud0NJ/RyM//k+8DAMCMjywv73a6gS9K+6P5eg9sb\ncLvnUZYjWgzeCDpGoEGSVIEWo9z8a6lkSlLmbEt1393kl3efpTl4BdpQtVC5PwKuJKqFKCqJstlt\nms1uG7cvLjTig+zGrE5bMa2mai5L/ZSrJLLUSbHpiXy+h0ymN/EZ0WIJMxtVm0MqBRplJZFfjO74\n1c2cRtXtDcoHtgvcZDSFXdw44nZ3/Y9pwXZ/g8PbxY3dkidpxRs5vZE0DZdseeuPLQGjKYygcbrc\nrBzuJVtuginMqNosH1STULLRtjw2SRQT3PFqueiahKkaL6UqsOJ8IUGcqVugRYrlPwlMwxQ4PBKU\ngfUQ1zWJqsFfhlesC1yPaKPwgbcIwo/3ggKbGS5NIxHULZbf4GxfqGOL50O7mqUSSRWoGzFLCdAf\nUd0pHRe36sk91kuUkdhboVRsn1+YmZWZQnllqSjsrXwqVg2VVLH7r2ZpZIx5r/O4vsR2986dO/Xg\nwYNN7L23BuXGq97XkpIpdsek2Y3Zks/9x5Z6nvTx7cGDB3Xnzp2FMWgjAl0ErATuwlzgfQPmxktL\n1CaJYkcafOAVbal9SReoakBJIh1/64dn623LYqkGf1fWu89/TFpI5TxoUsegYZImH6RNhOWwAm0R\nrA+SSSoFarGkhVQKNM53N4sK64NkkkqBWixpIZUCtWPQ8VgfJBMr0BbB+iCZpFKgFktaCFOg08fU\nFDaRapJEQ0NDDA0NNcegOgjaviCTRK3mu6Cpxb6aK4lE5CzgdmASZpHqYyIyH7gKOBNYpaongUn9\n/f0AtLe3l2ouFI4ePYp77mLbQKEg+dChQ021rVqCtq+YD+ql1XwXNOXsGx4eZnBwEMwtWKh62U1n\nMevLgeuAy4C3gQtU9XsiMh14BVgHfMER7XTMCvUngCONfSSLpWWYjgl+u1T1SM3r4orIIuBFQIDz\nVfX7zv6VwB5VbZ06LIslZOopln8KuAMTGTc6q9HPAC4FporIgKom/14JFksMCHNleXfFhYmq+sVQ\nTlIHzg/K+zH3nhFgMrBGw3JE9XZNBf4Oc5e4rGPX3Zg7nU8Evqaq+2Ng3z8AtwD/DjwM3Ba1fSJy\nE3AW8B7gAPHz3U2Y/My5wDnU4Lsws7iXq2ov0NwMUWXeD/wP8BugF/hXZ1/UdAD/gRm3u3a9D/gv\nYBPwsehMA4x9A8DFGP8JRhBxsO8HwOeAJcTTdz8APo/J3dTkuzAXDfut8xhpZCrCl1X1oIicxEQr\nIQY2qurzInIZJkGgjNrlPn87QvO89v0SeAT4M0ymMQ72vQ5swESk2PmOUfuWYURZte/C7OIuAs4H\n3nYiaSwQkWXAVIwQpmCcc6+qnorUMEBEPgX0A5/AsQtYDxwHtqrqgQjNc+37b+BajH3rMV3JSO0T\nkW2Yru3LwNnEzHce+14BMtTgu9AEarFYGseW+lksMcYK1GKJMVagCUREFonIe0u8dnMA7V8hIguK\nnVNEPtpo+057S0p9Bsso9tYPISMit2My2oOYuboZwDuB7wE9wHnAd4Fbgd1AGybD9wVgBSbxMYhJ\nxT+DmUc7AzhdRD6NSTx8y5NkKGQEReRJYCvwKqZM8zdAHvhjTAbxNMzdAD4HfBb4knPOacAPRWSN\n8563gJmOXX8oIjlgL/Ahx6Z3AMfcz6aq9zvnv9H5PO8EhoALgX8GrnSe73YWOnc/48eBvKp+sw5X\npxIbQcNnDyazeBqwAJO1+x3MxQUPYcQJ8J/AdzBf1KeBC4A/cI5/N/CymltqnA28gJkz/QVGNJMB\nROQiYJ/n3LtV9TsYce7DCGUSRliu6Nws4WmYebrtwD9i0v9TVXUL8HuecwKcUNWvAwdV9WuYbP2V\nwBvA2SJymnPcuc77Jzvn+SYwD/gi8CPMHLn/M1pxerACDZ/ZwFFgFvATjEgOA09i5u16MBHWjXzu\n41sYsbYBv2ZUSOq0l8VE3+MYoQF8APiZ59xXiMitmCj3Lky0/X3MD8Ak4CXMD8NngLnALkykvs45\nz3EnCu73nFM9NnptymOmr172TFkdcqK8W/p5CngCs+D5AmDY9xkjn+qKG3aaJSJE5EJM1JkJbFLV\nV0I4x3pVXRt0u5bmYQVqscQY28W1WGLM/wMv/6Up1ajppAAAAABJRU5ErkJggg==\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# define root cell for pseudotime analysis\n", - "# simply take the first data point (the first row of the\n", - "# data matrix X) for that\n", - "ddata['xroot'] = ddata['X'][0]\n", - "ddpt = sc.dpt(ddata)\n", - "sc.plot(ddpt, ddata)" - ] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python [conda env:py27]", - "language": "python", - "name": "conda-env-py27-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..6499e79cae --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,154 @@ +[build-system] +build-backend = "hatchling.build" +requires = [ "hatchling" ] + +[project] +name = "scanpy" +version = "0.0.1" +description = "Single-Cell Analysis in Python." +readme = "README.md" +license = { file = "LICENSE" } +maintainers = [ + { name = "Philipp Angerer", email = "philipp.angerer@helmholtz-munich.de" }, +] +authors = [ + { name = "Philipp Angerer" }, +] +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +dependencies = [ + "anndata", + # for debug logging (referenced from the issue template) + "session-info2", +] +# https://docs.pypi.org/project_metadata/#project-urls +urls.Documentation = "https://scanpy.readthedocs.io/" +urls.Homepage = "https://github.com/scverse/scanpy" +urls.Source = "https://github.com/scverse/scanpy" + +[dependency-groups] +dev = [ + "pre-commit", + "twine>=4.0.2", +] +test = [ + "coverage>=7.10", + "pytest", + "pytest-cov", # For VS Code’s coverage functionality +] +doc = [ + "ipykernel", + "ipython", + "myst-nb>=1.1", + "pandas", + "sphinx>=8.1", + "sphinx-autodoc-typehints", + "sphinx-book-theme>=1", + "sphinx-copybutton", + "sphinx-tabs", + "sphinxcontrib-bibtex>=1", + "sphinxcontrib-katex", + "sphinxext-opengraph", +] + +[tool.hatch.envs.default] +installer = "uv" +dependency-groups = [ "dev" ] + +[tool.hatch.envs.docs] +dependency-groups = [ "doc" ] +scripts.build = "sphinx-build -M html docs docs/_build -W {args}" +scripts.open = "python -m webbrowser -t docs/_build/html/index.html" +scripts.clean = "git clean -fdX -- {args:docs}" + +# Test the lowest and highest supported Python versions with normal deps +[[tool.hatch.envs.hatch-test.matrix]] +deps = [ "stable" ] +python = [ "3.11", "3.14" ] + +# Test the newest supported Python version also with pre-release deps +[[tool.hatch.envs.hatch-test.matrix]] +deps = [ "pre" ] +python = [ "3.14" ] + +[tool.hatch.envs.hatch-test] +dependency-groups = [ "dev", "test" ] + +[tool.hatch.envs.hatch-test.overrides] +# If the matrix variable `deps` is set to "pre", +# set the environment variable `UV_PRERELEASE` to "allow". +matrix.deps.env-vars = [ + { key = "UV_PRERELEASE", value = "allow", if = [ "pre" ] }, +] + +[tool.ruff] +line-length = 120 +src = [ "src" ] +extend-include = [ "*.ipynb" ] + +format.docstring-code-format = true + +lint.select = [ + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "D", # pydocstyle + "E", # Error detected by Pycodestyle + "F", # Errors detected by Pyflakes + "I", # isort + "RUF100", # Report unused noqa directives + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # Warning detected by Pycodestyle +] +lint.ignore = [ + "B008", # Errors from function calls in argument defaults. These are fine when the result is immutable. + "D100", # Missing docstring in public module + "D104", # Missing docstring in public package + "D105", # __magic__ methods are often self-explanatory, allow missing docstrings + "D107", # Missing docstring in __init__ + # Disable one in each pair of mutually incompatible rules + "D203", # We don’t want a blank line before a class docstring + "D213", # <> We want docstrings to start immediately after the opening triple quote + "D400", # first line should end with a period [Bug: doesn’t work with single-line docstrings] + "D401", # First line should be in imperative mood; try rephrasing + "E501", # line too long -> we accept long comment lines; formatter gets rid of long code lines + "E731", # Do not assign a lambda expression, use a def -> lambda expression assignments are convenient + "E741", # allow I, O, l as variable names -> I is the identity matrix +] +lint.per-file-ignores."*/__init__.py" = [ "F401" ] +lint.per-file-ignores."docs/*" = [ "I" ] +lint.per-file-ignores."tests/*" = [ "D" ] +lint.pydocstyle.convention = "numpy" + +[tool.pytest] +strict = true +testpaths = [ "tests" ] +addopts = [ + "--import-mode=importlib", # allow using test files with same name +] + +[tool.coverage.run] +source = [ "scanpy" ] +patch = [ "subprocess" ] +omit = [ + "**/test_*.py", +] + +[tool.cruft] +skip = [ + "tests", + "src/**/__init__.py", + "src/**/basic.py", + "docs/api.md", + "docs/changelog.md", + "docs/references.bib", + "docs/references.md", + "docs/notebooks/example.ipynb", +] diff --git a/scanpy/__init__.py b/scanpy/__init__.py deleted file mode 100644 index a304854141..0000000000 --- a/scanpy/__init__.py +++ /dev/null @@ -1,246 +0,0 @@ -# Copyright 2016-2017 F. Alexander Wolf (http://falexwolf.de). -""" -Scanpy - Single-Cell Analysis in Python -======================================= - -Defines shortcut functions for calling tools. - -Reference of Development Version --------------------------------- -Wolf & Theis, bioRxiv doi:... (2017) -""" - -# standard modules -import os -# scientific modules -from .compat.matplotlib import pyplot as pl -# scanpy modules -from . import settings as sett -from . import tools -from . import utils -from .tools import get_tool -# reexports -from .utils import read, write, read_params, transpose_ddata -from .exs import exdata, examples, example, annotate -from .tools.diffmap import diffmap -from .tools.tsne import tsne -from .tools.dpt import dpt -from .tools.difftest import difftest -from .tools.sim import sim -from .tools.preprocess import preprocess, subsample - -__all__ = [ - # example use cases - 'example', # call example - 'exdata', # show available example data - 'exs', # show available example use cases - # help - 'help', # show help for a given tool - # elementary operations - 'read', - 'write', - 'annotate', - # preprocessing - 'preprocess', - 'subsample', - # visualization - 'diffmap', - 'tsne', - # subgroup identification - 'dpt', - 'ctpaths', - # differential expression testing - 'difftest', - # simulation - 'sim' - # plotting - 'plot', - 'show', # show plots - # management - 'run' - # utils - 'transpose_ddata' -] - -def plot(*dtools,**kwargs): - """ - Plot the result of a computation with a tool. - - Parameters - ---------- - *dtools : dicts - An arbitrary number of tool dictionaries. - """ - if sett.savefigs: - if 'writekey' not in dtools[0]: - raise ValueError('Need key "writekey" in dict d' - + dtools[0]['type'],' - call sc.write first') - - toolkey = dtools[0]['type'] - - # TODO: this is not a good solution - if toolkey == 'sim': - dtools = [dtools[0]] - - get_tool(toolkey).plot(*dtools, **kwargs) - -def help(toolkey,string=False): - """ - Display help for tool. - """ - doc = get_tool(toolkey, func=True).__doc__.replace('\n ','\n') - if string: - return doc - print(doc) - -def show(): - """ - Show plots. - """ - pl.show() - -def run(toolkey, exkey, **kwargs): - """ - Run example with specified tool, do preprocessing and read/write outfiles. - - Output files store the dictionary returned by the tool. File type is - determined by variable sett.extd allowed are 'h5' (hdf5), 'xlsx' (Excel) or - 'csv' (comma separated value file). - - If called twice with the same settings the dictionary that is read from the - existing output file is returned. - - Parameters - ---------- - toolkey : str - Name of the tool. - exkey : str - Identifies the example. - kwargs : keyword arguments - """ - kwargs['exkey'] = exkey - run_args(toolkey, kwargs) - -def run_args(toolkey, args): - """ - Run specified tool, do preprocessing and read/write outfiles. - - Output files store the dictionary returned by the tool. File type is - determined by variable sett.extd allowed are 'h5' (hdf5), 'xlsx' (Excel) or - 'csv' (comma separated value file). - - If called twice with the same settings the existing output file is returned. - - Parameters - ---------- - toolkey : str - Name of the tool. - args : dict containing - exkey : str - String that identifies the example use key. - - Returns - ------- - dfunc : dict of type toolkey - dadd : dict - Additional dict used for plotting in a later step. - """ - if args['plotparams']: - if args['plotparams'][0] == 'help': - from sys import exit - exit(get_tool(toolkey).plot.__doc__) - - writekey = sett.basekey + '_' + toolkey + sett.fsig - resultfile = sett.writedir + writekey + '.' + sett.extd - paramsfile = sett.writedir + writekey + '_params.txt' - if args['logfile']: - logfile = sett.writedir + writekey + '_log.txt' - sett.logfile(logfile) - - if toolkey == 'sim': - if args['paramsfile'] != '': - params = read_params(args['paramsfile']) - else: - paramsfile_sim = 'sim/' + args['exkey'] + '_params.txt' - params = read_params(paramsfile_sim) - sett.m(0,'--> you can specify your custom params file using the option\n' - ' "--paramsfile" or provide parameters directly via "--params"') - if 'writedir' not in params: - params['writedir'] = sett.writedir + sett.basekey + '_' + toolkey - else: - ddata, exmodule = example(args['exkey'], return_module=True) - params = {} - if args['params']: - params = utils.get_params_from_list(args['params']) - # if a parameter file has been specified, load the parameter file - elif args['paramsfile'] != '': - params = read_params(args['paramsfile']) - # otherwise, load tool parameters from dexamples - else: - try: - dexample = exmodule.dexamples[args['exkey']] - params = {} - for key in dexample.keys(): - if toolkey in key: - params = dexample[key] - except: - sett.m(0, 'did not find any example parameters') - pass - - # subsampling - if args['subsample'] != 1: - ddata = subsample(ddata,args['subsample']) - - # previous tool - if 'prev' in args: - prevkey = sett.basekey + '_' + args['prev'] + sett.fsig - dprev = read(prevkey) - - # simply load resultfile - if os.path.exists(resultfile) and not sett.recompute: - dtool = read(writekey) - # call the tool resultfile - else: - # TODO: solve this in a nicer way, also get an ordered dict for params - from inspect import getcallargs - tool = get_tool(toolkey, func=True) - if toolkey == 'sim': - dtool = tool(**params) - params = getcallargs(tool, **params) - elif 'prev' in args: - dtool = tool(dprev, ddata, **params) - params = getcallargs(tool, dprev, ddata, **params) - # TODO: Would be good to name the first argument dprev_or_ddata - # in difftest, but this doesn't work - del params['dprev'] - del params['ddata'] - else: - dtool = tool(ddata, **params) - params = getcallargs(tool, ddata, **params) - del params['ddata'] - dtool['writekey'] = writekey - write(writekey, dtool) - # save a copy of the parameters to a file - utils.write_params(paramsfile, params) - - # plotting and postprocessing - plotparams = {} - if args['plotparams']: - plotparams = utils.get_params_from_list(args['plotparams']) - if toolkey == 'sim': - plot(dtool, plotparams) - else: - # post-processing specific to example and tool - postprocess = args['exkey'] + '_' + toolkey - if postprocess in dir(exmodule) and args['subsample'] == 1: - dtool = getattr(exmodule, postprocess)(dtool) - write(writekey, dtool) - # plot - plot(dtool, ddata, **plotparams) - -def read_args_run_tool(toolkey): - """ - Read arguments and run tool specified by toolkey. - """ - args = utils.read_args_tool(toolkey, exs.dexamples()) - return run_args(toolkey, args) diff --git a/scanpy/__main__.py b/scanpy/__main__.py deleted file mode 100755 index acd4c8f68f..0000000000 --- a/scanpy/__main__.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright 2016-2017 F. Alexander Wolf (http://falexwolf.de). -""" -Scanpy - Single-Cell Analysis in Python - -This is the general purpose command-line utility. -""" -# Notes -# ----- -# Regarding command-line parsing, 'Click' does not seem to be necessary at the -# moment (http://click.pocoo.org/5/). - -# this is necessary to import scanpy from within package -from __future__ import absolute_import -import argparse -from collections import OrderedDict as odict -from sys import argv, exit -import scanpy as sc - -# description of simple inquiries -dsimple = odict([ - ('exdata', 'show example data'), - ('examples', 'show example use cases'), -]) - -# description of standard tools -dtools = odict([ - ('pca', 'visualize using PCA'), - ('diffmap', 'visualize using Diffusion Map'''), - ('tsne', 'visualize using tSNE'), - ('dpt', 'perform Diffusion Pseudotime analysis'), - ('difftest', 'test for differential expression'), - ('sim', 'simulate stochastic gene expression models'), -]) - -# assemble main description -def main_descr(): - descr = '\nsimple inquiries\n----------------' - for key, help in dsimple.items(): - descr += '\n{:12}'.format(key) + help - descr += '\n\ntools\n-----' - for key, help in dtools.items(): - descr += '\n{:12}'.format(key) + help - descr += '\n\nexkey tool\n----------' - descr += ('\n{:12}'.format('exkey tool') - + 'shortcut for providing exkey for --exkey argument to tool') - return descr - -def init_main_parser(): - """ - Init subparser for each tool. - """ - - # the actual parser and parser container - main_parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter, - add_help=False) - sub_parsers = main_parser.add_subparsers(metavar='', - description=main_descr()) - - for key, help in dtools.items(): - sub_p = sub_parsers.add_parser( - key, - description=sc.help(key,string=True), - formatter_class=argparse.RawDescriptionHelpFormatter, - add_help=False) - try: - sub_p = sc.get_tool(key).add_args(sub_p) - except: - sub_p = sc.utils.add_args(sub_p) - sub_p.set_defaults(toolkey=key) - - return main_parser - -def main(): - - # check whether at least one subcommand has been supplied - if len(argv) == 1 or argv[1] == 'h' or argv[1] == '--help': - init_main_parser().print_help() - exit(0) - - # simple inquiries - if argv[1] in dsimple: - # same keys as in dsimple - func = { - 'exdata': sc.exdata, - 'examples': sc.examples - } - func[argv[1]]() - exit(0) - - # init the parsers for each tool - main_parser = init_main_parser() - - # test whether exkey is provided first - if argv[1] not in dtools: - if len(argv) > 2 and argv[2] in dtools: - exkey = argv[1] - argv[1] = argv[2] - argv[2] = exkey - else: - print('normal usage: ' + argv[0] + ' tool exkey') - print('efficient usage: ' + argv[0] + ' exkey tool') - print('help: ' + argv[0] + ' -h') - exit(0) - - args = vars(main_parser.parse_args(argv[1:])) - args = sc.sett.process_args(args) - # run Scanpy - sc.run_args(args['toolkey'], args) - -if __name__ == '__main__': - main() - diff --git a/scanpy/compat/matplotlib.py b/scanpy/compat/matplotlib.py deleted file mode 100644 index f6392b1dfa..0000000000 --- a/scanpy/compat/matplotlib.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import absolute_import # else this module tries to import itself - -import warnings - -__all__ = ['pyplot'] - -with warnings.catch_warnings(): - # import of pyplot raises warning only in Python 2 - warnings.simplefilter('ignore') - from matplotlib import pyplot diff --git a/scanpy/compat/urllib_request.py b/scanpy/compat/urllib_request.py deleted file mode 100644 index abe17bfcc7..0000000000 --- a/scanpy/compat/urllib_request.py +++ /dev/null @@ -1,6 +0,0 @@ -__all__ = ['urlretrieve'] - -try: - from urllib.request import urlretrieve -except ImportError: # Python 2 - from urllib import urlretrieve diff --git a/scanpy/exs/__init__.py b/scanpy/exs/__init__.py deleted file mode 100644 index b70a4aef15..0000000000 --- a/scanpy/exs/__init__.py +++ /dev/null @@ -1,182 +0,0 @@ -# coding: utf-8 -""" -Example Data and Example Use Cases -================================== - -From package Scanpy (https://github.com/theislab/scanpy). -Written in Python 3 (compatible with 2). -Copyright 2016-2017 F. Alexander Wolf (http://falexwolf.de). - -""" - -from . import builtin, user -from .. import utils -from .. import settings as sett - -def exdata(): - """ - Show available example data. - """ - s = utils.pretty_dict_string(dexdata()) - print(s) - -def examples(): - """ - Show available example use cases. - """ - s = utils.pretty_dict_string(dexamples()) - print(s) - -def dexdata(): - """ Example data. """ - all_dex = utils.merge_dicts(builtin.dexdata, user.dexdata) - try: - # additional possibility to add example module - from . import user_private - all_dex = utils.merge_dicts(all_dex, user_private.dexdata) - except: - pass - return all_dex - -def dexamples(): - """ Example use cases. """ - builtin_dex = utils.fill_in_datakeys(builtin.dexamples, builtin.dexdata) - user_dex = utils.fill_in_datakeys(user.dexamples, user.dexdata) - all_dex = utils.merge_dicts(builtin_dex, user_dex) - try: - # additional possibility to add example module - from . import user_private - user_private_dex = utils.fill_in_datakeys(user_private.dexamples, - user_private.dexdata) - all_dex = utils.merge_dicts(all_dex, user_private_dex) - except: - pass - return all_dex - -def example(exkey, return_module=False): - """ - Read and preprocess data for predefined example. - - Parameters - ---------- - exkey : str - Key for the example dictionary in _examples. - return_module : bool, optional - Return example module. - - Returns - ------- - ddata : dict containing - X : np.ndarray - Data array for further processing, columns correspond to genes, - rows correspond to samples. - rownames : np.ndarray - Array storing the experimental labels of samples. - colnames : np.ndarray - Array storing the names of genes. - - There might be further entries such as - groupnames_n : np.ndarray - Array of shape (number of samples) that indicates groups. - xroot : np.ndarray or int - Expression vector or index of root cell for DPT analysis. - - Returns additionally, if return_module == True: - exmodule : dict, optional - Example module. - """ - try: - _exkey = getattr(user, exkey) - exmodule = user - except AttributeError: - try: - # additional possibility to add example module - from . import user_private - _exkey = getattr(user_private, exkey) - exmodule = user_private - except AttributeError: - try: - _exkey = getattr(builtin, exkey) - exmodule = builtin - except AttributeError: - msg = ('Do not know how to run example "' + exkey + - '".\nEither define a function ' + exkey + '() ' - 'in scanpy/exs/user.py that returns a data dictionary.\n' - 'Or use one of the available examples:' - + exkeys_str()) - from sys import exit - exit(msg) - - # run the function - ddata = _exkey() - - # add exkey to ddata - ddata['exkey'] = exkey - sett.m(0, 'X has shape', ddata['X'].shape[0], 'x', ddata['X'].shape[1]) - # do sanity checks on data dictionary - ddata = check_ddata(ddata) - - if return_module: - return ddata, exmodule - else: - return ddata - -def annotate(ddata, exkey): - """ - Annotates ddata if a corresponding function is present. - """ - try: - ddata = getattr(user, exkey + '_annotate')(ddata) - except: - try: - ddata = getattr(builtin, exkey + '_annotate')(ddata) - except: - raise ValueError('Did not find separate function for annotation.\n' - 'Try calling sc.example(' + exkey + ').') - return ddata - -#------------------------------------------------------------------------------- -# Checking of data dictionary -# - Might be replaced with a data class. -#------------------------------------------------------------------------------- - -# howtos -howto_specify_subgroups = '''no key "groupnames_n" in ddata dictionary found ---> you might provide a list/1d-array of subgroup names (strings) or - integers with length = n = number of cells/samples.''' - -def check_ddata(ddata): - """ - Do sanity checks on ddata dictionary. - """ - import numpy as np - if not 'groupnames_n' in ddata: - sett.m(0, howto_specify_subgroups) - else: - try: - ddata['groupnames_n'] = np.array(ddata['groupnames_n'], dtype=int) - except: - ddata['groupnames_n'] = np.array(ddata['groupnames_n'], dtype=str) - if not 'groupnames' in ddata: - ddata['groupnames'] = np.unique(ddata['groupnames_n']) - # just an array for iterating quickly - if not 'groupmasks' in ddata: - groupmasks = [] - for groupname in ddata['groupnames']: - groupmasks.append(groupname == np.array(ddata['groupnames_n'])) - ddata['groupmasks'] = np.array(groupmasks) - # for indexing into groupnames and groupmasks - if not 'groupids' in ddata: - ddata['groupids'] = np.arange(len(ddata['groupnames']), dtype=int) - # for plotting - if not 'groupcolors' in ddata: - from ..compat.matplotlib import pyplot as pl - ddata['groupcolors'] = pl.cm.jet(pl.Normalize()(ddata['groupids'])) - sett.m(0,'groupnames in ddata', ddata['groupnames']) - return ddata - -def exkeys_str(): - str = '\n' - for k in sorted(dexamples().keys()): - str += ' ' + k + '\n' - return str diff --git a/scanpy/exs/builtin.py b/scanpy/exs/builtin.py deleted file mode 100644 index ae48140cfa..0000000000 --- a/scanpy/exs/builtin.py +++ /dev/null @@ -1,293 +0,0 @@ -""" -Example Data and Use Cases - Builtin Examples -============================================= - -Summarizes example data and use cases. - -Provides a set of functions for reading raw data, annotating the raw data and -preprocessing of the raw data. - -Attributes ----------- -dexdata: dict - Stores information about example data. -dexamples: dict - Stores information about example use cases. The keys in this dictionary also - exist as a function attribute of this module that performs data reading and - preprocessing. -""" - -# this is necessary to import scanpy from within package -from __future__ import absolute_import -# standard modules -from collections import OrderedDict as odict -# scientific modules -import numpy as np -# scanpy -import scanpy as sc -from .. import utils -from .. import settings as sett - -#-------------------------------------------------------------------------------- -# The 'dexdata dictionary' stores information about example data. -# - please respect formatting of the 'addedby' entry as -# "Initials Surname (github_name), 2016-12-15" -#-------------------------------------------------------------------------------- - -dexdata = { -'burczynski06': { - 'doi': '10.2353/jmoldx.2006.050079', - 'info': 'bulk data', - 'addedby': 'FA Wolf (falexwolf), 2016-12-15' }, -'krumsiek11': { - 'doi': '10.1371/journal.pone.0022649', - 'info': 'simulated data', - 'addedby': 'FA Wolf (falexwolf), 2016-12-15' }, -'moignard15': { - 'doi': '10.1038/nbt.3154', - 'addedby': 'FA Wolf (falexwolf), 2016-12-15' }, -'paul15': { - 'doi': '10.1016/j.cell.2015.11.013', - 'addedby': 'FA Wolf (falexwolf), 2016-12-15' }, -'toggleswitch': { - 'info': 'simulated data', - 'addedby': 'FA Wolf (falexwolf), 2016-12-15' }, -} - -#-------------------------------------------------------------------------------- -# The 'example dictionary' provides keys that are used to select a preprocessing -# method with the same name. Also, it provides information about tool parameters -# that deviate from default parameters. -# -# By default, any 'examplekey' ('exkey') is used as 'datakey'. If the -# 'examplekey' does not exist as a datakey (e.g. "paul15_alternative"), a -# datakey has to be specified. It will be used to complete dexamples with -# entries from dexdata during runtime. -# -# If you specified an example data file in dexdata above, you can also be lazy -# and omit to specify the example here. An entry will be generated -# automatically from the dexdata, assuming default settings everywhere. -#-------------------------------------------------------------------------------- - -dexamples = { -'krumsiek11': { - 'dpt': { - 'num_branchings': 2, # detect two branching points (default 1) - 'allow_branching_at_root': True }, # allow branching directly at root - 'ctpaths': { - 'k': 5, - 'num_fates': 4, # detect two branching points (default 1) - } - }, -'moignard15': { - 'ctpaths': { - 'k': 5, - 'num_fates': 2, # detect two branching points (default 1) - } - }, -'paul15': { - 'dpt/diffmap': { 'k': 20, 'knn': True }, - 'ctpaths': { - 'num_fates': 2, - 'k': 20, # increase number of neighbors (default 5) - 'knn': True }, # set a hard threshold on number of neighbors - 'difftest': { 'log': False, 'groupnames': ['GMP','MEP'] } - }, -'toggleswitch': { - 'difftest': { 'log': False } - } -} - -#-------------------------------------------------------------------------------- -# One function per example that reads, annotates and preprocesses data -# - one function 'exkey()' per 'exkey' (key in dexamples) -#-------------------------------------------------------------------------------- - -def burczynski06(): - """ - Bulk data with conditions ulcerative colitis (UC) and Crohn's disease (CD). - - The study assesses transcriptional profiles in peripheral blood mononuclear - cells from 42 healthy individuals, 59 CD patients, and 26 UC patients by - hybridization to microarrays interrogating more than 22,000 sequences. - - Reference - --------- - Burczynski ME, Peterson RL, Twine NC, Zuberek KA et al. - "Molecular classification of Crohn's disease and ulcerative colitis patients - using transcriptional profiles in peripheral blood mononuclear cells" - J Mol Diagn 8, 51 (2006). PMID:16436634. - - https://www.ncbi.nlm.nih.gov/sites/GDSbrowser?acc=GDS1615 - - Note - ---- - The function is based on a script by Kerby Shedden. - http://dept.stat.lsa.umich.edu/~kshedden/Python-Workshop/gene_expression_comparison.html - """ - filename = 'data/burczynski06/GDS1615_full.soft.gz' - url = 'ftp://ftp.ncbi.nlm.nih.gov/geo/datasets/GDS1nnn/GDS1615/soft/GDS1615_full.soft.gz' - ddata = sc.read(filename, backup_url=url) - groupnames_n = ddata['groupnames_n'] - # locations (indices) of samples for the ulcerative colitis group - locs_UC = [i for i, x in enumerate(groupnames_n) if x == 'ulcerative colitis'] - # locations (indices) of samples for the Crohn's disease group - locs_CD = [i for i, x in enumerate(groupnames_n) if x == 'Crohn\'s disease'] - grouplocs = [locs_UC, locs_CD] - # this is just a label that distinguishes the sets - ddata['grouplabels'] = ['ulcerative colitis','Crohn\'s disease'] - ddata['groupmasks'] = sc.utils.masks(grouplocs, ddata['X'].shape[0]) - # this is not actually needed - ddata['STP'] = groupnames_n - ddata['UC'] = locs_UC - ddata['CD'] = locs_CD - return ddata - -def krumsiek11(): - """ - Simulated myeloid progenitor data. - - Using a literature curated boolean network from Krumsiek et al. (2011). - - Returns - ------- - See paul15(). - """ - filename = 'write/krumsiek11_sim/sim_000000.txt' - ddata = sc.read(filename, first_column_names=True) - ddata['xroot'] = ddata['X'][0] - return ddata - -def moignard15(): - """ - Returns - ------- - See paul15. - """ - ddata = moignard15_raw() - return ddata - -def paul15(): - """ - Get preprocessed data matrix, gene names, cell names, and root cell. - - This largely follows an R tutorial by Maren Buttner. - https://github.com/theislab/scAnalysisTutorial - - Returns - ------- - ddata: dict containing - X: np.ndarray - Data array for further processing, columns correspond to genes, - rows correspond to samples. - rownames: np.ndarray - Array storing the experimental labels of samples. - colnames: np.ndarray - Array storing the names of genes. - xroot: np.ndarray - Expression vector of root cell. - """ - ddata = paul15_raw() - ddata = sc.preprocess(ddata,'log') - return ddata - -def toggleswitch(): - """ - Returns - ------- - See paul15. - """ - filename = 'write/toggleswitch_sim/sim_000000.txt' - ddata = sc.read(filename, first_column_names=True) - ddata['xroot'] = ddata['X'][0] - return ddata - -#-------------------------------------------------------------------------------- -# Optional functions for Raw Data, Annotation, Postprocessing, respectively -# - instead of having just one function per example in the section above, one we -# split this in parts for 'Raw Data', and, if wanted, tool-specific -# post-processing (e.g. annotation of groups identified by tools) -# - this is useful, if one wants to experiment with different preprocessing -# steps, all of which require the same raw data, annotation, and -# postprocessing steps -#-------------------------------------------------------------------------------- - -def moignard15_raw(): - """ - 1. Filter out a few genes. - 2. Choose 'root cell'. - 3. Define groupnames by inspecting cellnames. - """ - filename = 'data/moignard15/nbt.3154-S3.xlsx' - url = 'http://www.nature.com/nbt/journal/v33/n3/extref/nbt.3154-S3.xlsx' - ddata = sc.read(filename, sheet='dCt_values.txt', backup_url=url) - X = ddata['X'] # data matrix - genenames = ddata['colnames'] - cellnames = ddata['rownames'] - # filter genes - # filter out the 4th column (Eif2b1), the 31nd (Mrpl19), the 36th - # (Polr2a) and the 45th (last,UBC), as done by Haghverdi et al. (2016) - genes = np.r_[np.arange(0,4),np.arange(5,31),np.arange(32,36),np.arange(37,45)] - sett.m(0, 'selected',len(genes), 'genes') - ddata['X'] = X[:, genes] # filter data matrix - ddata['colnames'] = genenames[genes] # filter genenames - # choose root cell as in Haghverdi et al. (2016) - ddata['xroot'] = ddata['X'][532] # note that in Matlab/R, counting starts at 1 - # defne groupnames and groupnames_n - # coloring according to Moignard et al. (2015) experimental cell groups - groupnames = np.array(['HF', 'NP', 'PS', '4SG', '4SFG']) - groupnames_n = [] # a list with n entries (one for each sample) - for name in cellnames: - for groupname in groupnames: - if name.startswith(groupname): - groupnames_n.append(groupname) - ddata['groupnames_n'] = groupnames_n - ddata['groupnames'] = groupnames - # custom colors for each group - groupcolors = np.array(['#D7A83E', '#7AAE5D', '#497ABC', '#AF353A', '#765099']) - ddata['groupcolors'] = groupcolors - return ddata - -def moignard15_dpt(ddpt): - # switch on annotation by uncommenting the following - groupnames = ['trunk', 'undecided/endothelial', - 'endothelial', 'erythrocytes'] - ddpt['groupnames'] = [str(i) + ': ' + n for i, n in enumerate(groupnames)] - return ddpt - -def paul15_raw(): - filename = 'data/paul15/paul15.h5' - url = 'http://falexwolf.de/data/paul15.h5' - ddata = sc.read(filename, 'data.debatched', backup_url=url) - # the data has to be transposed (in the hdf5 and R files, each row - # corresponds to one gene, we use the opposite convention) - ddata = utils.transpose_ddata(ddata) - # define local variables to manipulate - X = ddata['X'] - genenames = ddata['colnames'] - # cluster assocations identified by Paul et al. - # groupnames_n = sc.read(filename,'cluster.id')['X'] - infogenenames = sc.read(filename, 'info.genes_strings')['X'] - sett.m(1,'the first 10 informative gene names are \n',infogenenames[:10]) - # just keep the first of the equivalent names for each gene - genenames = np.array([gn.split(';')[0] for gn in genenames]) - sett.m(1,'the first 10 trunkated gene names are \n',genenames[:10]) - # mask array for the informative genes - infogenes_idcs = np.array([(True if gn in infogenenames else False) - for gn in genenames]) - # restrict data array to the 3451 informative genes - X = X[:, infogenes_idcs] - genenames = genenames[infogenes_idcs] - sett.m(1,'after selecting info genes, the first 10 gene names are \n', - genenames[:10]) - # write to dict - ddata['X'] = X - ddata['colnames'] = genenames - # set root cell as in Haghverdi et al. (2016) - ddata['xroot'] = X[840] # note that in Matlab/R, counting starts at 1 - return ddata - -def paul15_dpt(ddpt): - ddpt['groupnames'] = ['','GMP','','MEP'] - return ddpt - diff --git a/scanpy/exs/user.py b/scanpy/exs/user.py deleted file mode 100644 index cddd8351d0..0000000000 --- a/scanpy/exs/user.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Example Data and Use Cases - User Examples -========================================== - -Add example data and use cases while being assured that no conflicts with -scanpy/exs/builtin.py in the master branch on GitHub arise. The latter -might be updated by other contributors. - -If you want to update the builtin Scanpy examples on GitHub, copy and paste the -example from here to there and make a pull request. - -Attributes ----------- -dexdata : dict - Stores information about example data. -dexamples : dict - Stores information about example use cases. The keys in this dictionary also - exist as a function attribute of this module that performs data reading and - preprocessing. -""" - -# this is necessary to import scanpy from within package -from __future__ import absolute_import -# scientific modules -import numpy as np -# scanpy -import scanpy as sc -from .. import utils -from .. import settings as sett - -#-------------------------------------------------------------------------------- -# The 'dexdata dictionary' stores information about example data. -#-------------------------------------------------------------------------------- - -dexdata = { -} - -#-------------------------------------------------------------------------------- -# The 'example dictionary' provides information about tool parameters -# that deviate from default parameters. -#-------------------------------------------------------------------------------- - -dexamples = { -} - -#-------------------------------------------------------------------------------- -# One function per example that reads, annotates and preprocesses data -# - one function 'exkey()' per 'exkey' -#-------------------------------------------------------------------------------- - -def myexample(): - - # get help - # help(sc.read) - - # read data from any path on your system - path_to_data = 'data/myexample/' - ddata = sc.read(path_to_data + 'myexample.csv') - - # other data reading examples - # ddata = sc.read(path_to_data + 'myexample.csv', first_column_names=True) - # ddata = sc.read(path_to_data + 'myexample.h5', sheet='countmatrix') - # ddata = sc.read(path_to_data + 'myexample.xlsx', sheet='countmatrix') - # ddata = sc.read(path_to_data + 'myexample.txt', sheet='countmatrix') - # ddata = sc.read(path_to_data + 'myexample.txt.gz', sheet='countmatrix') - # ddata = sc.read(path_to_data + 'myexample.soft.gz', sheet='countmatrix') - - # in ddata['X'], rows should correspond to samples, columns to genes - # to match this convention, transpose your data if necessary - # ddata = utils.transpose_ddata(ddata) - - # get groupnames (as strings) - dgroups = sc.read(path_to_data + 'mygroups.csv', as_strings=True) - ddata['groupnames_n'] = dgroups['X'][:, 0] - - # specify root cell - ddata['xroot'] = ddata['X'][336] - - return ddata - -#-------------------------------------------------------------------------------- -# Optional functions for Raw Data, Annotation, Postprocessing, respectively -#-------------------------------------------------------------------------------- - diff --git a/scanpy/plotting.py b/scanpy/plotting.py deleted file mode 100755 index a942d8eafe..0000000000 --- a/scanpy/plotting.py +++ /dev/null @@ -1,1047 +0,0 @@ -# coding: utf-8 -""" -Plotting -======== - -From package Scanpy (https://github.com/theislab/scanpy). -Written in Python 3 (compatible with 2). -Copyright 2016-2017 F. Alexander Wolf (http://falexwolf.de). -""" - -# standard modules -import os -import sys -import glob -import argparse -# scientific modules -import numpy as np -from .compat.matplotlib import pyplot as pl -from matplotlib import rcParams -from matplotlib import ticker -from matplotlib.figure import SubplotParams as sppars - -#-------------------------------------------------------------------------------- -# Scanpy Plotting Functions -#-------------------------------------------------------------------------------- - -def timeseries(*args,**kwargs): - """ - Plot X. See timeseries_subplot. - """ - pl.figure(figsize=(2*4,4), - subplotpars=sppars(left=0.12,right=0.98,bottom=0.13)) - timeseries_subplot(*args,**kwargs) - -def timeseries_subplot(X, - varnames=[], - highlightsX=[], - c = None, - xlabel='segments / pseudotime order', - ylabel='gene expression', - yticks=None, - xlim=None, - legend=True, - cmap='jet'): # consider changing to 'viridis' - """ - Plot X. - """ - for i in range(X.shape[1]): - pl.scatter( - np.arange(X.shape[0]),X[:,i], - marker='.', - edgecolor='face', - s=rcParams['lines.markersize'], - c=cl[i] if c is None else c, - label = (varnames[i] if len(varnames) > 0 else ''), - cmap=cmap, - ) - ylim = pl.ylim() - for ih,h in enumerate(highlightsX): - pl.plot([h,h],[ylim[0],ylim[1]], - '--',color='black') - pl.ylim(ylim) - if xlim is not None: - pl.xlim(xlim) - pl.xlabel(xlabel) - pl.ylabel(ylabel) - if yticks is not None: - pl.yticks(yticks) - if len(varnames) > 0 and legend==True: - pl.legend(frameon=False) - -def timeseries_as_heatmap(X, varnames=None, highlightsX = None): - """ - Plot timeseries as heatmap. - - Parameters - ---------- - X : np.ndarray - Data array. - varnames : array_like - Array of strings naming variables stored in columns of X. - """ - if highlightsX is None: - highlightsX = [] - if varnames is None: - varnames = [] - if len(varnames) == 0: - varnames = np.arange(X.shape[1]) - if varnames.ndim == 2: - varnames = varnames[:,0] - - # transpose X - X = X.T - minX = np.min(X) - - # insert space into X - if False: - # generate new array with highlightsX - space = 10 # integer - Xnew = np.zeros((X.shape[0], X.shape[1]+space*len(highlightsX))) - hold = 0 - _hold = 0 - space_sum = 0 - for ih, h in enumerate(highlightsX): - _h = h + space_sum - Xnew[:, _hold:_h] = X[:, hold:h] - Xnew[:, _h:_h+space] = minX * np.ones((X.shape[0], space)) - # update variables - space_sum += space - _hold = _h + space - hold = h - Xnew[:, _hold:] = X[:, hold:] - - fig = pl.figure(figsize=(1.5*4,2*4)) - im = pl.imshow(np.array(X,dtype=np.float_), aspect='auto', - interpolation='nearest', cmap='viridis') - pl.colorbar(shrink=0.5) - pl.yticks(range(X.shape[0]), varnames) - for ih,h in enumerate(highlightsX): - pl.plot([h,h], [0,X.shape[0]], - '--', color='black') - pl.xlim([0, X.shape[1]-1]) - pl.ylim([0, X.shape[0]-1]) - pl.xlabel('segments / pseudotime order') - # pl.tight_layout() - -def group(ax, igroup, dgroups, Y, layout='2d'): - """ - Plot group using representation of data Y. - """ - group = dgroups['groupmasks'][igroup] - color = dgroups['groupcolors'][igroup] - if not isinstance(color[0], str): - from matplotlib.colors import rgb2hex - color = rgb2hex(dgroups['groupcolors'][igroup]) - data = [Y[group,0], Y[group,1]] - if layout == '3d': - data.append(Y[group,2]) - markersize = 3 - ax.scatter(*data, - c=color, - edgecolors='face', - s=markersize, - alpha=1, - label=dgroups['groupnames'][igroup]) - -def scatter(Ys, - layout='2d', - subtitles=['pseudotime', 'segments', 'experimental labels'], - component_name='DC', - axlabels=None, - **kwargs): - """ - Plot scatter plot of data. - - Parameters - ---------- - Ys : np.ndarray or list of np.ndarray - Single data array or list of data arrays. Rows store observations, - columns store variables. For example X, or phi or [phi,psi]. Arrays must - be of dimension ndim=2. - c : string or np.array or list of np.array - Colors array, for example [pseudotimes] or [pseudotimes,colors_labels]. - title : str - Title of plot, default ''. - layout : str - Choose from '2d', '3d' and 'unfolded 3d', default '2d'. - cmap: - Color map - - Returns - ------- - axs : matplotlib.axis or list of matplotlib.axis - Depending on whether supplying a single array or a list of arrays, - return a single axis or a list of axes. - """ - axs = _scatter(Ys,layout=layout,subtitles=subtitles,**kwargs) - # set default axlabels - if axlabels is None: - if layout == '2d': - axlabels = [[component_name+str(i) for i in idcs] - for idcs in [[1,2] for iax in range(len(axs))]] - elif layout == '3d': - axlabels = [[component_name+str(i) for i in idcs] - for idcs in [[1,2,3] for iax in range(len(axs))]] - elif layout == 'unfolded 3d': - axlabels = [[component_name+str(i) for i in idcs] - for idcs in [[2,3],[1,2],[1,2,3],[1,3]]] - # set axlabels - bool3d = True if layout == '3d' else False - for iax,ax in enumerate(axs): - if layout == 'unfolded 3d' and iax != 2: - bool3d = False - elif layout == 'unfolded 3d' and iax == 2: - bool3d = True - if axlabels is not None: - ax.set_xlabel(axlabels[iax][0]) - ax.set_ylabel(axlabels[iax][1]) - if bool3d: - # shift the label closer to the axis - ax.set_zlabel(axlabels[iax][2],labelpad=-7) - return axs - -def _scatter(Ys, - layout='2d', - subtitles=['pseudotime', 'segments', 'experimental labels'], - c='blue', - highlights=[], - highlights_labels=[], - title='', - cmap='jet'): - # if we have a single array, transform it into a list with a single array - avail_layouts = ['2d', '3d', 'unfolded 3d'] - if layout not in avail_layouts: - raise ValueError('choose layout from',avail_layouts) - colors = c - if type(Ys) == np.ndarray: - Ys = [Ys] - if len(colors) == len(Ys[0]) or type(colors) == str: - colors = [colors] - # make a figure with panels len(colors) x len(Ys) - figsize = (4*len(colors), 4*len(Ys)) - # checks - if layout == 'unfolded 3d': - if len(Ys) != 1: - raise ValueError('use single 3d array') - if len(colors) > 1: - raise ValueError('choose a single color') - figsize = (4*2,4*2) - Y = Ys[0] - Ys = [Y[:,[1,2]], Y[:,[0,1]], Y, Y[:,[0,2]]] - # try importing Axes3D - if '3d' in layout: - from mpl_toolkits.mplot3d import Axes3D - fig = pl.figure(figsize=figsize, - subplotpars=sppars(left=0.07,right=0.98,bottom=0.08)) - fig.suptitle(title) - count = 1 - bool3d = True if layout == '3d' else False - axs = [] - for Y in Ys: - markersize = (2 if Y.shape[0] > 500 else 10) - for icolor,color in enumerate(colors): - # set up panel - if layout == 'unfolded 3d' and count != 3: - ax = fig.add_subplot(2,2,count) - bool3d = False - elif layout == 'unfolded 3d' and count == 3: - ax = fig.add_subplot(2,2,count, - projection='3d') - bool3d = True - elif layout == '2d': - ax = fig.add_subplot(len(Ys),len(colors),count) - elif layout == '3d': - ax = fig.add_subplot(len(Ys),len(colors),count, - projection='3d') - if not bool3d: - data = Y[:,0],Y[:,1] - else: - data = Y[:,0],Y[:,1],Y[:,2] - # do the plotting - if type(color) != str or 'white' != color: - ax.scatter(*data, - c=color, - edgecolors='face', - s=markersize, - cmap=cmap) - # set the subsubtitles - if icolor == 0: - ax.set_title(subtitles[0]) - if icolor == 1: - ax.set_title(subtitles[1]) - if icolor == 2: - ax.set_title(subtitles[2]) - # output highlighted data points - for iihighlight,ihighlight in enumerate(highlights): - data = [Y[ihighlight,0]],[Y[ihighlight,1]] - if bool3d: - data = [Y[ihighlight,0]],[Y[ihighlight,1]],[Y[ihighlight,2]] - ax.scatter(*data,c='black', - facecolors='black',edgecolors='black', - marker='x',s=40,zorder=20) - highlight = (highlights_labels[iihighlight] if - len(highlights_labels) > 0 - else str(ihighlight)) - # the following is a Python 2 compatibility hack - ax.text(*([d[0] for d in data]+[highlight]),zorder=20) - ax.set_xticks([]); ax.set_yticks([]) - if bool3d: - ax.set_zticks([]) - axs.append(ax) - count += 1 - # scatter.set_edgecolors = scatter.set_facecolors = lambda *args:None - return axs - -def _scatter_single(ax,Y,*args,**kwargs): - """ - Plot scatter plot of data. Just some wrapper of matplotlib.Axis.scatter. - - Parameters - ---------- - ax : matplotlib.axis - Axis to plot on. - Y : np.array - Data array, data to be plotted needs to be in the first two columns. - """ - if 's' not in kwargs: - kwargs['s'] = 2 if Y.shape[0] > 500 else 10 - if 'edgecolors' not in kwargs: - kwargs['edgecolors'] = 'face' - ax.scatter(Y[:,0],Y[:,1],**kwargs) - ax.set_xticks([]); ax.set_yticks([]) - -def ranking(drankings, ddata, nr=20): - """ - Plot ranking of genes - - Parameters - ---------- - drankings : dict containing - scoreskey : str - Key to identify scores. - scores : np.ndarray - Array of scores for genes according to which to rank them. - ddata : dict - Data dictionary. - nr : int - Number of genes. - """ - - scoreskey = drankings['scoreskey'] - - # one panel for each ranking - nr_panels = len(drankings['testnames']) - # maximal number of genes that is shown - nr_genes = nr - - def get_scores(irank): - allscores = drankings[scoreskey][irank] - allscores = np.ma.masked_invalid(allscores) - scores = allscores[drankings['genes_sorted'][irank,:nr_genes]] - scores = np.abs(scores) - return scores - - # the limits for the y axis - ymin = 1e100 - ymax = -1e100 - for irank in range(len(drankings['testnames'])): - scores = get_scores(irank) - ymin = np.min([ymin,np.min(scores)]) - ymax = np.max([ymax,np.max(scores)]) - ymax += 0.3*(ymax-ymin) - - if nr_panels <= 5: - nr_panels_y = 1 - nr_panels_x = nr_panels - else: - nr_panels_y = 2 - nr_panels_x = int(nr_panels/2+0.5) - - fig = pl.figure(figsize=(nr_panels_x*4,nr_panels_y*4)) - pl.subplots_adjust(left=0.15,top=0.9,right=0.98,bottom=0.13) - - count = 1 - for irank in range(len(drankings['testnames'])): - fig.add_subplot(nr_panels_y,nr_panels_x,count) - scores = get_scores(irank) - for ig,g in enumerate(drankings['genes_sorted'][irank,:nr_genes]): - marker = (r'\leftarrow' if drankings['zscores'][irank,g] < 0 - else r'\rightarrow') - pl.text(ig,scores[ig], - r'$ ' + marker + '$ '+ddata['colnames'][g], - color = 'red' if drankings['zscores'][irank,g] < 0 else 'green', - rotation='vertical',verticalalignment='bottom', - horizontalalignment='center', - fontsize=8) - title = drankings['testnames'][irank] - pl.title(title) - if nr_panels <= 5 or count > nr_panels_x: - pl.xlabel('ranking') - if count == 1 or count == nr_panels_x+1: - pl.ylabel(scoreskey) - else: - pl.yticks([]) - pl.ylim([ymin,ymax]) - pl.xlim(-0.9,ig+1-0.1) - count += 1 - -def arrows_transitions(ax,X,indices,weight=None): - """ - Plot arrows of transitions in data matrix. - - Parameters - ---------- - ax : matplotlib.axis - Axis object from matplotlib. - X : np.array - Data array, any representation wished (X, psi, phi, etc). - indices : array_like - Indices storing the transitions. - """ - step = 1 - width = axis_to_data(ax,0.001) - if X.shape[0] > 300: - step = 5 - width = axis_to_data(ax,0.0005) - if X.shape[0] > 500: - step = 30 - width = axis_to_data(ax,0.0001) - head_width = 10*width - for ix,x in enumerate(X): - if ix%step == 0: - X_step = X[indices[ix]] - x - # don't plot arrow of length 0 - for itrans in range(X_step.shape[0]): - alphai = 1 - widthi = width - head_widthi = head_width - if weight is not None: - alphai *= weight[ix,itrans] - widthi *= weight[ix,itrans] - if np.any(X_step[itrans,:1]): - ax.arrow(x[0], x[1], - X_step[itrans,0], X_step[itrans,1], - length_includes_head=True, - width=widthi, - head_width=head_widthi, - alpha=alphai, - color='grey') - -################################################################################ -# Helper Functions -################################################################################ - -def scale_to_zero_one(x): - """ - Take some 1d data and scale it so that min matches 0 and max 1. - """ - xscaled = x - np.min(x) - xscaled /= np.max(xscaled) - return xscaled - -def zoom(ax,xy='x',factor=1): - """ - Zoom into axis. - - Parameters - ---------- - """ - limits = ax.get_xlim() if xy == 'x' else ax.get_ylim() - new_limits = (0.5*(limits[0] + limits[1]) - + 1./factor * np.array((-0.5, 0.5)) * (limits[1] - limits[0])) - if xy == 'x': - ax.set_xlim(new_limits) - else: - ax.set_ylim(new_limits) - -def get_ax_size(ax,fig): - """ - Get axis size - - Parameters - ---------- - ax : matplotlib.axis - Axis object from matplotlib. - fig : matplotlib.Figure - Figure. - """ - bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted()) - width, height = bbox.width, bbox.height - width *= fig.dpi - height *= fig.dpi - -def axis_to_data(ax,width): - """ - For a width in axis coordinates, return the corresponding in data - coordinates. - - Parameters - ---------- - ax : matplotlib.axis - Axis object from matplotlib. - width : float - Width in xaxis coordinates. - """ - xlim = ax.get_xlim() - widthx = width*(xlim[1] - xlim[0]) - ylim = ax.get_ylim() - widthy = width*(ylim[1] - ylim[0]) - return 0.5*(widthx + widthy) - -def axis_to_data_points(ax,points_axis): - """ - Map points in axis coordinates to data coordinates. - - Uses matplotlib.transform. - - Parameters - ---------- - ax : matplotlib.axis - Axis object from matplotlib. - points_axis : np.array - Points in axis coordinates. - """ - axis_to_data = ax.transAxes + ax.transData.inverted() - return axis_to_data.transform(points_axis) - -def data_to_axis_points(ax,points_data): - """ - Map points in data coordinates to axis coordinates. - - Uses matplotlib.transform. - - Parameters - ---------- - ax : matplotlib.axis - Axis object from matplotlib. - points_axis : np.array - Points in data coordinates. - """ - data_to_axis = axis_to_data.inverted() - return data_to_axis(points_data) - - - - -#-------------------------------------------------------------------------------- -# Global Plotting Variables -#-------------------------------------------------------------------------------- - -# color dict -cDict = { - '0':'black', - # red - '1':'#8B0000', # darkred - '11':'#EE0000', # red - # orange - '2':'#DD2F00', # darkorange1 - '21':'#DD8C00', # darkorange - '22':'#FF7F50', # darkorange - '23':'#FFA500', # orange - '24':'#FFBB00', # orange - # green - '3':'#006400', # darkgreen - '31':'#556B2F', # DarkOliveGreen - '32':'#228B22', # forestgreen - '33':'#66CD00', # chartreuse3 - '34':'#42CD42', # limegreen - '35':'#7CFC00', # LawnGreen - # blue - '4':'#00008B', # darkblue - '41':'#104E8B', # DodgerBlue4 - '42':'#1874CD', # dodgerblue3 - '43':'#1E90FF', # DodgerBlue - '44':'#1E90FF', # DodgerBlue - '45':'#00BFFF', # DeepSkyBlue - # violett - '5':'#68228B', # DarkOrchid4 - '51':'#9932CC', # darkorchid - '52':'#8B008B', # darkmagenta - '53':'#FF34B3', # maroon1 - # yellow - '6':'#FFA500', # orange - '61':'#FFB90F', # DarkGoldenrod1 - '62':'#FFD700', # gold - '63':'#FFFF00', # yellow - # other - '7':'turquoise', # turquoise - '8':'#212121', # darkgrey - '81':'#424242', - '82':'grey', - '99':'white', -} -# list of colors -cl = [ cDict[k] for k in ['4','3','1','21','42','33','11', - '53','23','35','7','81','0']] -# list of colors (rainbow) -clrb = [ cDict[k] for k in ['1','11', # red (dark to light) - '2','21','23', # orange (dark to light) - '61','62', # yellow (dark to light) - '35','34','33','3', # green (light to dark) - '45','43','42','41','4', # blue (light to dark) - '5', # violet - '8' # dark grey - ]] -# standard linewidth -lw0 = rcParams['lines.linewidth'] -# list of markers -ml = ['o','s','^','d'] - -# init default values for rcParams -def init_fig_params(): - # args figure - rcParams['figure.figsize'] = (5,4) - rcParams['figure.subplot.left'] = 0.18 - rcParams['figure.subplot.right'] = 0.96 - rcParams['figure.subplot.bottom'] = 0.15 - rcParams['figure.subplot.top'] = 0.91 - - rcParams['lines.linewidth'] = 1.5 - rcParams['lines.markersize'] = 6 - rcParams['lines.markeredgewidth'] = 1 - - # args font - rcParams['font.family'] = ['Arial','Helvetica'] - fontsize = 14 - rcParams['font.size'] = fontsize - rcParams['legend.fontsize'] = 0.92*fontsize - rcParams['axes.titlesize'] = fontsize - - # legend - rcParams['legend.numpoints'] = 1 - rcParams['legend.scatterpoints'] = 1 - rcParams['legend.handlelength'] = 0.5 - - # resolution of png output - rcParams['savefig.dpi'] = 200 - -init_fig_params() - -#-------------------------------------------------------------------------------- -# For use as plotting script of data files -#-------------------------------------------------------------------------------- -if __name__ == '__main__': - - p = argparse.ArgumentParser( - description='Plot data files.') - aa = p.add_argument - aa('filenames',type=str,nargs='+', - help='filenames of datafiles (wildcards are allowed)') - aa('--type',type=str,default='standard', - help='choose type of plot [standard, matrix]') - aa('--full',dest='full',action='store_const',const=True,default=False, - help='plot all columns of the data file versus the first') - aa('--crainbow',dest='crainbow',action='store_const',const=True,default=False, - help='order colors as in rainbow') - aa('--transpose',action='store_const',const=True,default=False, - help='transpose data array before manipulating it') - aa('--scatter',dest='scatter',action='store_const',const=True,default=False, - help='use scatter plot to plot array that represents grid') - aa('--mataspect', dest='mataspect', default=[1,1,1,1,1,1], type = float, nargs='*', - help='aspect ratio for mataspect') - aa('--cshrink',dest='cshrink',type=float,default=1, - help='shrink the colorbar') - aa('--cticks',dest='cticks',type=float,default=[],nargs='*', - help='list of cticks') - aa('--yticks',dest='yticks',type=float,default=[],nargs='*', - help='list of yticks') - aa('--xticks',dest='xticks',type=float,default=[],nargs='*', - help='list of xticks') - aa('--xticklabels',dest='xticklabels',type=str,default=[],nargs='*', - help='list of xticklabels that matches xticks') - aa('--noshow',dest='noshow',action='store_const',const=True,default=False, - help='do not actually plot anything' + - '[only meaningful if computation is done instead]') - aa('--coly', dest='coly', type=int, default=[], nargs='*', - help='specify columns for the y values [1]') - aa('--colslice', dest='colslice', type=int, default=[], nargs='*', - help='if the value changes in the column colslice, then start new line') - aa('--contourlevels', dest='contourlevels', type=float, default=[], nargs='*', - help='specify contourlevels') - aa('--colx', dest='colx', type=int, default=[], nargs='*', - help='specify a column for the x values [0]') - aa('--pts', dest='pts', action='store_const', const=True, default=False, - help='use points instead of lines for display') - aa('--ls', dest='ls', default=[], type = str, nargs='*', - help='specify line style' + - '[allowed values {-}, . or list like [{-} {.} {:} {--}] ]') - aa('--lw', dest='lw', default=[1], type = float, nargs='*', - help='specify line width in list like fashion [1 1.5 1 0.7 ]') - aa('--ms', dest='ms', default=[], type = float, nargs='*', - help='marker size') - aa('--c', dest='c', default=[0], type = int, nargs='*', - help='specify color in list like fashion [1 2 1 4 ]') - aa('--zorder', dest='zorder', default=[], type = int, nargs='*', - help='specifyzorder in list like fashion [ 0 2 1 1 4 ]') - aa('--log', dest='log',action='store_const', const=True, default=False, - help='log log plot') - aa('--logy', dest='logy', action='store_const', const=True, default=False, - help='set log scale for y axis') - aa('--logx', dest='logx', action='store_const', const=True, default=False, - help='set log scale for x axis') - aa('--abs', dest='abs', action='store_const', const=True, default=False, - help='plot absolute values') - aa('--save', dest='save', default='', type=str, - help='save plot under filename specfied after save ') - aa('--legend', dest='legend', type=str, default=[], nargs='*', - help='specify the legend entries, enforce no legend by "none"') - aa('--legendloc', dest='legendloc', type=str, default='upper right', - help='specify legend location, specify "none" if no legend should be displayed') - aa('--legendframe',dest='legendframe',action='store_const',const=True,default=False, - help='put white background for legend') - aa('--handlelen', dest='handlelen', type=float, default=None, - help='specify handlelength of legend') - aa('--handletextpad', dest='handletextpad', type=float, default=None, - help='specify handletextpad') - aa('--xlabel', dest='xlabel', type=str, default='', - help='specify xlabel') - aa('--ylabel', dest='ylabel', type=str, default='', - help='specify ylabel') - aa('--clabel', dest='clabel', type=str, default='', - help='specify clabel (colorbar)') - aa('--figsize', dest='figsize', type=float, default=[], nargs=2, - help='x and y length of figure') - aa('--xlim', dest='xlim', type=float, default=[], nargs='*', - help='limits for x [expects 2 values]') - aa('--ylim', dest='ylim', type=float, default=[], nargs='*', - help='limits for y [expects 2 values]') - aa('--lb', dest='lb', type=float, default=0., - help='left border to change rcParams') - aa('--rb', dest='rb', type=float, default=0., - help='right border to change rcParams') - aa('--bb', dest='bb', type=float, default=0., - help='bottom border') - aa('--tb', dest='tb', type=float, default=0., - help='top border') - aa('--xnum', dest='xnum', action='store_const', - const=True, default=False, - help='plot versus data index') - aa('--add', dest='add', action='store_const', const=True, default=False, - help='add the first two ydata values') - aa('--label', dest='label', type=str, default=[], nargs='*', - help='specify one or more labels with position [LABEL1 posx1 posy1 LABEL2 posx2 posy2 ...]') - aa('--fs', dest='fs', type=float, default=18, - help='global font size') - aa('--labelfs', dest='labelfs', type=float, default=1.1, - help='specfiy relative label fontsize as float [default 1.1]') - aa('--legendfs', dest='legendfs', type=float, default=0.92, - help='specfiy relative label fontsize as float [default 0.92]') - - args = p.parse_args() - - filenames = args.filenames - if '*' in filenames[0]: - filenames = glob.glob(filenames[0]) - filenames = sorted(filenames) - for f in filenames: - print(f), - print - - def loadtxtH(filename): - """ return header as string """ - header='' - for line in open(filename): - if line.startswith("#"): - header += line - else: - break - return np.loadtxt(filename), header - - colx = args.colx - coly = args.coly - - if args.figsize != []: - rcParams['figure.figsize'] = args.figsize[0],args.figsize[1] - rcParams['font.size'] = args.fs - rcParams['legend.fontsize'] = args.legendfs*args.fs - - # define line styles - if len(args.ls) == 1: - lis = [ args.ls[0].strip('{}') for i in range(30)] - elif len(args.ls) > 1: - lis = [ l.strip('{}') for l in args.ls] - elif args.full: - lis = ['.' for i in range(30)] - elif args.pts: - lis = ['.','+','x','d','s','>'] - else: - lis = ['-' for i in range(30)] - - # define boundarys of image - if args.lb > 0.: rcParams['figure.subplot.left'] = args.lb - if args.rb > 0.: rcParams['figure.subplot.right'] = args.rb - if args.bb > 0.: rcParams['figure.subplot.bottom'] = args.bb - if args.tb > 0.: rcParams['figure.subplot.top'] = args.tb - - # define line width - lw0 = rcParams['lines.linewidth'] - lws = [args.lw[0]*lw0 for i in range(50)] - if len(args.lw) > 1: lws = [a*lw0 for a in args.lw] - - # define marker size - ms0 = 7 - msl = [ms0 for i in range(50)] - if args.ms != []: - msl = args.ms - - #new - if args.crainbow: - cl = clrb - if len(args.c)>1: cl = [mpl_plot.cl[i] for i in args.c] - - shiftx = 0 - shifty = 0 - scalex = 1 - scaley = 1 - - checkAllRequiredDataFilesExistent = True - - ##################################################################### - # postprocessing of plot - ##################################################################### - - def pimpaxis(): - ylabelunit = '' - if not args.log and not args.logy: - if abs(pl.ylim()[1])>10000 or (abs(pl.ylim()[1]) < 3e-2 and pl.ylim()[1] > 0): - #print(pl.ylim()[1]) - scale_pow = -int(np.floor(np.log10(pl.ylim()[1]))) - pl.gca().get_yaxis().set_major_formatter( - ticker.FuncFormatter(lambda x,p : - ("%.2f"%(x*(10**scale_pow))).rstrip('0').rstrip('.'))) - ylabelunit = r'$\times\,10^{'+str(-scale_pow)+'}$' - pl.ylabel(pl.gca().get_ylabel()+ylabelunit) - else: - # replace trailing zeros for ticks - pl.gca().get_yaxis().set_major_formatter( - ticker.FuncFormatter(lambda x,p : ("%.3f"%(x)).rstrip('0').rstrip('.'))) - if not args.logx: - pl.gca().get_xaxis().set_major_formatter( - ticker.FuncFormatter(lambda x,p : ("%.3f"%(x)).rstrip('0').rstrip('.'))) - - def postprocess(ax): - - if args.label != []: - for i in range(len(args.label)/3): - fig.text(float(args.label[3*i+1]),float(args.label[3*i+2]), - args.label[3*i],fontsize=args.labelfs*args.fs) - - if args.yticks != []: - pl.yticks(args.yticks) - - if args.xticks != []: - pl.xticks(args.xticks) - - if args.xticklabels != []: - ax.set_xticklabels(args.xticklabels) - - def showorsavefig(block=True): - - if args.save != '': - # save the python script - if args.savescript: os.system('cp ' + sys.argv[0] + ' ' + args.save[:-4]+'.py') - # make directory - if not os.path.exists(os.path.dirname(args.save)): - os.system('mkdir ' + os.path.dirname(args.save)) - # save the figure - pl.savefig(args.save,dpi=args.dpi) - # save the data - tarfile = args.save[:-4]+'.tar' - tarfileCompress = tarfile.replace('.tar','.tar.gz') - if os.path.exists(tarfileCompress) and checkAllRequiredDataFilesExistent: - print('remove tarfile and build it again') - os.system('rm ' + tarfileCompress) - # only if the tarfile does not exist (either as it is the first time - # this script is called, or as it has been removed in the preceding line) - if not os.path.exists(tarfileCompress): - # os.system('tar -rf ' + tarfile - # + ' -C '+ os.path.dirname(args.save) - # + ' ' + os.path.basename(args.save)) - for filename in filenames: - os.system('tar -rf ' + tarfile - + ' -C '+ os.path.dirname(filename) - + ' ' + os.path.basename(filename)) - # zip tarfile - os.system('gzip ' + tarfile) - else: - print('original data is no longer there, but stored in tarfile') - plotfile = args.save[:-4]+'.plot' - f = file(plotfile,'w') - string = '' - for arg in sys.argv: - string += (arg + ' ' - if (sum([c in arg for c in ['(',')','$',' ']])==0 - and arg!='') else '\"' + arg + '\" ') - # f.write('echo ' + string + '\n') - f.write(string+'\n') - f.close() - os.system('chmod +x ' + plotfile) - # os.system('tar -rf ' + tarfile + ' -C '+ os.path.dirname(plotfile) + ' ' + os.path.basename(plotfile)) - if sys.platform == "linux" or sys.platform == "linux2": - if '.pdf' == args.save[-4:]: - os.system('xpdf ' + args.save + ' & ') - else: - os.system('eog ' + args.save + ' & ') - elif sys.platform == "darwin": - os.system('open -a Preview ' + args.save) - - if not args.noshow and args.save == '': pl.show(block=block) - quit() - - def standardplot(): - """ - Generate standard plot. - """ - fig = pl.figure() - ax = pl.axes() - axMain = ax - # - data, header = loadtxtH(filenames[0]) - if len(header) > 0: - labels = header.split('\n')[-2].strip('#').split() - else: - labels = [] - if args.full: - filename = filenames[0] - if not os.path.exists(filename) and args.save!= '': - checkAllRequiredDataFilesExistent = False - tarfile = args.save[:-4]+'.tgz' - if not os.path.exists(tarfile): - tarfile = args.save[:-4]+'.tar.gz' - if not os.path.exists(tarfile): - print('missing file '+filename+' and the tarfile',tarfile) - quit() - if not os.path.exists(tarfile[:-4]): - os.system('../10_toolkit/gunzip ' + tarfile) - filename = args.save[:-4] + '/' + os.path.basename(filename) - numberOfCurves = len(data[0,:])-1 - else: - numberOfCurves = max(len(filenames),len(args.coly)) - if len(args.coly) > 1 and len(filenames) > 1 and len(args.coly) != len(filenames): - raise ValueError(('provide either one coly for all files' - +'or the same number of coly arguments as filenames')) - # - for i in range(numberOfCurves): - # run through list of filenames - if not args.full and numberOfCurves==len(filenames): - filename = filenames[i] - # for each plot, take the first filename - elif not args.full: - filename = filenames[0] - # check that all data files are there if we want to save - if not os.path.exists(filename) and args.save != '': - checkAllRequiredDataFilesExistent = False - tarfile = args.save[:-4]+'.tgz' - if not os.path.exists(tarfile): - tarfile = args.save[:-4]+'.tar.gz' - if not os.path.exists(tarfile): - print('missing file '+filename+' and the tarfile',tarfile) - quit() - if not os.path.exists(tarfile[:-4]): - os.system('../10_toolkit/gunzip ' + tarfile) - filename = args.save[:-4] + '/' + os.path.basename(filename) - # define parameters to manage data - zorder = (args.zorder[i] if i 1 - else args.colx[0] if len(args.colx) == 1 else 0) - coly, color_index = ( (args.coly[i], args.coly[i]-1) if len(args.coly) > 1 - else (args.coly[0], i) if len(args.coly) == 1 else (1, i)) - if args.full: - coly = i+1 - # load data - if not args.full: - data = np.loadtxt(filename) - # treat case of only one row in the file - if len(data.shape)==1: - data = np.array([data]) - if len(data)==0: - break - # slice data according to values provided in args.colslice - if args.colslice != []: - dold = data[0,args.colslice[0]] - itold = 0 - j = 0 - for it,d in enumerate(data[:,args.colslice[0]]): - if d != dold or it == len(data)-1: - if j == i: - print('consider slice',itold,'to',it,'value',dold) - data = np.array(data[itold : (it if it != len(data)-1 else len(data))]) - break - itold = it - j += 1 - dold = d - # transpose data if rows should be plotted - if args.transpose: - data = data.T - # plot versus integers - if not args.xnum: - datax = scalex*data[::,colx] + shiftx - if scaley != 1. or shifty != 0.: - data[::,coly] = scaley*data[::,coly] + shifty - if args.xnum: - datax = np.arange(0,len(data[:,coly]),) + shiftx - if args.abs: - data[::,coly] = abs(data[::,coly]) - # label - lb = str(i) - if i < len(args.coly): lb = coly - 1 - if i < len(args.legend): lb = args.legend[i].strip('{}') - elif len(args.legend) > 0: lb = '' - if args.legend == [] and labels != []: - lb = labels[coly] - # enforce no legend - if len(args.legend)>0 and args.legend[0] == 'none': - lb = '' - # initial definition of datay - datay = data[::,coly] - # actual plotting commands - c = cl[color_index] - mfc = 'white' - if lis[i] == '.': - mfc = c - if args.logy: - mask = abs(datay) > 1e-12 - ax.semilogy(datax[mask],datay[mask], - lis[i],ms=msl[i],c=c,lw=lws[i],label=lb,mec=c,mfc=mfc,zorder=zorder) - elif args.logx: - ax.semilogx(datax,datay, - lis[i],ms=msl[i],c=c,label=lb,mec=c,mfc=mfc,zorder=zorder) - elif args.log: - ax.loglog(datax,datay, - lis[i],c=c,lw=lws[i],label=lb,mec=c,mfc=mfc,zorder=zorder) - else: - ax.plot(datax,datay, - lis[i],ms=msl[i],c=c,lw=lws[i],label=lb,mec=c,mfc=mfc,zorder=zorder) - # post process plots (set labels ...) - if i == numberOfCurves-1: - ax.set_xlabel(args.xlabel) - ax.set_ylabel(args.ylabel) - if len(args.xlim)>0: - pl.xlim(args.xlim[0],args.xlim[1]) - if len(args.ylim)>0: - pl.ylim(args.ylim[0],args.ylim[1]) - pimpaxis() - postprocess(ax) - if args.legendloc != 'none' or args.full: - leg = axMain.legend(loc=args.legendloc,handlelength=args.handlelen, - handletextpad=args.handletextpad,frameon=args.legendframe) - frame = leg.get_frame() - frame.set_color('white') - - def matrixplot(): - for filename in args.filenames: - data = np.loadtxt(filename) - if args.transpose: - data = data.T - for i,row in enumerate(data): - data[i,i] = 0 - - pl.matshow(data) - - if args.type == 'standard': - standardplot() - else: - matrixplot() - - showorsavefig() diff --git a/scanpy/settings.py b/scanpy/settings.py deleted file mode 100644 index a97de7d246..0000000000 --- a/scanpy/settings.py +++ /dev/null @@ -1,372 +0,0 @@ -# coding: utf-8 -""" -Settings and Logfile -==================== - -From package Scanpy (https://github.com/theislab/scanpy). -Written in Python 3 (compatible with 2). -Copyright 2016-2017 F. Alexander Wolf (http://falexwolf.de). - -Sets global variables like verbosity, manages log output and timing. - -Note ----- -The very first version (tracking cpu time) of this was based on -http://stackoverflow.com/questions/1557571/how-to-get-time-of-a-python-program-execution -""" - -import atexit -from time import clock -from functools import reduce -from matplotlib import rcParams - -#-------------------------------------------------------------------------------- -# Global Settings -#-------------------------------------------------------------------------------- - -verbosity = 1 -""" Set global verbosity level, choose from {0,...,6}. """ - -suffix = '' -""" Global suffix, which is appended to basekey of output and figure files. -""" - -extd = 'h5' -""" Global file extension format for data storage. - -Allowed are 'h5' (hdf5), 'xlsx' (Excel) or 'csv' (comma separated value -file). -""" - -extf = 'pdf' -""" Global file extension for saving figures. - -Recommended are 'png' and 'pdf'. Many other formats work as well (see -matplotlib.pyplot.savefig). -""" - -recompute = False -""" Don't use the results of previous calculations. - -Recompute and overwrite previous files. -""" - -savefigs = False -""" Save plots/figures as files in directory 'figs'. - -Do not show plots/figures interactively. -""" - -autoshow = True -""" Show all plots/figures automatically if savefigs == False. - -There is no need to call sc.show() in this case. -""" - -writedir = 'write/' -""" Directory where the function scanpy.write writes to by default. -""" - -figdir = 'figs/' -""" Directory where plots are saved. -""" - -fsig = '' -""" File signature. -""" - -basekey = '' -""" Basename for file reading and writing. -""" - -#-------------------------------------------------------------------------------- -# Command-line arguments for global variables in settings -#-------------------------------------------------------------------------------- - -def add_args(p): - """ - Add arguments that affect the global variables in settings. - - Parameters - ---------- - p : argparse.ArgumentParser - Input parser. - - Returns - ------- - p : argparse.ArgumentParser - Updated parser. - """ - aa = p.add_argument_group('Subsampling speeds up computations').add_argument - aa('-s', '--subsample', - type=int, default=1, metavar='s', - help='Specify integer s > 1 if you want to use a fraction of 1/s' - ' of the data (default: %(default)d).') - aa = p.add_argument_group('Save figures').add_argument - aa('--savefigs', - type=str, default='', const='pdf', nargs='?', metavar='extf', - help='Save figures to files. With the exception of interactive sessions,' - ' and do not show interactive plots anymore.' - ' Specify the file format via the extension, e.g.' - ' "pdf" or "png". Just providing "--savefigs" will save to "pdf"' - ' (default: do not save figures).') - aa('--figdir', - type=str, default=figdir, metavar='dir', - help='Change figure directory (default: %(default)s).') - aa = p.add_argument_group('Run a tool repeatedly, to try out different parameters').add_argument - aa('-r', '--recompute', - action='store_const', default=False, const=True, - help='Recompute and overwrite result files of previous calculations.') - aa('--suffix', - type=str, default='', metavar='suffix', - help='Specify suffix to append to example key' - ' (default: "").') - aa = p.add_argument_group('General settings').add_argument - aa('-h', '--help', - action='help', - help='Show this help message and exit.') - aa('-v', '--verbosity', - type=int, default=1, metavar='v', - help='Specify v = 0 for no output and v > 1 for more output.' - ' (default: %(default)d).') - aa('--logfile', - action='store_const', default=False, const=True, - help='Write to logfile instead of to standard output.') - aa('--fileform', - type=str, default=extd, metavar='extd', - help='Specify file extension and by that' - ' file format for saving results, either "h5" or "xlsx".' - ' (default: %(default)s).') - aa('--writedir', - type=str, default=writedir, metavar='dir', - help='Change outfile directory (default: %(default)s).') - - return p - -def process_args(args): - """ - Init global variables from dictionary of arguments. - - Parameters - ---------- - args : dict - Dictionary of command-line arguments defined in add_args. - """ - - # set the arguments as global variables - global suffix - suffix = args['suffix'] - args.pop('suffix') - - global recompute - recompute = args['recompute'] - args.pop('recompute') - - global verbosity - verbosity = args['verbosity'] - args.pop('verbosity') - - global savefigs - global extf - if args['savefigs'] == '': - savefigs = False - else: - savefigs = True - extf = args['savefigs'] - args.pop('savefigs') - - global figdir - figdir = args['figdir'] + '/' - if figdir[-1] != '/': - figdir += '/' - from os import path, makedirs - if not path.exists(figdir): - makedirs(figdir) - args.pop('figdir') - - global extd - extd = args['fileform'] - args.pop('fileform') - - global writedir - writedir = args['writedir'] - if writedir[-1] != '/': - writedir += '/' - args.pop('writedir') - - # from these arguments, init further global variables - global basekey - if 'exkey' in args: - basekey = args['exkey'] + suffix - else: - basekey = 'test' + suffix - - # file signature to be appended to each filename - global fsig - if args['subsample'] != 1: - fsig += '_s{:02}'.format(args['subsample']) - - return args - -#-------------------------------------------------------------------------------- -# Output -#-------------------------------------------------------------------------------- - -def m(v=0,*msg): - """ - Write message to log output, depending on verbosity level. - - Parameters - ---------- - v : int - Verbosity level of message. - *msg : - One or more arguments to be formatted as string. Same behavior as print - function. - """ - if verbosity > v: - mi(*msg) - -def mi(*msg): - """ - Write message to log output, ignoring the verbosity level. - - Parameters - ---------- - *msg : - One or more arguments to be formatted as string. Same behavior as print - function. - """ - if logfilename == '': - # in python 3, the following works - # print(*msg) - # due to compatibility with the print statement in python 2 we choose - print(' '.join([str(m) for m in msg])) - else: - out = '' - for s in msg: - out += str(s) + ' ' - with open(logfilename) as f: - f.write(out + '\n') - -def mt(v=0,*msg): - """ - Write message to log output and show computation time. - - Depends on chosen verbosity level. - - Parameters - ---------- - v : int - Verbosity level of message. - *msg : str - One or more arguments to be formatted as string. Same behavior as print - function. - """ - if verbosity > v: - global intermediate - now = clock() - elapsed_since_start = now - start - elapsed = now - intermediate - intermediate = now - mi(_sec_to_str(elapsed),'-',*msg) - -def logfile(filename=''): - """ - Define filename of logfile. - - If not defined, log output will be to the standard output. - - Parameters - ---------- - filename : str - Filename of - """ - global logfilename, verbosity - logfilename = filename - # if providing a logfile name, automatically set verbosity to a very high level - verbosity = 5 - - -def dpi(dpi=200): - """ - Set resolution of png figures. - - Parameters - ---------- - dpi : int, optional - Resolution of png output in dots per inch. - """ - # default setting as in scanpy.plot - rcParams['savefig.dpi'] = dpi - -def jupyter(): - """ - Update figure resolution for use in jupyter notebook. - - Avoids that figures get displayed too large. To set a specific value for the - resolution, use the dpi function. - """ - dpi(60) - global autoshow - autoshow = True - -def _sec_to_str(t): - """ - Format time in seconds. - - Parameters - ---------- - t : int - Time in seconds. - """ - return "%d:%02d:%02d.%03d" % \ - reduce(lambda ll,b : divmod(ll[0],b) + ll[1:], - [(t*1000,),1000,60,60]) - -def _terminate(): - """ - Function called when program terminates. - - Similar to mt, but writes total runtime. - """ - if verbosity > 0: - now = clock() - elapsed_since_start = now - start - mi(27*"_") - mi(_sec_to_str(elapsed_since_start),'- total runtime') - -def _jupyter_deprecated(do=True): - """ - Update figure params for particular environments like jupyter. - """ - - fscale = 1 - fontsize = 14 - rcParams['savefig.dpi'] = 100 - - if do: - fscale = 0.375 - fontsize = 6 - - # figure unit length and figure scale - ful = fscale*4 - fontsize = fscale*14 - - rcParams['lines.linewidth'] = fscale*1.5 - rcParams['lines.markersize'] = fscale**2*6 - rcParams['lines.markeredgewidth'] = fscale**2*1 - - rcParams['figure.figsize'] = (1.25*ful,ful) - rcParams['font.size'] = fontsize - rcParams['legend.fontsize'] = 0.92*fontsize - rcParams['axes.titlesize'] = fontsize - -# further global variables -start = clock() # clock() is deprecated since version python version 3.3 -intermediate = start -logfilename = '' -separator = 80*"-" - -atexit.register(_terminate) - diff --git a/scanpy/tools/__init__.py b/scanpy/tools/__init__.py deleted file mode 100644 index 6b73bb9139..0000000000 --- a/scanpy/tools/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# coding: utf-8 - -from . import diffmap -from . import difftest -from . import preprocess -from . import dpt -from . import tsne -from . import sim -# development tools, not present in public scanpy, yet -try: - from . import ctpaths - from . import drawg -except ImportError: - pass - -def get_tool(toolkey, func=False): - """ - Parameters - ---------- - func : bool, optional - If True, return function, otherwise, return module. - """ - tool = globals().get(toolkey) - if tool is None: - raise NameError('tool {} not in {!r}'.format(toolkey, - dict(globals().keys()))) - if func: - return getattr(tool, toolkey) - else: - return tool diff --git a/scanpy/tools/diffmap.py b/scanpy/tools/diffmap.py deleted file mode 100644 index 3af9d2edb0..0000000000 --- a/scanpy/tools/diffmap.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright 2016-2017 F. Alexander Wolf (http://falexwolf.de). -""" -Diffusion Maps -============== - -Diffusion Maps for analysis of single-cell data. - -Reference ---------- -- Diffusion Maps: Coifman et al., PNAS 102, 7426 (2005). - -See also --------- -- Diffusion Maps applied to single-cell data: Haghverdi et al., Bioinformatics - 31, 2989 (2015). -- Diffusion Maps as a flavour of spectral clustering: von Luxburg, - arXiv:0711.0189 (2007). -""" - -# standard modules -from collections import OrderedDict as odict -# scientific modules -import matplotlib -from ..compat.matplotlib import pyplot as pl -from ..tools import dpt -from .. import utils -from .. import settings as sett -from .. import plotting as plott - -def diffmap(ddata, n_components=3, k=5, knn=False, sigma=0): - """ - Compute diffusion map embedding as of Coifman et al. (2005). - - Also implements the modifications to diffusion map introduced by Haghverdi - et al. (2016). - - Return dictionary that stores the new data representation 'Y', which - consists of the first few eigenvectors of a kernel matrix of the data, and - the eigenvalues 'evals'. - - Parameters - ---------- - ddata : dictionary containing - X : np.ndarray - Data array, rows store observations, columns covariates. - n_components : int, optional (default: 3) - The number of dimensions of the representation. - k : int, optional (default: 5) - Specify the number of nearest neighbors in the knn graph. If knn == - False, set the Gaussian kernel width to the distance of the kth - neighbor (method 'local'). - knn : bool, optional (default: False) - If True, use a hard threshold to restrict the number of neighbors to - k, that is, consider a knn graph. Otherwise, use a Gaussian Kernel - to assign low weights to neighbors more distant than the kth nearest - neighbor. - sigma : float, optional (default: 0) - If greater 0, ignore parameter 'k', but directly set a global width - of the Kernel Gaussian (method 'global'). - - Returns - ------- - ddmap : dict containing - Y : np.ndarray - Array of shape (number of samples) x (number of eigen - vectors). DiffMap representation of data, which is the right eigen - basis of transition matrix with eigenvectors as columns. - evals : np.ndarray - Array of size (number of cells). Eigenvalues of transition matrix. - """ - params = locals(); del params['ddata'] - X = ddata['X'] - diffmap, ddmap = dpt._init_DPT(X, params) - ddmap['type'] = 'diffmap' - # restrict number of components - ddmap['Y'] = ddmap['Y'][:,:params['n_components']] - return ddmap - -def plot(ddmap, ddata, - layout='2d', - legendloc='lower right', - cmap='jet', - adjust_right=0.75): # consider changing to 'viridis' - """ - Plot the results of a DPT analysis. - - Parameters - ---------- - ddmap : dict - Dict returned by diffmap tool. - ddata : dict - Data dictionary. - layout : {'2d', '3d', 'unfolded 3d'}, optional (default: '2d') - Layout of plot. - legendloc : see matplotlib.legend, optional (default: 'lower right') - Options for keyword argument 'loc'. - cmap : str (default: jet) - String denoting matplotlib color map. - """ - params = locals(); del params['ddata']; del params['ddmap'] - # highlights - highlights = [] - if False: - if 'highlights' in ddata: - highlights = ddata['highlights'] - # base figure - axs = plott.scatter(ddmap['Y'], - subtitles=['diffusion map'], - component_name='DC', - layout=params['layout'], - c='grey', - highlights=highlights, - cmap=params['cmap']) - # annotated groups - if 'groupmasks' in ddata: - for igroup, group in enumerate(ddata['groupmasks']): - plott.group(axs[0], igroup, ddata, ddmap['Y'], params['layout']) - axs[0].legend(frameon=False, loc='center left', bbox_to_anchor=(1, 0.5)) - # right margin - pl.subplots_adjust(right=params['adjust_right']) - - if sett.savefigs: - pl.savefig(sett.figdir+ddmap['writekey']+'.'+sett.extf) - elif sett.autoshow: - pl.show() - diff --git a/scanpy/tools/difftest.py b/scanpy/tools/difftest.py deleted file mode 100644 index c6abd558db..0000000000 --- a/scanpy/tools/difftest.py +++ /dev/null @@ -1,236 +0,0 @@ -# coding: utf-8 -""" -Differential Gene Expression Analysis -===================================== - -From package Scanpy (https://github.com/theislab/scanpy). -Written in Python 3 (compatible with 2). -Copyright 2016-2017 F. Alexander Wolf (http://falexwolf.de). - -This is a Beta Version of a tool for differential gene expression testing -between sets detected in previous tools. Tools such as dpt, cluster,... -""" - -from collections import OrderedDict as odict -from itertools import combinations - -import numpy as np -from scipy.stats.distributions import norm -from ..compat.matplotlib import pyplot as pl - -from .. import utils -from .. import plotting as plott -from .. import settings as sett - -def difftest(dprev, ddata=None, - groupids='all', - groupnames='all', - sig_level=0.05, - correction='Bonferroni', - log=True): - """ - Perform differential gene expression test for groups defined in dprev. - - Parameters - ---------- - dprev (or ddata) : dict containing - groupnames : np.ndarray of dtype str - Array of shape (number of groups) that names the groups. - groupnames_n : np.ndarray of dtype str - Array of shape (number of samples) that names the groups. - ddata : dict - Data dictionary containing gene names. - params: dict, optional. possible keys are - groupnames : list of str or int - Subset of names in dprev['groupnames'] to which comparison shall be - restricted. - - Returns - ------- - ddifftest : dict containing - zscores : np.ndarray - Array of shape (number of tests) x (number of genes) storing the - zscore of the each gene for each test. - testlabels : np.ndarray of dtype str - Array of shape (number of tests). Stores the labels for each test. - genes_sorted : np.ndarray - Array of shape (number of tests) x (number of genes) storing genes - sorted according the decreasing absolute value of the zscore. - """ - params = locals(); del params['ddata']; del params['dprev'] - # if ddata is empty, assume that ddata dprev also contains - # the data file elements - if not ddata: - ddata = dprev - # TODO: treat negativity explicitly - X = np.abs(ddata['X']) - # Convert X to log scale - if params['log']: - XL = np.log(X) / np.log(2) - else: - XL = X - - # select groups - groupnames = dprev['groupnames'] - groupids = list(range(len(groupnames))) - groupmasks = dprev['groupmasks'] - if params['groupnames'] != 'all': - groupnames = np.array(params['groupnames']) - groupids = np.where(np.in1d(dprev['groupnames'], groupnames))[0] - if not np.any(groupids): - sett.m(0, 'specify valid groupnames for testing, one of', - dprev['groupnames']) - from sys import exit - exit(0) - groupmasks = groupmasks[groupids] - sett.m(0, 'testing groups', groupnames, 'with ids', groupids) - - # loop over all groupmasks and compute means, variances and sample numbers in groupmasks - means = np.zeros((groupmasks.shape[0],X.shape[1])) - vars = np.zeros((groupmasks.shape[0],X.shape[1])) - ns = np.zeros(groupmasks.shape[0],dtype=int) - for igroup,group in enumerate(groupmasks): - means[igroup] = XL[group].mean(axis=0) - vars[igroup] = XL[group].var(axis=0) - ns[igroup] = np.where(group)[0].size - - ddifftest = {'type' : 'difftest'} - igroupmasks = np.arange(len(groupmasks),dtype=int) - pairs = list(combinations(igroupmasks,2)) - pvalues_all = np.zeros((len(pairs),X.shape[1])) - zscores_all = np.zeros((len(pairs),X.shape[1])) - genes_sorted = np.zeros((len(pairs),X.shape[1]),dtype=int) - ddifftest['testnames'] = [] - - # test all combinations of groups against each other - for ipair,(i,j) in enumerate(pairs): - # z-scores - denom = np.sqrt(vars[i]/ns[i] + vars[j]/ns[j]) - zeros = np.flatnonzero(denom==0) - denom[zeros] = np.nan - zscores = (means[i] - means[j]) / denom - zscores = np.ma.masked_invalid(zscores) - zscores_all[ipair] = zscores - - abszscores = np.abs(zscores) - # pvalues - if False: - pvalues = 2*norm.sf(abszscores) # two-sided test - pvalues = np.ma.masked_invalid(pvalues) - sig_genes = np.flatnonzero(pvalues < 0.05/zscores.shape[0]) - pvalues_all[ipair] = pvalues - - # sort genes according to score - genes_sorted[ipair] = np.argsort(abszscores)[::-1] - - # names - testlabel = groupnames[i] + ' vs '+ groupnames[j] - ddifftest['testnames'].append(testlabel) - if False: - ddifftest['pvalues'] = -np.log10(pvalues_all) - ddifftest['zscores'] = zscores_all - ddifftest['genes_sorted'] = genes_sorted - ddifftest['scoreskey'] = 'zscores' - return ddifftest - -def plot(ddifftest, ddata, params=None): - """ - Plot ranking of genes for all tested comparisons. - """ - plott.ranking(ddifftest, ddata) - if not sett.savefigs: - pl.show() - else: - pl.savefig(sett.figdir+ddifftest['writekey']+'.'+sett.extf) - - -def difftest_shedden(ddata=None, params=None): - """ - Perform differential gene expression test for groups defined in ddata. - - Parameters - ---------- - dgroup : dict from tool containing at least a key - groups : array of index arrays that denotes subgroups - - Returns - ------- - ddifftest : dict containing - indices : .... - - Note - ---- - The function is based on a script by Kerby Shedden. - http://dept.stat.lsa.umich.edu/~kshedden/Python-Workshop/gene_expression_comparison.html - """ - - if len(params) == 0: - params = default_params - - X = ddata['X'] - GID = ddata['colnames'] - STP = ddata['STP'] - UC = ddata['UC'] - CD = ddata['CD'] - - # Convert X to log scale - XL = np.log(X) / np.log(2) - - # Z-test statistics - MUC = XL[UC].mean(axis=0) # means in ulcerative colitis samples - MCD = XL[CD].mean(axis=0) # means in Crohn's disease samples - - VUC = XL[UC].var(axis=0) # variances in ulcerative colitis samples - VCD = XL[CD].var(axis=0) # variances in Crohn's disease samples - - nUC = len(UC) # Number of ulcerative colitis samples - nCD = len(CD) # Number of Crohn's disease samples - - Z = (MUC - MCD) / np.sqrt(VUC/nUC + VCD/nCD) - - # Split the sample in half based on marginal standard deviation of - # gene expression. - SD = X.std(axis=0) - - isdl = np.flatnonzero(SD < np.median(SD)) - isdh = np.flatnonzero(SD > np.median(SD)) - - # Split the sample in half based on the marginal mean gene expression - SD = X.mean(axis=0) - imnl = np.flatnonzero(SD < np.median(SD)) - imnh = np.flatnonzero(SD > np.median(SD)) - - # Z-score threshold under Bonferroni correction - zst = -norm.ppf(params['sig_level']/2/Z.shape[0]) - - # Find the genes that meet this condition - ii = np.flatnonzero(np.abs(Z) > zst) - - with open("out/bonferroni_genes.csv", "w") as OUT: - for i in ii: - OUT.write("%.4f,%s\n" % (Z[i], GID[i])) - - ddifftest = {} - ddifftest['indices'] = ii - - print(Z.mean()) - print(len(ii)) - - return ddifftest - -def add_args(p): - """ - Update parser. - """ - # dictionary for adding arguments - dadd_args = { - '--prev' : { - 'type' : str, - 'default' : 'dpt', - 'help' : 'Specify the "previous" tool ' - '- the one you used to generate subgroups of the ' - 'data. For example, "scct" or "dpt" (default: dpt).' - } - } - p = utils.add_args(p,dadd_args) - return p diff --git a/scanpy/tools/dpt.py b/scanpy/tools/dpt.py deleted file mode 100644 index 098cc2aa6a..0000000000 --- a/scanpy/tools/dpt.py +++ /dev/null @@ -1,1272 +0,0 @@ -# Copyright 2016-2017 F. Alexander Wolf (http://falexwolf.de). -""" -Diffusion Pseudotime Analysis -============================= - -Perform Diffusion Pseudotime analysis of an expression matrix, given the -expression vector of an "initial state" = "root cell". - -Reference ---------- -Diffusion Pseudotime: Haghverdi et al., Nature Methods 13, 3971 (2016). - -See also --------- -- Diffusion Maps: Coifman et al., PNAS 102, 7426 (2005). -- Diffusion Maps applied to single-cell data: Haghverdi et al., Bioinformatics - 31, 2989 (2015). -- Diffusion Maps as a flavour of spectral clustering: von Luxburg, - arXiv:0711.0189 (2007). -""" - -# standard modules -from collections import OrderedDict as odict -# scientific modules -import numpy as np -import scipy as sp -import matplotlib -from ..compat.matplotlib import pyplot as pl -# scanpy modules -from .. import settings as sett -from .. import plotting as plott -from .. import utils - -def dpt(ddata, num_branchings=1, k=5, knn=False, - sigma=0, allow_branching_at_root=False): - """ - Perform DPT analsysis as of Haghverdi et al. (2016). - - Reference - --------- - Diffusion Pseudotime: Haghverdi et al., Nature Methods 13, 3971 (2016). - - Parameters - ---------- - ddata : dict containing - X : np.ndarray - Data array, rows store observations, columns variables. - xroot : np.ndarray - Root of stochastic process on data points (root cell), specified - either as expression vector of shape X.shape[1] or as index. The - latter is not recommended. - num_branchings : int, optional (default: 1) - Number of branchings to detect. - k : int, optional (default: 5) - Specify the number of nearest neighbors in the knn graph. If knn == - False, set the Gaussian kernel width to the distance of the kth - neighbor (method 'local'). - knn : bool, optional (default: False) - If True, use a hard threshold to restrict the number of neighbors to - k, that is, consider a knn graph. Otherwise, use a Gaussian Kernel - to assign low weights to neighbors more distant than the kth nearest - neighbor. - sigma : float, optional (default: 0) - If greater 0, ignore parameter 'k', but directly set a global width - of the Kernel Gaussian (method 'global'). - allow_branching_at_root : bool, optional (default: False) - Allow to have branching directly at root point. - - Returns - ------- - ddpt : dict containing - pseudotimes : np.ndarray - Array of dim (number of cells) that stores the pseudotime of each - cell, that is, the DPT distance with respect to the root cell. - groupmasks : np.ndarray - Array of dim (number of groups) x (number of cells). In the rows, it - contains one-dimensional mask arrays that store the index sets that - correspond to subgroups detected by the 'branch detection' - algorithm. - groupnames : np.ndarray - Array of dimension (number of groups) that stores group names in the - order they appear in groupsmasks. - groupids_n : np.ndarray of dtype int - Array of dim (number of cells) that stores the segment=subgroup id - - an integer that indexes groupnames - of each cell. The groups might - either correspond to 'progenitor cells', 'undecided cells' or - 'branches'. - Y : np.ndarray - Array of shape (number of samples) x (number of eigen - vectors). DiffMap representation of data, which is the right eigen - basis of transition matrix with eigenvectors as columns. - evals : np.ndarray - Array of size (number of cells). Eigenvalues of transition matrix. - """ - params = locals(); del params['ddata'] - X = ddata['X'] - xroot = ddata['xroot'] - dpt, ddpt = _init_DPT(X,params) - sett.m(0,'perform Diffusion Pseudotime Analysis') - # compute M matrix of cumulative transition probabilities, - # see Haghverdi et al. (2016) - dpt.compute_M_matrix() - # compute DPT distance matrix, which we refer to as 'Ddiff' - dpt.compute_Ddiff_matrix() - # set root point, if it's a gene expression value, first locate the root - if type(xroot) == np.ndarray: - dpt.find_root(xroot) - # if it's an index, directly set the index - else: - dpt.iroot = xroot - ddpt['iroot'] = np.array([dpt.iroot]) - # pseudotimes are distances from root point - dpt.set_pseudotimes() - ddpt['pseudotimes'] = dpt.pseudotimes - # detect branchings and partition the data into segments - dpt.branchings_segments() - # as in every tool or data annotation, we define (sub)groups - ddpt['groupmasks'] = dpt.segs # array of shape (number of groups x number of samples) - # it's an array of mask arrays - ddpt['groupids_n'] = dpt.segslabels # array of shape (number of samples) - # store the group labels and default colors in the order they appear in 'groups' - ddpt['groupids'] = np.arange(len(ddpt['groupmasks']), dtype=int) - ddpt['groupnames'] = [str(i) for i in ddpt['groupids']] - # n-vector of groupnames - ddpt['groupnames_n'] = [ddpt['groupnames'][i] if i < len(ddpt['groupnames']) - else 'dontknow' - for i in ddpt['groupids_n']] - - # the ordering according to segments and pseudotimes - ddpt['indices'] = dpt.indices - ddpt['changepoints'] = dpt.changepoints - ddpt['segtips'] = dpt.segstips - # type of dict - ddpt['type'] = 'dpt' - return ddpt - -def plot(ddpt, ddata, - layout='2d', - legendloc='lower right', - cmap='jet'): # consider changing to 'viridis' - """ - Plot the results of a DPT analysis. - - Parameters - ---------- - ddpt : dict - Dict returned by DPT tool. - ddata : dict - Data dictionary. - layout : {'2d', '3d', 'unfolded 3d'}, optional (default: '2d') - Layout of plot. - legendloc : see matplotlib.legend, optional (default: 'lower right') - Options for keyword argument 'loc'. - cmap : str, optional (default: jet) - String denoting matplotlib color map. - """ - params = locals(); del params['ddata']; del params['ddpt'] - X = ddata['X'] - ddpt['groupcolors'] = pl.cm.get_cmap(params['cmap'])( - pl.Normalize()(ddpt['groupids'])) - - # color by pseudotime and by segments - colors = [ddpt['pseudotimes'], 'white'] - # coloring according to experimental labels - if 'groupmasks' in ddata: - colors.append('white') - # highlight root - highlights = list(ddpt['iroot']) - # highlight tip points of each segment - if False: - highlights = [i for l in ddpt['segtips'] for i in l if l[0] != -1] - - # a single figure for all colors using 2 diffusion components - plot_groups(ddpt, ddata, params, colors, highlights) - - # plot segments and pseudotimes - plot_segments_pseudotimes(ddpt, params['cmap']) - - # if number of genes is not too high, plot the time series - if X.shape[1] <= 11: - # plot time series as gene expression vs time - plott.timeseries(X[ddpt['indices']],ddata['colnames'], - highlightsX=ddpt['changepoints'], - xlim=[0,1.3*X.shape[0]]) - if sett.savefigs: - pl.savefig(sett.figdir+ddpt['writekey']+'_vsorder.'+sett.extf) - elif X.shape[1] < 50: - # plot time series as heatmap, as in Haghverdi et al. (2016), Fig. 1d - plott.timeseries_as_heatmap(X[ddpt['indices'],:40],ddata['colnames'], - highlightsX=ddpt['changepoints']) - if sett.savefigs: - pl.savefig(sett.figdir+ddpt['writekey']+'_heatmap.'+sett.extf) - - if not sett.savefigs and sett.autoshow: - pl.show() - - -def plot_groups(ddpt, ddata, params, colors, - highlights=[], highlights_labels=[]): - """ - Plot groups in diffusion map visualization. - """ - # base figure - axs = plott.scatter(ddpt['Y'], - subtitles=['pseudotime','segments', - 'experimental groups'], - layout=params['layout'], - c=colors, - highlights=highlights, - highlights_labels=highlights_labels, - cmap=params['cmap']) - - # dpt groups (segments) - for igroup, group in enumerate(ddpt['groupmasks']): - plott.group(axs[1], igroup, ddpt, ddpt['Y'], params['layout']) - axs[1].legend(frameon=False, loc=params['legendloc']) - - # annotated groups in data dict - if 'groupmasks' in ddata: - for igroup, group in enumerate(ddata['groupmasks']): - plott.group(axs[2], igroup, ddata, ddpt['Y'], params['layout']) - axs[2].legend(frameon=False, loc='center left', bbox_to_anchor=(1, 0.5)) - pl.subplots_adjust(right=0.87) - - if sett.savefigs: - pl.savefig(sett.figdir+ddpt['writekey']+'_diffmap.'+sett.extf) - -def plot_segments_pseudotimes(ddpt, cmap): - """ - Helper function for plot. - """ - pl.figure() - pl.subplot(211) - plott.timeseries_subplot(ddpt['groupids_n'][ddpt['indices'],np.newaxis], - c=ddpt['groupids_n'][ddpt['indices']], - highlightsX=ddpt['changepoints'], - ylabel='segments', - yticks=(np.arange(ddpt['groupmasks'].shape[0],dtype=int) if - ddpt['groupmasks'].shape[0] < 5 else None), - cmap=cmap) - pl.subplot(212) - plott.timeseries_subplot(ddpt['pseudotimes'][ddpt['indices'],np.newaxis], - c=ddpt['pseudotimes'][ddpt['indices']], - highlightsX=ddpt['changepoints'], - ylabel='pseudotime', - yticks=[0,1], - cmap=cmap) - if sett.savefigs: - pl.savefig(sett.figdir+ddpt['writekey']+'_segpt.'+sett.extf) - -def _init_DPT(X, params): - """ - Returns - ------- - dpt : DPT - Instance of DPT object. - ddmap : dict - Dictionary as returned by function diffmap.diffmap. - """ - # initialization of DPT merely computes diffusion map - dpt = DPT(X, params) - # write results to dictionary - ddmap = {} - # skip the first eigenvalue/eigenvector, it does not store information - ddmap['Y'] = dpt.rbasis[:,1:] - ddmap['evals'] = dpt.evals[1:] - return dpt, ddmap - -class DPT: - """ - Diffusion Pseudotime and Diffusion Map. - - Diffusion Pseudotime as of Haghverdi et al. (2016) and Diffusion Map as of - Coifman et al. (2005). - - Also implements the modifications to diffusion map introduced by Haghverdi - et al. (2016). - """ - - def __init__(self, X, params): - """ - Initilization of DPT computes a diffusion map. - - The corresponding class attributes are described below and can be - retrieved as needed. - - Parameters - ---------- - X : np.ndarray - Data array, where the row index distinguishes different - observations, column index distinguishes different features. - params : dict, optional - See attribute params for default keys/values. - - Writes Attributes - ----------------- - evals : np.ndarray - Eigenvalues of transition matrix - rbasis : np.ndarray - Matrix of right eigenvectors (stored in columns), also known as - 'diffusion components'. - lbasis : np.ndarray - Matrix of left eigenvectors (stored in columns). - """ - self.X = X - self.N = self.X.shape[0] - self.params = params - if self.params['sigma'] > 0: - self.params['method'] = 'global' - else: - self.params['method'] = 'local' - sett.m(0,'computing Diffusion Map with method', - '"'+self.params['method']+'"') - self.compute_transition_matrix() - # compute spectral embedding - self.embed() - - def compute_transition_matrix(self): - """ - Compute similarity matrix and transition matrix. - - Notes - ----- - In the code, the following two parameters are set. - - alpha : float - The density rescaling parameter of Coifman and Lafon (2006). Should - in all practical applications equal 1: Then only the geometry of the - data matters, not the sampling density. - zerodiagonal : bool - Set the diagonal of the transition matrix to zero as suggested by - Haghverdi et al. 2015. - - See also - -------- - Also Haghverdi et al. (2016, 2015) and Coifman and Lafon (2006) and - Coifman et al. (2005). - """ - # compute distance matrix in squared Euclidian norm - Dsq = utils.comp_distance(self.X,metric='sqeuclidean') - if self.params['method'] == 'local': - # choose sigma (width of a Gaussian kernel) according to the - # distance of the kth nearest neighbor of each point, including the - # point itself in the count - k = self.params['k'] - # deterimine the distance of the k nearest neighbors - indices = np.zeros((Dsq.shape[0],k),dtype=np.int_) - distances_sq = np.zeros((Dsq.shape[0],k),dtype=np.float_) - for irow, row in enumerate(Dsq): - # the last item is already in its sorted position as - # argpartition puts the (k-1)th element - starting to count from - # zero - in its sorted position - idcs = np.argpartition(row,k-1)[:k] - indices[irow] = idcs - distances_sq[irow] = np.sort(row[idcs]) - # choose sigma, the heuristic here often makes not much - # of a difference, but is used to reproduce the figures - # of Haghverdi et al. (2016) - if self.params['knn']: - # as the distances are not sorted except for last element - # take median - sigmas_sq = np.median(distances_sq,axis=1) - else: - # the last item is already in its sorted position as - # argpartition puts the (k-1)th element - starting to count from - # zero - in its sorted position - sigmas_sq = distances_sq[:,-1]/4 - sigmas = np.sqrt(sigmas_sq) - sett.mt(0,'determined k =',k, - 'nearest neighbors of each point') - elif self.params['method'] == 'standard': - sigmas = self.params['sigma']*np.ones(self.N) - sigmas_sq = sigmas**2 - # compute the symmetric weight matrix - Num = 2*np.multiply.outer(sigmas,sigmas) - Den = np.add.outer(sigmas_sq,sigmas_sq) - W = np.sqrt(Num/Den)*np.exp(-Dsq/Den) - # make the weight matrix sparse - if not self.params['knn']: - W[W < 1e-14] = 0 - else: - # restrict number of neighbors to k - Mask = np.zeros(Dsq.shape,dtype=bool) - for irow,row in enumerate(indices): - Mask[irow,row] = True - for j in row: - if irow not in indices[j]: - Mask[j,irow] = True - # set all entries that are not nearest neighbors to zero - W[Mask==False] = 0 - sett.mt(0,'computed W (weight matrix) with "knn" =', - self.params['knn']) - if False: - pl.matshow(W) - pl.title('$ W$') - pl.colorbar() - # zero diagonal as discussed in Haghverdi et al. (2015) - # then the kernel does not encode a notion of similarity then anymore - # and is not positive semidefinite anymore - # in practice, it doesn't matter too much - zerodiagonal = False - if zerodiagonal: - np.fill_diagonal(W, 0) - # density normalisation - # as discussed in Coifman et al. (2005) - # ensure that kernel matrix is independent of sampling density - alpha = 1 - if alpha == 0: - # nothing happens here, simply use the isotropic similarity matrix - self.K = np.array(W) - else: - # q[i] is an estimate for the sampling density at point x_i - # it's also the degree of the underlying graph - q = np.sum(W,axis=0) - # raise to power alpha - if alpha != 1: - q = q**alpha - Den = np.outer(q,q) - self.K = W/Den - sett.mt(0,'computed K (anisotropic kernel)') - if False: - pl.matshow(self.K) - pl.title('$ K$') - pl.colorbar() - # now compute the row normalization to build the transition matrix T - # and the adjoint Ktilde: both have the same spectrum - self.z = np.sum(self.K,axis=0) - # the following is the transition matrix - self.T = self.K/self.z[:,np.newaxis] - # now we need the square root of the density - self.sqrtz = np.array(np.sqrt(self.z)) - # now compute the density-normalized Kernel - # it's still symmetric - szszT = np.outer(self.sqrtz,self.sqrtz) - self.Ktilde = self.K/szszT - sett.mt(0,'computed Ktilde (normalized anistropic kernel)') - if False: - pl.matshow(self.Ktilde) - pl.title('$ \widetilde K$') - pl.colorbar() - pl.show() - - def embed(self, number=10, sym=True): - """ - Compute eigen decomposition of T. - - Parameters - ---------- - number : int - Number of eigenvalues/vectors to be computed, set number = 0 if - you need all eigenvectors. - sym : bool - Instead of computing the eigendecomposition of the assymetric - transition matrix, computed the eigendecomposition of the symmetric - Ktilde matrix. - - Writes class members - -------------------- - evals : np.ndarray - Eigenvalues of transition matrix - lbasis : np.ndarray - Matrix of left eigenvectors (stored in columns). - rbasis : np.ndarray - Matrix of right eigenvectors (stored in columns). - self.rbasis is projection of data matrix on right eigenvectors, - that is, the projection on the diffusion components. - these are simply the components of the right eigenvectors - and can directly be used for plotting. - """ - self.rbasisBool = True - # compute the spectrum - if number == 0: - w,u = np.linalg.eigh(self.Ktilde) - else: - number = min(self.Ktilde.shape[0]-1, number) - w,u = sp.sparse.linalg.eigsh(self.Ktilde, k=number) - self.evals = w[::-1] - u = u[:,::-1] - sett.mt(0,'computed Ktilde\'s eigenvalues:') - sett.m(0,self.evals) - sett.m(1,'computed',number,'eigenvalues. if you want more increase the' - 'parameter "number" or set it to zero, to compute all eigenvalues') - if sym: - self.rbasis = self.lbasis = u - else: - # The eigenvectors of T are stored in self.rbasis and self.lbasis - # and are simple trafos of the eigenvectors of Ktilde. - # rbasis and lbasis are right and left eigenvectors, respectively - self.rbasis = np.array(u/self.sqrtz[:,np.newaxis]) - self.lbasis = np.array(u*self.sqrtz[:,np.newaxis]) - # normalize in L2 norm - # note that, in contrast to that, a probability distribution - # on the graph is normalized in L1 norm - # therefore, the eigenbasis in this normalization does not correspond - # to a probability distribution on the graph - self.rbasis /= np.linalg.norm(self.rbasis,axis=0,ord=2) - self.lbasis /= np.linalg.norm(self.lbasis,axis=0,ord=2) - - def _embed(self): - """ - Checks and tests for embed. - """ - # pl.semilogy(w,'x',label=r'$ \widetilde K$') - # pl.show() - if sett.verbosity > 2: - # output of spectrum of K for comparison - w,v = np.linalg.eigh(self.K) - sett.mi('spectrum of K (kernel)') - if sett.verbosity > 3: - # direct computation of spectrum of T - w,vl,vr = sp.linalg.eig(self.T,left=True) - sett.mi('spectrum of transition matrix (should be same as of Ktilde)') - - def compute_M_matrix(self): - """ - The M matrix is the matrix that results from summing over all powers of - T in the subspace without the first eigenspace. - - See Haghverdi et al. (2016). - """ - # the projected inverse therefore is - self.M = sum([self.evals[i]/(1-self.evals[i]) - * np.outer(self.rbasis[:,i],self.lbasis[:,i]) - for i in range(1,self.evals.size)]) - sett.mt(0,'computed M matrix') - if False: - pl.matshow(self.Ktilde) - pl.title('Ktilde') - pl.colorbar() - pl.matshow(self.M) - pl.title('M') - pl.colorbar() - pl.show() - - def compute_Ddiff_matrix(self): - """ - Returns the distance matrix in the diffusion metric. - - Is based on the M matrix. self.Ddiff[self.iroot,:] stores diffusion - pseudotime as a vector. - """ - self.Dbool = True - self.Ddiff = sp.spatial.distance.pdist(self.M) - self.Ddiff = sp.spatial.distance.squareform(self.Ddiff) - sett.mt(0,'computed Ddiff distance matrix') - if False: - pl.matshow(self.Ddiff) - pl.title('Ddiff') - pl.colorbar() - pl.show() - return self.Ddiff - - def find_root(self,xroot): - """ - Determine the index of the root cell. - - Given an expression vector, find the observation index that is closest - to this vector. - - Parameters - ---------- - xroot : np.ndarray - Vector that marks the root cell, the vector storing the initial - condition, only relevant for computing pseudotime. - """ - # this is the squared distance - dsqroot = 1e10 - self.iroot = 0 - for i in range(self.N): - diff = self.X[i,:]-xroot - dsq = diff.dot(diff) - if dsq < dsqroot: - dsqroot = dsq - self.iroot = i - if np.sqrt(dsqroot) < 1e-10: - sett.m(2,'root found at machine prec') - break - sett.m(1,'sample',self.iroot,'has distance',np.sqrt(dsqroot),'from root') - return self.iroot - - def set_pseudotimes(self): - """ - Return pseudotimes with respect to root point. - """ - self.pseudotimes = self.Ddiff[self.iroot]/np.max(self.Ddiff[self.iroot]) - - def branchings_segments(self): - """ - Detect branchings and partition the data into corresponding segments. - - Detect all branchings up to params['num_branchings']. - - Writes - ------ - segs : np.ndarray - Array of dimension (number of segments) x (number of data - points). Each row stores a mask array that defines a segment. - segstips : np.ndarray - Array of dimension (number of segments) x 2. Each row stores the - indices of the two tip points of each segment. - segslabels : np.ndarray - Array of dimension (number of data points). Stores an integer label - for each segment. - """ - self.detect_branchings() - self.check_segments() - self.postprocess_segments() - self.order_segments() - self.set_segslabels() - self.order_pseudotime() - - def select_segment(self,segs,segstips): - """ - Out of a list of line segments, choose segment that has the most - distant second data point. - - Assume the distance matrix Ddiff is sorted according to seg_idcs. - Compute all the distances. - - Returns - ------- - iseg : int - Index identifying the position within the list of line segments. - tips3 : int - Positions of tips within chosen segment. - """ - scores_tips = np.zeros((len(segs),4)) - allindices = np.arange(self.N,dtype=int) - for iseg, seg in enumerate(segs): - # do not consider 'unproper segments' - if segstips[iseg][0] == -1: - continue - # restrict distance matrix to points in segment - Dseg = self.Ddiff[np.ix_(seg,seg)] - # obtain the two indices that maximize distance in the segment - # call them tips - if False: - # obtain the position within the segment by searching for - # the maximum - tips = list(np.unravel_index(np.argmax(Dseg),Dseg.shape)) - if True: - # map the global position to the position within the segment - tips = [np.where(allindices[seg] == tip)[0][0] - for tip in segstips[iseg]] - # find the third point on the segment that has maximal - # added distance from the two tip points - dseg = Dseg[tips[0]] + Dseg[tips[1]] - # add this point to tips, it's a third tip, we store it at the first - # position in an array called tips3 - tips3 = np.insert(tips,0,np.argmax(dseg)) - # compute the score as ratio of the added distance to the third tip, - # to what it would be if it were on the straight line between the - # two first tips, given by Dseg[tips[:2]] - # if we did not normalize with, there would be a danger of simply - # assigning the highest score to the longest segment - score = dseg[tips3[0]]/Dseg[tips3[1],tips3[2]] - # write result - scores_tips[iseg,0] = score - scores_tips[iseg,1:] = tips3 - iseg = np.argmax(scores_tips[:,0]) - tips3 = scores_tips[iseg,1:].astype(int) - return iseg, tips3 - - def detect_branchings(self): - """ - Detect all branchings up to params['num_branchings']. - - Writes Attributes - ----------------- - segs : np.ndarray - List of integer index arrays. - segstips : np.ndarray - List of indices of the tips of segments. - """ - sett.m(0,'detect',self.params['num_branchings'],'branchings') - # a segment is a subset of points of the data set - # it's completely defined by the indices of the points in the segment - # initialize the search for branchings with a single segment, - # that is, get the indices of the whole data set - indices_all = np.arange(self.Ddiff.shape[0],dtype=int) - # let's keep a list of segments, the first segment to add is the - # whole data set - segs = [indices_all] - # a segment can as well be defined by the two points that have maximal - # distance in the segment, the "tips" of the segment - # - # the rest of the points in the segment is then defined by demanding - # them to "be close to the line segment that connects the tips", that - # is, for such a point, the normalized added distance to both tips is - # smaller than one: - # (D[tips[0],i] + D[tips[1],i])/D[tips[0],tips[1] < 1 - # of course, this condition is fulfilled by the full cylindrical - # subspace surrounding that line segment, where the radius of the - # cylinder can be infinite - # - # if D denotes a euclidian distance matrix, a line segment is a linear - # object, and the name "line" is justified. if we take the - # diffusion-based distance matrix Ddiff, which approximates geodesic - # distance, with "line", we mean the shortest path between two points, - # which can be highly non-linear in the original space - # - # let us define the tips of the whole data set - tips_all = list(np.unravel_index(np.argmax(self.Ddiff),self.Ddiff.shape)) - # we keep a list of the tips of each segment - segstips = [tips_all] - for ibranch in range(self.params['num_branchings']): - # out of the list of segments, determine the segment - # that most strongly deviates from a straight line - # and provide the three tip points that span the triangle - # of maximally distant points - iseg, tips3 = self.select_segment(segs,segstips) - sett.m(0,'tip points',tips3,'= [third start end]') - # detect branching and update segs and segstips - segs, segstips = self.detect_branching(segs,segstips,iseg,tips3) - # store as class members - self.segs = segs - self.segstips = segstips - sett.mt(0,'finished branching detection') - - def postprocess_segments(self): - """ - Convert the format of the segment class members. - """ - # make segs a list of mask arrays, it's easier to store - # as there is a hdf5 equivalent - for iseg,seg in enumerate(self.segs): - mask = np.zeros(self.Ddiff.shape[0],dtype=bool) - mask[seg] = True - self.segs[iseg] = mask - # convert to arrays - self.segs = np.array(self.segs) - self.segstips = np.array(self.segstips) - - def check_segments(self): - """ - Perform checks on segments and sort them according to pseudotime. - """ - # find the segment that contains the root cell - for iseg,seg in enumerate(self.segs): - if self.iroot in seg: - isegroot = iseg - break - # check whether the root cell is one of the tip cells of the - # segment, if not we need to introduce a new branching, directly - # at the root cell - if self.iroot not in self.segstips[iseg]: - # if it's not exactly a tip, but very close to it, - # just keep it as it is - dist_to_root = self.Ddiff[self.iroot,self.segstips[iseg]] - # otherwise, allow branching at root - if (np.min(dist_to_root) > 0.01*self.Ddiff[tuple(self.segstips[iseg])] - and self.params['allow_branching_at_root']): - allindices = np.arange(self.N,dtype=int) - tips3_global = np.insert(self.segstips[iseg],0,self.iroot) - # map the global position to the position within the segment - tips3 = np.array([np.where(allindices[self.segs[iseg]] == tip)[0][0] - for tip in tips3_global]) - # detect branching and update self.segs and self.segstips - self.segs, self.segstips = self.detect_branching(self.segs, - self.segstips, - iseg,tips3) - - def order_segments(self): - """ - Order segments according to average pseudotime. - """ - # there are different options for computing the score - if False: - # minimum of pseudotimes in the segment - score = np.min - if True: - # average pseudotime - score = np.average - # score segments by minimal pseudotime - seg_scores = [] - for seg in self.segs: - seg_scores.append(score(self.pseudotimes[seg])) - indices = np.argsort(seg_scores) - # order segments by minimal pseudotime - self.segs = self.segs[indices] - self.segstips = self.segstips[indices] - # within segstips, order tips according to pseudotime - for itips, tips in enumerate(self.segstips): - if tips[0] != -1: - indices = np.argsort(self.pseudotimes[tips]) - self.segstips[itips] = self.segstips[itips][indices] - - def set_segslabels(self): - """ - Return a single array that stores integer segment labels. - """ - segslabels = np.zeros(self.Ddiff.shape[0],dtype=int) - for iseg,seg in enumerate(self.segs): - segslabels[seg] = iseg - self.segslabels = segslabels - - def order_pseudotime(self): - """ - Define indices that reflect segment and pseudotime order. - - Writes - ------ - indices : np.ndarray - Index array of shape n, which stores an ordering of the data points - with respect to increasing segment index and increasing pseudotime. - changepoints : np.ndarray - Index array of shape len(ssegs)-1, which stores the indices of - points where the segment index changes, with respect to the ordering - of indices. - """ - # sort indices according to segments - indices = np.argsort(self.segslabels) - segslabels = self.segslabels[indices] - # find changepoints of segments - changepoints = np.arange(indices.size-1)[np.diff(segslabels)==1]+1 - pseudotimes = self.pseudotimes[indices] - for iseg,seg in enumerate(self.segs): - # only consider one segment, it's already ordered by segment - seg_sorted = seg[indices] - # consider the pseudotimes on this segment and sort them - seg_indices = np.argsort(pseudotimes[seg_sorted]) - # within the segment, order indices according to increasing pseudotime - indices[seg_sorted] = indices[seg_sorted][seg_indices] - # define class members - self.indices = indices - self.changepoints = changepoints - - def detect_branching(self,segs,segstips,iseg,tips3): - """ - Detect branching on given segment. - - Call function _detect_branching and perform bookkeeping on segs and - segstips. - - Parameters - ---------- - segs : list of np.ndarray - Ddiff distance matrix restricted to segment. - segstips : list of np.ndarray - Stores all tip points for the segments in segs. - iseg : int - Position of segment under study in segs. - tips3 : np.ndarray - The three tip points. They form a 'triangle' that contains the data. - - Returns - ------- - segs : list of np.ndarray - Updated list of segments. - segstips : list of np.ndarray - Updated list of segstips. - """ - seg = segs[iseg] - # restrict distance matrix to points in chosen segment seg - Dseg = self.Ddiff[np.ix_(seg,seg)] - # given the three tip points and the distance matrix detect the - # branching on the segment, return the list ssegs of segments that - # are defined by splitting this segment - ssegs, ssegs_tips = self._detect_branching(Dseg,tips3) - # map back to global indices - for iseg_new,seg_new in enumerate(ssegs): - ssegs[iseg_new] = seg[seg_new] - if ssegs_tips[iseg_new][0] != -1: - ssegs_tips[iseg_new] = seg[ssegs_tips[iseg_new]] - # remove previous segment - segs.pop(iseg) - segstips.pop(iseg) - # append new segments - segs += ssegs - segstips += ssegs_tips - return segs, segstips - - def _detect_branching(self,Dseg,tips): - """ - Detect branching on given segment. - - Call function __detect_branching three times for all three orderings of - tips. Points that do not belong to the same segment in all three - orderings are assigned to a fourth segment. The latter is, by Haghverdi - et al. (2016) referred to as 'undecided cells'. - - Parameters - ---------- - Dseg : np.ndarray - Ddiff distance matrix restricted to segment. - tips : np.ndarray - The three tip points. They form a 'triangle' that contains the data. - - Returns - ------- - ssegs : list of np.ndarray - List of segments obtained from splitting the single segment defined - via the first two tip cells. - ssegstips : list of np.ndarray - List of tips of segments in ssegs. - """ - if False: - ssegs = self._detect_branching_versions(Dseg,tips) - if True: - ssegs = self._detect_branching_single(Dseg,tips) - # make sure that each data point has a unique association with a segment - masks = np.zeros((3,Dseg.shape[0]),dtype=bool) - for iseg,seg in enumerate(ssegs): - masks[iseg][seg] = True - nonunique = np.sum(masks,axis=0) > 1 - # obtain the corresponding index arrays from masks - ssegs = [] - for iseg,mask in enumerate(masks): - mask[nonunique] = False - ssegs.append(np.arange(Dseg.shape[0],dtype=int)[mask]) - # compute new tips within new segments - ssegstips = [] - for inewseg, newseg in enumerate(ssegs): - # get tip point position within segment - tip = np.where(np.arange(Dseg.shape[0])[newseg] - == tips[inewseg])[0][0] - # new tip within restricted distance matrix - secondtip = np.argmax(Dseg[np.ix_(newseg,newseg)][tip]) - # map back to position within segment - secondtip = np.arange(Dseg.shape[0])[newseg][secondtip] - # add to list - ssegstips.append([tips[inewseg],secondtip]) - # for the points that cannot be assigned to the three segments of the - # branching, hence have no tip cells, but form a subset of their own, - # add dummy tips [-1,-1] - # this is not a good solution, but it ensures that we can easily write - # to hdf5 as ssegstips can be transformed to np.ndarray with dtype = int - ssegstips.append(np.array([-1,-1])) - # the following would be preferrable, but then ssegstips results in - # a np.ndarray with dtype = object, for which there is no straight - # forward hdf5 format, a solution via masks seems too much work - # ssegstips.append(np.array([],dtype=int)) - # also add the points not associated with a clear seg to ssegs - mask = np.zeros(Dseg.shape[0],dtype=bool) - # all points assigned to segments (flatten ssegs) - mask[[i for l in ssegs for i in l]] = True - # append all the points that have not been assigned. in Haghverdi et - # al. (2016), we call them 'undecided cells' - ssegs.append(np.arange(Dseg.shape[0],dtype=int)[mask==False]) - - return ssegs, ssegstips - - def _detect_branching_single(self,Dseg,tips): - """ - Detect branching on given segment. - """ - # compute branchings using different starting points the first index of - # tips is the starting point for the other two, the order does not - # matter - ssegs = [] - # permutations of tip cells - ps = [[0,1,2], # start by computing distances from the first tip - [1,2,0], # -"- second tip - [2,0,1], # -"- third tip - ] - for i,p in enumerate(ps): - ssegs.append(self.__detect_branching(Dseg, - tips[p])[0]) - return ssegs - - def _detect_branching_versions(self,Dseg,tips): - """ - Detect branching on given segment using three different versions. - """ - # compute branchings using different starting points the first index of - # tips is the starting point for the other two, the order does not - # matter - ssegs_versions = [] - # permutations of tip cells - ps = [[0,1,2], # start by computing distances from the first tip - [1,2,0], # -"- second tip - [2,0,1], # -"- third tip - ] - # invert permutations - inv_ps = [[0,1,2], - [2,0,1], - [1,2,0], - ] - for i,p in enumerate(ps): - ssegs = self.__detect_branching(Dseg, - tips[p]) - ssegs_versions.append(np.array(ssegs)[inv_ps[i]]) - ssegs = [] - # run through all three assignments of segments, and keep - # only those assignments that were found in all three runs - for inewseg, newseg_versions in enumerate(np.array(ssegs_versions).T): - if len(newseg_versions) == 3: - newseg = np.intersect1d(np.intersect1d(newseg_versions[0], - newseg_versions[1]), - newseg_versions[2]) - else: - newseg = newseg_versions[0] - ssegs.append(newseg) - - return ssegs - - def __detect_branching(self,Dseg,tips): - """ - Detect branching on given segment. - - Compute point that maximizes kendall tau correlation of the sequences of - distances to the second and the third tip, respectively, when 'moving - away' from the first tip: tips[0]. 'Moving away' means moving in the - direction of increasing distance from the first tip. - - Parameters - ---------- - Dseg : np.ndarray - Ddiff distance matrix restricted to segment. - tips : np.ndarray - The three tip points. They form a 'triangle' that contains the data. - - Returns - ------- - ssegs : list of np.ndarray - List of segments obtained from splitting the single segment defined - via the first two tip cells. - """ - # sort distance from first tip point - idcs = np.argsort(Dseg[tips[0]]) - # then the sequence of distances Dseg[tips[0]][idcs] increases - # consider now the sequence of distances from the other - # two tip points, which only increase when being close to tips[0] - # where they become correlated - # at the point where this happens, we define a branching point - if True: - imax = self.kendall_tau_split(Dseg[tips[1]][idcs], - Dseg[tips[2]][idcs]) - if False: - # if we were in euclidian space, the following should work - # as well, but here, it doesn't because the scales in Dseg are - # highly different, one would need to write the following equation - # in terms of an ordering, such as exploited by the kendall - # correlation method above - imax = np.argmin(Dseg[tips[0]][idcs] - + Dseg[tips[1]][idcs] - + Dseg[tips[2]][idcs]) - # init list to store new segments - ssegs = [] - # first new segment: all points until, but excluding the branching point - ibranch = imax + 1 - # ibranch = int(0.95*imax) # more conservative here - ssegs.append(idcs[:ibranch]) - # define nomalized distances to tip points for the rest of the data - dist1 = Dseg[tips[1],idcs[ibranch:]]/Dseg[tips[1],idcs[ibranch-1]] - dist2 = Dseg[tips[2],idcs[ibranch:]]/Dseg[tips[2],idcs[ibranch-1]] - # assign points according to whether being closer to tip cell 1 or 2 - ssegs.append(idcs[ibranch:][dist1 <= dist2]) - ssegs.append(idcs[ibranch:][dist1 > dist2]) - - return ssegs - - def kendall_tau_split(self,a,b): - """ - Return splitting index that maximizes correlation in the sequences. - - Compute difference in Kendall tau for all splitted sequences. - - For each splitting index i, compute the difference of the two - correlation measures kendalltau(a[:i],b[:i]) and - kendalltau(a[i:],b[i:]). - - Returns the splitting index that maximizes - kendalltau(a[:i],b[:i]) - kendalltau(a[i:],b[i:]) - - Parameters - ---------- - a, b : np.ndarray - One dimensional sequences. - - Returns - ------- - i : int - Splitting index according to above description. - """ - if a.size != b.size: - raise ValueError('a and b need to have the same size') - if a.ndim != b.ndim != 1: - raise ValueError('a and b need to be one-dimensional arrays') - - min_length = 5 - n = a.size - idx_range = np.arange(min_length,a.size-min_length-1,dtype=int) - corr_coeff = np.zeros(idx_range.size) - pos_old = sp.stats.kendalltau(a[:min_length],b[:min_length])[0] - neg_old = sp.stats.kendalltau(a[min_length:],b[min_length:])[0] - for ii,i in enumerate(idx_range): - if True: - # compute differences in concordance when adding a[i] and b[i] - # to the first subsequence, and removing these elements from - # the second subsequence - diff_pos, diff_neg = self._kendall_tau_diff(a,b,i) - pos = pos_old + self._kendall_tau_add(i,diff_pos,pos_old) - neg = neg_old + self._kendall_tau_subtract(n-i,diff_neg,neg_old) - pos_old = pos - neg_old = neg - if False: - # computation using sp.stats.kendalltau, takes much longer! - # just for debugging purposes - pos = sp.stats.kendalltau(a[:i+1],b[:i+1])[0] - neg = sp.stats.kendalltau(a[i+1:],b[i+1:])[0] - if False: - # the following is much slower than using sp.stats.kendalltau, - # it is only good for debugging because it allows to compute the - # tau-a version, which does not account for ties, whereas - # sp.stats.kendalltau computes tau-b version, which accounts for - # ties - pos = sp.stats.mstats.kendalltau(a[:i],b[:i],use_ties=False)[0] - neg = sp.stats.mstats.kendalltau(a[i:],b[i:],use_ties=False)[0] - corr_coeff[ii] = pos - neg - iimax = np.argmax(corr_coeff) - imax = min_length + iimax - corr_coeff_max = corr_coeff[iimax] - if corr_coeff_max < 0.3: - sett.m(1,' -> is root itself, never obtain significant correlation') - return imax - - def _kendall_tau_add(self,len_old,diff_pos,tau_old): - """ - Compute Kendall tau delta. - - The new sequence has length len_old + 1. - - Parameters - ---------- - len_old : int - The length of the old sequence, used to compute tau_old. - diff_pos : int - Difference between concordant and non-concordant pairs. - tau_old : float - Kendall rank correlation of the old sequence. - """ - return 2./(len_old+1)*(float(diff_pos)/len_old-tau_old) - - def _kendall_tau_subtract(self,len_old,diff_neg,tau_old): - """ - Compute Kendall tau delta. - - The new sequence has length len_old - 1. - - Parameters - ---------- - len_old : int - The length of the old sequence, used to compute tau_old. - diff_neg : int - Difference between concordant and non-concordant pairs. - tau_old : float - Kendall rank correlation of the old sequence. - """ - return 2./(len_old-2)*(-float(diff_neg)/(len_old-1)+tau_old) - - def _kendall_tau_diff(self,a,b,i): - """ - Compute difference in concordance of pairs in split sequences. - - Consider splitting a and b at index i. - - Parameters - ---------- - a, b : np.ndarray - - Returns - ------- - diff_pos, diff_neg : int, int - Difference between concordant and non-concordant pairs for both - subsequences. - """ - # compute ordering relation of the single points a[i] and b[i] - # with all previous points of the sequences a and b, respectively - a_pos = np.zeros(a[:i].size,dtype=int) - a_pos[a[:i]>a[i]] = 1 - a_pos[a[:i]b[i]] = 1 - b_pos[b[:i]a[i]] = 1 - a_neg[a[i:]b[i]] = 1 - b_neg[b[i:]>> preprocess(ddata,preprocess_key) -Here, ddata is a data dictionary and preprocess_key is a string that -identifies the preprocessing function. - -Beta Version, still lacks many important cases. -""" - -import numpy as np -import scipy as sp -from .. import settings as sett -from .. import utils - -def preprocess(ddata,preprocess_key,*args,**kwargs): - """ - Preprocess data with a set of available functions in this module. - - A function is selected based on the 'preprocess key' and - is passed the optional arguments args and kwargs. - - Parameters - ---------- - ddata : dict - Data dictionary containing at least a data matrix 'X'. - preprocess_key : str - String that identifies the normalization function in this module. - - Returns - ------- - ddata : dict - Data dictionary that stores the preprocessed data matrix. - """ - - # dictionary of all globally defined functions to preprocess - # data for examples etc. - preprocess_functions = globals() - if preprocess_key in preprocess_functions: - return preprocess_functions[preprocess_key](ddata,*args,**kwargs) - else: - raise ValueError('Do not know preprocess function' + preprocess_key - + 'try one of: \n' + str(preprocess_functions)) - -# ------------------------------------------------------------------------------ -# Simple preprocessing functions -# ------------------------------------------------------------------------------ - -def filter_cells(X, min_reads): - """ - Filter out cells with total UMI count < min_reads. - - Paramaters - ---------- - X : np.ndarray - Data matrix. Rows correspond to cells and columns to genes. - min_reads : int - Minimum number of reads required for a cell to survive filtering. - - Returns - ------- - X : np.ndarray - Filtered data matrix. - cell_filter : np.ndarray - Boolean mask that reports filtering. True means that the cell is - kept. False means the cell is removed. - """ - total_counts = np.sum(X, axis=1) - cell_filter = total_counts >= min_reads - return X[cell_filter], cell_filter - -def filter_genes_cv(X, Ecutoff, cvFilter): - mean_filter = np.mean(X,axis=0)> Ecutoff - var_filter = np.std(X,axis=0) / (np.mean(X,axis=0)+.0001) > cvFilter - gene_filter = np.nonzero(np.all([mean_filter,var_filter],axis=0))[0] - return X[:,gene_filter], gene_filter - -def filter_genes_fano(X, Ecutoff, Vcutoff): - mean_filter = np.mean(X,axis=0) > Ecutoff - var_filter = np.var(X,axis=0) / (np.mean(X,axis=0)+.0001) > Vcutoff - gene_filter = np.nonzero(np.all([mean_filter,var_filter],axis=0))[0] - return X[:,gene_filter], gene_filter - -def log(ddata): - """ - Apply logarithm to pseudocounts. - - Used, for example, by Haghverdi et al. (2016), doi:10.1038/nmeth.3971. - """ - # apply logarithm to plain count data, shifted by one to avoid - # negative infinities, as done in Haghverdi et al. (2016) - ddata['X'] = np.log(ddata['X']+1) - - # if root cell is defined as expression vector - if 'xiroot' in ddata and type(ddata['xiroot']) == np.ndarray: - ddata['xiroot'] = np.log(ddata['xiroot'] + 1) - - return ddata - -def pca(X, n_components=2, exact=True): - """ - Return PCA representation of data. - - Beware, the sklearn implementation is not completely deterministic! - - Parameters - ---------- - X : np.ndarray - Data matrix. - n_components : int - Number of PCs to compute. - - Returns - ------- - Y : np.ndarray - Data projected on n_components PCs. - """ - # deal with multiple PCA implementations - try: - from sklearn.decomposition import PCA - if exact: - # run deterministic PCA - svd_solver = 'arpack' - else: - # run randomized, more efficient version - svd_solver = 'randomized' - p = PCA(n_components=n_components, svd_solver=svd_solver) - sett.m(0,'compute PCA using sklearn') - sett.m(1,'--> to speed this up, set option exact=False') - Y = p.fit_transform(X) - except ImportError: - sett.m(0,'compute PCA using fallback code\n', - '--> can be sped up by installing package scikit-learn\n', - ' or by setting the option exact=False') - Y = _pca_fallback(X, n_components=n_components, exact=exact) - return Y - -def row_norm(X, max_fraction=1, mult_with_mean=False): - """ - Normalize so that every cell has the same total read count. - - Used, for example, by Haghverdi et al. (2016) or Weinreb et al. (2016). - - Using Euclidian distance after this normalisation will yield the same result - as using cosine distance. - eucl_dist(Y1,Y2) = (Y1-Y2)^2 - = Y1^2 +Y2^2 - 2Y1*Y2 = 1 + 1 - 2 Y1*Y2 = 2*(1-(Y1*Y2)) - = 2*(1-(X1*X2)/(|X1|*|X2|)) = 2*cosine_dist(X1,X2) - - Parameters - ---------- - X : np.ndarray - Expression matrix. Rows correspond to cells and columns to genes. - max_fraction : float, optional - Only use genes that make up less than max_fraction of the total - reads in every cell. - mult_with_mean: bool, optional - Multiply the result with the mean of total counts. - - Returns - ------- - Xnormalized : np.ndarray - Normalized version of the original expression matrix. - """ - sett.m(0,'preprocess: normalizing rows of data matrix') - if max_fraction < 0 or max_fraction > 1: - raise ValueError('choose max_fraction between 0 and 1') - total_counts = np.sum(X,axis=1) - if max_fraction == 1: - X_norm = X / total_counts[:,np.newaxis] - return X_norm - # restrict computation of counts to genes that make up less than - # constrain_theshold of the total reads - tc_tiled = np.tile(total_counts[:,np.newaxis],(1,X.shape[1])) - included = np.all(X <= tc_tiled * max_fraction, axis=0) - tc_include = np.sum(X[:,included],axis=1) - tc_tiled = np.tile(tc_include[:,np.newaxis],(1,X.shape[1])) + 1e-6 - X_norm = X / tc_tiled - if mult_with_mean: - X_norm *= np.mean(total_counts) - return X_norm - -def subsample(ddata, subsample, seed=0): - """ - Subsample. - - Parameters - ---------- - ddata : data dictionary - subsample : int - Inverse fraction to sample to. - seed : int - Root to change subsampling. - - Returns - ------- - ddata : dict containing modified entries - 'rownames', 'expindices', 'explabels', 'expcolors' - """ - X, row_indices = utils.subsample(ddata['X'],subsample,seed) - for key in ['rownames']: - if key in ddata and len(ddata[key]) == ddata['X'].shape[0]: - ddata[key] = ddata[key][row_indices] - if 'groupmasks' in ddata: - ddata['groupmasks'] = ddata['groupmasks'][:,row_indices] - ddata['X'] = X - ddata['subsample'] = True - return ddata - -def zscore(X): - """ - Z-score standardize each column of X. - - Parameters - ---------- - X : np.ndarray - Data matrix. Rows correspond to cells and columns to genes. - - Returns - ------- - XZ : np.ndarray - Z-score standardized version of the data matrix. - """ - means = np.tile(np.mean(X,axis=0)[None,:],(X.shape[0],1)) - stds = np.tile(np.std(X,axis=0)[None,:],(X.shape[0],1)) - return (X - means) / (stds + .0001) - -#-------------------------------------------------------------------------------- -# Whole preprocessing workflows from the literature -#-------------------------------------------------------------------------------- - -def weinreb16(ddata): - """ - Normalization and filtering as of Weinreb et al. (2016). - - Reference - --------- - Weinreb et al., bioRxiv doi:10.1101/090332 (2016) - """ - - sett.m(0, 'preprocess: weinreb16') - - meanFilter = 0.01 - cvFilter = 2 - numPCs = 50 - - X = ddata['X'] - - # filter out cells with fewer than 1000 UMIs - # X, cell_filter = filter_cells(X,1000) - # ddata['rownames'] = ddata['rownames'][cell_filter] - - # row normalize - X = row_norm(X, max_fraction=0.05, mult_with_mean=True) - - # filter out genes with mean expression < 0.1 and coefficient of variance < - # cvFilter - _, gene_filter = filter_genes_cv(X, meanFilter, cvFilter) - - # compute zscore of filtered matrix - X_z = zscore(X[:, gene_filter]) - - # PCA - X_pca = pca(X_z, n_components=numPCs) - - ddata['X'] = X_pca - sett.m(0, 'after PCA, X has shape', - ddata['X'].shape[0], 'x', ddata['X'].shape[1]) - - return ddata - -#-------------------------------------------------------------------------------- -# Helper Functions -#-------------------------------------------------------------------------------- - -def _pca_fallback(data, n_components=2, exact=False): - # mean center the data - data -= data.mean(axis=0) - # calculate the covariance matrix - C = np.cov(data, rowvar=False) - # calculate eigenvectors & eigenvalues of the covariance matrix - # use 'eigh' rather than 'eig' since C is symmetric, - # the performance gain is substantial - if exact: - evals, evecs = np.linalg.eigh(C) - else: - evals, evecs = sp.sparse.linalg.eigsh(C, k=n_components) - # sort eigenvalues in decreasing order - idcs = np.argsort(evals)[::-1] - evecs = evecs[:, idcs] - evals = evals[idcs] - # select the first n eigenvectors (n is desired dimension - # of rescaled data array, or n_components) - evecs = evecs[:, :n_components] - # project data points on eigenvectors - return np.dot(evecs.T, data.T).T - - diff --git a/scanpy/tools/sim.py b/scanpy/tools/sim.py deleted file mode 100644 index b860605b51..0000000000 --- a/scanpy/tools/sim.py +++ /dev/null @@ -1,1185 +0,0 @@ -# coding: utf-8 -""" -Simulate Artificial Data -======================== - -From package Scanpy (https://github.com/theislab/scanpy). -Written in Python 3 (compatible with 2). -Copyright 2016-2017 F. Alexander Wolf (http://falexwolf.de). - -Simulate stochastic dynamic systems to model gene expression dynamics and -cause-effect data. - -TODO ----- -Beta Version. The code will be reorganized soon. -""" - -# standard library modules -import argparse -import os -import itertools -import collections -from collections import OrderedDict as odict -# scientific modules -import numpy as np -import scipy as sp -import scipy.optimize -import scipy.stats -from ..compat.matplotlib import pyplot as pl -# scanpy modules -from .. import utils -from .. import plotting as plott -from .. import settings as sett -# set options -np.set_printoptions(precision=14) - -def sim(model='sim/toggleswitch.txt', - tmax=100, - branching=True, - nrRealizations=2, - noiseObs=0.01, - noiseDyn=0.001, - step=1, - seed=0, - writedir=''): - """ - Sample dynamic single-cell data. - - Parameters - ---------- - model : str - Model file in models directory. - tmax : int, optional - Number of time steps per realization of time series. - branching : bool, optional - Only write realizations that constitute new branches. - nrRealizations : int, optional - Number of realizations. - noiseObs : float, optional - Observatory/Measurement noise. - noiseDyn : float, optional - Dynamic noise. - step : int, optional - Interval for saving state of system. - seed : int, optional - Seed for generation of random numbers. - writedir: str, optional - Path to directory for writing output files. - """ - params = locals() - return sample_dynamic_data(params) - -def plot(ddata): - """ - Plot results of simulation. - """ - - X = ddata['X'] - genenames = ddata['colnames'] - tmax = ddata['tmax_write'] - - nr_real = X.shape[0]/tmax - - plott.timeseries(X,genenames, - xlim=[0,1.25*X.shape[0]], - highlightsX = np.arange(tmax,nr_real*tmax,tmax), - xlabel='realizations / time steps') - if sett.savefigs: - pl.savefig(sett.figdir + sett.basekey + '_sim.' + sett.extf) - - # shuffled data - X, rows = utils.subsample(X,seed=1) - plott.timeseries(X,genenames, - xlim=[0,1.25*X.shape[0]], - highlightsX = np.arange(tmax,nr_real*tmax,tmax), - xlabel='index (arbitrary order)') - if sett.savefigs: - pl.savefig(sett.figdir + sett.basekey + '_sim_shuffled.' + sett.extf) - elif sett.autoshow: - pl.show() - -def add_args(p): - """ - Update parser with tool specific arguments. - """ - # dictionary for adding arguments - dadd_args = { - '--paramsfile': { - 'default': '', - 'metavar': 'pf', - 'type': str, - 'help': 'Specify a parameter file ' - '(default: "sim/${exkey}_params.txt")' - } - } - - p = utils.add_args(p,dadd_args) - - return p - -def sample_dynamic_data(params): - """ - Helper function. - """ - if params['writedir'] == '': - params['writedir'] = (sett.writedir + - params['model'].replace('sim/', - '').replace('.txt','_sim')) - if not os.path.exists('sim/'): - params['writedir'] = '../' + params['writedir'] - sett.m(0,'writing to directory', params['writedir']) - if not os.path.exists(params['writedir']): - os.makedirs(params['writedir']) - utils.write_params(params['writedir'] + '/params.txt', params) - # init variables - dir = params['writedir'] - modelkey = os.path.basename(params['model']).replace('.txt','') - tmax = params['tmax'] - branching = params['branching'] - noiseObs = params['noiseObs'] - noiseDyn = params['noiseDyn'] - nrRealizations = params['nrRealizations'] - step = params['step'] # step size for saving the figure - - nrSamples = 1 # how many files? - maxRestarts = 1000 - maxNrSamples = 1 - - # simple vector auto regressive process or - # hill kinetics process simulation - if 'krumsiek11' not in modelkey: - # create instance, set seed - grnsim = GRNsim(model=modelkey,params=params) - nrOffEdges_list = np.zeros(nrSamples) - for sample in range(nrSamples): - # random topology / for a given edge density - if 'hill' not in modelkey: - Coupl = np.array(grnsim.Coupl) - for sampleCoupl in range(10): - nrOffEdges = 0 - for gp in range(grnsim.dim): - for g in range(grnsim.dim): - # only consider off-diagonal edges - if g != gp: - Coupl[gp,g] = 0.7 if np.random.rand() < 0.4 else 0 - nrOffEdges += 1 if Coupl[gp,g] > 0 else 0 - else: - Coupl[gp,g] = 0.7 - # check that the coupling matrix does not have eigenvalues - # greater than 1, which would lead to an exploding var process - if max(sp.linalg.eig(Coupl)[0]) < 1: - break - nrOffEdges_list[sample] = nrOffEdges - grnsim.set_coupl(Coupl) - # init type - real = 0 - X0 = np.random.rand(grnsim.dim) - Xsamples = [] - for restart in range(nrRealizations+maxRestarts): - # slightly break symmetry in initial conditions - if 'toggleswitch' in modelkey: - X0 = (np.array([0.8 for i in range(grnsim.dim)]) - + 0.01*np.random.randn(grnsim.dim)) - X = grnsim.sim_model(tmax=tmax,X0=X0, - noiseDyn=noiseDyn) - # check branching - check = True - if branching: - check, Xsamples = _check_branching(X,Xsamples,restart) - if check: - real += 1 - grnsim.write_data(X[::step],dir=dir, - noiseObs=noiseObs, - append=(False if restart==0 else True), - branching=branching, - nrRealizations=nrRealizations) - # append some zeros - if 'zeros' in dir and real == 2: - grnsim.write_data(noiseDyn*np.random.randn(500,3),dir=dir, - noiseObs=noiseObs, - append=(False if restart==0 else True), - branching=branching, - nrRealizations=nrRealizations) - if real >= nrRealizations: - break - if False: - sett.m(0,'mean nr of offdiagonal edges',nrOffEdges_list.mean(), - 'compared to total nr',grnsim.dim*(grnsim.dim-1)/2.) - - # more complex models - else: - initType = 'random' - - dim = 11 - step = 5 - - grnsim = GRNsim(dim=dim,initType=initType,model=modelkey,params=params) - curr_nrSamples = 0 - Xsamples = [] - for sample in range(maxNrSamples): - # choose initial conditions such that branchings result - if initType == 'branch': - X0mean = grnsim.branch_init_model1(tmax) - if X0mean is None: - grnsim.set_coupl() - continue - real = 0 - for restart in range(nrRealizations+maxRestarts): - if initType == 'branch': - # vary initial conditions around mean - X0 = X0mean + (0.05*np.random.rand(dim) - 0.025*np.ones(dim)) - else: - # generate random initial conditions within [0.3,0.7] - X0 = 0.4*np.random.rand(dim)+0.3 - if modelkey in [5,6]: - X0 = np.array([0.3,0.3,0,0,0,0]) - if modelkey in [7,8,9,10]: - X0 = 0.6*np.random.rand(dim)+0.2 - X0[2:] = np.zeros(4) - if 'krumsiek11' in modelkey: - X0 = np.zeros(dim) - X0[grnsim.varNames['Gata2']] = 0.8 - X0[grnsim.varNames['Pu.1']] = 0.8 - X0[grnsim.varNames['Cebpa']] = 0.8 - X0 += 0.001*np.random.randn(dim) - if False: - switch_gene = restart - (nrRealizations - dim) - if switch_gene >= dim: - break - X0[switch_gene] = 0 if X0[switch_gene] > 0.1 else 0.8 - X = grnsim.sim_model(tmax,X0=X0, - noiseDyn=noiseDyn, - restart=restart) - # check branching - check = True - if branching: - check, Xsamples = _check_branching(X,Xsamples,restart) - if check: - real += 1 - grnsim.write_data(X[::step],dir=dir,noiseObs=noiseObs, - append=(False if restart==0 else True), - branching=branching, - nrRealizations=nrRealizations) - if real >= nrRealizations: - break - - filename = dir+'/sim_000000.txt' - ddata = utils.read_file(filename, first_column_names=True) - ddata['tmax_write'] = tmax/step - ddata['type'] = 'sim' - - dsim = ddata - - return dsim - -def write_data(X,dir='sim/test',append=False,header='', - varNames={},Adj=np.array([]),Coupl=np.array([]), - boolRules={},model='',modelType='',invTimeStep=1): - """ Write simulated data. - - Accounts for saving at the same time an ID - and a model file. - """ - # check if output directory exists - if not os.path.exists(dir): - os.makedirs(dir) - # update file with sample ids - filename = dir+'/id.txt' - if os.path.exists(filename): - f = open(filename,'r') - id = int(f.read()) + (0 if append else 1) - f.close() - else: - id = 0 - f = open(filename,'w') - id = '{:0>6}'.format(id) - f.write(str(id)) - f.close() - # dimension - dim = X.shape[1] - # write files with adjacancy and coupling matrices - if not append: - if False: - if Adj.size > 0: - # due to 'update formulation' of model, there - # is always a diagonal dependence - Adj = np.copy(Adj) - if 'hill' in model: - for i in range(Adj.shape[0]): - Adj[i,i] = 1 - np.savetxt(dir+'/adj_'+id+'.txt',Adj, - header=header, - fmt='%d') - if Coupl.size > 0: - np.savetxt(dir+'/coupl_'+id+'.txt',Coupl, - header=header, - fmt='%10.6f') - # write model file - if varNames and Coupl.size > 0: - f = open(dir+'/model_'+id+'.txt','w') - f.write('# For each "variable = ", there must be a right hand side: \n') - f.write('# either an empty string or a python-style logical expression \n') - f.write('# involving variable names, "or", "and", "(", ")". \n') - f.write('# The order of equations matters! \n') - f.write('# \n') - f.write('# modelType = ' + modelType + '\n') - f.write('# invTimeStep = '+ str(invTimeStep) + '\n') - f.write('# \n') - f.write('# boolean update rules: \n') - for rule in boolRules.items(): - f.write(rule[0] + ' = ' + rule[1] + '\n') - # write coupling via names - f.write('# coupling list: \n') - names = list(varNames.keys()) - for gp in range(dim): - for g in range(dim): - if np.abs(Coupl[gp,g]) > 1e-10: - f.write('{:10} '.format(names[gp]) - + '{:10} '.format(names[g]) - + '{:10.3} '.format(Coupl[gp,g]) + '\n') - f.close() - # write simulated data - # the binary mode option in the following line is a fix for python 3 - # variable names - if varNames: - header += '{:>2} '.format('it') - for v in varNames.keys(): - header += '{:>7} '.format(v) - f = open(dir+'/sim_'+id+'.txt','ab' if append else 'wb') - np.savetxt(f,np.c_[np.arange(0,X.shape[0]),X],header=('' if append else header), - fmt=['%4.f']+['%7.4f' for i in range(dim)]) - f.close() - -class GRNsim: - """ - Simlulation of stochastic dynamic systems. - - Main application: simulation of gene expression dynamics. - - Also standard models are implemented. - """ - - availModels = collections.OrderedDict([ - ('krumsiek11', - ('myeloid progenitor network, Krumsiek et al., PLOS One 6, e22649, \n ' - 'equations from Table 1 on page 3, doi:10.1371/journal.pone.0022649 \n')), - ('var','vector autoregressive process \n'), - ('hill','process with hill kinetics \n')]) - - writeOutputOnce = True - - def __init__(self,dim=3,model='ex0',modelType='var', - initType='random',show=False,verbosity=0, - Coupl=None,params={}): - """ - model : either string for predefined model, or directory with - a model file and a coupl matrix file - """ - self.dim = dim if Coupl is None else Coupl.shape[0] # number of nodes / dimension of system - self.maxnpar = 1 # maximal number of parents - self.p_indep = 0.4 # fraction of independent genes - self.model = model - self.modelType = modelType - self.initType = initType # string characterizing a specific initial - self.show = show - self.verbosity = verbosity - # checks - if initType not in ['branch','random']: - raise RuntimeError('initType must be either: branch, random') - read = False - if model not in self.availModels.keys(): - message = 'model not among predefined models \n' - # read from file - model = 'sim/'+model+'.txt' - if not os.path.exists(model): - model = '../' + model - if not os.path.exists(model): - message = ' cannot read model from file ' + model - message += '\n as the directory does not exist' - raise RuntimeError(message) - self.model = model - # set the coupling matrix, and with that the adjacency matrix - self.set_coupl(Coupl=Coupl) - # seed - np.random.seed(params['seed']) - # header - self.header = 'model = ' + self.model+ ' \n' - # params - self.params = params - - def sim_model(self,tmax,X0,noiseDyn=0,restart=0): - """ Simulate the model. - """ - self.noiseDyn = noiseDyn - # - X = np.zeros((tmax,self.dim)) - X[0] = X0 + noiseDyn*np.random.randn(self.dim) - # run simulation - for t in range(1,tmax): - if self.modelType == 'hill': - Xdiff = self.Xdiff_hill(X[t-1]) - elif self.modelType == 'var': - Xdiff = self.Xdiff_var(X[t-1]) - # - X[t] = X[t-1] + Xdiff - # add dynamic noise - X[t] += noiseDyn*np.random.randn(self.dim) - return X - - def Xdiff_hill(self,Xt): - """ Build Xdiff from coefficients of boolean network, - that is, using self.boolCoeff. The employed functions - are Hill type activation and deactivation functions. - - See Wittmann et al., BMC Syst. Biol. 3, 98 (2009), - doi:10.1186/1752-0509-3-98 for more details. - """ - verbosity = self.verbosity>0 and self.writeOutputOnce - self.writeOutputOnce = False - Xdiff = np.zeros(self.dim) - for ichild,child in enumerate(self.pas.keys()): - # check whether list of parents is non-empty, - # otherwise continue - if self.pas[child]: - Xdiff_syn = 0 # synthesize term - if verbosity > 0: - Xdiff_syn_str = '' - else: - continue - # loop over all tuples for which the boolean update - # rule returns true, these are stored in self.boolCoeff - for ituple,tuple in enumerate(self.boolCoeff[child]): - Xdiff_syn_tuple = 1 - Xdiff_syn_tuple_str = '' - for iv,v in enumerate(tuple): - iparent = self.varNames[self.pas[child][iv]] - x = Xt[iparent] - threshold = 0.1/np.abs(self.Coupl[ichild,iparent]) - Xdiff_syn_tuple *= self.hill_a(x,threshold) if v else self.hill_i(x,threshold) - if verbosity > 0: - Xdiff_syn_tuple_str += (('a' if v else 'i') - +'('+self.pas[child][iv]+','+'{:.2}'.format(threshold)+')') - Xdiff_syn += Xdiff_syn_tuple - if verbosity > 0: - Xdiff_syn_str += ('+' if ituple != 0 else '') + Xdiff_syn_tuple_str - # multiply with degradation term - Xdiff[ichild] = self.invTimeStep*(Xdiff_syn - Xt[ichild]) - if verbosity > 0: - Xdiff_str = (child+'_{+1}-' + child + ' = ' + str(self.invTimeStep) - + '*('+Xdiff_syn_str+'-'+child+')' ) - sett.m(0,Xdiff_str) - return Xdiff - - def Xdiff_var(self,Xt,verbosity=0): - """ - """ - # subtract the current state - Xdiff = -Xt - # add the information from the past - Xdiff += np.dot(self.Coupl,Xt) - return Xdiff - - def hill_a(self,x,threshold=0.1,power=2): - """ Activating hill function. """ - x_pow = np.power(x,power) - threshold_pow = np.power(threshold,power) - return x_pow / (x_pow + threshold_pow) - - def hill_i(self,x,threshold=0.1,power=2): - """ Inhibiting hill function. - - Is equivalent to 1-hill_a(self,x,power,threshold). - """ - x_pow = np.power(x,power) - threshold_pow = np.power(threshold,power) - return threshold_pow / (x_pow + threshold_pow) - - def nhill_a(self,x,threshold=0.1,power=2,ichild=2): - """ Normalized activating hill function. """ - x_pow = np.power(x,power) - threshold_pow = np.power(threshold,power) - return x_pow / (x_pow + threshold_pow) * (1 + threshold_pow) - - def nhill_i(self,x,threshold=0.1,power=2): - """ Normalized inhibiting hill function. - - Is equivalent to 1-nhill_a(self,x,power,threshold). - """ - x_pow = np.power(x,power) - threshold_pow = np.power(threshold,power) - return threshold_pow / (x_pow + threshold_pow) * (1 - x_pow) - - def read_model(self): - """ Read the model and the couplings from the model file. - """ - if self.verbosity > 0: - sett.m(0,'reading model',self.model) - # read model - boolRules = [] - for line in open(self.model): - if line.startswith('#') and 'modelType =' in line: - keyval = line - if '|' in line: - keyval, type = line.split('|')[:2] - self.modelType = keyval.split('=')[1].strip() - if line.startswith('#') and 'invTimeStep =' in line: - keyval = line - if '|' in line: - keyval, type = line.split('|')[:2] - self.invTimeStep = float(keyval.split('=')[1].strip()) - if not line.startswith('#'): - boolRules.append([s.strip() for s in line.split('=')]) - if line.startswith('# coupling list:'): - break - self.dim = len(boolRules) - self.boolRules = collections.OrderedDict(boolRules) - self.varNames = collections.OrderedDict([(s,i) - for i,s in enumerate(self.boolRules.keys())]) - names = self.varNames - # read couplings via names - self.Coupl = np.zeros((self.dim,self.dim)) - boolContinue = True - for line in open(self.model): #open(self.model.replace('/model','/couplList')): - if line.startswith('# coupling list:'): - boolContinue = False - if boolContinue: - continue - if not line.startswith('#'): - gps, gs, val = line.strip().split() - self.Coupl[int(names[gps]),int(names[gs])] = float(val) - # adjancecy matrices - self.Adj_signed = np.sign(self.Coupl) - self.Adj = np.abs(np.array(self.Adj_signed)) - # build bool coefficients (necessary for odefy type - # version of the discrete model) - self.build_boolCoeff() - - def set_coupl(self,Coupl=None): - """ Construct the coupling matrix (and adjacancy matrix) from predefined models - or via sampling. - """ - self.varNames = collections.OrderedDict([(str(i),i) for i in range(self.dim)]) - if (self.model not in self.availModels.keys() - and Coupl is None): - self.read_model() - elif 'var' in self.model: - # vector auto regressive process - self.Coupl = Coupl - self.boolRules = collections.OrderedDict( - [(s,'') for s in self.varNames.keys()]) - names = list(self.varNames.keys()) - for gp in range(self.dim): - pas = [] - for g in range(self.dim): - if np.abs(self.Coupl[gp,g] > 1e-10): - pas.append(names[g]) - self.boolRules[ - names[gp]] = ''.join(pas[:1] - + [' or ' + pa for pa in pas[1:]]) - self.Adj_signed = np.sign(Coupl) - elif self.model in ['6','7','8','9','10']: - self.Adj_signed = np.zeros((self.dim,self.dim)) - nr_sinknodes = 2 -# sinknodes = np.random.choice(np.arange(0,self.dim), -# size=nr_sinknodes,replace=False) - sinknodes = np.array([0,1]) - # assume sinknodes have feeback - self.Adj_signed[sinknodes,sinknodes] = np.ones(nr_sinknodes) -# # allow negative feedback -# if self.model == 10: -# plus_minus = (np.random.randint(0,2,nr_sinknodes) - 0.5)*2 -# self.Adj_signed[sinknodes,sinknodes] = plus_minus - leafnodes = np.array(sinknodes) - availnodes = np.array([i for i in range(self.dim) if i not in sinknodes]) -# sett.m(0,leafnodes,availnodes) - while len(availnodes) != 0: - # parent - parent_idx = np.random.choice(np.arange(0,len(leafnodes)), - size=1,replace=False) - parent = leafnodes[parent_idx] - # children - children_ids = np.random.choice(np.arange(0,len(availnodes)), - size=2,replace=False) - children = availnodes[children_ids] - sett.m(0,parent,children) - self.Adj_signed[children,parent] = np.ones(2) - if self.model == 8: - self.Adj_signed[children[0],children[1]] = -1 - if self.model in [9,10]: - self.Adj_signed[children[0],children[1]] = -1 - self.Adj_signed[children[1],children[0]] = -1 - # update leafnodes - leafnodes = np.delete(leafnodes,parent_idx) - leafnodes = np.append(leafnodes,children) - # update availnodes - availnodes = np.delete(availnodes,children_ids) -# sett.m(0,availnodes) -# sett.m(0,leafnodes) -# sett.m(0,self.Adj) -# sett.m(0,'-') - else: - self.Adj = np.zeros((self.dim,self.dim)) - for i in range(self.dim): - indep = np.random.binomial(1,self.p_indep) - if indep == 0: - # this number includes parents (other variables) - # and the variable itself, therefore its - # self.maxnpar+2 in the following line - nr = np.random.randint(1,self.maxnpar+2) - j_par = np.random.choice(np.arange(0,self.dim), - size=nr,replace=False) - self.Adj[i,j_par] = 1 - else: - self.Adj[i,i] = 1 - # - self.Adj = np.abs(np.array(self.Adj_signed)) - #sett.m(0,self.Adj) - - def set_coupl_old(self): - """ Using the adjacency matrix, sample a coupling matrix. - """ - if self.model == 'krumsiek11' or self.model == 'var': - # we already built the coupling matrix in set_coupl20() - return - self.Coupl = np.zeros((self.dim,self.dim)) - for i in range(self.Adj.shape[0]): - for j,a in enumerate(self.Adj[i]): - # if there is a 1 in Adj, specify co and antiregulation - # and strength of regulation - if a != 0: - co_anti = np.random.randint(2) - # set a lower bound for the coupling parameters - # they ought not to be smaller than 0.1 - # and not be larger than 0.4 - self.Coupl[i,j] = 0.0*np.random.rand() + 0.1 - # set sign for coupling - if co_anti == 1: - self.Coupl[i,j] *= -1 - # enforce certain requirements on models - if self.model == 1: - self.coupl_model1() - elif self.model == 5: - self.coupl_model5() - elif self.model in [6,7]: - self.coupl_model6() - elif self.model in [8,9,10]: - self.coupl_model8() - # output - if self.verbosity > 1: - sett.m(0,self.Coupl) - - def coupl_model1(self): - """ In model 1, we want enforce the following signs - on the couplings. Model 2 has the same couplings - but arbitrary signs. - """ - self.Coupl[0,0] = np.abs(self.Coupl[0,0]) - self.Coupl[0,1] = -np.abs(self.Coupl[0,1]) - self.Coupl[1,1] = np.abs(self.Coupl[1,1]) - - def coupl_model5(self): - """ Toggle switch. - """ - self.Coupl = -0.2*self.Adj - self.Coupl[2,0] *= -1 - self.Coupl[3,0] *= -1 - self.Coupl[4,1] *= -1 - self.Coupl[5,1] *= -1 - - def coupl_model6(self): - """ Variant of toggle switch. - """ - self.Coupl = 0.5*self.Adj_signed - - def coupl_model8(self): - """ Variant of toggle switch. - """ - self.Coupl = 0.5*self.Adj_signed - # reduce the value of the coupling of the repressing genes - # otherwise completely unstable solutions are obtained - for x in np.nditer(self.Coupl,op_flags=['readwrite']): - if x < -1e-6: - x[...] = -0.2 - - def coupl_model_krumsiek11(self): - """ Variant of toggle switch. - """ - self.Coupl = self.Adj_signed - - def sim_model_back_help(self,Xt,Xt1): - """ Yields zero when solved for X_t - given X_{t+1}. - """ - return - Xt1 + Xt + self.Xdiff(Xt) - - def sim_model_backwards(self,tmax,X0): - """ Simulate the model backwards in time. - """ - X = np.zeros((tmax,self.dim)) - X[tmax-1] = X0 - for t in range(tmax-2,-1,-1): - sol = sp.optimize.root(self.sim_model_back_help, - X[t+1], - args=(X[t+1]),method='hybr') - X[t] = sol.x - return X - - def branch_init_model1(self,tmax=100): - # check whether we can define trajectories - Xfix = np.array([self.Coupl[0,1]/self.Coupl[0,0],1]) - if Xfix[0] > 0.97 or Xfix[0] < 0.03: - sett.m(0,'... either no fixed point in [0,1]^2! \n' + - ' or fixed point is too close to bounds' ) - return None - # - XbackUp = grnsim.sim_model_backwards(tmax=tmax/3,X0=Xfix+np.array([0.02,-0.02])) - XbackDo = grnsim.sim_model_backwards(tmax=tmax/3,X0=Xfix+np.array([-0.02,-0.02])) - # - Xup = grnsim.sim_model(tmax=tmax,X0=XbackUp[0]) - Xdo = grnsim.sim_model(tmax=tmax,X0=XbackDo[0]) - # compute mean - X0mean = 0.5*(Xup[0] + Xdo[0]) - # - if np.min(X0mean) < 0.025 or np.max(X0mean) > 0.975: - sett.m(0,'... initial point is too close to bounds' ) - return None - # - if self.show and self.verbosity > 1: - pl.figure() - pl.plot(XbackUp[:,0],'.b',XbackUp[:,1],'.g') - pl.plot(XbackDo[:,0],'.b',XbackDo[:,1],'.g') - pl.plot(Xup[:,0],'b',Xup[:,1],'g') - pl.plot(Xdo[:,0],'b',Xdo[:,1],'g') - return X0mean - - def parents_from_boolRule(self,rule): - """ Determine parents based on boolean updaterule. - - Returns list of parents. - """ - rule_pa = rule.replace('(','').replace(')','').replace('or','').replace('and','').replace('not','') - rule_pa = rule_pa.split() - # if there are no parents, continue - if not rule_pa: - return [] - # check whether these are meaningful parents - pa_old = [] - pa_delete = [] - for pa in rule_pa: - if pa not in self.varNames.keys(): - sett.m(0,'list of available variables:') - sett.m(0,list(self.varNames.keys())) - message = ('processing of rule "' + rule - + ' yields an invalid parent: ' + pa - + ' | check whether the syntax is correct: \n' - + 'only python expressions "(",")","or","and","not" ' - + 'are allowed, variable names and expressions have to be separated ' - + 'by white spaces') - raise ValueError(message) - if pa in pa_old: - pa_delete.append(pa) - for pa in pa_delete: - rule_pa.remove(pa) - return rule_pa - - def build_boolCoeff(self): - ''' Compute coefficients for tuple space. - ''' - # coefficients for hill functions from boolean update rules - self.boolCoeff = collections.OrderedDict([(s,[]) for s in self.varNames.keys()]) - # parents - self.pas = collections.OrderedDict([(s,[]) for s in self.varNames.keys()]) - # - for key in self.boolRules.keys(): - rule = self.boolRules[key] - self.pas[key] = self.parents_from_boolRule(rule) - pasIndices = [self.varNames[pa] for pa in self.pas[key]] - # check whether there are coupling matrix entries for each parent - for g in range(self.dim): - if g in pasIndices: - if np.abs(self.Coupl[self.varNames[key],g]) < 1e-10: - raise ValueError('specify coupling value for '+str(key)+' <- '+str(g)) - else: - if np.abs(self.Coupl[self.varNames[key],g]) > 1e-10: - raise ValueError('there should be no coupling value for '+str(key)+' <- '+str(g)) - if self.verbosity > 1: - sett.m(0,'...'+key) - sett.m(0,rule) - sett.m(0,rule_pa) - # now evaluate coefficients - for tuple in list(itertools.product([False,True],repeat=len(self.pas[key]))): - if self.process_rule(rule,self.pas[key],tuple): - self.boolCoeff[key].append(tuple) - # - if self.verbosity > 1: - sett.m(0,self.boolCoeff[key]) - - def process_rule(self,rule,pa,tuple): - ''' Process a string that denotes a boolean rule. - ''' - for i,v in enumerate(tuple): - rule = rule.replace(pa[i],str(v)) - return eval(rule) - - def write_data(self,X,dir='sim/test',noiseObs=0.0,append=False, - branching=False,nrRealizations=1,seed=0): - header = self.header - tmax = int(X.shape[0]) - header += 'tmax = ' + str(tmax) + '\n' - header += 'branching = ' + str(branching) + '\n' - header += 'nrRealizations = ' + str(nrRealizations) + '\n' - header += 'noiseObs = ' + str(noiseObs) + '\n' - header += 'noiseDyn = ' + str(self.noiseDyn) + '\n' - header += 'seed = ' + str(seed) + '\n' - # add observational noise - X += noiseObs*np.random.randn(tmax,self.dim) - # call helper function - write_data(X,dir,append,header, - varNames=self.varNames, - Adj=self.Adj,Coupl=self.Coupl, - model=self.model,modelType=self.modelType, - boolRules=self.boolRules,invTimeStep=self.invTimeStep) - -def _check_branching(X,Xsamples,restart,threshold=0.25): - """ Check whether time series branches. - - Args: - X (np.array): current time series data. - Xsamples (np.array): list of previous branching samples. - restart (int): counts number of restart trials. - threshold (float, optional): sets threshold for attractor - identification. - - Returns: - check = true if branching realization, Xsamples = updated list - """ - check = True - if restart == 0: - Xsamples.append(X) - else: - for Xcompare in Xsamples: - Xtmax_diff = np.absolute(X[-1,:] - Xcompare[-1,:]) - # If the second largest element is smaller than threshold - # set check to False, i.e. at least two elements - # need to change in order to have a branching. - # If we observe all parameters of the system, - # a new attractor state must involve changes in two - # variables. - if np.partition(Xtmax_diff,-2)[-2] < threshold: - check = False - if check: - Xsamples.append(X) - # - if not check: - sett.m(0,'restart',restart,'no new branch') - else: - sett.m(0,'restart',restart,'new branch') - # - return check, Xsamples - -def check_nocycles(Adj,verbosity=2): - """ Checks that there are no cycles in graph described by adjacancy matrix. - - Args: - Adj (np.array): adjancancy matrix of dimension (dim, dim) - - Returns: - True if there is no cycle, False otherwise. - """ - dim = Adj.shape[0] - for g in range(dim): - v = np.zeros(dim) - v[g] = 1 - for i in range(dim): - v = Adj.dot(v) - if v[g] > 1e-10: - if verbosity > 2: - sett.m(0,Adj) - sett.m(0,'contains a cycle of length',i+1, - 'starting from node',g, - '-> reject') - return False - return True - - -def sample_coupling_matrix(dim=3,connectivity=0.5): - """ Sample coupling matrix. - - Checks that returned graphs contain no self-cycles. - - Args: - dim (int): dimension of coupling matrix. - connectivity (float): fraction of connectivity, fully connected means 1., - not-connected means 0, in the case of fully connected, one has - dim*(dim-1)/2 edges in the graph. - - Returns: - Tuple (Coupl,Adj,Adj_signed) of coupling matrix, adjancancy and - signed adjacancy matrix. - """ - max_trial = 10 - check = False - for trial in range(max_trial): - # random topology for a given connectivity / edge density - Coupl = np.zeros((dim,dim)) - nr_edges = 0 - for gp in range(dim): - for g in range(dim): - if gp != g: - # need to have the factor 0.5, otherwise - # connectivity=1 would lead to dim*(dim-1) edges - if np.random.rand() < 0.5*connectivity: - Coupl[gp,g] = 0.7 - nr_edges += 1 - # obtain adjacancy matrix - Adj_signed = np.zeros((dim,dim),dtype='int_') - Adj_signed = np.sign(Coupl) - Adj = np.abs(Adj_signed) - # check for cycles and whether there is at least one edge - if check_nocycles(Adj) and nr_edges > 0: - check = True - break - if not check: - raise ValueError('did not find graph without cycles after', - max_trial,'trials') - return Coupl, Adj, Adj_signed, nr_edges - -class StaticCauseEffect: - """ - Simulates static data to investigate structure learning. - """ - - availModels = odict([ - ('line', 'y = alpha x \n'), - ('noise', 'y = noise \n'), - ('absline', 'y = |x| \n'), - ('parabola', 'y = alpha x^2 \n'), - ('sawtooth', 'y = x - |x| \n'), - ('tanh', 'y = tanh(x) \n'), - ('combi', 'combinatorial regulation \n'), - ]) - - def __init__(self): - - # define a set of available functions - self.funcs = { - 'line': lambda x: x, - 'noise': lambda x: 0, - 'absline': lambda x: np.abs(x), - 'parabola': lambda x: x**2, - 'sawtooth': lambda x: 0.5*x - np.floor(0.5*x), - 'tanh': lambda x: np.tanh(2*x), - } - - def sim_givenAdj(self,Adj,model='line'): - """ Simulate data given only an adjacancy matrix and a model. - - The model is a bivariate funtional dependence. The adjacancy matrix - needs to be acyclic. - - Args: - Adj (np.array): adjacancy matrix of shape (dim,dim). - - Returns: - Data array of shape (nr_samples,dim). - """ - # nice examples - examples = [{'func' : 'sawtooth', 'gdist' : 'uniform', - 'sigma_glob' : 1.8, 'sigma_noise' : 0.1}] - - # nr of samples - nr_samples = 100 - - # noise - sigma_glob = 1.8 - sigma_noise = 0.4 - - # coupling function / model - func = self.funcs[model] - - # glob distribution - sourcedist = 'uniform' - - # loop over source nodes - dim = Adj.shape[0] - X = np.zeros((nr_samples,dim)) - # source nodes have no parents themselves - nrpar = 0 - children = list(range(dim)) - parents = [] - for gp in range(dim): - if Adj[gp,:].sum() == nrpar: - if sourcedist == 'gaussian': - X[:,gp] = np.random.normal(0,sigma_glob,nr_samples) - if sourcedist == 'uniform': - X[:,gp] = np.random.uniform(-sigma_glob,sigma_glob,nr_samples) - parents.append(gp) - children.remove(gp) - - # all of the following guarantees for 3 dim, that we generate the data - # in the correct sequence - # then compute all nodes that have 1 parent, then those with 2 parents - children_sorted = [] - nrchildren_par = np.zeros(dim) - nrchildren_par[0] = len(parents) - for nrpar in range(1,dim): - # loop over child nodes - for gp in children: - if Adj[gp,:].sum() == nrpar: - children_sorted.append(gp) - nrchildren_par[nrpar] += 1 - # if there is more than a child with a single parent - # order these children (there are two in three dim) - # by distance to the source/parent - if nrchildren_par[1] > 1: - if Adj[children_sorted[0],parents[0]] == 0: - help = children_sorted[0] - children_sorted[0] = children_sorted[1] - children_sorted[1] = help - - for gp in children_sorted: - for g in range(dim): - if Adj[gp,g] > 0: - X[:,gp] += 1./Adj[gp,:].sum()*func(X[:,g]) - X[:,gp] += np.random.normal(0,sigma_noise,nr_samples) - -# fig = pl.figure() -# fig.add_subplot(311) -# pl.plot(X[:,0],X[:,1],'.',mec='white') -# fig.add_subplot(312) -# pl.plot(X[:,1],X[:,2],'.',mec='white') -# fig.add_subplot(313) -# pl.plot(X[:,2],X[:,0],'.',mec='white') -# pl.show() - - return X - - def sim_combi(self): - """ Simulate data to model combi regulation. - - Args: - - Returns: - - """ - nr_samples = 500 - sigma_glob = 1.8 - - X = np.zeros((nr_samples,3)) - - X[:,0] = np.random.uniform(-sigma_glob,sigma_glob,nr_samples) - X[:,1] = np.random.uniform(-sigma_glob,sigma_glob,nr_samples) - - func = self.funcs['tanh'] - - # XOR type -# X[:,2] = (func(X[:,0])*sp.stats.norm.pdf(X[:,1],0,0.2) -# + func(X[:,1])*sp.stats.norm.pdf(X[:,0],0,0.2)) - # AND type / diagonal -# X[:,2] = (func(X[:,0]+X[:,1])*sp.stats.norm.pdf(X[:,1]-X[:,0],0,0.2)) - # AND type / horizontal - X[:,2] = (func(X[:,0])*sp.stats.norm.cdf(X[:,1],1,0.2)) - - pl.scatter(X[:,0],X[:,1],c=X[:,2],edgecolor='face') - pl.show() - - pl.plot(X[:,1],X[:,2],'.') - pl.show() - - return X - -def sample_static_data(model,dir,verbosity=0): - # fraction of connectivity as compared to fully connected - # in one direction, which amounts to dim*(dim-1)/2 edges - connectivity = 0.8 - dim = 3 - nr_Coupls = 50 - model = model.replace('static-','') - np.random.seed(0) - - if model != 'combi': - nr_edges = np.zeros(nr_Coupls) - for icoupl in range(nr_Coupls): - Coupl, Adj, Adj_signed, nr_e = sample_coupling_matrix(dim,connectivity) - if verbosity > 1: - sett.m(0,icoupl) - sett.m(0,Adj) - nr_edges[icoupl] = nr_e - # sample data - X = StaticCauseEffect().sim_givenAdj(Adj,model) - write_data(X,dir,Adj=Adj) - sett.m(0,'mean edge number:',nr_edges.mean()) - - else: - X = StaticCauseEffect().sim_combi() - Adj = np.zeros((3,3)) - Adj[2,0] = Adj[2,1] = 0 - write_data(X,dir,Adj=Adj) - -if __name__ == '__main__': -# epilog = (' 1: 2dim, causal direction X_1 -> X_0, constraint signs\n' -# + ' 2: 2dim, causal direction X_1 -> X_0, arbitrary signs\n' -# + ' 3: 2dim, causal direction X_1 <-> X_0, arbitrary signs\n' -# + ' 4: 2dim, mix of model 2 and 3\n' -# + ' 5: 6dim double toggle switch\n' -# + ' 6: two independent evolutions without repression, sync.\n' -# + ' 7: two independent evolutions without repression, random init\n' -# + ' 8: two independent evolutions directed repression, random init\n' -# + ' 9: two independent evolutions mutual repression, random init\n' -# + ' 10: two indep. evol., diff. self-loops possible, mut. repr., rand init\n') - epilog = '' - for k,v in StaticCauseEffect.availModels.items(): - epilog += ' static-' + k + ': ' + v - for k,v in GRNsim.availModels.items(): - epilog += ' ' + k + ': ' + v - # command line options - p = argparse.ArgumentParser( - description=('Simulate stochastic discrete-time dynamical systems,\n' - 'in particular gene regulatory networks.'), - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=(' MODEL: specify one of the following models, or one of \n' - ' the filenames (without ".txt") in the directory "models" \n' - + epilog - )) - aa = p.add_argument - aa('--dir',required=True, - type=str,default='', - help=('specify directory to store data, ' - + ' must start with "sim/MODEL_...", see possible values for MODEL below ')) - aa('--show', - action='store_true', - help='show plots') - aa('--verbosity', - type=int,default=0, - help='specify integer > 0 to get more output [default 0]') - args = p.parse_args() - - # run checks on output directory - dir = args.dir - if not dir.startswith('sim/'): - raise IOError('prepend "sim/..." to --dir argument,' - + '"..." being an arbitrary string') - else: - model = dir.split('/')[1].split('_')[0] - sett.m(0,'...model is: "'+model+'"') - if os.path.exists(dir) and 'test' not in dir: - message = ('directory ' + dir + - ' already exists, remove it and continue? [y/n, press enter]') - if str(input(message)) != 'y': - sett.m(0,' ...quit program execution') - quit() - else: - sett.m(0,' ...removing directory and continuing...') - os.system('rm -r ' + dir) - - sett.m(0,model) - sett.m(0,dir) - - # sample data - if 'static' in model: - sample_static_data(model=model,dir=dir,verbosity=args.verbosity) - else: - sample_dynamic_data(model=model,dir=dir) - - diff --git a/scanpy/tools/tsne.py b/scanpy/tools/tsne.py deleted file mode 100644 index 603effe6a1..0000000000 --- a/scanpy/tools/tsne.py +++ /dev/null @@ -1,266 +0,0 @@ -# Copyright 2016-2017 F. Alexander Wolf (http://falexwolf.de). -""" -t-SNE -===== - -Credits -------- -This module automatically choose from three t-SNE versions from -- sklearn.manifold.TSNE -- Dmitry Ulyanov (multicore, fastest) - https://github.com/DmitryUlyanov/Multicore-TSNE - install via 'pip install psutil cffi', get code from github -- Laurens van der Maaten (slowest, oldest), slow fall back option - https://lvdmaaten.github.io/tsne/ - Copyright 2008 Laurens van der Maaten, Tilburg University. -""" - -from collections import OrderedDict as odict -import numpy as np -from ..compat.matplotlib import pyplot as pl -from ..tools.pca import pca -from .. import settings as sett -from .. import plotting as plott -from .. import utils - -def tsne(ddata, numPCs=50, perplexity=30): - """ - Visualize data using t-SNE as of van der Maaten & Hinton (2008). - - Parameters - ---------- - ddata : dictionary containing - X : np.ndarray - Data array, rows store observations, columns covariates. - numPCs : int - Number of principal components in preprocessing PCA. - - Parameters as used in sklearn.manifold.TSNE: - perplexity : float, optional (default: 30) - Perplexity. - - Returns - ------- - dtsne : dict containing - Y : np.ndarray - tSNE representation of the data. - """ - params = locals(); del params['ddata'] - sett.m(0,'perform tSNE') - # preprocessing by PCA - if params['numPCs'] > 0 and ddata['X'].shape[1] > params['numPCs']: - sett.m(0, 'preprocess using PCA with', params['numPCs'], 'PCs') - sett.m(0, '--> avoid this by setting numPCs = 0') - dpca = pca(ddata, n_components=params['numPCs']) - X = dpca['Y'] - else: - X = ddata['X'] - # params for sklearn - params_sklearn = {k: v for k, v in params.items() if not k=='numPCs'} - params_sklearn['verbose'] = sett.verbosity - # deal with different tSNE implementations - try: - from MulticoreTSNE import MulticoreTSNE as TSNE - tsne = TSNE(n_jobs=4, **params_sklearn) - sett.m(0,'--> perform tSNE using MulticoreTSNE') - Y = tsne.fit_transform(X) - except ImportError: - try: - from sklearn.manifold import TSNE - tsne = TSNE(**params_sklearn) - sett.m(0,'--> perform tSNE using sklearn') - sett.m(1,'--> can be sped up by installing\n' - ' https://github.com/DmitryUlyanov/Multicore-TSNE') - Y = tsne.fit_transform(X) - except ImportError: - sett.m(0,'--> perform tSNE using slow/unreliable original\n' - ' code by L. van der Maaten!?\n' - '--> consider installing sklearn\n' - ' using "conda/pip install scikit-learn"') - Y = _tsne_vandermaaten(X, 2, params['perplexity']) - return {'type': 'tsne', 'Y': Y} - - -def plot(dtsne, ddata, - layout='2d', - legendloc='lower right', - cmap='jet', - adjust_right=0.75): # consider changing to 'viridis' - """ - Plot the results of a DPT analysis. - - Parameters - ---------- - dtsne : dict - Dict returned by tSNE tool. - ddata : dict - Data dictionary. - layout : {'2d', '3d', 'unfolded 3d'}, optional (default: '2d') - Layout of plot. - legendloc : see matplotlib.legend, optional (default: 'lower right') - Options for keyword argument 'loc'. - cmap : str, optional (default: jet) - String denoting matplotlib color map. - """ - params = locals(); del params['ddata']; del params['dtsne'] - # highlights - highlights = [] - if False: - if 'highlights' in ddata: - highlights = ddata['highlights'] - # base figure - axs = plott.scatter(dtsne['Y'], - subtitles=['tSNE'], - component_name='tSNE', - layout=params['layout'], - c='grey', - highlights=highlights, - cmap=params['cmap']) - # annotated groups - if 'groupmasks' in ddata: - for igroup, group in enumerate(ddata['groupmasks']): - plott.group(axs[0], igroup, ddata, dtsne['Y'], params['layout']) - axs[0].legend(frameon=False, loc='center left', bbox_to_anchor=(1, 0.5)) - # right margin - pl.subplots_adjust(right=params['adjust_right']) - - if sett.savefigs: - pl.savefig(sett.figdir+dtsne['writekey']+'.'+sett.extf) - elif sett.autoshow: - pl.show() - -def _tsne_vandermaaten(X = np.array([]), no_dims = 2, perplexity = 30.0): - """ - Runs t-SNE on the dataset in the NxD array X to reduce its dimensionality to - no_dims dimensions. The syntaxis of the function is Y = tsne.tsne(X, - no_dims, perplexity), where X is an NxD NumPy array. - """ - - # Initialize variables - (n, d) = X.shape; - max_iter = 1000; - initial_momentum = 0.5; - final_momentum = 0.8; - eta = 500; - min_gain = 0.01; - Y = np.random.randn(n, no_dims); - dY = np.zeros((n, no_dims)); - iY = np.zeros((n, no_dims)); - gains = np.ones((n, no_dims)); - - # Compute P-values - P = _x2p_vandermaaten(X, 1e-5, perplexity); - P = P + np.transpose(P); - P = P / np.sum(P); - P = P * 4; # early exaggeration - P = np.maximum(P, 1e-12); - - # Run iterations - for iter in range(max_iter): - - # Compute pairwise affinities - sum_Y = np.sum(np.square(Y), 1); - num = 1 / (1 + np.add(np.add(-2 * np.dot(Y, Y.T), sum_Y).T, sum_Y)); - num[range(n), range(n)] = 0; - Q = num / np.sum(num); - Q = np.maximum(Q, 1e-12); - - # Compute gradient - PQ = P - Q; - for i in range(n): - dY[i,:] = np.sum(np.tile(PQ[:,i] * num[:,i], (no_dims, 1)).T * (Y[i,:] - Y), 0); - - # Perform the update - if iter < 20: - momentum = initial_momentum - else: - momentum = final_momentum - gains = (gains + 0.2) * ((dY > 0) != (iY > 0)) + (gains * 0.8) * ((dY > 0) == (iY > 0)); - gains[gains < min_gain] = min_gain; - iY = momentum * iY - eta * (gains * dY); - Y = Y + iY; - Y = Y - np.tile(np.mean(Y, 0), (n, 1)); - - # Compute current value of cost function - if (iter + 1) % 10 == 0: - C = np.sum(P * np.log(P / Q)); - sett.m(0,"Iteration " + str(iter + 1) + ": error is " + str(C)) - - # Stop lying about P-values - if iter == 100: - P = P / 4; - - # Return solution - return Y; - -def _Hbeta_vandermaaten(D = np.array([]), beta = 1.0): - """ - Compute the perplexity and the P-row for a specific value of the - precision of a Gaussian distribution. - """ - - # Compute P-row and corresponding perplexity - P = np.exp(-D.copy() * beta); - sumP = sum(P); - H = np.log(sumP) + beta * np.sum(D * P) / sumP; - P = P / sumP; - return H, P; - -def _x2p_vandermaaten(X = np.array([]), tol = 1e-5, perplexity = 30.0): - """ - Performs a binary search to get P-values in such a way that each - conditional Gaussian has the same perplexity. - """ - - # Initialize some variables - sett.m(0,"Computing pairwise distances...") - (n, d) = X.shape; - sum_X = np.sum(np.square(X), 1); - D = np.add(np.add(-2 * np.dot(X, X.T), sum_X).T, sum_X); - P = np.zeros((n, n)); - beta = np.ones((n, 1)); - logU = np.log(perplexity); - - # Loop over all datapoints - for i in range(n): - - # Print progress - if i % 500 == 0: - sett.m(0,"Computing P-values for point ", i, " of ", n, "...") - - # Compute the Gaussian kernel and entropy for the current precision - betamin = -np.inf; - betamax = np.inf; - Di = D[i, np.concatenate((np.r_[0:i], np.r_[i+1:n]))]; - (H, thisP) = _Hbeta_vandermaaten(Di, beta[i]); - - # Evaluate whether the perplexity is within tolerance - Hdiff = H - logU; - tries = 0; - while np.abs(Hdiff) > tol and tries < 50: - - # If not, increase or decrease precision - if Hdiff > 0: - betamin = beta[i].copy(); - if betamax == np.inf or betamax == -np.inf: - beta[i] = beta[i] * 2; - else: - beta[i] = (beta[i] + betamax) / 2; - else: - betamax = beta[i].copy(); - if betamin == np.inf or betamin == -np.inf: - beta[i] = beta[i] / 2; - else: - beta[i] = (beta[i] + betamin) / 2; - - # Recompute the values - (H, thisP) = _Hbeta_vandermaaten(Di, beta[i]); - Hdiff = H - logU; - tries = tries + 1; - - # Set the final row of P - P[i, np.concatenate((np.r_[0:i], np.r_[i+1:n]))] = thisP; - - # Return final P-matrix - sett.m(0,"Mean value of sigma: ", np.mean(np.sqrt(1 / beta))) - return P; diff --git a/scanpy/utils.py b/scanpy/utils.py deleted file mode 100644 index aecea415b5..0000000000 --- a/scanpy/utils.py +++ /dev/null @@ -1,1188 +0,0 @@ -# Copyright 2016-2017 F. Alexander Wolf (http://falexwolf.de). -""" -Utility functions and classes -============================= - -TODO ----- -- Preserve case when writing params to files. -- Consider using openpyxl or xlsxwriter instead of pandas for reading and - writing Excel files. -""" - -# this is necessary to import scanpy from within package -from __future__ import absolute_import -# standard modules -import os -import argparse -import h5py -import sys -import warnings -import traceback -import gzip -from collections import OrderedDict as odict -# scientific modules -import numpy as np -import scipy as sp -import scipy.cluster -from .compat.matplotlib import pyplot as pl -# local modules -import scanpy as sc -from . import settings as sett - -avail_exts = ['csv','xlsx','txt','h5','soft.gz','txt.gz'] -""" Available file formats for writing data. """ - -#-------------------------------------------------------------------------------- -# Deal with examples -#-------------------------------------------------------------------------------- - -def fill_in_datakeys(dexamples, dexdata): - """ - Update the 'examples dictionary' _examples.dexamples. - - If a datakey (key in 'datafile dictionary') is not present in the 'examples - dictionary' it is used to initialize an entry with that key. - - If not specified otherwise, any 'exkey' (key in 'examples dictionary') is - used as 'datakey'. - """ - # default initialization of 'datakey' key with entries from data dictionary - for exkey in dexamples: - if 'datakey' not in dexamples[exkey]: - if exkey in dexdata: - dexamples[exkey]['datakey'] = exkey - else: - dexamples[exkey]['datakey'] = 'unspecified in dexdata' - return dexamples - -#-------------------------------------------------------------------------------- -# Deal with tool parameters -#-------------------------------------------------------------------------------- - -def init_params(params, default_params, check=True): - """ - Update default paramaters with params. - """ - _params = dict(default_params) - if params: # allow for params to be None - for key, val in params.items(): - if key in default_params: - _params[key] = val - elif check: - raise ValueError('\'' + key - + '\' is not a valid parameter key, ' - + 'consider one of \n' - + str(list(default_params.keys()))) - return _params - -#-------------------------------------------------------------------------------- -# Command-line argument reading and processing -#-------------------------------------------------------------------------------- - -def add_args(p, dadd_args=None): - """ - Add arguments to parser. - - Parameters - ------- - dadd_args : dict - Dictionary of additional arguments formatted as - {'arg': {'type': int, 'default': 0, ... }} - """ - - aa = p.add_argument_group('Tool parameters').add_argument - aa('exkey', - type=str, default='', metavar='exkey', - help='Specify the "example key" (just a shorthand), which is used' - ' to look up a data dictionary and parameters. ' - 'Use Scanpy subcommand "examples" to inspect possible values.') - # example key default argument - aa('-p', '--params', - nargs='*', default=None, metavar='k v', - help='Provide optional parameters as list, ' - 'e.g., "sigma 5 knn True" for setting "sigma" and "knn". See possible ' - 'keys in the function definition above (default: "").') - # make sure there are is conflict with dadd_args - if dadd_args is None or '--paramsfile' not in dadd_args: - aa('--paramsfile', - type=str, default='', metavar='pf', - help='Alternatively, specify the path to a parameter file (default: "").') - # arguments from dadd_args - if dadd_args is not None: - for key, val in dadd_args.items(): - if key != 'arg': - aa(key, **val) - - aa = p.add_argument_group('Plotting').add_argument - aa('-q', '--plotparams', - nargs='*', default=None, metavar='k v', - help='Provide specific plotting parameters as list, ' - 'e.g., "layout 3d cmap viridis". ' - 'See possible keys by calling "--plotparams help" (default: "").') - - aa = p.add_argument_group('Toolchain').add_argument - aa('--pre', - type=str, default='', metavar='tool', - help='Tool whose output should be used as input, ' - 'often a tool that detects subgroups (default: tool dependent).') - - # standard arguments - p = sett.add_args(p) - - return p - -def read_args_tool(toolkey, dexamples, tool_add_args=None): - """ - Read args for single tool. - """ - p = default_tool_argparser(sc.help(toolkey, string=True), dexamples) - if tool_add_args is None: - p = add_args(p) - else: - p = tool_add_args(p) - args = vars(p.parse_args()) - args = sett.process_args(args) - return args - -def default_tool_argparser(description, dexamples): - """ - Create default parser for single tools. - """ - epilog = '\n' - for k,v in sorted(dexamples.items()): - epilog += ' ' + k + '\n' - p = argparse.ArgumentParser( - description=description, - add_help=False, - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=('available values for examples (exkey):'+epilog)) - return p - -#-------------------------------------------------------------------------------- -# Reading and writing parameter files -#-------------------------------------------------------------------------------- - -def read_params(filename, asheader=False): - """ - Read parameter dictionary from text file. - - Assumes that parameters are specified in the format: - par1 = value1 - par2 = value2 - - Comments that start with '#' are allowed. - - Parameters - ---------- - filename : str - Filename of data file. - asheader : bool, optional - Read the dictionary from the header (comment section) of a file. - - Returns - ------- - params : dict - Dictionary that stores parameters. - """ - if not os.path.exists(filename): - filename = '../' + filename - if not asheader: - sett.m(0,'reading params file',filename) - params = odict([]) - for line in open(filename): - if '=' in line: - if not asheader or line.startswith('#'): - line = line[1:] if line.startswith('#') else line - key, val = line.split('=') - key = key.strip(); val = val.strip() - params[key] = convert_string(val) - return params - -def get_params_from_list(params_list): - """ - Transform params list to dictionary. - """ - if len(params_list)%2 != 0: - raise ValueError('need to provide a list of key value pairs') - params = {} - for i in range(0,len(params_list),2): - key, val = params_list[i:i+2] - params[key] = convert_string(val) - return params - -def write_params(filename,*args,**dicts): - """ - Write parameters to file, so that it's readable py read_params. - - Uses INI file format. - """ - if len(args) == 1: - d = args[0] - with open(filename, 'w') as f: - for key in d: - f.write(key + ' = ' + str(d[key]) + '\n') - else: - with open(filename, 'w') as f: - for k, d in dicts.items(): - f.write('[' + k + ']\n') - for key, val in d.items(): - f.write(key + ' = ' + str(val) + '\n') - -#-------------------------------------------------------------------------------- -# Reading and Writing data files and dictionaries -#-------------------------------------------------------------------------------- - -def write(filename_or_key, dictionary): - """ - Writes dictionaries - as returned by tools - to file. - - If a key is specified, the filename is generated as - filename = sett.writedir + key + sett.extd - This defaults to - filename = 'write/' + key + '.h5' - and can be changed by reseting sett.writedir and sett.extd. - - Parameters - ---------- - filename_or_key : str - Filename of data file or key used in function write(key,dict). - """ - if 'writekey' not in dictionary: - dictionary['writekey'] = filename_or_key - filename = sett.writedir + filename_or_key + '.' + sett.extd - if is_filename(filename_or_key): - filename = filename_or_key - write_dict_to_file(filename, dictionary) - -def read(filename_or_key, sheet='', sep=None, first_column_names=False, - as_strings=False, backup_url=''): - """ - Read file or dictionary and return data dictionary. - - To speed up reading and save storage space, this creates an hdf5 file if - it's not present yet. - - Parameters - ---------- - filename_or_key : str - Filename of data file or key used in function write(key,dict). - sheet : str, optional - Name of sheet in Excel file. - sep : str, optional - Separator that separates data within text file. If None, will split at - arbitrary number of white spaces, which is different from enforcing - splitting at single white space ' '. - first_column_names : bool, optional - Assume the first column stores samplenames. Is unnecessary if the sample - names are not floats or integers: if they are strings, this will be - detected automatically. - as_strings : bool, optional - Read names instead of numbers. - - Returns - ------- - ddata : dict containing - X : np.ndarray - Data array for further processing, columns correspond to genes, - rows correspond to samples. - rownames : np.ndarray - Array storing the names of rows (experimental labels of samples). - colnames : np.ndarray - Array storing the names of columns (gene names). - """ - if is_filename(filename_or_key): - return read_file(filename_or_key, sheet, sep, first_column_names, - as_strings, backup_url) - - # generate filename and read to dict - key = filename_or_key - filename = sett.writedir + key + '.' + sett.extd - if not os.path.exists(filename): - raise ValueError('Reading with key ' + key + ' failed! ' + - 'Provide valid key or filename directly: ' + - 'inferred filename ' + - filename + ' does not exist.') - return read_file_to_dict(filename) - -#-------------------------------------------------------------------------------- -# Reading and Writing data files -#-------------------------------------------------------------------------------- - -def read_file(filename, sheet='', sep=None, first_column_names=False, - as_strings=False, backup_url=''): - """ - Read file and return data dictionary. - - To speed up reading and save storage space, this creates an hdf5 file if - it's not present yet. - - Parameters - ---------- - filename : str - Filename of data file. - sheet : str, optional - Name of sheet in Excel file. - sep : str, optional - Separator that separates data within text file. If None, will split at - arbitrary number of white spaces, which is different from enforcing - splitting at single white space ' '. - first_column_names : bool, optional - Assume the first column stores samplenames. Is unnecessary if the sample - names are not floats or integers, but strings. - as_strings : bool, optional - Read names instead of numbers. - backup_url : str - URL for download of file in case it's not present. - - Returns - ------- - ddata : dict containing - X : np.ndarray - Data array for further processing, columns correspond to genes, - rows correspond to samples. - rownames : np.ndarray - Array storing the names of rows (experimental labels of samples). - colnames : np.ndarray - Array storing the names of columns (gene names). - - If sheet is unspecified, and an h5 or xlsx file is read, the dict - contains all sheets instead. - """ - ext = is_filename(filename, return_ext=True) - filename = check_datafile_present(filename, backup_url=backup_url) - return _read_file(filename, ext, sheet, sep, first_column_names, - as_strings) - -def _read_file(filename, ext, sheet, sep, first_column_names, - as_strings): - """ - Same as read_file(), one additional parameter ext. - - Parameters - ---------- - ext : str - Extension that denotes the type of the file. - """ - if ext == 'h5': - if sheet == '': - return read_file_to_dict(filename, ext='h5') - sett.m(0, 'reading sheet', sheet, 'from file', filename) - return _read_hdf5_single(filename, sheet) - # if filename is not in the hdf5 format, do some more book keeping - filename_hdf5 = filename.replace('.' + ext, '.h5') - # just to make sure that we have a different filename - if filename_hdf5 == filename: - filename_hdf5 = filename + '.h5' - if not os.path.exists(filename_hdf5): - sett.m(0,'reading file', filename, - '\n--> write an hdf5 version to speedup reading next time') - # do the actual reading - if ext == 'xlsx' or ext == 'xls': - if sheet=='': - ddata = read_file_to_dict(filename, ext=ext) - else: - ddata = _read_excel(filename, sheet) - elif ext == 'csv': - ddata = _read_text(filename, sep=',', - first_column_names=first_column_names, - as_strings=as_strings) - elif ext == 'txt': - ddata = _read_text(filename, sep, first_column_names, - as_strings=as_strings) - elif ext == 'soft.gz': - ddata = _read_softgz(filename) - elif ext == 'txt.gz': - print('TODO: implement similar to read_softgz') - sys.exit() - else: - raise ValueError('unkown extension', ext) - - # specify Scanpy type of dictionary - if 'type' not in ddata: - ddata['type'] = 'data' - # write as hdf5 for faster reading when calling the next time - write_dict_to_file(filename_hdf5,ddata) - else: - ddata = read_file_to_dict(filename_hdf5) - - return ddata - -def _read_text(filename, sep=None, first_column_names=False, as_strings=False): - """ - Return ddata dictionary. - - Parameters - ---------- - filename : str - Filename to read from. - sep : str, optional - Separator that separates data within text file. If None, will split at - arbitrary number of white spaces, which is different from enforcing - splitting at single white space ' '. - first_column_names : bool, optional - Assume the first column stores samplenames. - - Returns - ------- - ddata : dict containing - X : np.ndarray - Data array for further processing, columns correspond to genes, - rows correspond to samples. - rownames : np.ndarray - Array storing the names of rows (experimental labels of samples). - colnames : np.ndarray - Array storing the names of columns (gene names). - """ - data, header = _read_text_raw(filename,sep) - - if as_strings: - return _interpret_as_strings(data) - else: - return _interpret_as_floats(data, header, first_column_names) - -def _read_text_raw(filename, sep=None): - """ - Return data as list of lists of strings and the header as string. - - Parameters - ---------- - filename : str - Filename of data file. - sep : str, optional - Separator that separates data within text file. If None, will split at - arbitrary number of white spaces, which is different from enforcing - splitting at single white space ' '. - - Returns - ------- - data : list - List of lists of strings. - header : str - String storing the comment lines (those that start with '#'). - """ - header = '' - data = [] - for line in open(filename): - if line.startswith('#'): - header += line - else: - line_list = line[:-1].split(sep) - data.append(line_list) - return data, header - -def _interpret_as_strings(data): - """ - Interpret list of lists as floats. - """ - if len(data[0]) == len(data[1]): - X = np.array(data).astype(str) - else: - # strip quotation marks - if data[0][0].startswith('"'): - # using iterators over numpy arrays doesn't work efficiently here - # speed no problem here, only done once - for ir, r in enumerate(data): - for ic, elem in enumerate(r): - data[ir][ic] = elem.strip('"') - colnames = np.array(data[0]).astype(str) - data = np.array(data[1:]).astype(str) - rownames = data[:, 0] - X = data[:, 1:] - ddata = {'X': X, 'colnames': colnames, 'rownames': rownames} - return ddata - -def _interpret_as_floats(data, header, first_column_names): - """ - Interpret as float array with optional colnames and rownames. - """ - # if the first element of the data list cannot be interpreted as float, the - # first row of the data is assumed to store variable names - if not is_float(data[0][0]): - sett.m(0,'--> assuming first line in file stores variable names') - # if the first row is one element shorter - colnames = np.array(data[0]).astype(str) - data = np.array(data[1:]) - # try reading colnames from the last comment line - elif len(header) > 0 and type(header) == str: - sett.m(0,'--> assuming last comment line stores variable names') - potentialnames = header.split('\n')[-2].strip('#').split() - colnames = np.array(potentialnames) - # skip the first column - colnames = colnames[1:] - data = np.array(data) - # just numbers as colnames - else: - sett.m(0,'--> did not find variable names in file') - data = np.array(data) - colnames = np.arange(data.shape[1]).astype(str) - - # if the first element of the second row of the data cannot be interpreted - # as float, it is assumed to store sample names - if not is_float(data[1][0]) or first_column_names: - sett.m(0,'--> assuming first column stores sample names') - rownames = data[:,0].astype(str) - # skip the first column - X = data[:,1:].astype(float) - if colnames.size > X.shape[1]: - colnames = colnames[1:] - # just numbers as rownames - else: - sett.m(0,'--> did not find sample names in file') - X = data.astype(float) - rownames = np.arange(X.shape[0]).astype(str) - - ddata = { - 'X' : X, 'rownames' : rownames, 'colnames' : colnames - } - - return ddata - -def _read_hdf5_single(filename,key=''): - """ - Read a single dataset from an hdf5 file. - - See also function read_file_to_dict(), which reads all keys of the hdf5 file. - - Parameters - ---------- - filename : str - Filename of data file. - key : str, optional - Name of dataset in the file. If not specified, shows available keys but - raises an Error. - - Returns - ------- - ddata : dict containing - X : np.ndarray - Data array for further processing, columns correspond to genes, - rows correspond to samples. - rownames : np.ndarray - Array storing the names of rows (experimental labels of samples). - colnames : np.ndarray - Array storing the names of columns (gene names). - """ - with h5py.File(filename, 'r') as f: - # the following is necessary in Python 3, because only - # a view and not a list is returned - keys = [k for k in f.keys()] - if key == '': - raise ValueError('The file ' + filename + - ' stores the following sheets:\n' + str(keys) + - '\n Call read/read_hdf5 with one of them.') - # fill array - X = f[key][()] - if X.dtype.kind == 'S': - X = X.astype(str) - # init dict - ddata = { 'X' : X } - # set row and column names - for iname, name in enumerate(['rownames','colnames']): - if name in keys: - ddata[name] = f[name][()] - elif key + '_' + name in keys: - ddata[name] = f[key + '_' + name][()] - else: - ddata[name] = np.arange(X.shape[0 if name == 'rownames' else 1]) - ddata[name] = ddata[name].astype(str) - if X.ndim == 1: - break - return ddata - -def _read_excel(filename, sheet=''): - """ - Read excel file and return data dictionary. - - Parameters - ---------- - filename : str - Filename to read from. - sheet : str - Name of sheet in Excel file. - - Returns - ------- - ddata : dict containing - X : np.ndarray - Data array for further processing, columns correspond to genes, - rows correspond to samples. - rownames : np.ndarray - Array storing the names of rows (experimental labels of samples). - colnames : np.ndarray - Array storing the names of columns (gene names). - """ - # rely on pandas for reading an excel file - try: - from pandas import read_excel - df = read_excel(filename,sheet) - except Exception as e: - # in case this raises an error using Python 2.7 - print('if on Python 2.7 ' - 'try installing xlrd using "sudo pip install xlrd"') - raise e - return ddata_from_df(df) - -def _read_softgz(filename): - """ - Read a SOFT format data file. - - The SOFT format is documented here - http://www.ncbi.nlm.nih.gov/geo/info/soft2.html. - - The following values can be exported: - GID : A list of gene identifiers of length d. - SID : A list of sample identifiers of length n. - STP : A list of sample desriptions of length d. - X : A dxn array of gene expression values. - - We translate this to the conventions of scanpy. - - Note - ---- - The function is based on a script by Kerby Shedden. - http://dept.stat.lsa.umich.edu/~kshedden/Python-Workshop/gene_expression_comparison.html - """ - - with gzip.open(filename) as fid: - - #print(help(fid)) - - # The header part of the file contains information about the - # samples. Read that information first. - SIF = {} - for line in fid: - line = line.decode("utf-8") - if line.startswith("!dataset_table_begin"): - break - elif line.startswith("!subset_description"): - subset_description = line.split("=")[1].strip() - elif line.startswith("!subset_sample_id"): - subset_ids = line.split("=")[1].split(",") - subset_ids = [x.strip() for x in subset_ids] - for k in subset_ids: - SIF[k] = subset_description - - # Next line is the column headers (sample id's) - SID = fid.readline().decode("utf-8").split("\t") - - # The column indices that contain gene expression data - I = [i for i,x in enumerate(SID) if x.startswith("GSM")] - - # Restrict the column headers to those that we keep - SID = [SID[i] for i in I] - - # Get a list of sample labels - STP = [SIF[k] for k in SID] - - # Read the gene expression data as a list of lists, also get the gene - # identifiers - GID,X = [],[] - for line in fid: - line = line.decode("utf-8") - # This is what signals the end of the gene expression data - # section in the file - if line.startswith("!dataset_table_end"): - break - V = line.split("\t") - # Extract the values that correspond to gene expression measures - # and convert the strings to numbers - x = [float(V[i]) for i in I] - X.append(x) - GID.append(#V[0] + ";" + - V[1]) - - # Convert the Python list of lists to a Numpy array and transpose to match - # the Scanpy convention of storing observations in rows and variables in - # colums. - X = np.array(X).T - # rownames are the sample identifiers - rownames = SID - # labels identifying sets - setlabels = STP - # column names are the gene identifiers - colnames = GID - - ddata = { - 'X' : X, 'rownames' : rownames, 'colnames' : colnames, - 'groupnames_n' : setlabels - } - - return ddata - -#-------------------------------------------------------------------------------- -# Reading and writing for dictionaries -#-------------------------------------------------------------------------------- - -def read_file_to_dict(filename, ext='h5'): - """ - Read file and return dict with keys. - - The recommended format for this is hdf5. - - If reading from an Excel file, key names correspond to sheet names. - - Parameters - ---------- - filename : str - Filename of data file. - ext : {'h5', 'xlsx'}, optional - Choose file format. Excel is much slower. - - Returns - ------- - d : dict - Returns OrderedDict. - """ - sett.m(0,'reading file',filename) - d = odict([]) - if ext == 'h5': - with h5py.File(filename, 'r') as f: - for key in f.keys(): - # the '()' means 'read everything' (by contrast, ':' only works - # if not reading a scalar type) - value = f[key][()] - if value.dtype.kind == 'S': - d[key] = value.astype(str) - else: - d[key] = value - elif ext == 'xlsx': - import pandas as pd - xl = pd.ExcelFile(filename) - for sheet in xl.sheet_names: - d[sheet] = xl.parse(sheet).values - return d - -def write_dict_to_file(filename, d, ext='h5'): - """ - Write content of dictionary to file. - - Parameters - ---------- - filename : str - Filename of data file. - d : dict - Dictionary storing keys with np.ndarray-like data or scalars. - ext : string - Determines file type, allowed are 'h5' (hdf5), - 'xlsx' (Excel) [or 'csv' (comma separated value file)]. - """ - directory = os.path.dirname(filename) - if not os.path.exists(directory): - os.makedirs(directory) - if ext == 'h5': - with h5py.File(filename, 'w') as f: - for key, value in d.items(): - if type(value) != np.ndarray: - value = np.array(value) - # some output about the data to write - sett.m(1,key,type(value),value.dtype,value.dtype.kind,value.shape) - # make sure string format is chosen correctly - if value.dtype.kind == 'U': - value = value.astype(np.string_) - # try writing - try: - f.create_dataset(key,data=value) - except Exception as e: - sett.m(0,'error creating dataset for key =', key) - raise e - elif ext == 'xlsx': - import pandas as pd - with pd.ExcelWriter(filename,engine='openpyxl') as writer: - for key, value in d.items(): - pd.DataFrame(value).to_excel(writer,key) - -#-------------------------------------------------------------------------------- -# Helper functions for reading and writing -#-------------------------------------------------------------------------------- - -def ddata_from_df(df): - """ - Write pandas.dataframe to ddata dictionary. - """ - ddata = { - 'X' : df.values[:,1:].astype(float), - 'rownames' : df.iloc[:,0].values.astype(str), - # TODO: check whether we always have to start with the 1st - # column, same as in csv file - 'colnames' : np.array(df.columns[1:],dtype=str) - } - return ddata - -def download_progress(count, blockSize, totalSize): - percent = int(count*blockSize*100/totalSize) - sys.stdout.write("\r" + "... %d%%" % percent) - sys.stdout.flush() - -def check_datafile_present(filename, backup_url=''): - """ - Check whether the file is present, otherwise download. - """ - if filename.startswith('sim/'): - if not os.path.exists(filename): - exkey = filename.split('/')[1] - print('file ' + filename + ' does not exist') - print('you can produce the datafile by') - exit('running subcommand "sim ' + exkey + '"') - - if not os.path.exists(filename): - if os.path.exists('../' + filename): - # we are in a subdirectory of Scanpy - return '../' + filename - else: - # download the file - sett.m(0,'file ' + filename + ' is not present\n' + - 'try downloading from url\n' + backup_url + '\n' + - '... this may take a while but only happens once') - d = os.path.dirname(filename) - if not os.path.exists(d): - os.makedirs(d) - from .compat.urllib_request import urlretrieve - urlretrieve(backup_url, filename, reporthook=download_progress) - sett.m(0,'') - - return filename - -def is_filename(filename_or_key,return_ext=False): - """ Check whether it is a filename. """ - for ext in avail_exts: - l = len('.' + ext) - # check whether it ends on the extension - if '.'+ext in filename_or_key[-l:]: - if return_ext: - return ext - else: - return True - if return_ext: - raise ValueError(filename_or_key - + ' does not contain a valid extension\n' - + 'choose a filename that contains one of\n' - + avail_exts) - else: - return False - -#-------------------------------------------------------------------------------- -# Others -#-------------------------------------------------------------------------------- - -def pretty_dict_string(d, indent=0): - """ - Pretty output of nested dictionaries. - """ - s = '' - for key, value in sorted(d.items()): - s += ' ' * indent + str(key) - if isinstance(value, dict): - s += '\n' + pretty_dict_string(value, indent+1) - else: - s += ' = ' + str(value) + '\n' - return s - -def merge_dicts(*dicts): - """ - Given any number of dicts, shallow copy and merge into a new dict, - precedence goes to key value pairs in latter dicts. - - Note - ---- - http://stackoverflow.com/questions/38987/how-to-merge-two-python-dictionaries-in-a-single-expression - """ - result = {} - for d in dicts: - result.update(d) - return result - -def is_float(string): - """ - Check whether string is float. - - See also - -------- - http://stackoverflow.com/questions/736043/checking-if-a-string-can-be-converted-to-float-in-python - """ - try: - float(string) - return True - except ValueError: - return False - -def is_int(string): - """ - Check whether string is integer. - """ - try: - int(string) - return True - except ValueError: - return False - -def convert_bool(string): - """ - Check whether string is boolean. - """ - if string == 'True': - return True, True - elif string == 'False': - return True, False - else: - return False, False - -def convert_string(string): - """ - Convert string to int, float or bool. - """ - if is_int(string): - return int(string) - elif is_float(string): - return float(string) - elif convert_bool(string)[0]: - return convert_bool(string)[1] - else: - return string - -def masks(list_of_index_lists,n): - """ - Make an array in which rows store 1d mask arrays from list of index lists. - - Parameters - ---------- - n : int - Maximal index / number of samples. - """ - # make a list of mask arrays, it's easier to store - # as there is a hdf5 equivalent - for il,l in enumerate(list_of_index_lists): - mask = np.zeros(n,dtype=bool) - mask[l] = True - list_of_index_lists[il] = mask - # convert to arrays - masks = np.array(list_of_index_lists) - return masks - -def warn_with_traceback(message, category, filename, lineno, file=None, line=None): - """ - Get full tracebacks when warning is raised by setting - - warnings.showwarning = warn_with_traceback - - See also - -------- - http://stackoverflow.com/questions/22373927/get-traceback-of-warnings - """ - traceback.print_stack() - log = file if hasattr(file,'write') else sys.stderr - sett.write(warnings.formatwarning(message, category, filename, lineno, line)) - -def transpose_ddata(ddata): - """ - Transpose a data dictionary. - - Parameters - ---------- - ddata : dict containing (at least) - X : np.ndarray - Data array for further processing, columns correspond to genes, - rows correspond to samples. - rownames : np.ndarray - Array storing the names of rows. - colnames : np.ndarray - Array storing the names of columns. - Returns - ------- - ddata : dict - With X transposed and rownames and colnames interchanged. - """ - ddata['X'] = ddata['X'].T - colnames = ddata['colnames'] - ddata['colnames'] = ddata['rownames'] - ddata['rownames'] = colnames - return ddata - -def subsample(X,subsample=1,seed=0): - """ - Subsample a fraction of 1/subsample samples from the rows of X. - - Parameters - ---------- - X : np.ndarray - Data array. - subsample : int - 1/subsample is the fraction of data sampled, n = X.shape[0]/subsample. - seed : int - Seed for sampling. - - Returns - ------- - Xsampled : np.ndarray - Subsampled X. - rows : np.ndarray - Indices of rows that are stored in Xsampled. - """ - if subsample == 1 and seed == 0: - return X, np.arange(X.shape[0],dtype=int) - if seed == 0: - # this sequence is defined simply by skipping rows - # is faster than sampling - rows = np.arange(0,X.shape[0],subsample,dtype=int) - n = rows.size - Xsampled = np.array(X[rows]) - if seed > 0: - n = int(X.shape[0]/subsample) - np.random.seed(seed) - Xsampled, rows = subsample_n(X,n=n) - sett.m(0,'subsampled to',n,'of',X.shape[0],'data points') - return Xsampled, rows - -def subsample_n(X,n=0,seed=0): - """ - Subsample n samples from rows of array. - - Parameters - ---------- - X : np.ndarray - Data array. - seed : int - Seed for sampling. - - Returns - ------- - Xsampled : np.ndarray - Subsampled X. - rows : np.ndarray - Indices of rows that are stored in Xsampled. - """ - if n < 0: - raise ValueError('n must be greater 0') - np.random.seed(seed) - n = X.shape[0] if (n == 0 or n > X.shape[0]) else n - rows = np.random.choice(X.shape[0],size=n,replace=False) - Xsampled = np.array(X[rows]) - return Xsampled, rows - -def comp_distance(X,metric='euclidean'): - """ - Compute distance matrix for data array X - - Parameters - ---------- - X : np.ndarray - Data array (rows store samples, columns store variables). - metric : string - For example 'euclidean', 'sqeuclidean', see sp.spatial.distance.pdist. - - Returns - ------- - D : np.ndarray - Distance matrix. - """ - D = sp.spatial.distance.pdist(X,metric=metric) - D = sp.spatial.distance.squareform(D) - sett.mt(0,'computed distance matrix with metric =', metric) - sett.m(5,D) - if False: - pl.matshow(D) - pl.colorbar() - return D - -def hierarch_cluster(M): - """ - Cluster matrix using hierarchical clustering. - - Parameters - ---------- - M : np.ndarray - Matrix, for example, distance matrix. - - Returns - ------- - Mclus : np.ndarray - Clustered matrix. - indices : np.ndarray - Indices used to cluster the matrix. - """ - link = sp.cluster.hierarchy.linkage(M) - indices = sp.cluster.hierarchy.leaves_list(link) - Mclus = np.array(M[:,indices]) - Mclus = Mclus[indices,:] - sett.mt(0,'clustered matrix') - if False: - pl.matshow(Mclus) - pl.colorbar() - return Mclus, indices - -def check_datafile_deprecated(filename, ext=None): - """ - Check whether the file is present and is not just a placeholder. - - If the file is not present at all, look for a file with the same name but - ending on '_url.txt', and try to download the datafile from there. - - If the file size is below 500 Bytes, assume the file is just a placeholder - for a link to github. Download from github in this case. - """ - if filename.startswith('sim/'): - if not os.path.exists(filename): - exkey = filename.split('/')[1] - print('file ' + filename + ' does not exist') - print('you can produce the datafile by') - exit('running subcommand "sim ' + exkey + '"') - if ext is None: - _, ext = os.path.splitext() - if ext.startswith('.'): - ext = ext[1:] - if not os.path.exists(filename) and not os.path.exists('../'+filename): - basename = filename.replace('.'+ext,'') - urlfilename = basename + '_url.txt' - if not os.path.exists(urlfilename): - urlfilename = '../' + urlfilename - if not os.path.exists(urlfilename): - raise ValueError('Neither ' + filename + - ' nor ../' + filename + - ' nor files with a url to download from exist \n' + - '--> move your data file to one of these places \n' + - ' or cd/browse into scanpy root directory.') - with open(urlfilename) as f: - url = f.readline().strip() - sett.m(0,'data file is not present \n' + - 'try downloading data file from url \n' + url + '\n' + - '... this may take a while but only happens once') - # download the file - urlretrieve(url,filename,reporthook=download_progress) - sett.m(0,'') - - rel_filename = filename - if not os.path.exists(filename): - rel_filename = '../' + filename - - # if file is smaller than 500 Bytes = 0.5 KB - threshold = 500 - if os.path.getsize(rel_filename) < threshold: - # download the file - # note that this has 'raw' in the address - github_baseurl = r'https://github.com/theislab/scanpy/raw/master/' - fileurl = github_baseurl + filename - sett.m(0,'size of file',rel_filename,'is below',threshold/1000.,' kilobytes') - sett.m(0,'--> presumably is a placeholder for a git-lfs file') - sett.m(0,'... if you installed git-lfs, you can use \'git lfs checkout\'') - sett.m(0,'... to update all files with their actual content') - sett.m(0,'--> downloading data file from github using url') - sett.m(0,fileurl) - sett.m(0,'... this may take a while but only happens once') - # make a backup of the small file - from shutil import move - basename = rel_filename.replace('.'+ext,'') - move(rel_filename,basename+'_placeholder.txt') - # download the file - try: - urlretrieve(fileurl,rel_filename,reporthook=download_progress) - sett.m(0,'') - except Exception as e: - sett.m(0,e) - sett.m(0,'when calling urlretrieve() in module scanpy.utils.utils') - sett.m(0,'--> is the github repo/url private?') - sett.m(0,'--> if you have access, manually download the file') - sett.m(0,fileurl) - sett.m(0,'replace the small file',rel_filename,'with the downloaded file') - quit() - - return rel_filename diff --git a/scripts/RData_to_other_formats.R b/scripts/RData_to_other_formats.R deleted file mode 100644 index 40ffca71ae..0000000000 --- a/scripts/RData_to_other_formats.R +++ /dev/null @@ -1,117 +0,0 @@ -# -# Transfrom RData File to Other Formats -# -# Based on a script by Maren Buttner. -# -# Copyright (c) 2016 F. Alexander Wolf (http://falexwolf.de). -# - - -# specify the RData file name -datafilename = "Paul_Cell_MARSseq_GSE72857.RData" -# specify the name of the outfile without extension -outbasename = "paul15" - -# choose format: must be one of "csv", "xlsx" (stores multiple files, single csv -# and or xlsx files per data object) or "xlsx", "onehdf5" (stores one file for -# all data frames) -#format = "csv" -format = "onehdf5" -#format = "onexlsx" - -if (format == "onehdf5") -{ - library(rhdf5) - outfilename <- paste(outbasename,'.h5',sep="") - h5createFile(outfilename) -} - -if (format == "onexlsx") -{ - # the xlsx package needs java - # library(xlsx) - # the openxlsx package doesn't need java, and provides some additional - # functionality, like creating workbooks to write to a common single excel file - library(openxlsx) - wb <- createWorkbook() - outfilename <- paste(outbasename,'.xlsx',sep="") -} - -objlist = load(datafilename) - -for(objname in objlist) -{ - print(paste("-----",objname,"-----")) - # as data frame - if(format!="onehdf5") - obj <- as.data.frame(get(objname)) - # as matrix, h5write seems to have problems with some data frames - if(format=="onehdf5") - obj <- get(objname) - # print properties of the object - # print(head(obj,n=3)) - # print(class(obj)) - # print(dim(obj)) - # print(head(rownames(obj))) - # print(!is.null(rownames(obj))) - # print(head(colnames(obj))) - # print(!is.null(colnames(obj))) - - if(format=="csv") - write.csv(obj,file=paste(outbasename,"_",objname,".csv",sep="")) - - if(format=="onehdf5") - { - - # write rownames and colnames separately - if(is.matrix(obj)) - { - rn <- rownames(obj) - if(!is.null(rn)) - h5write(rn, file = outfilename, - paste(objname,"_rownames",sep="")) - cn <- colnames(obj) - if(!is.null(cn)) - h5write(cn, file = outfilename, - paste(objname,"_colnames",sep="")) - } - - # treat factor vectors separately - # otherwise this will give errors - if(is.factor(obj)) - { - h5write(as.integer(obj), file=outfilename, paste(objname,"_codes",sep="")) - h5write(levels(obj), file=outfilename, paste(objname,"_strings",sep="")) - } - # write other matrices and vectors here - else - # here we assume we will read with C-style program - # where data looks transposed as compared to the Fortran-style R - # therefore, we therefore transpose the data to match - # the C convention - h5write(t(obj), file = outfilename, objname) - - } - - if(format=="xlsx") - write.xlsx(df, file=paste(outbasename,"_",objname,".xlsx",sep="")) - - if(format=="onexlsx") - { - addWorksheet(wb, objname) - writeData(wb, objname, x = obj) - } - -} - -if(format=="onehdf5") -{ - # see properties of the file - print(h5ls(outfilename)) - H5close() -} - -if(format=="onexlsx") - saveWorkbook(wb, file = outfilename, overwrite=TRUE) - - diff --git a/scripts/diffmap.py b/scripts/diffmap.py deleted file mode 100755 index d128d024d2..0000000000 --- a/scripts/diffmap.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 -""" -Wrapper for single Scanpy Tool: diffmap -======================================= - -For package Scanpy (https://github.com/theislab/scanpy). -Written in Python 3 (compatible with 2). -""" - -from sys import path -# scanpy, first try loading it locally -path.insert(0, '.') -import scanpy as sc - -dtool, ddata = sc.read_args_run_tool('diffmap') -sc.plot(dtool, ddata) diff --git a/scripts/scanpy.py b/scripts/scanpy.py deleted file mode 100755 index 4709c8f3fd..0000000000 --- a/scripts/scanpy.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 -""" -Wrapper for scanpy.scanpy -========================= - -For package Scanpy (https://github.com/theislab/scanpy). -Written in Python 3 (compatible with 2). -""" -from sys import path - -# scanpy, first try loading it locally -path.insert(0, '.') -from scanpy.__main__ import main - -main() diff --git a/setup.py b/setup.py deleted file mode 100644 index 58e015f125..0000000000 --- a/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -import sys -from setuptools import setup - -more_requires = [] -if sys.version_info[:2] < (3, 5): - more_requires.append('configparser') - -setup( - name='scanpy', - version='0.1', - description='Single-Cell Analysis in Python.', - url='http://github.com/theislab/scanpy', - author='F. Alexander Wolf', - author_email='alex.wolf@helmholtz-muenchen.de', - license='GPL-3.0', - entry_points={ - 'console_scripts': [ - 'scanpy = scanpy.__main__:main', - ], - }, - install_requires=[ - 'matplotlib', - 'pandas', - 'scipy', - 'xlrd', # for reading excel data - 'h5py', - ] + more_requires, - packages=['scanpy'], - zip_safe=False, -) diff --git a/sim/krumsiek11.txt b/sim/krumsiek11.txt deleted file mode 100644 index 4a6a6081d9..0000000000 --- a/sim/krumsiek11.txt +++ /dev/null @@ -1,54 +0,0 @@ -# See Table 1 in Krumsiek et al. (2011), p. 3 or -# Table 1, in Suppl. Mat. of Moignard et al. (2015), p. 28. -# -# For each "variable = ", there must be a right hand side: -# either an empty string or a python-style logical expression -# involving variable names, "or", "and", "(", ")". -# The order of equations matters! -# -# modelType = hill -# invTimeStep = 0.02 -# -# boolean update rules: -Gata2 = Gata2 and not (Gata1 and Fog1) and not Pu.1 -Gata1 = (Gata1 or Gata2 or Fli1) and not Pu.1 -Fog1 = Gata1 -EKLF = Gata1 and not Fli1 -Fli1 = Gata1 and not EKLF -SCL = Gata1 and not Pu.1 -Cebpa = Cebpa and not (Gata1 and Fog1 and SCL) -Pu.1 = (Cebpa or Pu.1) and not (Gata1 or Gata2) -cJun = Pu.1 and not Gfi1 -EgrNab = (Pu.1 and cJun) and not Gfi1 -Gfi1 = Cebpa and not EgrNab -# coupling list: -Gata2 Gata2 1.0 -Gata2 Gata1 -0.1 -Gata2 Fog1 -1.0 -Gata2 Pu.1 -1.15 -Gata1 Gata2 1.0 -Gata1 Gata1 0.1 -Gata1 Fli1 1.0 -Gata1 Pu.1 -1.21 -Fog1 Gata1 0.1 -EKLF Gata1 0.2 -EKLF Fli1 -1.0 -Fli1 Gata1 0.2 -Fli1 EKLF -1.0 -SCL Gata1 1.0 -SCL Pu.1 -1.0 -Cebpa Gata1 -1.0 -Cebpa Fog1 -1.0 -Cebpa SCL -1.0 -Cebpa Cebpa 10.0 -Pu.1 Gata2 -1.0 -Pu.1 Gata1 -1.0 -Pu.1 Cebpa 10.0 -Pu.1 Pu.1 10.0 -cJun Pu.1 1.0 -cJun Gfi1 -1.0 -EgrNab Pu.1 1.0 -EgrNab cJun 1.0 -EgrNab Gfi1 -1.3 -Gfi1 Cebpa 1.0 -Gfi1 EgrNab -5.0 diff --git a/sim/krumsiek11_params.txt b/sim/krumsiek11_params.txt deleted file mode 100644 index 454e4d60ee..0000000000 --- a/sim/krumsiek11_params.txt +++ /dev/null @@ -1,8 +0,0 @@ -model = sim/krumsiek11.txt -tmax = 800 -branching = True -nrRealizations = 4 -noiseObs = 0 -noiseDyn = 0.001 -step = 5 -seed = 0 diff --git a/sim/toggleswitch.txt b/sim/toggleswitch.txt deleted file mode 100644 index 64ada5a8d5..0000000000 --- a/sim/toggleswitch.txt +++ /dev/null @@ -1,16 +0,0 @@ -# For each "variable = ", there must be a right hand side: -# either an empty string or a python-style logical expression -# involving variable names, "or", "and", "(", ")". -# The order of equations matters! -# -# modelType = hill -# invTimeStep = 0.1 -# -# boolean update rules: -0 = 0 and not 1 -1 = 1 and not 0 -# coupling list: -0 0 1.0 -0 1 -1.0 -1 1 1.0 -1 0 -1.0 \ No newline at end of file diff --git a/sim/toggleswitch_params.txt b/sim/toggleswitch_params.txt deleted file mode 100644 index 70cfbcd981..0000000000 --- a/sim/toggleswitch_params.txt +++ /dev/null @@ -1,8 +0,0 @@ -model = sim/toggleswitch.txt -tmax = 100 -branching = True -nrRealizations = 2 -noiseObs = 0.01 -noiseDyn = 0.001 -step = 1 -seed = 0