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": "iVBORw0KGgoAAAANSUhEUgAAAO0AAABzCAYAAAB0IYW8AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAAEnQAABJ0BfDRroQAAIABJREFUeJztnXl4VNX5xz8nMyFhCc6wY5CtLIpECJtoMToDKlVQrAtu\nlMivilSBEgIqCIoSsAKhCC0oNWpVytaCJeICTDQuIAiTQLUQ9iWEBnECCZFAJuf3x507mT2zJkTn\n+zzzzMy9557zzr3znvOedxVSSqKIIor6g5i6JiCKKKIIDFGmjSKKeoagmVYI0VgI8bYQopvt+3Qh\nxDNCiAnhIy+KKKJwhTaEaxMBs8P3LOAs8DyAEKIlMAioAE6HME4UUfyS0BKIA76UUnrkm6CZVkpZ\nIIS4weGQFZgFZNi+D3rhhRf+1b9/f1q1ahXsMDWiuLgYIKJj/NzG+Tn9lp/bOMXFxezYsYNZs2b9\nFljnqU0oKy2ABFoLIa4A5gGfAXcBfwcq+vfvz5133hniEL5RWFgIQGJiYtB9pKam8vbbb0d8HH/g\nbRx/aAx1jFDhSmNd37P6Oo4NFd5OhMS0Usq/O3y9xeX06UjPfFBrN/BnNc7P6bf8HMex8Y3XLWVU\newyMGDGirkmoEVEao1ARZVrqx58tSmMUKkLd00YRxS8WeXl5rFu3Dq1WS9++fbnjjjsAWL9+Pddf\nfz1t27a1t33++eeJiYnhtttuY9CgQSGNG2VawqvkiRSiNNYNvv66kObN4+nevbnbuY0bNzJr1iwA\n5s6dy969e6moqODixYvEx8fz7rvvcvbsWYYNG8b48eM5ffo0+fn5ITNtVDyOIgofOHeugvLySp9t\nli9fzscff0xCQgLHjx+nU6dOJCUl0bRpU3Q6HQcOHKC0tJT169fzyCOPhExTlGmpH3uxKI11g6FD\nO5Oc3NrjuXvuuYcZM2ZgsVgYMmQIVquV8vJyWrRoQW5uLrt378ZqtVJWVsZjjz1GTEwMX3/9dcg0\niUhF+Qgh+m/fvn17//79I9J/FFH8XLFjxw4GDBgwQEq5w9P56EobRRT1DOEMGBglhBgvhHgsfOTV\nDlJTU+uahBoRpTEKFaGstK4BA9dIKRcD3dUDxcXFdtevKKLwB6VGI6VGo9sxi1brdtwN47Yqr3qM\nwsJCu4+zN4QzYEBVsVUF0o9FrwdAb7HYj5UajSSYTMp5IbxfrNGA1ep8TKejyVMlsGY2ZQdfQNdr\nLSW77qFJt9kAaBOSXH5IFSNmeFagWIRwH0Onc6K1tuBLyWPR66GkBHQ65UBJif2c1mCw38tgYZxv\nJGdfDobuBnL35wLw6GgrDeLgYgU0iFPa/bddDPC2z74sWq3z/XS9vxqN0k597rbv2pQUKnNz7cd1\nfT9QzssqEDHVnwHRPxtp+4yQUKXobSrLvkPb5FrEzvAozAKx0xYUFDBv3jyWL18e8rjhDBg4LIR4\nCtinnmzVqpVPf02LVgsJCcpnR+bUaOwMo/dHUbai+lopS6AqBjHzefRVMRBzD8q08LzSwG0O0DBi\nhLNKv9RopDInx+PYFr3eaVIJBuN2aalC+aPGoPwp42MSOG8tcWonhHLeKq1c3cMAOP/ZLHo9lJZC\nQgJ6KasnQAe6LXq9ci8DnGz0E/WUlJegiVHo++NTOg4czyH1Jg0xWitVlRre6FtJZoGRtG7KvchM\n8L4Sqswa0iTSP7v6845hHpuoz05rMFBpVgRB/YMbAYg1a2Gb5+u8YfGWxXRu2Zk7r3MPfAnEThsf\nH0+nTp1qHC8xMZGTJ0/6bBPOgAGPmi5PUB+gXwzpBRkXtFRorEgBPKAShE3gt824COLQMV1jYaZV\ni8Tq1o8AXnI5Vmk2e6VNb7HYmcMfTMrTU15VgmN3Jwvhqy06Si+UAjD0TivNW5Tw71XKSmlZZHFg\nGgANKV0hLb26D3XC01dWTziemFI9VqNo6XjNRD0331ZK67bK7VQnmHaJGuJjEljYu3oclWFdPzvR\nGQqzGj+GUttv9MKojghVqnDFqBtG0UDTwGcb1U776KOPcuDAAQYOHEhSUhJHjhwB4MCBA4waNYqN\nGzeGhaZa9YgKy2y7Rs+Me0qIr4KX4qWyhzGfUc55mEUzrHpmWAWdMDBG43lMR08ei1aLNiXFJwna\n5GS/SH1ip6DoJGT/G/uKlRCfoDDlFj0pXVMwpTvQdGP1R8siZwZ0VPJY9Hq0KSlh/4OCwrD3/64E\njdCwtE9gk6qrR5RFq3WaVAJG/2zo1xyWVu/C0k4dwFxxnuS4xmS26eJXN8OP7WFD+6SaG3qArpHO\n6znVTpuQkODVTtu+fXvKy8sBEL62eoFAShmRF9B/+/btUsU5g0GeMxhkSNhkkDMu4nzs+g2h9Sml\nHD16tJQyMBp9tVuwzyAf/xapeUITMm0qHGn8UacLqo+arhv9NfLxb/HZxuf1NhrVsUJ63oaP3A5N\nKtovjUfypJRSGo/k2T/fciRPDju6295mUtF+p/bDju62t60P2L59uwT6Sy+8VWsrbaXZHLICZ+bN\nOXSMMVQfMH4c8B7FE1QlTzhoBDhebibrDQ2Vr4ewyrjAUREVLI2+rhvxoRZdM3ijb/BbFldlWbgl\ngfyK82zp0AuALR16kXbqAIOP5pPToRfDj+1h+LE9lNsUUIaj+cTY2oGyQqedOgDg9wp92cIbN4f6\nwmGlDccq++ZpnXzztMNK8eTXyitMCIskIKX8o1kn7/wg+NWqJgS7ykrpXTp4dJNO/n5H+GgOhUYp\npcfnqq6kvlBTG6PDinw5o6aVttY8okKadTcbKWpUwpgWDivFvrNOe51wIBAaPSl2MguMWMpLyL4r\nMq6hpUZjRMxN8boSlve7jJLW7zsb1GU17Vu3dOgV9N72ckKtiMeq6j0UtI0zOB/ofkXIfapITU1l\ncRj6MR8zU5hncFIohQvhoNHTpJRZYOTgZwboF2Ln2Gg8diz0icXl2Q4+mm8Xc2sbVZWViJgYRIz7\n+jZ79my7cmny5MnEx8e7tXnjjTdISEjgoYceChtNEV9pw7E6ZPw6x1nzO25r2FfZQOGJATSNS5y1\nwZchXCWE3WdyLi+aPTzbXnGN64gY2DF/PvvXeUyKCEBsbCxVVVXMnz+f559/nt27d5ORkcGtt97K\npUuXaNeunbpdDBsu/4CBNXpo4F3tHg7cvnNnUOK7IwNkFhg59pXBR+vQEImwt3DTHAka004dqFPF\n0fXPPku3e+/1eC4mJoapU6fSsGFDpk2bRvv27dm0aRPPPfccN954I7GxsVx77bVhp+myZ9qsQaVM\n10TWbXBYy5ZBXefI6N9bciO6Yg1+7bWwaGMd+zAfM4eV5p9jPK0/MBqNzJ07l1OnTjF48GDmzp3L\n1q2R84EOek8rhBgGdAK0UsqFQojHgca27/NBCRhoWlFRHUEQKNboKfptQrBX1yoqSusHnaG6YNZV\n35cjpk2bBkCfPn3o06cPAHv27EGv13PzzTcD0KFDBzp06OB3n/4EDISy0t4olageNblxI+AKqgMH\nKBv3AU1Wrw6u981GsgzQFhfvowjsZ8fuKgvqOlU8ziwwUpjnn5dUsAiWRldEkqlCpTHt810MfrZR\n9fc6Fo2DQVJSEn/4wx+YPn16xMZwYlohxEIhxFwhxBw/rlWZU91l66SUs4AWaoPO/3w8+ATPFjNF\nery6HoYTDUb0COo6lQE8+dyGE4eX6KnsfTysfUZiogn2PjqiV1xjuxPELxGJiYk1lh1xFY/3A2/6\n2f9OIcRE4CchRD/ggq1inu+13R9sNpIxvJTpmvB5FPlCsHsxVRwctVmPKT30fffhJXpkhXOkD0KD\naJDAw39cGnL/UE2zsp8Njeb7lhWw7VAZVtu0HVMeolmme1My23SpN55L6enp6G3BI6oYnJ+fz9mz\nZ+0rraNZKD09nbi4uJDHdWXa5sAzts+uwS9OkFJ+4HLo25CpAUUs7pEDsZHVGDsiWKZVV9rk9qGv\nWIcytYgGCXSe7Nk8UHNQl38IF80qw554tY/D0T5e2/uFfeegTfXXy0E8rijOIyZeT2xT932pEILY\n2FhiYmL45ptv+O6775g7dy5z5jgLqrGxsbRt2zYsDAvue9ofgFig7tJNFOdypLXGu8a4ju2znmA+\nFprzyKFMLfHtUuj0dO0E15cajSHR7JlhQ0PaqQNkrqyuOWU4mh+2vkPBpbOHsJYVeTyn0+mYOnUq\n999/Pw0bNgTAYrE42WWFEEydOpVRo0aFjSZXpo2VUs5AYdywYNdA1wXZB9boyRiZwEvexOIIpRIZ\n3tL/WFNHqIqoUPaGh5foiW+XwpUP+N4XB0ujJySYTEHT7IthU7uHWCHRNiFntulCTodedb7KAjTp\n+lvirxzo8VxJSQlz5szh/fffJykpiWeeeYaZM2faj7/33nvhC8dzgKt43FgIMRsoD9cAfbbd7V/D\nNf4Hlocbze9sH9R1CSYTmQXBM9PhJXriWiXXyLAQPI2eEArNvlbYT84Frz3OnHUaltY9kwaCefPm\nuR1bvDgcDrG+YWdaIcSvgR9tr9r3Htcnk2Ew+3akiJBoHIpTwO2/M5O2LXix1h+GhfA6LgRLc/cZ\neT5F4qVLJwVNU9oLLckM+upfFhzF4++BT4EvgF21SsVmP2f+CInHoTDEK7ODu+7wEn1Ae9hwMu34\n+yJjUw6FxsxZXsuxRuECO9NKKS3A74CRwIO1SoXFTJaBmt0VL0MlVDB7w5Orw7c/DQZz38sN+Jr7\nlhWw7+XeEaBGQdqD4dGs/hLgqohqClgJMA2qL/iliNInU4Qf2szLTBGVWWDkgwBzdZ1cbeTCidyA\nNcXhVER9tdp3DixP2FNYs5ojNTU1MMWjDWmnDpB5c/g00bWF2bNnk5GRQUZGBl9++SWffvopRUVF\nPPhgZNc8V6ZdBGQC7jvsIFGjImqNHoaY3N0VPSFCK22wSp60bibuvgOMxlXo9f4pICqKzXROC9xp\nJJyKqOx/B35NUmKjmhsRgOLRAZltuly2Scb/vvU0OXu9B+XHxsbSvn17hBB8++23nDlzhh49QvcM\n8wVXpn0MmEK1g4VXCCGG2cqATLJ9f0AIkWbzkvIf+mQyrPqa3RUj+FCD3YtlFhh5MaOY3NwTAH4x\nblyr4PaT4drTZhYYeXFZYDba7jPyWPtktxrbhUTjZbj1AbgjSUe/jk08nlNtsO3bV0+oPXv2RGNL\nsB4phCIeuwYMGIFLwEG1gV9lQep4lQU//2wrhJtZKvvf8GLyDCorJ2OxjAeUVdcbDi/R+60tDopG\nV3igOa2biZQDFoSYjxDz0WoXBEWPJwTLtGmf167eMxC0aBJLQrxnJlRtsK622FBss/5E+bgmY7sK\nuAFo6S2plEPbl2zvc2zvb9neF9ne+2dnZ8uPkt/0ncVqk0G+WelHQrUwJnFzxc7r1/tusFrn/G7D\ngn0GaTCsdEqYptO95rWbQ4tDS3hWI52O2GSofneg+9FNOjeaYZ7Xbu5dui9yNEbhhhMnTsjs7Gyf\nid1cGXEOMB140dsFDm3vBiYCM1AyDKUCTwMTpIe8xx7hL8NGGI75ej1ikwONDgyge/hat6YGw0qP\njFu4KrTfWSONrnCkeZPz2J7o8zbZBMK0AdNow6TPdgZ13c8VgWZjlCghd77rICgr9AdSykVSypel\nlN9KKd+WUi6RUr5W07WO8Dv0rq4UFTYbcpbVyAyrAH21KD/sLvfmJtNIAISYXyvkBYpRm/VYLOM9\nZpMMp6gcCOqj5rgu4cq0H6HsZ1eGbYQamC3L6qcpI8J7Wp+miiEmjpDLyxpJxqBqG6eqhXVlAItl\nPFKmh3XPOGLEiIAnrplWrfIy5Nonn3eHWDAaV7kFw1ss40lJaedE833LCvxSQIVCI1zee9rLEtJZ\n5H0FuAbo5m1p9veFTTw2GFZ6lgFsIpvf4nEE97Q+4SLCq58X7DNIwzz/aId58r2xv/J63us9CrKd\n6719s9JgP3b3ukZ+jQPzZOcnvvBvvGBorCeoqqryem7NmjVy1qxZcsKECXLRokXylVdekRMnTpQH\nDx6U7733XtBjBloW5L/AABQxuSAck4IqLoaMujIJDDGBqzSw2UjaEBPZG1dBes25kQpXbeTKBw54\nFJk1GlUD6Vmc1mgEVqtEyvSA7mWW1WjfeozRmMjqrWcMcO61LBjhm2Z1nNbjv0GrXUBKSjv7MVU7\n7o2WQJ932qkDtmCBy9Pk89GpObSO705f/X1u586ePUvLli0ZMmQIX3zxBc888wynT5/m7NmzEYnu\nUeHKtHpARxijfADPeZ2GmJz+WAFfH0a4VnuzY7NRYVoHKPQaGbWqJybTf4Ca8y5VFCt2USnTfbbz\nhbZtb6ao6POgr0efTGaBEXPbYmBkjTTft6yA/y2+HhZfj1a7wD6pOE4yrzKFGI0GaSsKvUJo2FUV\nmONIZpsul3V0zx1tved6uvHGG9Hr9SxcuJArrlASrJeWllJeXh72XMeOcGXaU1LKPwshxoVzEOO+\nY5hwYbrNRsYM8VMJVQuz8K6BH/jtzZNlgMKF1Xl8fK1aJ1cbPTpUrDIaGWm7ZoFWa//ju0LYmKJb\nmzYB0eiKMTnAEBNp79dMsysWJsymQq0u70BmupRotQuw2oLC2rTeEDCNaZ/vqreKqPz8fPbv34+U\nEqvVyuzZs7FYLDz11FN88MEHHD16lFGjRnHVVVeFd2DpvA9dBMwC/oLN/hrsi5pMPoGYeyK8n123\nbp3nE5s80/fmaZ18dKW7uccfzAM5D+R8jcb+vtKPwl9eaXSFF13Bm5UG+Z/VGr/3nPcu3SdXGgxy\nvkYjX/OzoJbfNEbhEzXtaV0Z7UmU1Xeatwv8fdXItDIAJVRtwNPE4GViefO0zk0J5akiXeEqg92h\nQmXSsNPoAR5p9nDMWxW9lQaD7H//UjlfE776ut4QtdG6I1A7bTGKMuqTcK7mbuYUm/nhcrLRGvcd\ncz/oRXwf08JC4lXOrmbeRM1//bGU+ULQLiWF9BD3OR5pdMVmz3qCMRoT361wronjjeYFLUeyffWT\nTA6yiru/kT71NbqnruHKtF2BvijeTj7hEDCQ5nBsrhDiNte2bnscmxLKb0R4T5uamuqu9fQRmJ9l\nNTKp3Nmn2pOzwrl9OUyurCRdSvv+Naw0+qDPE5bd4Owz44nmBVot7Q3B1fdJTU0F/I/0uZyje/xB\nfn4+s2bNYtKkSZgdKkNmZGREdFxXpi0CnkLJf1wT1ICBlqBE+QBOWabtAQOuD8bLauAVtfBgva0O\nnugsOQjp7y5zOua6am0bJ7h6RgQ0iCHci25x55y+u9K8ymikXUqKX7GzPuEnjWmnDly2ph4Vlfv2\nYT11yuO5Dz/8kBdeeIGFCxeyYcMGFi9ezIYNGzh+/DgLFy4kOzubJ598kn/84x988sknLFu2jD/9\n6U+sXbvW63jBlAVpCpyxvdf4e9QPQogGwCCgF3Cza0NPYt3ltNKOGDEiIGkgrZuJu//+iNMxx1Vr\nsV5PvM45b/PwY3sYHEJaUDWCxqeIbDNReZsQ161wfjSuK+2J3FzWPLAs6AwVftHoiH3nam5Tx7B+\n+y1V+/Z5PBfjULP2m2++IS4ujsLCQnr37s2kSZPYs2cPXbp04aGHHmL37t0kJCTQvHlz9u7dGxJN\nrkyrA+4FmvlxrVphoBy4Tko5AVgL2I2JrVq1IjEx0V2s8/HHckMtrLIeQ8pqyFuVEO9ccMt11brC\noRqKmjG/V1xjhh/bExKNpu4+guFte3BPk01mgZFh9zvbeR1pXqDVBr2HdaPRXweL7v6sDXWLuEce\nIfZmt3UIgNtvv51p06YxefJkBg8ezMWLF7nyyiv573//S1ZWFn369GHv3r0sXryYPn36kJeXx/nz\n56mq8h756k9ZkJA0xL5eOGiP3cK1LiNzjyOc6PRi7pHSsyugqol9Tadzi+gZdnS302fH74HCp8nG\nB832ax3aOGqPVbNTt+fNQdOmwp/wvElF++vONbUWkZGREfA1AZl8wvnCm8knUJ/jWoC3kDJvNBoM\nK+Wbp91tl+cMBvmaTucWNzupaL/T92FHd7sd85tGX390H5Pho5t0Xs+pdthQGTaQ0LxAf/8vCQGZ\nfIQQE4QQc4QQfwhEhPAHdkXPkGp/WL9Qi9pFO401mKQSpz3JmBYWNxF6IzDeYnHzgHLNlL+hfRL5\nFeeDotHnftGHh1nhnGWKD/IPzpksHPe1/uaBqgn+mHz2X/ypXmuO6xKue1orcBQIe5Ibu6JnszEw\nJVQtQN2L2Wn0sTcE4Eyics6FSfqazW4pZbyVbdzSoVdAiim/9rQ+tPKJ0570ePx/ZjPjLRbuWxZ6\nfIjbffSBDe2TQh7vlwpXphXAkEgM5Dj7BmTuqQWTgPpnc1xpfQUzFB5v5XHV+ijAcXvFNfabcZ00\ns55WKJvm2NtEUzhHMVGNaeGculWleU9heUCxs75oBN+rreFofsSDQGoD6enpzJ07l7Fjx/LDDz84\nnTt37hx33eUhS0IY4Mq0R4A9KNE+kUEgjhV1KD55Y9jMAiNsHOt2fJXRyMiWpQHlM85s04UtHXoF\npFEONtRRXWkdJ5pVRiOpyYooHy7RWEWf5GoN6OCj+RgcXjkdQqxjW5vIPg7bPVc/aNasGc899xzX\nXHMNn3zyCe+//z7Hjinbl6KiIpKSIiNNuDJtF5RA+FfCPZD9IQbqWFELcPPk8bE3zH5yLNzxOuC8\nahWb3dOSBlJftabq5yqNALvM3o3vnu5tZoHRvtKiT3baiyeYTGzqOCDkVdaRxrRTB7jj6TjSTh1g\n8NF8esU1JqdDL/urXqFvc+jm2TRlsVh49dVXKS0txWq1YnWI1OrevTuNGoV3IlThyrS/QYnyeTHc\nAxn3HYO/9g3chbGWUfqn63zuu4ctex1Tum3P67Bq3T61lFYFlU6KHX8ZdkP7JEUxEyJ8OYPYJxqN\niazezhPM06PeCHlsR6i/Jb/iPFs8laysTwqoto1A57lkiV6vZ+rUqaSnp/PVV1/x+eefU1VVxcaN\nStmJiAXCS2czTQfgdqCjN3Wzvy88mHx2zgsgnK0WbXiOIWWqjdGbecTRRqumcVlpMNhts462z0DM\nGpOK9stbjuTJW47keTw3POuv9u8Gw0rn+1ODGU1Nm+rafqXBIBOn7JQ/+hl6N+zobjuN6svR5qze\nR/vv9vYMDR/5Nd4vFYGmm/k/oAQlJWrYvZ6vq1QWdr8yVuzzXooh3HBUoFz3xCNAmV8ivJrFIvnW\nXMC9Pk4gRZEd2xqO5pMc1xhzxXm7KLTlsXH2cwICuj+lZRed9sJZBmhsNDK533xOvNqH0m9rThY/\n+Gg+jUSMm3ibduoAhqP5xAC9BvYk17YlMBpXYeIK947GbYXuHo5H4TdcxeNGQBugeU0XeigLMl0I\n8YwQYoLaxrXCwE8Csr7v618JkDp6sD+d7gzjtnp1Bfx8TSenY2p2xmArB3hCTode5FecJ6dDL7bY\nXqD4L+d06IV862p2lVeA8WOn6/zWFVjMLOk6loGdlXIXCSaTx4gfFeq+1JOZRq3a3ivOOezPZBqp\n0OgoCo/bCuYz9V5rHEkEEzAwD9gNLPSjfzXKp7XtexawGIXp3bHZyN2fTIeyS773NOO2KqtILT5Y\nu5Jns5GEZ3bz5uixHhnAbC4mOdnZL/ThvyVwRWy1H7IjA9SkXPKFLS4rWmpqKl0bNAQUpp6cZUth\n4nAv/Z1o/lPenbnJ82pUPjkqkmqSGjLbdOHHZ52L9aY3uqA8y4HZ0D9bYdhtw3z2E0XNcBWPVwNf\nA2OFEDlSypk+rlW9y9X4MyuKEssuVqsBAwBYzJhMI9HrixnT/YzyEDW2q9R3bJ+Ta1zoIwOLoqBJ\n/83j/F+XbNjh/Af7x4qLVH5QzcwnVxvZcFspTZolMMahXaVNkxyIeBwUTEPhrSbw3EqydL0Y08/d\n5vu9JZfkovedjn24fBn6Z5MZVEP3qiIpEDj+ZrO5GCwjfxY22ZqQkZHB9Onek8D5i8TERE6ePOmz\njSvT/lNK+ZoQYjxwoYb+7VE+Qoj+KKv0Z8BdwN/VRsOP7YHKMjbYMvOPWLcY40svO9sb6/ihjhgx\nAsORPHJsNC74YR36Fvdg6Z8N/ZrD0hvQ6xfzm3cOOl1XUWym9ZMpjJyGk0uK1mb7DMTk4w+NIxz6\nEk/v57HZ3/PWVQNgxzDGGLVQmq2ctNGcWWDk7TmdqXS4191n5NGqeB+JP+nAWaJ1SvYWjD3VNVrK\nYhmv7G3DlUa3LvCfDGjaHdq7p1BdvXo1Z86cITExESEEubm5aDQaPvvsMwYNGsTHH3/MpUuXmD8/\nvNUmXMXjk0KIl4AfAM+RvzZI57IgO6SUt0gpX5RS/t2x3Yb2SSCtpPVUzAo33dROmYEdUcezcO7A\nnuTsMDC82yKn4+LbvVi2FWPqvopBf3sGXaNqe51jlkXHqgOgiMgWvT6sK23uwJ7VXzYbMZlGcrpV\nB/T33wNA1qZXFclgxzBFDB2YzfeWXBKaOGerSEpsxNP7X3frP8FksksI6t45UIwYMcJtS+D2rOsb\nek73yLAAhw4dYuzYsfTurcQfqyYeq9WKEII777yTZs38iXINDE5MK6VcK6WcKaX8h5RyQzgGSDt1\ngA2Hq6XsMRoTycmtEGK+34WYawX6ZPuecYzGZC/toTffjVHfmIyVz/PukGpniopiM18sg5EmE21j\nU9yCB2a+kRlW8uwTwAoBxbmwQtDt0v/ovaIvQszniy9OVDfeNozdV+1mYfq7WN7/j/2wo3/xmBYW\nsoqcBS1tcjITPvKeVSEgOm2wWMZftnWNQkW7du3IzMzklltuoaysjI4dO7J69Wp27VLKnNSKnTac\nL1Q7ra3K3LCju93siGr5ibouJTH61jZSSsW+OKlovxudmic0snKAc3rQwlUGp9Sns39yz1zor/3T\nLxpHj5byffcxVDtpak5XCfOU1x2/kv+3HXmu37+dbKXdnjfLlQaD3P+qkhXy5fPu2SGfXp0VGo2r\ndR7phHk+y4BGUY1AszGGH7Z94ob2SWSfHuV0ymQaiZTp5OaeiEiR40CR2aYLJ8lz0hwb5xt56P4E\nNN+MULRSrXnAAAAP0ElEQVSgOBeHbjd1F22n7KLlGee+gs1Q4ROt3G3BG9onceuxL3nrlgIM8zbC\n76fw+EsH+Vt/ScKO4XZ77n3LCkhKbETyrbk06mDg0AJBmx/hUKaWQ5nVK+7ha6/2af6pEfpkhU6X\ngtZqdYWf66pbm3BVRIUfDn68ZRevdjplnG8kZ18Ov8uFOAdPsSd2pkNVDG/095x1P9wY8Yel9s8/\nXqzOE6OfqPzpht1lcz5Ibq4oza5VFDrn+syj6FUlBWjasX58+Xp/Hjz4Oide7UPXBg3RWyxYtFpI\nSEBv8T+QwCONnXbCEO8TgX6intILpTw5TsPSPg5pY0xDYWA221Ku5Iv2g7lEtU25M5AxTvDYBoWJ\nlzyyn+weN2DJzfU4Rk348ebrgWPVz3yNHlZoASu0MmCxKMeFmE8oJVJ+6Yj4SqsqJjKseno2aEna\nqQPoJ+oRjwvMx80smGKgWSMdb/SV9teav+s4WSgZtyvycwpAbvfqP/nNiXcw/NgejPOV1cayyMLx\ncpuv7tIbsO4+yhTtN1wqK6Nofl/7dZntt/HpmG8Z2LkJA77Ks+/t9La8SxYhsOiDD57KHbvO4/Fn\nz2vZ/PJN9PzDBl6dnELXJu6r8bYr4jh4cBsA5k3O59ue1/HxTbl0niz56XgOh5fo0VdWKvQKEdCq\n++/HxjkHW9xvgYcr4WEJxblUvS9ghUC+PwXL8sg409cm0tPTycjIICMjgxK1bIoNn3/+ORMmKH5G\nrilV33nnHSeno0ARcabN/M8T1Z/bdMH8k5KBTy6XzHgqmf1luSzs7bwKWRZZ+CrnCpb9OZZJeZGL\nErTTdY2iHcyw6pmusdC1QUPMx81YFlmYlKd3ou9is6OkrzvKpLeHu/XT8ayOtU170PoqDW2n7KLd\nVEUhobdY0NsSlavMYNFqA2Jix/uo4tnzWv53XLmXuoZXsL8sVwkOcED3GXmMHtAS64USsmfgln95\nTAsLRTorbDZyovVAQBH/9VKil5JKs9k+4dQ48XhIhqdO0CLHypAiAyIHRA6graDyvbDnWgg7SqqO\n85P0LCUJIYiNjaWqqorMzEzuvfdevvrqKx599FH2799PVVUVW7duRQjBzp07eeWVV3j22WcRQvDO\nO+/w8ssvB0VT5Pe0Q0x2ZgDI+/NgHntuB5Py9BwvNzuLcg6wLLJguO4Gfiwv8Xg+nEhNTXXyJsrd\n/wW9n95IZoGRqxpV++UeWiDoes2t9Ci5QKtkd3/dMS0szLzfSlfLNorm9WFg5ya0m7qL7jPygGrm\n1UuJNkVZ8SxabTUje3rZzj8y6r9OYz17Xkul1cpbV1eSWWDkyh+uo6D5Tvt5da+dlNiIj3TJNPqp\nD493WoontG1g4Laus9jQPskeD6zucx0nHLeJx4E+ixA8fMeXTv3qJ+pJvioZuVyyYIqBrsYcnhgH\nT4yDZ66rItciFfF5TeQn5mBxkE0Uyp0ez+l0OqZOnco111xDamoqQ4cORQjB0KFD6datGw899BBr\n167l4sWLaLVamjZtanecmDRpElptcJJkxOVPR2bQjtWS0jUF84Wz9Ab7CnYoU0vntEoSp2yndwMz\nS9o+S6enLZjSTegn6nkiTnB1gsFtFQknjpDLS5pKxOMCXSMdgybn8qF1Fls63cThJXpkRQm/PrWT\nX/+qCWc//J6RVWke+/nipJmbevaDFeNY+6QyIXWfkUfbKdXVzjUCTgRYcSDW5mo5wyqQEiqtML+p\nJLPAyP6yXF7vI+1uh3uXVNC7wS4WNxsH5Ro6T5YseEbL5JRsxV/ZNNSp7zEaE/+OW05GeQrTG0k6\nPW3h5GojhxYIRJwOebGUzhblt/jamzdwiPkdf1hhRFO6ibG7BDFoeL2Pc/L2JxAMntmRc/cVkbBC\nNY9oFEWWvxUVI4y+MWO8nispKWHOnDkAbNy4kbNnz3LdddfZ8yELIZgwYQITJkygU6dOSCkpKysD\n4O233w6aaYUMsr6MEGIY0AnQSikXCiFGoeRNLpNSviWE6L99+/btn/a5jekai33WHXYXHC83k6fL\nwVoh2f83K0qWG0mfuHzyK67DqsSx0ESU8UlrA1USXrkZYgRc3TT8zPvIvxrz/m/Pox2rpfJ15c85\nbpeWY1d8Qfsf/8PUb6Zy49Et/HlnOsdzcrjKYFDETNUVM1nxQFK9iLKsRo5U5fDSGiBWp+ztHHDf\nsgK+OlhG0Tz/69isX7+encPv4WWNRD9RbxfdQZn8us/I49yFSq4aoaFxYgw5HXuxyrYfdaLZ+DGU\n2qSbfs0Z/lwTymQVyXGNaa67mgtaK0JCXIyO6RqFeQEuHM9BxOl8ZuZYv349u4bfR5W0UlUFmhiQ\nEoSjPCcVv9eig/0AiCn9Finhn7e9hsUyXhGxi3Ox+7U+LJVjFrPbfQSUVVqfXKdMfvLkSVavXs3p\n06eZPXt2yPbZHTt2MGDAgAFSyh2ezofCtHOklNOEEHOllM85fH9FSvmsEKJ/dnb29rXNHubtrHPo\nGumY8VQye899zoZV3wDQdWg5ms7VzvZ3dq7+EwuNhq1LvuerA8oeOEZU8Xnr65l/C0jbPbFeakjh\n+kxeODuOGyohdUwTzlq6U9r6LUrOXqBxx1j7BmDfX6ro+vsYNLHl5HS+kf3zNFzz3KtUVk5m/GE9\n54pKiGsAQih/KktxMl+a3qDr77XExIGwWiEmRnnZkBzXmPyK81RVKfcw54FqB4esJdM40vs7OhY2\nZMxWJfPizAcg7hJUaKFjXhKpBd/RfvcOVv5qLPcfcPdSAmVVtkp47MVbeOfFjZwsuQm5XDJmW1Ok\nlHz6ry1UEUMTytjQ1EC2zY/lw0O2lb2qCrzN6FWS5O8ryHzROZ1K1qoHKWpzngsNQMhq5/L4ssZU\nNPoJGVPFizev4Oi9D4PQ0GbLLE4Znyf7t7C9HE5+ZKCLIQcpNWxc9bUylMNOrHFVGQ/PVeoFWYmh\nyiqwVliJbQjCYZt7sQzmKvHkXIiBRlWAhLcMUKQWcIjRMP1fSk/SgVaVbYSodmxX/+rikcgVfA4V\nhYWF5OXlMWzYsIgw7UtSypkOzOr6vf+v3xq/vUPrxTS2BcaoD7Fw3gB7P47FlQ2OSc6kBJVRAOlh\n9hKySvlXuW3NlQzu3X/oR5VQHmBB813ESEU0qRJNnNq2jNlF48LJnC3pzlbTUoS1igYXy3n6LwZ7\nUeftq7NYff9jTqMMPprv06E+y2qkCDMXUPblnTBQdCmXtj9aOdJSmXyErJ6EPEE9/13xG7QoesJ2\nazRYLzXk1AfzGDz/6ZArA3iEoz+4TXTNGAFtS5QC1TNGOtMHsOf0G7Q6qdD44cpv+Gfp9Xz1Nw2r\n7v0L21e559UCRen13mMlXIoF7UV4fdY3dgYfkzEQIazVHKhCgrQl6b/0EzRoohyzt/P22fZ9dmz9\nZtpQPJ7uBiYCM1CC5h9DKd41Wrpkrji02D3rfrgQaNJr3QSltqxugk7ye+Sjm3QBJdmuK4wePVqu\ntCVDn7oyfJ5W4UQk7+O9S/dJKaVMnLJTJk7Zaf+uwtGLTf386Cad1DyhkZonIl9nN5wINHOF35BS\nuubI/NZb20AyFAaKQJ3yLYvcaUl9LzVM1EQWoZbLrM9QY39PvOpZD+DoxaZ+fneIhXcjkhBYQV5e\nHuvWrUOr1dKrVy8OHz5Mjx496NmzJ5MmTWLlypURGbd2vBcuc3gswHWZIUpj3WBLmYWW2liui2/i\ndm7jxo3MmjULUIpxPfXUU+zYsYM2bdrQo0ePiNEUeTttPUB9+LNFaawbdIlryJXaBj7bLF++nL17\n99Kvn6IRT0pKQqOJnONIdKWNIgof6BAb7/XcPffcw4wZM0hISCA9PV3V9QARDMuDOqiadxmiviii\nLnfUBxrrA+o+NC+KKKIIK6JMS/3Yi0VpjEJFlGmpH3+2KI1RqIgybRRR1DNEkmlb1pQpPRwoLCwM\nKaAYnCvSRXIcf+BtHH9oDHWMUOFKY13fs/o6jo1vWno7H7DJRwhxBTAZiAMypJTnhBCDgDuBpsAk\nKeVFIG7HDsV1slWrVt66CxnqxFBTgmdf+OGHH1BpjeQ4/sDbOP7QGOoYocKVxrq+Z/VxnOLiYvUe\nei7VRwABA7YE5jcC9wI3oHjl/0pKuVYI0RI4A7wALLAxcktgEFABeK7KG0UUUbiiJQrDfiml9Mg3\nAUf5CCHuBo6hxE50lFL+y3Z8IrBHSvnLdZCNIopaQDAeUZ8B01FW0HlCiPtQinBdDzQWQpil9JJU\nJ4oooggZQcfT1thxdWaLWCllWNPtCyEaA39BqdRnABoCM4FpQCywXEp5JMQx/gBcAXQFjkZiDNs4\ndwAdgRYo0ktExrGNNRZlgo3YOLZJvA9KLaiIjCOEMKKEg15NZJ/N/SiZZscCb6OUgo3UPWsP9MSP\n3xNJ7bFaCjMSWqhEIA9lz7wY+Aa4DqVM53zAc/GVwLAO+BMwOoJjIKXcCBxBiUuO2DhCiMEoBcOt\nkRwHhWH/B5yP4DhDbf0fjOAYSCnXoDDRm8CSSI2DEqrfEngAP35PJJnWtRRm2CClLAAsKH9ANTeB\ndPhcFYZhzgJzgbQIjoEQ4mob41ZGchzgdqAHyuwdyXH+KqVchHLvIjVOcynlX4ApERxDxQ1AWYTH\nuVZK+ZxL317HiaR4fDeK2FdlW3HD3f/vgB3Awyg/6iVgDlAOvC2lPBpi/ytRZtlTQLNIjGEb50GU\nbQQo4ldExrGN1R7FNHdlpMaxieCNUTSgEfk9QojhKNuWxkCDSIxhG6cx8EdgBTAmguM8DiTg5++J\nGNNGEUUUkUHUjTGKKOoZokwbRRT1DFGmvcwhhOgghBhr28d5a9NaCPE7X218XDtGCNHA9nmqEKKL\nEOLqmq7z0tfUYK6LIjBE083UMYQQi4BDQDGKDbUJsB64B+gAqAV7bxBCnAX6A32B5Si2yi7AKhQX\n0x+FEIdRTAdxwELgHeAj4IDt2vPAFintBWripZQXbUqXFOAEcEYIsRDYhqIYibfRMQHFnFMgpfxQ\nCJGIol0/B/wDSBFCmIEBKEqoLSiKr+PAWuAZ4AcgU0pZGq57+EtDdKWte1ywmUhuQ9EUNkN5LiUo\nhvUWDm23AlcBjwMXUUwRiSgMaUYxDwxFse2tQGHkXcBfUeyn36NMCnFgD/6wAEgpzwN7UJgWIB+F\nEfcDnwO/QnFmOE+1tvtHFNNYjO26PShMqpbYbg1ko0wa6oQQi8LQUQSJKNPWPdTK2VtQGOoUij01\nFmWVc3QaX4DCNI+grLbY2l1CsSdK4BOU1e+3wBc428k7oTBUR9v324BPHc43QDGlQLVtUH2/hMLA\nDVCcQUAxUzRBMVU0RvEe2oIy0aiBIg/Z6DVRHW7WzOvdiKJGRE0+UUQMQoibgTgp5ac1No7Cb0SZ\nNooo6hmi4nEUUdQz/D8llZCJcvnEywAAAABJRU5ErkJggg==\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": "iVBORw0KGgoAAAANSUhEUgAAAOgAAABzCAYAAACSCE74AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAAEnQAABJ0BfDRroQAAIABJREFUeJztnXl4k1X2xz83SWkpLSQqCBYKKIsgCAXrygAJjDoKWsfd\nkaHiiigOlWFGNgek4CiWUfSHDoLLOMjmUIVBHCFo3UFIoS50kaVSqAUMpaVQSHJ/f7x50+xN2qQt\nmu/z5Mm73XPufd/3vOfec885V0gpiSGGGFomNM1dgRhiiCEwYgIaQwwtGA0WUCFEGyHE60KIXs79\naUKIvwghJkauejHE8OuGrhFlUwCL2/5SoBKYDiCEaA8MAWqBQ43gE0MMvya0B+KBT6WUhxosoFLK\nIiHEFW6H7MAsINu5P+TJJ5/8T3p6Oh06dGh4dYOgoqICIGr0fyk8fglt+LXwqKiooLi4mEmTJg0F\nDiGlbPAP+CPwGyAd+Aj4G/BH57nr1q1bJwNh7NixAc/VB7Xs/v375f79+xtMJxS0FB5jx45t8D2L\nRBvq4x0uj4a0paU8i2jz2LJliwTSpZSN6uIipXzTbXe41+lD0fwKAaSkpESV/i+Fxy+hDb9WHo0S\n0MYgIyOjWcqeqWjuNkeaf3O350yBkFFyVBBCpG/ZsmVLenp6VOjHEMMvFVu3buXSSy+9VEq5tdk0\naAwxnOnIz89nzZo16HQ6Bg8ezHXXXQdAbm4ul112GZ06dXJdO336dDQaDVdffTVDhgwJmUezCWhm\nZiavv/56k5c9U5GZmQnQbO2O9D0/U57h55+XcfbZCfTufbbPufXr1zNr1iwA5s2bx65du6itreXU\nqVMkJCTwr3/9i8rKSkaNGsWjjz7KoUOH2LFjR1gCGvMkiiGGIDh2rJaaGlvQaxYvXsyGDRtITk7m\nxx9/pHv37vTv35+2bdui1+spKSmhqqqK3Nxc/vCHP4TFv9kENGYkCg8ZGRnN2u5fq5Ho2mvPJy3t\nXL/nbrrpJmbMmIHVamXkyJHY7XZqamo455xzyMvLY+fOndjtdqqrq7nnnnvQaDR8/vnnYfGPGYli\niKGFwd1IFOvixhBDC0YkneXHCCEeFULcE0p51ejREDSm7JmKzMzMZm13pHn/Gp9hQ9AYDertLN9H\nSrkQ6K0eqKiooKysrBEszixYDQYAqkwm17/VYIDxX3he6L3vViaGXzfKyspcvrrQiGkW6essr5q6\nHO7XHe3bl8TjxyE5GaqqADDYbIx84w2sb7xRd6Fe79o0WK1BeUfCwOAtEDaLBV1aGgDJZjNVJhPJ\nZrPH9bbNm4MT1euxCgFares/qXc2cstPcMl7IB0gNMr/Je/V8a4qICl5InJwLggN0n4cAKFpja36\nW6XNf8+g+qabwG1qwr2OVp0O7PagdePoUZJ6zQFAl3QRCA22qgIAqoum027gMoSmNdJxwlXMfmIP\nurH/5pptoX3LTfNN5BXnkZyQDMANtx/l5yNwbkfQaMDhfDv2dk8EXg9I50xAOPOgRUVFPPvssyxe\nvDgsHo2dB5XAuUKIdsAeIcQEoFA92ebJJ0kdPNjnRbcKwd1exilV+wBYhUBnNHqUc0cwAbUaDC4B\nV4VQpeMulIFoWw0GH01o27wZtFoMDTCoKTQ+wGaxYLhjPVWF0xSaFournnGXa7BRCoDuy4cRpg1K\n4d7tiLPonG0eBVIqgq9C/RBA0PvF/w1GttuOcGhA+6TzoCLMcQ4NCAdKi+8CAQKUJ4uyLaSGO8cP\nDNhGVSjtDjtajZbMcXD4cBX6s+zoNFo6dbJzYVsjWb3M5BQp9/TVJwPU1Q9yikwUV+fhoO4DJB1K\nFYVaSSeEACnBYdPy6mXBp0dCwcJNCzm//flcf/H1PufCmQdNSEige/fu9fJLSUnhwIEDrv1IOstv\nDXRdlcnk0lCBXiJvrWk1GHy0WDCoQu1Ox7tsIFrugmuwWusEMy9PORauYJo2QI0NEnUk12TBl6Pq\n6oCfOnw5yvNBmK8NSDrcumSf1HHyAbvyEku3zo3zpRYOB1IDAi3YFdpSOEjQ6Km1H0NqHYCD7rRj\nXAAeecV5jLvfjtAA2EnU6InrVEWCRk+XxDSyetW12X07EMZv1+HAjtpU6YDyclj3Hmg1WpITkhky\n8ihajRa7w86nG/VUnVR6Z3aHHX2inrQuaXBZWLfKL8ZcMYZW2lZBr1HnQe+++25KSkq4/PLL6d+/\nP3v37gWgpKSEMWPGsH79+rD5R9WTqM2iRSQHmGapz5NE7W6GWtYgpWvMF6yL7C6MNotF6XYnJ6NL\nS8Nmsfho3bAx/gtFwMZ/AYWVkObrgeKN0aUFrE3tD0BWeQkAxadOsDa1v2v/578qXdOQvW82mpg5\nbDNo4SmdVOqz6Arl33JEqdeiK1haOJhxfbex1G4CrVJ0nNZMtt1AN+0wxmmV+5CZmck4P6zHfi64\n90HonWQMSfhUBHr+D2wTHDzgKYxpXZR3QS42Y5pvwvKjhbWj3T5Uo0NmGzb0ifqA59R50OTk5IDz\noKmpqdTU1AAg3Hs/IaLF+uImm81ha1HvrqzH+CxZGRO5d3/rG+uGDXfjzyJleJ5VXgLlJeR07MHo\nUmW8VyMdDIhvA8CO2uNs6joAwHW+Z6vWrv2erVqzo/Y4XcKsSvaVm+mmMboEzCWc4KHRxy18ERZR\nd50T07T135sHtgnOStSzYGDj72NOkYldVZtZ9aai/eRi/8/cPLmBH84ooE+fPjz11FMexx566CHX\n9p133ulxburUqWHzaNHhZu7dzXDKqkYeUIRTN3So32uaAjkdezBi3w6X8AEMiG9D8akTru2s8hIs\ntcfZ3HUAWeUlLu05urSA4lMn2NR1ALlhGMayTwiI19cJnWmDotUXXeF7sb9jfuB9z+/bKng1veFO\nLt70fqyxsPSfWmyvRPijeaZDNiKjQrAfkL5ly5ZGR58fMxobVO5nvV4eMxp9yh8zGl3nooaHPndt\njtq30+NfxaSDxXLSwWLXtr9rGoo5NXjWxa0+kcB9W5DPFUbu/v3JopfXv0v9F/5K4J5R4YzwJAp3\njtC9++rPgmuwWj20bETh1s3NKi9xdVfVMaaKnI49yOnYw7Xt75qGILtGMK21m2YrrAxZS4aC+77S\ncbJSH9Z4MxhyikxYa46y7oZYAnV/aDYBDdWTxF9XNFBZdXrEZrH4nFPpuNOLeDfXaYgZ/UQSWc5x\npyp8jUVInkQbTXSKN3oe690uYvwBaquS+dfIxndDVXqWUgulnxuDX9yMcNhsSIfD77k5c+aQnZ1N\ndnY2J0+e9HvNP//5T95+++0G8z8jNGh9UOcCVa3prT2bGpHQhA3B0ovyoko/p8hEVW1VRGlq2xxt\nUYYfb2ydP5/iNWsCno+Li8PhcDB//nymT5/Ozp07yc7O5re//S2nT5+mc+fO6pCvQThjws3cu6Pu\nZa1CNMiBIOJQpzGihHrDzTaaOGjwssZGsE4ZGRlYSi3kXt/4yX+VXk6RidLPWq72BLjsr3+l1803\n+z2n0WiYMmUKrVu3ZurUqaSmpvLhhx/yxBNPcOWVVxIXF8dFF13UKP5nhIAmm80e2lAt22KE0w9y\nOvZwzWFGAvUKqNVCpzhfa3Uk+Uea3nfWvBatPUOFyWRi3rx5lJeXM2LECObNm8cXX/j6WzcEDZ5m\nEUKMAroDOinlAiHE/UAb5/58qHOWj0QqQ+/50JYsnE2OjSayR1cxTXtmvey1VcnNXYVGQZ3XHDRo\nEIMGDQKgoKAAg8HAsGHDAOjatStdu3YNmaa3s3xjNOiVUoleUZPfJgLtqHOaD4pGhZudM6LFCafx\nL619jkVSiwY1ElktEBfdlz39950oyw/s3dXc9FoK+vfvz8MPP8y0adMiQs9DQIUQC4QQ84QQc0Mo\nqwqiKil6KeUs4Bz1gg4dOkQsEbCHwWdUuH410UXWx9vZ3G2ghzMC4LLkRhUu7ellWY3wmPjn6iMR\n7Y5e1PaaiNFScWClyfWvbp9pSElJ8VgSwruLWwwsCZHWNiHEY8AJIcQlwEnnymYV9ZQDGm4kSjab\nW14+m95tAVy+s97zm5FAwDY3gfYE6HJVXKPK3/JyEZ/9UO3a755q5vPJpQ2md2CliZP78xCtlLbL\n2qMgtOx+ToDQktA5euNxFZMnT8bgnNpTu7I7duygsrLSpUHnzJnj8sGdPHky8fHxYfHwFtCzgb84\nt2cHKyilfNfr0NfhMA5XyPwZiVoMCo9BR2XTvVsbSQHd3fcFX+eAZTpm3mpntjb63f2PnjzeoHK9\nZ+TTPyURgIPPDnIdzylq3L057zYzB1aaqK1Q5rzPfzw696C2Ih9NgoG4tr7jSCEEcXFxaDQavvrq\nK7799lvmzZvH3LmeHdC4uDg6deoUtnCC7xj0MBAHtMg0CC0968Do0gJG7NsBRFY4QfFV9cAqA9m/\nh26aANMUEZ7yMc1v2L0vfGogBWU1FJTVeBxf916AAvVgz4sG1/95t5np/oiV7o9Ez3/3dOVu7NUH\n/Z7T6/VMmTKFW2+9ldatFRuE1Wr1mPcUQjBlyhTGjBnTIP7eGjROSjlDCPGQ36sjiMYmrl76zc1o\nvo5inFGIyCovIWeYohmi6aCw/L4ErP3q7tnSIVV0ihvqE4XiQoTHoKnfpIZdpveMfEARUm98v/x7\nmBwevQMrTXR/xMqBlSbiOzSNgSmp5+8Dnjt69KhLW/bv35/Ro0czY8YMEhISmDt3LqmpqQ0KMXOH\nt4C2EULMAWr8XdzcSDarEfmpjGxb4y/0ucmR07FH1J0UAOL0P9XtLBNwe5AJ/iaoTyhQu7a3vFzE\n6od6eZy7pl94RiJ3A9B5t7WEJw/PPvusz7GFCxdGlIdLQIUQVwE/O39RH9Q0ZBxZZTIxvDqP86eu\nbjHj0KzyEnKaQBheeOQ/ysZGE9wlOWg3BI7ZjEJ9wr3ft7xcBOAjmA2l5y6Ue140RLVb26Ig68LD\nDCiOB32BS2QLCTfzxt0f6iNOs1GIcChXIBifdYZ3rdTLObZ67kET1SkYek23RIVu2QqjLFsRxVDB\nFgC/C/hKKa3OaRINikdQWFbZpkBOkYk77qmCH5u7JnXIuiOenCbgY55sVrSnIY1O9V3cgrq3kYJq\nsf3VaE4nvK24bVHSvfmPr4kgGuJJ9GONhaE9h7rKOi5ZG9lKNQTOOdBoIzMzk8xnvmdpKL7lfvLu\nRoJ/qOg9Iz9g1zZcegdWmlyW25YmnO7hZp9++in/+9//OHjwIHfccUfEeHgL6PNADuA7+m0B2LEx\njRuvq9tvCVbcnFmHmo7ZqSOAb/4gHzSzBg1Fe37wwR4MhvoNKufdZm4yi60/vPnFITbvqgx4Pi4u\nzmWt/frrrzly5Ah9+/aNGH9vAb0H+DN1zgoBIYQY5VzqYZJz/zYhRJbTuyg4NprCNhLkFJlIGWjh\n3fUtzFGhiYSh87/+R8afVnIQ32B0H0RBg4Z6z1XjUH04dkzRsKEIKdBsltvr+uu5pFuS33PqHGdq\nat0UVL9+/dBqtRHj35gurrezvAk4DfygXhBw6YeRTne9jSZlymCZgGXBA2uyepkpy0/jRh5sMQKa\nVV4SFWHwh5faPsHPo18IKdteND4at9zyA0LMr1egCspq6u3eAhw/vhir9VEATKYVAa9THRKaC+ck\nxZGc4F/g1DlO77nOxsx9ekezeFteuwBXAO1l/Vba2c7/uc7/15z/zzv/09etWyf379/va6b60Kj8\nVuo9j/07cOKoe7/USiml1Otf8DhuH/xe+GayMxA/6/VyiS0E62WULbh6/Qs+z8AdNy8qbBBNf/g1\nWGy9sX//frlu3bqAScPGo2jCCSEIu7ez/MdCiEdw06DBolkyn/keDG5ji5FmuEsG1KSvXmZDd2Mv\n0tI6eBgYmnMcmvXx9ibjNenGG1ky7vv6L4xSl1u956rW8wd/Dgn10QNIS+sQUDO3FKeEpoJ3NIu3\ngEqUMLLgue5RnOWllM9LKZ+SUn4tpXxdSvmilPKFoAVX1a3Bwkjl5i+1m8i2G8i2G6DDUKXr64ac\nIhNjNhoYWvUUZvPt9VWtyaC6+DUFTn/wAT1FCN43TdDltlofRYj5Qbum4UB9pkLMjwi9XxK8BfR9\nlPHn8qhyHWkm46pzWGo3MdOuYy91ya5mGvOUECo3ZPUys+61Ok3sPQZ1pDfPdEtTatBbFy0i6YZt\n9V8YJQ3qfc+lnExe3v4GC+o12zzbYrU+ipSTEWI+Ot1zjarrLwrSc1z5NNAH6CXD9Bzy/uHPk+jf\nyjhSHUstsRl9xlWuYx/WHb/3S63U33VRwH67Vjs/nG7+LxvN4EWk1c6XWu38Bo0//cFoXC7fevAC\nv8/VaFweER6hwuFwBDy3atUqOWvWLDlx4kT5/PPPy6efflo+9thj8ocffpBvvfVWg3n69SRy4nvg\nUpSubmj28lCx0aR0X51Yale6sd5zeuO0ZrJP6oC6a1+9zMY76wNbD222xyNa1VCR9fH2Ju3mhoQo\nadBga+TYbI+TMmkr74x/D8MT8T7WWX/DkmD0zObbObDyFYYWdfbp9mq1IqSusNHYJSLDoffL53Ju\nQm8GG27xOVdZWUn79u0ZOXIkn3zyCX/5y184dOgQlZWVjY5iUeEtoAZATzSiWawWuLVuimDJuO+5\nd2kfv5dOS7CxdKCBcSjjz80FX2K11lUpYKhaE0dxNKVwqkaVekP0onQPHk1NDbrc7hW927FaTsZg\nWOgSIK1WMNM+nYeEkl2gR539kDfi9BT4paSgtsLSKAGL1Pj4uk6BcwtdeeWVGAwGFixYQLt2SoLw\nqqoqampqGpUL1x3eAloupfyHEGJ8RKirWGVwWWyX2k2M05pZQqeAHjFL7SbG5afBSOf4873QgoXz\n8vYzFNh++bsM+vJG1380kFVeongRtQC/Vw9EqT6nP/gg4Dl36+3LaWv4UV2J3A5Cq6UnewGYbK9z\nULvr7MDjzGDRKs/pdDxus/GcTocMsqL4TXo9EF2D4o4dOyguLkZKid1uZ86cOVitViZMmMC7777L\nvn37GDNmDF26NCKHlvQcNz4PzAJewjm/2dAf6hjUe75TKuPMCe/0C9oPn2PTS7lSL+/9JNFn/Llm\nzZoG9+/PVKxZs6b+dkdx/BmMtzr2nK/VyvlabaPpuWO50SifBddP3V8ezcWvmhnuY1BvoXoIRatO\nlY0QTuktoE4ssRnrD5Vyu3bJIX1dmFU48PeiRvjlnfTRtojSa+kIthrcfK1Wpt+6KGyh8Udz90K9\nLFthlMuNxrAE/peEYKubVaAYigL3Z8LBFs+e8jitmU6E5vg8Tmtm5BeQ0qUi7PFEXt5+n2OmQiWD\n3PbLvXOdNRBNFMUSFqI4BxrIoLPQYCD33n+zZeVD3B7mWjj+aFaWwdt35bE/L4/OQ4fyuC0yS02c\nqfAW0J7AYKDegZubs3yW27F5QoirXRddusijzEy7zmNJ9frwr+vg/p93+RgL6is79NtbfY6pNMIa\nk24MPPZt0igWQlzdLIrj4czMTJ+kbSuc++emhR9t4o8ewMmjRxnx52Qet9nCFvimxo4dO5g1axaT\nJk3C4raiXnZ2dsR4eBuJDqK4+RWHUPZKKeVUIcQ8UKJZAI806hX/u5eyzPdJQTH8zNaG9zWsrq6C\n8vPDKuOB8V+w3VLhK5SqpTOYxXOVwcPq7I2sJ9s3SaB2WIiyFdtb41VYLLz4yGYKQ3Tvq4/el+MF\nly9qWSsG2AoLEe3aoe3Y0efcf//7X5588kkAZs+ezaeffkq3bt348ccfWbBgAT179mTdunUMGzaM\ns846iz179lBZWckFF1zALbf4TttA/Us/tAWOOP/rrbu6IYRoBQwBBgDDXFcMnOfa9A6TCiUi5avF\nQ7Hcv9vneKjRLKbCUr8aU+3uqv8+cGYuCASXBbcJUe/iSRD11dXcNd5Cg4EOaWkNzpzgj16CXu9x\nzYh9O3yy9Tc17F9/jaOw0O85jaZOfL766ivi4+MpKytj4MCBTJo0iYKCAnr06MGdd97Jzp07SU5O\n5uyzz2bXrl0h8xfSM4fndOA3wKdSyqeCFhTiRqAbijC/L6X8WggxFEiQUv5PCJG+ZcuWLenp6WQH\nS3AVADlFJp6aVcHkJbuY9ulQl99uRBCKptloCsizSZZ0aAiaaB54hclEhcXCo1ZrWA7ywbDQYGDU\nU3VZE0aXFrhWJ1fR0u65xWJh1apV1NbWkpKSQqtWrUhNTWXTpk0MGDCAlJQUVq1axYABA+jbty8b\nNmygc+fOVFZWMnPmzIB0t27dyqWXXnqplHJroyy1wX54ufqFFCrlhVGrWysbH4ZfdttluXLbZbl+\nzxmNywOe8zct5I1JB4tbRGIuDzRBfVSrq2qtbWxiMJXeC3q9T1jZqH07pZTKvTbtzW8Un+ZEdnZ2\n2GUCTrNE8qcKaKCplbFjxwat5N0f6qXRuNw13RJO2YAI5SWuRzildApoE2Ps2LENb3eE+Kt4Qa/c\no8YIqErvmNEoX9Dr5e6Fdffd3/0dtW9ns9z35kDAaRYhxEQhxFwhxMOR6AK8Zx/POK057O4tAEdS\n4LpX6ryNglhU/cE6yHc6xVRYWv80S5CxJyhdr6Yef4aEJggzqzKZWGgwuLq2/jLGh4v1wKNWq0fe\nIX9dWTVrf1Z5SUQXRm7p8DYS2YF9QESSqtygXaTEePpBMINHTpGJqvg9ruXuDnqRCMVIZNjuZhxy\nvrxm8+3Bp1lWGeod60ZzeYdgCMlIFGX+yWYzv3Pue6+10hB6AIMtFp+0JoEEUBXc4lMnGsX7TIK3\ngApgZCQZBHJMCPayZfUy8/Gq7q79aVorSwfWWYFDelGdQrn98nfJy9tfv+ZcpvOZVlEjblSMLi1o\nNqtivQIaZeNQRkYGK0wmtqWlccvLRY3Oe6u25f0Qr88qL1F6Lx17+BiPmguTJ09m3rx5PPjggxw+\nfNjj3LFjx7jhhhsazcNbQPcCBShRLY2G2sUNF/d92oa0tA4exw4mVYXezXWzZva0a0hKbhVcc3qF\nwoEinN8c+ifGfTtcP2g+DVovmih5mZr1NBKW2xUmE7e3r/JwjA9kIc/p2MN173M69mi6D+W6H2GL\n/yHNWWedxRNPPEGfPn344IMP+Pe//01pqTJ1d/DgQfr3b/y74i2gPVCCtp9uNGWULm4gBPOKOXwY\nWP+gx7FOcXUCFNSjximcqnvgDbPPZlBaB1f2A7/dJ6sFRppdDz2rvIRxWjPFp06wuesA129tav9m\nW5goqCdRE9TJnXdju7cqvQpLCClEvaA+oybTooPPhl7+3QKsVivPPPMMVVVV2O127G7RNb179yYx\nsfHZ9b0F9Hco0Sx/azTlBiKnyMSxkst83PvGac1kX7U5JBom0wpX+c2/uxLjDZKMh38g6/0trq/z\n6NICsr5fTdaOxWT9ZitQpx37tX+AbLvBv7YsDJzE+NeAS67KiYhxCOCaKVV0KLJ5OCzUN9fprkXV\nj21UjUadEkHvf+Fdg8HAlClTmDx5Mp999hkff/wxDoeD9evXA41Lv6nC29XvAeBCwL/rRAQRaDz1\nXcVX5H3nv0ynVkZYZSAj4zX/F4z/AlNhqY9wb/7dlfA7+JtpBfc7HCy+/nLWzqsG0/1+x53jtGaf\n8adKn97t6m1bNNDcuYDP2baNVRPe4cWn76bqs6SAzvMqsspLsNR6rsq9uesAlzCds20bSdf5LlMf\nrhPIiH07SBQaRuzbwaauA1w0IPqODVOnTgWgdevWvPLKK67j3bp18zjfGHgL6L3AUeASIHIev37g\n74XLKTLx088nGbp3WeCChjQyRvp5Wf2MwdSHrf7LP3dhcdxURhU9RW76NejMH4LZWc6ti6gKqQ8K\nK8F8bb1tiwYCCmgTjT3T27dn+e5qVu/dUu9K56NLC6iRDjY7BUaFuvp4otCQ8upE2Pe2T9lQhSqr\nvITiUycYEN/GdUy1E6TFt2lxXkcNhbeAJgIJfo77QAgxCmW5Qp2UcoEQYhqKf+4J6Uy9qWaWD5Qb\n1xvfWfN4f8kF2AKkuhinNTNzmGC21/Gs8hJyLEcwJR6nzet9XW5i6kPK6diDrO9Xsznu/2CkmXUb\nTZjeWsLfftrP0HOT67SiaQN8GKByl6+DtLNDakc0ULvMQPxdfuaTm+ij8cSA2ex/pv4UL+oYcZOX\ncAIMcApOVnkJf8ydwnnzGr4YUjABHF1a0HLdMetBfZnlzwXuBrrI+j2F1IzyTzv/O6EIuHo8cGZ5\n6esN9FyhUY5a3breAO3HSi7yKDvpYLHLw2T4t9ukNL5fd7HqOfShW5ZAL7dBrXa+4vZnfF/OqUyS\nS7ZeXEfD+L6Ul6xVfs3s2nfnEJ2vJ5Hx/SarV9fLb3JtBwreNu3Nd7no1QfvtrjT/LV4DPmDd2Z5\nb025EvgceFAIsVlKGdijty6aRfW2t6MYmFxd4w5Ly0mxFkDvUuVLr2qqr4/ARXWEcopM5H33Fe+/\n2Rnbu4HHNqNnrwBmcLbX7FlOxx4Yv9vO5r6DQC2+0QSLzPCWFjoOUxwQ/m8wFLwI79R1C222x8m7\naBU5OdPYlPEqAON6t6vTmC0k59C33W2k/fdHSF9XdzBZ12T1G+4WoJ5sNvtk5RtdWuDSkCHhgGdO\nAJubRfdM1HzZ2dlMmxY4wVioSElJ4cCBA659bwF9R0r5ghDiUeBkPbTUpR9qhBDpKEsWfgTcALzp\ncaXlSF330PlCZeQq8m0yrSDlwQps8ScY+pvOAZn1+uun9E6Eqxc8RJfXPI1Eo0sLOLqzktFJBcxa\neSdP3vY2a60WWGUga/AKcvo4Y+8e9pP4efwXDP32Vj45No6//aQhLTGBvLz9ytxpPTegKTHr92ug\nYhdsGNUs/JNMq8Atr5+7QKnd2nAEK+Oqczz2dc6g7xbXNf0mG9r2hlTf+M2VK1dy5MgRUlJSEEKQ\nl5eHVqvlo48+YsiQIWzYsIHTp08zf37DM+Z7C+gBIcRslLQn1cEKSim9XXOGe18zft9P/PCDMme2\nCTs97RoSBq8lbttohs0W5E1bRcrMp9B0+RZDjZ51k/1rz9GzV/DRoFc47zYzCxc85DKYqGPNtan9\nyTq+mpxpr33rAAASlElEQVSip5g4YhJrvxrKiEs/Iv5UNScf3F+nVf1h0RVk2w10apPuysTwN9MK\nLJZS0hJPYqZlaNCMjAxoRkPuoOs8VwPRpaV5aNFwHTi+/cv3Hs1JNpuxGgzkWFvWIr30C6wVd+/e\nzZQpU9i/fz/ffPONa1rFbrcjhOD6668nLy8vYPlQ4DEPKqVcLaWcKaV8W0rZ6PUUFi36LWlpHUhL\n68CIwlJuTK7hmnY1mEwrMOjjubJNHC+/NI1//70n/xrp/8HsedFA0eneinA6HbXdkdOxB2w0kWO5\nAyry0CW0A0Mam4oncWHyuQxcNpis71cDsH1+P0ymFVS91gaTaYXrV25J8bDams23Y7U+isVSEfL6\nlb8KuHlyJZvN2CwWRuzb0SDvqk5xvmvwzPxni8tRERSdO3cmJyeH4cOHU11dTbdu3Vi5ciXbtytO\nMRFJXi2jGG42eHC2NBqXu37aG3pKKRXDzAW/Q/7xM2TVVSsUI4wf3LyoUI6apaT6X240yq8maOXu\nhXpPA8NKvWtJCRfcDEJqmNKoHz5XjA/uxqIPjXKGrW5ZQ6Nxuetf/en1L0h4NvRRfpQwduxYKcQl\nzcb/1mEd5ZwTnvf5mNEoH12/qkH0xo4d60NPpRksg+CvAcGWfogsrvoPdGlLitPRPTO5CsNjBmw2\nK5mZBVx8TilJn96mXJu+Di6pM8rc8nIRJ/Z/wn/n3AvAANNm2vYyYizI4Qr+oZRRV0q7yyvXkRqR\nMtLMWrevdFbKw4w+P5s25au5eqCFvYYqZm8eCiM9l9Uzza8rY3V24YSYj14fH3T5vWjiP1+Xc+65\niR5Z25OTWzVZfRwnjnDuYTu7VwgSuhg57zYzd73+PG8OGApW//l1gmHV10dYcARwm4EbXVrAmxYL\nhpbWzW1GeKQ8iShhIdIfWzV4S+rFbVn3nue5vOI8xgzow2sPezk8X74OEnWMHl5JYU0KRU8PYfTs\nFThOWvn+RD/ikpIofGogubm55F3ej5xP0oMm9vKG6h30v0MP8/OpFOwyUZmv22jCsNZCaXoVqVuT\nsY5Og5Fm7MsEWrSuD4Da3W0OIc3NzeWml27ifm8vaQkXtjWS1Su6GfByc3P5ecg9AAx/swrRKpmF\nt2xl1l0PYGuAUOXm5pI36Gl+/76dO354hf3PDCKrvEShl5eHbujQer2VfqlwT3kSVQEdPHHwlvyT\n+SQnJAOQ1iWNlIEWyvLTGHUDfl+qqiuUoW/VpAXcv/MPlIjBPPKi0WfsOWLvdjZ1q9/OquZDyrYb\nKHfmCPv2nboQOHmzopE2d6+jlVOkCHJWLzNsNOH4SfEB1ghwSND8oXkyz43frmPRIM/egrhPcN8D\nGvroh0VdSAGyawTTEiV7XjTwzGXPsCj9fqwGAxw9ClothjDz2M48LfhuSSGl/WvYclWdj69VpwO7\nHZ3R2GIFdfLkyRgMSi9uwoQJ6N2Snn388ce88847vPDCCz5TMG+88QYjR44M6MDjLqBR7eIuunsR\n6enpHscm5RtIMuWR1cvzQfaekQ/Ahj/8ka7LF7N7yQRa39SfBx7pS4ehvj6bmz6/FLr5vgx/Pa7j\n6TY2ltpNWEqVrnV2qjNpmRpiOtm3rsZ9OzjbUczx01a+fMlC1ckqpqDD7rCjT9RTdbIKu8OOVqPl\nFALNucbIJjILAT2TfO+DfFUi7hPcm/kVND4CrF50ijey9IBgzQ07WZvan93PCc63Kh+sKpMJqxCg\n1ysCq9djsFqDrmTWrVLP7LZ9Gd3FQqc/b6dtgob+KYmsdgq6VadTaGq1kKx86HVpaU0mtEcdPxIv\nkmgtfCMwhRDExcVRW1tLTk4O3377LVlZWSxatIjhw4fjcDj44osvEEKwbds2PvzwQ44ePUrfvn15\n4403sNvtzJgxIyh/72iWqGFSvoFJ+Qa6JKbRM2ko3a74Pb1n5NN7Rj6dp2xnw7lG3tencc3+tVw4\npCMDKuNZsfg7Og8d6pPAODMz02fcOd2mWMy2LFJe4oNY6JgKaalpAVOu5BSZXKt3dz0wkB9KrZyM\nG8zAR9YzcdYubK/YkIsld4yt4t4H7TwwHsY9YOee7nD64MdKkHcTITMzk51zU/2ek69Kyn+uYcyK\nflHlD4q75cGztHBaieo5/3HJ7ucEB1YqQmhw9sjUf6sQ2PLysArh8ftDp04KvXOszLzVTk/rlxx8\nVunFfLm7mlteVla/NNhsGKRE5/aR9kfP52eISEgzP/AhZdL/wsl6vZ4pU6bQp08fMjMzufbaaxFC\ncO2119KrVy/uvPNOVq9ezalTp9DpdLRt29blhDBp0iR0uvrfn6i+YRMO7KTNXg2Jp7fTviKFzza+\nRPEzStrcnONXYNbXdTWvLv0ITXw7Lu6WyOqHejFfCMZc9hK3V2UpBqStgSfos+0GPv+Hkel/Egx+\nUMlg342hrqiUpXYTby2AQZl5/JSfTJchR/luk5aP/5dM1ckqkhOSsT5f123NKi9hR+1xRpe8xnnH\nxtE7yXOMNynfwH0XHOXeD3oydJlqStdCXHJYY+JwUbP3A/a8aGDizy+zvao7n3S8jPMfV+pdU2Sk\n47DN3PdpG14dcrweSo1DwdHlDOz6G9io9CLOf1yyO0fH7ucECC3nW51j9iDj0ji3+NJPDlj4Tb9L\nYNl4Cp9Synaesp1Of65bwbztVTkUmiMT5hYOBmvGBTx39OhR5s6dC8D69euprKzk4osvduXLFUIw\nceJEJk6cSPfu3ZFSUl2tuBe8/vrrIQlog8egfpzlx6CsLVotpXxNCJE+5IHsLW0e60Zt64tASECy\n5J1BZI6ykPnqGNZf8A4FZTV0qCjkkeJX2J+X51pSbrJ3vdxc3HKn2cjIyCDbbuAkR0lA79KSk/IN\n2NrBW/+AP80BS14Vyd3tdOwCJw/radOhiqfb2PjrcR3HK5KJ0/amX0oi6xe/TO97elNsHkS3y/M5\n+gN0GOBAuPcxJDgcWsr3pOH4eRdxrapZetl8ZQHhjSYl8Pv0UUBbl6GhIs9T26v5dr3/60Fuj2nc\n089AQs/haHAwML6A7bVpgAOAzzqmIyU8NwRsOmij07NgYOQ+Frm9ppFRlI1x3w40KM7wM5y9lgSb\nlk5xygfxwEoTJ3/crAhqVuDxaG5uLhkZGRj37WBz1wEstZvY69jM7FVAnN7nQ3fLy0UUlNW4Uq1E\nIqNDpHDgwAFWrlzJoUOHmDNnTqPnPyNiJBJCzJXOpR+klE+47T8tpfyrECJ94I0TthzseC3aZCV9\nSdzJGro9rieusorT7ZTxhJASHA7QaEAI5ecHGpQwpWpnGNN0m8BWq2XpzFewO2zcfPdDHPrpQr75\n6GUO12Yw7OoqDn/yIhfe+DiauBocdi2VP19MgqzhwKY/0fWWP2OvTCH+nGIkIG3xVFovJFEcp0a2\nId+sTJpXk1RXCQnTJ6TzUwo4NCAdWg4V2GnfD4RbmrVWp2HaGmX7pAZa29V7BnaHBq3GwdLhMO4j\nEGjJzrAzNVehP/cmmLoGXjMqZe5xxqibhuQr9wnQHavmmoF1Xb4uRiN/GvQMIEBIPjk3nRd+A7XO\nD3Rp8hISi1/kUI+lWOd8ycpe4xln2kqi9hSnDYnUHob4c+BExXESNQ7OqnyAsrbLSEqppmpPAr/p\na2BH7XEcDsnm23wXpsr+eDS1cSDVRychoboNJ5OOIxwauuX35Y7Zd1Bumk5H8xzKTdO5P8OCQ4MH\nvaUvTmXvwG/pdgjGfexgqRH2tne2sSyJ2+5czoplt3PPR/DVqT7cWvKKT11UtE3QRCywvClRVlZG\nfn4+o0aNarSAzpZSznQTTO/99HXr1m3Zftdd6DUaOjh9Lffn5fG4zcZC5xihQ1oaXy37Z73+l2r8\n39rU/hj3Weh52NOCu3n5ZobfPgL76UR0rao5fSoZXdxxbKfbUHm0N+30hQgkEtDF1XD00ACS9MV8\nsnot3R272KO9kLN+LCRer+e0sxvisNl4/NAKv4v4HFhpYk36Zsq7gK4G3npqs0uYx8w00ap1VdD2\nSHudUPvdliAddcfn6KSSw8espGaplo46Yg63bSHwfqICO6BBI6txCPWDo5RpfXo7J+IGoZE1OISa\nokMDOBCyBuk8Fn98Fxsu+oP/xrinXFmmA+xkZygfqeyb8BReJ7ofSGJcqu89Wmo3sZc8JHYEWrox\nFCry2HuOHal8f3xo+UPCKZiW2LLWeQkF3gLaGE+hG4HHgBkoAd73oCy8NFb6ySzvjcYkYQ5WVs16\n/lyh0bXWpPtxKesSLzcF1MVt3WF81igf2a33ucb4rFHevKhQ3vp+uuw13SIf2a2sj9o+69WoJa6e\ndLDYFeJ34zqt1E9UeHr/Is27OZNwt3RExJNI+jrLf91QWpGEqu2yepk9ph3ctaD3nGo04W+sZPYK\nClCvqTu+Ba4FsLqmhDIzP4lK/XI69gAn39zrbXC9/+tCWS7y14b8/HzWrFmDTqdjwIAB7Nmzh759\n+9KvXz8mTZrE8uXLG82j6eYJvNCYHDvNnZ+nOdDcbY40/+ZuT6jYVG2lvS6OixOSfM6tX7+eWbNm\nAXDNNdcwYcIEtm7dSseOHenbt29E+DfZPKg3YgIaHlpCZvmWTC9a6BHfmvN0rYJes3jxYnbt2sUl\nl1wCQP/+/dFqI7I4Q/Np0BhiOBPQNS4h4LmbbrqJGTNmkJyczOTJk1XbDBChUDOI/upmgRAtI9Ev\nFS1pdbOWSO+XhICrm8UQQwwtCzEj0RmC5m7zr3UM2tyIarjZli1btnhHs8QQQwzB4e7qF+vixhBD\nC0Y0BbS9R4ZsLzRm4lstW1ZWRllZWYPphIKWwiPo6mYRoF8f6uMdLo+GtKWlPIum5BH2GFQI0Q54\nHIgHsqWUx4QQQ1B8UNoCk6SUp4D4rVuVVcM6dOjgQ+fw4cOo58OFWlb9ALgn+o00WgoPdYHYhtyz\nSLShvucVLo+GPP+W8iyiyaOiooLi4mJQlmAJfQzqTGZ9JXAzcAWKt/UFUsrVQoj2wBHgSeA5p9C2\nB4YAtYD/FVBjiCEGb7RHUX6fSikPhW0kEkLcCJQCAugmpfyP8/hjQIGUsmUmkIkhhjMQDZlm+QiY\nhqIZnxVC3IKy6NJlQBshhEVKGcubGEMMEUA0p1nUjAtxUsqIpQwXQrQBXgKWAkagNTATmArEAYul\nlHsbQf9hoB3QE9gXafpOHtcB3YBzUHoiEefh5PMgysczKjycH+dBKOv4RJyHEMKEEsp4IdF7FrcC\n5wMPoiw+kxgFHrcAqUA/wmxHNK24V0opFwK+FqLGIQXIRxnfLgS+Ai4GdgLzgfCzKHtiDfB3YGyU\n6COlXA/sRYmljQoPIcQIlMWY7dHigSKcPwHHo8TjWiftH6JEHynlKhShWQK8GA0eKCsAtgduI8x2\nRFNAvZcnjAiklEWAFeXFkyhfbum27QhcOiRUAvOArCjRRwhxoVNIbdHiAVwD9EX5SkeLx/9JKZ9H\nuV/R4HG2lPIl4M9Roq/iCpTFwqLF4yIp5RNedEPiEc0u7o0o3TiHU5NGkvYfga3AXSiNmw3MBWqA\n16WU+xpBeznKF7UcOCvS9J087qAuS29iNHg4+aSiTH+dFw0ezi50GxSrY8TbIYQYjTLUaAO0ilIb\n2gB/ApYB46LE434gmQa0I2oCGkMMMTQeMVe/GGJowYgJaAwxtGDEBPQMhBDiRiGE38zNQogJEaA/\nTAhxtT+eznFhoyGEGBuoDTHUIZbyJMoQQjyOYq0tRplfPRdIAlYDtwNdgFUohoovUYwICcBzwEQU\nY1Uxiik+D2XuNBlIEEKMR3Gx/JebkcFlERRCfIAyt/czipvmccAMXI5iQdSirAbwd+BvwD+cPPXA\nu0KIJ5xlTgOdnPUa6Jyf/AYY7qyTBjimtk1KOcfJ/yFne5KAMqAP8B7wW+f2l0KIp93aeBtgllK+\n0YBb/YtETINGHwUo1mAtcDWK1e5sFOvqAhThBNgBvI3yon4MXIAyQV+DMpFeLqV8zUmrBGUuuBBF\naFoDCCH6At+58f5SSvk2inB+hyIo8SiCpQqdaiXUokw3rECxaAqgjZTyBRQrsMoToFZKuQTYL6Vc\njGKt/y1wAjhLCFee/c7O8q2dfN4ALgVygLUoc+TebYwJpxtiAhp99AAOo3iSbEIRkoPAByhzrbej\naFhV86n/p1GEtRWwhzpBkk56RhTtW4MiaABXAe4JdIcJIf6EouW6o2jbrigfgHjgR5QPw1+A/sCn\nKJr6ZiefGqcW3OvGU7rV0b1OZpRphHIppXOxCw44tbzq+mkH3kdJeH41UOHVRrVcDE7EplmaCUKI\nPihapxMwX0p5JAo85kopp0aabgxNh5iAxhBDC0asixtDDC0Y/w/B2ilvcN+rXQAAAABJRU5ErkJg\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": "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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": [
- "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": "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\nYII=\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "image/png": "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\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