From b6c998ee5153b7c3a7ac2d467228396c044b6ead Mon Sep 17 00:00:00 2001 From: Martin Kim Date: Mon, 17 Jun 2024 12:55:29 -0700 Subject: [PATCH 1/5] misc: update pyproject, typing, ci, style --- .bumpversion.cfg | 8 - .codecov.yaml | 22 +-- .cruft.json | 23 --- .editorconfig | 6 + .flake8 | 63 -------- .github/ISSUE_TEMPLATE/bug_report.yml | 164 ++++++++++----------- .github/ISSUE_TEMPLATE/config.yml | 6 +- .github/ISSUE_TEMPLATE/feature_request.yml | 14 +- .github/workflows/build.yaml | 46 +++--- .github/workflows/sync.yaml | 46 ------ .github/workflows/test.yaml | 108 +++++++------- .gitignore | 2 + .markdownlint.yaml | 21 +++ .pre-commit-config.yaml | 138 ++++++++--------- .readthedocs.yaml | 23 ++- CHANGELOG.md | 14 +- LICENSE | 29 ---- README.md | 63 ++++---- pyproject.toml | 157 +++++++++++++------- src/simple_scvi/_mymodel.py | 25 ++-- src/simple_scvi/_mymodule.py | 48 ++++-- src/simple_scvi/_mypyromodel.py | 51 ++++--- src/simple_scvi/_mypyromodule.py | 4 +- tests/test_basic.py | 4 +- 24 files changed, 508 insertions(+), 577 deletions(-) delete mode 100644 .bumpversion.cfg delete mode 100644 .cruft.json delete mode 100644 .flake8 delete mode 100644 .github/workflows/sync.yaml create mode 100644 .markdownlint.yaml delete mode 100644 LICENSE diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index fdba9e4..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[bumpversion] -current_version = 0.0.1 -tag = True -commit = True - -[bumpversion:file:./pyproject.toml] -search = version = "{current_version}" -replace = version = "{new_version}" diff --git a/.codecov.yaml b/.codecov.yaml index 829e56c..d0c0e29 100644 --- a/.codecov.yaml +++ b/.codecov.yaml @@ -1,17 +1,17 @@ # Based on pydata/xarray codecov: - require_ci_to_pass: no + require_ci_to_pass: no coverage: - status: - project: - default: - # Require 1% coverage, i.e., always succeed - target: 1 - patch: false - changes: false + 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 + layout: diff, flags, files + behavior: once + require_base: no diff --git a/.cruft.json b/.cruft.json deleted file mode 100644 index 69182b6..0000000 --- a/.cruft.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "template": "https://github.com/scverse/cookiecutter-scverse", - "commit": "7cc5403b05e299d7a4bb169c2bd8c27a2a7676f3", - "checkout": null, - "context": { - "cookiecutter": { - "project_name": "simple-scvi", - "package_name": "simple_scvi", - "project_description": "External and simple implementation of scVI", - "author_full_name": "Adam Gayoso", - "author_email": "adamgayoso@berkeley.edu", - "github_user": "adamgayoso", - "project_repo": "https://github.com/scverse/simple-scvi", - "license": "BSD 3-Clause License", - "_copy_without_render": [ - ".github/workflows/**.yaml", - "docs/_templates/autosummary/**.rst" - ], - "_template": "https://github.com/scverse/cookiecutter-scverse" - } - }, - "directory": null -} diff --git a/.editorconfig b/.editorconfig index 2fe0ce0..a8775f0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,5 +8,11 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +[*.{yml,yaml}] +indent_size = 2 + +[LICENSE] +insert_final_newline = false + [Makefile] indent_style = tab diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 192854e..0000000 --- a/.flake8 +++ /dev/null @@ -1,63 +0,0 @@ -[flake8] -max-line-length = 119 -ignore = - # Unnecessary dict call - rewrite as a literal. - C408 - # line break before a binary operator -> black does not adhere to PEP8 - W503 - # line break occured after a binary operator -> black does not adhere to PEP8 - W504 - # line too long -> we accept long comment lines; black gets rid of long code lines - E501 - # whitespace before : -> black does not adhere to PEP8 - E203 - # missing whitespace after ,', ';', or ':' -> black does not adhere to PEP8 - E231 - # continuation line over-indented for hanging indent -> black does not adhere to PEP8 - E126 - # too many leading '#' for block comment -> this is fine for indicating sections - E262 - # Do not assign a lambda expression, use a def -> lambda expression assignments are convenient - E731 - # allow I, O, l as variable names -> I is the identity matrix - E741 - # Missing docstring in public package - D104 - # Missing docstring in public module - D100 - # Missing docstring in __init__ - D107 - # Missing docstring in magic method - D105 - # format string does contain unindexed parameters - P101 - # first line should end with a period [Bug: doesn't work with single-line docstrings] - D400 - # First line should be in imperative mood; try rephrasing - D401 -exclude = .git,__pycache__,build,docs/_build,dist,scvi/__init__.py -per-file-ignores = - tests/*: D - */__init__.py: F401 -extend-immutable-calls = - # Add functions returning immutable values here to avoid B008 - pathlib.Path - Path -rst-roles = - class, - func, - ref, - meth, - doc, - py:class, - method, - attr, - cite:p, - cite:t, -rst-directives = - envvar, - exception, -rst-substitutions = - version, -extend-ignore = - RST307,RST210,RST201,RST203,RST301 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9dfd4ac..e49de1f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -2,88 +2,88 @@ name: Bug report description: Report something that is broken or incorrect labels: 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 procecure. - * Sometimes it is not possible to share the data but usually it is possible to replicate problems on publicly - available datasets or to share a subset of your data. + - 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 procecure. + * Sometimes it is not possible to share the data but usually it is possible to replicate problems on publicly + available datasets or to share a subset of your data. - - type: textarea - id: report - attributes: - label: Report - description: A clear and concise description of what the bug is. - validations: - required: true + - 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: Version information - description: | - Please paste below the output of + - type: textarea + id: versions + attributes: + label: Version information + description: | + Please paste below the output of - ```python - import session_info - session_info.show(html=False, dependencies=True) - ``` - placeholder: | - ----- - anndata 0.8.0rc2.dev27+ge524389 - session_info 1.0.0 - ----- - asttokens NA - awkward 1.8.0 - backcall 0.2.0 - cython_runtime NA - dateutil 2.8.2 - debugpy 1.6.0 - decorator 5.1.1 - entrypoints 0.4 - executing 0.8.3 - h5py 3.7.0 - ipykernel 6.15.0 - jedi 0.18.1 - mpl_toolkits NA - natsort 8.1.0 - numpy 1.22.4 - packaging 21.3 - pandas 1.4.2 - parso 0.8.3 - pexpect 4.8.0 - pickleshare 0.7.5 - pkg_resources NA - prompt_toolkit 3.0.29 - psutil 5.9.1 - ptyprocess 0.7.0 - pure_eval 0.2.2 - pydev_ipython NA - pydevconsole NA - pydevd 2.8.0 - pydevd_file_utils NA - pydevd_plugins NA - pydevd_tracing NA - pygments 2.12.0 - pytz 2022.1 - scipy 1.8.1 - setuptools 62.5.0 - setuptools_scm NA - six 1.16.0 - stack_data 0.3.0 - tornado 6.1 - traitlets 5.3.0 - wcwidth 0.2.5 - zmq 23.1.0 - ----- - IPython 8.4.0 - jupyter_client 7.3.4 - jupyter_core 4.10.0 - ----- - Python 3.9.13 | packaged by conda-forge | (main, May 27 2022, 16:58:50) [GCC 10.3.0] - Linux-5.18.6-arch1-1-x86_64-with-glibc2.35 - ----- - Session information updated at 2022-07-07 17:55 + ```python + import session_info + session_info.show(html=False, dependencies=True) + ``` + placeholder: | + ----- + anndata 0.8.0rc2.dev27+ge524389 + session_info 1.0.0 + ----- + asttokens NA + awkward 1.8.0 + backcall 0.2.0 + cython_runtime NA + dateutil 2.8.2 + debugpy 1.6.0 + decorator 5.1.1 + entrypoints 0.4 + executing 0.8.3 + h5py 3.7.0 + ipykernel 6.15.0 + jedi 0.18.1 + mpl_toolkits NA + natsort 8.1.0 + numpy 1.22.4 + packaging 21.3 + pandas 1.4.2 + parso 0.8.3 + pexpect 4.8.0 + pickleshare 0.7.5 + pkg_resources NA + prompt_toolkit 3.0.29 + psutil 5.9.1 + ptyprocess 0.7.0 + pure_eval 0.2.2 + pydev_ipython NA + pydevconsole NA + pydevd 2.8.0 + pydevd_file_utils NA + pydevd_plugins NA + pydevd_tracing NA + pygments 2.12.0 + pytz 2022.1 + scipy 1.8.1 + setuptools 62.5.0 + setuptools_scm NA + six 1.16.0 + stack_data 0.3.0 + tornado 6.1 + traitlets 5.3.0 + wcwidth 0.2.5 + zmq 23.1.0 + ----- + IPython 8.4.0 + jupyter_client 7.3.4 + jupyter_core 4.10.0 + ----- + Python 3.9.13 | packaged by conda-forge | (main, May 27 2022, 16:58:50) [GCC 10.3.0] + Linux-5.18.6-arch1-1-x86_64-with-glibc2.35 + ----- + Session information updated at 2022-07-07 17:55 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5cad625..5b62547 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +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. + - 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 index e3593b2..b8e1b4b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -2,10 +2,10 @@ name: Feature request description: Propose a new feature for simple-scvi labels: 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 + - 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 index 0242943..a633730 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,23 +1,31 @@ -name: Check Build +name: build on: - push: - branches: [main] - pull_request: - branches: [main] + 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@v2 - - name: Set up Python 3.10 - uses: actions/setup-python@v2 - with: - python-version: "3.10" - - name: Install build dependencies - run: python -m pip install --upgrade pip wheel twine build - - name: Build package - run: python -m build - - name: Check package - run: twine check --strict dist/*.whl + package: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install build dependencies + run: python -m pip install --upgrade pip wheel twine build + + - name: Build package + run: python -m build + + - name: Check package + run: twine check --strict dist/*.whl diff --git a/.github/workflows/sync.yaml b/.github/workflows/sync.yaml deleted file mode 100644 index 5219499..0000000 --- a/.github/workflows/sync.yaml +++ /dev/null @@ -1,46 +0,0 @@ -name: Sync Template - -on: - workflow_dispatch: - schedule: - - cron: "0 2 * * *" # every night at 2:00 UTC - -jobs: - sync: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.10 - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Install dependencies - # for now, pin cookiecutter version, due to https://github.com/cruft/cruft/issues/166 - run: python -m pip install --upgrade cruft "cookiecutter<2" pre-commit toml - - name: Find Latest Tag - uses: oprypin/find-latest-tag@v1.1.0 - id: get-latest-tag - with: - repository: scverse/cookiecutter-scverse - releases-only: false - sort-tags: true - regex: '^v\d+\.\d+\.\d+$' # vX.X.X - - name: Sync - run: | - cruft update --checkout ${{ steps.get-latest-tag.outputs.tag }} --skip-apply-ask --project-dir . - - name: Create Pull Request - uses: peter-evans/create-pull-request@v4 - with: - commit-message: Automated template update from cookiecutter-scverse - branch: template-update - title: Automated template update from cookiecutter-scverse - body: | - A new version of the [scverse cookiecutter template](https://github.com/scverse/cookiecutter-scverse/releases) - got released. This PR adds all new changes to your repository and helps to to stay in sync with - the latest best-practice template maintained by the scverse team. - - **If a merge conflict arised, a `.rej` file with the rejected patch is generated. You'll need to - manually merge these changes.** - - For more information about the template sync, please refer to the - [template documentation](https://cookiecutter-scverse-instance.readthedocs.io/en/latest/developer_docs.html#automated-template-sync). diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2f0ffd7..63186ea 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,62 +1,58 @@ -name: Test +name: test on: - push: - branches: [main] - pull_request: - branches: [main] + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "0 10 * * *" # runs at 10:00 UTC (03:00 PST) every day + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - test: - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash -e {0} # -e to fail on error - - strategy: - fail-fast: false - matrix: - python: ["3.8", "3.10"] - os: [ubuntu-latest] + test: + runs-on: ${{ matrix.os }} + + defaults: + run: + shell: bash -e {0} # -e to fail on error + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python: ["3.9", "3.10", "3.11"] + + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python }} + steps: + - uses: actions/checkout@v4 + + - name: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip wheel uv + python -m uv pip install --system "simple-scvi[test] @ ." + + - name: Pytest env: - OS: ${{ matrix.os }} - PYTHON: ${{ matrix.python }} - - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - - - name: Get pip cache dir - id: pip-cache-dir - run: | - echo "::set-output name=dir::$(pip cache dir)" - - name: Restore pip cache - uses: actions/cache@v2 - with: - path: ${{ steps.pip-cache-dir.outputs.dir }} - key: pip-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - pip-${{ runner.os }}-${{ env.pythonLocation }}- - - name: Install test dependencies - run: | - python -m pip install --upgrade pip wheel - pip install codecov - - name: Install dependencies - run: | - pip install ".[dev,test]" - - name: Test - env: - MPLBACKEND: agg - PLATFORM: ${{ matrix.os }} - DISPLAY: :42 - run: | - pytest -v --cov --color=yes - - name: Upload coverage - env: - CODECOV_NAME: ${{ matrix.python }}-${{ matrix.os }} - run: | - codecov --required --flags=unittests + MPLBACKEND: agg + PLATFORM: ${{ matrix.os }} + DISPLAY: :42 + COLUMNS: 120 + run: | + coverage run -m pytest -v --color=yes + coverage report + + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 7bb0bd5..60c7fc3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ __pycache__/ # IDEs /.idea/ /.vscode/ + +/node_modules/ diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..4d2ed7a --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,21 @@ +# default to true for all rules +default: true + +# MD007/unordered-list-indent +MD007: + indent: 4 + +# MD033/no-inline-html +MD033: false + +# MD041/first-line-h1 +MD041: false + +# MD013/line-length +MD013: + line_length: 99 + +# MD024/no-duplicate-heading +MD024: + # Allow when nested under different parents e.g. CHANGELOG.md + siblings_only: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a2319b..a512f5d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,79 +1,69 @@ fail_fast: false default_language_version: - python: python3 + python: python3 default_stages: - - commit - - push + - commit + - push minimum_pre_commit_version: 2.16.0 repos: - - repo: https://github.com/psf/black - rev: 24.4.2 - hooks: - - id: black - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v4.0.0-alpha.8 - hooks: - - id: prettier - - repo: https://github.com/asottile/blacken-docs - rev: 1.16.0 - hooks: - - id: blacken-docs - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - - repo: https://github.com/asottile/yesqa - rev: v1.5.0 - hooks: - - id: yesqa - additional_dependencies: - - flake8-tidy-imports - - flake8-docstrings - - flake8-rst-docstrings - - flake8-comprehensions - - flake8-bugbear - - flake8-blind-except - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.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 - - repo: https://github.com/PyCQA/autoflake - rev: v2.3.1 - hooks: - - id: autoflake - args: - - --in-place - - --remove-all-unused-imports - - --remove-unused-variable - - --ignore-init-module-imports - - repo: https://github.com/PyCQA/flake8 - rev: 7.1.0 - hooks: - - id: flake8 - additional_dependencies: - - flake8-tidy-imports - - flake8-docstrings - - flake8-rst-docstrings - - flake8-comprehensions - - flake8-bugbear - - flake8-blind-except - - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 - hooks: - - id: pyupgrade - args: [--py3-plus, --py38-plus, --keep-runtime-typing] - - repo: local - hooks: - - id: forbid-to-commit - name: Don't commit rej files - entry: | - Cannot commit .rej files. These indicate merge conflicts that arise during automated template updates. - Fix the merge conflicts manually and remove the .rej files. - language: fail - files: '.*\.rej$' + - repo: https://github.com/asottile/blacken-docs + rev: 1.16.0 + hooks: + - id: blacken-docs + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + types: [yaml] + + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.17 + hooks: + - id: mdformat + additional_dependencies: + - mdformat-mkdocs + - mdformat-admon + exclude: | + (?x)^( + \.github/.*\.md + | docs/.*\.md + )$ + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.41.0 + hooks: + - id: markdownlint-fix + exclude: | + (?x)^( + \.github/.*\.md + | docs/.*\.md + )$ + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.8 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.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 + + - repo: local + hooks: + - id: forbid-to-commit + name: Don't commit rej files + entry: | + Cannot commit .rej files. These indicate merge conflicts that arise during automated template updates. + Fix the merge conflicts manually and remove the .rej files. + language: fail + files: '.*\.rej$' diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1bf3915..b842928 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,16 +1,15 @@ -# https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: - os: ubuntu-20.04 - tools: - python: "3.10" + os: ubuntu-20.04 + tools: + python: "3.11" sphinx: - configuration: docs/conf.py - # disable this for more lenient docs builds - fail_on_warning: false + configuration: docs/conf.py + # disable this for more lenient docs builds + fail_on_warning: false python: - install: - - method: pip - path: . - extra_requirements: - - doc + install: + - method: pip + path: . + extra_requirements: + - doc diff --git a/CHANGELOG.md b/CHANGELOG.md index e7b7808..1a4ab48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,14 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog][], -and this project adheres to [Semantic Versioning][]. +The format is based on [Keep a Changelog], +and this project adheres to [Semantic Versioning]. -[keep a changelog]: https://keepachangelog.com/en/1.0.0/ -[semantic versioning]: https://semver.org/spec/v2.0.0.html - -## [Unreleased] +## \[Unreleased\] ### Added -- Basic tool, preprocessing and plotting functions +- Basic tool, preprocessing and plotting functions + +[keep a changelog]: https://keepachangelog.com/en/1.0.0/ +[semantic versioning]: https://semver.org/spec/v2.0.0.html diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 18461d3..0000000 --- a/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2023, Adam Gayoso -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index c2d91ba..b462e51 100644 --- a/README.md +++ b/README.md @@ -3,36 +3,24 @@ [![Tests][badge-tests]][link-tests] [![Documentation][badge-docs]][link-docs] -[badge-tests]: https://img.shields.io/github/actions/workflow/status/scverse/simple-scvi/test.yaml?branch=main -[link-tests]: https://github.com/scverse/simple-scvi/actions/workflows/test.yml -[badge-docs]: https://img.shields.io/readthedocs/simple-scvi +External and simple implementation of scVI. This repository shows a minimal implementation of the +[scVI](https://www.nature.com/articles/s41592-018-0229-2) model using +[scvi-tools](https://scvi-tools.org) in an externally deployed package. -External and simple implementation of scVI. This repository shows a minimal implementation of the [scVI](https://www.nature.com/articles/s41592-018-0229-2) model using [scvi-tools](https://scvi-tools.org) in an externally deployed package. - -This package was initialized using the [cookicutter-scverse](https://github.com/scverse/cookiecutter-scverse) template. We advise all external projects to use the cookicutter template. +This package was initialized using the +[cookicutter-scverse](https://github.com/scverse/cookiecutter-scverse) template. We advise all +external projects to use the cookicutter template. ## Getting started Please refer to the [documentation][link-docs]. In particular, the -- [API documentation][link-api]. +- [API documentation][link-api]. ## Installation -You need to have Python 3.8 or newer installed on your system. If you don't have -Python installed, we recommend installing [Mambaforge](https://github.com/conda-forge/miniforge#mambaforge). - -There are several alternative options to install simple-scvi: - - - -1. Install the latest development version: +You need to have Python 3.9 or newer installed on your system. Install the latest development +version using pip: ```bash pip install git+https://github.com/adamgayoso/simple-scvi.git@main @@ -40,7 +28,7 @@ pip install git+https://github.com/adamgayoso/simple-scvi.git@main ## Release notes -See the [changelog][changelog]. +See the [changelog]. ## Contact @@ -49,21 +37,22 @@ If you found a bug, please use the [issue tracker][issue-tracker]. ## Citation -``` -@article{gayoso2022python, - title={A Python library for probabilistic analysis of single-cell omics data}, - author={Gayoso, Adam and Lopez, Romain and Xing, Galen and Boyeau, Pierre and Valiollah Pour Amiri, Valeh and Hong, Justin and Wu, Katherine and Jayasuriya, Michael and Mehlman, Edouard and Langevin, Maxime and others}, - journal={Nature biotechnology}, - volume={40}, - number={2}, - pages={163--166}, - year={2022}, - publisher={Nature Publishing Group US New York} -} -``` +> **A Python library for probabilistic analysis of single-cell omics data** +> +> Adam Gayoso, Romain Lopez, Galen Xing, Pierre Boyeau, Valeh Valiollah Pour Amiri, Justin Hong, +> Katherine Wu, Michael Jayasuriya, Edouard Mehlman, Maxime Langevin, Yining Liu, Jules Samaran, +> Gabriel Misrachi, Achille Nazaret, Oscar Clivio, Chenling Xu, Tal Ashuach, Mariano Gabitto, +> Mohammad Lotfollahi, Valentine Svensson, Eduardo da Veiga Beltrame, Vitalii Kleshchevnikov, +> Carlos Talavera-López, Lior Pachter, Fabian J. Theis, Aaron Streets, Michael I. Jordan, +> Jeffrey Regier & Nir Yosef +> +> _Nature Biotechnology_ 2022 Feb 07. doi: [10.1038/s41587-021-01206-w](https://doi.org/10.1038/s41587-021-01206-w). -[scverse-discourse]: https://discourse.scverse.org/ -[issue-tracker]: https://github.com/adamgayoso/simple-scvi/issues +[badge-docs]: https://img.shields.io/readthedocs/simple-scvi +[badge-tests]: https://img.shields.io/github/actions/workflow/status/scverse/simple-scvi/test.yaml?branch=main [changelog]: https://simple-scvi.readthedocs.io/latest/changelog.html -[link-docs]: https://simple-scvi.readthedocs.io +[issue-tracker]: https://github.com/adamgayoso/simple-scvi/issues [link-api]: https://simple-scvi.readthedocs.io/latest/api.html +[link-docs]: https://simple-scvi.readthedocs.io +[link-tests]: https://github.com/scverse/simple-scvi/actions/workflows/test.yml +[scverse-discourse]: https://discourse.scverse.org/ diff --git a/pyproject.toml b/pyproject.toml index 0b68238..656b170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,55 +2,52 @@ build-backend = "hatchling.build" requires = ["hatchling"] - [project] name = "simple-scvi" version = "0.0.1" description = "External and simple implementation of scVI" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = {file = "LICENSE"} authors = [ - {name = "Adam Gayoso"}, + {name = "The scvi-tools development team"}, ] maintainers = [ - {name = "Adam Gayoso", email = "adamgayoso@berkeley.edu"}, + {name = "The scvi-tools development team", email = "adamgayoso@berkeley.edu"}, ] urls.Documentation = "https://simple-scvi.readthedocs.io/" urls.Source = "https://github.com/scverse/simple-scvi" urls.Home-page = "https://github.com/scverse/simple-scvi" dependencies = [ - "anndata", # for debug logging (referenced from the issue template) "session-info", - "rich", - "scvi-tools>=0.20.1", - "torch", + "scvi-tools>=1.0", ] [project.optional-dependencies] dev = [ - # CLI for bumping the version number - "bump2version", + "jupyter", "pre-commit", - "twine>=4.0.2" ] doc = [ - "sphinx>=4", - "sphinx-book-theme>=0.3.3", + "docutils>=0.20", + "ipython", "myst-nb", + "sphinx>=4.1", "sphinxcontrib-bibtex>=1.0.0", "sphinx-autodoc-typehints", - # For notebooks - "ipykernel", - "ipython", + "sphinx-book-theme>=1.0.1", "sphinx-copybutton", ] test = [ "pytest", - "pytest-cov", + "pytest-pretty", + "coverage", ] +[tool.hatch.build.targets.wheel] +packages = ['src/simple_scvi'] + [tool.coverage.run] source = ["simple_scvi"] omit = [ @@ -64,44 +61,96 @@ addopts = [ "--import-mode=importlib", # allow using test files with same name ] -[tool.isort] -include_trailing_comma = true -multi_line_output = 3 -profile = "black" -skip_glob = ["docs/*"] +[tool.ruff] +src = ["src"] +line-length = 99 +indent-width = 4 +target-version = "py39" + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +[tool.ruff.lint] +select = [ + "F", # Errors detected by Pyflakes + "E", # Error detected by Pycodestyle + "W", # Warning detected by Pycodestyle + "I", # isort + "D", # pydocstyle + "B", # flake8-bugbear + "TID", # flake8-tidy-imports + "C4", # flake8-comprehensions + "BLE", # flake8-blind-except + "UP", # pyupgrade + "RUF100", # Report unused noqa directives +] +ignore = [ + # allow I, O, l as variable names -> I is the identity matrix + "E741", + # Missing docstring in public package + "D104", + # Missing docstring in public module + "D100", + # Missing docstring in __init__ + "D107", + # Errors from function calls in argument defaults. These are fine when the result is immutable. + "B008", + # __magic__ methods are are often self-explanatory, allow missing docstrings + "D105", + # first line should end with a period [Bug: doesn't work with single-line docstrings] + "D400", + # First line should be in imperative mood; try rephrasing + "D401", + ## Disable one in each pair of mutually incompatible rules + # We don’t want a blank line before a class docstring + "D203", + # We want docstrings to start immediately after the opening triple quote + "D213", +] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.ruff.lint.per-file-ignores] +"docs/*" = ["I", "BLE001"] +"tests/*" = ["D"] +"*/__init__.py" = ["F401"] +"src/scvi/__init__.py" = ["I"] + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" -[tool.black] -line-length = 120 -target-version = ['py38'] -include = '\.pyi?$' -exclude = ''' -( - /( - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ -) -''' +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" [tool.jupytext] formats = "ipynb,md" - -[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/src/simple_scvi/_mymodel.py b/src/simple_scvi/_mymodel.py index 69cc50d..038f207 100644 --- a/src/simple_scvi/_mymodel.py +++ b/src/simple_scvi/_mymodel.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import logging -from typing import List, Optional from anndata import AnnData from scvi import REGISTRY_KEYS @@ -38,6 +39,7 @@ class MyModel(VAEMixin, UnsupervisedTrainingMixin, BaseModelClass): Number of hidden layers used for encoder and decoder NNs. **model_kwargs Keyword args for :class:`~mypackage.MyModule` + Examples -------- >>> adata = anndata.read_h5ad(path_to_anndata) @@ -57,7 +59,9 @@ def __init__( ): super().__init__(adata) - library_log_means, library_log_vars = _init_library_size(self.adata_manager, self.summary_stats["n_batch"]) + library_log_means, library_log_vars = _init_library_size( + self.adata_manager, self.summary_stats["n_batch"] + ) # self.summary_stats provides information about anndata dimensions and other tensor info @@ -70,7 +74,9 @@ def __init__( library_log_vars=library_log_vars, **model_kwargs, ) - self._model_summary_string = "Overwrite this attribute to get an informative representation for your model" + self._model_summary_string = ( + "Overwrite this attribute to get an informative representation for your model" + ) # necessary line to get params that will be used for saving/loading self.init_params_ = self._get_init_params(locals()) @@ -81,13 +87,13 @@ def __init__( def setup_anndata( cls, adata: AnnData, - batch_key: Optional[str] = None, - labels_key: Optional[str] = None, - layer: Optional[str] = None, - categorical_covariate_keys: Optional[List[str]] = None, - continuous_covariate_keys: Optional[List[str]] = None, + batch_key: str | None = None, + labels_key: str | None = None, + layer: str | None = None, + categorical_covariate_keys: list[str] | None = None, + continuous_covariate_keys: list[str] | None = None, **kwargs, - ) -> Optional[AnnData]: + ) -> AnnData | None: """ %(summary)s. @@ -99,6 +105,7 @@ def setup_anndata( %(param_layer)s %(param_cat_cov_keys)s %(param_cont_cov_keys)s + Returns ------- %(returns)s diff --git a/src/simple_scvi/_mymodule.py b/src/simple_scvi/_mymodule.py index 28bf534..038a3c1 100644 --- a/src/simple_scvi/_mymodule.py +++ b/src/simple_scvi/_mymodule.py @@ -1,4 +1,4 @@ -from typing import Dict +from __future__ import annotations import numpy as np import torch @@ -10,7 +10,7 @@ from torch.distributions import Normal from torch.distributions import kl_divergence as kl -TensorDict = Dict[str, torch.Tensor] +TensorDict = dict[str, torch.Tensor] class MyModule(BaseModuleClass): @@ -28,8 +28,8 @@ class MyModule(BaseModuleClass): 1 x n_batch array of means of the log library sizes. Parameterizes prior on library size if not using observed library size. library_log_vars - 1 x n_batch array of variances of the log library sizes. Parameterizes prior on library size if - not using observed library size. + 1 x n_batch array of variances of the log library sizes. Parameterizes prior on library + size if not using observed library size. n_batch Number of batches, if 0, no batch correction is performed. n_hidden @@ -93,7 +93,7 @@ def _get_inference_input(self, tensors): """Parse the dictionary to get appropriate args""" x = tensors[REGISTRY_KEYS.X_KEY] - input_dict = dict(x=x) + input_dict = {"x": x} return input_dict def _get_generative_input(self, tensors, inference_outputs): @@ -119,7 +119,14 @@ def inference(self, x): qz_m, qz_v, z = self.z_encoder(x_) ql_m, ql_v, library = self.l_encoder(x_) - outputs = dict(z=z, qz_m=qz_m, qz_v=qz_v, ql_m=ql_m, ql_v=ql_v, library=library) + outputs = { + "z": z, + "qz_m": qz_m, + "qz_v": qz_v, + "ql_m": ql_m, + "ql_v": ql_v, + "library": library, + } return outputs @auto_move_data @@ -129,7 +136,12 @@ def generative(self, z, library): px_scale, _, px_rate, px_dropout = self.decoder("gene", z, library) px_r = torch.exp(self.px_r) - return dict(px_scale=px_scale, px_r=px_r, px_rate=px_rate, px_dropout=px_dropout) + return { + "px_scale": px_scale, + "px_r": px_r, + "px_rate": px_rate, + "px_dropout": px_dropout, + } def loss( self, @@ -164,7 +176,9 @@ def loss( ).sum(dim=1) reconst_loss = ( - -ZeroInflatedNegativeBinomial(mu=px_rate, theta=px_r, zi_logits=px_dropout).log_prob(x).sum(dim=-1) + -ZeroInflatedNegativeBinomial(mu=px_rate, theta=px_r, zi_logits=px_dropout) + .log_prob(x) + .sum(dim=-1) ) kl_local_for_warmup = kl_divergence_z @@ -174,7 +188,10 @@ def loss( loss = torch.mean(reconst_loss + weighted_kl_local) - kl_local = dict(kl_divergence_l=kl_divergence_l, kl_divergence_z=kl_divergence_z) + kl_local = { + "kl_divergence_l": kl_divergence_l, + "kl_divergence_z": kl_divergence_z, + } return LossOutput(loss=loss, reconstruction_loss=reconst_loss, kl_local=kl_local) @torch.no_grad() @@ -197,12 +214,13 @@ def sample( Number of required samples for each cell library_size Library size to scale scamples to + Returns ------- x_new tensor with shape (n_cells, n_genes, n_samples) """ - inference_kwargs = dict(n_samples=n_samples) + inference_kwargs = {"n_samples": n_samples} ( _, generative_outputs, @@ -249,9 +267,15 @@ def marginal_ll(self, tensors: TensorDict, n_mc_samples: int): # Log-probabilities n_batch = self.library_log_means.shape[1] - local_library_log_means = F.linear(one_hot(batch_index, n_batch), self.library_log_means) + local_library_log_means = F.linear( + one_hot(batch_index, n_batch), self.library_log_means + ) local_library_log_vars = F.linear(one_hot(batch_index, n_batch), self.library_log_vars) - p_l = Normal(local_library_log_means, local_library_log_vars.sqrt()).log_prob(library).sum(dim=-1) + p_l = ( + Normal(local_library_log_means, local_library_log_vars.sqrt()) + .log_prob(library) + .sum(dim=-1) + ) p_z = Normal(torch.zeros_like(qz_m), torch.ones_like(qz_v)).log_prob(z).sum(dim=-1) p_x_zl = -reconst_loss diff --git a/src/simple_scvi/_mypyromodel.py b/src/simple_scvi/_mypyromodel.py index 104483f..a13700e 100644 --- a/src/simple_scvi/_mypyromodel.py +++ b/src/simple_scvi/_mypyromodel.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import logging -from typing import List, Optional, Sequence, Union +from collections.abc import Sequence import numpy as np import torch @@ -16,6 +18,7 @@ from scvi.model.base import BaseModelClass from scvi.train import PyroTrainingPlan, TrainRunner from scvi.utils import setup_anndata_dsp +from scvi.utils._docstrings import devices_dsp from ._mypyromodule import MyPyroModule @@ -40,6 +43,7 @@ class MyPyroModel(BaseModelClass): Number of hidden layers used for encoder and decoder NNs. **model_kwargs Keyword args for :class:`~mypackage.MyModule` + Examples -------- >>> adata = anndata.read_h5ad(path_to_anndata) @@ -68,7 +72,9 @@ def __init__( n_layers=n_layers, **model_kwargs, ) - self._model_summary_string = "Overwrite this attribute to get an informative representation for your model" + self._model_summary_string = ( + "Overwrite this attribute to get an informative representation for your model" + ) # necessary line to get params that will be used for saving/loading self.init_params_ = self._get_init_params(locals()) @@ -76,9 +82,9 @@ def __init__( def get_latent( self, - adata: Optional[AnnData] = None, - indices: Optional[Sequence[int]] = None, - batch_size: Optional[int] = None, + adata: AnnData | None = None, + indices: Sequence[int] | None = None, + batch_size: int | None = None, ): """ Return the latent representation for each cell. @@ -94,6 +100,7 @@ def get_latent( Indices of cells in adata to use. If `None`, all cells are used. batch_size Minibatch size for data loading into model. Defaults to `scvi.settings.batch_size`. + Returns ------- latent_representation : np.ndarray @@ -107,14 +114,16 @@ def get_latent( latent += [qz_m.cpu()] return np.array(torch.cat(latent)) + @devices_dsp.dedent def train( self, - max_epochs: Optional[int] = None, - use_gpu: Optional[Union[str, int, bool]] = None, + max_epochs: int | None = None, + accelerator: str = "auto", + devices: int | list[int] | str = "auto", train_size: float = 0.9, - validation_size: Optional[float] = None, + validation_size: float | None = None, batch_size: int = 128, - plan_kwargs: Optional[dict] = None, + plan_kwargs: dict | None = None, **trainer_kwargs, ): """ @@ -125,9 +134,8 @@ def train( max_epochs Number of passes through the dataset. If `None`, defaults to `np.min([round((20000 / n_cells) * 400), 400])` - use_gpu - Use default GPU if available (if None or True), or index of GPU to use (if int), - or name of GPU (if str), or use CPU (if False). + %(param_accelerator)s + %(param_devices)s train_size Size of training set in the range [0.0, 1.0]. validation_size @@ -145,14 +153,13 @@ def train( n_cells = self.adata.n_obs max_epochs = np.min([round((20000 / n_cells) * 400), 400]) - plan_kwargs = plan_kwargs if isinstance(plan_kwargs, dict) else dict() + plan_kwargs = plan_kwargs if isinstance(plan_kwargs, dict) else {} data_splitter = DataSplitter( self.adata_manager, train_size=train_size, validation_size=validation_size, batch_size=batch_size, - use_gpu=use_gpu, ) training_plan = PyroTrainingPlan(self.module, **plan_kwargs) runner = TrainRunner( @@ -160,7 +167,8 @@ def train( training_plan=training_plan, data_splitter=data_splitter, max_epochs=max_epochs, - use_gpu=use_gpu, + accelerator=accelerator, + devices=devices, **trainer_kwargs, ) return runner() @@ -170,13 +178,13 @@ def train( def setup_anndata( cls, adata: AnnData, - batch_key: Optional[str] = None, - labels_key: Optional[str] = None, - layer: Optional[str] = None, - categorical_covariate_keys: Optional[List[str]] = None, - continuous_covariate_keys: Optional[List[str]] = None, + batch_key: str | None = None, + labels_key: str | None = None, + layer: str = None, + categorical_covariate_keys: list[str] | None = None, + continuous_covariate_keys: list[str] | None = None, **kwargs, - ) -> Optional[AnnData]: + ) -> AnnData | None: """ %(summary)s. @@ -188,6 +196,7 @@ def setup_anndata( %(param_layer)s %(param_cat_cov_keys)s %(param_cont_cov_keys)s + Returns ------- %(returns)s diff --git a/src/simple_scvi/_mypyromodule.py b/src/simple_scvi/_mypyromodule.py index ad6f412..124ec11 100644 --- a/src/simple_scvi/_mypyromodule.py +++ b/src/simple_scvi/_mypyromodule.py @@ -1,4 +1,4 @@ -from typing import Dict +from __future__ import annotations import pyro import pyro.distributions as dist @@ -8,7 +8,7 @@ from scvi.module.base import PyroBaseModuleClass, auto_move_data from scvi.nn import DecoderSCVI, Encoder -TensorDict = Dict[str, torch.Tensor] +TensorDict = dict[str, torch.Tensor] class MyPyroModule(PyroBaseModuleClass): diff --git a/tests/test_basic.py b/tests/test_basic.py index e425235..9b8ef55 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -14,7 +14,7 @@ def test_mymodel(): model.get_latent_representation() model.get_marginal_ll(n_mc_samples=5) model.get_reconstruction_error() - model.history + _ = model.history # tests __repr__ print(model) @@ -27,7 +27,7 @@ def test_mypyromodel(): model = MyPyroModel(adata) model.train(max_epochs=1, train_size=1) model.get_latent(adata) - model.history + _ = model.history # tests __repr__ print(model) From 22cc5fa5f9204f99d3466c79832309940e9ac3aa Mon Sep 17 00:00:00 2001 From: Martin Kim Date: Mon, 17 Jun 2024 12:57:59 -0700 Subject: [PATCH 2/5] update --- .editorconfig | 3 --- pyproject.toml | 1 - 2 files changed, 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index a8775f0..4d1d5b3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,8 +11,5 @@ insert_final_newline = true [*.{yml,yaml}] indent_size = 2 -[LICENSE] -insert_final_newline = false - [Makefile] indent_style = tab diff --git a/pyproject.toml b/pyproject.toml index 656b170..f6efc53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,6 @@ version = "0.0.1" description = "External and simple implementation of scVI" readme = "README.md" requires-python = ">=3.9" -license = {file = "LICENSE"} authors = [ {name = "The scvi-tools development team"}, ] From c85f98d3ab48a0bbd1d6a5b59f671e576da102ad Mon Sep 17 00:00:00 2001 From: Martin Kim Date: Mon, 17 Jun 2024 13:02:45 -0700 Subject: [PATCH 3/5] add license back --- .editorconfig | 3 +++ .github/workflows/test.yaml | 1 - LICENSE | 28 ++++++++++++++++++++++++++++ pyproject.toml | 1 + 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 LICENSE diff --git a/.editorconfig b/.editorconfig index 4d1d5b3..a8775f0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,5 +11,8 @@ insert_final_newline = true [*.{yml,yaml}] indent_size = 2 +[LICENSE] +insert_final_newline = false + [Makefile] indent_style = tab diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 63186ea..00e89e1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,7 +4,6 @@ on: push: branches: [main] pull_request: - branches: [main] schedule: - cron: "0 10 * * *" # runs at 10:00 UTC (03:00 PST) every day workflow_dispatch: diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1a8434a --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Adam Gayoso, Martin Kim + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pyproject.toml b/pyproject.toml index f6efc53..656b170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ version = "0.0.1" description = "External and simple implementation of scVI" readme = "README.md" requires-python = ">=3.9" +license = {file = "LICENSE"} authors = [ {name = "The scvi-tools development team"}, ] From 61dfc046b668e4575fda1518c63e178435790f8a Mon Sep 17 00:00:00 2001 From: Martin Kim Date: Mon, 17 Jun 2024 13:20:46 -0700 Subject: [PATCH 4/5] update typing and docs --- .github/workflows/test.yaml | 1 + src/simple_scvi/_mymodel.py | 24 ++++--- src/simple_scvi/_mymodule.py | 107 +++++++++++++++---------------- src/simple_scvi/_mypyromodel.py | 25 ++++---- src/simple_scvi/_mypyromodule.py | 30 ++++----- 5 files changed, 89 insertions(+), 98 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 00e89e1..63186ea 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,6 +4,7 @@ on: push: branches: [main] pull_request: + branches: [main] schedule: - cron: "0 10 * * *" # runs at 10:00 UTC (03:00 PST) every day workflow_dispatch: diff --git a/src/simple_scvi/_mymodel.py b/src/simple_scvi/_mymodel.py index 038f207..839a727 100644 --- a/src/simple_scvi/_mymodel.py +++ b/src/simple_scvi/_mymodel.py @@ -15,22 +15,21 @@ from scvi.model.base import BaseModelClass, UnsupervisedTrainingMixin, VAEMixin from scvi.utils import setup_anndata_dsp -from ._mymodule import MyModule +from simple_scvi._mymodule import MyModule logger = logging.getLogger(__name__) class MyModel(VAEMixin, UnsupervisedTrainingMixin, BaseModelClass): - """ - Skeleton for an scvi-tools model. + """Skeleton for an scvi-tools model. - Please use this skeleton to create new models. This is a simple - implementation of the scVI model :cite:p:`Lopez18`. + Please use this skeleton to create new models. This is a simple implementation of the scVI + model :cite:p:`Lopez18`. Parameters ---------- adata - AnnData object that has been registered via :meth:`~mypackage.MyModel.setup_anndata`. + AnnData object that has been registered via :meth:`~simple_scvi.MyModel.setup_anndata`. n_hidden Number of nodes per hidden layer. n_latent @@ -38,13 +37,13 @@ class MyModel(VAEMixin, UnsupervisedTrainingMixin, BaseModelClass): n_layers Number of hidden layers used for encoder and decoder NNs. **model_kwargs - Keyword args for :class:`~mypackage.MyModule` + Keyword args for :class:`~simple_scvi.MyModule` Examples -------- >>> adata = anndata.read_h5ad(path_to_anndata) - >>> mypackage.MyModel.setup_anndata(adata, batch_key="batch") - >>> vae = mypackage.MyModel(adata) + >>> simple_scvi.MyModel.setup_anndata(adata, batch_key="batch") + >>> vae = simple_scvi.MyModel(adata) >>> vae.train() >>> adata.obsm["X_mymodel"] = vae.get_latent_representation() """ @@ -56,7 +55,7 @@ def __init__( n_latent: int = 10, n_layers: int = 1, **model_kwargs, - ): + ) -> None: super().__init__(adata) library_log_means, library_log_vars = _init_library_size( @@ -93,9 +92,8 @@ def setup_anndata( categorical_covariate_keys: list[str] | None = None, continuous_covariate_keys: list[str] | None = None, **kwargs, - ) -> AnnData | None: - """ - %(summary)s. + ) -> None: + """%(summary)s. Parameters ---------- diff --git a/src/simple_scvi/_mymodule.py b/src/simple_scvi/_mymodule.py index 038a3c1..677833c 100644 --- a/src/simple_scvi/_mymodule.py +++ b/src/simple_scvi/_mymodule.py @@ -1,29 +1,29 @@ from __future__ import annotations import numpy as np +import numpy.typing as npt import torch import torch.nn.functional as F from scvi import REGISTRY_KEYS from scvi.distributions import ZeroInflatedNegativeBinomial +from scvi.module._constants import MODULE_KEYS from scvi.module.base import BaseModuleClass, LossOutput, auto_move_data from scvi.nn import DecoderSCVI, Encoder, one_hot +from torch import Tensor from torch.distributions import Normal from torch.distributions import kl_divergence as kl -TensorDict = dict[str, torch.Tensor] - class MyModule(BaseModuleClass): - """ - Skeleton Variational auto-encoder model. + """Skeleton variational auto-encoder (VAE) model. - Here we implement a basic version of scVI's underlying VAE :cite:p:`Lopez18`. - This implementation is for instructional purposes only. + Here we implement a basic version of scVI's underlying VAE :cite:p:`Lopez18`. This + implementation is for instructional purposes only. Parameters ---------- n_input - Number of input genes + Number of input genes. library_log_means 1 x n_batch array of means of the log library sizes. Parameterizes prior on library size if not using observed library size. @@ -33,20 +33,20 @@ class MyModule(BaseModuleClass): n_batch Number of batches, if 0, no batch correction is performed. n_hidden - Number of nodes per hidden layer + Number of nodes per hidden layer. n_latent - Dimensionality of the latent space + Dimensionality of the latent space. n_layers - Number of hidden layers used for encoder and decoder NNs + Number of hidden layers used for encoder and decoder NNs. dropout_rate - Dropout rate for neural networks + Dropout rate for neural networks. """ def __init__( self, n_input: int, - library_log_means: np.ndarray, - library_log_vars: np.ndarray, + library_log_means: npt.NDArray, + library_log_vars: npt.NDArray, n_batch: int = 0, n_hidden: int = 128, n_latent: int = 10, @@ -89,48 +89,44 @@ def __init__( n_hidden=n_hidden, ) - def _get_inference_input(self, tensors): - """Parse the dictionary to get appropriate args""" - x = tensors[REGISTRY_KEYS.X_KEY] - - input_dict = {"x": x} - return input_dict - - def _get_generative_input(self, tensors, inference_outputs): - z = inference_outputs["z"] - library = inference_outputs["library"] + def _get_inference_input(self, tensors: dict[str, Tensor]) -> dict[str, Tensor]: + """Parse the dictionary to get appropriate args.""" + return {MODULE_KEYS.X_KEY: tensors[REGISTRY_KEYS.X_KEY]} - input_dict = { - "z": z, - "library": library, + def _get_generative_input( + self, + tensors: dict[str, Tensor], + inference_outputs: dict[str, Tensor], + ) -> dict[str, Tensor]: + return { + MODULE_KEYS.Z_KEY: inference_outputs["z"], + MODULE_KEYS.LIBRARY_KEY: inference_outputs["library"], } - return input_dict @auto_move_data - def inference(self, x): + def inference(self, x: Tensor) -> dict[str, Tensor]: """ High level inference method. Runs the inference (encoder) model. """ # log the input to the variational distribution for numerical stability - x_ = torch.log(1 + x) + x_ = torch.log1p(x) # get variational parameters via the encoder networks qz_m, qz_v, z = self.z_encoder(x_) ql_m, ql_v, library = self.l_encoder(x_) - outputs = { - "z": z, - "qz_m": qz_m, - "qz_v": qz_v, + return { + MODULE_KEYS.Z_KEY: z, + MODULE_KEYS.QZM_KEY: qz_m, + MODULE_KEYS.QZV_KEY: qz_v, "ql_m": ql_m, "ql_v": ql_v, - "library": library, + MODULE_KEYS.LIBRARY_KEY: library, } - return outputs @auto_move_data - def generative(self, z, library): + def generative(self, z: Tensor, library: Tensor) -> dict[str, Tensor]: """Runs the generative model.""" # form the parameters of the ZINB likelihood px_scale, _, px_rate, px_dropout = self.decoder("gene", z, library) @@ -145,15 +141,16 @@ def generative(self, z, library): def loss( self, - tensors, - inference_outputs, - generative_outputs, + tensors: dict[str, Tensor], + inference_outputs: dict[str, Tensor], + generative_outputs: dict[str, Tensor], kl_weight: float = 1.0, - ): + ) -> LossOutput: """Loss function.""" x = tensors[REGISTRY_KEYS.X_KEY] - qz_m = inference_outputs["qz_m"] - qz_v = inference_outputs["qz_v"] + batch_index = tensors[REGISTRY_KEYS.BATCH_KEY] + qz_m = inference_outputs[MODULE_KEYS.QZM_KEY] + qz_v = inference_outputs[MODULE_KEYS.QZV_KEY] ql_m = inference_outputs["ql_m"] ql_v = inference_outputs["ql_v"] px_rate = generative_outputs["px_rate"] @@ -165,7 +162,6 @@ def loss( kl_divergence_z = kl(Normal(qz_m, torch.sqrt(qz_v)), Normal(mean, scale)).sum(dim=1) - batch_index = tensors[REGISTRY_KEYS.BATCH_KEY] n_batch = self.library_log_means.shape[1] local_library_log_means = F.linear(one_hot(batch_index, n_batch), self.library_log_means) local_library_log_vars = F.linear(one_hot(batch_index, n_batch), self.library_log_vars) @@ -189,20 +185,19 @@ def loss( loss = torch.mean(reconst_loss + weighted_kl_local) kl_local = { - "kl_divergence_l": kl_divergence_l, - "kl_divergence_z": kl_divergence_z, + MODULE_KEYS.KL_L_KEY: kl_divergence_l, + MODULE_KEYS.KL_Z_KEY: kl_divergence_z, } return LossOutput(loss=loss, reconstruction_loss=reconst_loss, kl_local=kl_local) @torch.no_grad() def sample( self, - tensors, - n_samples=1, - library_size=1, - ) -> torch.Tensor: - r""" - Generate observation samples from the posterior predictive distribution. + tensors: dict[str, Tensor], + n_samples: int = 1, + library_size: int = 1, + ) -> Tensor: + r"""Generate observation samples from the posterior predictive distribution. The posterior predictive distribution is written as :math:`p(\hat{x} \mid x)`. @@ -245,7 +240,7 @@ def sample( @torch.no_grad() @auto_move_data - def marginal_ll(self, tensors: TensorDict, n_mc_samples: int): + def marginal_ll(self, tensors: dict[str, Tensor], n_mc_samples: int) -> float: """Marginal ll.""" sample_batch = tensors[REGISTRY_KEYS.X_KEY] batch_index = tensors[REGISTRY_KEYS.BATCH_KEY] @@ -255,12 +250,12 @@ def marginal_ll(self, tensors: TensorDict, n_mc_samples: int): for i in range(n_mc_samples): # Distribution parameters and sampled variables inference_outputs, _, losses = self.forward(tensors) - qz_m = inference_outputs["qz_m"] - qz_v = inference_outputs["qz_v"] - z = inference_outputs["z"] + qz_m = inference_outputs[MODULE_KEYS.QZM_KEY] + qz_v = inference_outputs[MODULE_KEYS.QZV_KEY] + z = inference_outputs[MODULE_KEYS.Z_KEY] ql_m = inference_outputs["ql_m"] ql_v = inference_outputs["ql_v"] - library = inference_outputs["library"] + library = inference_outputs[MODULE_KEYS.LIBRARY_KEY] # Reconstruction Loss reconst_loss = losses.dict_sum(losses.reconstruction_loss) diff --git a/src/simple_scvi/_mypyromodel.py b/src/simple_scvi/_mypyromodel.py index a13700e..f271969 100644 --- a/src/simple_scvi/_mypyromodel.py +++ b/src/simple_scvi/_mypyromodel.py @@ -4,6 +4,7 @@ from collections.abc import Sequence import numpy as np +import numpy.typing as npt import torch from anndata import AnnData from scvi import REGISTRY_KEYS @@ -26,15 +27,14 @@ class MyPyroModel(BaseModelClass): - """ - Skeleton for a pyro version of a scvi-tools model. + """Skeleton for a Pyro version of a scvi-tools model. Please use this skeleton to create new models. Parameters ---------- adata - AnnData object that has been registered via :meth:`~mypackage.MyPyroModel.setup_anndata`. + AnnData object that has been registered via :meth:`~simple_scvi.MyPyroModel.setup_anndata`. n_hidden Number of nodes per hidden layer. n_latent @@ -42,7 +42,7 @@ class MyPyroModel(BaseModelClass): n_layers Number of hidden layers used for encoder and decoder NNs. **model_kwargs - Keyword args for :class:`~mypackage.MyModule` + Keyword args for :class:`~simple_scvi.MyModule` Examples -------- @@ -60,7 +60,7 @@ def __init__( n_latent: int = 10, n_layers: int = 1, **model_kwargs, - ): + ) -> None: super().__init__(adata) # self.summary_stats provides information about anndata dimensions and other tensor info @@ -85,9 +85,8 @@ def get_latent( adata: AnnData | None = None, indices: Sequence[int] | None = None, batch_size: int | None = None, - ): - """ - Return the latent representation for each cell. + ) -> npt.NDArray: + """Return the latent representation for each cell. This is denoted as :math:`z_n` in our manuscripts. @@ -125,9 +124,8 @@ def train( batch_size: int = 128, plan_kwargs: dict | None = None, **trainer_kwargs, - ): - """ - Train the model. + ) -> None: + """Train the model. Parameters ---------- @@ -184,9 +182,8 @@ def setup_anndata( categorical_covariate_keys: list[str] | None = None, continuous_covariate_keys: list[str] | None = None, **kwargs, - ) -> AnnData | None: - """ - %(summary)s. + ) -> None: + """%(summary)s. Parameters ---------- diff --git a/src/simple_scvi/_mypyromodule.py b/src/simple_scvi/_mypyromodule.py index 124ec11..f8e0526 100644 --- a/src/simple_scvi/_mypyromodule.py +++ b/src/simple_scvi/_mypyromodule.py @@ -7,30 +7,28 @@ from scvi import REGISTRY_KEYS from scvi.module.base import PyroBaseModuleClass, auto_move_data from scvi.nn import DecoderSCVI, Encoder - -TensorDict = dict[str, torch.Tensor] +from torch import Tensor class MyPyroModule(PyroBaseModuleClass): - """ - Skeleton Variational auto-encoder Pyro model. + """Skeleton variational auto-encoder (VAE) Pyro model. - Here we implement a basic version of scVI's underlying VAE :cite:p:`Lopez18`. - This implementation is for instructional purposes only. + Here we implement a basic version of scVI's underlying VAE :cite:p:`Lopez18`. This + implementation is for instructional purposes only. Parameters ---------- n_input - Number of input genes + Number of input genes. n_latent - Dimensionality of the latent space + Dimensionality of the latent space. n_hidden - Number of nodes per hidden layer + Number of nodes per hidden layer. n_layers - Number of hidden layers used for encoder and decoder NNs + Number of hidden layers used for encoder and decoder NNs. """ - def __init__(self, n_input: int, n_latent: int, n_hidden: int, n_layers: int): + def __init__(self, n_input: int, n_latent: int, n_hidden: int, n_layers: int) -> None: super().__init__() self.n_input = n_input self.n_latent = n_latent @@ -55,12 +53,14 @@ def __init__(self, n_input: int, n_latent: int, n_hidden: int, n_layers: int): self.px_r = torch.nn.Parameter(torch.ones(self.n_input)) @staticmethod - def _get_fn_args_from_batch(tensor_dict: TensorDict): + def _get_fn_args_from_batch( + tensor_dict: dict[str, Tensor], + ) -> tuple[tuple[Tensor, Tensor], dict]: x = tensor_dict[REGISTRY_KEYS.X_KEY] log_library = torch.log(torch.sum(x, dim=1, keepdim=True) + 1e-6) return (x, log_library), {} - def model(self, x: torch.Tensor, log_library: torch.Tensor, kl_weight: float = 1.0): + def model(self, x: torch.Tensor, log_library: torch.Tensor, kl_weight: float = 1.0) -> None: """Pyro model.""" # register PyTorch module `decoder` with Pyro pyro.module("scvi", self) @@ -81,7 +81,7 @@ def model(self, x: torch.Tensor, log_library: torch.Tensor, kl_weight: float = 1 # score against actual counts pyro.sample("obs", x_dist.to_event(1), obs=x) - def guide(self, x: torch.Tensor, log_library: torch.Tensor, kl_weight: float = 1.0): + def guide(self, x: torch.Tensor, log_library: torch.Tensor, kl_weight: float = 1.0) -> None: """Pyro guide.""" # define the guide (i.e. variational distribution) q(z|x) pyro.module("scvi", self) @@ -94,7 +94,7 @@ def guide(self, x: torch.Tensor, log_library: torch.Tensor, kl_weight: float = 1 @torch.no_grad() @auto_move_data - def get_latent(self, tensor_dict: TensorDict): + def get_latent(self, tensor_dict: dict[str, Tensor]) -> Tensor: """Get the latent representation of the data.""" x = tensor_dict[REGISTRY_KEYS.X_KEY] x_ = torch.log(1 + x) From 270c66cb3210b1c7c9a8f6837c7d43b3486e9325 Mon Sep 17 00:00:00 2001 From: Martin Kim Date: Mon, 17 Jun 2024 13:33:30 -0700 Subject: [PATCH 5/5] fix refs --- src/simple_scvi/_mymodel.py | 4 ++-- src/simple_scvi/_mypyromodel.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/simple_scvi/_mymodel.py b/src/simple_scvi/_mymodel.py index 839a727..c1cd1ff 100644 --- a/src/simple_scvi/_mymodel.py +++ b/src/simple_scvi/_mymodel.py @@ -29,7 +29,7 @@ class MyModel(VAEMixin, UnsupervisedTrainingMixin, BaseModelClass): Parameters ---------- adata - AnnData object that has been registered via :meth:`~simple_scvi.MyModel.setup_anndata`. + AnnData object that has been registered via :meth:`~MyModel.setup_anndata`. n_hidden Number of nodes per hidden layer. n_latent @@ -37,7 +37,7 @@ class MyModel(VAEMixin, UnsupervisedTrainingMixin, BaseModelClass): n_layers Number of hidden layers used for encoder and decoder NNs. **model_kwargs - Keyword args for :class:`~simple_scvi.MyModule` + Keyword args for :class:`~MyModule` Examples -------- diff --git a/src/simple_scvi/_mypyromodel.py b/src/simple_scvi/_mypyromodel.py index f271969..98e8037 100644 --- a/src/simple_scvi/_mypyromodel.py +++ b/src/simple_scvi/_mypyromodel.py @@ -34,7 +34,7 @@ class MyPyroModel(BaseModelClass): Parameters ---------- adata - AnnData object that has been registered via :meth:`~simple_scvi.MyPyroModel.setup_anndata`. + AnnData object that has been registered via :meth:`~MyPyroModel.setup_anndata`. n_hidden Number of nodes per hidden layer. n_latent @@ -42,7 +42,7 @@ class MyPyroModel(BaseModelClass): n_layers Number of hidden layers used for encoder and decoder NNs. **model_kwargs - Keyword args for :class:`~simple_scvi.MyModule` + Keyword args for :class:`~MyModule`. Examples --------