diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b290e09..e2a410d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json", "name": "nfcore", "image": "nfcore/gitpod:latest", "remoteUser": "gitpod", diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 3a5f6ae..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,125 +0,0 @@ -# `nf-core/dmscore`: Contributing Guidelines - -Hi there! -Many thanks for taking an interest in improving nf-core/dmscore. - -We try to manage the required tasks for nf-core/dmscore using GitHub issues, you probably came to this page when creating one. -Please use the pre-filled template to save time. - -However, don't be put off by this template - other more general issues and suggestions are welcome! -Contributions to the code are even more welcome ;) - -> [!NOTE] -> If you need help using or modifying nf-core/dmscore then the best place to ask is on the nf-core Slack [#dmscore](https://nfcore.slack.com/channels/dmscore) channel ([join our Slack here](https://nf-co.re/join/slack)). - -## Contribution workflow - -If you'd like to write some code for nf-core/dmscore, the standard workflow is as follows: - -1. Check that there isn't already an issue about your idea in the [nf-core/dmscore issues](https://github.com/nf-core/dmscore/issues) to avoid duplicating work. If there isn't one already, please create one so that others know you're working on this -2. [Fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) the [nf-core/dmscore repository](https://github.com/nf-core/dmscore) to your GitHub account -3. Make the necessary changes / additions within your forked repository following [Pipeline conventions](#pipeline-contribution-conventions) -4. Use `nf-core pipelines schema build` and add any new parameters to the pipeline JSON schema (requires [nf-core tools](https://github.com/nf-core/tools) >= 1.10). -5. Submit a Pull Request against the `dev` branch and wait for the code to be reviewed and merged - -If you're not used to this workflow with git, you can start with some [docs from GitHub](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests) or even their [excellent `git` resources](https://try.github.io/). - -## Tests - -You have the option to test your changes locally by running the pipeline. For receiving warnings about process selectors and other `debug` information, it is recommended to use the debug profile. Execute all the tests with the following command: - -```bash -nf-test test --profile debug,test,docker --verbose -``` - -When you create a pull request with changes, [GitHub Actions](https://github.com/features/actions) will run automatic tests. -Typically, pull-requests are only fully reviewed when these tests are passing, though of course we can help out before then. - -There are typically two types of tests that run: - -### Lint tests - -`nf-core` has a [set of guidelines](https://nf-co.re/developers/guidelines) which all pipelines must adhere to. -To enforce these and ensure that all pipelines stay in sync, we have developed a helper tool which runs checks on the pipeline code. This is in the [nf-core/tools repository](https://github.com/nf-core/tools) and once installed can be run locally with the `nf-core pipelines lint ` command. - -If any failures or warnings are encountered, please follow the listed URL for more documentation. - -### Pipeline tests - -Each `nf-core` pipeline should be set up with a minimal set of test-data. -`GitHub Actions` then runs the pipeline on this data to ensure that it exits successfully. -If there are any failures then the automated tests fail. -These tests are run both with the latest available version of `Nextflow` and also the minimum required version that is stated in the pipeline code. - -## Patch - -:warning: Only in the unlikely and regretful event of a release happening with a bug. - -- On your own fork, make a new branch `patch` based on `upstream/main` or `upstream/master`. -- Fix the bug, and bump version (X.Y.Z+1). -- Open a pull-request from `patch` to `main`/`master` with the changes. - -## Getting help - -For further information/help, please consult the [nf-core/dmscore documentation](https://nf-co.re/dmscore/usage) and don't hesitate to get in touch on the nf-core Slack [#dmscore](https://nfcore.slack.com/channels/dmscore) channel ([join our Slack here](https://nf-co.re/join/slack)). - -## Pipeline contribution conventions - -To make the `nf-core/dmscore` code and processing logic more understandable for new contributors and to ensure quality, we semi-standardise the way the code and other contributions are written. - -### Adding a new step - -If you wish to contribute a new step, please use the following coding standards: - -1. Define the corresponding input channel into your new process from the expected previous process channel. -2. Write the process block (see below). -3. Define the output channel if needed (see below). -4. Add any new parameters to `nextflow.config` with a default (see below). -5. Add any new parameters to `nextflow_schema.json` with help text (via the `nf-core pipelines schema build` tool). -6. Add sanity checks and validation for all relevant parameters. -7. Perform local tests to validate that the new code works as expected. -8. If applicable, add a new test command in `.github/workflow/ci.yml`. -9. Update MultiQC config `assets/multiqc_config.yml` so relevant suffixes, file name clean up and module plots are in the appropriate order. If applicable, add a [MultiQC](https://https://multiqc.info/) module. -10. Add a description of the output files and if relevant any appropriate images from the MultiQC report to `docs/output.md`. - -### Default values - -Parameters should be initialised / defined with default values within the `params` scope in `nextflow.config`. - -Once there, use `nf-core pipelines schema build` to add to `nextflow_schema.json`. - -### Default processes resource requirements - -Sensible defaults for process resource requirements (CPUs / memory / time) for a process should be defined in `conf/base.config`. These should generally be specified generic with `withLabel:` selectors so they can be shared across multiple processes/steps of the pipeline. A nf-core standard set of labels that should be followed where possible can be seen in the [nf-core pipeline template](https://github.com/nf-core/tools/blob/main/nf_core/pipeline-template/conf/base.config), which has the default process as a single core-process, and then different levels of multi-core configurations for increasingly large memory requirements defined with standardised labels. - -The process resources can be passed on to the tool dynamically within the process with the `${task.cpus}` and `${task.memory}` variables in the `script:` block. - -### Naming schemes - -Please use the following naming schemes, to make it easy to understand what is going where. - -- initial process channel: `ch_output_from_` -- intermediate and terminal channels: `ch__for_` - -### Nextflow version bumping - -If you are using a new feature from core Nextflow, you may bump the minimum required version of nextflow in the pipeline with: `nf-core pipelines bump-version --nextflow . [min-nf-version]` - -### Images and figures - -For overview images and other documents we follow the nf-core [style guidelines and examples](https://nf-co.re/developers/design_guidelines). - -## GitHub Codespaces - -This repo includes a devcontainer configuration which will create a GitHub Codespaces for Nextflow development! This is an online developer environment that runs in your browser, complete with VSCode and a terminal. - -To get started: - -- Open the repo in [Codespaces](https://github.com/nf-core/dmscore/codespaces) -- Tools installed - - nf-core - - Nextflow - -Devcontainer specs: - -- [DevContainer config](.devcontainer/devcontainer.json) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 58a5523..78f6876 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,7 +8,7 @@ body: Before you post this issue, please check the documentation: - [nf-core website: troubleshooting](https://nf-co.re/usage/troubleshooting) - - [nf-core/dmscore pipeline documentation](https://nf-co.re/dmscore/usage) + - [nf-core/deepmutscan pipeline documentation](https://nf-co.re/deepmutscan/usage) - type: textarea id: description attributes: @@ -46,4 +46,4 @@ body: * Executor _(eg. slurm, local, awsbatch)_ * Container engine: _(e.g. Docker, Singularity, Conda, Podman, Shifter, Charliecloud, or Apptainer)_ * OS _(eg. CentOS Linux, macOS, Linux Mint)_ - * Version of nf-core/dmscore _(eg. 1.1, 1.5, 1.8.2)_ + * Version of nf-core/deepmutscan _(eg. 1.1, 1.5, 1.8.2)_ diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 08f0c49..572e0cf 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,6 +2,6 @@ contact_links: - name: Join nf-core url: https://nf-co.re/join about: Please join the nf-core community here - - name: "Slack #dmscore channel" - url: https://nfcore.slack.com/channels/dmscore - about: Discussion about the nf-core/dmscore pipeline + - name: "Slack #deepmutscan channel" + url: https://nfcore.slack.com/channels/deepmutscan + about: Discussion about the nf-core/deepmutscan pipeline diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index a08dd5f..0731019 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,5 +1,5 @@ name: Feature request -description: Suggest an idea for the nf-core/dmscore pipeline +description: Suggest an idea for the nf-core/deepmutscan pipeline labels: enhancement body: - type: textarea diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0d6f133..1e78c19 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,22 +1,22 @@ ## PR checklist - [ ] This comment contains a description of changes (with reason). - [ ] If you've fixed a bug or added code that should be tested, add tests! -- [ ] If you've added a new tool - have you followed the pipeline conventions in the [contribution docs](https://github.com/nf-core/dmscore/tree/master/.github/CONTRIBUTING.md) -- [ ] If necessary, also make a PR on the nf-core/dmscore _branch_ on the [nf-core/test-datasets](https://github.com/nf-core/test-datasets) repository. +- [ ] If you've added a new tool - have you followed the pipeline conventions in the [contribution docs](https://github.com/nf-core/deepmutscan/tree/master/docs/CONTRIBUTING.md) +- [ ] If necessary, also make a PR on the nf-core/deepmutscan _branch_ on the [nf-core/test-datasets](https://github.com/nf-core/test-datasets) repository. - [ ] Make sure your code lints (`nf-core pipelines lint`). - [ ] Ensure the test suite passes (`nextflow run . -profile test,docker --outdir `). - [ ] Check for unexpected warnings in debug mode (`nextflow run . -profile debug,test,docker --outdir `). diff --git a/.github/actions/get-shards/action.yml b/.github/actions/get-shards/action.yml new file mode 100644 index 0000000..e2833ee --- /dev/null +++ b/.github/actions/get-shards/action.yml @@ -0,0 +1,69 @@ +name: "Get number of shards" +description: "Get the number of nf-test shards for the current CI job" +inputs: + max_shards: + description: "Maximum number of shards allowed" + required: true + paths: + description: "Component paths to test" + required: false + tags: + description: "Tags to pass as argument for nf-test --tag parameter" + required: false +outputs: + shard: + description: "Array of shard numbers" + value: ${{ steps.shards.outputs.shard }} + total_shards: + description: "Total number of shards" + value: ${{ steps.shards.outputs.total_shards }} +runs: + using: "composite" + steps: + - name: Install nf-test + uses: nf-core/setup-nf-test@4069fbbaabe94c08faba4ad261bfa88225ba133f # v2 + with: + version: ${{ env.NFT_VER }} + - name: Get number of shards + id: shards + shell: bash + run: | + # Run nf-test with dynamic parameter + nftest_output=$(nf-test test \ + --profile +docker \ + $(if [ -n "${{ inputs.tags }}" ]; then echo "--tag ${{ inputs.tags }}"; fi) \ + --dry-run \ + --ci \ + --changed-since HEAD^) || { + echo "nf-test command failed with exit code $?" + echo "Full output: $nftest_output" + exit 1 + } + echo "nf-test dry-run output: $nftest_output" + + # Default values for shard and total_shards + shard="[]" + total_shards=0 + + # Check if there are related tests + if echo "$nftest_output" | grep -q 'No tests to execute'; then + echo "No related tests found." + else + # Extract the number of related tests + number_of_shards=$(echo "$nftest_output" | sed -n 's|.*Executed \([0-9]*\) tests.*|\1|p') + if [[ -n "$number_of_shards" && "$number_of_shards" -gt 0 ]]; then + shards_to_run=$(( $number_of_shards < ${{ inputs.max_shards }} ? $number_of_shards : ${{ inputs.max_shards }} )) + shard=$(seq 1 "$shards_to_run" | jq -R . | jq -c -s .) + total_shards="$shards_to_run" + else + echo "Unexpected output format. Falling back to default values." + fi + fi + + # Write to GitHub Actions outputs + echo "shard=$shard" >> $GITHUB_OUTPUT + echo "total_shards=$total_shards" >> $GITHUB_OUTPUT + + # Debugging output + echo "Final shard array: $shard" + echo "Total number of shards: $total_shards" diff --git a/.github/actions/nf-test/action.yml b/.github/actions/nf-test/action.yml new file mode 100644 index 0000000..5e66435 --- /dev/null +++ b/.github/actions/nf-test/action.yml @@ -0,0 +1,121 @@ +name: "nf-test Action" +description: "Runs nf-test with common setup steps" +inputs: + profile: + description: "Profile to use" + required: true + shard: + description: "Shard number for this CI job" + required: true + total_shards: + description: "Total number of test shards(NOT the total number of matrix jobs)" + required: true + paths: + description: "Test paths" + required: true + tags: + description: "Tags to pass as argument for nf-test --tag parameter" + required: false +runs: + using: "composite" + steps: + # The pipeline pulls ~17 container images, three of them heavy bioconductor/R stacks, which + # overruns the runner disk part-way through (docker exit 125, "no space left on device"). + # Reclaim the preinstalled toolchain first: the RunsOn default image is ubuntu24-full-x64, so the + # same Android/.NET/Haskell bloat as a GitHub-hosted runner is present and removable. + - name: Free disk space + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + with: + tool-cache: false # later steps need the toolchain + large-packages: false # slowest sweep, smallest reliable gain + + - name: Setup Nextflow + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 + with: + version: "${{ env.NXF_VERSION }}" + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + with: + python-version: "3.14" + + - name: Install nf-test + uses: nf-core/setup-nf-test@4069fbbaabe94c08faba4ad261bfa88225ba133f # v2 + with: + version: "${{ env.NFT_VER }}" + install-pdiff: true + + - name: Setup apptainer + if: contains(inputs.profile, 'singularity') + uses: eWaterCycle/setup-apptainer@3f706d898c9db585b1d741b4692e66755f3a1b40 # v2 + + - name: Set up Singularity + if: contains(inputs.profile, 'singularity') + shell: bash + run: | + mkdir -p $NXF_SINGULARITY_CACHEDIR + mkdir -p $NXF_SINGULARITY_LIBRARYDIR + + - name: Conda setup + if: contains(inputs.profile, 'conda') + uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4 + with: + auto-update-conda: true + conda-solver: libmamba + channels: conda-forge + channel-priority: strict + conda-remove-defaults: true + + - name: Run nf-test + shell: bash + env: + NFT_WORKDIR: ${{ env.NFT_WORKDIR }} + run: | + nf-test test \ + --profile=+${{ inputs.profile }} \ + $(if [ -n "${{ inputs.tags }}" ]; then echo "--tag ${{ inputs.tags }}"; fi) \ + --ci \ + --changed-since HEAD^ \ + --verbose \ + --tap=test.tap \ + --shard ${{ inputs.shard }}/${{ inputs.total_shards }} + + # Save the absolute path of the test.tap file to the output + echo "tap_file_path=$(realpath test.tap)" >> $GITHUB_OUTPUT + + - name: Generate test summary + if: always() + shell: bash + run: | + # Add header if it doesn't exist (using a token file to track this) + if [ ! -f ".summary_header" ]; then + echo "# πŸš€ nf-test results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Status | Test Name | Profile | Shard |" >> $GITHUB_STEP_SUMMARY + echo "|:------:|-----------|---------|-------|" >> $GITHUB_STEP_SUMMARY + touch .summary_header + fi + + if [ -f test.tap ]; then + while IFS= read -r line; do + if [[ $line =~ ^ok ]]; then + test_name="${line#ok }" + # Remove the test number from the beginning + test_name="${test_name#* }" + echo "| βœ… | ${test_name} | ${{ inputs.profile }} | ${{ inputs.shard }}/${{ inputs.total_shards }} |" >> $GITHUB_STEP_SUMMARY + elif [[ $line =~ ^not\ ok ]]; then + test_name="${line#not ok }" + # Remove the test number from the beginning + test_name="${test_name#* }" + echo "| ❌ | ${test_name} | ${{ inputs.profile }} | ${{ inputs.shard }}/${{ inputs.total_shards }} |" >> $GITHUB_STEP_SUMMARY + fi + done < test.tap + else + echo "| ⚠️ | No test results found | ${{ inputs.profile }} | ${{ inputs.shard }}/${{ inputs.total_shards }} |" >> $GITHUB_STEP_SUMMARY + fi + + - name: Clean up + if: always() + shell: bash + run: | + sudo rm -rf /home/ubuntu/tests/ diff --git a/.github/workflows/awsfulltest.yml b/.github/workflows/awsfulltest.yml index 50287a8..4d4cddd 100644 --- a/.github/workflows/awsfulltest.yml +++ b/.github/workflows/awsfulltest.yml @@ -4,66 +4,61 @@ name: nf-core AWS full size tests # It runs the -profile 'test_full' on AWS batch on: - pull_request: - branches: - - main - - master workflow_dispatch: pull_request_review: types: [submitted] + release: + types: [published] jobs: run-platform: name: Run AWS full tests - # run only if the PR is approved by at least 2 reviewers and against the master branch or manually triggered - if: github.repository == 'nf-core/dmscore' && github.event.review.state == 'approved' && github.event.pull_request.base.ref == 'master' || github.event_name == 'workflow_dispatch' + # run only if the PR is approved by at least 2 reviewers and against the master/main branch or manually triggered + if: github.repository == 'nf-core/deepmutscan' && github.event.review.state == 'approved' && (github.event.pull_request.base.ref == 'master' || github.event.pull_request.base.ref == 'main') || github.event_name == 'workflow_dispatch' || github.event_name == 'release' runs-on: ubuntu-latest steps: - - name: Get PR reviews - uses: octokit/request-action@v2.x - if: github.event_name != 'workflow_dispatch' - id: check_approvals - continue-on-error: true - with: - route: GET /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews?per_page=100 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Check for approvals - if: ${{ failure() && github.event_name != 'workflow_dispatch' }} - run: | - echo "No review approvals found. At least 2 approvals are required to run this action automatically." - exit 1 - - - name: Check for enough approvals (>=2) - id: test_variables - if: github.event_name != 'workflow_dispatch' + - name: Set revision variable + id: revision run: | - JSON_RESPONSE='${{ steps.check_approvals.outputs.data }}' - CURRENT_APPROVALS_COUNT=$(echo $JSON_RESPONSE | jq -c '[.[] | select(.state | contains("APPROVED")) ] | length') - test $CURRENT_APPROVALS_COUNT -ge 2 || exit 1 # At least 2 approvals are required + echo "revision=${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'release') && github.sha || 'dev' }}" >> "$GITHUB_OUTPUT" - name: Launch workflow via Seqera Platform - uses: seqeralabs/action-tower-launch@v2 - # TODO nf-core: You can customise AWS full pipeline tests as required - # Add full size test data (but still relatively small datasets for few samples) - # on the `test_full.config` test runs with only one set of parameters + uses: seqeralabs/action-tower-launch@51565b514bff1827cf34620de25d0055759f1fc9 # v2 with: - workspace_id: ${{ secrets.TOWER_WORKSPACE_ID }} + workspace_id: ${{ vars.TOWER_WORKSPACE_ID }} access_token: ${{ secrets.TOWER_ACCESS_TOKEN }} - compute_env: ${{ secrets.TOWER_COMPUTE_ENV }} - revision: ${{ github.sha }} - workdir: s3://${{ secrets.AWS_S3_BUCKET }}/work/dmscore/work-${{ github.sha }} + compute_env: ${{ vars.TOWER_COMPUTE_ENV }} + revision: ${{ steps.revision.outputs.revision }} + workdir: s3://${{ vars.AWS_S3_BUCKET }}/work/deepmutscan/work-${{ steps.revision.outputs.revision }} + nextflow_config: | + plugins { + id 'nf-slack@0.5.0' + } + slack { + enabled = true + bot { + token = '${{ secrets.NFSLACK_BOT_TOKEN }}' + channel = 'deepmutscan' + } + onStart { + enabled = false + } + onComplete { + message = ':white_check_mark: *deepmutscan/test_full* completed successfully! :tada:' + } + onError { + message = ':x: *deepmutscan/test_full* failed :crying_cat_face:' + } + } parameters: | { - "hook_url": "${{ secrets.MEGATESTS_ALERTS_SLACK_HOOK_URL }}", - "outdir": "s3://${{ secrets.AWS_S3_BUCKET }}/dmscore/results-${{ github.sha }}" + "outdir": "s3://${{ vars.AWS_S3_BUCKET }}/deepmutscan/results-${{ steps.revision.outputs.revision }}" } profiles: test_full - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: Seqera Platform debug log file path: | - seqera_platform_action_*.log - seqera_platform_action_*.json + tower_action_*.log + tower_action_*.json diff --git a/.github/workflows/awstest.yml b/.github/workflows/awstest.yml index 4a4b5db..2471331 100644 --- a/.github/workflows/awstest.yml +++ b/.github/workflows/awstest.yml @@ -7,27 +7,27 @@ on: jobs: run-platform: name: Run AWS tests - if: github.repository == 'nf-core/dmscore' + if: github.repository == 'nf-core/deepmutscan' runs-on: ubuntu-latest steps: # Launch workflow using Seqera Platform CLI tool action - name: Launch workflow via Seqera Platform - uses: seqeralabs/action-tower-launch@v2 + uses: seqeralabs/action-tower-launch@51565b514bff1827cf34620de25d0055759f1fc9 # v2 with: - workspace_id: ${{ secrets.TOWER_WORKSPACE_ID }} + workspace_id: ${{ vars.TOWER_WORKSPACE_ID }} access_token: ${{ secrets.TOWER_ACCESS_TOKEN }} - compute_env: ${{ secrets.TOWER_COMPUTE_ENV }} + compute_env: ${{ vars.TOWER_COMPUTE_ENV }} revision: ${{ github.sha }} - workdir: s3://${{ secrets.AWS_S3_BUCKET }}/work/dmscore/work-${{ github.sha }} + workdir: s3://${{ vars.AWS_S3_BUCKET }}/work/deepmutscan/work-${{ github.sha }} parameters: | { - "outdir": "s3://${{ secrets.AWS_S3_BUCKET }}/dmscore/results-test-${{ github.sha }}" + "outdir": "s3://${{ vars.AWS_S3_BUCKET }}/deepmutscan/results-test-${{ github.sha }}" } profiles: test - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: Seqera Platform debug log file path: | - seqera_platform_action_*.log - seqera_platform_action_*.json + tower_action_*.log + tower_action_*.json diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 92e3db0..ef39819 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -13,15 +13,15 @@ jobs: steps: # PRs to the nf-core repo main/master branch are only ok if coming from the nf-core repo `dev` or any `patch` branches - name: Check PRs - if: github.repository == 'nf-core/dmscore' + if: github.repository == 'nf-core/deepmutscan' run: | - { [[ ${{github.event.pull_request.head.repo.full_name }} == nf-core/dmscore ]] && [[ $GITHUB_HEAD_REF == "dev" ]]; } || [[ $GITHUB_HEAD_REF == "patch" ]] + { [[ ${{github.event.pull_request.head.repo.full_name }} == nf-core/deepmutscan ]] && [[ $GITHUB_HEAD_REF == "dev" ]]; } || [[ $GITHUB_HEAD_REF == "patch" ]] # If the above check failed, post a comment on the PR explaining the failure # NOTE - this doesn't currently work if the PR is coming from a fork, due to limitations in GitHub actions secrets - name: Post PR comment if: failure() - uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2 + uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3 with: message: | ## This PR is against the `${{github.event.pull_request.base.ref}}` branch :x: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 92396ce..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: nf-core CI -# This workflow runs the pipeline with the minimal test dataset to check that it completes without any syntax errors -on: - push: - branches: - - dev - pull_request: - release: - types: [published] - workflow_dispatch: - -env: - NXF_ANSI_LOG: false - NXF_SINGULARITY_CACHEDIR: ${{ github.workspace }}/.singularity - NXF_SINGULARITY_LIBRARYDIR: ${{ github.workspace }}/.singularity - -concurrency: - group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" - cancel-in-progress: true - -jobs: - test: - name: "Run pipeline with test data (${{ matrix.NXF_VER }} | ${{ matrix.test_name }} | ${{ matrix.profile }})" - # Only run on push if this is the nf-core dev branch (merged PRs) - if: "${{ github.event_name != 'push' || (github.event_name == 'push' && github.repository == 'nf-core/dmscore') }}" - runs-on: ubuntu-latest - strategy: - matrix: - NXF_VER: - - "24.04.2" - - "latest-everything" - profile: - - "conda" - - "docker" - - "singularity" - test_name: - - "test" - isMaster: - - ${{ github.base_ref == 'master' }} - # Exclude conda and singularity on dev - exclude: - - isMaster: false - profile: "conda" - - isMaster: false - profile: "singularity" - steps: - - name: Check out pipeline code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - - name: Set up Nextflow - uses: nf-core/setup-nextflow@v2 - with: - version: "${{ matrix.NXF_VER }}" - - - name: Set up Apptainer - if: matrix.profile == 'singularity' - uses: eWaterCycle/setup-apptainer@main - - - name: Set up Singularity - if: matrix.profile == 'singularity' - run: | - mkdir -p $NXF_SINGULARITY_CACHEDIR - mkdir -p $NXF_SINGULARITY_LIBRARYDIR - - - name: Set up Miniconda - if: matrix.profile == 'conda' - uses: conda-incubator/setup-miniconda@a4260408e20b96e80095f42ff7f1a15b27dd94ca # v3 - with: - miniconda-version: "latest" - auto-update-conda: true - conda-solver: libmamba - channels: conda-forge,bioconda - - - name: Set up Conda - if: matrix.profile == 'conda' - run: | - echo $(realpath $CONDA)/condabin >> $GITHUB_PATH - echo $(realpath python) >> $GITHUB_PATH - - - name: Clean up Disk space - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - - - name: "Run pipeline with test data ${{ matrix.NXF_VER }} | ${{ matrix.test_name }} | ${{ matrix.profile }}" - run: | - nextflow run ${GITHUB_WORKSPACE} -profile ${{ matrix.test_name }},${{ matrix.profile }} --outdir ./results diff --git a/.github/workflows/clean-up.yml b/.github/workflows/clean-up.yml index 0b6b1f2..172de6f 100644 --- a/.github/workflows/clean-up.yml +++ b/.github/workflows/clean-up.yml @@ -10,7 +10,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10 with: stale-issue-message: "This issue has been tagged as awaiting-changes or awaiting-feedback by an nf-core contributor. Remove stale label or add a comment otherwise this issue will be closed in 20 days." stale-pr-message: "This PR has been tagged as awaiting-changes or awaiting-feedback by an nf-core contributor. Remove stale label or add a comment if it is still useful." diff --git a/.github/workflows/download_pipeline.yml b/.github/workflows/download_pipeline.yml index ab06316..a7bf4fc 100644 --- a/.github/workflows/download_pipeline.yml +++ b/.github/workflows/download_pipeline.yml @@ -12,14 +12,6 @@ on: required: true default: "dev" pull_request: - types: - - opened - - edited - - synchronize - branches: - - main - - master - pull_request_target: branches: - main - master @@ -46,15 +38,18 @@ jobs: runs-on: ubuntu-latest needs: configure steps: + - name: Check out pipeline code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Install Nextflow - uses: nf-core/setup-nextflow@v2 + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 - name: Disk space cleanup uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: - python-version: "3.12" + python-version: "3.14" architecture: "x64" - name: Setup Apptainer @@ -62,10 +57,15 @@ jobs: with: apptainer-version: 1.3.4 + - name: Read .nf-core.yml + id: read_yml + run: | + echo "nf_core_version=$(yq '.nf_core_version' ${{ github.workspace }}/.nf-core.yml)" >> "$GITHUB_OUTPUT" + - name: Install dependencies run: | python -m pip install --upgrade pip - pip install git+https://github.com/nf-core/tools.git@dev + pip install nf-core==${{ steps.read_yml.outputs['nf_core_version'] }} - name: Make a cache directory for the container images run: | @@ -120,6 +120,7 @@ jobs: echo "IMAGE_COUNT_AFTER=$image_count" >> "$GITHUB_OUTPUT" - name: Compare container image counts + id: count_comparison run: | if [ "${{ steps.count_initial.outputs.IMAGE_COUNT_INITIAL }}" -ne "${{ steps.count_afterwards.outputs.IMAGE_COUNT_AFTER }}" ]; then initial_count=${{ steps.count_initial.outputs.IMAGE_COUNT_INITIAL }} @@ -132,3 +133,10 @@ jobs: else echo "The pipeline can be downloaded successfully!" fi + + - name: Upload Nextflow logfile for debugging purposes + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: nextflow_logfile.txt + path: .nextflow.log* + include-hidden-files: true diff --git a/.github/workflows/fix-linting.yml b/.github/workflows/fix-linting.yml index addd34e..8bda64e 100644 --- a/.github/workflows/fix-linting.yml +++ b/.github/workflows/fix-linting.yml @@ -9,7 +9,7 @@ jobs: if: > contains(github.event.comment.html_url, '/pull/') && contains(github.event.comment.body, '@nf-core-bot fix linting') && - github.repository == 'nf-core/dmscore' + github.repository == 'nf-core/deepmutscan' runs-on: ubuntu-latest steps: # Use the @nf-core-bot token to check out so we can push later @@ -86,4 +86,4 @@ jobs: issue-number: ${{ github.event.issue.number }} body: | @${{ github.actor }} I tried to fix the linting errors, but it didn't work. Please fix them manually. - See [CI log](https://github.com/nf-core/dmscore/actions/runs/${{ github.run_id }}) for more details. + See [CI log](https://github.com/nf-core/deepmutscan/actions/runs/${{ github.run_id }}) for more details. diff --git a/.github/workflows/fix_linting.yml b/.github/workflows/fix_linting.yml new file mode 100644 index 0000000..844a1c0 --- /dev/null +++ b/.github/workflows/fix_linting.yml @@ -0,0 +1,85 @@ +name: Fix linting from a comment +on: + issue_comment: + types: [created] + +jobs: + fix-linting: + # Only run if comment is on a PR with the main repo, and if it contains the magic keywords + if: > + contains(github.event.comment.html_url, '/pull/') && + contains(github.event.comment.body, '@nf-core-bot fix linting') && + github.repository == 'nf-core/deepmutscan' + runs-on: ubuntu-latest + steps: + # Use the @nf-core-bot token to check out so we can push later + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.nf_core_bot_auth_token }} + + # indication that the linting is being fixed + - name: React on comment + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 + with: + comment-id: ${{ github.event.comment.id }} + reactions: eyes + + # Action runs on the issue comment, so we don't get the PR by default + # Use the gh cli to check out the PR + - name: Checkout Pull Request + run: gh pr checkout ${{ github.event.issue.number }} + env: + GITHUB_TOKEN: ${{ secrets.nf_core_bot_auth_token }} + + - name: Install Nextflow + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 + + # Install and run prek + - name: Run prek + id: prek + uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2 + continue-on-error: true + + # indication that the linting has finished + - name: react if linting finished succesfully + if: steps.prek.outcome == 'success' + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 + with: + comment-id: ${{ github.event.comment.id }} + reactions: "+1" + + - name: Commit & push changes + id: commit-and-push + if: steps.prek.outcome == 'failure' + run: | + git config user.email "core@nf-co.re" + git config user.name "nf-core-bot" + git config push.default upstream + git add . + git status + git commit -m "[automated] Fix code linting" + git push + + - name: react if linting errors were fixed + id: react-if-fixed + if: steps.commit-and-push.outcome == 'success' + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 + with: + comment-id: ${{ github.event.comment.id }} + reactions: hooray + + - name: react if linting errors were not fixed + if: steps.commit-and-push.outcome == 'failure' + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 + with: + comment-id: ${{ github.event.comment.id }} + reactions: confused + + - name: react if linting errors were not fixed + if: steps.commit-and-push.outcome == 'failure' + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 + with: + issue-number: ${{ github.event.issue.number }} + body: | + @${{ github.actor }} I tried to fix the linting errors, but it didn't work. Please fix them manually. + See [CI log](https://github.com/nf-core/deepmutscan/actions/runs/${{ github.run_id }}) for more details. diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index dbd52d5..8738ffc 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -3,9 +3,6 @@ name: nf-core linting # It runs the `nf-core pipelines lint` and markdown lint tests to ensure # that the code meets the nf-core guidelines. on: - push: - branches: - - dev pull_request: release: types: [published] @@ -14,46 +11,42 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Set up Python 3.12 - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5 - with: - python-version: "3.12" - - - name: Install pre-commit - run: pip install pre-commit + - name: Install Nextflow + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 - - name: Run pre-commit - run: pre-commit run --all-files + - name: Run prek + uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2 nf-core: runs-on: ubuntu-latest steps: - name: Check out pipeline code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Nextflow - uses: nf-core/setup-nextflow@v2 + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 - - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: - python-version: "3.12" + python-version: "3.14" architecture: "x64" + - name: Setup uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + - name: read .nf-core.yml - uses: pietrobolcato/action-read-yaml@1.1.0 + uses: pietrobolcato/action-read-yaml@9f13718d61111b69f30ab4ac683e67a56d254e1d # 1.1.0 id: read_yml with: config: ${{ github.workspace }}/.nf-core.yml - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install nf-core==${{ steps.read_yml.outputs['nf_core_version'] }} + run: uv tool install nf-core==${{ steps.read_yml.outputs['nf_core_version'] }} - name: Run nf-core pipelines lint - if: ${{ github.base_ref != 'master' }} + if: ${{ github.base_ref != 'master' || github.base_ref != 'main' }} env: GITHUB_COMMENTS_URL: ${{ github.event.pull_request.comments_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -61,7 +54,7 @@ jobs: run: nf-core -l lint_log.txt pipelines lint --dir ${GITHUB_WORKSPACE} --markdown lint_results.md - name: Run nf-core pipelines lint --release - if: ${{ github.base_ref == 'master' }} + if: ${{ github.base_ref == 'master' || github.base_ref == 'main' }} env: GITHUB_COMMENTS_URL: ${{ github.event.pull_request.comments_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -74,7 +67,7 @@ jobs: - name: Upload linting log file artifact if: ${{ always() }} - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: linting-logs path: | diff --git a/.github/workflows/linting_comment.yml b/.github/workflows/linting_comment.yml index 0bed96d..5b0c24f 100644 --- a/.github/workflows/linting_comment.yml +++ b/.github/workflows/linting_comment.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download lint results - uses: dawidd6/action-download-artifact@80620a5d27ce0ae443b965134db88467fc607b43 # v7 + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21 with: workflow: linting.yml workflow_conclusion: completed @@ -21,7 +21,7 @@ jobs: run: echo "pr_number=$(cat linting-logs/PR_number.txt)" >> $GITHUB_OUTPUT - name: Post PR comment - uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # v2 + uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} number: ${{ steps.pr_number.outputs.pr_number }} diff --git a/.github/workflows/nf-test.yml b/.github/workflows/nf-test.yml new file mode 100644 index 0000000..efd72d6 --- /dev/null +++ b/.github/workflows/nf-test.yml @@ -0,0 +1,144 @@ +name: Run nf-test +on: + pull_request: + paths-ignore: + - "docs/**" + - "**/meta.yml" + - "**/*.md" + - "**/*.png" + - "**/*.svg" + release: + types: [published] + workflow_dispatch: + +# Cancel if a newer run is started +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NFT_VER: "0.9.4" + NFT_WORKDIR: "~" + NXF_ANSI_LOG: false + NXF_SINGULARITY_CACHEDIR: ${{ github.workspace }}/.singularity + NXF_SINGULARITY_LIBRARYDIR: ${{ github.workspace }}/.singularity + +jobs: + nf-test-changes: + name: nf-test-changes + runs-on: # use self-hosted runners + - runs-on=${{ github.run_id }}-nf-test-changes + - runner=4cpu-linux-x64 + outputs: + shard: ${{ steps.set-shards.outputs.shard }} + total_shards: ${{ steps.set-shards.outputs.total_shards }} + steps: + - name: Clean Workspace # Purge the workspace in case it's running on a self-hosted runner + run: | + ls -la ./ + rm -rf ./* || true + rm -rf ./.??* || true + ls -la ./ + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: get number of shards + id: set-shards + uses: ./.github/actions/get-shards + env: + NFT_VER: ${{ env.NFT_VER }} + with: + max_shards: 7 + + - name: debug + run: | + echo ${{ steps.set-shards.outputs.shard }} + echo ${{ steps.set-shards.outputs.total_shards }} + + nf-test: + name: "${{ matrix.profile }} | ${{ matrix.NXF_VER }} | ${{ matrix.shard }}/${{ needs.nf-test-changes.outputs.total_shards }}" + needs: [nf-test-changes] + if: ${{ needs.nf-test-changes.outputs.total_shards != '0' }} + runs-on: # use self-hosted runners + - runs-on=${{ github.run_id }}-nf-test + - runner=4cpu-linux-x64 + strategy: + fail-fast: false + matrix: + shard: ${{ fromJson(needs.nf-test-changes.outputs.shard) }} + profile: [conda, docker, singularity] + isMain: + - ${{ github.base_ref == 'master' || github.base_ref == 'main' }} + # Exclude conda and singularity on dev + exclude: + - isMain: false + profile: "conda" + - isMain: false + profile: "singularity" + NXF_VER: + - "25.10.4" + - "latest-everything" + env: + NXF_ANSI_LOG: false + TOTAL_SHARDS: ${{ needs.nf-test-changes.outputs.total_shards }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Run nf-test + id: run_nf_test + uses: ./.github/actions/nf-test + continue-on-error: ${{ matrix.NXF_VER == 'latest-everything' }} + env: + NFT_WORKDIR: ${{ env.NFT_WORKDIR }} + NXF_VERSION: ${{ matrix.NXF_VER }} + with: + profile: ${{ matrix.profile }} + shard: ${{ matrix.shard }} + total_shards: ${{ env.TOTAL_SHARDS }} + + - name: Report test status + if: ${{ always() }} + run: | + if [[ "${{ steps.run_nf_test.outcome }}" == "failure" ]]; then + echo "::error::Test with ${{ matrix.NXF_VER }} failed" + # Add to workflow summary + echo "## ❌ Test failed: ${{ matrix.profile }} | ${{ matrix.NXF_VER }} | Shard ${{ matrix.shard }}/${{ env.TOTAL_SHARDS }}" >> $GITHUB_STEP_SUMMARY + if [[ "${{ matrix.NXF_VER }}" == "latest-everything" ]]; then + echo "::warning::Test with latest-everything failed but will not cause workflow failure. Please check if the error is expected or if it needs fixing." + fi + if [[ "${{ matrix.NXF_VER }}" != "latest-everything" ]]; then + exit 1 + fi + fi + + confirm-pass: + needs: [nf-test] + if: always() + runs-on: # use self-hosted runners + - runs-on=${{ github.run_id }}-confirm-pass + - runner=2cpu-linux-x64 + steps: + - name: One or more tests failed (excluding latest-everything) + if: ${{ contains(needs.*.result, 'failure') }} + run: exit 1 + + - name: One or more tests cancelled + if: ${{ contains(needs.*.result, 'cancelled') }} + run: exit 1 + + - name: All tests ok + if: ${{ contains(needs.*.result, 'success') }} + run: exit 0 + + - name: debug-print + if: always() + run: | + echo "::group::DEBUG: `needs` Contents" + echo "DEBUG: toJSON(needs) = ${{ toJSON(needs) }}" + echo "DEBUG: toJSON(needs.*.result) = ${{ toJSON(needs.*.result) }}" + echo "::endgroup::" diff --git a/.github/workflows/release-announcements.yml b/.github/workflows/release-announcements.yml index 450b1d5..78d5dbe 100644 --- a/.github/workflows/release-announcements.yml +++ b/.github/workflows/release-announcements.yml @@ -14,7 +14,11 @@ jobs: run: | echo "topics=$(curl -s https://nf-co.re/pipelines.json | jq -r '.remote_workflows[] | select(.full_name == "${{ github.repository }}") | .topics[]' | awk '{print "#"$0}' | tr '\n' ' ')" | sed 's/-//g' >> $GITHUB_OUTPUT - - uses: rzr/fediverse-action@master + - name: get description + id: get_description + run: | + echo "description=$(curl -s https://nf-co.re/pipelines.json | jq -r '.remote_workflows[] | select(.full_name == "${{ github.repository }}") | .description')" >> $GITHUB_OUTPUT + - uses: rzr/fediverse-action@563159eb8d45f70ab6aaba36ed55cd037e51f441 # master with: access-token: ${{ secrets.MASTODON_ACCESS_TOKEN }} host: "mstdn.science" # custom host if not "mastodon.social" (default) @@ -22,48 +26,15 @@ jobs: # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release message: | Pipeline release! ${{ github.repository }} v${{ github.event.release.tag_name }} - ${{ github.event.release.name }}! - + ${{ steps.get_description.outputs.description }} Please see the changelog: ${{ github.event.release.html_url }} ${{ steps.get_topics.outputs.topics }} #nfcore #openscience #nextflow #bioinformatics - send-tweet: - runs-on: ubuntu-latest - - steps: - - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5 - with: - python-version: "3.10" - - name: Install dependencies - run: pip install tweepy==4.14.0 - - name: Send tweet - shell: python - run: | - import os - import tweepy - - client = tweepy.Client( - access_token=os.getenv("TWITTER_ACCESS_TOKEN"), - access_token_secret=os.getenv("TWITTER_ACCESS_TOKEN_SECRET"), - consumer_key=os.getenv("TWITTER_CONSUMER_KEY"), - consumer_secret=os.getenv("TWITTER_CONSUMER_SECRET"), - ) - tweet = os.getenv("TWEET") - client.create_tweet(text=tweet) - env: - TWEET: | - Pipeline release! ${{ github.repository }} v${{ github.event.release.tag_name }} - ${{ github.event.release.name }}! - - Please see the changelog: ${{ github.event.release.html_url }} - TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} - TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} - TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} - TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} - bsky-post: runs-on: ubuntu-latest steps: - - uses: zentered/bluesky-post-action@80dbe0a7697de18c15ad22f4619919ceb5ccf597 # v0.1.0 + - uses: zentered/bluesky-post-action@5a91cc2ad10a304a4e96c16182dbe4918710bcf6 # v0.4.0 with: post: | Pipeline release! ${{ github.repository }} v${{ github.event.release.tag_name }} - ${{ github.event.release.name }}! diff --git a/.github/workflows/template-version-comment.yml b/.github/workflows/template-version-comment.yml new file mode 100644 index 0000000..ea30827 --- /dev/null +++ b/.github/workflows/template-version-comment.yml @@ -0,0 +1,46 @@ +name: nf-core template version comment +# This workflow is triggered on PRs to check if the pipeline template version matches the latest nf-core version. +# It posts a comment to the PR, even if it comes from a fork. + +on: pull_request_target + +jobs: + template_version: + runs-on: ubuntu-latest + steps: + - name: Check out pipeline code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Read template version from .nf-core.yml + uses: nichmor/minimal-read-yaml@1f7205277e25e156e1f63815781db80a6d490b8f # v0.0.2 + id: read_yml + with: + config: ${{ github.workspace }}/.nf-core.yml + + - name: Install nf-core + run: | + python -m pip install --upgrade pip + pip install nf-core==${{ steps.read_yml.outputs['nf_core_version'] }} + + - name: Check nf-core outdated + id: nf_core_outdated + run: echo "OUTPUT=$(pip list --outdated | grep nf-core)" >> ${GITHUB_ENV} + + - name: Post nf-core template version comment + uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3 + if: | + contains(env.OUTPUT, 'nf-core') + with: + repo-token: ${{ secrets.NF_CORE_BOT_AUTH_TOKEN }} + allow-repeats: false + message: | + > [!WARNING] + > Newer version of the nf-core template is available. + > + > Your pipeline is using an old version of the nf-core template: ${{ steps.read_yml.outputs['nf_core_version'] }}. + > Please update your pipeline to the latest version. + > + > For more documentation on how to update your pipeline, please see the [Synchronisation documentation](https://nf-co.re/docs/developing/template-syncs/overview). + # diff --git a/.gitignore b/.gitignore index a42ce01..0187478 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,11 @@ testing/ testing* *.pyc null/ +.lineage/ + +# Pipeline outputs +test_results/ +work/ +.nextflow* +.nf-test* +.Rhistory diff --git a/.nf-core.yml b/.nf-core.yml index 59e948b..2bf2e97 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -1,24 +1,12 @@ -repository_type: pipeline - -nf_core_version: 3.1.2 - lint: {} - +nf_core_version: 4.0.2 +repository_type: pipeline template: + author: Benjamin Wehnert & Maximilian R. Stammnitz + description: "Processing and analysis of deep mutational scanning (DMS) data from shotgun and amplicon sequencing: read alignment, variant counting, sequencing-error correction, library QC and fitness estimation" + force: false + is_nfcore: true + name: deepmutscan org: nf-core - name: dmscore - description: "Until now, most Deep Mutational Scanning (DMS) experiments relied\ - \ on variant-specific barcoded libraries for sequencing. This method enabled DMS\ - \ on large proteins and led to many great publications. Recently, efforts have\ - \ increased to make use of the classic and more simple random fragmentation-based\ - \ short-read sequencing (\u201Cshotgun-sequencing\u201D). This saves time and\ - \ money and due to its simpler experimental design is less prone to mistakes.\ - \ dmscore handles the essential computational steps, processing the raw FASTQ\ - \ files and generating a count table of variants. Along the way, it provides multiple\ - \ QC metrics, enabling users to quickly evaluate the success of their experimental\ - \ setup." - author: Benjamin Wehnert & Max Stammnitz - version: 1.0.0dev - force: true outdir: . - is_nfcore: true + version: 1.0.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e9f0e1..55cc71a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,10 +4,32 @@ repos: hooks: - id: prettier additional_dependencies: - - prettier@3.2.5 - - - repo: https://github.com/editorconfig-checker/editorconfig-checker.python - rev: "3.0.3" + - prettier@3.8.3 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 hooks: - - id: editorconfig-checker - alias: ec + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + exclude: | + (?x)^( + .*ro-crate-metadata.json$| + modules/(?!local/).*| + subworkflows/(?!local/).*| + .*\.snap$| + assets/variant_effect_inspection_tool/3Dmol-min\.js.* + )$ + - id: end-of-file-fixer + exclude: | + (?x)^( + .*ro-crate-metadata.json$| + modules/(?!local/).*| + subworkflows/(?!local/).*| + .*\.snap$| + assets/variant_effect_inspection_tool/3Dmol-min\.js.* + )$ + - repo: https://github.com/seqeralabs/nf-lint-pre-commit + rev: v0.3.0 + hooks: + - id: nextflow-lint + files: '\.nf$|nextflow\.config$' + args: ["-output", "json"] diff --git a/.prettierignore b/.prettierignore index edd29f0..0bd1d5e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,4 @@ email_template.html -adaptivecard.json -slackreport.json .nextflow* work/ data/ @@ -10,4 +8,8 @@ testing/ testing* *.pyc bin/ +.nf-test/ ro-crate-metadata.json +modules/nf-core/ +subworkflows/nf-core/ +assets/variant_effect_inspection_tool/3Dmol-min.js diff --git a/.prettierrc.yml b/.prettierrc.yml index c81f9a7..07dbd8b 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -1 +1,6 @@ printWidth: 120 +tabWidth: 4 +overrides: + - files: "*.{md,yml,yaml,html,css,scss,js,cff}" + options: + tabWidth: 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ea82f..3cedede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,59 @@ -# nf-core/dmscore: Changelog +# nf-core/deepmutscan: Changelog The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## v1.0.0dev - [date] +## v1.0.0 - [date] -Initial release of nf-core/dmscore, created with the [nf-core](https://nf-co.re/) template. +Initial release of nf-core/deepmutscan, created with the [nf-core](https://nf-co.re/) template. ### `Added` +- Samplesheet input of paired-end FASTQ files per library, annotated with `sample`, `type` (`input`, `output`, `wildtype`) and `replicate` +- Raw read QC with FastQC and MultiQC +- Read alignment to a gene-sized reference ORF with BWA-MEM (`--fasta`, `--reading_frame`) +- Filtering of unmapped, secondary, low mapping-quality (MAPQ < 30) and indel-containing alignments with samtools; wildtype reads are retained for error correction +- Read-pair merging with `vsearch --fastq_mergepairs`, re-alignment, coordinate sorting and indexing +- Light-weight variant counter built on the `pysam` and `polars` libraries in Python, replacing `GATK AnalyzeSaturationMutagenesis` with a column-compatible output, including a minimum base quality (`--base_qual`, default Q40) and read-edge exclusion window (`--min_flank`, default 2 bp) +- Annotation and filtering of variant counts against the programmed mutagenesis library (`--mutagenesis_type` `nnk`, `nns`, `nnh`, `nnn`, `nnk_nns`, `nnk_nns_nnh` or `custom` with `--custom_codon_library`) +- Single-nucleotide variant sequencing-error correction (`--error_correction`): + - `false_doubles` (default): error rates estimated from read-linked false double mutant codons, by maximum likelihood (`--false_doubles_method mle`, default) or empirical Bayes (`eb`), within a configurable codon window (`--false_doubles_codon_window`) + - `wildtype`: error profile from additional deep sequencing of the unmutated template (`type: wildtype` samplesheet rows) + - `none` +- Interactive, run-level HTML error-correction report +- DMS library QC per library: count and count-per-coverage heatmaps, sorted count distributions, sliding-window coverage and count profiles (`--sliding_window_size`, `--aimed_cov`), and sequencing-depth rarefaction on a geometric depth grid (`--run_seqdepth`) +- Optional fitness estimation (`--fitness`): merged count tables, experimental design, synonymous wildtype proxy selection, default log-ratio fitness with replicate rescaling and summary statistics, replicate correlation plots and fitness heatmap +- Optional fitness estimation with DiMSum (`--dimsum`) and mutscan edgeR / limma (`--mutscan`) +- Minimum input read count per replicate for fitness estimation (`--min_counts`, default 10), applied to the default estimator, DiMSum and mutscan +- Optional output pseudocount for dropout variants (`--output_pseudocount`, default 1; 0 disables), applied to the default estimator and DiMSum +- Optional interactive 3D variant effect inspection tool built from a user-supplied wildtype structure (`--pdb`) +- Self-contained, all-in-one run report (`deepmutscan_report.html`) embedding QC, error-correction, fitness, MultiQC and run statistics +- `test` profile on a 50,000 read-pair subsample of a GID1A nicking-mutagenesis GluePCA experiment, with nf-test snapshot testing, and `test_full` profile on the complete dataset (ENA PRJEB110196); test data and the PDB structure are hosted on [nf-core/test-datasets](https://github.com/nf-core/test-datasets/tree/deepmutscan) + ### `Fixed` ### `Dependencies` +| Dependency | Version | +| ------------- | --------------------- | +| `fastqc` | 0.12.1 | +| `multiqc` | 1.35 | +| `bwa` | 0.7.19 | +| `samtools` | 1.21, 1.22.1, 1.23.1 | +| `vsearch` | 2.30.0 | +| `python` | 3.12 | +| `pysam` | 0.24.0 | +| `polars` | 1.33.1 (lts-cpu) | +| `pyarrow` | 24.0.0 | +| `biopython` | 1.87 | +| `numpy` | 1.26.4, 2.5.1 | +| `pandas` | 2.2.1 | +| `r-base` | 4.4.2 (DiMSum), 4.5.1 | +| `biostrings` | 2.74.0, 2.78.0 | +| `r-tidyverse` | 2.0.0 | +| `r-ggplot2` | 3.5.1, 4.0.2 | +| `r-zoo` | 1.8_15 | +| `r-dimsum` | 1.4 | +| `mutscan` | 1.0.0 | + ### `Deprecated` diff --git a/CITATIONS.md b/CITATIONS.md index a7e69f7..f8c2fd9 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -1,22 +1,98 @@ -# nf-core/dmscore: Citations +# nf-core/deepmutscan: Citations -## [nf-core](https://pubmed.ncbi.nlm.nih.gov/32055031/) +## [nf-core](https://www.nature.com/articles/s41587-020-0439-x) -> Ewels PA, Peltzer A, Fillinger S, Patel H, Alneberg J, Wilm A, Garcia MU, Di Tommaso P, Nahnsen S. The nf-core framework for community-curated bioinformatics pipelines. Nat Biotechnol. 2020 Mar;38(3):276-278. doi: 10.1038/s41587-020-0439-x. PubMed PMID: 32055031. +> Ewels PA, Peltzer A, Fillinger S, Patel H, Alneberg J, Wilm A, Garcia MU, Di Tommaso P, Nahnsen S. The nf-core framework for community-curated bioinformatics pipelines. Nat Biotechnol. 2020 Mar;38(3):276-278. doi: 10.1038/s41587-020-0439-x. ## [Nextflow](https://pubmed.ncbi.nlm.nih.gov/28398311/) -> Di Tommaso P, Chatzou M, Floden EW, Barja PP, Palumbo E, Notredame C. Nextflow enables reproducible computational workflows. Nat Biotechnol. 2017 Apr 11;35(4):316-319. doi: 10.1038/nbt.3820. PubMed PMID: 28398311. +> Di Tommaso P, Chatzou M, Floden EW, Barja PP, Palumbo E, Notredame C. Nextflow enables reproducible computational workflows. Nat Biotechnol. 2017 Apr 11;35(4):316-319. doi: 10.1038/nbt.3820. ## Pipeline tools - [FastQC](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/) -> Andrews, S. (2010). FastQC: A Quality Control Tool for High Throughput Sequence Data [Online]. + > Andrews S. (2010). FastQC: A Quality Control Tool for High Throughput Sequence Data [Online]. -- [MultiQC](https://pubmed.ncbi.nlm.nih.gov/27312411/) +- [BWA-MEM](https://arxiv.org/abs/1303.3997) -> Ewels P, Magnusson M, Lundin S, KΓ€ller M. MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics. 2016 Oct 1;32(19):3047-8. doi: 10.1093/bioinformatics/btw354. Epub 2016 Jun 16. PubMed PMID: 27312411; PubMed Central PMCID: PMC5039924. + > Li H. Aligning sequence reads, clone sequences and assembly contigs with BWA-MEM. arXiv. 2013;1303.3997 [q-bio.GN]. doi: 10.48550/arXiv.1303.3997. + +- [SAMtools](https://doi.org/10.1093/gigascience/giab008) + + > Danecek P, Bonfield JK, Liddle J, Marshall J, Ohan V, Pollard MO, Whitwham A, Keane T, McCarthy SA, Davies RM, Li H. Twelve years of SAMtools and BCFtools. Gigascience. 2021 Feb 16;10(2):giab008. doi: 10.1093/gigascience/giab008. + +- [VSEARCH](https://peerj.com/articles/2584/) + + > Rognes T, Flouri T, Nichols B, Quince C, MahΓ© F. VSEARCH: a versatile open source tool for metagenomics. PeerJ. 2016 Oct 18;4:e2584. doi: 10.7717/peerj.2584. + +- [pysam](https://github.com/pysam-developers/pysam) + + > Heger A, Jacobs K, et al. pysam: a Python module for reading, manipulating and writing genomic data sets. https://github.com/pysam-developers/pysam + +- [Polars](https://github.com/pola-rs/polars) + + > Vink R, et al. Polars: Lightning-fast DataFrame library for Rust and Python. https://github.com/pola-rs/polars + +- [Biopython](https://doi.org/10.1093/bioinformatics/btp163) + + > Cock PJ, Antao T, Chang JT, Chapman BA, Cox CJ, Dalke A, Friedberg I, Hamelryck T, Kauff F, Wilczynski B, de Hoon MJ. Biopython: freely available Python tools for computational molecular biology and bioinformatics. Bioinformatics. 2009 Jun 1;25(11):1422-3. doi: 10.1093/bioinformatics/btp163. + +- [NumPy](https://www.nature.com/articles/s41586-020-2649-2) + + > Harris CR, Millman KJ, van der Walt SJ, et al. Array programming with NumPy. Nature. 2020;585(7825):357-362. doi: 10.1038/s41586-020-2649-2. + +- [pandas](https://pandas.pydata.org/) + + > McKinney W. Data Structures for Statistical Computing in Python. Proceedings of the 9th Python in Science Conference. 2010;56-61. doi: 10.25080/Majora-92bf1922-00a. + +- [R](https://www.r-project.org/) + + > R Core Team. R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. https://www.R-project.org/ + +- [Biostrings](https://bioconductor.org/packages/Biostrings/) + + > PagΓ¨s H, Aboyoun P, Gentleman R, DebRoy S. Biostrings: Efficient manipulation of biological strings. R package. doi: 10.18129/B9.bioc.Biostrings. + +- [ggplot2](https://ggplot2.tidyverse.org/) + + > Wickham H. ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York; 2016. ISBN: 978-3-319-24277-4. + +- [tidyverse](https://www.tidyverse.org/) (dplyr, tidyr, stringr) + + > Wickham H, Averick M, Bryan J, et al. Welcome to the tidyverse. Journal of Open Source Software. 2019;4(43):1686. doi: 10.21105/joss.01686. + +- [zoo](https://cran.r-project.org/package=zoo) + + > Zeileis A, Grothendieck G. zoo: S3 Infrastructure for Regular and Irregular Time Series. Journal of Statistical Software. 2005;14(6):1-27. doi: 10.18637/jss.v014.i06. + +- [DiMSum](https://link.springer.com/article/10.1186/s13059-020-02091-3) + + > Faure AJ, Schmiedel JM, Baeza-Centurion P, Lehner B. DiMSum: an error model and pipeline for analyzing deep mutational scanning data and diagnosing common experimental pathologies. Genome Biol. 2020;21(1):207. doi: 10.1186/s13059-020-02091-3. + +- [mutscan](https://link.springer.com/article/10.1186/s13059-023-02967-0) + + > Soneson C, Bendel AM, Diss G, Stadler MB. mutscan-a flexible R package for efficient end-to-end analysis of multiplexed assays of variant effect data. Genome Biol. 2023;24(1):132. doi: 10.1186/s13059-023-02967-0. + +- [edgeR](https://doi.org/10.1093/bioinformatics/btp616) + + > Robinson MD, McCarthy DJ, Smyth GK. edgeR: a Bioconductor package for differential expression analysis of digital gene expression data. Bioinformatics. 2010 Jan 1;26(1):139-40. doi: 10.1093/bioinformatics/btp616. + +- [limma](https://doi.org/10.1093/nar/gkv007) + + > Ritchie ME, Phipson B, Wu D, Hu Y, Law CW, Shi W, Smyth GK. limma powers differential expression analyses for RNA-sequencing and microarray studies. Nucleic Acids Res. 2015 Apr 20;43(7):e47. doi: 10.1093/nar/gkv007. + +- [3Dmol.js](https://doi.org/10.1093/bioinformatics/btu829) + + > Rego N, Koes D. 3Dmol.js: molecular visualization with WebGL. Bioinformatics. 2015 Apr 15;31(8):1322-4. doi: 10.1093/bioinformatics/btu829. (Bundled in the interactive variant effect inspection tool; BSD-3-Clause.) + +- [AlphaFold Protein Structure Database](https://alphafold.ebi.ac.uk/) (test-profile structure only) + + > Varadi M, Bertoni D, Magana P, et al. AlphaFold Protein Structure Database in 2024: providing structure coverage for over 214 million protein sequences. Nucleic Acids Res. 2024;52(D1):D368-D375. doi: 10.1093/nar/gkad1011. + +- [MultiQC](https://doi.org/10.1093/bioinformatics/btw354) + + > Ewels P, Magnusson M, Lundin S, KΓ€ller M. MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics. 2016 Oct 1;32(19):3047-8. doi: 10.1093/bioinformatics/btw354. Epub 2016 Jun 16. ## Software packaging/containerisation tools @@ -24,18 +100,18 @@ > Anaconda Software Distribution. Computer software. Vers. 2-2.4.0. Anaconda, Nov. 2016. Web. -- [Bioconda](https://pubmed.ncbi.nlm.nih.gov/29967506/) +- [Bioconda](https://www.nature.com/articles/s41592-018-0046-7) - > GrΓΌning B, Dale R, SjΓΆdin A, Chapman BA, Rowe J, Tomkins-Tinch CH, Valieris R, KΓΆster J; Bioconda Team. Bioconda: sustainable and comprehensive software distribution for the life sciences. Nat Methods. 2018 Jul;15(7):475-476. doi: 10.1038/s41592-018-0046-7. PubMed PMID: 29967506. + > GrΓΌning B, Dale R, SjΓΆdin A, Chapman BA, Rowe J, Tomkins-Tinch CH, Valieris R, KΓΆster J; Bioconda Team. Bioconda: sustainable and comprehensive software distribution for the life sciences. Nat Methods. 2018 Jul;15(7):475-476. doi: 10.1038/s41592-018-0046-7. -- [BioContainers](https://pubmed.ncbi.nlm.nih.gov/28379341/) +- [BioContainers](https://doi.org/10.1093/bioinformatics/btx192) - > da Veiga Leprevost F, GrΓΌning B, Aflitos SA, RΓΆst HL, Uszkoreit J, Barsnes H, Vaudel M, Moreno P, Gatto L, Weber J, Bai M, Jimenez RC, Sachsenberg T, Pfeuffer J, Alvarez RV, Griss J, Nesvizhskii AI, Perez-Riverol Y. BioContainers: an open-source and community-driven framework for software standardization. Bioinformatics. 2017 Aug 15;33(16):2580-2582. doi: 10.1093/bioinformatics/btx192. PubMed PMID: 28379341; PubMed Central PMCID: PMC5870671. + > da Veiga Leprevost F, GrΓΌning B, Aflitos SA, RΓΆst HL, Uszkoreit J, Barsnes H, Vaudel M, Moreno P, Gatto L, Weber J, Bai M, Jimenez RC, Sachsenberg T, Pfeuffer J, Alvarez RV, Griss J, Nesvizhskii AI, Perez-Riverol Y. BioContainers: an open-source and community-driven framework for software standardization. Bioinformatics. 2017 Aug 15;33(16):2580-2582. doi: 10.1093/bioinformatics/btx192. -- [Docker](https://dl.acm.org/doi/10.5555/2600239.2600241) +- [Docker](https://dl.acm.org/doi/10.1145/3830418.3843858) > Merkel, D. (2014). Docker: lightweight linux containers for consistent development and deployment. Linux Journal, 2014(239), 2. doi: 10.5555/2600239.2600241. -- [Singularity](https://pubmed.ncbi.nlm.nih.gov/28494014/) +- [Singularity](https://doi.org/10.1371/journal.pone.0177459) - > Kurtzer GM, Sochat V, Bauer MW. Singularity: Scientific containers for mobility of compute. PLoS One. 2017 May 11;12(5):e0177459. doi: 10.1371/journal.pone.0177459. eCollection 2017. PubMed PMID: 28494014; PubMed Central PMCID: PMC5426675. + > Kurtzer GM, Sochat V, Bauer MW. Singularity: Scientific containers for mobility of compute. PLoS One. 2017 May 11;12(5):e0177459. doi: 10.1371/journal.pone.0177459. eCollection 2017. diff --git a/LICENSE b/LICENSE index 48133aa..8119d7b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) The nf-core/dmscore team +Copyright (c) The nf-core/deepmutscan team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b6ae647..01ef6b4 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,124 @@

- - nf-core/dmscore + + nf-core/deepmutscan

-[![GitHub Actions CI Status](https://github.com/nf-core/dmscore/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/dmscore/actions/workflows/ci.yml) -[![GitHub Actions Linting Status](https://github.com/nf-core/dmscore/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/dmscore/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/dmscore/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX) +[![GitHub Actions CI Status](https://github.com/nf-core/deepmutscan/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/deepmutscan/actions/workflows/ci.yml) +[![GitHub Actions Linting Status](https://github.com/nf-core/deepmutscan/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/deepmutscan/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/deepmutscan/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX) [![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com) -[![Nextflow](https://img.shields.io/badge/nextflow%20DSL2-%E2%89%A524.04.2-23aa62.svg)](https://www.nextflow.io/) +[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.10.4-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/) +[![nf-core template version](https://img.shields.io/badge/nf--core_template-4.0.2-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/4.0.2) [![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/) [![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/) [![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/) -[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/nf-core/dmscore) +[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/nf-core/deepmutscan) -[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23dmscore-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/dmscore)[![Follow on Twitter](http://img.shields.io/badge/twitter-%40nf__core-1DA1F2?labelColor=000000&logo=twitter)](https://twitter.com/nf_core)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core) +[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23deepmutscan-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/deepmutscan)[![Follow on Twitter](http://img.shields.io/badge/twitter-%40nf__core-1DA1F2?labelColor=000000&logo=twitter)](https://twitter.com/nf_core)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core) ## Introduction -**nf-core/dmscore** is a bioinformatics pipeline that ... +**nf-core/deepmutscan** is a workflow designed for the analysis of deep mutational scanning (DMS) data. DMS enables researchers to experimentally measure the fitness effects of thousands of gene variants simultaneously, helping to classify disease-causing mutants in human and other species populations, and to learn the fundamental rules of protein architecture, small-molecule binding, mRNA splicing, viral evolution and many other quantifiable phenotypes. - +While DNA synthesis and sequencing technologies have advanced substantially, long open reading frame (ORF) targets still present a major challenge for DMS studies. Shotgun DNA sequencing of randomly fragmented variant libraries can greatly speed up the inference of long ORF mutant fitness landscapes, as it avoids the need for library barcoding or multi-tile sequencing. We have designed `nf-core/deepmutscan` to unlock shotgun sequencing-based DMS studies on long ORFs, and to simplify and standardise the bioinformatics steps involved in processing such experiments – from read alignment to QC reporting, variant count error correction and fitness landscape inference. Amplicon (tile) sequencing data can be processed in the same way. - -1. Read QC ([`FastQC`](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/))2. Present QC for raw reads ([`MultiQC`](http://multiqc.info/)) +![nf-core/deepmutscan workflow](docs/images/pipeline.png) -## Usage +## Major features -> [!NOTE] -> If you are new to Nextflow and nf-core, please refer to [this page](https://nf-co.re/docs/usage/installation) on how to set-up Nextflow. Make sure to [test your setup](https://nf-co.re/docs/usage/introduction#how-to-run-a-pipeline) with `-profile test` before running the workflow on actual data. +- End-to-end processing of DMS libraries from shotgun (randomly fragmented) or amplicon short-read sequencing +- Light-weight variant counter with base-quality and read-edge filters, replacing `GATK AnalyzeSaturationMutagenesis` +- Intrinsic sequencing-error correction of single-nucleotide variant counts from read-linked false double mutants (maximum-likelihood or empirical-Bayes estimators), or from additional wildtype template sequencing +- Library quality control: mutant count heatmaps, positional coverage and mutation-type biases, sequencing-depth rarefaction +- Fitness estimation with a built-in log-ratio estimator, plus optional [DiMSum](https://github.com/lehner-lab/DiMSum) and [mutscan](https://github.com/fmicompbio/mutscan) +- Support for degenerate codon libraries (NNK, NNS, NNH, NNN and combinations), e.g. from nicking mutagenesis, and for custom (position-specific) codon libraries, e.g. from Twist tiles +- A single, self-contained HTML report per run, and an optional interactive 3D variant effect inspection tool when a wildtype structure is supplied +- Containerisation via Docker, Singularity/Apptainer and Conda; scalability across HPC and cloud systems - +First, prepare a samplesheet with your input data. Each row represents one sequencing library as a pair of FASTQ files (paired-end), annotated with the biological sample, its type in the selection experiment (`input`, `output` or `wildtype`) and the replicate number: -Now, you can run the pipeline using: +```csv title="samplesheet.csv" +sample,type,replicate,file1,file2 +ORF1,input,1,/reads/input1_R1.fastq.gz,/reads/input1_R2.fastq.gz +ORF1,input,2,/reads/input2_R1.fastq.gz,/reads/input2_R2.fastq.gz +ORF1,output,1,/reads/output1_R1.fastq.gz,/reads/output1_R2.fastq.gz +ORF1,output,2,/reads/output2_R1.fastq.gz,/reads/output2_R2.fastq.gz +``` - +Secondly, provide the gene or gene region of interest as a reference FASTA file via `--fasta`, and the nucleotide coordinates of the mutagenised open reading frame within it via `--reading_frame` (1-based, inclusive, e.g. `1-300` for the first 100 codons). -```bash -nextflow run nf-core/dmscore \ +Now, you can run the pipeline using: + +```bash title="example_run.sh" +nextflow run nf-core/deepmutscan \ -profile \ - --input samplesheet.csv \ - --outdir + --input ./samplesheet.csv \ + --fasta ./ref.fa \ + --reading_frame 1-300 \ + --outdir ./results ``` +Add `--fitness` to estimate variant fitness from the input and output samples, and see the [usage documentation](https://nf-co.re/deepmutscan/usage) and the [parameter documentation](https://nf-co.re/deepmutscan/parameters) for all other options. + > [!WARNING] > Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; see [docs](https://nf-co.re/docs/usage/getting_started/configuration#custom-configuration-files). -For more details and further functionality, please refer to the [usage documentation](https://nf-co.re/dmscore/usage) and the [parameter documentation](https://nf-co.re/dmscore/parameters). - ## Pipeline output -To see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/dmscore/results) tab on the nf-core website pipeline page. -For more details about the output files and reports, please refer to the -[output documentation](https://nf-co.re/dmscore/output). +To see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/deepmutscan/results) tab on the nf-core website pipeline page. +For more details about the output files and reports, please refer to the [output documentation](https://nf-co.re/deepmutscan/output). ## Credits -nf-core/dmscore was originally written by Benjamin Wehnert & Max Stammnitz. +nf-core/deepmutscan was originally written by [Benjamin Wehnert](https://github.com/BenjaminWehnert1008) and [Maximilian Stammnitz](https://github.com/MaximilianStammnitz) at the [Centre for Genomic Regulation (CRG), Barcelona](https://www.crg.eu/), with the generous support of an EMBO Long-term Postdoctoral Fellowship, the Erasmus+ programme and a Marie SkΕ‚odowska-Curie grant by the European Union. We thank the following people for their extensive assistance in the development of this pipeline: - +- [Fei Sang](https://github.com/fei-hgi) (Wellcome Sanger Institute) – original variant counting implementation +- [JΓΊlia Mir-Pedrol](https://github.com/mirpedrol) (CRG) – nf-core development guidance and code review +- [Matthias HΓΆrtenhuber](https://github.com/mashehu) (SciLifeLab) – nf-core development guidance and code review ## Contributions and Support -If you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md). +If you would like to contribute to this pipeline, please see the [contributing guidelines](docs/CONTRIBUTING.md). -For further information or help, don't hesitate to get in touch on the [Slack `#dmscore` channel](https://nfcore.slack.com/channels/dmscore) (you can join with [this invite](https://nf-co.re/join/slack)). +For further information or help, don't hesitate to get in touch on the [Slack `#deepmutscan` channel](https://nfcore.slack.com/channels/deepmutscan) (you can join with [this invite](https://nf-co.re/join/slack)). Bug reports and feature requests are welcome as GitHub [issues](https://github.com/nf-core/deepmutscan/issues). + +For scientific discussions around the use of this pipeline (e.g. on experimental design or sequencing data requirements), please feel free to get in touch with us directly: + +- Benjamin Wehnert β€” wehnertbenjamin@gmail.com +- Maximilian Stammnitz β€” maximilian.stammnitz@crg.eu ## Citations - - +If you use `nf-core/deepmutscan` for your analysis, please cite it as follows: + +> Wehnert B, et al. _bioRxiv_ preprint (in preparation). - + + An extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file. diff --git a/assets/adaptivecard.json b/assets/adaptivecard.json deleted file mode 100644 index 62ad62a..0000000 --- a/assets/adaptivecard.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "type": "message", - "attachments": [ - { - "contentType": "application/vnd.microsoft.card.adaptive", - "contentUrl": null, - "content": { - "\$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "msteams": { - "width": "Full" - }, - "type": "AdaptiveCard", - "version": "1.2", - "body": [ - { - "type": "TextBlock", - "size": "Large", - "weight": "Bolder", - "color": "<% if (success) { %>Good<% } else { %>Attention<%} %>", - "text": "nf-core/dmscore v${version} - ${runName}", - "wrap": true - }, - { - "type": "TextBlock", - "spacing": "None", - "text": "Completed at ${dateComplete} (duration: ${duration})", - "isSubtle": true, - "wrap": true - }, - { - "type": "TextBlock", - "text": "<% if (success) { %>Pipeline completed successfully!<% } else { %>Pipeline completed with errors. The full error message was: ${errorReport}.<% } %>", - "wrap": true - }, - { - "type": "TextBlock", - "text": "The command used to launch the workflow was as follows:", - "wrap": true - }, - { - "type": "TextBlock", - "text": "${commandLine}", - "isSubtle": true, - "wrap": true - } - ], - "actions": [ - { - "type": "Action.ShowCard", - "title": "Pipeline Configuration", - "card": { - "type": "AdaptiveCard", - "\$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "body": [ - { - "type": "FactSet", - "facts": [<% out << summary.collect{ k,v -> "{\"title\": \"$k\", \"value\" : \"$v\"}"}.join(",\n") %> - ] - } - ] - } - } - ] - } - } - ] -} diff --git a/assets/email_template.html b/assets/email_template.html index 8baab9e..0fec607 100644 --- a/assets/email_template.html +++ b/assets/email_template.html @@ -4,21 +4,21 @@ - - nf-core/dmscore Pipeline Report + + nf-core/deepmutscan Pipeline Report
-

nf-core/dmscore ${version}

+

nf-core/deepmutscan ${version}

Run Name: $runName

<% if (!success){ out << """
-

nf-core/dmscore execution completed unsuccessfully!

+

nf-core/deepmutscan execution completed unsuccessfully!

The exit status of the task that caused the workflow execution to fail was: $exitStatus.

The full error message was:

${errorReport}
@@ -27,7 +27,7 @@

nf-core/dmscore execution completed un } else { out << """
- nf-core/dmscore execution completed successfully! + nf-core/deepmutscan execution completed successfully!
""" } @@ -44,8 +44,8 @@

Pipeline Configuration:

-

nf-core/dmscore

-

https://github.com/nf-core/dmscore

+

nf-core/deepmutscan

+

https://github.com/nf-core/deepmutscan

diff --git a/assets/email_template.txt b/assets/email_template.txt index 4247bc4..907c089 100644 --- a/assets/email_template.txt +++ b/assets/email_template.txt @@ -4,15 +4,15 @@ |\\ | |__ __ / ` / \\ |__) |__ } { | \\| | \\__, \\__/ | \\ |___ \\`-._,-`-, `._,._,' - nf-core/dmscore ${version} + nf-core/deepmutscan ${version} ---------------------------------------------------- Run Name: $runName <% if (success){ - out << "## nf-core/dmscore execution completed successfully! ##" + out << "## nf-core/deepmutscan execution completed successfully! ##" } else { out << """#################################################### -## nf-core/dmscore execution completed unsuccessfully! ## +## nf-core/deepmutscan execution completed unsuccessfully! ## #################################################### The exit status of the task that caused the workflow execution to fail was: $exitStatus. The full error message was: @@ -35,5 +35,5 @@ Pipeline Configuration: <% out << summary.collect{ k,v -> " - $k: $v" }.join("\n") %> -- -nf-core/dmscore -https://github.com/nf-core/dmscore +nf-core/deepmutscan +https://github.com/nf-core/deepmutscan diff --git a/assets/error_correction_report/ec_report_template.html b/assets/error_correction_report/ec_report_template.html new file mode 100644 index 0000000..70f4708 --- /dev/null +++ b/assets/error_correction_report/ec_report_template.html @@ -0,0 +1,1839 @@ + + + + + + nf-core/deepmutscan Β· sequencing-error correction Β· __SAMPLE__ + + + +
+
+

Sequencing-error correction

+ sample + +
+

+ +
+ + +
+ 1 Β· COMPARE +

+ Reproducibility of the error signature + +

+
+

+ Where errors recur. The upper triangle scores how strongly two files agree on which variants are error-prone; + the lower triangle shows each pair as a scatter (click to enlarge). Higher agreement means the correction + reflects sequencing chemistry rather than chance. +

+
+
+
+ + +
+ 2 Β· SELECTION +

+ Raw versus corrected frequency + +

+
+

+ Effect of correction on measured variants, split by input and output libraries. Toggle files; with two or more, + the bars span their spread. +

+
+ + + +
+
+ 3 Β· DETAIL +

+ One sequencing file in detail + +

+
+

Choose one file; every panel below recomputes for it alone.

+
+
+ +
+
+
+
+
+ Sequencing-error frequency by substitution class + +
+
+
+
+
+ Correction magnitude + +
+
+
+
+
+ +
+

+ Where errors concentrate along the ORF + +

+

Fraction of counts removed at each residue position.

+
+
+ +
+

+ Single-nucleotide substitutions by position + +

+
+ + +
+
+ + + + + + + + + + + + + +
PositionSubstitutionResidue changeClassRaw freqCorrected freq% change
+
+
+ +
nf-core/deepmutscan Β· sequencing-error correction report
+
+ +
+
+ + + + + diff --git a/assets/methods_description_template.yml b/assets/methods_description_template.yml index e4e7ab3..b15a7ba 100644 --- a/assets/methods_description_template.yml +++ b/assets/methods_description_template.yml @@ -1,29 +1,23 @@ -id: "nf-core-dmscore-methods-description" +id: "nf-core-deepmutscan-methods-description" description: "Suggested text and references to use when describing pipeline usage within the methods section of a publication." -section_name: "nf-core/dmscore Methods Description" -section_href: "https://github.com/nf-core/dmscore" +section_name: "nf-core/deepmutscan Methods Description" +section_href: "https://github.com/nf-core/deepmutscan" plot_type: "html" -## TODO nf-core: Update the HTML below to your preferred methods description, e.g. add publication citation for this pipeline ## You inject any metadata in the Nextflow '${workflow}' object data: |

Methods

-

Data was processed using nf-core/dmscore v${workflow.manifest.version} ${doi_text} of the nf-core collection of workflows (Ewels et al., 2020), utilising reproducible software environments from the Bioconda (GrΓΌning et al., 2018) and Biocontainers (da Veiga Leprevost et al., 2017) projects.

-

The pipeline was executed with Nextflow v${workflow.nextflow.version} (Di Tommaso et al., 2017) with the following command:

+

Data was processed using nf-core/deepmutscan v${workflow.manifest.version} ${doi_text} (Wehnert et al., in preparation), part of the nf-core collection of workflows (Ewels et al., 2020), with Nextflow v${workflow.nextflow.version} and the following command:

${workflow.commandLine}
-

${tool_citations}

References

    -
  • Di Tommaso, P., Chatzou, M., Floden, E. W., Barja, P. P., Palumbo, E., & Notredame, C. (2017). Nextflow enables reproducible computational workflows. Nature Biotechnology, 35(4), 316-319. doi: 10.1038/nbt.3820
  • +
  • Wehnert, B., et al. nf-core/deepmutscan. bioRxiv preprint (in preparation).
  • Ewels, P. A., Peltzer, A., Fillinger, S., Patel, H., Alneberg, J., Wilm, A., Garcia, M. U., Di Tommaso, P., & Nahnsen, S. (2020). The nf-core framework for community-curated bioinformatics pipelines. Nature Biotechnology, 38(3), 276-278. doi: 10.1038/s41587-020-0439-x
  • -
  • GrΓΌning, B., Dale, R., SjΓΆdin, A., Chapman, B. A., Rowe, J., Tomkins-Tinch, C. H., Valieris, R., KΓΆster, J., & Bioconda Team. (2018). Bioconda: sustainable and comprehensive software distribution for the life sciences. Nature Methods, 15(7), 475–476. doi: 10.1038/s41592-018-0046-7
  • -
  • da Veiga Leprevost, F., GrΓΌning, B. A., Alves Aflitos, S., RΓΆst, H. L., Uszkoreit, J., Barsnes, H., Vaudel, M., Moreno, P., Gatto, L., Weber, J., Bai, M., Jimenez, R. C., Sachsenberg, T., Pfeuffer, J., Vera Alvarez, R., Griss, J., Nesvizhskii, A. I., & Perez-Riverol, Y. (2017). BioContainers: an open-source and community-driven framework for software standardization. Bioinformatics (Oxford, England), 33(16), 2580–2582. doi: 10.1093/bioinformatics/btx192
  • - ${tool_bibliography}
Notes:
    ${nodoi_text}
  • The command above does not include parameters contained in any configs or profiles that may have been used. Ensure the config file is also uploaded with your publication!
  • -
  • You should also cite all software used within this run. Check the "Software Versions" of this report to get version information.
  • +
  • Software versions of all tools used in this run are listed in the "Software Versions" section of this report.
diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml index 2f6ceb9..bcfed63 100644 --- a/assets/multiqc_config.yml +++ b/assets/multiqc_config.yml @@ -1,13 +1,11 @@ report_comment: > - This report has been generated by the nf-core/dmscore - analysis pipeline. For information about how to interpret these results, please see the - documentation. + This report has been generated by the nf-core/deepmutscan analysis pipeline. For information about how to interpret these results, please see the documentation. report_section_order: - "nf-core-dmscore-methods-description": + "nf-core-deepmutscan-methods-description": order: -1000 software_versions: order: -1001 - "nf-core-dmscore-summary": + "nf-core-deepmutscan-summary": order: -1002 export_plots: true diff --git a/assets/nf-core-deepmutscan_logo_light.png b/assets/nf-core-deepmutscan_logo_light.png new file mode 100644 index 0000000..d6c8e55 Binary files /dev/null and b/assets/nf-core-deepmutscan_logo_light.png differ diff --git a/assets/nf-core-dmscore_logo_light.png b/assets/nf-core-dmscore_logo_light.png deleted file mode 100644 index 3faa4ca..0000000 Binary files a/assets/nf-core-dmscore_logo_light.png and /dev/null differ diff --git a/assets/samplesheet.csv b/assets/samplesheet.csv index 5f653ab..5b5d503 100644 --- a/assets/samplesheet.csv +++ b/assets/samplesheet.csv @@ -1,3 +1,5 @@ -sample,fastq_1,fastq_2 -SAMPLE_PAIRED_END,/path/to/fastq/files/AEG588A1_S1_L002_R1_001.fastq.gz,/path/to/fastq/files/AEG588A1_S1_L002_R2_001.fastq.gz -SAMPLE_SINGLE_END,/path/to/fastq/files/AEG588A4_S4_L003_R1_001.fastq.gz, +sample,type,replicate,file1,file2 +ORF1,input,1,/reads/forward1.fastq.gz,/reads/reverse1.fastq.gz +ORF1,input,2,/reads/forward2.fastq.gz,/reads/reverse2.fastq.gz +ORF1,output,1,/reads/forward3.fastq.gz,/reads/reverse3.fastq.gz +ORF1,output,2,/reads/forward4.fastq.gz,/reads/reverse4.fastq.gz diff --git a/assets/schema_input.json b/assets/schema_input.json index 2d9d8f6..88afb3a 100644 --- a/assets/schema_input.json +++ b/assets/schema_input.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/nf-core/dmscore/master/assets/schema_input.json", - "title": "nf-core/dmscore pipeline - params.input schema", + "$id": "https://raw.githubusercontent.com/nf-core/deepmutscan/master/assets/schema_input.json", + "title": "nf-core/deepmutscan pipeline - params.input schema", "description": "Schema for the file provided with params.input", "type": "array", "items": { @@ -9,25 +9,46 @@ "properties": { "sample": { "type": "string", - "pattern": "^\\S+$", - "errorMessage": "Sample name must be provided and cannot contain spaces", + "pattern": "^[^\\s/]+$", + "errorMessage": "Sample name must be provided, cannot contain spaces, and must not include special characters", "meta": ["id"] }, - "fastq_1": { + "file1": { "type": "string", "format": "file-path", "exists": true, - "pattern": "^\\S+\\.f(ast)?q\\.gz$", - "errorMessage": "FastQ file for reads 1 must be provided, cannot contain spaces and must have extension '.fq.gz' or '.fastq.gz'" + "pattern": "^\\S+\\.(bam|fastq|fq|fastq\\.gz|fq\\.gz)$", + "allOf": [ + { + "if": { + "pattern": "^\\S+\\.bam$" + }, + "then": { + "pattern": "^\\S+_(pe|se)\\.bam$", + "errorMessage": "If file1 ends with .bam, it must contain '_pe.bam' or '_se.bam', defining paired-end or single-end" + } + } + ], + "errorMessage": "File 1 must be provided, cannot contain spaces, and must have an allowed extension (.bam, .fastq, .fq, .fastq.gz, .fq.gz)" }, - "fastq_2": { - "type": "string", + "file2": { + "type": ["string", "null"], "format": "file-path", "exists": true, - "pattern": "^\\S+\\.f(ast)?q\\.gz$", - "errorMessage": "FastQ file for reads 2 cannot contain spaces and must have extension '.fq.gz' or '.fastq.gz'" + "pattern": "^\\S+\\.(fastq|fq|fastq\\.gz|fq\\.gz)$", + "errorMessage": "File 2 must have an allowed extension (.fastq, .fq, .fastq.gz, .fq.gz) or be null for single-end reads" + }, + "type": { + "type": "string", + "enum": ["input", "output", "quality", "wildtype"], + "errorMessage": "Type must be one of: input, output, quality, or wildtype" + }, + "replicate": { + "type": "integer", + "minimum": 1, + "errorMessage": "Replicate must be a positive integer" } }, - "required": ["sample", "fastq_1"] + "required": ["sample", "file1", "type", "replicate"] } } diff --git a/assets/sendmail_template.txt b/assets/sendmail_template.txt index cdd2e19..4c84df3 100644 --- a/assets/sendmail_template.txt +++ b/assets/sendmail_template.txt @@ -9,12 +9,12 @@ Content-Type: text/html; charset=utf-8 $email_html --nfcoremimeboundary -Content-Type: image/png;name="nf-core-dmscore_logo.png" +Content-Type: image/png;name="nf-core-deepmutscan_logo.png" Content-Transfer-Encoding: base64 Content-ID: -Content-Disposition: inline; filename="nf-core-dmscore_logo_light.png" +Content-Disposition: inline; filename="nf-core-deepmutscan_logo_light.png" -<% out << new File("$projectDir/assets/nf-core-dmscore_logo_light.png"). +<% out << new File("$projectDir/assets/nf-core-deepmutscan_logo_light.png"). bytes. encodeBase64(). toString(). diff --git a/assets/slackreport.json b/assets/slackreport.json deleted file mode 100644 index e5aa3f8..0000000 --- a/assets/slackreport.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "attachments": [ - { - "fallback": "Plain-text summary of the attachment.", - "color": "<% if (success) { %>good<% } else { %>danger<%} %>", - "author_name": "nf-core/dmscore ${version} - ${runName}", - "author_icon": "https://www.nextflow.io/docs/latest/_static/favicon.ico", - "text": "<% if (success) { %>Pipeline completed successfully!<% } else { %>Pipeline completed with errors<% } %>", - "fields": [ - { - "title": "Command used to launch the workflow", - "value": "```${commandLine}```", - "short": false - } - <% - if (!success) { %> - , - { - "title": "Full error message", - "value": "```${errorReport}```", - "short": false - }, - { - "title": "Pipeline configuration", - "value": "<% out << summary.collect{ k,v -> k == "hook_url" ? "_${k}_: (_hidden_)" : ( ( v.class.toString().contains('Path') || ( v.class.toString().contains('String') && v.contains('/') ) ) ? "_${k}_: `${v}`" : (v.class.toString().contains('DateTime') ? ("_${k}_: " + v.format(java.time.format.DateTimeFormatter.ofLocalizedDateTime(java.time.format.FormatStyle.MEDIUM))) : "_${k}_: ${v}") ) }.join(",\n") %>", - "short": false - } - <% } - %> - ], - "footer": "Completed at <% out << dateComplete.format(java.time.format.DateTimeFormatter.ofLocalizedDateTime(java.time.format.FormatStyle.MEDIUM)) %> (duration: ${duration})" - } - ] -} diff --git a/assets/summary_report/summary_report_template.html b/assets/summary_report/summary_report_template.html new file mode 100644 index 0000000..16dbfb1 --- /dev/null +++ b/assets/summary_report/summary_report_template.html @@ -0,0 +1,1948 @@ + + + + + + nf-core/deepmutscan Β· report Β· __SAMPLE__ + + + +
+ + report + + + +
+ +
+ +
+
+ +
+ + + + + + + diff --git a/assets/variant_effect_inspection_tool/3Dmol-min.js b/assets/variant_effect_inspection_tool/3Dmol-min.js new file mode 100644 index 0000000..c63bd14 --- /dev/null +++ b/assets/variant_effect_inspection_tool/3Dmol-min.js @@ -0,0 +1,2 @@ +/*! For license information please see 3Dmol-min.js.LICENSE.txt */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports["3Dmol"]=e():t["3Dmol"]=e()}(this,(()=>(()=>{var __webpack_modules__={620:function(){"use strict";!function(t){if(t.TextEncoder&&t.TextDecoder)return!1;function e(t="utf-8"){if("utf-8"!==t)throw new RangeError(`Failed to construct 'TextEncoder': The encoding label provided ('${t}') is invalid.`)}function i(t="utf-8",e={fatal:!1}){if("utf-8"!==t)throw new RangeError(`Failed to construct 'TextDecoder': The encoding label provided ('${t}') is invalid.`);if(e.fatal)throw new Error("Failed to construct 'TextDecoder': the 'fatal' option is unsupported.")}Object.defineProperty(e.prototype,"encoding",{value:"utf-8"}),e.prototype.encode=function(t,e={stream:!1}){if(e.stream)throw new Error("Failed to encode: the 'stream' option is unsupported.");let i=0;const r=t.length;let s=0,n=Math.max(32,r+(r>>1)+7),a=new Uint8Array(n>>3<<3);for(;i=55296&&e<=56319){if(i=55296&&e<=56319)continue}if(s+4>a.length){n+=8,n*=1+i/t.length*2,n=n>>3<<3;const e=new Uint8Array(n);e.set(a),a=e}if(4294967168&e){if(4294965248&e)if(4294901760&e){if(4292870144&e)continue;a[s++]=e>>18&7|240,a[s++]=e>>12&63|128,a[s++]=e>>6&63|128}else a[s++]=e>>12&15|224,a[s++]=e>>6&63|128;else a[s++]=e>>6&31|192;a[s++]=63&e|128}else a[s++]=e}return a.slice(0,s)},Object.defineProperty(i.prototype,"encoding",{value:"utf-8"}),Object.defineProperty(i.prototype,"fatal",{value:!1}),Object.defineProperty(i.prototype,"ignoreBOM",{value:!1}),i.prototype.decode=function(t,e={stream:!1}){if(e.stream)throw new Error("Failed to decode: the 'stream' option is unsupported.");const i=new Uint8Array(t);let r=0;const s=i.length,n=[];for(;r65535&&(e-=65536,n.push(e>>>10&1023|55296),e=56320|1023&e),n.push(e)}}else n.push(t)}return String.fromCharCode.apply(null,n)},t.TextEncoder=e,t.TextDecoder=i}("undefined"!=typeof window?window:"undefined"!=typeof self?self:this)},546:(t,e,i)=>{"use strict";i.r(e),i.d(e,{CustomLinear:()=>CustomLinear,Gradient:()=>Gradient,GradientType:()=>GradientType,ROYGB:()=>ROYGB,RWB:()=>RWB,Sinebow:()=>Sinebow,builtinGradients:()=>a,getGradient:()=>n,normalizeValue:()=>s});var r=i(222);class GradientType{}function s(t,e,i){return e>=t?(ie&&(i=e),{lo:t,hi:e,val:i}):(i>t&&(i=t),i=2?(this.max=t[1],this.min=t[0]):t&&e&&!Array.isArray(t)&&(this.min=t,this.max=e)}range(){return void 0!==this.min&&void 0!==this.max?[this.min,this.max]:null}valueToHex(t,e){var i,r;if(t=this.mult*t,e?(i=e[0],r=e[1]):(i=this.min,r=this.max),void 0===t)return 16777215;var n=s(i,r,t);i=n.lo;var a,o=((r=n.hi)+i)/2;return(t=n.val)<(o=e&&void 0!==e[2]?e[2]:void 0!==this.mid?this.mid:(i+r)/2)?16711680+256*(a=Math.floor(255*Math.sqrt((t-i)/(o-i))))+a:t>o?65536*(a=Math.floor(255*Math.sqrt(1-(t-o)/(r-o))))+256*a+255:16777215}}class ROYGB extends GradientType{constructor(t,e){super(),this.gradient="ROYGB",this.mult=1,this.min=t,this.max=e,void 0===e&&Array.isArray(t)&&t.length>=2?(this.max=t[1],this.min=t[0]):t&&e&&!Array.isArray(t)&&(this.min=t,this.max=e)}valueToHex(t,e){var i,r;if(t=this.mult*t,e?(i=e[0],r=e[1]):(i=this.min,r=this.max),void 0===t)return 16777215;var n=s(i,r,t),a=((i=n.lo)+(r=n.hi))/2,o=(i+a)/2,l=(a+r)/2;return(t=n.val)=2&&(this.max=t[1],this.min=t[0]),e=2?(this.max=t[1],this.min=t[0],s=e):(this.min=t,this.max=e,s=i),s)for(let t of s)this.colors.push(r.CC.color(t));else this.colors.push(r.CC.color(0))}range(){return void 0!==this.min&&void 0!==this.max?[this.min,this.max]:null}valueToHex(t,e){var i,n;if(e?(i=e[0],n=e[1]):(i=this.min,n=this.max),void 0===t)return 16777215;var a=s(i,n,t);i=a.lo,n=a.hi,t=a.val;let o=this.colors.length,l=(n-i)/o,h=Math.min(Math.floor((t-i)/l),o-1),c=Math.min(h+1,o-1),d=(t-i-h*l)/l,u=this.colors[h],f=this.colors[c];return new r.Color(u.r+d*(f.r-u.r),u.g+d*(f.g-u.g),u.b+d*(f.b-u.b)).getHex()}}const a={rwb:RWB,RWB,roygb:ROYGB,ROYGB,sinebow:Sinebow,linear:CustomLinear};class Gradient extends GradientType{valueToHex(t,e){return 0}range(){return null}}Gradient.RWB=RWB,Gradient.ROYGB=ROYGB,Gradient.Sinebow=Sinebow,Gradient.CustomLinear=CustomLinear,Gradient.builtinGradients=a,Gradient.normalizeValue=s,Gradient.getGradient=n},848:(t,e,i)=>{"use strict";i.r(e),i.d(e,{VolumeData:()=>VolumeData});var r=i(864),s=i(529),n=i(797),a=i(865),o=i(75);class VolumeData{constructor(t,e,i){if(this.unit={x:1,y:1,z:1},this.origin={x:0,y:0,z:0},this.size={x:0,y:0,z:0},this.data=new Float32Array([]),this.matrix=null,this.inversematrix=null,this.isbinary=new Set(["ccp4","CCP4"]),this.getCoordinates=function(t){var e=t/(this.size.y*this.size.z),i=t%(this.size.y*this.size.z),r=t%this.size.z;return e*=this.unit.x,i*=this.unit.y,r*=this.unit.z,{x:e+=this.origin.x,y:i+=this.origin.y,z:r+=this.origin.z}},this.vasp=function(t){var e=t.replace(/^\s+/,"").split(/[\n\r]/),i=(0,n.VASP)(t)[0].length;if(0==i)return console.warn("No good formating of CHG or CHGCAR file, not atomic information provided in the file."),void(this.data=[]);var r,a=1.889725992,o=parseFloat(e[1]);r=e[2].replace(/^\s+/,"").split(/\s+/);var l=new s.Vector3(parseFloat(r[0]),parseFloat(r[1]),parseFloat(r[2])).multiplyScalar(o*a);r=e[3].replace(/^\s+/,"").split(/\s+/);var h=new s.Vector3(parseFloat(r[0]),parseFloat(r[1]),parseFloat(r[2])).multiplyScalar(o*a);r=e[4].replace(/^\s+/,"").split(/\s+/);var c=new s.Vector3(parseFloat(r[0]),parseFloat(r[1]),parseFloat(r[2])).multiplyScalar(o*a),d=l.x*(h.y*c.z-c.y*h.z)-h.x*(l.y*c.z-c.y*l.z)+c.x*(l.y*h.z-h.y*l.z),u=1/(d=Math.abs(d)/Math.pow(a,3));e.splice(0,8+i+1);var f=e[0].replace(/^\s+/,"").replace(/\s+/g," ").split(" "),p=Math.abs(parseFloat(f[0])),g=Math.abs(parseFloat(f[1])),m=Math.abs(parseFloat(f[2])),v=this.origin=new s.Vector3(0,0,0);this.size={x:p,y:g,z:m},this.unit=new s.Vector3(l.x,h.y,c.z),l=l.multiplyScalar(1/(a*p)),h=h.multiplyScalar(1/(a*g)),c=c.multiplyScalar(1/(a*m)),0==l.y&&0==l.z&&0==h.x&&0==h.z&&0==c.x&&0==c.y||(this.matrix=new s.Matrix4(l.x,h.x,c.x,0,l.y,h.y,c.y,0,l.z,h.z,c.z,0,0,0,0,1),this.matrix=this.matrix.multiplyMatrices(this.matrix,(new s.Matrix4).makeTranslation(v.x,v.y,v.z)),this.origin=new s.Vector3(0,0,0),this.unit=new s.Vector3(1,1,1)),e.splice(0,1);var _=e.join(" "),y=(_=_.replace(/^\s+/,"")).split(/[\s\r]+/);y.splice(p*g*m+1);for(var b=Float32Array.from(y,parseFloat),x=0;x=this.size.x||e<0||e>=this.size.y||i<0||i>=this.size.z?-1:t*this.size.y*this.size.z+e*this.size.z+i}getVal(t,e,i){let r=this.getIndex(t,e,i);return r<0?0:this.data[r]}cube(t){var e=t.split(/\r?\n/);if(!(e.length<6)){var i=(0,a.CUBE)(t,{}).modelData[0].cryst,r=e[2].replace(/^\s+/,"").replace(/\s+/g," ").split(" "),s=parseFloat(r[0]),n=Math.abs(s);this.origin=i.origin,this.size=i.size,this.unit=i.unit,this.matrix=i.matrix4;var o=6;s<0&&o++;var l=e.splice(n+o).join(" "),h=(l=l.replace(/^\s+/,"")).split(/[\s\r]+/);this.data=Float32Array.from(h,parseFloat)}}ccp4(t){var e={};t=new Int8Array(t);var i=new Int32Array(t.buffer,0,56),r=new Float32Array(t.buffer,0,56),n=new DataView(t.buffer);if(e.MAP=String.fromCharCode(n.getUint8(208),n.getUint8(209),n.getUint8(210),n.getUint8(211)),e.MACHST=[n.getUint8(212),n.getUint8(213)],17===e.MACHST[0]&&17===e.MACHST[1])for(var a=t.byteLength,o=0;o{"use strict";var r;i.r(e),i.d(e,{BackSide:()=>n,Camera:()=>Camera,ClampToEdgeWrapping:()=>c,Coloring:()=>r,Cylinder:()=>M.Cylinder,DoubleSide:()=>a,EventDispatcher:()=>EventDispatcher,FloatType:()=>g,Fog:()=>Fog,FrontSide:()=>s,Geometry:()=>Geometry,GeometryGroup:()=>GeometryGroup,GeometryIDCount:()=>A,ImposterMaterial:()=>ImposterMaterial,InstancedMaterial:()=>InstancedMaterial,Light:()=>Light,Line:()=>Line,LineBasicMaterial:()=>LineBasicMaterial,LineStyle:()=>N,LinearFilter:()=>d,LinearMipMapLinearFilter:()=>f,Material:()=>Material,MaterialIdCount:()=>x,Matrix3:()=>l.Matrix3,Matrix4:()=>l.Matrix4,Mesh:()=>Mesh,MeshDoubleLambertMaterial:()=>MeshDoubleLambertMaterial,MeshLambertMaterial:()=>MeshLambertMaterial,MeshOutlineMaterial:()=>MeshOutlineMaterial,NearestFilter:()=>u,Object3D:()=>Object3D,Object3DIDCount:()=>C,Projector:()=>Projector,Quaternion:()=>l.Quaternion,R32Format:()=>_,RFormat:()=>v,RGBAFormat:()=>m,Ray:()=>l.Ray,Raycaster:()=>Raycaster,Renderer:()=>Renderer,Scene:()=>Scene,ShaderLib:()=>ct,ShaderUtils:()=>ut,Shading:()=>o,Sphere:()=>M.Sphere,SphereImposterMaterial:()=>SphereImposterMaterial,SphereImposterOutlineMaterial:()=>SphereImposterOutlineMaterial,Sprite:()=>Sprite,SpriteAlignment:()=>h,SpriteMaterial:()=>SpriteMaterial,SpritePlugin:()=>SpritePlugin,StickImposterMaterial:()=>StickImposterMaterial,StickImposterOutlineMaterial:()=>StickImposterOutlineMaterial,Texture:()=>Texture,TextureIdCount:()=>B,TextureOperations:()=>y,Triangle:()=>M.Triangle,UVMapping:()=>UVMapping,UnsignedByteType:()=>p,Vector2:()=>l.Vector2,Vector3:()=>l.Vector3,VolumetricMaterial:()=>VolumetricMaterial,basic:()=>V,clamp:()=>l.clamp,clone:()=>dt,conversionMatrix3:()=>l.conversionMatrix3,degToRad:()=>l.degToRad,instanced:()=>H,intersectObject:()=>U,lambert:()=>q,lambertdouble:()=>Z,outline:()=>K,screen:()=>Q,screenaa:()=>$,sphereimposter:()=>tt,sphereimposteroutline:()=>it,sprite:()=>rt,stickimposter:()=>at,stickimposteroutline:()=>ot,volumetric:()=>ht}),function(t){t[t.NoColors=0]="NoColors",t[t.FaceColors=1]="FaceColors",t[t.VertexColors=2]="VertexColors"}(r||(r={}));const s=0,n=1,a=2;var o;!function(t){t[t.NoShading=0]="NoShading",t[t.FlatShading=1]="FlatShading",t[t.SmoothShading=2]="SmoothShading"}(o||(o={}));var l=i(529);const h={topLeft:new l.Vector2(1,-1),topCenter:new l.Vector2(0,-1),topRight:new l.Vector2(-1,-1),centerLeft:new l.Vector2(1,0),center:new l.Vector2(0,0),centerRight:new l.Vector2(-1,0),bottomLeft:new l.Vector2(1,1),bottomCenter:new l.Vector2(0,1),bottomRight:new l.Vector2(-1,1)},c=1001,d=1006,u=1007,f=1008,p=1009,g=1010,m=1021,v=1022,_=1023;var y;!function(t){t[t.MultiplyOperation=0]="MultiplyOperation",t[t.MixOperation=1]="MixOperation",t[t.AddOperation=2]="AddOperation"}(y||(y={}));class EventDispatcher{constructor(){this.listeners={}}dispatchEvent(t){var e=this.listeners[t.type];if(void 0!==e){t.target=this;for(var i=0,r=e.length;i0?this.lineArray=(null==a?void 0:a.subarray(0,this.lineidx))||null:this.lineArray=new Uint16Array(0)):(this.normalArray=new Float32Array(0),this.faceArray=new Uint16Array(0),this.lineArray=new Uint16Array(0)),o&&(this.radiusArray=o.subarray(0,this.vertices)),e&&(this.normalArray&&(this.normalArray=new Float32Array(this.normalArray)),this.faceArray&&(this.faceArray=new Uint16Array(this.faceArray)),this.lineArray&&(this.lineArray=new Uint16Array(this.lineArray)),this.vertexArray&&(this.vertexArray=new Float32Array(this.vertexArray)),this.colorArray&&(this.colorArray=new Float32Array(this.colorArray)),this.radiusArray&&(this.radiusArray=new Float32Array(this.radiusArray))),this.__inittedArrays=!0}}class Geometry extends EventDispatcher{constructor(t=!1,e=!1,i=!1){super(),this.name="",this.hasTangents=!1,this.dynamic=!0,this.verticesNeedUpdate=!1,this.elementsNeedUpdate=!1,this.normalsNeedUpdate=!1,this.colorsNeedUpdate=!1,this.buffersNeedUpdate=!1,this.imposter=!1,this.instanced=!1,this.geometryGroups=[],this.groups=0,this.id=A++,this.mesh=t,this.radii=e,this.offset=i}updateGeoGroup(t=0){var e,i=this.groups>0?this.geometryGroups[this.groups-1]:null;return(!i||i.vertices+t>((null===(e=null==i?void 0:i.vertexArray)||void 0===e?void 0:e.length)||0)/3)&&(i=this.addGeoGroup()),i}vrml(t,e){for(var i="",r=this.geometryGroups.length,s=0;st.distance-e.distance,T=new l.Matrix4;class Raycaster{constructor(t,e,i,r){this.precision=1e-4,this.linePrecision=.2,this.ray=new l.Ray(t,e),this.ray.direction.lengthSq()>0&&this.ray.direction.normalize(),this.near=r||0,this.far=i||1/0}set(t,e){this.ray.set(t,e)}setFromCamera(t,e){e.ortho?(this.ray.origin.set(t.x,t.y,(e.near+e.far)/(e.near-e.far)).unproject(e),this.ray.direction.set(0,0,-1).transformDirection(e.matrixWorld)):(this.ray.origin.setFromMatrixPosition(e.matrixWorld),this.ray.direction.set(t.x,t.y,t.z),e.projectionMatrixInverse.getInverse(e.projectionMatrix),T.multiplyMatrices(e.matrixWorld,e.projectionMatrixInverse),this.ray.direction.applyProjection(T),this.ray.direction.sub(this.ray.origin).normalize())}intersectObjects(t,e){for(var i=[],r=0,s=e.length;rMath.min(Math.max(t,-1),1);var L=new M.Sphere,F=new M.Cylinder,I=new M.Triangle,O=new l.Vector3,D=new l.Vector3,k=new l.Vector3,R=new l.Vector3,P=new l.Vector3;function U(t,e,i,r){if(P.getPositionFromMatrix(t.matrixWorld),void 0===e.intersectionShape)return r;var s,n,a,o,l,h,c,d,u,f,p,g,m,v,_=e.intersectionShape,y=i.linePrecision,b=(y*=t.matrixWorld.getMaxScaleOnAxis())*y;if(void 0!==e.boundingSphere&&e.boundingSphere instanceof M.Sphere&&(L.copy(e.boundingSphere),L.applyMatrix4(t.matrixWorld),!i.ray.isIntersectionSphere(L)))return r;for(s=0,n=_.triangle.length;s=0)continue;if(O.subVectors(I.a,i.ray.origin),(c=a.dot(O)/o)<0)continue;D.copy(i.ray.direction).multiplyScalar(c).add(i.ray.origin),D.sub(I.a),k.copy(I.b).sub(I.a),R.copy(I.c).sub(I.a);var x=k.dot(R),w=k.lengthSq(),A=R.lengthSq();if((g=(w*D.dot(R)-x*D.dot(k))/(w*A-x*x))<0||g>1)continue;if((p=(D.dot(k)-g*x)/w)<0||p>1||p+g>1)continue;r.push({clickable:e,distance:c})}for(s=0,n=_.cylinder.length;sF.lengthSq()||g<0)continue;r.push({clickable:e,distance:c})}}for(s=0,n=_.line.length;s 0.0) {\n vec4 unadjusted = projectionMatrix*modelViewMatrix * vec4( position, 1.0 );\n float w = outpos.w;\n //normalize homogeneous coords\n unadjusted /= unadjusted.w;\n outpos /= outpos.w;\n vec2 diff = outpos.xy-unadjusted.xy;\n //put into pixels\n diff.x *= vWidth;\n diff.y *= vHeight;\n if ( length(diff) > outlineMaxPixels) {\n vec2 ndiff = normalize(diff)*outlineMaxPixels;\n ndiff.x /= vWidth;\n ndiff.y /= vHeight;\n outpos.xy = unadjusted.xy;\n outpos.xy += ndiff;\n }\n outpos *= w; //if I don't do this things blow up\n }\n gl_Position = outpos;\n mvPosition.z -= outlinePushback; //go backwards in model space\n vec4 pushpos = projectionMatrix*mvPosition; //project to get z in projection space, I'm probably missing some simple math to do the same thing..\n gl_Position.z = gl_Position.w*pushpos.z/pushpos.w;\n}\n\n".replace("#define GLSLIFY 1",""),uniforms:X},Q={fragmentShader:"uniform sampler2D colormap;\nvarying highp vec2 vTexCoords;\nuniform vec2 dimensions;\n//DEFINEFRAGCOLOR\nvoid main (void) {\n gl_FragColor = texture2D(colormap, vTexCoords);\n\n //gl_FragColor.g = gl_FragColor.b = gl_FragColor.r; //debug shading \n}\n ".replace("#define GLSLIFY 1",""),vertexShader:"attribute vec2 vertexPosition;\nvarying highp vec2 vTexCoords;\nconst vec2 scale = vec2(0.5, 0.5);\n\nvoid main() {\n vTexCoords = vertexPosition * scale + scale; // scale vertex attribute to [0,1] range\n gl_Position = vec4(vertexPosition, 0.0, 1.0);\n}\n ".replace("#define GLSLIFY 1",""),uniforms:{}},$={fragmentShader:"uniform highp sampler2D colormap;\nvarying highp vec2 vTexCoords;\n\n\n// Basic FXAA implementation based on the code on geeks3d.com \n#define FXAA_REDUCE_MIN (1.0/ 128.0)\n#define FXAA_REDUCE_MUL (1.0 / 8.0)\n#define FXAA_SPAN_MAX 8.0\n\n\nvec4 applyFXAA(vec2 fragCoord, highp sampler2D tex)\n{\n vec4 color;\n ivec2 dim = textureSize(tex,0);\n vec2 dimensions = vec2(float(dim.x),float(dim.y));\n vec2 inverseVP = vec2(1.0 / dimensions.x, 1.0 / dimensions.y);\n vec4 rgbNW = texture2D(tex, fragCoord + vec2(-1.0, -1.0) * inverseVP);\n vec4 rgbNE = texture2D(tex, fragCoord + vec2(1.0, -1.0) * inverseVP);\n vec4 rgbSW = texture2D(tex, fragCoord + vec2(-1.0, 1.0) * inverseVP);\n vec4 rgbSE = texture2D(tex, fragCoord + vec2(1.0, 1.0) * inverseVP);\n vec4 rgbM = texture2D(tex, fragCoord );\n vec3 luma = vec3(0.299, 0.587, 0.114);\n float lumaNW = dot(rgbNW.xyz, luma);\n float lumaNE = dot(rgbNE.xyz, luma);\n float lumaSW = dot(rgbSW.xyz, luma);\n float lumaSE = dot(rgbSE.xyz, luma);\n float lumaM = dot(rgbM.xyz, luma);\n float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE)));\n float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE)));\n\n vec2 dir;\n dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));\n dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE));\n\n float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) *\n (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN);\n\n float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce);\n dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX),\n max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX),\n dir * rcpDirMin)) * inverseVP;\n\n vec4 rgbA = 0.5 * (\n texture2D(tex, fragCoord + dir * (1.0 / 3.0 - 0.5)) +\n texture2D(tex, fragCoord + dir * (2.0 / 3.0 - 0.5)));\n vec4 rgbB = rgbA * 0.5 + 0.25 * (\n texture2D(tex, fragCoord + dir * -0.5) +\n texture2D(tex, fragCoord + dir * 0.5));\n\n float lumaB = dot(rgbB.xyz, luma);\n if ((lumaB < lumaMin) || (lumaB > lumaMax))\n color = rgbA;\n else\n color = rgbB;\n\n return color;\n}\n\n\n//DEFINEFRAGCOLOR\nvoid main (void) {\n ivec2 dim = textureSize(colormap,0);\n\n gl_FragColor = applyFXAA(vTexCoords, colormap);\n}\n ".replace("#define GLSLIFY 1",""),vertexShader:"attribute vec2 vertexPosition;\nvarying highp vec2 vTexCoords;\nconst vec2 scale = vec2(0.5, 0.5);\n\nvoid main() {\n vTexCoords = vertexPosition * scale + scale; // scale vertex attribute to [0,1] range\n gl_Position = vec4(vertexPosition, 0.0, 1.0);\n}\n ".replace("#define GLSLIFY 1",""),uniforms:{}},J={opacity:{type:"f",value:1},fogColor:{type:"c",value:new b.Color(1,1,1)},fogNear:{type:"f",value:1},fogFar:{type:"f",value:2e3},directionalLightColor:{type:"fv",value:[]},directionalLightDirection:{type:"fv",value:[]}},tt={vertexShader:"uniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat4 viewMatrix;\nuniform mat3 normalMatrix;\nuniform vec3 directionalLightColor[ 1 ];\nuniform vec3 directionalLightDirection[ 1 ];\n\nattribute vec3 position;\nattribute vec3 normal;\nattribute vec3 color;\n\nvarying vec2 mapping;\nvarying vec3 vColor;\nvarying float rval;\nvarying vec3 vLight;\nvarying vec3 center;\n\nvoid main() {\n\n vColor = color;\n vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );\n center = mvPosition.xyz;\n vec4 projPosition = projectionMatrix * mvPosition;\n vec4 adjust = projectionMatrix* vec4(normal,0.0); adjust.z = 0.0; adjust.w = 0.0;\n vec4 lDirection = viewMatrix * vec4( directionalLightDirection[ 0 ], 0.0 );\n vLight = normalize( lDirection.xyz );\n mapping = normal.xy;\n rval = abs(normal.x);\n gl_Position = projPosition+adjust;\n\n}\n".replace("#define GLSLIFY 1",""),fragmentShader:"\nuniform mat4 viewMatrix;\nuniform float opacity;\nuniform mat4 projectionMatrix;\n\nuniform vec3 fogColor;\nuniform float fogNear;\nuniform float fogFar;\nuniform float uDepth;\nuniform vec3 directionalLightColor[ 1 ];\n\nvarying vec3 vColor;\nvarying vec2 mapping;\nvarying float rval;\nvarying vec3 vLight;\nvarying vec3 center;\n\n#ifdef SHADED\nuniform highp sampler2D shading;\n#endif\n\n//DEFINEFRAGCOLOR\n\nvoid main() {\n float lensqr = dot(mapping,mapping);\n float rsqr = rval*rval;\n if(lensqr > rsqr)\n discard;\n float z = sqrt(rsqr-lensqr);\n vec3 cameraPos = center+ vec3(mapping.x,mapping.y,z);\n vec4 clipPos = projectionMatrix * vec4(cameraPos, 1.0);\n float ndcDepth = clipPos.z / clipPos.w;\n gl_FragDepthEXT = ((gl_DepthRange.diff * ndcDepth) + gl_DepthRange.near + gl_DepthRange.far) / 2.0;\n vec3 norm = normalize(vec3(mapping.x,mapping.y,z));\n float dotProduct = dot( norm, vLight );\n vec3 directionalLightWeighting = vec3( max( dotProduct, 0.0 ) );\n vec3 vLight = directionalLightColor[ 0 ] * directionalLightWeighting;\n vec3 color = vLight*vColor;\n#ifdef SHADED\n ivec2 dim = textureSize(shading,0);\n float shadowFactor = texture2D(shading,vec2(gl_FragCoord.x/float(dim.x),gl_FragCoord.y/float(dim.y))).r;\n color *= shadowFactor;\n#endif \n gl_FragColor = vec4(color, opacity*opacity );\n float fogFactor = smoothstep( fogNear, fogFar, gl_FragDepthEXT/gl_FragCoord.w );\n gl_FragColor = mix( gl_FragColor, vec4( fogColor, gl_FragColor.w ), fogFactor );\n}\n\n".replace("#define GLSLIFY 1",""),uniforms:J},et={opacity:{type:"f",value:1},outlineColor:{type:"c",value:new b.Color(0,0,0)},fogColor:{type:"c",value:new b.Color(1,1,1)},fogNear:{type:"f",value:1},fogFar:{type:"f",value:2e3},outlineWidth:{type:"f",value:.1},outlinePushback:{type:"f",value:1},outlineMaxPixels:{type:"f",value:0}},it={fragmentShader:"\n\nuniform float opacity;\nuniform vec3 outlineColor;\nuniform vec3 fogColor;\nuniform float fogNear;\nuniform float fogFar;\nuniform mat4 projectionMatrix;\nvarying vec2 mapping;\nvarying float rval;\nvarying vec3 center;\n\nuniform float outlinePushback;\n\n//DEFINEFRAGCOLOR\n\nvoid main() {\n float lensqr = dot(mapping,mapping);\n float rsqr = rval*rval;\n if(lensqr > rsqr)\n discard;\n float z = sqrt(rsqr-lensqr);\n vec3 cameraPos = center+ vec3(mapping.x,mapping.y,z-outlinePushback);\n vec4 clipPos = projectionMatrix * vec4(cameraPos, 1.0);\n float ndcDepth = clipPos.z / clipPos.w;\n gl_FragDepthEXT = ((gl_DepthRange.diff * ndcDepth) + gl_DepthRange.near + gl_DepthRange.far) / 2.0;\n gl_FragColor = vec4(outlineColor, 1 );\n}\n\n\n".replace("#define GLSLIFY 1",""),vertexShader:"\n\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform float outlineWidth;\nuniform float outlinePushback;\nuniform float outlineMaxPixels;\nuniform float vWidth;\nuniform float vHeight;\n\nattribute vec3 position;\nattribute vec3 normal;\nattribute vec3 color;\n\nvarying vec2 mapping;\nvarying float rval;\nvarying vec3 center;\n\nvoid main() {\n\n vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );\n center = mvPosition.xyz;\n vec4 projPosition = projectionMatrix * mvPosition;\n vec2 norm = normal.xy + vec2(sign(normal.x)*outlineWidth,sign(normal.y)*outlineWidth);\n\n vec4 adjust = projectionMatrix* vec4(norm,normal.z,1.0); \n mapping = norm.xy;\n rval = abs(norm.x);\n gl_Position = projPosition+vec4(adjust.xy,0.0,0.0);\n\n if(outlineMaxPixels > 0.0) {\n vec4 unadjusted = projectionMatrix*vec4(center.x+normal.x, center.y,center.z,1.0); \n vec4 ccoord = projectionMatrix*vec4(center.xyz,1.0);\n adjust = projectionMatrix* vec4(center.x+norm.x,center.y,center.z,1.0); \n //subtract center \n unadjusted.xyz -= ccoord.xyz;\n adjust.xyz -= ccoord.xyz;\n unadjusted /= unadjusted.w;\n adjust /= adjust.w;\n float diff = abs(adjust.x-unadjusted.x);\n diff *= vWidth;\n if(diff > outlineMaxPixels) {\n \n float fixlen = abs(unadjusted.x) + outlineMaxPixels/vWidth;\n //adjsut reval by ratio of lengths\n rval *= fixlen/abs(adjust.x);\n }\n\n }\n}\n\n".replace("#define GLSLIFY 1",""),uniforms:et},rt={fragmentShader:"\n\nuniform vec3 color;\nuniform sampler2D map;\nuniform float opacity;\n\nuniform int fogType;\nuniform vec3 fogColor;\nuniform float fogDensity;\nuniform float fogNear;\nuniform float fogFar;\nuniform float alphaTest;\n\nvarying vec2 vUV;\n//DEFINEFRAGCOLOR\n\nvoid main() {\n\n vec4 texture = texture2D(map, vUV);\n\n if (texture.a <= alphaTest) discard;\n\n gl_FragColor = vec4(color * texture.xyz, texture.a * opacity);\n\n if (fogType > 0) {\n\n float depth = gl_FragCoord.z / gl_FragCoord.w;\n float fogFactor = 0.0;\n\n if (fogType == 1) {\n fogFactor = smoothstep(fogNear, fogFar, depth);\n }\n\n else {\n const float LOG2 = 1.442695;\n float fogFactor = exp2(- fogDensity * fogDensity * depth * depth * LOG2);\n fogFactor = 1.0 - clamp(fogFactor, 0.0, 1.0);\n }\n\n gl_FragColor = mix(gl_FragColor, vec4(fogColor, gl_FragColor.w), fogFactor);\n\n }\n}\n\n".replace("#define GLSLIFY 1",""),vertexShader:"\n\nuniform int useScreenCoordinates;\nuniform vec3 screenPosition;\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform float rotation;\nuniform vec2 scale;\nuniform vec2 alignment;\nuniform vec2 uvOffset;\nuniform vec2 uvScale;\n\nattribute vec2 position;\nattribute vec2 uv;\n\nvarying vec2 vUV;\n\nvoid main() {\n\n vUV = uvOffset + uv * uvScale;\n\n vec2 alignedPosition = position + alignment;\n\n vec2 rotatedPosition;\n rotatedPosition.x = ( cos(rotation) * alignedPosition.x - sin(rotation) * alignedPosition.y ) * scale.x;\n rotatedPosition.y = ( sin(rotation) * alignedPosition.x + cos(rotation) * alignedPosition.y ) * scale.y;\n\n vec4 finalPosition;\n\n if(useScreenCoordinates != 0) {\n finalPosition = vec4(screenPosition.xy + rotatedPosition, screenPosition.z, 1.0);\n }\n\n else {\n finalPosition = projectionMatrix * modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0); finalPosition /= finalPosition.w;\n finalPosition.xy += rotatedPosition; \n }\n\n gl_Position = finalPosition;\n\n}\n\n".replace("#define GLSLIFY 1",""),uniforms:{}},st={opacity:{type:"f",value:1},fogColor:{type:"c",value:new b.Color(1,1,1)},fogNear:{type:"f",value:1},fogFar:{type:"f",value:2e3},directionalLightColor:{type:"fv",value:[]},directionalLightDirection:{type:"fv",value:[]}},nt="uniform float opacity;\nuniform mat4 projectionMatrix;\n\nuniform vec3 fogColor;\nuniform float fogNear;\nuniform float fogFar;\n\nvarying vec3 vLight;\nvarying vec3 vColor;\nvarying vec3 cposition;\nvarying vec3 p1;\nvarying vec3 p2;\nvarying float r;\n\n#ifdef SHADED\nuniform highp sampler2D shading;\n#endif\n\n//DEFINEFRAGCOLOR\n\n//cylinder-ray intersection testing taken from http://mrl.nyu.edu/~dzorin/cg05/lecture12.pdf\n//also useful: http://stackoverflow.com/questions/9595300/cylinder-impostor-in-glsl\n//with a bit more care (caps) this could be a general cylinder imposter (see also outline)\nvoid main() {\n vec3 color = abs(vColor);\n vec3 pos = cposition;\n vec3 p = pos; //ray point\n vec3 v = vec3(0.0,0.0,-1.0); //ray normal - orthographic\n if(projectionMatrix[3][3] == 0.0) v = normalize(pos); //ray normal - perspective\n vec3 pa = p1; //cyl start\n vec3 va = normalize(p2-p1); //cyl norm\n vec3 tmp1 = v-(dot(v,va)*va);\n vec3 deltap = p-pa;\n float A = dot(tmp1,tmp1);\n if(A == 0.0) discard;\n vec3 tmp2 = deltap-(dot(deltap,va)*va);\n float B = 2.0*dot(tmp1, tmp2);\n float C = dot(tmp2,tmp2)-r*r;\n//quadratic equation!\n float det = (B*B) - (4.0*A*C);\n if(det < 0.0) discard;\n float sqrtDet = sqrt(det);\n float posT = (-B+sqrtDet)/(2.0*A);\n float negT = (-B-sqrtDet)/(2.0*A);\n float intersectionT = min(posT,negT);\n vec3 qi = p+v*intersectionT;\n float dotp1 = dot(va,qi-p1);\n float dotp2 = dot(va,qi-p2);\n vec3 norm;\n if( dotp1 < 0.0 || dotp2 > 0.0) { //(p-c)^2 + 2(p-c)vt +v^2+t^2 - r^2 = 0\n vec3 cp;\n if( dotp1 < 0.0) { \n// if(vColor.x < 0.0 ) discard; //color sign bit indicates if we should cap or not\n cp = p1;\n } else {\n// if(vColor.y < 0.0 ) discard;\n cp = p2;\n }\n vec3 diff = p-cp;\n A = dot(v,v);\n B = dot(diff,v)*2.0;\n C = dot(diff,diff)-r*r;\n det = (B*B) - (4.0*C);\n if(det < 0.0) discard;\n sqrtDet = sqrt(det);\n posT = (-B+sqrtDet)/(2.0);\n negT = (-B-sqrtDet)/(2.0);\n float t = min(posT,negT);\n qi = p+v*t; \n norm = normalize(qi-cp); \n } else {\n norm = normalize(qi-(dotp1*va + p1));\n }\n vec4 clipPos = projectionMatrix * vec4(qi, 1.0);\n float ndcDepth = clipPos.z / clipPos.w;\n float depth = ((gl_DepthRange.diff * ndcDepth) + gl_DepthRange.near + gl_DepthRange.far) / 2.0;\n gl_FragDepthEXT = depth;",at={fragmentShader:[nt," float dotProduct = dot( norm, vLight );\n vec3 light = vec3( max( dotProduct, 0.0 ) );\n color *= light;\n#ifdef SHADED\n ivec2 dim = textureSize(shading,0);\n float shadowFactor = texture2D(shading,vec2(gl_FragCoord.x/float(dim.x),gl_FragCoord.y/float(dim.y))).r;\n color *= shadowFactor;\n#endif \n gl_FragColor = vec4(color, opacity*opacity );\n float fogFactor = smoothstep( fogNear, fogFar, depth );\n gl_FragColor = mix( gl_FragColor, vec4( fogColor, gl_FragColor.w ), fogFactor );\n}"].join("\n"),vertexShader:"\n\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat4 viewMatrix;\nuniform mat3 normalMatrix;\nuniform vec3 directionalLightColor[ 1 ];\nuniform vec3 directionalLightDirection[ 1 ];\n\nattribute vec3 position;\nattribute vec3 normal;\nattribute vec3 color;\nattribute float radius;\n\nvarying vec3 vColor;\nvarying vec3 vLight;\nvarying vec3 cposition;\nvarying vec3 p1;\nvarying vec3 p2;\nvarying float r;\n\nvoid main() {\n\n vColor = color; vColor.z = abs(vColor.z); //z indicates which vertex and so would vary\n r = abs(radius);\n vec4 to = modelViewMatrix*vec4(normal, 1.0); //normal is other point of cylinder\n vec4 pt = modelViewMatrix*vec4(position, 1.0);\n vec4 mvPosition = pt;\n p1 = pt.xyz; p2 = to.xyz;\n vec3 norm = to.xyz-pt.xyz;\n float mult = 1.1; //slop to account for perspective of sphere\n if(length(p1) > length(p2)) { //billboard at level of closest point\n mvPosition = to;\n }\n vec3 n = normalize(mvPosition.xyz);\n bool isperspective = (projectionMatrix[3][3] == 0.0);\n//intersect with the plane defined by the camera looking at the billboard point\n if(color.z >= 0.0) { //p1\n if(isperspective) { //perspective\n vec3 pnorm = normalize(p1);\n float t = dot(mvPosition.xyz-p1,n)/dot(pnorm,n);\n mvPosition.xyz = p1+t*pnorm; \n } else { //orthographic\n mvPosition.xyz = p1;\n }\n } else {\n if(isperspective) { //perspective\n vec3 pnorm = normalize(p2);\n float t = dot(mvPosition.xyz-p2,n)/dot(pnorm,n);\n mvPosition.xyz = p2+t*pnorm;\n } else { //orthographic\n mvPosition.xyz = p2;\n } \n mult *= -1.0;\n }\n\n if(isperspective) { //perspective\n vec3 cr = normalize(cross(mvPosition.xyz,norm))*radius;\n vec3 doublecr = normalize(cross(mvPosition.xyz,cr))*radius;\n mvPosition.xyz += mult*(cr + doublecr).xyz;\n } else {\n vec3 cr = normalize(cross(vec3(0.0,0.0,-1.0),norm))*radius;\n vec3 doublecr = normalize(cross(vec3(0.0,0.0,-1.0),cr))*radius;\n mvPosition.xyz += mult*(cr + doublecr).xyz; \n }\n\n cposition = mvPosition.xyz;\n gl_Position = projectionMatrix * mvPosition;\n vec4 lDirection = viewMatrix * vec4( directionalLightDirection[ 0 ], 0.0 );\n vLight = normalize( lDirection.xyz )*directionalLightColor[0]; //not really sure this is right, but color is always white so..\n}\n\n",uniforms:st},ot={fragmentShader:nt+"gl_FragColor = vec4(color,1.0);}",vertexShader:"\n\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat4 viewMatrix;\nuniform mat3 normalMatrix;\nuniform vec3 directionalLightColor[ 1 ];\nuniform vec3 directionalLightDirection[ 1 ];\nuniform vec3 outlineColor;\nuniform float outlineWidth;\nuniform float outlinePushback;\nuniform float outlineMaxPixels;\nuniform float vWidth;\nuniform mat4 projinv;\n\n\nattribute vec3 position;\nattribute vec3 normal;\nattribute vec3 color;\nattribute float radius;\n\nvarying vec3 vColor;\nvarying vec3 vLight;\nvarying vec3 cposition;\nvarying vec3 p1;\nvarying vec3 p2;\nvarying float r;\n\nvoid main() {\n\n vColor = outlineColor;\n float rad = radius+sign(radius)*outlineWidth;\n r = abs(rad);\n\n vec4 to = modelViewMatrix*vec4(normal, 1.0); //normal is other point of cylinder\n vec4 pt = modelViewMatrix*vec4(position, 1.0);\n//pushback\n float scale = 1.0;\n if(projectionMatrix[3][3] != 0.0) { //orthographic\n to.z -= outlinePushback;\n pt.z -= outlinePushback;\n } else { //perspective\n vec4 midbefore = pt;\n if(length(to.xyz) < length(pt)) {\n midbefore = to;\n }\n vec4 midafter = midbefore;\n midafter.xyz += normalize(midbefore.xyz)*outlinePushback;\n\n to.xyz += normalize(to.xyz)*outlinePushback;\n pt.xyz += normalize(pt.xyz)*outlinePushback;\n\n //figure out a scaling factor for radius to account for perspective setback\n vec4 midbeforer = vec4(midbefore.x+rad,midbefore.y, midbefore.z, midbefore.w);\n vec4 midafterr = vec4(midafter.x+rad,midafter.y, midafter.z, midafter.w);\n\n vec4 mb = projectionMatrix*midbefore;\n vec4 mbr = projectionMatrix*midbeforer;\n vec4 ma = projectionMatrix*midafter;\n vec4 mar = projectionMatrix*midafterr;\n mb /= mb.w;\n mbr /= mbr.w;\n ma /= ma.w;\n mar /= mar.w;\n scale = abs((mbr.x-mb.x)/(mar.x-ma.x));\n rad *= scale;\n r = abs(rad);\n }\n vec4 mvPosition = pt;\n p1 = pt.xyz; p2 = to.xyz;\n vec3 norm = to.xyz-pt.xyz;\n float mult = 1.1; //slop to account for perspective of sphere\n if(length(p1) > length(p2)) { //billboard at level of closest point\n mvPosition = to;\n }\n\n vec3 n = normalize(mvPosition.xyz);\n//intersect with the plane defined by the camera looking at the billboard point\n if(color.z >= 0.0) { //p1\n vec3 pnorm = normalize(p1);\n float t = dot(mvPosition.xyz-p1,n)/dot(pnorm,n);\n mvPosition.xyz = p1+t*pnorm;\n } else {\n vec3 pnorm = normalize(p2);\n float t = dot(mvPosition.xyz-p2,n)/dot(pnorm,n);\n mvPosition.xyz = p2+t*pnorm;\n mult *= -1.0;\n }\n\n if(outlineMaxPixels > 0.0) {\n vec4 cpos = mvPosition;\n vec4 unadjusted = projectionMatrix*vec4(cpos.x+abs(scale*radius), cpos.y,cpos.z,cpos.w); \n vec4 ccoord = projectionMatrix*cpos;\n vec4 adjust = projectionMatrix*vec4(cpos.x+r,cpos.y,cpos.z,cpos.w); \n unadjusted /= unadjusted.w;\n adjust /= adjust.w;\n unadjusted.xyz -= ccoord.xyz/ccoord.w;\n adjust.xyz -= ccoord.xyz/ccoord.w;\n float diff = abs(adjust.x-unadjusted.x);\n diff *= vWidth; //this should now be in pixels\n if(diff > outlineMaxPixels) {\n float fixlen = abs(unadjusted.x) + outlineMaxPixels/vWidth; \n vec4 pcoord = ccoord;\n pcoord.x += fixlen*pcoord.w;\n vec4 altc = projinv*pcoord;\n r= abs(altc.x-cpos.x);\n }\n }\n\n vec3 cr = normalize(cross(mvPosition.xyz,norm))*rad;\n vec3 doublecr = normalize(cross(mvPosition.xyz,cr))*rad;\n mvPosition.xy += mult*(cr + doublecr).xy;\n cposition = mvPosition.xyz;\n gl_Position = projectionMatrix * mvPosition;\n vLight = vec3(1.0,1.0,1.0);\n}\n\n",uniforms:{opacity:{type:"f",value:1},fogColor:{type:"c",value:new b.Color(1,1,1)},fogNear:{type:"f",value:1},fogFar:{type:"f",value:2e3},outlineColor:{type:"c",value:new b.Color(0,0,0)},outlineWidth:{type:"f",value:.1},outlinePushback:{type:"f",value:1},outlineMaxPixels:{type:"f",value:0},projinv:{type:"mat4",value:[]}}},lt={opacity:{type:"f",value:1},fogColor:{type:"c",value:new b.Color(1,1,1)},fogNear:{type:"f",value:1},fogFar:{type:"f",value:2e3},data:{type:"i",value:3},colormap:{type:"i",value:4},depthmap:{type:"i",value:5},step:{type:"f",value:1},maxdepth:{type:"f",value:100},subsamples:{type:"f",value:5},textmat:{type:"mat4",value:[]},projinv:{type:"mat4",value:[]},transfermin:{type:"f",value:-.2},transfermax:{type:"f",value:.2}},ht={fragmentShader:"\nuniform highp sampler3D data;\nuniform highp sampler2D colormap;\nuniform highp sampler2D depthmap;\n\n\nuniform mat4 textmat;\nuniform mat4 projinv;\nuniform mat4 projectionMatrix;\n\nuniform float step;\nuniform float subsamples;\nuniform float maxdepth;\nuniform float transfermin;\nuniform float transfermax;\nin vec4 mvPosition;\nout vec4 color;\nvoid main(void) {\n\n vec4 pos = mvPosition;\n bool seengood = false;\n float i = 0.0;\n color = vec4(1,1,1,0);\n float increment = 1.0/subsamples;\n float maxsteps = (maxdepth*subsamples/step);\n//there's probably a better way to do this..\n//calculate farthest possible point in model coordinates\n vec4 maxpos = vec4(pos.x,pos.y,pos.z-maxdepth,1.0);\n// convert to projection\n maxpos = projectionMatrix*maxpos;\n vec4 startp = projectionMatrix*pos;\n// homogonize\n maxpos /= maxpos.w;\n startp /= startp.w;\n//take x,y from start and z from max\n maxpos = vec4(startp.x,startp.y,maxpos.z,1.0);\n//convert back to model space\n maxpos = projinv*maxpos;\n maxpos /= maxpos.w;\n float incr = step/subsamples;\n//get depth from depthmap\n//startp is apparently [-1,1]\n vec2 tpos = startp.xy/2.0+0.5;\n float depth = texture(depthmap, tpos).r;\n//compute vector between start and end\n vec4 direction = maxpos-pos;\n for( i = 0.0; i <= maxsteps; i++) {\n vec4 pt = (pos+(i/maxsteps)*direction);\n vec4 ppt = projectionMatrix*pt;\n float ptdepth = ppt.z/ppt.w;\n ptdepth = ((gl_DepthRange.diff * ptdepth) + gl_DepthRange.near + gl_DepthRange.far) / 2.0;\n if(ptdepth > depth) break;\n pt = textmat*pt;\n// pt /= pt.w;\n if(pt.x >= -0.01 && pt.y >= -0.01 && pt.z >= -0.01 && pt.x <= 1.01 && pt.y <= 1.01 && pt.z <= 1.01) {\n seengood = true;\n } else if(seengood) {\n break;\n }\n if( pt.x < -0.01 || pt.x > 1.01 || pt.y < -0.01 || pt.y > 1.01 || pt.z < -0.01 || pt.z > 1.01 ){\n color.a = 0.0;\n continue;\n }\n else {\n float val = texture(data, pt.zyx).r;\n if(isinf(val)) continue; //masked out\n float cval = (val-transfermin)/(transfermax-transfermin); //scale to texture 0-1 range\n vec4 val_color = texture(colormap, vec2(cval,0.5));\n color.rgb = color.rgb*color.a + (1.0-color.a)*val_color.a*val_color.rgb;\n color.a += (1.0 - color.a) * val_color.a; \n if(color.a > 0.0) color.rgb /= color.a;\n// color = vec4(pt.x, pt.y, pt.z, 1.0);\n }\n// color = vec4(pt.x, pt.y, pt.z, 0.0)\n }\n}\n\n ".replace("#define GLSLIFY 1",""),vertexShader:"uniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat4 viewMatrix;\n\nin vec3 position;\nout vec4 mvPosition;\nvoid main() {\n\n mvPosition = modelViewMatrix * vec4( position, 1.0 );\n gl_Position = projectionMatrix*mvPosition;\n}\n".replace("#define GLSLIFY 1",""),uniforms:lt},ct={basic:V,blur:{fragmentShader:"const float INV_TOTAL_SAMPLES_FACTOR = 1.0 / 9.0;\nuniform highp sampler2D inTex;\nvarying highp vec2 vTexCoords;\n \nvoid main() {\n \n vec2 texelSize = 1.0 / vec2(textureSize(inTex,0));\n float blurred_visibility_factor = 0.0f;\n for (int t = -1; t <= 1; ++t) {\n for (int s = -1; s <= 1; ++s) {\n vec2 offset = vec2(float(s), float(t)) * texelSize;\n blurred_visibility_factor += texture2D(inTex, vTexCoords + offset).r;\n }\n }\n \n gl_FragDepthEXT = blurred_visibility_factor * INV_TOTAL_SAMPLES_FACTOR;\n \n}",vertexShader:"attribute vec2 vertexPosition;\nvarying highp vec2 vTexCoords;\nconst vec2 scale = vec2(0.5, 0.5);\n\nvoid main() {\n vTexCoords = vertexPosition * scale + scale; // scale vertex attribute to [0,1] range\n gl_Position = vec4(vertexPosition, 0.0, 1.0);\n}\n ",uniforms:{}},instanced:H,lambert:q,lambertdouble:Z,outline:K,screen:Q,screenaa:$,ssao:{fragmentShader:"uniform sampler2D depthmap;\nvarying highp vec2 vTexCoords;\nuniform float vWidth;\nuniform float vHeight;\nuniform float total_strength;\nuniform float radius;\nuniform mat4 projinv;\nuniform mat4 projectionMatrix;\n\n//code for pseudorandom vector from chatgpt\nfloat hash(vec3 p) {\n p = fract(p * vec3(0.1031, 0.1030, 0.0973));\n p += dot(p, p.yzx + 33.33);\n return fract((p.x + p.y) * p.z);\n}\n\n// Generate a pseudorandom vec3 from a seed vec3\nvec3 pseudorandomVec3(vec3 seed) {\n vec3 randomVec;\n randomVec.x = hash(seed);\n randomVec.y = hash(seed + vec3(1.0, 0.0, 17.1));\n randomVec.z = hash(seed + vec3(0.0, 13.23, 0.0));\n return randomVec;\n}\n\nvoid main(void)\n{ \n const float base = 0.2;\n const float area = 0.75;\n const int cycles = 1;\n\n const int samples = 64;\n vec3 sample_sphere[64] = vec3[](\n vec3(0.091258,-0.510164,0.000000),\n vec3(-0.204347,-0.872967,0.187199),\n vec3(0.009690,-0.263696,-0.110414),\n vec3(0.175208,-0.563987,0.228527),\n vec3(-0.001824,-0.003113,-0.000323),\n vec3(0.411134,-0.719869,-0.261530),\n vec3(-0.074272,-0.377368,0.276290),\n vec3(-0.147773,-0.381587,-0.284529),\n vec3(0.173317,-0.199635,0.063295),\n vec3(-0.186452,-0.199460,0.076965),\n vec3(0.143985,-0.308160,-0.307687),\n vec3(0.053194,-0.148286,0.169589),\n vec3(-0.547656,-0.486476,-0.317378),\n vec3(0.020804,-0.015092,-0.004574),\n vec3(-0.038006,-0.043165,0.054059),\n vec3(-0.094795,-0.443908,-0.731525),\n vec3(0.547552,-0.396466,0.461477),\n vec3(-0.176886,-0.089989,0.007315),\n vec3(0.074401,-0.048840,-0.074039),\n vec3(-0.008240,-0.075697,0.178197),\n vec3(-0.307880,-0.185053,-0.368943),\n vec3(0.309520,-0.108483,0.041646),\n vec3(-0.773478,-0.292946,0.538166),\n vec3(0.184487,-0.231594,-0.820065),\n vec3(0.207318,-0.100531,0.361797),\n vec3(-0.173306,-0.037737,-0.055289),\n vec3(0.548102,-0.105342,-0.253237),\n vec3(-0.119342,-0.043907,0.285162),\n vec3(-0.270247,-0.087861,-0.751357),\n vec3(0.449312,-0.039777,0.236146),\n vec3(-0.743773,-0.036095,0.196056),\n vec3(0.148819,-0.004300,-0.231448),\n vec3(0.008773,0.000809,0.051047),\n vec3(-0.461467,0.027390,-0.357386),\n vec3(0.169626,0.013338,-0.014053),\n vec3(-0.043786,0.007095,0.047331),\n vec3(0.004821,0.140371,-0.988260),\n vec3(0.092402,0.023994,0.101860),\n vec3(-0.295335,0.061530,-0.027372),\n vec3(0.024903,0.007537,-0.018901),\n vec3(-0.081463,0.125402,0.447794),\n vec3(-0.397119,0.231805,-0.631062),\n vec3(0.163853,0.059014,0.044905),\n vec3(-0.495220,0.214357,0.254139),\n vec3(0.306123,0.373687,-0.825715),\n vec3(0.021665,0.026737,0.053220),\n vec3(-0.208231,0.117129,-0.098685),\n vec3(0.139749,0.080968,-0.043086),\n vec3(-0.153599,0.182814,0.262090),\n vec3(-0.159673,0.496777,-0.743568),\n vec3(0.134797,0.117152,0.095753),\n vec3(-0.155626,0.120533,0.019395),\n vec3(0.042311,0.054462,-0.049709),\n vec3(0.001257,0.031288,0.034468),\n vec3(-0.002271,0.003199,-0.002304),\n vec3(0.662104,0.717307,0.033854),\n vec3(-0.373100,0.576021,0.308274),\n vec3(0.024233,0.231316,-0.173688),\n vec3(0.161311,0.420217,0.234273),\n vec3(-0.045248,0.078031,-0.010411),\n vec3(0.167453,0.376942,-0.094872),\n vec3(-0.056194,0.433247,0.173218),\n vec3(-0.016224,0.123149,-0.035569),\n vec3(0.067127,0.407641,0.028479)\n );\n\n float depth = texture2D(depthmap, vTexCoords).r;\n if(depth == 1.0) {\n discard;\n }\n\n vec4 screen_position = vec4(vTexCoords, depth,1.0);\n vec4 pos = projinv*screen_position;\n pos /= pos.w;\n vec3 position = pos.xyz;\n\n //approximate the normal from the depth map; I find this simpler\n //than trying to recompute the exact normal within every possible object shader\n\n //pixel offset positions in screen space\n ivec2 dim = textureSize(depthmap,0);\n vec2 offset1 = vec2(0.0,1.0/float(dim.y));\n vec2 offset2 = vec2(1.0/float(dim.x),0.0);\n float depth1 = texture2D(depthmap, vTexCoords + offset1).r;\n float depth2 = texture2D(depthmap, vTexCoords + offset2).r;\n \n vec3 p1 = vec3(screen_position.xy+offset1, depth1 - depth);\n vec3 p2 = vec3(screen_position.xy+offset2, depth2 - depth);\n\n //convert to model space\n vec4 pos1 = projinv*vec4(p1,1);\n pos1 /= pos1.w;\n vec4 pos2 = projinv*vec4(p2,1);\n pos2 /= pos2.w;\n\n vec3 normal = normalize(cross(pos1.xyz-position, pos2.xyz-position)); //model normal, important we normalize in model space\n\n //pseudo random number\n\n float occlusion = 0.0;\n for(int c = 0; c < cycles; c++) {\n vec3 random = normalize(pseudorandomVec3(position+float(c)));\n for(int i=0; i < samples; i++) {\n\n vec3 ray = radius * reflect(sample_sphere[i],random);\n vec3 hemi_ray = position + sign(dot(ray,normal)) * ray; //model space\n vec4 hemi_screen = projectionMatrix*vec4(hemi_ray,1.0);\n hemi_screen /= hemi_screen.w;\n \n float occ_depth = texture2D(depthmap, clamp(hemi_screen.xy,0.0,1.0)).r;\n float difference = hemi_screen.z - occ_depth;\n \n occlusion += step(0.0, difference) * (1.0-smoothstep(0.0, area, difference));\n }\n }\n float ao = 1.0 - total_strength * occlusion * (1.0 / float(cycles*samples));\n gl_FragDepthEXT = clamp(ao+base,0.0,1.0);\n\n}",vertexShader:"attribute vec2 vertexPosition;\nvarying highp vec2 vTexCoords;\nconst vec2 scale = vec2(0.5, 0.5);\n\nvoid main() {\n vTexCoords = vertexPosition * scale + scale; // scale vertex attribute to [0,1] range\n gl_Position = vec4(vertexPosition, 0.0, 1.0);\n}\n ",uniforms:{total_strength:{type:"f",value:1},radius:{type:"f",value:5},projinv:{type:"mat4",value:[]}}},sphereimposter:tt,sphereimposteroutline:it,sprite:rt,stickimposter:at,stickimposteroutline:ot,volumetric:ht};function dt(t){let e={};for(const r in t){e[r]={},e[r].type=t[r].type;var i=t[r].value;i instanceof b.Color?e[r].value=i.clone():"number"==typeof i?e[r].value=i:i instanceof Array?e[r].value=[]:console.error("Error copying shader uniforms from ShaderLib: unknown type for uniform")}return e}const ut={clone:dt};class Camera extends Object3D{constructor(t=50,e=1,i=.1,r=2e3,s=!1){super(),this.projectionMatrix=new l.Matrix4,this.projectionMatrixInverse=new l.Matrix4,this.matrixWorldInverse=new l.Matrix4,this.fov=t,this.aspect=e,this.near=i,this.far=r;var n=this.position.z;this.right=n*Math.tan(Math.PI/180*t),this.left=-this.right,this.top=this.right/this.aspect,this.bottom=-this.top,this.ortho=!!s,this.updateProjectionMatrix()}lookAt(t){this.matrix.lookAt(this.position,t,this.up),this.rotationAutoUpdate&&(!1===this.useQuaternion&&this.rotation instanceof l.Vector3?this.rotation.setEulerFromRotationMatrix(this.matrix,this.eulerOrder):console.error("Unimplemented math operation."))}updateProjectionMatrix(){this.ortho?this.projectionMatrix.makeOrthographic(this.left,this.right,this.top,this.bottom,this.near,this.far):this.projectionMatrix.makePerspective(this.fov,this.aspect,this.near,this.far),this.projectionMatrixInverse.getInverse(this.projectionMatrix)}}class Fog{constructor(t,e=1,i=1e3){this.name="",this.color=new b.Color(t),this.near=e,this.far=i}clone(){return new Fog(this.color.getHex(),this.near,this.far)}}class SpritePlugin{constructor(){this.sprite={vertices:null,faces:null,vertexBuffer:null,elementBuffer:null,program:null,attributes:{},uniforms:null}}init(t){this.gl=t.context,this.renderer=t,this.precision=t.getPrecision(),this.sprite.vertices=new Float32Array(16),this.sprite.faces=new Uint16Array(6);var e=0;this.sprite.vertices[e++]=-1,this.sprite.vertices[e++]=-1,this.sprite.vertices[e++]=0,this.sprite.vertices[e++]=0,this.sprite.vertices[e++]=1,this.sprite.vertices[e++]=-1,this.sprite.vertices[e++]=1,this.sprite.vertices[e++]=0,this.sprite.vertices[e++]=1,this.sprite.vertices[e++]=1,this.sprite.vertices[e++]=1,this.sprite.vertices[e++]=1,this.sprite.vertices[e++]=-1,this.sprite.vertices[e++]=1,this.sprite.vertices[e++]=0,this.sprite.vertices[e++]=1,e=0,this.sprite.faces[e++]=0,this.sprite.faces[e++]=1,this.sprite.faces[e++]=2,this.sprite.faces[e++]=0,this.sprite.faces[e++]=2,this.sprite.faces[e++]=3,this.sprite.vertexBuffer=this.gl.createBuffer(),this.sprite.elementBuffer=this.gl.createBuffer(),this.gl.bindBuffer(this.gl.ARRAY_BUFFER,this.sprite.vertexBuffer),this.gl.bufferData(this.gl.ARRAY_BUFFER,this.sprite.vertices,this.gl.STATIC_DRAW),this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER,this.sprite.elementBuffer),this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER,this.sprite.faces,this.gl.STATIC_DRAW),this.sprite.program=this.createProgram(ct.sprite,this.precision||1),this.sprite.attributes={};const i={};this.sprite.attributes.position=this.gl.getAttribLocation(this.sprite.program,"position"),this.sprite.attributes.uv=this.gl.getAttribLocation(this.sprite.program,"uv"),i.uvOffset=this.gl.getUniformLocation(this.sprite.program,"uvOffset"),i.uvScale=this.gl.getUniformLocation(this.sprite.program,"uvScale"),i.rotation=this.gl.getUniformLocation(this.sprite.program,"rotation"),i.scale=this.gl.getUniformLocation(this.sprite.program,"scale"),i.alignment=this.gl.getUniformLocation(this.sprite.program,"alignment"),i.color=this.gl.getUniformLocation(this.sprite.program,"color"),i.map=this.gl.getUniformLocation(this.sprite.program,"map"),i.opacity=this.gl.getUniformLocation(this.sprite.program,"opacity"),i.useScreenCoordinates=this.gl.getUniformLocation(this.sprite.program,"useScreenCoordinates"),i.screenPosition=this.gl.getUniformLocation(this.sprite.program,"screenPosition"),i.modelViewMatrix=this.gl.getUniformLocation(this.sprite.program,"modelViewMatrix"),i.projectionMatrix=this.gl.getUniformLocation(this.sprite.program,"projectionMatrix"),i.fogType=this.gl.getUniformLocation(this.sprite.program,"fogType"),i.fogDensity=this.gl.getUniformLocation(this.sprite.program,"fogDensity"),i.fogNear=this.gl.getUniformLocation(this.sprite.program,"fogNear"),i.fogFar=this.gl.getUniformLocation(this.sprite.program,"fogFar"),i.fogColor=this.gl.getUniformLocation(this.sprite.program,"fogColor"),i.alphaTest=this.gl.getUniformLocation(this.sprite.program,"alphaTest"),this.sprite.uniforms=i}render(t,e,i,r,s){var n,a,o,l,h,c,d,u,f,p;if(!this.gl)throw new Error("WebGLRenderer not initialized");let g=[];null===(n=null==t?void 0:t.__webglSprites)||void 0===n||n.forEach((t=>{(s&&0==t.material.depthTest||!s&&t.material.depthTest)&&g.push(t)}));let m=g.length;if(!m)return;const v=this.sprite.attributes,_=this.sprite.uniforms;if(!_)throw new Error("Uniforms not defined");var y=.5*i,b=.5*r;this.gl.useProgram(this.sprite.program),this.gl.enableVertexAttribArray(v.position),this.gl.enableVertexAttribArray(v.uv),this.gl.disable(this.gl.CULL_FACE),this.gl.enable(this.gl.BLEND),this.gl.bindBuffer(this.gl.ARRAY_BUFFER,this.sprite.vertexBuffer),this.gl.vertexAttribPointer(v.position,2,this.gl.FLOAT,!1,16,0),this.gl.vertexAttribPointer(v.uv,2,this.gl.FLOAT,!1,16,8),this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER,this.sprite.elementBuffer),this.gl.uniformMatrix4fv(_.projectionMatrix,!1,e.projectionMatrix.elements),this.gl.activeTexture(this.gl.TEXTURE0),this.gl.uniform1i(_.map,0);var x,w=0,A=0,C=t.fog;let S,M,z,T;C?(this.gl.uniform3f(_.fogColor,C.color.r,C.color.g,C.color.b),this.gl.uniform1f(_.fogNear,C.near),this.gl.uniform1f(_.fogFar,C.far),this.gl.uniform1i(_.fogType,1),w=1,A=1):(this.gl.uniform1i(_.fogType,0),w=0,A=0);let E=[];for(x=0;x0}disableAmbientOcclusion(){this._AOEnabled=!1}setViewport(){if(this._offscreen&&(this._offscreen.width=this._canvas.width,this._offscreen.height=this._canvas.height),null!=this.rows&&null!=this.cols&&null!=this.row&&null!=this.col){var t=this._canvas.width/this.cols,e=this._canvas.height/this.rows;this._viewportWidth=t,this._viewportHeight=e,this.isLost()||(this._gl.enable(this._gl.SCISSOR_TEST),this._gl.scissor(t*this.col,e*this.row,t,e),this._gl.viewport(t*this.col,e*this.row,t,e))}}setSize(t,e){if(this.devicePixelRatio=void 0!==window.devicePixelRatio?window.devicePixelRatio:1,this._antialias&&this.devicePixelRatio<2&&(this.devicePixelRatio*=2),null!=this.rows&&null!=this.cols&&null!=this.row&&null!=this.col){var i=t/this.cols,r=e/this.rows;this._canvas.width=t*this.devicePixelRatio,this._canvas.height=e*this.devicePixelRatio,this._viewportWidth=i*this.devicePixelRatio,this._viewportHeight=r*this.devicePixelRatio,this._canvas.style.width=t+"px",this._canvas.style.height=e+"px",this.setViewport()}else this._viewportWidth=this._canvas.width=t*this.devicePixelRatio,this._viewportHeight=this._canvas.height=e*this.devicePixelRatio,this._canvas.style.width=t+"px",this._canvas.style.height=e+"px",this.isLost()||this._gl.viewport(0,0,this._gl.drawingBufferWidth,this._gl.drawingBufferHeight);this.initFrameBuffer()}clear(t,e,i){var r=0;(void 0===t||t)&&(r|=this._gl.COLOR_BUFFER_BIT),(void 0===e||e)&&(r|=this._gl.DEPTH_BUFFER_BIT),(void 0===i||i)&&(r|=this._gl.STENCIL_BUFFER_BIT),this._gl.clear(r)}setMaterialFaces(t,e){var i=t.side===a,r=t.side===n;t.imposter||(r=e?!r:r),this._oldDoubleSided!==i&&(i?this._gl.disable(this._gl.CULL_FACE):this._gl.enable(this._gl.CULL_FACE),this._oldDoubleSided=i),this._oldFlipSided!==r&&(r?this._gl.frontFace(this._gl.CW):this._gl.frontFace(this._gl.CCW),this._oldFlipSided=r),this._gl.cullFace(this._gl.BACK)}setDepthTest(t){this._oldDepthTest!==t&&(t?this._gl.enable(this._gl.DEPTH_TEST):this._gl.disable(this._gl.DEPTH_TEST),this._oldDepthTest=t)}setDepthWrite(t){this._oldDepthWrite!==t&&(this._gl.depthMask(t),this._oldDepthWrite=t)}setBlending(t){t?(this._gl.enable(this._gl.BLEND),this._gl.blendEquationSeparate(this._gl.FUNC_ADD,this._gl.FUNC_ADD),this._gl.blendFuncSeparate(this._gl.SRC_ALPHA,this._gl.ONE_MINUS_SRC_ALPHA,this._gl.ONE,this._gl.ONE_MINUS_SRC_ALPHA)):this._gl.disable(this._gl.BLEND)}initMaterial(t,e,i,r){var s,n;if(t.addEventListener("dispose",this.onMaterialDispose.bind(this)),n=t.shaderID){var a=ct[n];t.vertexShader=a.vertexShader,t.fragmentShader=a.fragmentShader,t.uniforms=ut.clone(a.uniforms),t.shaded&&t.makeShaded(this.SHADE_TEXTURE)}s={wireframe:t.wireframe,fragdepth:t.imposter,volumetric:t.volumetric,shaded:t.shaded},t.program=this.buildProgram(t.fragmentShader,t.vertexShader,t.uniforms,s)}renderBuffer(t,e,i,r,s,n){var a,o;if(r.visible&&(a=this.setProgram(t,e,i,r,n,this))){o=a.attributes;var l,h,c=!1,d=r.wireframe?1:0,u=16777215*s.id+2*a.id+d;if(u!==this._currentGeometryGroupHash&&(this._currentGeometryGroupHash=u,c=!0),c&&(this.disableAttributes(),o.position>=0&&(this._gl.bindBuffer(this._gl.ARRAY_BUFFER,s.__webglVertexBuffer),this.enableAttribute(o.position),this._gl.vertexAttribPointer(o.position,3,this._gl.FLOAT,!1,0,0)),o.color>=0&&(this._gl.bindBuffer(this._gl.ARRAY_BUFFER,s.__webglColorBuffer),this.enableAttribute(o.color),this._gl.vertexAttribPointer(o.color,3,this._gl.FLOAT,!1,0,0)),o.normal>=0&&(this._gl.bindBuffer(this._gl.ARRAY_BUFFER,s.__webglNormalBuffer),this.enableAttribute(o.normal),this._gl.vertexAttribPointer(o.normal,3,this._gl.FLOAT,!1,0,0)),o.offset>=0&&(this._gl.bindBuffer(this._gl.ARRAY_BUFFER,s.__webglOffsetBuffer),this.enableAttribute(o.offset),this._gl.vertexAttribPointer(o.offset,3,this._gl.FLOAT,!1,0,0)),o.radius>=0&&(this._gl.bindBuffer(this._gl.ARRAY_BUFFER,s.__webglRadiusBuffer),this.enableAttribute(o.radius),this._gl.vertexAttribPointer(o.radius,1,this._gl.FLOAT,!1,0,0))),n instanceof Mesh){if("instanced"===r.shaderID){var f=r.sphere.geometryGroups[0];c&&(this._gl.bindBuffer(this._gl.ARRAY_BUFFER,s.__webglVertexBuffer),this._gl.bufferData(this._gl.ARRAY_BUFFER,f.vertexArray,this._gl.STATIC_DRAW),this._gl.bindBuffer(this._gl.ARRAY_BUFFER,s.__webglNormalBuffer),this._gl.bufferData(this._gl.ARRAY_BUFFER,f.normalArray,this._gl.STATIC_DRAW),this._gl.bindBuffer(this._gl.ELEMENT_ARRAY_BUFFER,s.__webglFaceBuffer),this._gl.bufferData(this._gl.ELEMENT_ARRAY_BUFFER,f.faceArray,this._gl.STATIC_DRAW)),l=f.faceidx,this._extInstanced.vertexAttribDivisorANGLE(o.offset,1),this._extInstanced.vertexAttribDivisorANGLE(o.radius,1),this._extInstanced.vertexAttribDivisorANGLE(o.color,1),this._extInstanced.drawElementsInstancedANGLE(this._gl.TRIANGLES,l,this._gl.UNSIGNED_SHORT,0,s.radiusArray.length),this._extInstanced.vertexAttribDivisorANGLE(o.offset,0),this._extInstanced.vertexAttribDivisorANGLE(o.radius,0),this._extInstanced.vertexAttribDivisorANGLE(o.color,0)}else r.wireframe?(h=s.lineidx,this.setLineWidth(r.wireframeLinewidth),c&&this._gl.bindBuffer(this._gl.ELEMENT_ARRAY_BUFFER,s.__webglLineBuffer),this._gl.drawElements(this._gl.LINES,h,this._gl.UNSIGNED_SHORT,0)):(l=s.faceidx,c&&this._gl.bindBuffer(this._gl.ELEMENT_ARRAY_BUFFER,s.__webglFaceBuffer),this._gl.drawElements(this._gl.TRIANGLES,l,this._gl.UNSIGNED_SHORT,0));this.info.render.calls++,this.info.render.vertices+=l,this.info.render.faces+=l/3}else n instanceof Line&&(h=s.vertices,this.setLineWidth(r.linewidth),this._gl.drawArrays(this._gl.LINES,0,h),this.info.render.calls++)}}clearShading(){this._gl.framebufferTexture2D(this._gl.FRAMEBUFFER,this._gl.DEPTH_ATTACHMENT,this._gl.TEXTURE_2D,this._shadingTexture,0),this.clear(!1,!0,!1),this._gl.framebufferTexture2D(this._gl.FRAMEBUFFER,this._gl.DEPTH_ATTACHMENT,this._gl.TEXTURE_2D,this._depthTexture,0)}setShading(t,e,i){let r=t.__lights,s=t.fog,n=[];for(let e=0,r=t.__webglObjects.length;e=0;--i)t[i].object===e&&t.splice(i,1)}removeInstancesDirect(t,e){for(var i=t.length-1;i>=0;--i)t[i]===e&&t.splice(i,1)}unrollBufferMaterial(t){var e=t.object.material;if(e.volumetric)t.opaque=null,t.transparent=null,t.volumetric=e;else if(e.transparent){if(t.opaque=null,t.volumetric=null,t.transparent=e,!e.wireframe){let i=e.clone();i.opacity=0,t.transparentDepth=i}}else{if(t.opaque=e,t.transparent=null,t.volumetric=null,!e.wireframe){let i=e.clone();i.opacity=0,t.opaqueDepth=i}e.hasAO&&(t.hasAO=!0),(this._AOEnabled||t.hasAO)&&(t.opaqueShaded=e.clone(),t.opaqueShaded.shaded=!0)}}setBuffers(t,e){var i=t.vertexArray,r=t.colorArray;if(void 0!==t.__webglOffsetBuffer?(this._gl.bindBuffer(this._gl.ARRAY_BUFFER,t.__webglOffsetBuffer),this._gl.bufferData(this._gl.ARRAY_BUFFER,i,e)):(this._gl.bindBuffer(this._gl.ARRAY_BUFFER,t.__webglVertexBuffer),this._gl.bufferData(this._gl.ARRAY_BUFFER,i,e)),this._gl.bindBuffer(this._gl.ARRAY_BUFFER,t.__webglColorBuffer),this._gl.bufferData(this._gl.ARRAY_BUFFER,r,e),t.normalArray&&void 0!==t.__webglNormalBuffer){var s=t.normalArray;this._gl.bindBuffer(this._gl.ARRAY_BUFFER,t.__webglNormalBuffer),this._gl.bufferData(this._gl.ARRAY_BUFFER,s,e)}if(t.radiusArray&&void 0!==t.__webglRadiusBuffer&&(this._gl.bindBuffer(this._gl.ARRAY_BUFFER,t.__webglRadiusBuffer),this._gl.bufferData(this._gl.ARRAY_BUFFER,t.radiusArray,e)),t.faceArray&&void 0!==t.__webglFaceBuffer){var n=t.faceArray;this._gl.bindBuffer(this._gl.ELEMENT_ARRAY_BUFFER,t.__webglFaceBuffer),this._gl.bufferData(this._gl.ELEMENT_ARRAY_BUFFER,n,e)}if(t.lineArray&&void 0!==t.__webglLineBuffer){var a=t.lineArray;this._gl.bindBuffer(this._gl.ELEMENT_ARRAY_BUFFER,t.__webglLineBuffer),this._gl.bufferData(this._gl.ELEMENT_ARRAY_BUFFER,a,e)}}createMeshBuffers(t){t.radiusArray&&(t.__webglRadiusBuffer=this._gl.createBuffer()),t.useOffset&&(t.__webglOffsetBuffer=this._gl.createBuffer()),t.__webglVertexBuffer=this._gl.createBuffer(),t.__webglNormalBuffer=this._gl.createBuffer(),t.__webglColorBuffer=this._gl.createBuffer(),t.__webglFaceBuffer=this._gl.createBuffer(),t.__webglLineBuffer=this._gl.createBuffer(),this.info.memory.geometries++}createLineBuffers(t){t.__webglVertexBuffer=this._gl.createBuffer(),t.__webglColorBuffer=this._gl.createBuffer(),this.info.memory.geometries++}addBuffer(t,e,i){t.push({buffer:e,object:i,opaque:null,transparent:null})}setupMatrices(t,e){t._modelViewMatrix.multiplyMatrices(e.matrixWorldInverse,t.matrixWorld),t._normalMatrix.getInverse(t._modelViewMatrix),t._normalMatrix.transpose()}filterFallback(t){return this._gl.LINEAR}setTextureParameters(t,e){t==this._gl.TEXTURE_2D?(this._gl.texParameteri(t,this._gl.TEXTURE_WRAP_S,this._gl.CLAMP_TO_EDGE),this._gl.texParameteri(t,this._gl.TEXTURE_WRAP_T,this._gl.CLAMP_TO_EDGE),this._gl.texParameteri(t,this._gl.TEXTURE_MAG_FILTER,this.filterFallback(e.magFilter)),this._gl.texParameteri(t,this._gl.TEXTURE_MIN_FILTER,this.filterFallback(e.minFilter))):(this._gl.texParameteri(t,this._gl.TEXTURE_WRAP_S,this._gl.CLAMP_TO_EDGE),this._gl.texParameteri(t,this._gl.TEXTURE_WRAP_T,this._gl.CLAMP_TO_EDGE),this._gl.texParameteri(t,this._gl.TEXTURE_WRAP_R,this._gl.CLAMP_TO_EDGE),this._extColorBufferFloat&&this._extFloatLinear?(this._gl.texParameteri(t,this._gl.TEXTURE_MAG_FILTER,this._gl.LINEAR),this._gl.texParameteri(t,this._gl.TEXTURE_MIN_FILTER,this._gl.LINEAR)):(this._gl.texParameteri(t,this._gl.TEXTURE_MAG_FILTER,this._gl.NEAREST),this._gl.texParameteri(t,this._gl.TEXTURE_MIN_FILTER,this._gl.NEAREST)))}paramToGL(t){return t===p?this._gl.UNSIGNED_BYTE:t===m?this._gl.RGBA:t===u?this._gl.NEAREST:0}setupLights(t,e){var i,r,s,n,a,o=this._lights,l=o.directional.colors,h=o.directional.positions,c=0,d=0;for(i=0,r=e.length;i{"use strict";i.r(e),i.d(e,{Matrix3:()=>Matrix3,Matrix4:()=>Matrix4,Quaternion:()=>Quaternion,Ray:()=>Ray,Vector2:()=>Vector2,Vector3:()=>Vector3,clamp:()=>r,conversionMatrix3:()=>d,degToRad:()=>n});class Quaternion{constructor(t,e,i,r){this.x=t||0,this.y=e||0,this.z=i||0,this.w=void 0!==r?r:1}set(t,e,i,r){return this.x=t,this.y=e,this.z=i,this.w=r,this}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this.w=t.w,this}conjugate(){return this.x*=-1,this.y*=-1,this.z*=-1,this}inverse(){return this.conjugate().normalize()}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w)}lengthxyz(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}normalize(){let t=this.length();return 0===t?(this.x=0,this.y=0,this.z=0,this.w=1):(t=1/t,this.x*=t,this.y*=t,this.z*=t,this.w*=t),this}multiply(t){return this.multiplyQuaternions(this,t)}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this.w*=t,this}multiplyQuaternions(t,e){const i=t.x,r=t.y,s=t.z,n=t.w,a=e.x,o=e.y,l=e.z,h=e.w;return this.x=i*h+n*a+r*l-s*o,this.y=r*h+n*o+s*a-i*l,this.z=s*h+n*l+i*o-r*a,this.w=n*h-i*a-r*o-s*l,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this.w-=t.w,this}clone(){return new Quaternion(this.x,this.y,this.z,this.w)}setFromEuler(t){const e=Math.cos(t.x/2),i=Math.cos(t.y/2),r=Math.cos(t.z/2),s=Math.sin(t.x/2),n=Math.sin(t.y/2),a=Math.sin(t.z/2);return this.x=s*i*r+e*n*a,this.y=e*n*r-s*i*a,this.z=e*i*a+s*n*r,this.w=e*i*r-s*n*a,this}}class Vector2{constructor(t,e){this.x=t||0,this.y=e||0}set(t,e){return this.x=t,this.y=e,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this}copy(t){return this.x=t.x,this.y=t.y,this}clone(){return new Vector2(this.x,this.y)}}function r(t,e,i){return Math.min(Math.max(t,e),i)}const s=Math.PI/180;function n(t){return t*s}var a,o,l,h,c;class Matrix4{constructor(t=1,e=0,i=0,r=0,s=0,n=1,a=0,o=0,l=0,h=0,c=1,d=0,u=0,f=0,p=0,g=1){void 0!==t&&"number"!=typeof t?this.elements=new Float32Array(t):(this.elements=new Float32Array(16),this.elements[0]=t,this.elements[4]=e,this.elements[8]=i,this.elements[12]=r,this.elements[1]=s,this.elements[5]=n,this.elements[9]=a,this.elements[13]=o,this.elements[2]=l,this.elements[6]=h,this.elements[10]=c,this.elements[14]=d,this.elements[3]=u,this.elements[7]=f,this.elements[11]=p,this.elements[15]=g)}makeScale(t,e,i){throw new Error("Method not implemented.")}set(t,e,i,r,s,n,a,o,l,h,c,d,u,f,p,g){const m=this.elements;return m[0]=t,m[4]=e,m[8]=i,m[12]=r,m[1]=s,m[5]=n,m[9]=a,m[13]=o,m[2]=l,m[6]=h,m[10]=c,m[14]=d,m[3]=u,m[7]=f,m[11]=p,m[15]=g,this}identity(){return this.set(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1),this}copy(t){const e=t.elements;return this.set(e[0],e[4],e[8],e[12],e[1],e[5],e[9],e[13],e[2],e[6],e[10],e[14],e[3],e[7],e[11],e[15]),this}matrix3FromTopLeft(){const t=this.elements;return new Matrix3(t[0],t[4],t[8],t[1],t[5],t[9],t[2],t[6],t[10])}setRotationFromEuler(t,e){const i=this.elements,{x:r,y:s,z:n}=t,a=Math.cos(r),o=Math.sin(r),l=Math.cos(s),h=Math.sin(s),c=Math.cos(n),d=Math.sin(n);if(void 0===e||"XYZ"===e){const t=a*c,e=a*d,r=o*c,s=o*d;i[0]=l*c,i[4]=-l*d,i[8]=h,i[1]=e+r*h,i[5]=t-s*h,i[9]=-o*l,i[2]=s-t*h,i[6]=r+e*h,i[10]=a*l}else console.error(`Error with matrix4 setRotationFromEuler. Order: ${e}`);return this}setRotationFromQuaternion(t){const e=this.elements,{x:i,y:r,z:s,w:n}=t,a=i+i,o=r+r,l=s+s,h=i*a,c=i*o,d=i*l,u=r*o,f=r*l,p=s*l,g=n*a,m=n*o,v=n*l;return e[0]=1-(u+p),e[4]=c-v,e[8]=d+m,e[1]=c+v,e[5]=1-(h+p),e[9]=f-g,e[2]=d-m,e[6]=f+g,e[10]=1-(h+u),this}multiplyMatrices(t,e){const i=t.elements,r=e.elements,s=this.elements,n=i[0],a=i[4],o=i[8],l=i[12],h=i[1],c=i[5],d=i[9],u=i[13],f=i[2],p=i[6],g=i[10],m=i[14],v=i[3],_=i[7],y=i[11],b=i[15],x=r[0],w=r[4],A=r[8],C=r[12],S=r[1],M=r[5],z=r[9],T=r[13],E=r[2],L=r[6],F=r[10],I=r[14],O=r[3],D=r[7],k=r[11],R=r[15];return s[0]=n*x+a*S+o*E+l*O,s[4]=n*w+a*M+o*L+l*D,s[8]=n*A+a*z+o*F+l*k,s[12]=n*C+a*T+o*I+l*R,s[1]=h*x+c*S+d*E+u*O,s[5]=h*w+c*M+d*L+u*D,s[9]=h*A+c*z+d*F+u*k,s[13]=h*C+c*T+d*I+u*R,s[2]=f*x+p*S+g*E+m*O,s[6]=f*w+p*M+g*L+m*D,s[10]=f*A+p*z+g*F+m*k,s[14]=f*C+p*T+g*I+m*R,s[3]=v*x+_*S+y*E+b*O,s[7]=v*w+_*M+y*L+b*D,s[11]=v*A+_*z+y*F+b*k,s[15]=v*C+_*T+y*I+b*R,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[4]*=t,e[8]*=t,e[12]*=t,e[1]*=t,e[5]*=t,e[9]*=t,e[13]*=t,e[2]*=t,e[6]*=t,e[10]*=t,e[14]*=t,e[3]*=t,e[7]*=t,e[11]*=t,e[15]*=t,this}makeTranslation(t,e,i){return this.set(1,0,0,t,0,1,0,e,0,0,1,i,0,0,0,1),this}snap(t){t||(t=4);const e=Math.pow(10,4),i=this.elements;for(let t=0;t<16;t++){const r=Math.round(i[t]);r===Math.round(i[t]*e)/e&&(i[t]=r)}return this}transpose(){const t=this.elements;let e;return e=t[1],t[1]=t[4],t[4]=e,e=t[2],t[2]=t[8],t[8]=e,e=t[6],t[6]=t[9],t[9]=e,e=t[3],t[3]=t[12],t[12]=e,e=t[7],t[7]=t[13],t[13]=e,e=t[11],t[11]=t[14],t[14]=e,this}setPosition(t){const e=this.elements;return e[12]=t.x,e[13]=t.y,e[14]=t.z,this}translate(t){const e=this.elements;return e[12]+=t.x,e[13]+=t.y,e[14]+=t.z,this}getInverse(t,e){const i=this.elements,r=t.elements,s=r[0],n=r[4],a=r[8],o=r[12],l=r[1],h=r[5],c=r[9],d=r[13],u=r[2],f=r[6],p=r[10],g=r[14],m=r[3],v=r[7],_=r[11],y=r[15];i[0]=c*g*v-d*p*v+d*f*_-h*g*_-c*f*y+h*p*y,i[4]=o*p*v-a*g*v-o*f*_+n*g*_+a*f*y-n*p*y,i[8]=a*d*v-o*c*v+o*h*_-n*d*_-a*h*y+n*c*y,i[12]=o*c*f-a*d*f-o*h*p+n*d*p+a*h*g-n*c*g,i[1]=d*p*m-c*g*m-d*u*_+l*g*_+c*u*y-l*p*y,i[5]=a*g*m-o*p*m+o*u*_-s*g*_-a*u*y+s*p*y,i[9]=o*c*m-a*d*m-o*l*_+s*d*_+a*l*y-s*c*y,i[13]=a*d*u-o*c*u+o*l*p-s*d*p-a*l*g+s*c*g,i[2]=h*g*m-d*f*m+d*u*v-l*g*v-h*u*y+l*f*y,i[6]=o*f*m-n*g*m-o*u*v+s*g*v+n*u*y-s*f*y,i[10]=n*d*m-o*h*m+o*l*v-s*d*v-n*l*y+s*h*y,i[14]=o*h*u-n*d*u-o*l*f+s*d*f+n*l*g-s*h*g,i[3]=c*f*m-h*p*m-c*u*v+l*p*v+h*u*_-l*f*_,i[7]=n*p*m-a*f*m+a*u*v-s*p*v-n*u*_+s*f*_,i[11]=a*h*m-n*c*m-a*l*v+s*c*v+n*l*_-s*h*_,i[15]=n*c*u-a*h*u+a*l*f-s*c*f-n*l*p+s*h*p;const b=s*i[0]+l*i[4]+u*i[8]+m*i[12];if(0===b){const t="Matrix4.getInverse(): can't invert matrix, determinant is 0";if(e)throw new Error(t);return console.warn(t),this.identity(),this}return this.multiplyScalar(1/b),this}isReflected(){const t=this.elements,e=t[0],i=t[4],r=t[8],s=t[1],n=t[5],a=t[9],o=t[2],l=t[6],h=t[10];return e*n*h+s*l*r+o*i*a-o*n*r-s*i*h-e*l*a<0}scale(t){const e=this.elements,{x:i}=t,{y:r}=t,{z:s}=t;return e[0]*=i,e[4]*=r,e[8]*=s,e[1]*=i,e[5]*=r,e[9]*=s,e[2]*=i,e[6]*=r,e[10]*=s,e[3]*=i,e[7]*=r,e[11]*=s,this}getMaxScaleOnAxis(){const t=this.elements,e=t[0]*t[0]+t[1]*t[1]+t[2]*t[2],i=t[4]*t[4]+t[5]*t[5]+t[6]*t[6],r=t[8]*t[8]+t[9]*t[9]+t[10]*t[10];return Math.sqrt(Math.max(e,Math.max(i,r)))}makeFrustum(t,e,i,r,s,n){const a=this.elements,o=2*s/(e-t),l=2*s/(r-i),h=(e+t)/(e-t),c=(r+i)/(r-i),d=-(n+s)/(n-s),u=-2*n*s/(n-s);return a[0]=o,a[4]=0,a[8]=h,a[12]=0,a[1]=0,a[5]=l,a[9]=c,a[13]=0,a[2]=0,a[6]=0,a[10]=d,a[14]=u,a[3]=0,a[7]=0,a[11]=-1,a[15]=0,this}makePerspective(t,e,i,r){const s=i*Math.tan(n(.5*t)),a=-s,o=a*e,l=s*e;return this.makeFrustum(o,l,a,s,i,r)}makeOrthographic(t,e,i,r,s,n){const a=this.elements,o=1/(e-t),l=1/(i-r),h=1/(n-s),c=(e+t)*o,d=(i+r)*l,u=(n+s)*h;return a[0]=2*o,a[4]=0,a[8]=0,a[12]=-c,a[1]=0,a[5]=2*l,a[9]=0,a[13]=-d,a[2]=0,a[6]=0,a[10]=-2*h,a[14]=-u,a[3]=0,a[7]=0,a[11]=0,a[15]=1,this}isEqual(t){const e=t.elements,i=this.elements;return i[0]===e[0]&&i[4]===e[4]&&i[8]===e[8]&&i[12]===e[12]&&i[1]===e[1]&&i[5]===e[5]&&i[9]===e[9]&&i[13]===e[13]&&i[2]===e[2]&&i[6]===e[6]&&i[10]===e[10]&&i[14]===e[14]&&i[3]===e[3]&&i[7]===e[7]&&i[11]===e[11]&&i[15]===e[15]}clone(){const t=this.elements;return new Matrix4(t[0],t[4],t[8],t[12],t[1],t[5],t[9],t[13],t[2],t[6],t[10],t[14],t[3],t[7],t[11],t[15])}isIdentity(){const t=this.elements;return 1===t[0]&&0===t[4]&&0===t[8]&&0===t[12]&&0===t[1]&&1===t[5]&&0===t[9]&&0===t[13]&&0===t[2]&&0===t[6]&&1===t[10]&&0===t[14]&&0===t[3]&&0===t[7]&&0===t[11]&&1===t[15]}isNearlyIdentity(t){return this.clone().snap(t).isIdentity()}getScale(t){const e=this.elements;return t=t||new Vector3,l.set(e[0],e[1],e[2]),h.set(e[4],e[5],e[6]),c.set(e[8],e[9],e[10]),t.x=l.length(),t.y=h.length(),t.z=c.length(),t}lookAt(t,e,i){const r=this.elements;return c.subVectors(t,e).normalize(),0===c.length()&&(c.z=1),l.crossVectors(i,c).normalize(),0===l.length()&&(c.x+=1e-4,l.crossVectors(i,c).normalize()),h.crossVectors(c,l),r[0]=l.x,r[4]=h.x,r[8]=c.x,r[1]=l.y,r[5]=h.y,r[9]=c.y,r[2]=l.z,r[6]=h.z,r[10]=c.z,this}compose(t,e,i){const r=this.elements;return a.identity(),a.setRotationFromQuaternion(e),o.makeScale(i.x,i.y,i.z),this.multiplyMatrices(a,o),r[12]=t.x,r[13]=t.y,r[14]=t.z,this}}a=new Matrix4,o=new Matrix4;class Vector3{constructor(t,e,i){this.x=t||0,this.y=e||0,this.z=i||0,this.atomid=void 0}set(t,e,i){return this.x=t,this.y=e,this.z=i,this}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this}multiplyVectors(t,e){return this.x=t.x*e.x,this.y=t.y*e.y,this.z=t.z*e.z,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this}divideScalar(t){return 0!==t?(this.x/=t,this.y/=t,this.z/=t):(this.x=0,this.y=0,this.z=0),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this.z=Math.max(this.z,t.z),this}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this.z=Math.min(this.z,t.z),this}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,i=this.y-t.y,r=this.z-t.z;return e*e+i*i+r*r}applyMatrix3(t){const{x:e}=this,{y:i}=this,{z:r}=this,s=t.elements;return this.x=s[0]*e+s[3]*i+s[6]*r,this.y=s[1]*e+s[4]*i+s[7]*r,this.z=s[2]*e+s[5]*i+s[8]*r,this}applyMatrix4(t){const{x:e}=this,{y:i}=this,{z:r}=this,s=t.elements;return this.x=s[0]*e+s[4]*i+s[8]*r+s[12],this.y=s[1]*e+s[5]*i+s[9]*r+s[13],this.z=s[2]*e+s[6]*i+s[10]*r+s[14],this}applyProjection(t){const{x:e}=this,{y:i}=this,{z:r}=this,s=t.elements,n=s[3]*e+s[7]*i+s[11]*r+s[15];return this.x=(s[0]*e+s[4]*i+s[8]*r+s[12])/n,this.y=(s[1]*e+s[5]*i+s[9]*r+s[13])/n,this.z=(s[2]*e+s[6]*i+s[10]*r+s[14])/n,this}applyQuaternion(t){const{x:e}=this,{y:i}=this,{z:r}=this,s=t.x,n=t.y,a=t.z,o=t.w,l={};l.x=2*(i*a-r*n),l.y=2*(r*s-e*a),l.z=2*(e*n-i*s);const h={};return h.x=l.y*a-l.z*n,h.y=l.z*s-l.x*a,h.z=l.x*n-l.y*s,this.x=e+o*l.x+h.x,this.y=i+o*l.y+h.y,this.z=r+o*l.z+h.z,this}negate(){return this.multiplyScalar(-1)}dot(t){return this.x*t.x+this.y*t.y+this.z*t.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}normalize(){return this.divideScalar(this.length())}cross(t){const{x:e}=this,{y:i}=this,{z:r}=this;return this.x=i*t.z-r*t.y,this.y=r*t.x-e*t.z,this.z=e*t.y-i*t.x,this}crossVectors(t,e){return this.x=t.y*e.z-t.z*e.y,this.y=t.z*e.x-t.x*e.z,this.z=t.x*e.y-t.y*e.x,this}equals(t){return this.x==t.x&&this.y==t.y&&this.z==t.z}getPositionFromMatrix(t){return this.x=t.elements[12],this.y=t.elements[13],this.z=t.elements[14],this}setEulerFromRotationMatrix(t,e){const i=t.elements,s=i[0],n=i[4],a=i[8],o=i[5],l=i[9],h=i[6],c=i[10];return void 0===e||"XYZ"===e?(this.y=Math.asin(r(a,-1,1)),Math.abs(a)<.99999?(this.x=Math.atan2(-l,c),this.z=Math.atan2(-n,s)):(this.x=Math.atan2(h,o),this.z=0)):console.error(`Error with vector's setEulerFromRotationMatrix: Unknown order: ${e}`),this}rotateAboutVector(t,e){t.normalize();const i=Math.cos(e),r=Math.sin(e),s=this.clone().multiplyScalar(i),n=t.clone().cross(this).multiplyScalar(r),a=t.clone().multiplyScalar(t.clone().dot(this)).multiplyScalar(1-i),o=s.add(n).add(a);return this.x=o.x,this.y=o.y,this.z=o.z,this}setFromMatrixPosition(t){const e=t.elements;return this.x=e[12],this.y=e[13],this.z=e[14],this}transformDirection(t){const{x:e}=this,{y:i}=this,{z:r}=this,s=t.elements;return this.x=s[0]*e+s[4]*i+s[8]*r,this.y=s[1]*e+s[5]*i+s[9]*r,this.z=s[2]*e+s[6]*i+s[10]*r,this.normalize()}clone(){return new Vector3(this.x,this.y,this.z)}unproject(t){const e=a;return e.multiplyMatrices(t.matrixWorld,e.getInverse(t.projectionMatrix)),this.applyMatrix4(e)}}l=new Vector3,h=new Vector3,c=new Vector3;class Matrix3{constructor(t=1,e=0,i=0,r=0,s=1,n=0,a=0,o=0,l=1){this.elements=new Float32Array(9),this.set(t,e,i,r,s,n,a,o,l)}set(t,e,i,r,s,n,a,o,l){const h=this.elements;return h[0]=t,h[3]=e,h[6]=i,h[1]=r,h[4]=s,h[7]=n,h[2]=a,h[5]=o,h[8]=l,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(t){const e=t.elements;this.set(e[0],e[3],e[6],e[1],e[4],e[7],e[2],e[5],e[8])}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[3]*=t,e[6]*=t,e[1]*=t,e[4]*=t,e[7]*=t,e[2]*=t,e[5]*=t,e[8]*=t,this}getInverse3(t){const e=t.elements,i=this.elements;i[0]=e[4]*e[8]-e[5]*e[7],i[3]=e[6]*e[5]-e[3]*e[8],i[6]=e[3]*e[7]-e[6]*e[4],i[1]=e[7]*e[2]-e[1]*e[8],i[4]=e[0]*e[8]-e[6]*e[2],i[7]=e[1]*e[6]-e[0]*e[7],i[2]=e[1]*e[5]-e[2]*e[4],i[5]=e[2]*e[3]-e[0]*e[5],i[8]=e[0]*e[4]-e[1]*e[3];const r=e[0]*i[0]+e[3]*i[1]+e[6]*i[2];return this.multiplyScalar(1/r),this}getInverse(t,e){const i=t.elements,r=this.elements;r[0]=i[10]*i[5]-i[6]*i[9],r[1]=-i[10]*i[1]+i[2]*i[9],r[2]=i[6]*i[1]-i[2]*i[5],r[3]=-i[10]*i[4]+i[6]*i[8],r[4]=i[10]*i[0]-i[2]*i[8],r[5]=-i[6]*i[0]+i[2]*i[4],r[6]=i[9]*i[4]-i[5]*i[8],r[7]=-i[9]*i[0]+i[1]*i[8],r[8]=i[5]*i[0]-i[1]*i[4];const s=i[0]*r[0]+i[1]*r[3]+i[2]*r[6];if(0===s){const t="Matrix3.getInverse(): can't invert matrix, determinant is 0";if(e)throw new Error(t);return console.warn(t),this.identity(),this}return this.multiplyScalar(1/s),this}getDeterminant(){const t=this.elements;return t[0]*t[4]*t[8]+t[1]*t[5]*t[6]+t[2]*t[3]*t[7]-t[2]*t[4]*t[6]-t[1]*t[3]*t[8]-t[0]*t[5]*t[7]}transpose(){let t;const e=this.elements;return t=e[1],e[1]=e[3],e[3]=t,t=e[2],e[2]=e[6],e[6]=t,t=e[5],e[5]=e[7],e[7]=t,this}clone(){const t=this.elements;return new Matrix3(t[0],t[3],t[6],t[1],t[4],t[7],t[2],t[5],t[8])}getMatrix4(){const t=this.elements;return new Matrix4(t[0],t[3],t[6],0,t[1],t[4],t[7],0,t[2],t[5],t[8],0)}}class Ray{constructor(t,e){this.origin=void 0!==t?t:new Vector3,this.direction=void 0!==e?e:new Vector3}set(t,e){return this.origin.copy(t),this.direction.copy(e),this}copy(t){return this.origin.copy(t.origin),this.direction.copy(t.direction),this}at(t,e){return(e||new Vector3).copy(this.direction).multiplyScalar(t).add(this.origin)}recast(t){const e=l;return this.origin.copy(this.at(t,e)),this}closestPointToPoint(t,e){const i=e||new Vector3;i.subVectors(t,this.origin);const r=i.dot(this.direction);return i.copy(this.direction).multiplyScalar(r).add(this.origin)}distanceToPoint(t){const e=l,i=e.subVectors(t,this.origin).dot(this.direction);return e.copy(this.direction).multiplyScalar(i).add(this.origin),e.distanceTo(t)}isIntersectionCylinder(){}isIntersectionSphere(t){return this.distanceToPoint(t.center)<=t.radius}isIntersectionPlane(t){return 0!==t.normal.dot(this.direction)||0===t.distanceToPoint(this.origin)}distanceToPlane(t){const e=t.normal.dot(this.direction);if(0===e)return 0===t.distanceToPoint(this.origin)?0:void 0;return-(this.origin.dot(t.normal)+t.constant)/e}intersectPlane(t,e){const i=this.distanceToPlane(t);if(void 0!==i)return this.at(i,e)}applyMatrix4(t){return this.direction.add(this.origin).applyMatrix4(t),this.origin.applyMatrix4(t),this.direction.sub(this.origin),this}clone(){return(new Ray).copy(this)}}function d(t,e,i,r,s,n){r=r*Math.PI/180,s=s*Math.PI/180,n=n*Math.PI/180;const a=t=>t*t,o=Math.cos(r),l=Math.cos(s),h=Math.cos(n),c=Math.sin(n);return new Matrix3(t,e*h,i*l,0,e*c,i*(o-l*h)/c,0,0,i*Math.sqrt(1-a(o)-a(l)-a(h)+2*o*l*h)/c)}},99:(t,e,i)=>{"use strict";i.r(e),i.d(e,{Cylinder:()=>Cylinder,Sphere:()=>Sphere,Triangle:()=>Triangle});var r=i(529);class Sphere{constructor(t={x:0,y:0,z:0},e=0){this.center=new r.Vector3(t.x,t.y,t.z),this.radius=e}set(t,e){return this.center.copy(t),this.radius=e,this}copy(t){return this.center.copy(t.center),this.radius=t.radius,this}applyMatrix4(t){return this.center.applyMatrix4(t),this.radius=this.radius*t.getMaxScaleOnAxis(),this}translate(t){return this.center.add(t),this}equals(t){return t.center.equals(this.center)&&t.radius===this.radius}clone(){return(new Sphere).copy(this)}}let s=new r.Vector3;class Cylinder{constructor(t=new r.Vector3,e=new r.Vector3,i=0){this.c1=t,this.c2=e,this.radius=i,this.direction=(new r.Vector3).subVectors(this.c2,this.c1).normalize()}copy(t){return this.c1.copy(t.c1),this.c2.copy(t.c2),this.direction.copy(t.direction),this.radius=t.radius,this}lengthSq(){return s.subVectors(this.c2,this.c1).lengthSq()}applyMatrix4(t){return this.direction.add(this.c1).applyMatrix4(t),this.c1.applyMatrix4(t),this.c2.applyMatrix4(t),this.direction.sub(this.c1).normalize(),this.radius=this.radius*t.getMaxScaleOnAxis(),this}}const n=new r.Vector3;class Triangle{constructor(t=new r.Vector3,e=new r.Vector3,i=new r.Vector3){this.a=t,this.b=e,this.c=i}copy(t){return this.a.copy(t.a),this.b.copy(t.b),this.c.copy(t.c),this}applyMatrix4(t){return this.a.applyMatrix4(t),this.b.applyMatrix4(t),this.c.applyMatrix4(t),this}getNormal(){var t=this.a.clone();return t.sub(this.b),n.subVectors(this.c,this.b),t.cross(n),t.normalize(),t}}},222:(t,e,i)=>{"use strict";i.r(e),i.d(e,{CC:()=>CC,Color:()=>Color,builtinColorSchemes:()=>h,chains:()=>l,elementColors:()=>a,htmlColors:()=>r,residues:()=>o,ssColors:()=>s});class Color{constructor(t,e,i){return this.r=0,this.g=0,this.b=0,arguments.length>1&&"number"==typeof t?(this.r=t||0,this.g=e||0,this.b=i||0,this):this.set(t||0)}set(t){return t instanceof Color?t.clone():("number"==typeof t?this.setHex(t):"object"==typeof t&&(this.r=(null==t?void 0:t.r)||0,this.g=(null==t?void 0:t.g)||0,this.b=(null==t?void 0:t.b)||0),this)}setHex(t){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(255&t)/255,this}getHex(){return Math.round(255*this.r)<<16|Math.round(255*this.g)<<8|Math.round(255*this.b)}clone(){return new Color(this.r,this.g,this.b)}copy(t){return this.r=t.r,this.g=t.g,this.b=t.b,this}scaled(){var t={};return t.r=Math.round(255*this.r),t.g=Math.round(255*this.g),t.b=Math.round(255*this.b),t.a=1,t}}class CC{static color(t){if(!t)return CC.cache[0];if(t instanceof Color)return t;if("number"==typeof t&&void 0!==CC.cache[t])return CC.cache[t];if(t&&Array.isArray(t))return t.map(CC.color);let e=CC.getHex(t),i=new Color(e);return CC.cache[e]=i,i}static getHex(t){var e;if(Array.isArray(t))return t.map(CC.getHex);if("string"==typeof t){let i=t;if(!isNaN(parseInt(i)))return parseInt(i);if(i=i.trim(),4==i.length&&"#"==i[0]&&(i="#"+i[1]+i[1]+i[2]+i[2]+i[3]+i[3]),7==i.length&&"#"==i[0])return parseInt(i.substring(1),16);let r=CC.rgbRegEx.exec(i);if(r){""!=r[1]&&console.log("WARNING: Opacity value in rgba ignored. Specify separately as opacity attribute.");let t=0;for(let e=2;e<5;e++){t*=256;let i=r[e].endsWith("%")?255*parseFloat(r[e])/100:parseFloat(r[e]);t+=Math.round(i)}return t}return(null===(e=null===window||void 0===window?void 0:window.$3Dmol)||void 0===e?void 0:e.htmlColors[t.toLowerCase()])||0}return t}}CC.rgbRegEx=/rgb(a?)\(\s*([^ ,\)\t]+)\s*,\s*([^ ,\)\t]+)\s*,\s*([^ ,\)\t]+)/i,CC.cache={0:new Color(0)};const r={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgrey:11119017,darkgreen:25600,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,grey:8421504,green:32768,greenyellow:11403055,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgrey:13882323,lightgreen:9498256,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},s={pyMol:{h:16711680,s:16776960,c:65280},Jmol:{h:16711808,s:16762880,c:16777215}},n={H:16777215,He:16761035,HE:16761035,Li:11674146,LI:11674146,B:65280,C:13158600,N:9408511,O:15728640,F:14329120,Na:255,NA:255,Mg:2263842,MG:2263842,Al:8421520,AL:8421520,Si:14329120,SI:14329120,P:16753920,S:16762930,Cl:65280,CL:65280,Ca:8421520,CA:8421520,Ti:8421520,TI:8421520,Cr:8421520,CR:8421520,Mn:8421520,MN:8421520,Fe:16753920,FE:16753920,Ni:10824234,NI:10824234,Cu:10824234,CU:10824234,Zn:10824234,ZN:10824234,Br:10824234,BR:10824234,Ag:8421520,AG:8421520,I:10494192,Ba:16753920,BA:16753920,Au:14329120,AU:14329120},a={defaultColor:16716947,Jmol:{H:16777215,He:14286847,HE:14286847,Li:13402367,LI:13402367,Be:12779264,BE:12779264,B:16758197,C:9474192,N:3166456,O:16715021,F:9494608,Ne:11789301,NE:11789301,Na:11230450,NA:11230450,Mg:9109248,MG:9109248,Al:12560038,AL:12560038,Si:1578e4,SI:1578e4,P:16744448,S:16777008,Cl:2093087,CL:2093087,Ar:8442339,AR:8442339,K:9388244,Ca:4062976,CA:4062976,Sc:15132390,SC:15132390,Ti:12567239,TI:12567239,V:10921643,Cr:9083335,CR:9083335,Mn:10255047,MN:10255047,Fe:14706227,FE:14706227,Co:15765664,CO:15765664,Ni:5296208,NI:5296208,Cu:13140019,CU:13140019,Zn:8224944,ZN:8224944,Ga:12750735,GA:12750735,Ge:6721423,GE:6721423,As:12419299,AS:12419299,Se:16752896,SE:16752896,Br:10889513,BR:10889513,Kr:6076625,KR:6076625,Rb:7351984,RB:7351984,Sr:65280,SR:65280,Y:9764863,Zr:9756896,ZR:9756896,Nb:7586505,NB:7586505,Mo:5551541,MO:5551541,Tc:3907230,TC:3907230,Ru:2396047,RU:2396047,Rh:687500,RH:687500,Pd:27013,PD:27013,Ag:12632256,AG:12632256,Cd:16767375,CD:16767375,In:10909043,IN:10909043,Sn:6717568,SN:6717568,Sb:10380213,SB:10380213,Te:13924864,TE:13924864,I:9699476,Xe:4366e3,XE:4366e3,Cs:5707663,CS:5707663,Ba:51456,BA:51456,La:7394559,LA:7394559,Ce:16777159,CE:16777159,Pr:14286791,PR:14286791,Nd:13107143,ND:13107143,Pm:10747847,PM:10747847,Sm:9437127,SM:9437127,Eu:6422471,EU:6422471,Gd:4587463,GD:4587463,Tb:3211207,TB:3211207,Dy:2097095,DY:2097095,Ho:65436,HO:65436,Er:58997,ER:58997,Tm:54354,TM:54354,Yb:48952,YB:48952,Lu:43812,LU:43812,Hf:5096191,HF:5096191,Ta:5089023,TA:5089023,W:2200790,Re:2522539,RE:2522539,Os:2516630,OS:2516630,Ir:1528967,IR:1528967,Pt:13684960,PT:13684960,Au:16765219,AU:16765219,Hg:12105936,HG:12105936,Tl:10900557,TL:10900557,Pb:5724513,PB:5724513,Bi:10375093,BI:10375093,Po:11230208,PO:11230208,At:7688005,AT:7688005,Rn:4358806,RN:4358806,Fr:4325478,FR:4325478,Ra:32e3,RA:32e3,Ac:7384058,AC:7384058,Th:47871,TH:47871,Pa:41471,PA:41471,U:36863,Np:33023,NP:33023,Pu:27647,PU:27647,Am:5528818,AM:5528818,Cm:7888099,CM:7888099,Bk:9064419,BK:9064419,Cf:10565332,CF:10565332,Es:11739092,ES:11739092,Fm:11739066,FM:11739066,Md:11734438,MD:11734438,No:12389767,NO:12389767,Lr:13041766,LR:13041766,Rf:13369433,RF:13369433,Db:13697103,DB:13697103,Sg:14221381,SG:14221381,Bh:14680120,BH:14680120,Hs:15073326,HS:15073326,Mt:15400998,MT:15400998},rasmol:n,defaultColors:Object.assign({},n),greenCarbon:Object.assign(Object.assign({},n),{C:65280}),cyanCarbon:Object.assign(Object.assign({},n),{C:65535}),magentaCarbon:Object.assign(Object.assign({},n),{C:16711935}),yellowCarbon:Object.assign(Object.assign({},n),{C:16776960}),whiteCarbon:Object.assign(Object.assign({},n),{C:16777215}),orangeCarbon:Object.assign(Object.assign({},n),{C:16753920}),purpleCarbon:Object.assign(Object.assign({},n),{C:8388736}),blueCarbon:Object.assign(Object.assign({},n),{C:255})},o={amino:{ALA:13158600,ARG:1334015,ASN:56540,ASP:15075850,CYS:15132160,GLN:56540,GLU:15075850,GLY:15461355,HIS:8553170,ILE:1016335,LEU:1016335,LYS:1334015,MET:15132160,PHE:3289770,PRO:14456450,SER:16422400,THR:16422400,TRP:11819700,TYR:3289770,VAL:1016335,ASX:16738740,GLX:16738740},shapely:{ALA:9240460,ARG:124,ASN:16743536,ASP:10485826,CYS:16777072,GLN:16731212,GLU:6684672,GLY:16777215,HIS:7368959,ILE:19456,LEU:4546117,LYS:4671416,MET:12099650,PHE:5459026,PRO:5395026,SER:16740418,THR:12078080,TRP:5195264,TYR:9203788,VAL:16747775,ASX:16711935,GLX:16711935},nucleic:{A:10526975,G:16740464,I:8454143,C:16747595,T:10551200,U:16744576}},l={atom:{A:12636415,B:11599792,C:16761032,D:16777088,E:16761087,F:11596016,G:16765040,H:15761536,I:16113331,J:49151,K:13458524,L:6737322,M:10145074,N:15631086,O:52945,P:65407,Q:3978097,R:139,S:12433259,T:25600,U:8388608,V:8421376,W:8388736,X:32896,Y:12092939,Z:11674146},hetatm:{A:9478351,B:8441752,C:13602992,D:13619056,E:13603023,F:8437952,G:13607008,H:12603504,I:12955267,J:42959,K:11881548,L:5682578,M:9090346,N:12481214,O:46753,P:53103,Q:3447649,R:187,S:10854235,T:37888,U:11534336,V:11579392,W:11534512,X:45232,Y:15250963,Z:12726834}},h={ssPyMol:{prop:"ss",map:s.pyMol},ssJmol:{prop:"ss",map:s.Jmol},Jmol:{prop:"elem",map:a.Jmol},amino:{prop:"resn",map:o.amino},shapely:{prop:"resn",map:o.shapely},nucleic:{prop:"resn",map:o.nucleic},chain:{prop:"chain",map:l.atom},rasmol:{prop:"elem",map:a.rasmol},default:{prop:"elem",map:a.defaultColors},greenCarbon:{prop:"elem",map:a.greenCarbon},chainHetatm:{prop:"chain",map:l.hetatm},cyanCarbon:{prop:"elem",map:a.cyanCarbon},magentaCarbon:{prop:"elem",map:a.magentaCarbon},purpleCarbon:{prop:"elem",map:a.purpleCarbon},whiteCarbon:{prop:"elem",map:a.whiteCarbon},orangeCarbon:{prop:"elem",map:a.orangeCarbon},yellowCarbon:{prop:"elem",map:a.yellowCarbon},blueCarbon:{prop:"elem",map:a.blueCarbon}}},421:(t,e,i)=>{"use strict";i.r(e),i.d(e,{CAP:()=>it,CC:()=>s.CC,CONTEXTS_PER_VIEWPORT:()=>Wt,Color:()=>s.Color,CustomLinear:()=>r.CustomLinear,Cylinder:()=>J.Cylinder,GLDraw:()=>rt,GLModel:()=>GLModel,GLShape:()=>GLShape,GLViewer:()=>GLViewer,GLVolumetricRender:()=>GLVolumetricRender,Gradient:()=>r.Gradient,GradientType:()=>r.GradientType,Label:()=>Label,LabelCount:()=>a,MarchingCube:()=>MarchingCube,MarchingCubeInitializer:()=>MarchingCubeInitializer,Matrix3:()=>$.Matrix3,Matrix4:()=>$.Matrix4,Parsers:()=>K,PausableTimer:()=>L.PausableTimer,PointGrid:()=>PointGrid,ProteinSurface:()=>ProteinSurface,Quaternion:()=>$.Quaternion,ROYGB:()=>r.ROYGB,RWB:()=>r.RWB,Ray:()=>$.Ray,Sinebow:()=>r.Sinebow,Sphere:()=>J.Sphere,SurfaceType:()=>Q,Triangle:()=>J.Triangle,Vector2:()=>$.Vector2,Vector3:()=>$.Vector3,VolumeData:()=>st.VolumeData,adjustVolumeStyle:()=>L.adjustVolumeStyle,applyPartialCharges:()=>h,autoinit:()=>Xt,autoload:()=>$t,base64ToArray:()=>L.base64ToArray,bondLength:()=>C.bondLength,builtinColorSchemes:()=>s.builtinColorSchemes,builtinGradients:()=>r.builtinGradients,chains:()=>s.chains,clamp:()=>$.clamp,conversionMatrix3:()=>$.conversionMatrix3,createStereoViewer:()=>Zt,createViewer:()=>qt,createViewerGrid:()=>Yt,deepCopy:()=>L.deepCopy,degToRad:()=>$.degToRad,download:()=>L.download,drawCartoon:()=>xt,elementColors:()=>s.elementColors,extend:()=>L.extend,get:()=>L.get,getAtomProperty:()=>L.getAtomProperty,getColorFromStyle:()=>L.getColorFromStyle,getElement:()=>L.getElement,getExtent:()=>L.getExtent,getGradient:()=>r.getGradient,getPropertyRange:()=>L.getPropertyRange,getbin:()=>L.getbin,htmlColors:()=>s.htmlColors,inflateString:()=>L.inflateString,isEmptyObject:()=>L.isEmptyObject,isNumeric:()=>L.isNumeric,makeFunction:()=>L.makeFunction,mergeGeos:()=>L.mergeGeos,normalizeValue:()=>r.normalizeValue,partialCharges:()=>l,processing_autoinit:()=>Kt,residues:()=>s.residues,setBondLength:()=>C.setBondLength,setSyncSurface:()=>et,specStringToObject:()=>L.specStringToObject,splitMesh:()=>wt,ssColors:()=>s.ssColors,subdivide_spline:()=>nt,syncSurface:()=>tt,viewers:()=>Qt});var r=i(546),s=i(222),n=i(638);let a=0;function o(t,e,i){var r=i;return void 0!==t&&(t instanceof s.Color?r=t.scaled():void 0!==(r=s.CC.color(t)).scaled&&(r=r.scaled())),void 0!==e&&(r.a=parseFloat(e)),r}class Label{constructor(t,e){this.id=a++,this.stylespec=e||{},this.canvas=document.createElement("canvas"),this.canvas.width=134,this.canvas.height=35,this.context=this.canvas.getContext("2d"),this.sprite=new n.Sprite,this.text=t,this.frame=this.stylespec.frame}getStyle(){return this.stylespec}hide(){this.sprite&&(this.sprite.visible=!1)}show(){this.sprite&&(this.sprite.visible=!0)}setContext(){var t=this.stylespec,e=void 0!==t.useScreen&&t.useScreen,i=t.showBackground;"0"!==i&&"false"!==i||(i=!1),void 0===i&&(i=!0);var s=t.font?t.font:"sans-serif",a=parseInt(t.fontSize)?parseInt(t.fontSize):18,l=o(t.fontColor,t.fontOpacity,{r:255,g:255,b:255,a:1}),h=t.padding?t.padding:4,c=t.borderThickness?t.borderThickness:0,d=o(t.backgroundColor,t.backgroundOpacity,{r:0,g:0,b:0,a:1}),u=o(t.borderColor,t.borderOpacity,d),f=t.position?t.position:{x:-10,y:1,z:1},p=void 0===t.inFront||t.inFront;"false"!==p&&"0"!==p||(p=!1);var g=t.alignment||n.SpriteAlignment.topLeft;"string"==typeof g&&g in n.SpriteAlignment&&(g=n.SpriteAlignment[g]);var m="";t.bold&&(m="bold "),this.context.font=m+a+"px "+s;var v=this.context.measureText(this.text).width;i||(c=0);var _=v+2.5*c+2*h,y=1.25*a+2*c+2*h;if(t.backgroundImage){var b=t.backgroundImage,x=t.backgroundWidth?t.backgroundWidth:b.width,w=t.backgroundHeight?t.backgroundHeight:b.height;x>_&&(_=x),w>y&&(y=w)}if(this.canvas.width=_,this.canvas.height=y,this.context.clearRect(0,0,this.canvas.width,this.canvas.height),m="",t.bold&&(m="bold "),this.context.font=m+a+"px "+s,this.context.fillStyle="rgba("+d.r+","+d.g+","+d.b+","+d.a+")",this.context.strokeStyle="rgba("+u.r+","+u.g+","+u.b+","+u.a+")",t.backgroundGradient){let e=this.context.createLinearGradient(0,y/2,_,y/2),i=r.Gradient.getGradient(t.backgroundGradient),s=i.range(),n=-1,a=1;s&&(n=s[0],a=s[1]);let l=a-n;for(let t=0;t<1.01;t+=.1){let r=o(i.valueToHex(n+l*t)),s="rgba("+r.r+","+r.g+","+r.b+","+r.a+")";e.addColorStop(t,s)}this.context.fillStyle=e}this.context.lineWidth=c,i&&function(t,e,i,r,s,n,a){t.beginPath(),t.moveTo(e+n,i),t.lineTo(e+r-n,i),t.quadraticCurveTo(e+r,i,e+r,i+n),t.lineTo(e+r,i+s-n),t.quadraticCurveTo(e+r,i+s,e+r-n,i+s),t.lineTo(e+n,i+s),t.quadraticCurveTo(e,i+s,e,i+s-n),t.lineTo(e,i+n),t.quadraticCurveTo(e,i,e+n,i),t.closePath(),t.fill(),a&&t.stroke()}(this.context,c,c,_-2*c,y-2*c,6,c>0),t.backgroundImage&&this.context.drawImage(b,0,0,_,y),this.context.fillStyle="rgba("+l.r+","+l.g+","+l.b+","+l.a+")",this.context.fillText(this.text,c+h,a+c+h,v);var A=new n.Texture(this.canvas);A.needsUpdate=!0,this.sprite.material=new n.SpriteMaterial({map:A,useScreenCoordinates:e,alignment:g,depthTest:!p,screenOffset:t.screenOffset||null}),this.sprite.scale.set(1,1,1),this.sprite.position.set(f.x,f.y,f.z)}dispose(){void 0!==this.sprite.material.map&&this.sprite.material.map.dispose(),void 0!==this.sprite.material&&this.sprite.material.dispose()}}const l={"ALA:N":-.15,"ALA:CA":.1,"ALA:CB":0,"ALA:C":.6,"ALA:O":-.55,"ARG:N":-.15,"ARG:CA":.1,"ARG:CB":0,"ARG:CG":0,"ARG:CD":.1,"ARG:NE":-.1,"ARG:CZ":.5,"ARG:NH1":.25,"ARG:NH2":.25,"ARG:C":.6,"ARG:O":-.55,"ASN:N":-.15,"ASN:CA":.1,"ASN:CB":0,"ASN:CG":.55,"ASN:OD1":-.55,"ASN:ND2":0,"ASN:C":.6,"ASN:O":-.55,"ASP:N":-.15,"ASP:CA":.1,"ASP:CB":0,"ASP:CG":.14,"ASP:OD1":-.57,"ASP:OD2":-.57,"ASP:C":.6,"ASP:O":-.55,"CYS:N":-.15,"CYS:CA":.1,"CYS:CB":.19,"CYS:SG":-.19,"CYS:C":.6,"CYS:O":-.55,"GLN:N":-.15,"GLN:CA":.1,"GLN:CB":0,"GLN:CG":0,"GLN:CD":.55,"GLN:OE1":-.55,"GLN:NE2":0,"GLN:C":.6,"GLN:O":-.55,"GLU:N":-.15,"GLU:CA":.1,"GLU:CB":0,"GLU:CG":0,"GLU:CD":.14,"GLU:OE1":-.57,"GLU:OE2":-.57,"GLU:C":.6,"GLU:O":-.55,"GLY:N":-.15,"GLY:CA":.1,"GLY:C":.6,"GLY:O":-.55,"HIS:N":-.15,"HIS:CA":.1,"HIS:CB":0,"HIS:CG":.1,"HIS:ND1":-.1,"HIS:CD2":.1,"HIS:NE2":-.4,"HIS:CE1":.3,"HIS:C":.6,"HIS:O":-.55,"ILE:N":-.15,"ILE:CA":.1,"ILE:CB":0,"ILE:CG2":0,"ILE:CG1":0,"ILE:CD":0,"ILE:C":.6,"ILE:O":-.55,"LEU:N":-.15,"LEU:CA":.1,"LEU:CB":0,"LEU:CG":0,"LEU:CD1":0,"LEU:CD2":0,"LEU:C":.6,"LEU:O":-.55,"LYS:N":-.15,"LYS:CA":.1,"LYS:CB":0,"LYS:CG":0,"LYS:CD":0,"LYS:CE":.25,"LYS:NZ":.75,"LYS:C":.6,"LYS:O":-.55,"MET:N":-.15,"MET:CA":.1,"MET:CB":0,"MET:CG":.06,"MET:SD":-.12,"MET:CE":.06,"MET:C":.6,"MET:O":-.55,"PHE:N":-.15,"PHE:CA":.1,"PHE:CB":0,"PHE:CG":0,"PHE:CD1":0,"PHE:CD2":0,"PHE:CE1":0,"PHE:CE2":0,"PHE:CZ":0,"PHE:C":.6,"PHE:O":-.55,"PRO:N":-.25,"PRO:CD":.1,"PRO:CA":.1,"PRO:CB":0,"PRO:CG":0,"PRO:C":.6,"PRO:O":-.55,"SER:N":-.15,"SER:CA":.1,"SER:CB":.25,"SER:OG":-.25,"SER:C":.6,"SER:O":-.55,"THR:N":-.15,"THR:CA":.1,"THR:CB":.25,"THR:OG1":-.25,"THR:CG2":0,"THR:C":.6,"THR:O":-.55,"TRP:N":-.15,"TRP:CA":.1,"TRP:CB":0,"TRP:CG":-.03,"TRP:CD2":.1,"TRP:CE2":-.04,"TRP:CE3":-.03,"TRP:CD1":.06,"TRP:NE1":-.06,"TRP:CZ2":0,"TRP:CZ3":0,"TRP:CH2":0,"TRP:C":.6,"TRP:O":-.55,"TYR:N":-.15,"TYR:CA":.1,"TYR:CB":0,"TYR:CG":0,"TYR:CD1":0,"TYR:CE1":0,"TYR:CD2":0,"TYR:CE2":0,"TYR:CZ":.25,"TYR:OH":-.25,"TYR:C":.6,"TYR:O":-.55,"VAL:N":-.15,"VAL:CA":.1,"VAL:CB":0,"VAL:CG1":0,"VAL:CG2":0,"VAL:C":.6,"VAL:O":-.55};function h(t,e){if((!e||void 0===t.partialCharge)&&t.resn&&t.atom){var i=t.resn+":"+t.atom;t.properties.partialCharge=l[i]}}var c=i(797),d=i(865),u=i(392);function f(t,e){for(var i=[[]],r=void 0===(e=e||{}).assignBonds||e.assignBonds,s=t.trimStart().split(/\r?\n|\r/);s.length>0&&!(s.length<3);){var a=parseInt(s[0]);if(isNaN(a)||a<=0)break;if(s.length1){var l=new Float32Array(o[1].split(/\s+/)),h=new n.Matrix3(l[0],l[3],l[6],l[1],l[4],l[7],l[2],l[5],l[8]);i.modelData=[{cryst:{matrix:h}}]}for(var c=2,d=i[i.length-1].length,f=d+a,p=d;p=7&&(m.dx=parseFloat(g[4]),m.dy=parseFloat(g[5]),m.dz=parseFloat(g[6]))}if(!e.multimodel)break;i.push([]),s.splice(0,c)}if(r)for(let t=0;t3&&r[3].length>38&&(i=r[3].substring(34,39)),"V2000"===i?function(t,e){var i=[[]],r=!1;for(void 0!==e.keepH&&(r=!e.keepH);t.length>0&&!(t.length<4);){var s=parseInt(t[3].substring(0,3));if(isNaN(s)||s<=0)break;var n=parseInt(t[3].substring(3,6)),a=4;if(t.length<4+s+n)break;var o,l,h=[],c=i[i.length-1].length,d=c+s;for(o=c;o0&&!(t.length<8)&&t[4].startsWith("M V30 BEGIN CTAB")&&t[5].startsWith("M V30 COUNTS")&&!(t[5].length<14);){var s=t[5].substring(13).match(/\S+/g);if(s.length<2)break;var n=parseInt(s[0]);if(isNaN(n)||n<=0)break;var a=parseInt(s[1]),o=7;if(t.length<8+n+a)break;var l,h=[],c=i[i.length-1].length,d=c+n;for(l=c;l4){var f={},p=u[1].replace(/ /g,"");f.atom=f.elem=p[0].toUpperCase()+p.substring(1).toLowerCase(),"H"===f.elem&&r||(f.serial=l,h[l]=i[i.length-1].length,f.x=parseFloat(u[2]),f.y=parseFloat(u[3]),f.z=parseFloat(u[4]),f.hetflag=!0,f.bonds=[],f.bondOrder=[],f.properties={},f.index=i[i.length-1].length,i[i.length-1].push(f))}}if("M V30 END ATOM"!==t[o])break;if(o++,0===a||"M V30 BEGIN BOND"!==t[o])break;for(o++,l=0;l3){var m=h[parseInt(g[2])-1+c],v=h[parseInt(g[3])-1+c],_=parseFloat(g[1]);void 0!==m&&void 0!==v&&(i[i.length-1][m].bonds.push(v),i[i.length-1][m].bondOrder.push(_),i[i.length-1][v].bonds.push(m),i[i.length-1][v].bondOrder.push(_))}}if(!e.multimodel)break;for(e.onemol||i.push([]);"$$$$"!==t[o]&&oi)break;if(t.atom==n.atom)continue;const o=Math.abs(t.y-n.y);if(o>i)continue;const l=Math.abs(t.x-n.x);if(l>i)continue;const h=l*l+o*o+e*e;h>r||t.chain==n.chain&&Math.abs(t.resi-n.resi)<4||(h1.001;)i[t]-=1,r[t]-=1}const s=new n.Vector3(r[0],r[1],r[2]);return s.applyMatrix3(h),s};if(i.normalizeAssembly&&r)for(let i=0;i.001&&(l=i),t[i].translate(s)}if(s){if(t.length>1)for(let i=0;i=0){const i=new n.Vector3;for(let r=0;r1)break;(0,_.areConnected)(r,t,e)&&(-1===r.bonds.indexOf(t.index)&&(r.bonds.push(t.index),r.bondOrder.push(1),t.bonds.push(r.index),t.bondOrder.push(1)),r.resi!==t.resi&&(s=!0))}}}function x(t,e={}){const i=[],r=!e.doAssembly,s=i.modelData=[],a=void 0===e.assignBonds||e.assignBonds;function o(t,e){const i=[];let r=0,s=0;for(;s-1){let e=d.split("");e[t]="_",d=(d=e.join("")).substring(0,t)+"_"+d.substring(t+1)}}h.push(d)}}let u=0;for(;uMOLECULE/),n=t.search(/@ATOM/);if(-1==s||-1==n)return i;for(var a=t.substring(s).split(/\r?\n|\r/);a.length>0;){var o=[],l=a[2].replace(/^\s+/,"").replace(/\s+/g," ").split(" "),h=parseInt(l[0]),c=0;l.length>1&&(c=parseInt(l[1]));var d,u=4;for(d=3;dATOM"==a[d]){u=d+1;break}var f=i[i.length-1].length,p=f+h;for(d=f;dBOND"==a[u++]){b=!0;break}if(b&&c)for(d=0;d0&&"H"===i[0]&&"Hg"!==i&&"He"!==i&&"Hf"!==i&&"Hs"!==i&&"Ho"!==i&&(i="H"),i.length>1&&(i=i[0].toUpperCase()+i.substring(1).toLowerCase(),void 0===C.bondTable[i]?i=i[0]:e&&("Ca"===i||"Cd"===i)&&(i="C")),i}function M(t){for(const e in t)return!1;return!0}function z(t,e,i){const r=[],s=void 0===e.assignBonds||e.assignBonds,a=!e.keepH,o=!!e.noSecondaryStructure,l=!e.noComputeSecondaryStructure,h=!e.doAssembly,c=e.altLoc?e.altLoc:"A",d={symmetries:[],cryst:void 0};let u,f=[];const p=[];let g;const _={};for(let e=0;e=4?1:i}}else _[a]=1,0!=e.bonds.length&&e.bonds[e.bonds.length-1]===n||(e.bonds.push(n),e.bondOrder.push(1))}}else if("HELIX "===s){o=g.substring(19,20),l=parseInt(g.substring(21,25)),m=parseInt(g.substring(33,37)),o in i||(i[o]={}),i[o][l]="h1";for(let t=l+1;t1&&("1"==t[1]?e.ssbegin=!0:"2"==t[1]&&(e.ssend=!0))}}return[r,d,f]}function T(t,e){e=e||{};var i=[],r={};i.modelData=[];for(var s=t.split(/\r?\n|\r/);s.length>0;){var n=z(s,e,r),a=n[0],o=n[1];if(s=n[2],0!=a.length){if(e.multimodel&&e.onemol&&i.length>0)for(var l=i[0].length,h=0;h1&&e[1].toUpperCase()!=e[1]&&(g=e.substring(0,2)),l="H"==s[0],n[t]=i[i.length-1].length,i[i.length-1].push({resn:r,x:c,y:d,z:u,elem:g,hetflag:l,chain:a,resi:o,serial:t,atom:e,bonds:[],ss:"c",bondOrder:[],properties:{charge:f,partialCharge:f,radius:p},pdbline:s})}else if("CONECT"==o){var v=parseInt(s.substring(6,11)),_=i[i.length-1][n[v]];for(let t=0;t<4;t++){var y=parseInt(s.substring([11,16,21,26][t],[11,16,21,26][t]+5)),x=i[i.length-1][n[y]];void 0!==_&&void 0!==x&&(_.bonds.push(n[y]),_.bondOrder.push(1))}}}for(let t=0;t0){var R=g.bioAssemblyList[l].transformList;for(h=0,p=R.length;h{t.chainIndexList.forEach((e=>{N[e]="polymer"==t.type}))}));var G=0;for(f=0;f=A.length||O(A[et]!=tt))&&(J=!0)}var it=g.groupIdList[x],rt=X.groupName;let t=X.chemCompType;var st=w;let e=D.has(t)||!N[b];for(d=0;d=w){G=t;break}let s=H[e],n=H[i],a=j[s],o=j[n];a&&o&&(a.bonds.push(n),a.bondOrder.push(r),o.bonds.push(s),o.bondOrder.push(r))}e.multimodel&&(e.onemol||_.push([]))}if(!o)for(let t=0;t<_.length;t++)v(y[t].symmetries,_[t],e,y[t].cryst);return a&&!s&&m(_,e.hbondCutoff),_}function R(t){var e,i=[],r=0,s=t.split(/\r?\n|\r/);if(!(s.length>0&&s[0].includes("VERSION")))return[];var n=s.filter((function(t){return t.includes("POINTERS")||t.includes("ATOM_NAME")||t.includes("CHARGE")||t.includes("RADII")||t.includes("BONDS_INC_HYDROGEN")||t.includes("BONDS_WITHOUT_HYDROGEN")})),a=c("POINTERS");if(-1==a)return[];var o=d(a),l=parseInt(s[a+1].slice(0,o[1]));if(isNaN(l)||l<=0)return[];if(-1==(a=c("ATOM_NAME")))return[];var h=(o=d(a))[0];for(let t=0;t0){for(;!s[e].includes("FORMAT");)e++;return e}return-1}function d(t){var e=s[t].match(/\((\d*)\S*/),i=s[t].match(/[a-zA-Z](\d*)\)\s*/);return null==i&&(i=s[t].match(/[a-zA-Z](\d*)\.\d*\)\s*/)),[e[1],i[1]]}return[i]}function P(t,e){const i=[],r=t.split(/\r?\n|\r/);for(;r.length>0;){const t=parseInt(r[1]);if(r.length<3||isNaN(t)||t<=0||r.length44&&(n.dx=10*parseFloat(i.slice(44,52)),n.dy=10*parseFloat(i.slice(52,60)),n.dz=10*parseFloat(i.slice(60,68))),e[t]=n}if(r.length<=s+3){const t=r[s++].trim().split(/\s+/);if(3===t.length){for(let e=0;e<3;e++)t[e]=(10*parseFloat(t[e])).toString();i.box=t}}r.splice(0,++s)}for(let t=0;t=0;i--)e=j(e,t.encoding[i]);return e}function j(t,e){switch(e.kind){case"ByteArray":switch(e.type){case N.IntDataType.Uint8:return t;case N.IntDataType.Int8:return function(t){return new Int8Array(t.buffer,t.byteOffset)}(t);case N.IntDataType.Int16:return function(t){return q(t,2,Int16Array)}(t);case N.IntDataType.Uint16:return function(t){return q(t,2,Uint16Array)}(t);case N.IntDataType.Int32:return function(t){return q(t,4,Int32Array)}(t);case N.IntDataType.Uint32:return function(t){return q(t,4,Uint32Array)}(t);case N.FloatDataType.Float32:return function(t){return q(t,4,Float32Array)}(t);case N.FloatDataType.Float64:return function(t){return q(t,8,Float64Array)}(t);default:throw new Error("unreachable")}case"FixedPoint":return function(t,e){const i=t.length,r=W(e.srcType,i),s=1/e.factor;for(let e=0;et.name)),getField(t){const r=e[t];if(r)return i[t]||(i[t]=V(r.data)),i[t]}}}var Z=i(471);class Connectivity{constructor(t){if(this.C={},t){let e=t.getField("comp_id"),i=t.getField("atom_id_1"),r=t.getField("atom_id_2"),s=t.getField("value_order");for(let t=0;t0&&(n.bonds.push(a.index),a.bonds.push(n.index),n.bondOrder.push(o),a.bondOrder.push(o))}}for(let t of e.C){let e=t[0],i=t[1],r=t[2],s=this.geta(e),n=this.geta(i);null!=s&&null!=n&&(s.bonds.push(n.index),n.bonds.push(s.index),s.bondOrder.push(r),n.bondOrder.push(r))}}}function X(t,e){var i=!e.keepH,r=e.altLoc?e.altLoc:"A",s=!e.noComputeSecondaryStructure;const a=!e.doAssembly,o=void 0===e.assignBonds||e.assignBonds;if("string"==typeof t)try{t=(0,L.base64ToArray)(t)}catch(e){t=(new TextEncoder).encode(t)}else t=new Uint8Array(t);var l=Z.decodeMsgpack(t);31==l&&(t=(0,L.inflateString)(t,!1),l=Z.decodeMsgpack(t));var h=[],c=h.modelData=[],d=l.dataBlocks.length;if(0==d)return h;e.multimodel||(d=1);for(let t=0;t1&&("1"==t[1]?i.ssbegin=!0:"2"==t[1]&&(i.ssend=!0))}}}e.multimodel&&t=0||window.navigator.userAgent.indexOf("Trident/")>=0)&&(tt=!0);class MarchingCubeInitializer{constructor(){this.ISDONE=2,this.edgeTable=new Uint32Array([0,0,0,0,0,0,0,2816,0,0,0,1792,0,3328,3584,3840,0,0,0,138,0,21,0,134,0,0,0,652,0,2067,3865,3600,0,0,0,42,0,0,0,294,0,0,21,28,0,3875,1049,3360,0,168,162,170,0,645,2475,2210,0,687,293,172,4010,3747,3497,3232,0,0,0,0,0,69,0,900,0,0,0,1792,138,131,1608,1920,0,81,0,2074,84,85,84,86,0,81,0,3676,330,1105,1881,1616,0,0,0,42,0,69,0,502,0,0,21,3580,138,2035,1273,1520,2816,104,2337,106,840,581,367,102,2816,3695,3429,3180,1898,1635,1385,1120,0,0,0,0,0,0,0,3910,0,0,69,588,42,2083,41,2880,0,0,0,1722,0,2293,4095,3830,0,255,757,764,2538,2291,3065,2800,0,0,81,338,0,3925,1119,3414,84,855,85,340,2130,2899,89,2384,1792,712,194,1162,4036,3781,3535,3270,708,719,197,204,3018,2755,2505,2240,0,0,0,0,168,420,168,1958,162,162,676,2988,170,163,680,928,3328,3096,3328,3642,52,53,1855,1590,2340,2111,2869,2620,298,51,825,560,3584,3584,3090,3482,1668,1941,1183,1430,146,2975,2069,2460,154,915,153,400,3840,3592,3329,3082,1796,1541,1295,1030,2818,2575,2309,2060,778,515,265,0]),this.triTable=[[],[],[],[],[],[],[],[11,9,8],[],[],[],[8,10,9],[],[10,8,11],[9,11,10],[8,10,9,8,11,10],[],[],[],[1,7,3],[],[4,2,0],[],[2,1,7],[],[],[],[2,7,3,2,9,7],[],[1,4,11,1,0,4],[3,8,0,11,9,4,11,10,9],[4,11,9,11,10,9],[],[],[],[5,3,1],[],[],[],[2,5,8,2,1,5],[],[],[2,4,0],[3,2,4],[],[0,9,1,8,10,5,8,11,10],[3,4,0,3,10,4],[5,8,10,8,11,10],[],[3,5,7],[7,1,5],[1,7,3,1,5,7],[],[9,2,0,9,7,2],[0,3,8,1,7,11,1,5,7],[11,1,7,1,5,7],[],[9,1,0,5,3,2,5,7,3],[8,2,5,8,0,2],[2,5,3,5,7,3],[3,9,1,3,8,9,7,11,10,7,10,5],[9,1,0,10,7,11,10,5,7],[3,8,0,7,10,5,7,11,10],[11,5,7,11,10,5],[],[],[],[],[],[0,6,2],[],[7,2,9,7,9,8],[],[],[],[8,10,9],[7,1,3],[7,1,0],[6,9,3,6,10,9],[7,10,8,10,9,8],[],[6,0,4],[],[11,1,4,11,3,1],[2,4,6],[2,0,4,2,4,6],[2,4,6],[1,4,2,4,6,2],[],[6,0,4],[],[2,11,3,6,9,4,6,10,9],[8,6,1,8,1,3],[10,0,6,0,4,6],[8,0,3,9,6,10,9,4,6],[10,4,6,10,9,4],[],[],[],[5,3,1],[],[0,6,2],[],[7,4,8,5,2,1,5,6,2],[],[],[2,4,0],[7,4,8,2,11,3,10,5,6],[7,1,3],[5,6,10,0,9,1,8,7,4],[5,6,10,7,0,3,7,4,0],[10,5,6,4,8,7],[9,11,8],[3,5,6],[0,5,11,0,11,8],[6,3,5,3,1,5],[3,9,6,3,8,9],[9,6,0,6,2,0],[0,3,8,2,5,6,2,1,5],[1,6,2,1,5,6],[9,11,8],[1,0,9,6,10,5,11,3,2],[6,10,5,2,8,0,2,11,8],[3,2,11,10,5,6],[10,5,6,9,3,8,9,1,3],[0,9,1,5,6,10],[8,0,3,10,5,6],[10,5,6],[],[],[],[],[],[],[],[1,10,2,9,11,6,9,8,11],[],[],[6,0,2],[3,6,9,3,2,6],[3,5,1],[0,5,1,0,11,5],[0,3,5],[6,9,11,9,8,11],[],[],[],[4,5,9,7,1,10,7,3,1],[],[11,6,7,2,4,5,2,0,4],[11,6,7,8,0,3,1,10,2,9,4,5],[6,7,11,1,10,2,9,4,5],[],[4,1,0,4,5,1,6,7,3,6,3,2],[9,4,5,0,6,7,0,2,6],[4,5,9,6,3,2,6,7,3],[6,7,11,5,3,8,5,1,3],[6,7,11,4,1,0,4,5,1],[4,5,9,3,8,0,11,6,7],[9,4,5,7,11,6],[],[],[0,6,4],[8,6,4,8,1,6],[],[0,10,2,0,9,10,4,8,11,4,11,6],[10,2,1,6,0,3,6,4,0],[10,2,1,11,4,8,11,6,4],[4,2,6],[1,0,9,2,4,8,2,6,4],[2,4,0,2,6,4],[8,2,4,2,6,4],[11,4,1,11,6,4],[0,9,1,4,11,6,4,8,11],[3,6,0,6,4,0],[8,6,4,8,11,6],[10,8,9],[6,3,9,6,7,3],[6,7,1],[10,7,1,7,3,1],[7,11,6,8,10,2,8,9,10],[11,6,7,10,0,9,10,2,0],[2,1,10,7,11,6,8,0,3],[1,10,2,6,7,11],[7,2,6,7,9,2],[1,0,9,3,6,7,3,2,6],[7,0,6,0,2,6],[2,7,3,2,6,7],[7,11,6,3,9,1,3,8,9],[9,1,0,11,6,7],[0,3,8,11,6,7],[11,6,7],[],[],[],[],[5,3,7],[8,5,2,8,7,5],[5,3,7],[1,10,2,5,8,7,5,9,8],[1,7,5],[1,7,5],[9,2,7,9,7,5],[11,3,2,8,5,9,8,7,5],[1,3,7,1,7,5],[0,7,1,7,5,1],[9,3,5,3,7,5],[9,7,5,9,8,7],[8,10,11],[3,4,10,3,10,11],[8,10,11],[5,9,4,1,11,3,1,10,11],[2,4,5],[5,2,4,2,0,4],[0,3,8,5,9,4,10,2,1],[2,1,10,9,4,5],[2,8,5,2,11,8],[3,2,11,1,4,5,1,0,4],[9,4,5,8,2,11,8,0,2],[11,3,2,9,4,5],[8,5,3,5,1,3],[5,0,4,5,1,0],[3,8,0,4,5,9],[9,4,5],[11,9,10],[11,9,10],[1,11,4,1,10,11],[8,7,4,11,1,10,11,3,1],[2,7,9,2,9,10],[4,8,7,0,10,2,0,9,10],[2,1,10,0,7,4,0,3,7],[10,2,1,8,7,4],[1,7,4],[3,2,11,4,8,7,9,1,0],[11,4,2,4,0,2],[2,11,3,7,4,8],[4,1,7,1,3,7],[1,0,9,8,7,4],[3,4,0,3,7,4],[8,7,4],[8,9,10,8,10,11],[3,9,11,9,10,11],[0,10,8,10,11,8],[10,3,1,10,11,3],[2,8,10,8,9,10],[9,2,0,9,10,2],[8,0,3,1,10,2],[10,2,1],[1,11,9,11,8,9],[11,3,2,0,9,1],[11,0,2,11,8,0],[11,3,2],[8,1,3,8,9,1],[9,1,0],[8,0,3],[]],this.edgeTable2=[0,265,515,778,2060,2309,2575,2822,1030,1295,1541,1804,3082,3331,3593,3840,400,153,915,666,2460,2197,2975,2710,1430,1183,1941,1692,3482,3219,3993,3728,560,825,51,314,2620,2869,2111,2358,1590,1855,1077,1340,3642,3891,3129,3376,928,681,419,170,2988,2725,2479,2214,1958,1711,1445,1196,4010,3747,3497,3232,2240,2505,2755,3018,204,453,719,966,3270,3535,3781,4044,1226,1475,1737,1984,2384,2137,2899,2650,348,85,863,598,3414,3167,3925,3676,1370,1107,1881,1616,2800,3065,2291,2554,764,1013,255,502,3830,4095,3317,3580,1786,2035,1273,1520,2912,2665,2403,2154,876,613,367,102,3942,3695,3429,3180,1898,1635,1385,1120,1120,1385,1635,1898,3180,3429,3695,3942,102,367,613,876,2154,2403,2665,2912,1520,1273,2035,1786,3580,3317,4095,3830,502,255,1013,764,2554,2291,3065,2800,1616,1881,1107,1370,3676,3925,3167,3414,598,863,85,348,2650,2899,2137,2384,1984,1737,1475,1226,4044,3781,3535,3270,966,719,453,204,3018,2755,2505,2240,3232,3497,3747,4010,1196,1445,1711,1958,2214,2479,2725,2988,170,419,681,928,3376,3129,3891,3642,1340,1077,1855,1590,2358,2111,2869,2620,314,51,825,560,3728,3993,3219,3482,1692,1941,1183,1430,2710,2975,2197,2460,666,915,153,400,3840,3593,3331,3082,1804,1541,1295,1030,2822,2575,2309,2060,778,515,265,0],this.triTable2=[[],[8,3,0],[9,0,1],[8,3,1,8,1,9],[11,2,3],[11,2,0,11,0,8],[11,2,3,0,1,9],[2,1,11,1,9,11,11,9,8],[10,1,2],[8,3,0,1,2,10],[9,0,2,9,2,10],[3,2,8,2,10,8,8,10,9],[10,1,3,10,3,11],[1,0,10,0,8,10,10,8,11],[0,3,9,3,11,9,9,11,10],[8,10,9,8,11,10],[8,4,7],[3,0,4,3,4,7],[1,9,0,8,4,7],[9,4,1,4,7,1,1,7,3],[2,3,11,7,8,4],[7,11,4,11,2,4,4,2,0],[3,11,2,4,7,8,9,0,1],[2,7,11,2,1,7,1,4,7,1,9,4],[10,1,2,8,4,7],[2,10,1,0,4,7,0,7,3],[4,7,8,0,2,10,0,10,9],[2,7,3,2,9,7,7,9,4,2,10,9],[8,4,7,11,10,1,11,1,3],[11,4,7,1,4,11,1,11,10,1,0,4],[3,8,0,7,11,4,11,9,4,11,10,9],[7,11,4,4,11,9,11,10,9],[9,5,4],[3,0,8,4,9,5],[5,4,0,5,0,1],[4,8,5,8,3,5,5,3,1],[11,2,3,9,5,4],[9,5,4,8,11,2,8,2,0],[3,11,2,1,5,4,1,4,0],[8,5,4,2,5,8,2,8,11,2,1,5],[2,10,1,9,5,4],[0,8,3,5,4,9,10,1,2],[10,5,2,5,4,2,2,4,0],[3,4,8,3,2,4,2,5,4,2,10,5],[5,4,9,1,3,11,1,11,10],[0,9,1,4,8,5,8,10,5,8,11,10],[3,4,0,3,10,4,4,10,5,3,11,10],[4,8,5,5,8,10,8,11,10],[9,5,7,9,7,8],[0,9,3,9,5,3,3,5,7],[8,0,7,0,1,7,7,1,5],[1,7,3,1,5,7],[11,2,3,8,9,5,8,5,7],[9,2,0,9,7,2,2,7,11,9,5,7],[0,3,8,2,1,11,1,7,11,1,5,7],[2,1,11,11,1,7,1,5,7],[1,2,10,5,7,8,5,8,9],[9,1,0,10,5,2,5,3,2,5,7,3],[5,2,10,8,2,5,8,5,7,8,0,2],[10,5,2,2,5,3,5,7,3],[3,9,1,3,8,9,7,11,10,7,10,5],[9,1,0,10,7,11,10,5,7],[3,8,0,7,10,5,7,11,10],[11,5,7,11,10,5],[11,7,6],[0,8,3,11,7,6],[9,0,1,11,7,6],[7,6,11,3,1,9,3,9,8],[2,3,7,2,7,6],[8,7,0,7,6,0,0,6,2],[1,9,0,3,7,6,3,6,2],[7,6,2,7,2,9,2,1,9,7,9,8],[1,2,10,6,11,7],[2,10,1,7,6,11,8,3,0],[11,7,6,10,9,0,10,0,2],[7,6,11,3,2,8,8,2,10,8,10,9],[6,10,7,10,1,7,7,1,3],[6,10,1,6,1,7,7,1,0,7,0,8],[9,0,3,6,9,3,6,10,9,6,3,7],[6,10,7,7,10,8,10,9,8],[8,4,6,8,6,11],[11,3,6,3,0,6,6,0,4],[0,1,9,4,6,11,4,11,8],[1,9,4,11,1,4,11,3,1,11,4,6],[3,8,2,8,4,2,2,4,6],[2,0,4,2,4,6],[1,9,0,3,8,2,2,8,4,2,4,6],[9,4,1,1,4,2,4,6,2],[10,1,2,11,8,4,11,4,6],[10,1,2,11,3,6,6,3,0,6,0,4],[0,2,10,0,10,9,4,11,8,4,6,11],[2,11,3,6,9,4,6,10,9],[8,4,6,8,6,1,6,10,1,8,1,3],[1,0,10,10,0,6,0,4,6],[8,0,3,9,6,10,9,4,6],[10,4,6,10,9,4],[9,5,4,7,6,11],[4,9,5,3,0,8,11,7,6],[6,11,7,4,0,1,4,1,5],[6,11,7,4,8,5,5,8,3,5,3,1],[4,9,5,6,2,3,6,3,7],[9,5,4,8,7,0,0,7,6,0,6,2],[4,0,1,4,1,5,6,3,7,6,2,3],[7,4,8,5,2,1,5,6,2],[6,11,7,1,2,10,9,5,4],[11,7,6,8,3,0,1,2,10,9,5,4],[11,7,6,10,5,2,2,5,4,2,4,0],[7,4,8,2,11,3,10,5,6],[4,9,5,6,10,7,7,10,1,7,1,3],[5,6,10,0,9,1,8,7,4],[5,6,10,7,0,3,7,4,0],[10,5,6,4,8,7],[5,6,9,6,11,9,9,11,8],[0,9,5,0,5,3,3,5,6,3,6,11],[0,1,5,0,5,11,5,6,11,0,11,8],[11,3,6,6,3,5,3,1,5],[9,5,6,3,9,6,3,8,9,3,6,2],[5,6,9,9,6,0,6,2,0],[0,3,8,2,5,6,2,1,5],[1,6,2,1,5,6],[1,2,10,5,6,9,9,6,11,9,11,8],[1,0,9,6,10,5,11,3,2],[6,10,5,2,8,0,2,11,8],[3,2,11,10,5,6],[10,5,6,9,3,8,9,1,3],[0,9,1,5,6,10],[8,0,3,10,5,6],[10,5,6],[10,6,5],[8,3,0,10,6,5],[0,1,9,5,10,6],[10,6,5,9,8,3,9,3,1],[3,11,2,10,6,5],[6,5,10,2,0,8,2,8,11],[1,9,0,6,5,10,11,2,3],[1,10,2,5,9,6,9,11,6,9,8,11],[1,2,6,1,6,5],[0,8,3,2,6,5,2,5,1],[5,9,6,9,0,6,6,0,2],[9,6,5,3,6,9,3,9,8,3,2,6],[11,6,3,6,5,3,3,5,1],[0,5,1,0,11,5,5,11,6,0,8,11],[0,5,9,0,3,5,3,6,5,3,11,6],[5,9,6,6,9,11,9,8,11],[10,6,5,4,7,8],[5,10,6,7,3,0,7,0,4],[5,10,6,0,1,9,8,4,7],[4,5,9,6,7,10,7,1,10,7,3,1],[7,8,4,2,3,11,10,6,5],[11,6,7,10,2,5,2,4,5,2,0,4],[11,6,7,8,0,3,1,10,2,9,4,5],[6,7,11,1,10,2,9,4,5],[7,8,4,5,1,2,5,2,6],[4,1,0,4,5,1,6,7,3,6,3,2],[9,4,5,8,0,7,0,6,7,0,2,6],[4,5,9,6,3,2,6,7,3],[6,7,11,4,5,8,5,3,8,5,1,3],[6,7,11,4,1,0,4,5,1],[4,5,9,3,8,0,11,6,7],[9,4,5,7,11,6],[10,6,4,10,4,9],[8,3,0,9,10,6,9,6,4],[1,10,0,10,6,0,0,6,4],[8,6,4,8,1,6,6,1,10,8,3,1],[2,3,11,6,4,9,6,9,10],[0,10,2,0,9,10,4,8,11,4,11,6],[10,2,1,11,6,3,6,0,3,6,4,0],[10,2,1,11,4,8,11,6,4],[9,1,4,1,2,4,4,2,6],[1,0,9,3,2,8,2,4,8,2,6,4],[2,4,0,2,6,4],[3,2,8,8,2,4,2,6,4],[1,4,9,11,4,1,11,1,3,11,6,4],[0,9,1,4,11,6,4,8,11],[11,6,3,3,6,0,6,4,0],[8,6,4,8,11,6],[6,7,10,7,8,10,10,8,9],[9,3,0,6,3,9,6,9,10,6,7,3],[6,1,10,6,7,1,7,0,1,7,8,0],[6,7,10,10,7,1,7,3,1],[7,11,6,3,8,2,8,10,2,8,9,10],[11,6,7,10,0,9,10,2,0],[2,1,10,7,11,6,8,0,3],[1,10,2,6,7,11],[7,2,6,7,9,2,2,9,1,7,8,9],[1,0,9,3,6,7,3,2,6],[8,0,7,7,0,6,0,2,6],[2,7,3,2,6,7],[7,11,6,3,9,1,3,8,9],[9,1,0,11,6,7],[0,3,8,11,6,7],[11,6,7],[11,7,5,11,5,10],[3,0,8,7,5,10,7,10,11],[9,0,1,10,11,7,10,7,5],[3,1,9,3,9,8,7,10,11,7,5,10],[10,2,5,2,3,5,5,3,7],[5,10,2,8,5,2,8,7,5,8,2,0],[9,0,1,10,2,5,5,2,3,5,3,7],[1,10,2,5,8,7,5,9,8],[2,11,1,11,7,1,1,7,5],[0,8,3,2,11,1,1,11,7,1,7,5],[9,0,2,9,2,7,2,11,7,9,7,5],[11,3,2,8,5,9,8,7,5],[1,3,7,1,7,5],[8,7,0,0,7,1,7,5,1],[0,3,9,9,3,5,3,7,5],[9,7,5,9,8,7],[4,5,8,5,10,8,8,10,11],[3,0,4,3,4,10,4,5,10,3,10,11],[0,1,9,4,5,8,8,5,10,8,10,11],[5,9,4,1,11,3,1,10,11],[3,8,4,3,4,2,2,4,5,2,5,10],[10,2,5,5,2,4,2,0,4],[0,3,8,5,9,4,10,2,1],[2,1,10,9,4,5],[8,4,5,2,8,5,2,11,8,2,5,1],[3,2,11,1,4,5,1,0,4],[9,4,5,8,2,11,8,0,2],[11,3,2,9,4,5],[4,5,8,8,5,3,5,1,3],[5,0,4,5,1,0],[3,8,0,4,5,9],[9,4,5],[7,4,11,4,9,11,11,9,10],[3,0,8,7,4,11,11,4,9,11,9,10],[11,7,4,1,11,4,1,10,11,1,4,0],[8,7,4,11,1,10,11,3,1],[2,3,7,2,7,9,7,4,9,2,9,10],[4,8,7,0,10,2,0,9,10],[2,1,10,0,7,4,0,3,7],[10,2,1,8,7,4],[2,11,7,2,7,1,1,7,4,1,4,9],[3,2,11,4,8,7,9,1,0],[7,4,11,11,4,2,4,0,2],[2,11,3,7,4,8],[9,1,4,4,1,7,1,3,7],[1,0,9,8,7,4],[3,4,0,3,7,4],[8,7,4],[8,9,10,8,10,11],[0,9,3,3,9,11,9,10,11],[1,10,0,0,10,8,10,11,8],[10,3,1,10,11,3],[3,8,2,2,8,10,8,9,10],[9,2,0,9,10,2],[8,0,3,1,10,2],[10,2,1],[2,11,1,1,11,9,11,8,9],[11,3,2,0,9,1],[11,0,2,11,8,0],[11,3,2],[8,1,3,8,9,1],[9,1,0],[8,0,3],[]]}march(t,e,i,r){let s=!!r.fulltable,n=r.hasOwnProperty("origin")&&r.origin.hasOwnProperty("x")?r.origin:{x:0,y:0,z:0},a=!!r.voxel,o=r.matrix,l=r.nX||0,h=r.nY||0,c=r.nZ||0,d=r.scale||1,u=null;u=r.unitCube?r.unitCube:{x:d,y:d,z:d};let f,p,g=new Int32Array(l*h*c);for(f=0,p=g.length;f>2))+r+((2&e)>>1))*c+s+(1&e)]&this.ISDONE)<=3&&(e.push(e[r]),r=e.length-1,e.push(e[s]),s=e.length-1,e.push(e[n]),n=e.length-1),i.push(r),i.push(s),i.push(n)}}}laplacianSmooth(t,e,i){let r,s,n,a,o,l=new Array(e.length);for(r=0,s=e.length;r1e6&&(this.scaleFactor=this.defaultScaleFactor/2);let r=1/this.scaleFactor*5.5;this.pminx=t[0][0],this.pmaxx=t[1][0],this.pminy=t[0][1],this.pmaxy=t[1][1],this.pminz=t[0][2],this.pmaxz=t[1][2],e?(this.pminx-=this.probeRadius+r,this.pminy-=this.probeRadius+r,this.pminz-=this.probeRadius+r,this.pmaxx+=this.probeRadius+r,this.pmaxy+=this.probeRadius+r,this.pmaxz+=this.probeRadius+r):(this.pminx-=r,this.pminy-=r,this.pminz-=r,this.pmaxx+=r,this.pmaxy+=r,this.pmaxz+=r),this.pminx=Math.floor(this.pminx*this.scaleFactor)/this.scaleFactor,this.pminy=Math.floor(this.pminy*this.scaleFactor)/this.scaleFactor,this.pminz=Math.floor(this.pminz*this.scaleFactor)/this.scaleFactor,this.pmaxx=Math.ceil(this.pmaxx*this.scaleFactor)/this.scaleFactor,this.pmaxy=Math.ceil(this.pmaxy*this.scaleFactor)/this.scaleFactor,this.pmaxz=Math.ceil(this.pmaxz*this.scaleFactor)/this.scaleFactor,this.ptranx=-this.pminx,this.ptrany=-this.pminy,this.ptranz=-this.pminz,this.pLength=Math.ceil(this.scaleFactor*(this.pmaxx-this.pminx))+1,this.pWidth=Math.ceil(this.scaleFactor*(this.pmaxy-this.pminy))+1,this.pHeight=Math.ceil(this.scaleFactor*(this.pmaxz-this.pminz))+1,this.boundingatom(e),this.cutRadius=this.probeRadius*this.scaleFactor,this.vpBits=new Uint8Array(this.pLength*this.pWidth*this.pHeight),this.vpDistance=new Float64Array(this.pLength*this.pWidth*this.pHeight),this.vpAtomID=new Int32Array(this.pLength*this.pWidth*this.pHeight)}boundingatom(t){let e={};for(const i in this.vdwRadii){let r=this.vdwRadii[i];e[i]=t?(r+this.probeRadius)*this.scaleFactor+.5:r*this.scaleFactor+.5;let s=e[i]*e[i];this.widxz[i]=Math.floor(e[i])+1,this.depty[i]=new Int32Array(this.widxz[i]*this.widxz[i]);let n=0;for(let t=0;ts)this.depty[i][n]=-1;else{let t=Math.sqrt(s-r);this.depty[i][n]=Math.floor(t)}n++}}}fillvoxels(t,e){for(let t=0,e=this.vpBits.length;t=this.pLength||h>=this.pWidth||c>=this.pHeight)continue;let u=a*o+h*this.pHeight+c;if(this.vpBits[u]&this.INOUT){let a=e[this.vpAtomID[u]];if(a.serial!=t.serial){let e=i+f-Math.floor(.5+this.scaleFactor*(a.x+this.ptranx)),o=r+n-Math.floor(.5+this.scaleFactor*(a.y+this.ptrany)),l=s+p-Math.floor(.5+this.scaleFactor*(a.z+this.ptranz));f*f+n*n+p*p=this.pLength||h>=this.pWidth||c>=this.pHeight)continue;let u=a*o+h*this.pHeight+c;if(this.vpBits[u]&this.ISDONE){let a=e[this.vpAtomID[u]];if(a.serial!=t.serial){let e=r+f-Math.floor(.5+this.scaleFactor*(a.x+this.ptranx)),o=s+i-Math.floor(.5+this.scaleFactor*(a.y+this.ptrany)),l=n+p-Math.floor(.5+this.scaleFactor*(a.z+this.ptranz));f*f+i*i+p*p-1&&a-1&&l-1&&o=o)||(this.vpBits[t]|=this.ISBOUND))}fastoneshell(t,e){let i,r,s,n,a,o,l,h,c,d=[];if(0===t.length)return d;let u={ix:-1,iy:-1,iz:-1},f=this.pWidth*this.pHeight;for(let p=0,g=t.length;p-1&&u.iy-1&&u.iz-1&&(c=u.ix*f+this.pHeight*u.iy+u.iz,this.vpBits[c]&this.INOUT&&!(this.vpBits[c]&this.ISDONE)?(e.set(u.ix,u.iy,s+this.nb[t][2],h),n=u.ix-h.ix,a=u.iy-h.iy,o=u.iz-h.iz,l=n*n+a*a+o*o,this.vpDistance[c]=l,this.vpBits[c]|=this.ISDONE,this.vpBits[c]|=this.ISBOUND,d.push({ix:u.ix,iy:u.iy,iz:u.iz})):this.vpBits[c]&this.INOUT&&this.vpBits[c]&this.ISDONE&&(n=u.ix-h.ix,a=u.iy-h.iy,o=u.iz-h.iz,l=n*n+a*a+o*o,l-1&&u.iy-1&&u.iz-1&&(c=u.ix*f+this.pHeight*u.iy+u.iz,this.vpBits[c]&this.INOUT&&!(this.vpBits[c]&this.ISDONE)?(e.set(u.ix,u.iy,s+this.nb[t][2],h),n=u.ix-h.ix,a=u.iy-h.iy,o=u.iz-h.iz,l=n*n+a*a+o*o,this.vpDistance[c]=l,this.vpBits[c]|=this.ISDONE,this.vpBits[c]|=this.ISBOUND,d.push({ix:u.ix,iy:u.iy,iz:u.iz})):this.vpBits[c]&this.INOUT&&this.vpBits[c]&this.ISDONE&&(n=u.ix-h.ix,a=u.iy-h.iy,o=u.iz-h.iz,l=n*n+a*a+o*o,l-1&&u.iy-1&&u.iz-1&&(c=u.ix*f+this.pHeight*u.iy+u.iz,this.vpBits[c]&this.INOUT&&!(this.vpBits[c]&this.ISDONE)?(e.set(u.ix,u.iy,s+this.nb[t][2],h),n=u.ix-h.ix,a=u.iy-h.iy,o=u.iz-h.iz,l=n*n+a*a+o*o,this.vpDistance[c]=l,this.vpBits[c]|=this.ISDONE,this.vpBits[c]|=this.ISBOUND,d.push({ix:u.ix,iy:u.iy,iz:u.iz})):this.vpBits[c]&this.INOUT&&this.vpBits[c]&this.ISDONE&&(n=u.ix-h.ix,a=u.iy-h.iy,o=u.iz-h.iz,l=n*n+a*a+o*o,l0){var m=[n,n+1,n-1,n-2],v=d.faceidx;p[v]=m[0],p[v+1]=m[1],p[v+2]=m[3],p[v+3]=m[1],p[v+4]=m[2],p[v+5]=m[3],d.faceidx+=6}d.vertices+=2}}function ut(t,e,i,r,n,a,o){o&&"default"!==o||(o="rectangle"),"edged"===o?function(t,e,i,r,n){if(!(e.length<2)){var a,o;if(a=e[0],o=e[e.length-1],a=nt(a,r),o=nt(o,r),!n)return dt(t,a,o,i);var l,h,c,d,u,f,p,g,m,v,_,y,b,x,w,A,C,S,M,z,T,E,L=[],F=[[0,2,-6,-8],[-4,-2,6,4],[7,-1,-5,3],[-3,5,1,-7]];for(b=0,x=a.length;b0){var O=void 0!==y&&void 0!==_&&y.serial!==_.serial;for(w=0;w<4;w++){var D=[f+F[w][0],f+F[w][1],f+F[w][2],f+F[w][3]];if(E[g=M.faceidx]=D[0],E[g+1]=D[1],E[g+2]=D[3],E[g+3]=D[1],E[g+4]=D[2],E[g+5]=D[3],M.faceidx+=6,_.clickable||y.clickable||_.hoverable||y.hoverable){var k=L[D[3]].clone(),R=L[D[0]].clone(),P=L[D[2]].clone(),U=L[D[1]].clone();if(k.atom=L[D[3]].atom||null,P.atom=L[D[2]].atom||null,R.atom=L[D[0]].atom||null,U.atom=L[D[1]].atom||null,O){var B=k.clone().add(R).multiplyScalar(.5),N=P.clone().add(U).multiplyScalar(.5),G=k.clone().add(U).multiplyScalar(.5);w%2==0?((y.clickable||y.hoverable)&&(A=new J.Triangle(B,G,k),C=new J.Triangle(N,P,G),S=new J.Triangle(G,P,k),y.intersectionShape.triangle.push(A),y.intersectionShape.triangle.push(C),y.intersectionShape.triangle.push(S)),(_.clickable||_.hoverable)&&(A=new J.Triangle(R,U,G),C=new J.Triangle(U,N,G),S=new J.Triangle(R,G,B),_.intersectionShape.triangle.push(A),_.intersectionShape.triangle.push(C),_.intersectionShape.triangle.push(S))):((_.clickable||_.hoverable)&&(A=new J.Triangle(B,G,k),C=new J.Triangle(N,P,G),S=new J.Triangle(G,P,k),_.intersectionShape.triangle.push(A),_.intersectionShape.triangle.push(C),_.intersectionShape.triangle.push(S)),(y.clickable||y.hoverable)&&(A=new J.Triangle(R,U,G),C=new J.Triangle(U,N,G),S=new J.Triangle(R,G,B),y.intersectionShape.triangle.push(A),y.intersectionShape.triangle.push(C),y.intersectionShape.triangle.push(S)))}else(_.clickable||_.hoverable)&&(A=new J.Triangle(R,U,k),C=new J.Triangle(U,P,k),_.intersectionShape.triangle.push(A),_.intersectionShape.triangle.push(C))}}}M.vertices+=8,y=_}var V=L.length-8;for(z=(M=t.updateGeoGroup(8)).vertexArray,T=M.colorArray,E=M.faceArray,p=3*(f=M.vertices),g=M.faceidx,b=0;b<4;b++){L.push(L[2*b]),L.push(L[V+2*b]);var j=L[2*b],H=L[V+2*b];z[p+6*b]=j.x,z[p+1+6*b]=j.y,z[p+2+6*b]=j.z,z[p+3+6*b]=H.x,z[p+4+6*b]=H.y,z[p+5+6*b]=H.z,T[p+6*b]=m.r,T[p+1+6*b]=m.g,T[p+2+6*b]=m.b,T[p+3+6*b]=m.r,T[p+4+6*b]=m.g,T[p+5+6*b]=m.b}V+=8,A=[f,f+2,f+6,f+4],C=[f+1,f+5,f+7,f+3],E[g]=A[0],E[g+1]=A[1],E[g+2]=A[3],E[g+3]=A[1],E[g+4]=A[2],E[g+5]=A[3],E[g+6]=C[0],E[g+7]=C[1],E[g+8]=C[3],E[g+9]=C[1],E[g+10]=C[2],E[g+11]=C[3],M.faceidx+=12,M.vertices+=8}}(t,e,i,r,n):"rectangle"!==o&&"oval"!==o&&"parabola"!==o||function(t,e,i,r,n,a,o){var l,h,c,d,u,f,p,g,m,v;if((c=e.length)<2||e[0].length<2)return;for(l=0;l0&&(l-=1,a=!0),M=Math.round(l*(i.length-1)/d),S=s.CC.color(i[M]),m=p,v=g,p=[],g=[],u=[],void 0!==e[0][l].atom&&(C=e[0][l].atom,"oval"===o?f=_:"rectangle"===o?f=y:"parabola"===o&&(f=b)),f||(f=y),h=0;h0&&!a){for(h=0;h<2*c;h++)L=[x+F[h][0],x+F[h][1],x+F[h][2],x+F[h][3]],E[A=I.faceidx]=L[0],E[A+1]=L[1],E[A+2]=L[3],E[A+3]=L[1],E[A+4]=L[2],E[A+5]=L[3],I.faceidx+=6;if(C.clickable||C.hoverable){var k=[];for(h in k.push(new J.Triangle(m[0],p[0],p[c-1])),k.push(new J.Triangle(m[0],p[c-1],m[c-1])),k.push(new J.Triangle(m[c-1],p[c-1],g[c-1])),k.push(new J.Triangle(m[c-1],g[c-1],v[c-1])),k.push(new J.Triangle(g[0],v[0],v[c-1])),k.push(new J.Triangle(g[c-1],g[0],v[c-1])),k.push(new J.Triangle(p[0],m[0],v[0])),k.push(new J.Triangle(g[0],p[0],v[0])),k)C.intersectionShape.triangle.push(k[h])}}I.vertices+=2*c}for(z=I.vertexArray,T=I.colorArray,E=I.faceArray,w=3*(x=I.vertices),A=I.faceidx,l=0;l=0&&i<1&&(a.transparent=!0,a.opacity=i),a.outline=r;var o=new n.Mesh(e,a);t.add(o)}}function mt(t,e,i,r,s,n,a,o,l){var h,c,d,u,f,p;if(r&&s&&a){var g=s.sub(r);g.normalize();var m=o[l];for(c=l+1;c0&&ut(T,F,E,l,g,0,F.style);var i=[],r=null;if(e){for(m=0;m0&&ut(T,F,E,l,g,0,F.style),F=[],m=0;m0&&e.truncateArrayBuffers(!0,!0)}static updateColor(t,e){var i,r,n;e=e||s.CC.color(e),t.colorsNeedUpdate=!0,e.constructor!==Array&&(i=e.r,r=e.g,n=e.b);for(let s in t.geometryGroups){let a=t.geometryGroups[s],o=a.colorArray;for(let t=0,s=a.vertices;t0?l/t:(t+l)/t}c.multiplyScalar(o);var d=new $.Vector3(r.x,r.y,r.z).add(c),u=c.clone().negate();let f=new $.Vector3(r.x,r.y,r.z);t.intersectionShape.cylinder.push(new J.Cylinder(f,d.clone(),n)),t.intersectionShape.sphere.push(new J.Sphere(f,n));var p=[];p[0]=c.clone(),Math.abs(p[0].x)>1e-4?p[0].y+=1:p[0].x+=1,p[0].cross(c),p[0].normalize(),p[4]=p[0].clone(),p[4].crossVectors(p[0],c),p[4].normalize(),p[8]=p[0].clone().negate(),p[12]=p[4].clone().negate(),p[2]=p[0].clone().add(p[4]).normalize(),p[6]=p[4].clone().add(p[8]).normalize(),p[10]=p[8].clone().add(p[12]).normalize(),p[14]=p[12].clone().add(p[0]).normalize(),p[1]=p[0].clone().add(p[2]).normalize(),p[3]=p[2].clone().add(p[4]).normalize(),p[5]=p[4].clone().add(p[6]).normalize(),p[7]=p[6].clone().add(p[8]).normalize(),p[9]=p[8].clone().add(p[10]).normalize(),p[11]=p[10].clone().add(p[12]).normalize(),p[13]=p[12].clone().add(p[14]).normalize(),p[15]=p[14].clone().add(p[0]).normalize();var g,m,v,_,y,b,x,w,A,C,S,M,z,T,E,L,F,I,O,D,k,R,P=h.vertices,U=h.vertexArray,B=h.faceArray,N=h.normalArray,G=h.lineArray;for(m=0,v=p.length;m0){var W=U[g-3],q=U[g-2],Y=U[g-1],Z=new $.Vector3(W,q,Y),X=new $.Vector3(s.x,s.y,s.z),K=d.clone(),Q=new $.Vector3(H.x,H.y,H.z);t.intersectionShape.triangle.push(new J.Triangle(Q,X,Z)),t.intersectionShape.triangle.push(new J.Triangle(Z.clone(),K,Q.clone()))}}h.vertices+=48,U[g=3*h.vertices]=r.x,U[g+1]=r.y,U[g+2]=r.z,U[g+3]=d.x,U[g+4]=d.y,U[g+5]=d.z,U[g+6]=s.x,U[g+7]=s.y,U[g+8]=s.z,h.vertices+=3;var tt=h.vertices-3,et=h.vertices-2,it=h.vertices-1,rt=3*tt,st=3*et,nt=3*it;for(m=0,v=p.length-1;mo&&(o=c),d>l&&(l=d),u>h&&(h=u)}t.center.set((o+s)/2,(l+n)/2,(h+a)/2),t.radius=t.center.distanceTo({x:o,y:l,z:h}),t.box={min:{x:s,y:n,z:a},max:{x:o,y:l,z:h}}}static addCustomGeo(t,e,i,r,s){var n,a,o,l,h,c,d,u,f,p=e.addGeoGroup(),g=i.vertexArr,m=i.normalArr,v=i.faceArr;p.vertices=g.length,p.faceidx=v.length;var _=p.vertexArray,y=p.colorArray;for(r.constructor!==Array&&(u=r.r,f=r.g,l=r.b),c=0,d=p.vertices;ch?(c.fromCap=0,c.toCap=2):(c.fromCap=2,c.toCap=2),this.addCylinder(c)}}addLine(t){var e,i;e=t.start?new $.Vector3(t.start.x||0,t.start.y||0,t.start.z||0):new $.Vector3(0,0,0),t.end?void 0===(i=new $.Vector3(t.end.x,t.end.y||0,t.end.z||0)).x&&(i.x=3):i=new $.Vector3(3,0,0);var r=this.geo.updateGeoGroup(2),s=r.vertices,n=3*s,a=r.vertexArray;a[n]=e.x,a[n+1]=e.y,a[n+2]=e.z,a[n+3]=i.x,a[n+4]=i.y,a[n+5]=i.z,r.vertices+=2;var o=r.lineArray,l=r.lineidx;o[l]=s,o[l+1]=s+1,r.lineidx+=2;var h=new $.Vector3;this.components.push({centroid:h.addVectors(e,i).multiplyScalar(.5)}),r=this.geo.updateGeoGroup(0),GLShape.updateBoundingFromPoints(this.boundingSphere,this.components,r.vertexArray,r.vertices)}addArrow(t){if(t.start?t.start=new $.Vector3(t.start.x||0,t.start.y||0,t.start.z||0):t.start=new $.Vector3(0,0,0),t.dir instanceof $.Vector3&&"number"==typeof t.length){var e=t.dir.clone().multiplyScalar(t.length).add(t.start);t.end=e}else t.end?(t.end=new $.Vector3(t.end.x,t.end.y||0,t.end.z||0),void 0===t.end.x&&(t.end.x=3)):t.end=new $.Vector3(3,0,0);t.radius=t.radius||.1,t.radiusRatio=t.radiusRatio||1.618034,t.mid=0=0?f[s]-a:a-f[s])>0&&(p[s]|=GLShape.ISDONE)}var g=[],m=[];MarchingCube.march(p,g,m,{fulltable:!0,voxel:o,unitCube:t.unit,origin:t.origin,matrix:t.matrix,nX:h,nY:c,nZ:d}),!o&&l>0&&MarchingCube.laplacianSmooth(l,g,m);var v=[],_=[],y=[];if(e.selectedRegion&&void 0===e.coords&&(e.coords=e.selectedRegion),void 0===e.coords&&void 0!==e.selection&&(r?e.coords=r.selectedAtoms(e.selection):console.log("addIsosurface needs viewer is selection provided.")),void 0!==e.coords){var b=e.coords[0].x,x=e.coords[0].y,w=e.coords[0].z,A=e.coords[0].x,C=e.coords[0].y,S=e.coords[0].z;for(let t=0;tb?b=e.coords[t].x:e.coords[t].xx?x=e.coords[t].y:e.coords[t].yw?w=e.coords[t].z:e.coords[t].zA&&g[t].xC&&g[t].yS&&g[t].z0){var r=new n.LineBasicMaterial({linewidth:this.linewidth,color:this.color}),s=new n.Line(this.linegeo,r,n.LineStyle.LinePieces);this.shapeObj.add(s)}this.renderedShapeObj=this.shapeObj.clone(),t.add(this.renderedShapeObj)}}removegl(t){this.renderedShapeObj&&(void 0!==this.renderedShapeObj.geometry&&this.renderedShapeObj.geometry.dispose(),void 0!==this.renderedShapeObj.material&&this.renderedShapeObj.material.dispose(),t.remove(this.renderedShapeObj),this.renderedShapeObj=null),this.shapeObj=null}get position(){return this.boundingSphere.center}get x(){return this.boundingSphere.center.x}get y(){return this.boundingSphere.center.y}get z(){return this.boundingSphere.center.z}}function wt(t){if(t.vertexArr.length<64e3)return[t];var e=[{vertexArr:[],normalArr:[],faceArr:[]}];t.colorArr&&(e.colorArr=[]);var i=[],r=[],s=0,n=t.faceArr;for(let o=0,l=n.length;o=64e3&&(e.push({vertexArr:[],normalArr:[],faceArr:[]}),t.colorArr&&(e.colorArr=[]),s++)}return e}GLShape.ISDONE=2,GLShape.drawCustom=function(t,e,i){var r=i,n=r.vertexArr,a=r.faceArr;0!==n.length&&0!==a.length||console.warn("Error adding custom shape component: No vertices and/or face indices supplied!");var o=i.color;void 0===o&&(o=t.color),o=s.CC.color(o);for(var l=wt(r),h=0,c=l.length;h=0||t.getIndex(c,d,u)>=0)for(let e=o;e=0&&!i[o]){(e-a.x)*(e-a.x)+(r-a.y)*(r-a.y)+(n-a.z)*(n-a.z){const t=new Uint8Array(4);return!((new Uint32Array(t.buffer)[0]=1)&t[0])})(),Mt={int8:globalThis.Int8Array,uint8:globalThis.Uint8Array,int16:globalThis.Int16Array,uint16:globalThis.Uint16Array,int32:globalThis.Int32Array,uint32:globalThis.Uint32Array,uint64:globalThis.BigUint64Array,int64:globalThis.BigInt64Array,float32:globalThis.Float32Array,float64:globalThis.Float64Array};class IOBuffer{constructor(t=8192,e={}){let i=!1;"number"==typeof t?t=new ArrayBuffer(t):(i=!0,this.lastWrittenByte=t.byteLength);const r=e.offset?e.offset>>>0:0,s=t.byteLength-r;let n=r;(ArrayBuffer.isView(t)||t instanceof IOBuffer)&&(t.byteLength!==t.buffer.byteLength&&(n=t.byteOffset+r),t=t.buffer),this.lastWrittenByte=i?s:0,this.buffer=t,this.length=s,this.byteLength=s,this.byteOffset=n,this.offset=0,this.littleEndian=!0,this._data=new DataView(this.buffer,n,s),this._mark=0,this._marks=[]}available(t=1){return this.offset+t<=this.length}isLittleEndian(){return this.littleEndian}setLittleEndian(){return this.littleEndian=!0,this}isBigEndian(){return!this.littleEndian}setBigEndian(){return this.littleEndian=!1,this}skip(t=1){return this.offset+=t,this}back(t=1){return this.offset-=t,this}seek(t){return this.offset=t,this}mark(){return this._mark=this.offset,this}reset(){return this.offset=this._mark,this}pushMark(){return this._marks.push(this.offset),this}popMark(){const t=this._marks.pop();if(void 0===t)throw new Error("Mark stack empty");return this.seek(t),this}rewind(){return this.offset=0,this}ensureAvailable(t=1){if(!this.available(t)){const e=2*(this.offset+t),i=new Uint8Array(e);i.set(new Uint8Array(this.buffer)),this.buffer=i.buffer,this.length=this.byteLength=e,this._data=new DataView(this.buffer)}return this}readBoolean(){return 0!==this.readUint8()}readInt8(){return this._data.getInt8(this.offset++)}readUint8(){return this._data.getUint8(this.offset++)}readByte(){return this.readUint8()}readBytes(t=1){return this.readArray(t,"uint8")}readArray(t,e){const i=Mt[e].BYTES_PER_ELEMENT*t,r=this.byteOffset+this.offset,s=this.buffer.slice(r,r+i);if(this.littleEndian===St&&"uint8"!==e&&"int8"!==e){const t=new Uint8Array(this.buffer.slice(r,r+i));t.reverse();const s=new Mt[e](t.buffer);return this.offset+=i,s.reverse(),s}const n=new Mt[e](s);return this.offset+=i,n}readInt16(){const t=this._data.getInt16(this.offset,this.littleEndian);return this.offset+=2,t}readUint16(){const t=this._data.getUint16(this.offset,this.littleEndian);return this.offset+=2,t}readInt32(){const t=this._data.getInt32(this.offset,this.littleEndian);return this.offset+=4,t}readUint32(){const t=this._data.getUint32(this.offset,this.littleEndian);return this.offset+=4,t}readFloat32(){const t=this._data.getFloat32(this.offset,this.littleEndian);return this.offset+=4,t}readFloat64(){const t=this._data.getFloat64(this.offset,this.littleEndian);return this.offset+=8,t}readBigInt64(){const t=this._data.getBigInt64(this.offset,this.littleEndian);return this.offset+=8,t}readBigUint64(){const t=this._data.getBigUint64(this.offset,this.littleEndian);return this.offset+=8,t}readChar(){return String.fromCharCode(this.readInt8())}readChars(t=1){let e="";for(let i=0;ithis.lastWrittenByte&&(this.lastWrittenByte=this.offset)}}const zt=1,Tt=2,Et=3,Lt=4,Ft=5,It=6;function Ot(t){switch(Number(t)){case zt:return"byte";case Tt:return"char";case Et:return"short";case Lt:return"int";case Ft:return"float";case It:return"double";default:return"undefined"}}function Dt(t){switch(Number(t)){case zt:case Tt:return 1;case Et:return 2;case Lt:case Ft:return 4;case It:return 8;default:return-1}}function kt(t){switch(String(t)){case"byte":return zt;case"char":return Tt;case"short":return Et;case"int":return Lt;case"float":return Ft;case"double":return It;default:return-1}}function Rt(t,e){if(1!==t){const i=new Array(t);for(let r=0;r6,`non valid type ${c}`);const d=t.readUint32();let u=t.readUint32();2===i&&(Ut(u>0,"offsets larger than 4GB not supported"),u=t.readUint32());let f=!1;void 0!==e&&l[0]===e&&(n+=d,f=!0),s[r]={name:a,dimensions:l,attributes:h,type:Ot(c),size:d,offset:u,record:f}}}return{variables:s,recordStep:n}}(t,r?.id,e);return Array.isArray(n)||(i.variables=n.variables,r.recordStep=n.recordStep),i.recordDimension=r,i}function Vt(t){const e=t.readUint32();let i;if(0===e)return Ut(0!==t.readUint32(),"wrong empty tag for list of attributes"),[];{Ut(12!==e,"wrong tag for list of attributes");const r=t.readUint32();i=new Array(r);for(let e=0;e6,`non valid type ${s}`);const n=t.readUint32(),a=Pt(t,s,n);Bt(t),i[e]={name:r,type:Ot(s),value:a}}}return i}function jt(){const t=[];t.push("DIMENSIONS");for(const e of this.dimensions)t.push(` ${e.name.padEnd(30)} = size: ${e.size}`);t.push(""),t.push("GLOBAL ATTRIBUTES");for(const e of this.globalAttributes)t.push(` ${e.name.padEnd(30)} = ${e.value}`);const e=JSON.parse(JSON.stringify(this.variables));t.push(""),t.push("VARIABLES:");for(const i of e){i.value=this.getDataVariable(i);let e=JSON.stringify(i.value);e.length>50&&(e=e.substring(0,50)),isNaN(i.value.length)||(e+=` (length: ${i.value.length})`),t.push(` ${i.name.padEnd(30)} = ${e}`)}return t.join("\n")}class NetCDFReader{constructor(t){this.toString=jt;const e=new IOBuffer(t);e.setBigEndian(),Ut("CDF"!==e.readChars(3),"should start with CDF");const i=e.readByte();Ut(i>2,"unknown version"),this.header=Gt(e,i),this.buffer=e}get version(){return 1===this.header.version?"classic format":"64-bit offset format"}get recordDimension(){return this.header.recordDimension}get dimensions(){return this.header.dimensions}get globalAttributes(){return this.header.globalAttributes}getAttribute(t){const e=this.globalAttributes.find((e=>e.name===t));return e?e.value:null}getDataVariableAsString(t){const e=this.getDataVariable(t);return e?e.join(""):null}get variables(){return this.header.variables}getDataVariable(t){let e;if(e="string"==typeof t?this.header.variables.find((e=>e.name===t)):t,void 0===e)throw new Error("Not a valid NetCDF v3.x file: variable not found");return this.buffer.seek(e.offset),e.record?function(t,e,i){const r=kt(e.type),s=e.size?e.size/Dt(r):1,n=i.length,a=new Array(n),o=i.recordStep;if(!o)throw new Error("recordDimension.recordStep is undefined");for(let e=0;ee.name===t))}attributeExists(t){return void 0!==this.globalAttributes.find((e=>e.name===t))}}class GLModel{static sameObj(t,e){return t&&e?JSON.stringify(t)==JSON.stringify(e):t==e}constructor(t,e){this.atoms=[],this.frames=[],this.box=null,this.atomdfs=null,this.id=0,this.hidden=!1,this.molObj=null,this.renderedMolObj=null,this.lastColors=null,this.modelData={},this.modelDatas=null,this.idMatrix=new $.Matrix4,this.dontDuplicateAtoms=!0,this.defaultColor=s.elementColors.defaultColor,this.defaultStickRadius=.25,this.options=e||{},this.ElementColors=this.options.defaultcolors?this.options.defaultcolors:s.elementColors.defaultColors,this.defaultSphereRadius=this.options.defaultSphereRadius?this.options.defaultSphereRadius:1.5,this.defaultCartoonQuality=this.options.cartoonQuality?this.options.cartoonQuality:10,this.id=t}getRadiusFromStyle(t,e){var i=this.defaultSphereRadius;if(void 0!==e.radius)i=e.radius;else if(GLModel.vdwRadii[t.elem])i=GLModel.vdwRadii[t.elem];else if(t.elem.length>1){let e=t.elem;e=e[0].toUpperCase()+e[1].toLowerCase(),GLModel.vdwRadii[e]&&(i=GLModel.vdwRadii[e])}return void 0!==e.scale&&(i*=e.scale),i}drawAtomCross(t,e){if(t.style.cross){var i=t.style.cross;if(!i.hidden){var r=i.linewidth||GLModel.defaultlineWidth;e[r]||(e[r]=new n.Geometry);var s=e[r].updateGeoGroup(6),a=this.getRadiusFromStyle(t,i),o=[[a,0,0],[-a,0,0],[0,a,0],[0,-a,0],[0,0,a],[0,0,-a]],l=t.clickable||t.hoverable;l&&void 0===t.intersectionShape&&(t.intersectionShape={sphere:[],cylinder:[],line:[]});for(var h=(0,L.getColorFromStyle)(t,i),c=s.vertexArray,d=s.colorArray,u=0;u<6;u++){var f=3*s.vertices;if(s.vertices++,c[f]=t.x+o[u][0],c[f+1]=t.y+o[u][1],c[f+2]=t.z+o[u][2],d[f]=h.r,d[f+1]=h.g,d[f+2]=h.b,l){var p=new $.Vector3(o[u][0],o[u][1],o[u][2]);p.multiplyScalar(.1),p.set(p.x+t.x,p.y+t.y,p.z+t.z),t.intersectionShape.line.push(p)}}}}}getGoodCross(t,e,i,r){for(var s=null,n=-1,a=0,o=t.bonds.length;an&&(s=c,(n=l)>.1))return s}return s}getSideBondV(t,e,i){var r,s,n,a,o=new $.Vector3(t.x,t.y,t.z),l=new $.Vector3(e.x,e.y,e.z).clone(),h=null;if(l.sub(o),1===t.bonds.length)1===e.bonds.length?(h=l.clone(),Math.abs(h.x)>1e-4?h.y+=1:h.x+=1):(r=(i+1)%e.bonds.length,s=e.bonds[r],(n=this.atoms[s]).index==t.index&&(r=(r+1)%e.bonds.length,s=e.bonds[r],n=this.atoms[s]),(a=new $.Vector3(n.x,n.y,n.z).clone()).sub(o),(h=a.clone()).cross(l));else if((h=this.getGoodCross(t,e,o,l)).lengthSq()<.01){var c=this.getGoodCross(e,t,o,l);null!=c&&(h=c)}return h.lengthSq()<.01&&(h=l.clone(),Math.abs(h.x)>1e-4?h.y+=1:h.x+=1),h.cross(l),h.normalize(),h}addLine(t,e,i,r,s,n){t[i]=r.x,t[i+1]=r.y,t[i+2]=r.z,e[i]=n.r,e[i+1]=n.g,e[i+2]=n.b,t[i+3]=s.x,t[i+4]=s.y,t[i+5]=s.z,e[i+3]=n.r,e[i+4]=n.g,e[i+5]=n.b}drawBondLines(t,e,i){if(t.style.line){var r=t.style.line;if(!r.hidden){var a,o,l,h,c=r.linewidth||GLModel.defaultlineWidth;i[c]||(i[c]=new n.Geometry);for(var d=i[c].updateGeoGroup(6*t.bonds.length),u=d.vertexArray,f=d.colorArray,p=0;p=g.index)){var m=new $.Vector3(t.x,t.y,t.z),v=new $.Vector3(g.x,g.y,g.z),_=m.clone().add(v).multiplyScalar(.5),y=!1,b=t.clickable||t.hoverable,x=g.clickable||g.hoverable;(b||x)&&(b&&(void 0===t.intersectionShape&&(t.intersectionShape={sphere:[],cylinder:[],line:[],triangle:[]}),t.intersectionShape.line.push(m),t.intersectionShape.line.push(_)),x&&(void 0===g.intersectionShape&&(g.intersectionShape={sphere:[],cylinder:[],line:[],triangle:[]}),g.intersectionShape.line.push(_),g.intersectionShape.line.push(v)));var w=(0,L.getColorFromStyle)(t,t.style.line),A=(0,L.getColorFromStyle)(g,g.style.line);if(t.bondStyles&&t.bondStyles[p]){var C=t.bondStyles[p];if(!C.iswire)continue;C.singleBond&&(y=!0),void 0!==C.color1&&(w=s.CC.color(C.color1)),void 0!==C.color2&&(A=s.CC.color(C.color2))}var S,M,z=3*d.vertices;if(t.bondOrder[p]>1&&t.bondOrder[p]<4&&!y){var T=this.getSideBondV(t,g,p),E=v.clone();E.sub(m),2==t.bondOrder[p]?(T.multiplyScalar(.1),(a=m.clone()).add(T),(o=m.clone()).sub(T),(l=a.clone()).add(E),(h=o.clone()).add(E),w==A?(d.vertices+=4,this.addLine(u,f,z,a,l,w),this.addLine(u,f,z+6,o,h,w)):(d.vertices+=8,E.multiplyScalar(.5),(S=a.clone()).add(E),(M=o.clone()).add(E),this.addLine(u,f,z,a,S,w),this.addLine(u,f,z+6,S,l,A),this.addLine(u,f,z+12,o,M,w),this.addLine(u,f,z+18,M,h,A))):3==t.bondOrder[p]&&(T.multiplyScalar(.1),(a=m.clone()).add(T),(o=m.clone()).sub(T),(l=a.clone()).add(E),(h=o.clone()).add(E),w==A?(d.vertices+=6,this.addLine(u,f,z,m,v,w),this.addLine(u,f,z+6,a,l,w),this.addLine(u,f,z+12,o,h,w)):(d.vertices+=12,E.multiplyScalar(.5),(S=a.clone()).add(E),(M=o.clone()).add(E),this.addLine(u,f,z,m,_,w),this.addLine(u,f,z+6,_,v,A),this.addLine(u,f,z+12,a,S,w),this.addLine(u,f,z+18,S,l,A),this.addLine(u,f,z+24,o,M,w),this.addLine(u,f,z+30,M,h,A)))}else w==A?(d.vertices+=2,this.addLine(u,f,z,m,v,w)):(d.vertices+=4,this.addLine(u,f,z,m,_,w),this.addLine(u,f,z+6,_,v,A))}}}}}drawAtomSphere(t,e){if(t.style.sphere){var i=t.style.sphere;if(!i.hidden){var r=(0,L.getColorFromStyle)(t,i),s=this.getRadiusFromStyle(t,i);if((!0===t.clickable||t.hoverable)&&void 0!==t.intersectionShape){var n=new $.Vector3(t.x,t.y,t.z);t.intersectionShape.sphere.push(new J.Sphere(n,s))}rt.drawSphere(e,t,s,r)}}}drawAtomClickSphere(t){if(t.style.clicksphere){var e=t.style.clicksphere;if(!e.hidden){var i=this.getRadiusFromStyle(t,e);if((!0===t.clickable||t.hoverable)&&void 0!==t.intersectionShape){var r=new $.Vector3(t.x,t.y,t.z);t.intersectionShape.sphere.push(new J.Sphere(r,i))}}}}drawAtomInstanced(t,e){if(t.style.sphere){var i=t.style.sphere;if(!i.hidden){var r=this.getRadiusFromStyle(t,i),s=(0,L.getColorFromStyle)(t,i),n=e.updateGeoGroup(1),a=n.vertices,o=3*a,l=n.vertexArray,h=n.colorArray,c=n.radiusArray;if(l[o]=t.x,l[o+1]=t.y,l[o+2]=t.z,h[o]=s.r,h[o+1]=s.g,h[o+2]=s.b,c[a]=r,(!0===t.clickable||t.hoverable)&&void 0!==t.intersectionShape){var d=new $.Vector3(t.x,t.y,t.z);t.intersectionShape.sphere.push(new J.Sphere(d,r))}n.vertices+=1}}}drawSphereImposter(t,e,i,r){var s,n=t.updateGeoGroup(4),a=n.vertices,o=3*a,l=n.vertexArray,h=n.colorArray;for(s=0;s<4;s++)l[o+3*s]=e.x,l[o+3*s+1]=e.y,l[o+3*s+2]=e.z;var c=n.normalArray;for(s=0;s<4;s++)h[o+3*s]=r.r,h[o+3*s+1]=r.g,h[o+3*s+2]=r.b;c[o+0]=-i,c[o+1]=i,c[o+2]=0,c[o+3]=-i,c[o+4]=-i,c[o+5]=0,c[o+6]=i,c[o+7]=-i,c[o+8]=0,c[o+9]=i,c[o+10]=i,c[o+11]=0,n.vertices+=4;var d=n.faceArray,u=n.faceidx;d[u+0]=a,d[u+1]=a+1,d[u+2]=a+2,d[u+3]=a+2,d[u+4]=a+3,d[u+5]=a,n.faceidx+=6}drawAtomImposter(t,e){if(t.style.sphere){var i=t.style.sphere;if(!i.hidden){var r=this.getRadiusFromStyle(t,i),s=(0,L.getColorFromStyle)(t,i);if((!0===t.clickable||t.hoverable)&&void 0!==t.intersectionShape){var n=new $.Vector3(t.x,t.y,t.z);t.intersectionShape.sphere.push(new J.Sphere(n,r))}this.drawSphereImposter(e,t,r,s)}}}calculateDashes(t,e,i,r,s){var n=Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2)+Math.pow(t.z-e.z,2));i=Math.max(i,0),s=Math.max(s,0)+2*i,(r=Math.max(r,.001))+s>n&&(r=n,s=0);var a,o=Math.floor((n-r)/(r+s))+1;s=(n-o*r)/o;for(var l=new $.Vector3(t.x,t.y,t.z),h=new $.Vector3((e.x-t.x)/(n/s),(e.y-t.y)/(n/s),(e.z-t.z)/(n/s)),c=new $.Vector3((e.x-t.x)/(n/r),(e.y-t.y)/(n/r),(e.z-t.z)/(n/r)),d=[],u=0;u{var e=i.imposter?GLModel.drawStickImposter:rt.drawCylinder;return!T&&t>=1?e:(t,i,r,s,n,a=0,o=0,l=.1,h=.25)=>{this.calculateDashes(i,r,s,l,h).forEach((i=>{e(t,i.from,i.to,s,n,a,o)}))}};for(h=0;h3){if(t.bondOrder[h]<1&&(M*=t.bondOrder[h]),!k.capDrawn&&k.bonds.length<4&&(F=2),I!=P?(_=(new $.Vector3).addVectors(U,B).multiplyScalar(.5),D(i,U,_,M,I,E,0,C,S),D(i,_,B,M,P,0,F,C,S)):D(i,U,B,M,I,E,F,C,S),o=t.clickable||t.hoverable,l=k.clickable||k.hoverable,o||l){if(_||(_=(new $.Vector3).addVectors(U,B).multiplyScalar(.5)),o){var N=new J.Cylinder(U,_,M),G=new J.Sphere(U,M);t.intersectionShape.cylinder.push(N),t.intersectionShape.sphere.push(G)}if(l){var V=new J.Cylinder(B,_,M),j=new J.Sphere(B,M);k.intersectionShape.cylinder.push(V),k.intersectionShape.sphere.push(j)}}}else if(t.bondOrder[h]>1){var H=0,W=0;M!=x&&(H=2,W=2);var q,Y,Z,X,K,Q=B.clone(),tt=null;Q.sub(U),tt=this.getSideBondV(t,k,h),2==t.bondOrder[h]?(q=M*w,tt.multiplyScalar(1.5*q),(Y=U.clone()).add(tt),(Z=U.clone()).sub(tt),(X=Y.clone()).add(Q),(K=Z.clone()).add(Q),I!=P?(_=(new $.Vector3).addVectors(Y,X).multiplyScalar(.5),y=(new $.Vector3).addVectors(Z,K).multiplyScalar(.5),D(i,Y,_,q,I,H,0),D(i,_,X,q,P,0,W),D(i,Z,y,q,I,H,0),D(i,y,K,q,P,0,W)):(D(i,Y,X,q,I,H,W),D(i,Z,K,q,I,H,W)),o=t.clickable||t.hoverable,l=k.clickable||k.hoverable,(o||l)&&(_||(_=(new $.Vector3).addVectors(Y,X).multiplyScalar(.5)),y||(y=(new $.Vector3).addVectors(Z,K).multiplyScalar(.5)),o&&(u=new J.Cylinder(Y,_,q),f=new J.Cylinder(Z,y,q),t.intersectionShape.cylinder.push(u),t.intersectionShape.cylinder.push(f)),l&&(g=new J.Cylinder(X,_,q),m=new J.Cylinder(K,y,q),k.intersectionShape.cylinder.push(g),k.intersectionShape.cylinder.push(m)))):3==t.bondOrder[h]&&(q=M*A,tt.cross(Q),tt.normalize(),tt.multiplyScalar(3*q),(Y=U.clone()).add(tt),(Z=U.clone()).sub(tt),(X=Y.clone()).add(Q),(K=Z.clone()).add(Q),I!=P?(_=(new $.Vector3).addVectors(Y,X).multiplyScalar(.5),y=(new $.Vector3).addVectors(Z,K).multiplyScalar(.5),b=(new $.Vector3).addVectors(U,B).multiplyScalar(.5),D(i,Y,_,q,I,H,0),D(i,_,X,q,P,0,W),D(i,U,b,q,I,E,0),D(i,b,B,q,P,0,F),D(i,Z,y,q,I,H,0),D(i,y,K,q,P,0,W)):(D(i,Y,X,q,I,H,W),D(i,U,B,q,I,E,F),D(i,Z,K,q,I,H,W)),o=t.clickable||t.hoverable,l=k.clickable||k.hoverable,(o||l)&&(_||(_=(new $.Vector3).addVectors(Y,X).multiplyScalar(.5)),y||(y=(new $.Vector3).addVectors(Z,K).multiplyScalar(.5)),b||(b=(new $.Vector3).addVectors(U,B).multiplyScalar(.5)),o&&(u=new J.Cylinder(Y.clone(),_.clone(),q),f=new J.Cylinder(Z.clone(),y.clone(),q),p=new J.Cylinder(U.clone(),b.clone(),q),t.intersectionShape.cylinder.push(u),t.intersectionShape.cylinder.push(f),t.intersectionShape.cylinder.push(p)),l&&(g=new J.Cylinder(X.clone(),_.clone(),q),m=new J.Cylinder(K.clone(),y.clone(),q),v=new J.Cylinder(B.clone(),b.clone(),q),k.intersectionShape.cylinder.push(g),k.intersectionShape.cylinder.push(m),k.intersectionShape.cylinder.push(v))))}}}var et=!1,it=0,st=!1;for(h=0;h0&&(et=!0):0==it&&(t.bonds.length>0||a.showNonBonded)&&(et=!0),et&&(M=x,i.imposter?this.drawSphereImposter(i.sphereGeometry,t,M,I):rt.drawSphere(i,t,M,I))}}}createMolObj(t,e){e=e||{};var i,r,a,o,l=new n.Object3D,h=[],c={},d={},u=this.drawAtomSphere,f=null,p=null;e.supportsImposters?(u=this.drawAtomImposter,(f=new n.Geometry(!0)).imposter=!0,(p=new n.Geometry(!0,!0)).imposter=!0,p.sphereGeometry=new n.Geometry(!0),p.sphereGeometry.imposter=!0,p.drawnCaps={}):e.supportsAIA?(u=this.drawAtomInstanced,(f=new n.Geometry(!1,!0,!0)).instanced=!0,p=new n.Geometry(!0)):(f=new n.Geometry(!0),p=new n.Geometry(!0));var g,m={},v=[Number.POSITIVE_INFINITY,Number.NEGATIVE_INFINITY];for(i=0,a=t.length;iv[1]&&(v[1]=_.resi)),h.push(_))}}if(h.length>0&&xt(l,h,v,this.defaultCartoonQuality),f&&f.vertices>0){f.initTypedArrays();var y=null,b=null;f.imposter?y=new n.SphereImposterMaterial({ambient:0,vertexColors:!0,reflectivity:0}):f.instanced?(b=new n.Geometry(!0),rt.drawSphere(b,{x:0,y:0,z:0},1,new s.Color(.5,.5,.5)),b.initTypedArrays(),y=new n.InstancedMaterial({sphereMaterial:new n.MeshLambertMaterial({ambient:0,vertexColors:!0,reflectivity:0}),sphere:b})):y=new n.MeshLambertMaterial({ambient:0,vertexColors:!0,reflectivity:0}),m.sphere<1&&m.sphere>=0&&(y.transparent=!0,y.opacity=m.sphere),b=new n.Mesh(f,y),l.add(b)}if(p.vertices>0){var x=null,w=null,A=p.sphereGeometry;A&&void 0!==A.vertices&&0!=A.vertices||(A=null),p.initTypedArrays(),A&&A.initTypedArrays();var C={ambient:0,vertexColors:!0,reflectivity:0};p.imposter?(x=new n.StickImposterMaterial(C),w=new n.SphereImposterMaterial(C)):(x=new n.MeshLambertMaterial(C),w=new n.MeshLambertMaterial(C),x.wireframe&&(p.setUpWireframe(),A&&A.setUpWireframe())),m.stick<1&&m.stick>=0&&(x.transparent=!0,x.opacity=m.stick,w.transparent=!0,w.opacity=m.stick);var S=new n.Mesh(p,x);if(l.add(S),A){var M=new n.Mesh(A,w);l.add(M)}}for(i in c)if(c.hasOwnProperty(i)){g=i;var z=new n.LineBasicMaterial({linewidth:g,vertexColors:!0});m.line<1&&m.line>=0&&(z.transparent=!0,z.opacity=m.line),c[i].initTypedArrays();var T=new n.Line(c[i],z,n.LineStyle.LinePieces);l.add(T)}for(i in d)if(d.hasOwnProperty(i)){g=i;var E=new n.LineBasicMaterial({linewidth:g,vertexColors:!0});m.cross<1&&m.cross>=0&&(E.transparent=!0,E.opacity=m.cross),d[i].initTypedArrays();var L=new n.Line(d[i],E,n.LineStyle.LinePieces);l.add(L)}if(this.dontDuplicateAtoms&&this.modelData.symmetries&&this.modelData.symmetries.length>0){var F,I=new n.Object3D;for(F=0;Fi?e-r:e}adjustCoordinatesToBox(){if(this.box&&this.atomdfs)for(var t=this.box[0],e=this.box[1],i=this.box[2],r=.9*t,s=.9*e,n=.9*i,a=0;a=i)&&(t=i-1),null!=r.frames.url){var a=r.frames.url;(0,L.getbin)(a+"/traj/frame/"+t+"/"+r.frames.path,void 0,"POST",void 0).then((function(t){for(var e=new Float32Array(t,44),i=0,n=0;n=r&&t<=s)return!0}}return!1}static deepCopyAndCache(t,e){if("object"!=typeof t||null==t)return t;if(t.__cache_created)return t;const i={};for(const r in t){const s=t[r];if(Array.isArray(s)){i[r]=[];for(let t=0;t=r[0][0]&&a<=r[1][0]&&o>=r[0][1]&&o<=r[1][1]&&l>=r[0][2]&&l<=r[1][2]&&(a>=i[0][0]&&a<=i[1][0]&&o>=i[0][1]&&o<=i[1][1]&&l>=i[0][2]&&l<=i[1][2]||n.push(this.atoms[t]))}return n}static getFloat(t){return"number"==typeof t?t:parseFloat(t)}selectedAtoms(t,e){var i=[];t=GLModel.deepCopyAndCache(t||{},this),e||(e=this.atoms);for(var r=e.length,s=0;s0&&i.push(r[t])}}if(t.hasOwnProperty("within")&&t.within.hasOwnProperty("sel")&&t.within.hasOwnProperty("distance")){var o=this.selectedAtoms(t.within.sel,this.atoms),l={};const e=GLModel.getFloat(t.within.distance),r=e*e;for(let t=0;t0&&(l[e]=1)}var h=[];if(t.within.invert)for(let t=0;t0;)if(e=u.pop(),f=e.chain,p=e.resi,void 0===d[e.index]){d[e.index]=!0;for(var g=0;g0&&(this.molObj=null)}else console.log("Callback is not a function")}setHoverable(t,e,i,r){if(e=!!e,i=(0,L.makeFunction)(i),r=(0,L.makeFunction)(r),null!==i)if(null!==r){var s=this.selectedAtoms(t,this.atoms),n=s.length;for(let t=0;t0&&(this.molObj=null)}else console.log("Unhover_callback is not a function");else console.log("Hover_callback is not a function")}enableContextMenu(t,e){var i;e=!!e;var r=this.selectedAtoms(t,this.atoms),s=r.length;for(i=0;i0&&(this.molObj=null)}setColorByElement(t,e){if(null===this.molObj||!GLModel.sameObj(e,this.lastColors)){this.lastColors=e;var i=this.selectedAtoms(t,i);i.length>0&&(this.molObj=null);for(var r=0;r0&&(this.molObj=null),"string"==typeof i&&void 0!==r.Gradient.builtinGradients[i]&&(i=new r.Gradient.builtinGradients[i]),s||(s=i.range()),s||(s=(0,L.getPropertyRange)(o,e)),n=0;n0&&(this.molObj=null);for(let t=0;t=s)continue;let a={b:i,e:s},o=n.bondOrder[t];1!=o&&(a.o=o),e.b.push(a)}}return e}globj(t,e){(null===this.molObj||e.regen)&&(this.molObj=this.createMolObj(this.atoms,e),this.renderedMolObj&&(t.remove(this.renderedMolObj),this.renderedMolObj=null),this.renderedMolObj=this.molObj.clone(),this.hidden&&(this.renderedMolObj.setVisible(!1),this.molObj.setVisible(!1)),t.add(this.renderedMolObj))}exportVRML(){return this.createMolObj(this.atoms,{supportsImposters:!1,supportsAIA:!1}).vrml()}removegl(t){this.renderedMolObj&&(void 0!==this.renderedMolObj.geometry&&this.renderedMolObj.geometry.dispose(),void 0!==this.renderedMolObj.material&&this.renderedMolObj.material.dispose(),t.remove(this.renderedMolObj),this.renderedMolObj=null),this.molObj=null}hide(){this.hidden=!0,this.renderedMolObj&&this.renderedMolObj.setVisible(!1),this.molObj&&this.molObj.setVisible(!1)}show(){this.hidden=!1,this.renderedMolObj&&this.renderedMolObj.setVisible(!0),this.molObj&&this.molObj.setVisible(!0)}addPropertyLabels(t,e,i,r){for(var s=this.selectedAtoms(e,s),n=(0,L.deepCopy)(r),a=0;aMOLECULE/gm)?"mol2":t.match(/^data_/gm)&&t.match(/^loop_/gm)?"cif":t.match(/^HETATM/gm)||t.match(/^ATOM/gm)?"pdb":t.match(/ITEM: TIMESTEP/gm)?"lammpstrj":t.match(/^.*\n.*\n.\s*(\d+)\s+(\d+)/gm)?"sdf":t.match(/^%VERSION\s+VERSION_STAMP/gm)?"prmtop":"xyz",console.log("Best guess: "+e))),(0,K[e])(t,i)}}GLModel.defaultAtomStyle={line:{}},GLModel.defaultlineWidth=1,GLModel.vdwRadii={H:1.2,He:1.4,Li:1.82,Be:1.53,B:1.92,C:1.7,N:1.55,O:1.52,F:1.47,Ne:1.54,Na:2.27,Mg:1.73,Al:1.84,Si:2.1,P:1.8,S:1.8,Cl:1.75,Ar:1.88,K:2.75,Ca:2.31,Ni:1.63,Cu:1.4,Zn:1.39,Ga:1.87,Ge:2.11,As:1.85,Se:1.9,Br:1.85,Kr:2.02,Rb:3.03,Sr:2.49,Pd:1.63,Ag:1.72,Cd:1.58,In:1.93,Sn:2.17,Sb:2.06,Te:2.06,I:1.98,Xe:2.16,Cs:3.43,Ba:2.68,Pt:1.75,Au:1.66,Hg:1.55,Tl:1.96,Pb:2.02,Bi:2.07,Po:1.97,At:2.02,Rn:2.2,Fr:3.48,Ra:2.83,U:1.86},GLModel.ignoredKeys=new Set(["props","invert","model","frame","byres","expand","within","and","or","not"]);var Ht=i(111);const Wt=16;class GLViewer{getWidth(){let t=this.container,e=t.offsetWidth;if(0==e&&"none"===t.style.display){let i=t.style.position,r=t.style.visibility;t.style.display="block",t.style.visibility="hidden",t.style.position="absolute",e=t.offsetWidth,t.style.display="none",t.style.visibility=r,t.style.position=i}return e}getHeight(){let t=this.container,e=t.offsetHeight;if(0==e&&"none"===t.style.display){let i=t.style.position,r=t.style.visibility;t.style.display="block",t.style.visibility="hidden",t.style.position="absolute",e=t.offsetHeight,t.style.display="none",t.style.visibility=r,t.style.position=i}return e}setupRenderer(){this.renderer=new n.Renderer({antialias:this.config.antialias,preserveDrawingBuffer:!0,premultipliedAlpha:!1,id:this.config.id,row:this.config.row,col:this.config.col,rows:this.config.rows,cols:this.config.cols,canvas:this.config.canvas,containerWidth:this.WIDTH,containerHeight:this.HEIGHT,ambientOcclusion:this.config.ambientOcclusion,outline:this.config.outline}),this.renderer.domElement.style.width="100%",this.renderer.domElement.style.height="100%",this.renderer.domElement.style.padding="0",this.renderer.domElement.style.position="absolute",this.renderer.domElement.style.top="0px",this.renderer.domElement.style.left="0px",this.renderer.domElement.style.zIndex="0"}initializeScene(){this.scene=new n.Scene,this.scene.fog=new n.Fog(this.bgColor,100,200),this.modelGroup=new n.Object3D,this.rotationGroup=new n.Object3D,this.rotationGroup.useQuaternion=!0,this.rotationGroup.quaternion=new $.Quaternion(0,0,0,1),this.rotationGroup.add(this.modelGroup),this.scene.add(this.rotationGroup);var t=new n.Light(16777215);t.position=new $.Vector3(.2,.2,1).normalize(),t.intensity=1,this.scene.add(t)}_handleLostContext(t){const e=function(t){const e=t.getBoundingClientRect();return!(e.right<0||e.bottom<0||e.top>(window.innerHeight||document.documentElement.clientHeight)||e.left>(window.innerWidth||document.documentElement.clientWidth))};if(e(this.container)){let t=0;for(let i of document.getElementsByTagName("canvas"))if(e(i)&&null!=i._3dmol_viewer&&(i._3dmol_viewer.resize(),t+=1,t>=Wt))break}}initContainer(t){this.container=t,this.WIDTH=this.getWidth(),this.HEIGHT=this.getHeight(),this.ASPECT=this.renderer.getAspect(this.WIDTH,this.HEIGHT),this.renderer.setSize(this.WIDTH,this.HEIGHT),this.container.append(this.renderer.domElement),this.glDOM=this.renderer.domElement,this.glDOM._3dmol_viewer=this,this.glDOM.addEventListener("webglcontextlost",this._handleLostContext.bind(this)),this.nomouse||(this.glDOM.addEventListener("mousedown",this._handleMouseDown.bind(this),{passive:!1}),this.glDOM.addEventListener("touchstart",this._handleMouseDown.bind(this),{passive:!1}),this.glDOM.addEventListener("wheel",this._handleMouseScroll.bind(this),{passive:!1}),this.glDOM.addEventListener("mousemove",this._handleMouseMove.bind(this),{passive:!1}),this.glDOM.addEventListener("touchmove",this._handleMouseMove.bind(this),{passive:!1}),this.glDOM.addEventListener("contextmenu",this._handleContextMenu.bind(this),{passive:!1}))}decAnim(){this.animated--,this.animated<0&&(this.animated=0)}incAnim(){this.animated++}nextSurfID(){var t=0;for(let i in this.surfaces)if(this.surfaces.hasOwnProperty(i)){var e=parseInt(i);isNaN(e)||e>t&&(t=e)}return t+1}setSlabAndFog(){let t=this.camera.position.z-this.rotationGroup.position.z;t<1&&(t=1),this.camera.near=t+this.slabNear,!this.camera.ortho&&this.camera.near<1&&(this.camera.near=1),this.camera.far=t+this.slabFar,this.camera.near+1>this.camera.far&&(this.camera.far=this.camera.near+1),this.camera.fov=this.fov,this.camera.right=t*Math.tan(Math.PI/180*this.fov),this.camera.left=-this.camera.right,this.camera.top=this.camera.right/this.ASPECT,this.camera.bottom=-this.camera.top,this.camera.updateProjectionMatrix(),this.scene.fog.near=this.camera.near+this.fogStart*(this.camera.far-this.camera.near),this.scene.fog.far=this.camera.far,this.config.disableFog&&(this.scene.fog.near=this.scene.fog.far)}show(t){if(this.renderer.setViewport(),this.scene&&(this.setSlabAndFog(),this.renderer.render(this.scene,this.camera),this.viewChangeCallback&&this.viewChangeCallback(this._viewer.getView()),!t&&this.linkedViewers.length>0))for(var e=this._viewer.getView(),i=0;i0){let e=this.CAMERA_Z-this.config.lowerZoomLimit;t>e&&(t=e)}if(this.config.upperZoomLimit&&this.config.upperZoomLimit>0){let e=this.CAMERA_Z-this.config.upperZoomLimit;tthis.CAMERA_Z-1&&(t=this.CAMERA_Z-1),t}static slerp(t,e,i){if(1==i)return e.clone();if(0==i)return t.clone();let r=t.x*e.x+t.y*e.y+t.z*e.z+t.w*e.w;if(r>.9995){let r=new $.Quaternion(t.x+i*(e.x-t.x),t.y+i*(e.y-t.y),t.z+i*(e.z-t.z),t.w+i*(e.w-t.w));return r.normalize(),r}r<0&&(e=e.clone().multiplyScalar(-1),r=-r),r>1?r=1:r<-1&&(r=-1);var s=Math.acos(r)*i,n=e.clone();n.sub(t.clone().multiplyScalar(r)),n.normalize();var a=Math.cos(s),o=Math.sin(s),l=new $.Quaternion(t.x*a+n.x*o,t.y*a+n.y*o,t.z*a+n.z*o,t.w*a+n.w*o);return l.normalize(),l}constructor(t,e={}){if(this.nomouse=!1,this.glDOM=null,this.models=[],this.surfaces={},this.shapes=[],this.labels=[],this.clickables=[],this.hoverables=[],this.contextMenuEnabledObjects=[],this.current_hover=null,this.hoverDuration=500,this.longTouchDuration=1e3,this.viewer_frame=0,this.viewChangeCallback=null,this.stateChangeCallback=null,this.NEAR=1,this.FAR=800,this.CAMERA_Z=150,this.fov=20,this.linkedViewers=[],this.renderer=null,this.control_all=!1,this.scene=null,this.rotationGroup=null,this.modelGroup=null,this.fogStart=.4,this.slabNear=-50,this.slabFar=50,this.cq=new $.Quaternion(0,0,0,1),this.dq=new $.Quaternion(0,0,0,1),this.animated=0,this.animationTimers=new Set,this.isDragging=!1,this.mouseStartX=0,this.mouseStartY=0,this.touchDistanceStart=0,this.touchHold=!1,this.currentModelPos=0,this.cz=0,this.cslabNear=0,this.cslabFar=0,this.userContextMenuHandler=null,this.config=e,this.callback=this.config.callback,this.defaultcolors=this.config.defaultcolors,this.defaultcolors||(this.defaultcolors=s.elementColors.defaultColors),this.nomouse=Boolean(this.config.nomouse),this.bgColor=0,this.config.backgroundColor=this.config.backgroundColor||"#ffffff",void 0!==this.config.backgroundColor&&(this.bgColor=s.CC.color(this.config.backgroundColor).getHex()),this.config.backgroundAlpha=null==this.config.backgroundAlpha?1:this.config.backgroundAlpha,this.camerax=0,void 0!==this.config.camerax&&(this.camerax="string"==typeof this.config.camerax?parseFloat(this.config.camerax):this.config.camerax),this._viewer=this,this.container=t,null!=this.config.hoverDuration&&(this.hoverDuration=this.config.hoverDuration),void 0===this.config.antialias&&(this.config.antialias=!0),void 0===this.config.cartoonQuality&&(this.config.cartoonQuality=10),this.WIDTH=this.getWidth(),this.HEIGHT=this.getHeight(),this.setupRenderer(),this.row=null==this.config.row?0:this.config.row,this.col=null==this.config.col?0:this.config.col,this.cols=this.config.cols,this.rows=this.config.rows,this.viewers=this.config.viewers,this.control_all=this.config.control_all,this.ASPECT=this.renderer.getAspect(this.WIDTH,this.HEIGHT),this.camera=new n.Camera(this.fov,this.ASPECT,this.NEAR,this.FAR,this.config.orthographic),this.camera.position=new $.Vector3(this.camerax,0,this.CAMERA_Z),this.lookingAt=new $.Vector3,this.camera.lookAt(this.lookingAt),this.raycaster=new n.Raycaster(new $.Vector3(0,0,0),new $.Vector3(0,0,0)),this.projector=new n.Projector,this.initializeScene(),this.renderer.setClearColorHex(this.bgColor,this.config.backgroundAlpha),this.scene.fog.color=s.CC.color(this.bgColor),document.body.addEventListener("mouseup",this._handleMouseUp.bind(this)),document.body.addEventListener("touchend",this._handleMouseUp.bind(this)),this.initContainer(this.container),this.config.style&&this.setViewStyle(this.config),window.addEventListener("resize",this.resize.bind(this)),void 0!==window.ResizeObserver&&(this.divwatcher=new window.ResizeObserver(this.resize.bind(this)),this.divwatcher.observe(this.container)),void 0!==window.IntersectionObserver){let t=(t,e)=>{t.forEach((t=>{t.isIntersecting&&this.resize()}))};this.intwatcher=new window.IntersectionObserver(t),this.intwatcher.observe(this.container)}try{"function"==typeof this.callback&&this.callback(this)}catch(t){console.log("error with glviewer callback: "+t)}}targetedObjects(t,e,i){var r={x:t,y:e,z:-1};return Array.isArray(i)||(i=this.selectedAtoms(i)),0==i.length?[]:(this.raycaster.setFromCamera(r,this.camera),this.raycaster.intersectObjects(this.modelGroup,i))}modelToScreen(t){let e=!1;Array.isArray(t)||(t=[t],e=!0);let i=this.renderer.getXRatio(),r=this.renderer.getYRatio(),s=this.col,n=this.row,a=s*(this.WIDTH/i),o=(r-n-1)*(this.HEIGHT/r),l=[],h=this.canvasOffset();return t.forEach((t=>{let e=new $.Vector3(t.x,t.y,t.z);e.applyMatrix4(this.modelGroup.matrixWorld),this.projector.projectVector(e,this.camera);let s=this.WIDTH/i*(e.x+1)/2+h.left+a,n=-this.HEIGHT/r*(e.y-1)/2+h.top+o;l.push({x:s,y:n})})),e&&(l=l[0]),l}screenOffsetToModel(t,e,i){var r=t/this.WIDTH,s=e/this.HEIGHT,n=void 0===i?this.rotationGroup.position.z:i,a=this.rotationGroup.quaternion,o=new $.Vector3(0,0,n);return this.projector.projectVector(o,this.camera),o.x+=2*r,o.y-=2*s,this.projector.unprojectVector(o,this.camera),o.z=0,o.applyQuaternion(a),o}screenToModelDistance(t,e){let i=this.canvasOffset(),r=new $.Vector3(e.x,e.y,e.z);r.applyMatrix4(this.modelGroup.matrixWorld);let s=r.clone();this.projector.projectVector(r,this.camera);let n=new $.Vector3(2*(t.x-i.left)/this.WIDTH-1,2*(t.y-i.top)/-this.HEIGHT+1,r.z);return this.projector.unprojectVector(n,this.camera),n.distanceTo(s)}setViewChangeCallback(t){"function"!=typeof t&&null!=t||(this.viewChangeCallback=t)}setStateChangeCallback(t){"function"!=typeof t&&null!=t||(this.stateChangeCallback=t)}getConfig(){return this.config}setConfig(t){this.config=t,t.ambientOcclusion&&this.renderer.enableAmbientOcclusion(t.ambientOcclusion)}getInternalState(){var t={models:[],surfaces:[],shapes:[],labels:[]};for(let e=0;e{e.getCanvas().toBlob((function(e){e.arrayBuffer().then(t)}),"image/png")}))),r+=1,r==t&&(e.viewChangeCallback=s,Promise.all(n).then((t=>{let r=[];for(let e=0;e0&&(this.hoverTimeout=setTimeout((function(){a.handleHoverSelection(n.x,n.y,t)}),this.hoverDuration)),this.isDragging)){t.targetTouches&&(t.targetTouches.length>1||1===t.targetTouches.length&&!this.closeEnoughForClick(t))&&clearTimeout(this.longTouchTimeout);var l=(e-this.mouseStartX)/this.WIDTH,h=(i-this.mouseStartY)/this.HEIGHT;if(0!=this.touchDistanceStart&&t.targetTouches&&2==t.targetTouches.length)o=2,h=2*(this.calcTouchDistance(t)-this.touchDistanceStart)/(this.WIDTH+this.HEIGHT);else t.targetTouches&&3==t.targetTouches.length&&(o=1);l*=r,h*=s;var c,d=Math.hypot(l,h);if(3==o||3==this.mouseButton&&t.ctrlKey)this.slabNear=this.cslabNear+100*l,this.slabFar=this.cslabFar-100*h;else if(2==o||3==this.mouseButton||t.shiftKey)(c=.85*(this.CAMERA_Z-this.rotationGroup.position.z))<80&&(c=80),this.rotationGroup.position.z=this.cz+h*c,this.rotationGroup.position.z=this.adjustZoomToLimits(this.rotationGroup.position.z);else if(1==o||2==this.mouseButton||t.ctrlKey){var u=this.screenOffsetToModel(r*(e-this.mouseStartX),s*(i-this.mouseStartY));this.modelGroup.position.addVectors(this.currentModelPos,u)}else if((0===o||1==this.mouseButton)&&0!==d){var f=Math.sin(d*Math.PI)/d;this.dq.x=Math.cos(d*Math.PI),this.dq.y=0,this.dq.z=f*l,this.dq.w=-f*h,this.rotationGroup.quaternion.set(1,0,0,0),this.rotationGroup.quaternion.multiply(this.dq),this.rotationGroup.quaternion.multiply(this.cq)}this.show()}}_handleContextMenu(t){if(t.preventDefault(),this.closeEnoughForClick(t)){var e=this.mouseStartX,i=this.mouseStartY,r=this.canvasOffset();let n=this.mouseXY(e,i),a=n.x,o=n.y,l=this.targetedObjects(a,o,this.contextMenuEnabledObjects);var s=null;l.length&&(s=l[0].clickable);r=this.canvasOffset(),e=this.mouseStartX-r.left,i=this.mouseStartY-r.top;this.userContextMenuHandler&&(this.userContextMenuHandler(s,e,i,l,t),this.isDragging=!1)}}setContainer(t){let e=(0,L.getElement)(t)||this.container;return this.initContainer(e),this}setBackgroundColor(t,e){(void 0===e||e<0||e>1)&&(e=1);var i=s.CC.color(t);return this.scene.fog.color=i,this.bgColor=i.getHex(),this.renderer.setClearColorHex(i.getHex(),e),this.show(),this}setProjection(t){this.camera.ortho="orthographic"===t,this.setSlabAndFog()}setViewStyle(t){if((t=t||{}).style=t.style||"",t.style.includes("outline")?this.renderer.enableOutline(t):this.renderer.disableOutline(),t.style.includes("ambientOcclusion")){var e={};t.strength&&(e.strength=t.strength),t.radius&&(e.radius=t.radius),this.renderer.enableAmbientOcclusion(e)}else this.renderer.disableAmbientOcclusion();return this}updateSize(){this.renderer.setSize(this.WIDTH,this.HEIGHT),this.ASPECT=this.renderer.getAspect(this.WIDTH,this.HEIGHT),this.renderer.setSize(this.WIDTH,this.HEIGHT),this.camera.aspect=this.ASPECT,this.camera.updateProjectionMatrix()}setWidth(t){return this.WIDTH=t||this.WIDTH,this.updateSize(),this}setHeight(t){return this.HEIGHT=t||this.HEIGHT,this.updateSize(),this}resize(){this.WIDTH=this.getWidth(),this.HEIGHT=this.getHeight();let t=!1;if(this.renderer.isLost()&&this.WIDTH>0&&this.HEIGHT>0){let e=!1,i=this.container.querySelector("canvas");i&&i!=this.renderer.getCanvas()?this.config.canvas=i:(i.remove(),this.config&&null!=this.config.canvas&&(delete this.config.canvas,e=!0)),this.setupRenderer(),this.initContainer(this.container),this.renderer.setClearColorHex(this.bgColor,this.config.backgroundAlpha),t=!0,e&&(this.config.canvas=this.renderer.getCanvas())}if(0==this.WIDTH||0==this.HEIGHT?this.animated&&this._viewer.pauseAnimate():this.animated&&this._viewer.resumeAnimate(),this.updateSize(),t){let t=this.renderer.supportedExtensions();if(t.regen=!0,this.viewers)for(let e=0,i=this.viewers.length;e=0&&(this.modelGroup.remove(this.labels[i].sprite),(this.viewer_frame<0||this.labels[i].frame==this.viewer_frame)&&this.modelGroup.add(this.labels[i].sprite));for(i in this.surfaces)if(this.surfaces.hasOwnProperty(i)){var a=this.surfaces[i];for(r=0;r1||1==a[r].symmetries.length&&!a[r].symmetries[r].isIdentity()){var h,c=new n.Object3D;for(h=0;h0?this.animateMotion(e,i,this.modelGroup.position,this.adjustZoomToLimits(s),this.rotationGroup.quaternion,this.lookingAt):(this.rotationGroup.position.z=this.adjustZoomToLimits(s),this.show()),this}translate(t,e,i=0,r=!1){var s=t/this.WIDTH,n=e/this.HEIGHT,a=new $.Vector3(0,0,-this.CAMERA_Z);this.projector.projectVector(a,this.camera),a.x-=s,a.y-=n,this.projector.unprojectVector(a,this.camera),a.z=0;var o=this.lookingAt.clone().add(a);return i>0?this.animateMotion(i,r,this.modelGroup.position,this.rotationGroup.position.z,this.rotationGroup.quaternion,o):(this.lookingAt=o,this.camera.lookAt(this.lookingAt),this.show()),this}translateScene(t,e,i=0,r=!1){var s=this.screenOffsetToModel(t,e),n=this.modelGroup.position.clone().add(s);return i>0?this.animateMotion(i,r,this.modelGroup.position,this.rotationGroup.position.z,this.rotationGroup.quaternion,this.lookingAt):(this.modelGroup.position=n,this.show()),this}fitSlab(t){t=t||{};var e=this.getAtomsFromSel(t),i=(0,L.getExtent)(e),r=i[1][0]-i[0][0],s=i[1][1]-i[0][1],n=i[1][2]-i[0][2],a=Math.hypot(r,s,n);return a<5&&(a=5),this.slabNear=-a/1.9,this.slabFar=a/2,this}center(t={},e=0,i=!1){var r,s,n=this.getAtomsFromSel(t),a=(0,L.getExtent)(n);(0,L.isEmptyObject)(t)?(this.shapes.forEach((t=>{if(t&&t.boundingSphere&&t.boundingSphere.center){var e=t.boundingSphere.center,i=t.boundingSphere.radius;i>0?(n.push(new $.Vector3(e.x+i,e.y,e.z)),n.push(new $.Vector3(e.x-i,e.y,e.z)),n.push(new $.Vector3(e.x,e.y+i,e.z)),n.push(new $.Vector3(e.x,e.y-i,e.z)),n.push(new $.Vector3(e.x,e.y,e.z+i)),n.push(new $.Vector3(e.x,e.y,e.z-i))):n.push(e)}})),a=(0,L.getExtent)(n),r=n,s=a):(r=this.getAtomsFromSel({}),s=(0,L.getExtent)(r));var o=new $.Vector3(a[2][0],a[2][1],a[2][2]),l=s[1][0]-s[0][0],h=s[1][1]-s[0][1],c=s[1][2]-s[0][2],d=Math.hypot(l,h,c);d<5&&(d=5),this.slabNear=-d/1.9,this.slabFar=d/2,l=a[1][0]-a[0][0],h=a[1][1]-a[0][1],c=a[1][2]-a[0][2],(d=Math.hypot(l,h,c))<5&&(d=5);for(var u=25,f=0;fu&&(u=p)}d=2*Math.sqrt(u);var g=o.clone().multiplyScalar(-1);return e>0?this.animateMotion(e,i,g,this.rotationGroup.position.z,this.rotationGroup.quaternion,this.lookingAt):(this.modelGroup.position=g,this.show()),this}zoomTo(t={},e=0,i=!1){let r=this.getAtomsFromSel(t),s=(0,L.getExtent)(r),n=s;if((0,L.isEmptyObject)(t)){let t=r&&r.length;if(this.shapes.forEach((t=>{if(t&&t.boundingSphere)if(t.boundingSphere.box){let e=t.boundingSphere.box;r.push(new $.Vector3(e.min.x,e.min.y,e.min.z)),r.push(new $.Vector3(e.max.x,e.max.y,e.max.z))}else if(t.boundingSphere.center){var e=t.boundingSphere.center,i=t.boundingSphere.radius;i>0?(r.push(new $.Vector3(e.x+i,e.y,e.z)),r.push(new $.Vector3(e.x-i,e.y,e.z)),r.push(new $.Vector3(e.x,e.y+i,e.z)),r.push(new $.Vector3(e.x,e.y-i,e.z)),r.push(new $.Vector3(e.x,e.y,e.z+i)),r.push(new $.Vector3(e.x,e.y,e.z-i))):r.push(e)}})),n=(0,L.getExtent)(r),!t)for(let t=0;t<3;t++)s[2][t]=(n[0][t]+n[1][t])/2}else{let t=this.getAtomsFromSel({});n=(0,L.getExtent)(t)}var a=new $.Vector3(s[2][0],s[2][1],s[2][2]),o=n[1][0]-n[0][0],l=n[1][1]-n[0][1],h=n[1][2]-n[0][2],c=Math.hypot(o,l,h);c<5&&(c=5),this.slabNear=-c/1.9,this.slabFar=c/2,0===Object.keys(t).length&&(this.slabNear=Math.min(2*-c,-50),this.slabFar=Math.max(2*c,50));var d=this.config.minimumZoomToDistance||5;o=s[1][0]-s[0][0],l=s[1][1]-s[0][1],h=s[1][2]-s[0][2],(c=Math.hypot(o,l,h))u&&(u=p)}c=2*Math.sqrt(u);var g=a.clone().multiplyScalar(-1),m=-(.5*c/Math.tan(Math.PI/180*this.camera.fov/2)-this.CAMERA_Z);return m=this.adjustZoomToLimits(m),e>0?this.animateMotion(e,i,g,m,this.rotationGroup.quaternion,this.lookingAt):(this.modelGroup.position=g,this.rotationGroup.position.z=m,this.show()),this}setSlab(t,e){this.slabNear=t,this.slabFar=e}getSlab(){return{near:this.slabNear,far:this.slabFar}}addLabel(t,e={},i,r=!1){if(i){var s=(0,L.getExtent)(this.getAtomsFromSel(i));e.position={x:s[2][0],y:s[2][1],z:s[2][2]}}var n=new Label(t,e);return n.setContext(),this.modelGroup.add(n.sprite),this.labels.push(n),r||this.show(),n}addResLabels(t,e,i=!1){let r=this.labels.length;return this.applyToModels("addResLabels",t,this,e,i),this.show(),this.labels.slice(r)}addPropertyLabels(t,e,i){return this.applyToModels("addPropertyLabels",t,e,this,i),this.show(),this}removeLabel(t){for(var e=0;e0&&void 0===this.shapes[this.shapes.length-1];)this.shapes.pop();return this}removeAllShapes(){for(var t=0;t-1e-4&&s.x<1.0001&&s.y>-1e-4&&s.y<1.0001&&s.z>-1e-4&&s.z<1.0001)}}for(let s=0;ss){t.start=i,t.end=r,e.addLine(t);break}c.addVectors(i,l),t.start=i,t.end=c,e.addLine(t),i=c.clone(),d+=n,c.addVectors(i,h),i=c.clone(),d+=a}return e.finalize(),e}addCustom(t){var e=new GLShape(t=t||{});return e.shapePosition=this.shapes.length,e.addCustom(t),this.shapes.push(e),e.finalize(),e}addVolumetricData(t,e,i={}){var r=new st.VolumeData(t,e);return i.hasOwnProperty("transferfn")?this.addVolumetricRender(r,i):this.addIsosurface(r,i)}addIsosurface(t,e={},i){var r=new GLShape(e);return r.shapePosition=this.shapes.length,r.addIsosurface(t,e,i,this),this.shapes.push(r),r}addVolumetricRender(t,e){var i=new GLVolumetricRender(t,e=e||{},this);return i.shapePosition=this.shapes.length,this.shapes.push(i),i}hasVolumetricRender(){return this.renderer.supportsVolumetric()}enableFog(t){t?this.scene.fog=new n.Fog(this.bgColor,100,200):(this.config.disableFog=!0,this.show())}setFrame(t){this.viewer_frame=t;let e=this;return new Promise((function(i){var r=e.models.map((function(i){return i.setFrame(t,e)}));Promise.all(r).then((function(){i()}))}))}getFrame(){return this.viewer_frame}getNumFrames(){var t=0;for(let e=0;et&&(t=this.models[e].getNumFrames());for(let e=0;e=t&&(t=this.shapes[e].frame+1);for(let e=0;e=t&&(t=this.labels[e].frame+1);return t}animate(t){this.incAnim();var e=100,i="forward",r=1/0;(t=t||{}).interval&&(e=t.interval),t.loop&&(i=t.loop),t.reps&&(r=t.reps);var s=this.getNumFrames(),n=this,a=0;t.startFrame&&(a=t.startFrame%s);var o=1;t.step&&(r/=o=t.step);var l,h,c=0,d=s*r,u=new Date,f=function(t){u=new Date,"forward"==t?n.setFrame(a).then((function(){a=(a+o)%s,l()})):"backward"==t?n.setFrame(s-1-a).then((function(){a=(a+o)%s,l()})):n.setFrame(a).then((function(){o*=(a+=o)%(s-1)==0?-1:1,l()}))};return l=function(){if(n.render(),n.getCanvas().isConnected)if(++c>=d||!n.isAnimated())h.cancel(),n.animationTimers.delete(h),n.decAnim();else{var t=e-((new Date).getTime()-u.getTime());t=t>0?t:0,n.animationTimers.delete(h),h=new L.PausableTimer(f,t,i),n.animationTimers.add(h)}else n.stopAnimate()},h=new L.PausableTimer(f,0,i),this.animationTimers.add(h),this}stopAnimate(){return this.animated=0,this.animationTimers.forEach((function(t){t.cancel()})),this.animationTimers=new Set,this}pauseAnimate(){return this.animationTimers.forEach((function(t){t.pause()})),this}resumeAnimate(){return this.animationTimers.forEach((function(t){t.resume()})),this}isAnimated(){return this.animated>0}getModelOpt(t){return t&&!t.defaultcolors?(t.defaultcolors=this.defaultcolors,t.cartoonQuality=t.cartoonQuality||this.config.cartoonQuality):void 0===t&&(t={defaultcolors:this.defaultcolors,cartoonQuality:this.config.cartoonQuality}),t}addModel(t,e="",i){i=this.getModelOpt(i);var r=new GLModel(this.models.length,i);return r.addMolData(t,e,i),this.models.push(r),r}addModels(t,e,i){(i=this.getModelOpt(i)).multimodel=!0,i.frames=!0;for(var r=GLModel.parseMolData(t,e,i),s=0;s0&&void 0===this.models[this.models.length-1];)this.models.pop();return this}}removeAllModels(){for(var t=0;te[1][0]||r.ye[1][1]||r.ze[1][2]||i.push(r))}return i}static volume(t){return(t[1][0]-t[0][0])*(t[1][1]-t[0][1])*(t[1][2]-t[0][2])}carveUpExtent(t,e,i){let r=[],s={};for(let t=0,i=e.length;tr&&i>s?0:r>i&&r>s?1:2;var n=a(t),l=a(t),h=(t[1][e]-t[0][e])/2+t[0][e];n[1][e]=h,l[0][e]=h;var c=o(n),d=o(l);return c.concat(d)},l=o(t);for(let t=0,s=l.length;t0)for(let e=0,i=c.length;e1||1==v.length&&!v[0].isIdentity()){m=!0;break}}var _=function(t,i,s){var n;f=a?GLViewer.shallowCopy(c.getAtomsFromSel(a)):s;var o=(0,L.getExtent)(s,!0);if(e.map&&e.map.prop){var u=e.map.prop;let t=(0,r.getGradient)(e.map.scheme||e.map.gradient||new r.Gradient.RWB),i=t.range();i||(i=(0,L.getPropertyRange)(s,u)),e.colorscheme={prop:u,gradient:t}}for(let t=0,r=i.length;t0){var m=(0,L.getExtent)(f,!0);g.sort((function(t,e){var i=function(t,e){var i=t.extent,r=i[1][0]-i[0][0],s=i[1][1]-i[0][1],n=i[1][2]-i[0][2],a=r-e[2][0];a*=a;var o=s-e[2][1];o*=o;var l=n-e[2][2];return a+o+(l*=l)};return i(t,m)-i(e,m)}))}var v=[];for(let t=0,e=i.length;t0&&(y.push({geo:new n.Geometry(!0),mat:h,done:!1,finished:!1,symmetries:this.models[g].getSymmetries()}),A.push(_(y[y.length-1],x[g],w[g])));b=Promise.all(A)}else y.push({geo:new n.Geometry(!0),mat:h,done:!1,finished:!1,symmetries:[new $.Matrix4]}),b=_(y[y.length-1],u,p);return this.surfaces[l]=y,b.surfid=l,o&&"function"==typeof o?(b.then((function(t){o(t)})),l):b}setSurfaceMaterialStyle(t,e){if((0,L.adjustVolumeStyle)(e),this.surfaces[t]){var i=this.surfaces[t];for(let t=0;t0?this.camera.position.x=i*Math.tan(Math.PI/180*e):this.camera.position.x=-i*Math.tan(Math.PI/180*e),this.camera.lookAt(new $.Vector3(0,0,this.rotationGroup.position.z)),this.camera.position.x}setDefaultCartoonQuality(t){this.config.cartoonQuality=t}}function qt(t,e){if(t=(0,L.getElement)(t)){e=e||{};try{return new GLViewer(t,e)}catch(t){throw"error creating viewer: "+t}}}function Yt(t,e={},i={}){if(t=(0,L.getElement)(t)){var r=[],s=document.createElement("canvas");i.rows=e.rows,i.cols=e.cols,i.control_all=null!=e.control_all&&e.control_all,t.appendChild(s);try{for(var n=0;n{var l=[],h=[],c="";"static"==o.style.position&&(o.style.position="relative");var d=null;if(n=null,o.dataset.pdb)l.push("https://files.rcsb.org/view/"+o.dataset.pdb+".pdb"),h.push("pdb");else if(o.dataset.cid)h.push("sdf"),l.push("https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/"+o.dataset.cid+"/SDF?record_type=3d");else if(o.dataset.href||o.dataset.url){if(c=o.dataset.href?o.dataset.href:o.dataset.url,l.push(c),"gz"==(n=c.substring(c.lastIndexOf(".")+1))){let t=c.substring(0,c.lastIndexOf(".")).lastIndexOf(".");n=c.substring(t+1)}h.push(n);var u=c.substring(c.lastIndexOf("/")+1,c.lastIndexOf("."));"/"==u&&(u=c.substring(c.lastIndexOf("/")+1)),o.dataset[h[h.length-1]]=u}let f=o.dataset;for(i in f)"pdb"===i.substring(0,3)&&"pdb"!==i?(l.push("https://files.rcsb.org/view/"+f[i]+".pdb"),h.push("pdb")):"href"===i.substring(0,4)&&"href"!==i?(c=f[i],l.push(c),h.push(c.substring(c.lastIndexOf(".")+1))):"cid"===i.substring(0,3)&&"cid"!==i&&(l.push("https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/"+f[i]+"/SDF?record_type=3d"),h.push("sdf"));var p={};o.dataset.options&&(p=(0,L.specStringToObject)(o.dataset.options));var g=s.CC.color(o.dataset.backgroundcolor),m=o.dataset.backgroundalpha;m=null==m?1:parseFloat(m);var v={line:{}};o.dataset.style&&(v=(0,L.specStringToObject)(o.dataset.style));var _={};o.dataset.select&&(_=(0,L.specStringToObject)(o.dataset.select));var y=[],b=[],x=[],w={},A=null,C=o.dataset,S=/style(.+)/,M=/surface(.*)/,z=/labelres(.*)/,T=[];for(r in C)Object.prototype.hasOwnProperty.call(C,r)&&T.push(r);for(T.sort(),i=0;i{d.loadSurface("VDW",e,r,t)})):t.addSurface(Q.VDW,r,e,e)}for(i=0;ifunction(n){c=l[t];var a=r.dataset.type||r.dataset.datatype||h[t];if(s.addModel(n,a,p),d){var o=r.dataset[h[t]];d.setModelTitle(o)}if(t+=1,t{"complete"===document.readyState&&$t()},window&&(window.$3Dmol=e)},865:(t,e,i)=>{"use strict";i.r(e),i.d(e,{CUBE:()=>a});var r=i(638),s=i(392);const n={1:"H",2:"He",3:"Li",4:"Be",5:"B",6:"C",7:"N",8:"O",9:"F",10:"Ne",11:"Na",12:"Mg",13:"Al",14:"Si",15:"P",16:"S",17:"Cl",18:"Ar",19:"K",20:"Ca",21:"Sc",22:"Ti",23:"V",24:"Cr",25:"Mn",26:"Fe",27:"Co",28:"Ni",29:"Cu",30:"Zn",31:"Ga",32:"Ge",33:"As",34:"Se",35:"Br",36:"Kr",37:"Rb",38:"Sr",39:"Y",40:"Zr",41:"Nb",42:"Mo",43:"Tc",44:"Ru",45:"Rh",46:"Pd",47:"Ag",48:"Cd",49:"In",50:"Sn",51:"Sb",52:"Te",53:"I",54:"Xe",55:"Cs",56:"Ba",71:"Lu",72:"Hf",73:"Ta",74:"W",75:"Re",76:"Os",77:"Ir",78:"Pt",79:"Au",80:"Hg",81:"Tl",82:"Pb",83:"Bi",84:"Po",85:"At",86:"Rn",87:"Fr",88:"Ra",104:"Rf",105:"Db",106:"Sg",107:"Bh",108:"Hs",109:"Mt",110:"Ds",111:"Rg",112:"Cn",113:"Nh",114:"Fl",115:"Mc",116:"Lv",117:"Ts",118:"Og",57:"La",58:"Ce",59:"Pr",60:"Nd",61:"Pm",62:"Sm",63:"Eu",64:"Gd",65:"Tb",66:"Dy",67:"Ho",68:"Er",69:"Tm",70:"Yb",89:"Ac",90:"Th",91:"Pa",92:"U",93:"Np",94:"Pu",95:"Am",96:"Cm",97:"Bk",98:"Cf",99:"Es",100:"Fm",101:"Md",102:"No"};function a(t,e){e=e||{};const i=[[]];let a=t.split(/\r?\n/);const o=void 0===e.assignBonds||e.assignBonds;if(a.length<6)return i;let l=a[2].replace(/^\s+/,"").replace(/\s+/g," ").split(" ");const h=Math.abs(parseFloat(l[0]));let c={origin:void 0,size:void 0,unit:void 0,matrix4:void 0,matrix:void 0};const d=c.origin=new r.Vector3(parseFloat(l[1]),parseFloat(l[2]),parseFloat(l[3]));l=a[3].replace(/^\s+/,"").replace(/\s+/g," ").split(" "),l=a[3].replace(/^\s+/,"").replace(/\s+/g," ").split(" ");const u=l[0]>0?.529177:1;d.multiplyScalar(u);const f=Math.abs(l[0]),p=new r.Vector3(parseFloat(l[1]),parseFloat(l[2]),parseFloat(l[3])).multiplyScalar(u);l=a[4].replace(/^\s+/,"").replace(/\s+/g," ").split(" ");const g=Math.abs(l[0]),m=new r.Vector3(parseFloat(l[1]),parseFloat(l[2]),parseFloat(l[3])).multiplyScalar(u);l=a[5].replace(/^\s+/,"").replace(/\s+/g," ").split(" ");const v=Math.abs(l[0]),_=new r.Vector3(parseFloat(l[1]),parseFloat(l[2]),parseFloat(l[3])).multiplyScalar(u);if(c.size={x:f,y:g,z:v},c.unit=new r.Vector3(p.x,m.y,_.z),0!=p.y||0!=p.z||0!=m.x||0!=m.z||0!=_.x||0!=_.y){c.matrix4=new r.Matrix4(p.x,m.x,_.x,0,p.y,m.y,_.y,0,p.z,m.z,_.z,0,0,0,0,1);let t=(new r.Matrix4).makeTranslation(d.x,d.y,d.z);c.matrix4=c.matrix4.multiplyMatrices(t,c.matrix4),c.matrix=c.matrix4.matrix3FromTopLeft(),c.origin=new r.Vector3(0,0,0),c.unit=new r.Vector3(1,1,1)}i.modelData=[{cryst:c}],a=a.splice(6,h);for(var y=i[i.length-1].length,b=y+a.length,x=y;x{"use strict";i.r(e),i.d(e,{VASP:()=>n});var r=i(638),s=i(392);function n(t,e={}){var i=[[]],n={};const a=void 0===e.assignBonds||e.assignBonds;var o=t.replace(/^\s+/,"").split(/\r?\n/);if(o.length<3)return i;if(!o[1].match(/\d+/))return console.log("Warning: second line of the vasp structure file must be a number"),i;if(n.length=parseFloat(o[1]),n.length<0)return console.log("Warning: Vasp implementation for negative lattice lengths is not yet available"),i;n.xVec=new Float32Array(o[2].replace(/^\s+/,"").split(/\s+/)),n.yVec=new Float32Array(o[3].replace(/^\s+/,"").split(/\s+/)),n.zVec=new Float32Array(o[4].replace(/^\s+/,"").split(/\s+/));var l=new r.Matrix3(n.xVec[0],n.xVec[1],n.xVec[2],n.yVec[0],n.yVec[1],n.yVec[2],n.zVec[0],n.zVec[1],n.zVec[2]);l.multiplyScalar(n.length),i.modelData=[{symmetries:[],cryst:{matrix:l}}];var h=o[5].trim().split(/\s+/),c=new Int16Array(o[6].trim().split(/\s+/)),d=o[7].trim(),u=!1;if(d.match(/S/)&&(u=!0,d=o[8].trim()),"c"==d.toLowerCase()[0])d="cartesian";else{if("d"!=d.toLowerCase()[0])return console.log("Warning: Unknown vasp mode in POSCAR file: mode must be either C(artesian) or D(irect)"),i;d="direct"}if(h.length!=c.length)return console.log("Warning: declaration of atomary species wrong:"),console.log(h),console.log(c),i;u?o.splice(0,9):o.splice(0,8);for(var f=0,p=0,g=h.length;p{"use strict";i.r(e),i.d(e,{areConnected:()=>n});var r=i(40);const s=new Set(["Na","K","Ca","Mg","Mn","Sr"]);function n(t,e,i){if(i&&i.unboundCations&&(s.has(t.elem)||s.has(e.elem)))return!1;let n=(0,r.bondLength)(t.elem)+(0,r.bondLength)(e.elem);n+=.25,n*=n;let a=t.x-e.x;if(a*=a,a>n)return!1;let o=t.y-e.y;if(o*=o,o>n)return!1;let l=t.z-e.z;if(l*=l,l>n)return!1;const h=a+o+l;return!(isNaN(h)||h<.5||h>n||t.altLoc!==e.altLoc&&""!==t.altLoc.trim()&&""!==e.altLoc.trim())}},392:(t,e,i)=>{"use strict";i.r(e),i.d(e,{assignBonds:()=>a});var r=i(408);const s=[{x:0,y:0,z:1},{x:0,y:1,z:-1},{x:0,y:1,z:0},{x:0,y:1,z:1},{x:1,y:-1,z:-1},{x:1,y:-1,z:0},{x:1,y:-1,z:1},{x:1,y:0,z:-1},{x:1,y:0,z:0},{x:1,y:0,z:1},{x:1,y:1,z:-1},{x:1,y:1,z:0},{x:1,y:1,z:1}],n=4.95;function a(t,e){for(let e=0,i=t.length;e{"use strict";i.r(e),i.d(e,{bondLength:()=>s,bondTable:()=>r,setBondLength:()=>n});let r={H:.37,He:.32,Li:1.34,Be:.9,B:.82,C:.77,N:.75,O:.73,F:.71,Ne:.69,Na:1.54,Mg:1.3,Al:1.18,Si:1.11,P:1.06,S:1.02,Cl:.99,Ar:.97,K:1.96,Ca:1.74,Sc:1.44,Ti:1.56,V:1.25,Mn:1.39,Fe:1.25,Co:1.26,Ni:1.21,Cu:1.38,Zn:1.31,Ga:1.26,Ge:1.22,Se:1.16,Br:1.14,Kr:1.1,Rb:2.11,Sr:1.92,Y:1.62,Zr:1.48,Nb:1.37,Mo:1.45,Tc:1.56,Ru:1.26,Rh:1.35,Pd:1.31,Ag:1.53,Cd:1.48,In:1.44,Sn:1.41,Sb:1.38,Te:1.35,I:1.33,Xe:1.3,Cs:2.25,Ba:1.98,Lu:1.6,Hf:1.5,Ta:1.38,W:1.46,Re:1.59,Os:1.44,Ir:1.37,Pt:1.28,Au:1.44,Hg:1.49,Tl:1.48,Pb:1.47,Bi:1.46,Rn:1.45};function s(t){return r[t]||1.6}function n(t,e){e<0&&(e=0),r[t]=e}},864:(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{"use strict";__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{PausableTimer:()=>PausableTimer,adjustVolumeStyle:()=>adjustVolumeStyle,base64ToArray:()=>base64ToArray,deepCopy:()=>deepCopy,download:()=>download,extend:()=>extend,get:()=>get,getAtomProperty:()=>getAtomProperty,getColorFromStyle:()=>getColorFromStyle,getElement:()=>getElement,getExtent:()=>getExtent,getPropertyRange:()=>getPropertyRange,getbin:()=>getbin,inflateString:()=>inflateString,isEmptyObject:()=>isEmptyObject,isNumeric:()=>isNumeric,makeFunction:()=>makeFunction,mergeGeos:()=>mergeGeos,specStringToObject:()=>specStringToObject});var _Gradient__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(546),_VolumeData__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__(848),_colors__WEBPACK_IMPORTED_MODULE_2__=__webpack_require__(222),pako__WEBPACK_IMPORTED_MODULE_3__=__webpack_require__(75);function extend(t,e){for(var i in e)e.hasOwnProperty(i)&&void 0!==e[i]&&(t[i]=e[i]);return t}function deepCopy(t){let e,i,r;if(null==t)return{};if("object"!=typeof t||null===t)return t;for(r in e=Array.isArray(t)?[]:{},t)i=t[r],e[r]=deepCopy(i);return e}function isNumeric(t){var e=typeof t;return("number"===e||"string"===e)&&!isNaN(t-parseFloat(t))}function isEmptyObject(t){var e;for(e in t)return!1;return!0}function makeFunction(callback){return callback&&"string"==typeof callback&&(callback=eval("("+callback+")")),callback&&"function"!=typeof callback?(console.warn("Invalid callback provided."),()=>{}):callback}function adjustVolumeStyle(t){t&&(!t.volformat||t.voldata instanceof _VolumeData__WEBPACK_IMPORTED_MODULE_1__.VolumeData||(t.voldata=new _VolumeData__WEBPACK_IMPORTED_MODULE_1__.VolumeData(t.voldata,t.volformat)),t.volscheme&&(t.volscheme=_Gradient__WEBPACK_IMPORTED_MODULE_0__.Gradient.getGradient(t.volscheme)))}function getExtent(t,e){var i,r,s,n,a,o,l,h,c,d,u=!e;if(i=r=s=9999,n=a=o=-9999,l=h=c=d=0,0===t.length)return[[0,0,0],[0,0,0],[0,0,0]];for(var f=0;fp.x?n:p.x,a=a>p.y?a:p.y,o=o>p.z?o:p.z,p.symmetries&&u))for(var g=0;gp.symmetries[g].x?n:p.symmetries[g].x,a=a>p.symmetries[g].y?a:p.symmetries[g].y,o=o>p.symmetries[g].z?o:p.symmetries[g].z}return[[i,r,s],[n,a,o],[l/d,h/d,c/d]]}function getPropertyRange(t,e){for(var i=Number.POSITIVE_INFINITY,r=Number.NEGATIVE_INFINITY,s=0,n=t.length;sr&&(r=a))}return isFinite(i)||isFinite(r)?isFinite(i)?isFinite(r)||(r=i):i=r:i=r=0,[i,r]}class PausableTimer{constructor(t,e,i){this.total_time_run=0,this.fn=t,this.arg=i,this.countdown=e,this.start_time=(new Date).getTime(),this.ident=setTimeout(t,e,i)}cancel(){clearTimeout(this.ident)}pause(){clearTimeout(this.ident),this.total_time_run=(new Date).getTime()-this.start_time}resume(){this.ident=setTimeout(this.fn,Math.max(0,this.countdown-this.total_time_run),this.arg)}}function base64ToArray(t){for(var e=window.atob(t),i=e.length,r=new Uint8Array(i),s=0;s=0?parseFloat(t):parseInt(t):"true"===t||"false"!==t&&t},i={};if("all"===(t=t.replace(/%7E/g,"~")))return i;for(var r=t.split(";"),s=0;st.text()));return e?i.then(e):i}function getbin(t,e,i,r){var s;return s="POST"==i?fetch(t,{method:"POST",body:r}).then((t=>checkStatus(t))).then((t=>t.arrayBuffer())):fetch(t).then((t=>checkStatus(t))).then((t=>t.arrayBuffer())),e?s.then(e):s}function download(t,e,i,r){var s="",n="",a="",o=null,l=e.addModel();if(t.indexOf(":")<0&&(t=4==t.length?"pdb:"+t:isNaN(t)?"url:"+t:"cid:"+t),"mmtf:"==t.substring(0,5)&&(console.warn("WARNING: MMTF now deprecated. Reverting to bcif."),t="bcif:"+t.slice(5)),"bcif:"===t.substring(0,5))t=t.substring(5).toUpperCase(),a="https://models.rcsb.org/"+t+".bcif.gz",i&&void 0===i.noComputeSecondaryStructure&&(i.noComputeSecondaryStructure=!0),o=new Promise((function(t){getbin(a).then((function(r){l.addMolData(r,"bcif.gz",i),e.zoomTo(),e.render(),t(l)}),(function(){console.error("fetch of "+a+" failed.")}))}));else{if("pdb:"===t.substring(0,4)){if(s="bcif",i&&i.format&&(s=i.format),i&&void 0===i.noComputeSecondaryStructure&&(i.noComputeSecondaryStructure=!0),!(t=t.substring(4).toUpperCase()).match(/^[1-9][A-Za-z0-9]{3}$/))return void alert("Wrong PDB ID");"bcif"==s?a="https://models.rcsb.org/"+t.toUpperCase()+".bcif.gz":(n=i&&i.pdbUri?i.pdbUri:"https://files.rcsb.org/view/",a=n+t+"."+s)}else if("cid:"==t.substring(0,4)){if(s="sdf",!(t=t.substring(4)).match(/^[0-9]+$/))return void alert("Wrong Compound ID");a="https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/"+t+"/SDF?record_type=3d"}else"url:"==t.substring(0,4)&&(a=t.substring(4),s=a);var h=function(t){l.addMolData(t,s,i),e.zoomTo(),e.render()};o=new Promise((function(e){"bcif"==s?getbin(a).then((function(t){h(t),e(l)})).catch((function(){n=i&&i.pdbUri?i.pdbUri:"https://files.rcsb.org/view/",a=n+t+".pdb",s="pdb",console.warn("falling back to pdb format"),get(a).then((function(t){h(t),e(l)})).catch((function(t){h(""),e(l),console.error("fetch of "+a+" failed: "+t.statusText)}))})):get(a).then((function(t){h(t),e(l)})).catch((function(t){h(""),e(l),console.error("fetch of "+a+" failed: "+t.statusText)}))}))}return r?(o.then((function(t){r(t)})),l):o}function getColorFromStyle(t,e){let i=e.colorscheme;if(void 0!==_colors__WEBPACK_IMPORTED_MODULE_2__.builtinColorSchemes[i])i=_colors__WEBPACK_IMPORTED_MODULE_2__.builtinColorSchemes[i];else if("string"==typeof i&&i.endsWith("Carbon")){let t=i.substring(0,i.lastIndexOf("Carbon")).toLowerCase();if(void 0!==_colors__WEBPACK_IMPORTED_MODULE_2__.htmlColors[t]){let e=Object.assign({},_colors__WEBPACK_IMPORTED_MODULE_2__.elementColors.defaultColors);e.C=_colors__WEBPACK_IMPORTED_MODULE_2__.htmlColors[t],_colors__WEBPACK_IMPORTED_MODULE_2__.builtinColorSchemes[i]={prop:"elem",map:e},i=_colors__WEBPACK_IMPORTED_MODULE_2__.builtinColorSchemes[i]}}let r=t.color;if(void 0!==e.color&&"spectrum"!=e.color&&(r=e.color),void 0!==i){let n,a;if(void 0!==_colors__WEBPACK_IMPORTED_MODULE_2__.elementColors[i])i=_colors__WEBPACK_IMPORTED_MODULE_2__.elementColors[i],void 0!==i[t[i.prop]]&&(r=i.map[t[i.prop]]);else if(void 0!==i[t[i.prop]])r=i.map[t[i.prop]];else if(void 0!==i.prop&&void 0!==i.gradient){n=i.prop;var s=i.gradient;s instanceof _Gradient__WEBPACK_IMPORTED_MODULE_0__.GradientType||(s=(0,_Gradient__WEBPACK_IMPORTED_MODULE_0__.getGradient)(i));let e=s.range()||[-1,1];a=getAtomProperty(t,n),null!=a&&(r=s.valueToHex(a,e))}else void 0!==i.prop&&void 0!==i.map?(n=i.prop,a=getAtomProperty(t,n),void 0!==i.map[a]&&(r=i.map[a])):void 0!==e.colorscheme[t.elem]?r=e.colorscheme[t.elem]:console.warn("Could not interpret colorscheme "+i)}else void 0!==e.colorfunc&&(r=e.colorfunc(t));return _colors__WEBPACK_IMPORTED_MODULE_2__.CC.color(r)}function getElement(t){let e=t;return"string"==typeof t?e=document.querySelector("#"+t):"object"==typeof t&&t.get&&(e=t.get(0)),e}function inflateString(t,e=!0){let i;if("string"==typeof t){i=(new TextEncoder).encode(t)}else i=new Uint8Array(t);return(0,pako__WEBPACK_IMPORTED_MODULE_3__.inflate)(i,{to:e?"string":null})}},111:(t,e,i)=>{var r;r={},t.exports=r,function(t,e){t.toRGBA8=function(e){var i=e.width,r=e.height;if(null==e.tabs.acTL)return[t.toRGBA8.decodeImage(e.data,i,r,e).buffer];var s=[];null==e.frames[0].data&&(e.frames[0].data=e.data);for(var n,a=new Uint8Array(i*r*4),o=0;o>3)]>>7-(7&p)&1);l[M]=b[z],l[M+1]=b[z+1],l[M+2]=b[z+2],l[M+3]=T>2)]>>6-((3&p)<<1)&3),l[M]=b[z],l[M+1]=b[z+1],l[M+2]=b[z+2],l[M+3]=T>1)]>>4-((1&p)<<2)&15),l[M]=b[z],l[M+1]=b[z+1],l[M+2]=b[z+2],l[M+3]=T>3]>>7-(7&p)&1))==255*m?0:255;h[p]=F<<24|E<<16|E<<8|E}if(2==d)for(p=0;p>2]>>6-((3&p)<<1)&3))==85*m?0:255,h[p]=F<<24|E<<16|E<<8|E;if(4==d)for(p=0;p>1]>>4-((1&p)<<2)&15))==17*m?0:255,h[p]=F<<24|E<<16|E<<8|E;if(8==d)for(p=0;p>3,o=Math.ceil(r*n/8),l=new Uint8Array(s*o),h=0,c=[0,0,4,0,2,0,1],d=[0,4,0,2,0,1,0],u=[8,8,8,4,4,2,2],f=[8,8,4,4,2,2,1],p=0;p<7;){for(var g=u[p],m=f[p],v=0,_=0,y=c[p];y>3])>>7-(7&S)&1,l[A*o+(C>>3)]|=M<<7-(3&C)),2==n&&(M=(M=e[S>>3])>>6-(7&S)&3,l[A*o+(C>>2)]|=M<<6-((3&C)<<1)),4==n&&(M=(M=e[S>>3])>>4-(7&S)&15,l[A*o+(C>>1)]|=M<<4-((1&C)<<2)),n>=8)for(var z=A*o+C*a,T=0;T>3)+T];S+=n,C+=m}w++,A+=g}v*_!=0&&(h+=_*(1+x)),p+=1}return l},t.decode._getBPP=function(t){return[1,null,3,1,2,null,4][t.ctype]*t.depth},t.decode._filterZero=function(e,i,r,s,n){var a=t.decode._getBPP(i),o=Math.ceil(s*a/8),l=t.decode._paeth;a=Math.ceil(a/8);for(var h=0;h>1)&255;if(4==u)for(f=a;f>1)&255;for(f=a;f>1)&255}if(4==u){for(f=0;f>8&255,t[e+1]=255&i},readUint:function(t,e){return 16777216*t[e]+(t[e+1]<<16|t[e+2]<<8|t[e+3])},writeUint:function(t,e,i){t[e]=i>>24&255,t[e+1]=i>>16&255,t[e+2]=i>>8&255,t[e+3]=255&i},readASCII:function(t,e,i){for(var r="",s=0;s=0&&o>=0?(d=f*e+p<<2,u=(o+f)*s+a+p<<2):(d=(-o+f)*e-a+p<<2,u=f*s+p<<2),0==l)r[u]=t[d],r[u+1]=t[d+1],r[u+2]=t[d+2],r[u+3]=t[d+3];else if(1==l){var g=t[d+3]*(1/255),m=t[d]*g,v=t[d+1]*g,_=t[d+2]*g,y=r[u+3]*(1/255),b=r[u]*y,x=r[u+1]*y,w=r[u+2]*y,A=1-g,C=g+y*A,S=0==C?0:1/C;r[u+3]=255*C,r[u+0]=(m+b*A)*S,r[u+1]=(v+x*A)*S,r[u+2]=(_+w*A)*S}else if(2==l)g=t[d+3],m=t[d],v=t[d+1],_=t[d+2],y=r[u+3],b=r[u],x=r[u+1],w=r[u+2],g==y&&m==b&&v==x&&_==w?(r[u]=0,r[u+1]=0,r[u+2]=0,r[u+3]=0):(r[u]=m,r[u+1]=v,r[u+2]=_,r[u+3]=g);else if(3==l){if(g=t[d+3],m=t[d],v=t[d+1],_=t[d+2],y=r[u+3],b=r[u],x=r[u+1],w=r[u+2],g==y&&m==b&&v==x&&_==w)continue;if(g<220&&y>20)return!1}return!0},t.encode=function(e,i,r,s,n,a){null==s&&(s=0),null==a&&(a=!1);for(var o=new Uint8Array(e[0].byteLength*e.length+100),l=[137,80,78,71,13,10,26,10],h=0;h<8;h++)o[h]=l[h];var c=8,d=t._bin,u=t.crc.crc,f=d.writeUint,p=d.writeUshort,g=d.writeASCII,m=t.encode.compressPNG(e,i,r,s,a);f(o,c,13),g(o,c+=4,"IHDR"),f(o,c+=4,i),f(o,c+=4,r),o[c+=4]=m.depth,o[++c]=m.ctype,o[++c]=0,o[++c]=0,o[++c]=0,f(o,++c,u(o,c-17,17)),f(o,c+=4,1),g(o,c+=4,"sRGB"),o[c+=4]=1,f(o,++c,u(o,c-5,5)),c+=4;var v=e.length>1;if(v&&(f(o,c,8),g(o,c+=4,"acTL"),f(o,c+=4,e.length),f(o,c+=4,0),f(o,c+=4,u(o,c-12,12)),c+=4),3==m.ctype){for(f(o,c,3*(M=m.plte.length)),g(o,c+=4,"PLTE"),c+=4,h=0;h>8&255,w=y>>16&255;o[c+_+0]=b,o[c+_+1]=x,o[c+_+2]=w}if(f(o,c+=3*M,u(o,c-3*M-4,3*M+4)),c+=4,m.gotAlpha){for(f(o,c,M),g(o,c+=4,"tRNS"),c+=4,h=0;h>24&255;f(o,c+=M,u(o,c-M-4,M+4)),c+=4}}for(var A=0,C=0;C=300))break}}var x=!!g&&n,w=v.length;w<=256&&0==a&&(l=w<=2?1:w<=4?2:w<=16?4:8,n&&(l=8),g=!0);var A=[];for(d=0;dB&&(B=V),GN&&(N=G));var j=-1==B?1:(B-P+1)*(N-U+1);j>1)]|=m[S[q+V]]<<4-4*(1&V);else if(2==l)for(V=0;V>2)]|=m[S[q+V]]<<6-2*(3&V);else if(1==l)for(V=0;V>3)]|=m[S[q+V]]<<7-1*(7&V)}C=H,o=3,h=1}else if(0==g&&1==e.length){H=new Uint8Array(T*E*3);var Y=T*E;for(p=0;p5e5)||2!=l&&3!=l&&4!=l){for(var h=0;h>1)+256&255;if(4==a)for(c=n;c>1)&255;for(c=n;c>1)&255}if(4==a){for(c=0;c>>1:i>>>=1;t[e]=i}return t}(),update:function(e,i,r,s){for(var n=0;n>>8;return e},crc:function(e,i,r){return 4294967295^t.crc.update(4294967295,e,i,r)}},t.quantize=function(e,i,r){for(var s=[],n=0,a=0;ag&&(g=p[a].est.L,m=a);if(g<.001)break;var v=p[m],_=t.quantize.splitPixels(o,l,v.i0,v.i1,v.est.e,v.est.eMq255),y={i0:v.i0,i1:_,bst:null,est:null,tdst:0,left:null,right:null};y.bst=t.quantize.stats(o,y.i0,y.i1),y.est=t.quantize.estats(y.bst);var b={i0:_,i1:v.i1,bst:null,est:null,tdst:0,left:null,right:null};for(b.bst={R:[],m:[],N:v.bst.N-y.bst.N},a=0;a<16;a++)b.bst.R[a]=v.bst.R[a]-y.bst.R[a];for(a=0;a<4;a++)b.bst.m[a]=v.bst.m[a]-y.bst.m[a];b.est=t.quantize.estats(b.bst),v.left=y,v.right=b,p[m]=y,p.push(b)}p.sort((function(t,e){return e.bst.N-t.bst.N}));for(var x=0;x>2]=L.est.rgba}s[x]=C.buffer}return{bufs:s,plte:p}},t.quantize.getNearest=function(e,i,r,s,n){if(null==e.left)return e.tdst=t.quantize.dist(e.est.q,i,r,s,n),e;var a=t.quantize.planeDst(e.est,i,r,s,n),o=e.left,l=e.right;a>0&&(o=e.right,l=e.left);var h=t.quantize.getNearest(o,i,r,s,n);if(h.tdst<=a*a)return h;var c=t.quantize.getNearest(l,i,r,s,n);return c.tdsta;)s-=4;if(r>=s)break;var l=i[r>>2];i[r>>2]=i[s>>2],i[s>>2]=l,r+=4,s-=4}for(;o(e,r,n)>a;)r-=4;return r+4},t.quantize.vecDot=function(t,e,i){return t[e]*i[0]+t[e+1]*i[1]+t[e+2]*i[2]+t[e+3]*i[3]},t.quantize.stats=function(t,e,i){for(var r=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],s=[0,0,0,0],n=i-e>>2,a=e;a>>0}},t.M4={multVec:function(t,e){return[t[0]*e[0]+t[1]*e[1]+t[2]*e[2]+t[3]*e[3],t[4]*e[0]+t[5]*e[1]+t[6]*e[2]+t[7]*e[3],t[8]*e[0]+t[9]*e[1]+t[10]*e[2]+t[11]*e[3],t[12]*e[0]+t[13]*e[1]+t[14]*e[2]+t[15]*e[3]]},dot:function(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]+t[3]*e[3]},sml:function(t,e){return[t*e[0],t*e[1],t*e[2],t*e[3]]}},t.encode.alphaMul=function(t,e){for(var i=new Uint8Array(t.length),r=t.length>>2,s=0;s{"use strict";var r={};(0,i(981).assign)(r,i(71),i(3),i(681)),t.exports=r},71:(t,e,i)=>{"use strict";var r=i(107),s=i(981),n=i(972),a=i(834),o=i(746),l=Object.prototype.toString;function h(t){if(!(this instanceof h))return new h(t);this.options=s.assign({level:-1,method:8,chunkSize:16384,windowBits:15,memLevel:8,strategy:0,to:""},t||{});var e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new o,this.strm.avail_out=0;var i=r.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(0!==i)throw new Error(a[i]);if(e.header&&r.deflateSetHeader(this.strm,e.header),e.dictionary){var c;if(c="string"==typeof e.dictionary?n.string2buf(e.dictionary):"[object ArrayBuffer]"===l.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,0!==(i=r.deflateSetDictionary(this.strm,c)))throw new Error(a[i]);this._dict_set=!0}}function c(t,e){var i=new h(e);if(i.push(t,!0),i.err)throw i.msg||a[i.err];return i.result}h.prototype.push=function(t,e){var i,a,o=this.strm,h=this.options.chunkSize;if(this.ended)return!1;a=e===~~e?e:!0===e?4:0,"string"==typeof t?o.input=n.string2buf(t):"[object ArrayBuffer]"===l.call(t)?o.input=new Uint8Array(t):o.input=t,o.next_in=0,o.avail_in=o.input.length;do{if(0===o.avail_out&&(o.output=new s.Buf8(h),o.next_out=0,o.avail_out=h),1!==(i=r.deflate(o,a))&&0!==i)return this.onEnd(i),this.ended=!0,!1;0!==o.avail_out&&(0!==o.avail_in||4!==a&&2!==a)||("string"===this.options.to?this.onData(n.buf2binstring(s.shrinkBuf(o.output,o.next_out))):this.onData(s.shrinkBuf(o.output,o.next_out)))}while((o.avail_in>0||0===o.avail_out)&&1!==i);return 4===a?(i=r.deflateEnd(this.strm),this.onEnd(i),this.ended=!0,0===i):2!==a||(this.onEnd(0),o.avail_out=0,!0)},h.prototype.onData=function(t){this.chunks.push(t)},h.prototype.onEnd=function(t){0===t&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=s.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},e.Deflate=h,e.deflate=c,e.deflateRaw=function(t,e){return(e=e||{}).raw=!0,c(t,e)},e.gzip=function(t,e){return(e=e||{}).gzip=!0,c(t,e)}},3:(t,e,i)=>{"use strict";var r=i(663),s=i(981),n=i(972),a=i(681),o=i(834),l=i(746),h=i(670),c=Object.prototype.toString;function d(t){if(!(this instanceof d))return new d(t);this.options=s.assign({chunkSize:16384,windowBits:0,to:""},t||{});var e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&(15&e.windowBits||(e.windowBits|=15)),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new l,this.strm.avail_out=0;var i=r.inflateInit2(this.strm,e.windowBits);if(i!==a.Z_OK)throw new Error(o[i]);if(this.header=new h,r.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=n.string2buf(e.dictionary):"[object ArrayBuffer]"===c.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(i=r.inflateSetDictionary(this.strm,e.dictionary))!==a.Z_OK))throw new Error(o[i])}function u(t,e){var i=new d(e);if(i.push(t,!0),i.err)throw i.msg||o[i.err];return i.result}d.prototype.push=function(t,e){var i,o,l,h,d,u=this.strm,f=this.options.chunkSize,p=this.options.dictionary,g=!1;if(this.ended)return!1;o=e===~~e?e:!0===e?a.Z_FINISH:a.Z_NO_FLUSH,"string"==typeof t?u.input=n.binstring2buf(t):"[object ArrayBuffer]"===c.call(t)?u.input=new Uint8Array(t):u.input=t,u.next_in=0,u.avail_in=u.input.length;do{if(0===u.avail_out&&(u.output=new s.Buf8(f),u.next_out=0,u.avail_out=f),(i=r.inflate(u,a.Z_NO_FLUSH))===a.Z_NEED_DICT&&p&&(i=r.inflateSetDictionary(this.strm,p)),i===a.Z_BUF_ERROR&&!0===g&&(i=a.Z_OK,g=!1),i!==a.Z_STREAM_END&&i!==a.Z_OK)return this.onEnd(i),this.ended=!0,!1;u.next_out&&(0!==u.avail_out&&i!==a.Z_STREAM_END&&(0!==u.avail_in||o!==a.Z_FINISH&&o!==a.Z_SYNC_FLUSH)||("string"===this.options.to?(l=n.utf8border(u.output,u.next_out),h=u.next_out-l,d=n.buf2string(u.output,l),u.next_out=h,u.avail_out=f-h,h&&s.arraySet(u.output,u.output,l,h,0),this.onData(d)):this.onData(s.shrinkBuf(u.output,u.next_out)))),0===u.avail_in&&0===u.avail_out&&(g=!0)}while((u.avail_in>0||0===u.avail_out)&&i!==a.Z_STREAM_END);return i===a.Z_STREAM_END&&(o=a.Z_FINISH),o===a.Z_FINISH?(i=r.inflateEnd(this.strm),this.onEnd(i),this.ended=!0,i===a.Z_OK):o!==a.Z_SYNC_FLUSH||(this.onEnd(a.Z_OK),u.avail_out=0,!0)},d.prototype.onData=function(t){this.chunks.push(t)},d.prototype.onEnd=function(t){t===a.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=s.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},e.Inflate=d,e.inflate=u,e.inflateRaw=function(t,e){return(e=e||{}).raw=!0,u(t,e)},e.ungzip=u},981:(t,e)=>{"use strict";var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;function r(t,e){return Object.prototype.hasOwnProperty.call(t,e)}e.assign=function(t){for(var e=Array.prototype.slice.call(arguments,1);e.length;){var i=e.shift();if(i){if("object"!=typeof i)throw new TypeError(i+"must be non-object");for(var s in i)r(i,s)&&(t[s]=i[s])}}return t},e.shrinkBuf=function(t,e){return t.length===e?t:t.subarray?t.subarray(0,e):(t.length=e,t)};var s={arraySet:function(t,e,i,r,s){if(e.subarray&&t.subarray)t.set(e.subarray(i,i+r),s);else for(var n=0;n{"use strict";var r=i(981),s=!0,n=!0;try{String.fromCharCode.apply(null,[0])}catch(t){s=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(t){n=!1}for(var a=new r.Buf8(256),o=0;o<256;o++)a[o]=o>=252?6:o>=248?5:o>=240?4:o>=224?3:o>=192?2:1;function l(t,e){if(e<65534&&(t.subarray&&n||!t.subarray&&s))return String.fromCharCode.apply(null,r.shrinkBuf(t,e));for(var i="",a=0;a>>6,e[a++]=128|63&i):i<65536?(e[a++]=224|i>>>12,e[a++]=128|i>>>6&63,e[a++]=128|63&i):(e[a++]=240|i>>>18,e[a++]=128|i>>>12&63,e[a++]=128|i>>>6&63,e[a++]=128|63&i);return e},e.buf2binstring=function(t){return l(t,t.length)},e.binstring2buf=function(t){for(var e=new r.Buf8(t.length),i=0,s=e.length;i4)h[r++]=65533,i+=n-1;else{for(s&=2===n?31:3===n?15:7;n>1&&i1?h[r++]=65533:s<65536?h[r++]=s:(s-=65536,h[r++]=55296|s>>10&1023,h[r++]=56320|1023&s)}return l(h,r)},e.utf8border=function(t,e){var i;for((e=e||t.length)>t.length&&(e=t.length),i=e-1;i>=0&&128==(192&t[i]);)i--;return i<0||0===i?e:i+a[t[i]]>e?i:e}},701:t=>{"use strict";t.exports=function(t,e,i,r){for(var s=65535&t,n=t>>>16&65535,a=0;0!==i;){i-=a=i>2e3?2e3:i;do{n=n+(s=s+e[r++]|0)|0}while(--a);s%=65521,n%=65521}return s|n<<16}},681:t=>{"use strict";t.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},407:t=>{"use strict";var e=function(){for(var t,e=[],i=0;i<256;i++){t=i;for(var r=0;r<8;r++)t=1&t?3988292384^t>>>1:t>>>1;e[i]=t}return e}();t.exports=function(t,i,r,s){var n=e,a=s+r;t^=-1;for(var o=s;o>>8^n[255&(t^i[o])];return~t}},107:(t,e,i)=>{"use strict";var r,s=i(981),n=i(697),a=i(701),o=i(407),l=i(834),h=-2,c=258,d=262,u=103,f=113,p=666;function g(t,e){return t.msg=l[e],e}function m(t){return(t<<1)-(t>4?9:0)}function v(t){for(var e=t.length;--e>=0;)t[e]=0}function _(t){var e=t.state,i=e.pending;i>t.avail_out&&(i=t.avail_out),0!==i&&(s.arraySet(t.output,e.pending_buf,e.pending_out,i,t.next_out),t.next_out+=i,e.pending_out+=i,t.total_out+=i,t.avail_out-=i,e.pending-=i,0===e.pending&&(e.pending_out=0))}function y(t,e){n._tr_flush_block(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,_(t.strm)}function b(t,e){t.pending_buf[t.pending++]=e}function x(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function w(t,e){var i,r,s=t.max_chain_length,n=t.strstart,a=t.prev_length,o=t.nice_match,l=t.strstart>t.w_size-d?t.strstart-(t.w_size-d):0,h=t.window,u=t.w_mask,f=t.prev,p=t.strstart+c,g=h[n+a-1],m=h[n+a];t.prev_length>=t.good_match&&(s>>=2),o>t.lookahead&&(o=t.lookahead);do{if(h[(i=e)+a]===m&&h[i+a-1]===g&&h[i]===h[n]&&h[++i]===h[n+1]){n+=2,i++;do{}while(h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&na){if(t.match_start=e,a=r,r>=o)break;g=h[n+a-1],m=h[n+a]}}}while((e=f[e&u])>l&&0!=--s);return a<=t.lookahead?a:t.lookahead}function A(t){var e,i,r,n,l,h,c,u,f,p,g=t.w_size;do{if(n=t.window_size-t.lookahead-t.strstart,t.strstart>=g+(g-d)){s.arraySet(t.window,t.window,g,g,0),t.match_start-=g,t.strstart-=g,t.block_start-=g,e=i=t.hash_size;do{r=t.head[--e],t.head[e]=r>=g?r-g:0}while(--i);e=i=g;do{r=t.prev[--e],t.prev[e]=r>=g?r-g:0}while(--i);n+=g}if(0===t.strm.avail_in)break;if(h=t.strm,c=t.window,u=t.strstart+t.lookahead,f=n,p=void 0,(p=h.avail_in)>f&&(p=f),i=0===p?0:(h.avail_in-=p,s.arraySet(c,h.input,h.next_in,p,u),1===h.state.wrap?h.adler=a(h.adler,c,p,u):2===h.state.wrap&&(h.adler=o(h.adler,c,p,u)),h.next_in+=p,h.total_in+=p,p),t.lookahead+=i,t.lookahead+t.insert>=3)for(l=t.strstart-t.insert,t.ins_h=t.window[l],t.ins_h=(t.ins_h<=3&&(t.ins_h=(t.ins_h<=3)if(r=n._tr_tally(t,t.strstart-t.match_start,t.match_length-3),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=3){t.match_length--;do{t.strstart++,t.ins_h=(t.ins_h<=3&&(t.ins_h=(t.ins_h<4096)&&(t.match_length=2)),t.prev_length>=3&&t.match_length<=t.prev_length){s=t.strstart+t.lookahead-3,r=n._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-3),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=s&&(t.ins_h=(t.ins_h<15&&(o=2,r-=16),n<1||n>9||8!==i||r<8||r>15||e<0||e>9||a<0||a>4)return g(t,h);8===r&&(r=9);var l=new z;return t.state=l,l.strm=t,l.wrap=o,l.gzhead=null,l.w_bits=r,l.w_size=1<t.pending_buf_size-5&&(i=t.pending_buf_size-5);;){if(t.lookahead<=1){if(A(t),0===t.lookahead&&0===e)return 1;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var r=t.block_start+i;if((0===t.strstart||t.strstart>=r)&&(t.lookahead=t.strstart-r,t.strstart=r,y(t,!1),0===t.strm.avail_out))return 1;if(t.strstart-t.block_start>=t.w_size-d&&(y(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,4===e?(y(t,!0),0===t.strm.avail_out?3:4):(t.strstart>t.block_start&&(y(t,!1),t.strm.avail_out),1)})),new M(4,4,8,4,C),new M(4,5,16,8,C),new M(4,6,32,32,C),new M(4,4,16,16,S),new M(8,16,32,32,S),new M(8,16,128,128,S),new M(8,32,128,256,S),new M(32,128,258,1024,S),new M(32,258,258,4096,S)],e.deflateInit=function(t,e){return L(t,e,8,15,8,0)},e.deflateInit2=L,e.deflateReset=E,e.deflateResetKeep=T,e.deflateSetHeader=function(t,e){return t&&t.state?2!==t.state.wrap?h:(t.state.gzhead=e,0):h},e.deflate=function(t,e){var i,s,a,l;if(!t||!t.state||e>5||e<0)return t?g(t,h):h;if(s=t.state,!t.output||!t.input&&0!==t.avail_in||s.status===p&&4!==e)return g(t,0===t.avail_out?-5:h);if(s.strm=t,i=s.last_flush,s.last_flush=e,42===s.status)if(2===s.wrap)t.adler=0,b(s,31),b(s,139),b(s,8),s.gzhead?(b(s,(s.gzhead.text?1:0)+(s.gzhead.hcrc?2:0)+(s.gzhead.extra?4:0)+(s.gzhead.name?8:0)+(s.gzhead.comment?16:0)),b(s,255&s.gzhead.time),b(s,s.gzhead.time>>8&255),b(s,s.gzhead.time>>16&255),b(s,s.gzhead.time>>24&255),b(s,9===s.level?2:s.strategy>=2||s.level<2?4:0),b(s,255&s.gzhead.os),s.gzhead.extra&&s.gzhead.extra.length&&(b(s,255&s.gzhead.extra.length),b(s,s.gzhead.extra.length>>8&255)),s.gzhead.hcrc&&(t.adler=o(t.adler,s.pending_buf,s.pending,0)),s.gzindex=0,s.status=69):(b(s,0),b(s,0),b(s,0),b(s,0),b(s,0),b(s,9===s.level?2:s.strategy>=2||s.level<2?4:0),b(s,3),s.status=f);else{var d=8+(s.w_bits-8<<4)<<8;d|=(s.strategy>=2||s.level<2?0:s.level<6?1:6===s.level?2:3)<<6,0!==s.strstart&&(d|=32),d+=31-d%31,s.status=f,x(s,d),0!==s.strstart&&(x(s,t.adler>>>16),x(s,65535&t.adler)),t.adler=1}if(69===s.status)if(s.gzhead.extra){for(a=s.pending;s.gzindex<(65535&s.gzhead.extra.length)&&(s.pending!==s.pending_buf_size||(s.gzhead.hcrc&&s.pending>a&&(t.adler=o(t.adler,s.pending_buf,s.pending-a,a)),_(t),a=s.pending,s.pending!==s.pending_buf_size));)b(s,255&s.gzhead.extra[s.gzindex]),s.gzindex++;s.gzhead.hcrc&&s.pending>a&&(t.adler=o(t.adler,s.pending_buf,s.pending-a,a)),s.gzindex===s.gzhead.extra.length&&(s.gzindex=0,s.status=73)}else s.status=73;if(73===s.status)if(s.gzhead.name){a=s.pending;do{if(s.pending===s.pending_buf_size&&(s.gzhead.hcrc&&s.pending>a&&(t.adler=o(t.adler,s.pending_buf,s.pending-a,a)),_(t),a=s.pending,s.pending===s.pending_buf_size)){l=1;break}l=s.gzindexa&&(t.adler=o(t.adler,s.pending_buf,s.pending-a,a)),0===l&&(s.gzindex=0,s.status=91)}else s.status=91;if(91===s.status)if(s.gzhead.comment){a=s.pending;do{if(s.pending===s.pending_buf_size&&(s.gzhead.hcrc&&s.pending>a&&(t.adler=o(t.adler,s.pending_buf,s.pending-a,a)),_(t),a=s.pending,s.pending===s.pending_buf_size)){l=1;break}l=s.gzindexa&&(t.adler=o(t.adler,s.pending_buf,s.pending-a,a)),0===l&&(s.status=u)}else s.status=u;if(s.status===u&&(s.gzhead.hcrc?(s.pending+2>s.pending_buf_size&&_(t),s.pending+2<=s.pending_buf_size&&(b(s,255&t.adler),b(s,t.adler>>8&255),t.adler=0,s.status=f)):s.status=f),0!==s.pending){if(_(t),0===t.avail_out)return s.last_flush=-1,0}else if(0===t.avail_in&&m(e)<=m(i)&&4!==e)return g(t,-5);if(s.status===p&&0!==t.avail_in)return g(t,-5);if(0!==t.avail_in||0!==s.lookahead||0!==e&&s.status!==p){var w=2===s.strategy?function(t,e){for(var i;;){if(0===t.lookahead&&(A(t),0===t.lookahead)){if(0===e)return 1;break}if(t.match_length=0,i=n._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,i&&(y(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,4===e?(y(t,!0),0===t.strm.avail_out?3:4):t.last_lit&&(y(t,!1),0===t.strm.avail_out)?1:2}(s,e):3===s.strategy?function(t,e){for(var i,r,s,a,o=t.window;;){if(t.lookahead<=c){if(A(t),t.lookahead<=c&&0===e)return 1;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=3&&t.strstart>0&&(r=o[s=t.strstart-1])===o[++s]&&r===o[++s]&&r===o[++s]){a=t.strstart+c;do{}while(r===o[++s]&&r===o[++s]&&r===o[++s]&&r===o[++s]&&r===o[++s]&&r===o[++s]&&r===o[++s]&&r===o[++s]&&st.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=3?(i=n._tr_tally(t,1,t.match_length-3),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(i=n._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),i&&(y(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,4===e?(y(t,!0),0===t.strm.avail_out?3:4):t.last_lit&&(y(t,!1),0===t.strm.avail_out)?1:2}(s,e):r[s.level].func(s,e);if(3!==w&&4!==w||(s.status=p),1===w||3===w)return 0===t.avail_out&&(s.last_flush=-1),0;if(2===w&&(1===e?n._tr_align(s):5!==e&&(n._tr_stored_block(s,0,0,!1),3===e&&(v(s.head),0===s.lookahead&&(s.strstart=0,s.block_start=0,s.insert=0))),_(t),0===t.avail_out))return s.last_flush=-1,0}return 4!==e?0:s.wrap<=0?1:(2===s.wrap?(b(s,255&t.adler),b(s,t.adler>>8&255),b(s,t.adler>>16&255),b(s,t.adler>>24&255),b(s,255&t.total_in),b(s,t.total_in>>8&255),b(s,t.total_in>>16&255),b(s,t.total_in>>24&255)):(x(s,t.adler>>>16),x(s,65535&t.adler)),_(t),s.wrap>0&&(s.wrap=-s.wrap),0!==s.pending?0:1)},e.deflateEnd=function(t){var e;return t&&t.state?42!==(e=t.state.status)&&69!==e&&73!==e&&91!==e&&e!==u&&e!==f&&e!==p?g(t,h):(t.state=null,e===f?g(t,-3):0):h},e.deflateSetDictionary=function(t,e){var i,r,n,o,l,c,d,u,f=e.length;if(!t||!t.state)return h;if(2===(o=(i=t.state).wrap)||1===o&&42!==i.status||i.lookahead)return h;for(1===o&&(t.adler=a(t.adler,e,f,0)),i.wrap=0,f>=i.w_size&&(0===o&&(v(i.head),i.strstart=0,i.block_start=0,i.insert=0),u=new s.Buf8(i.w_size),s.arraySet(u,e,f-i.w_size,i.w_size,0),e=u,f=i.w_size),l=t.avail_in,c=t.next_in,d=t.input,t.avail_in=f,t.next_in=0,t.input=e,A(i);i.lookahead>=3;){r=i.strstart,n=i.lookahead-2;do{i.ins_h=(i.ins_h<{"use strict";t.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},165:t=>{"use strict";t.exports=function(t,e){var i,r,s,n,a,o,l,h,c,d,u,f,p,g,m,v,_,y,b,x,w,A,C,S,M;i=t.state,r=t.next_in,S=t.input,s=r+(t.avail_in-5),n=t.next_out,M=t.output,a=n-(e-t.avail_out),o=n+(t.avail_out-257),l=i.dmax,h=i.wsize,c=i.whave,d=i.wnext,u=i.window,f=i.hold,p=i.bits,g=i.lencode,m=i.distcode,v=(1<>>=b=y>>>24,p-=b,0===(b=y>>>16&255))M[n++]=65535&y;else{if(!(16&b)){if(64&b){if(32&b){i.mode=12;break t}t.msg="invalid literal/length code",i.mode=30;break t}y=g[(65535&y)+(f&(1<>>=b,p-=b),p<15&&(f+=S[r++]<>>=b=y>>>24,p-=b,16&(b=y>>>16&255)){if(w=65535&y,p<(b&=15)&&(f+=S[r++]<l){t.msg="invalid distance too far back",i.mode=30;break t}if(f>>>=b,p-=b,w>(b=n-a)){if((b=w-b)>c&&i.sane){t.msg="invalid distance too far back",i.mode=30;break t}if(A=0,C=u,0===d){if(A+=h-b,b2;)M[n++]=C[A++],M[n++]=C[A++],M[n++]=C[A++],x-=3;x&&(M[n++]=C[A++],x>1&&(M[n++]=C[A++]))}else{A=n-w;do{M[n++]=M[A++],M[n++]=M[A++],M[n++]=M[A++],x-=3}while(x>2);x&&(M[n++]=M[A++],x>1&&(M[n++]=M[A++]))}break}if(64&b){t.msg="invalid distance code",i.mode=30;break t}y=m[(65535&y)+(f&(1<>3,f&=(1<<(p-=x<<3))-1,t.next_in=r,t.next_out=n,t.avail_in=r{"use strict";var r=i(981),s=i(701),n=i(407),a=i(165),o=i(358),l=-2,h=12,c=30;function d(t){return(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function u(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new r.Buf16(320),this.work=new r.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function f(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=1,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new r.Buf32(852),e.distcode=e.distdyn=new r.Buf32(592),e.sane=1,e.back=-1,0):l}function p(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,f(t)):l}function g(t,e){var i,r;return t&&t.state?(r=t.state,e<0?(i=0,e=-e):(i=1+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?l:(null!==r.window&&r.wbits!==e&&(r.window=null),r.wrap=i,r.wbits=e,p(t))):l}function m(t,e){var i,r;return t?(r=new u,t.state=r,r.window=null,0!==(i=g(t,e))&&(t.state=null),i):l}var v,_,y=!0;function b(t){if(y){var e;for(v=new r.Buf32(512),_=new r.Buf32(32),e=0;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(o(1,t.lens,0,288,v,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;o(2,t.lens,0,32,_,0,t.work,{bits:5}),y=!1}t.lencode=v,t.lenbits=9,t.distcode=_,t.distbits=5}function x(t,e,i,s){var n,a=t.state;return null===a.window&&(a.wsize=1<=a.wsize?(r.arraySet(a.window,e,i-a.wsize,a.wsize,0),a.wnext=0,a.whave=a.wsize):((n=a.wsize-a.wnext)>s&&(n=s),r.arraySet(a.window,e,i-s,n,a.wnext),(s-=n)?(r.arraySet(a.window,e,i-s,s,0),a.wnext=s,a.whave=a.wsize):(a.wnext+=n,a.wnext===a.wsize&&(a.wnext=0),a.whave>>8&255,i.check=n(i.check,U,2,0),_=0,y=0,i.mode=2;break}if(i.flags=0,i.head&&(i.head.done=!1),!(1&i.wrap)||(((255&_)<<8)+(_>>8))%31){t.msg="incorrect header check",i.mode=c;break}if(8!=(15&_)){t.msg="unknown compression method",i.mode=c;break}if(y-=4,O=8+(15&(_>>>=4)),0===i.wbits)i.wbits=O;else if(O>i.wbits){t.msg="invalid window size",i.mode=c;break}i.dmax=1<>8&1),512&i.flags&&(U[0]=255&_,U[1]=_>>>8&255,i.check=n(i.check,U,2,0)),_=0,y=0,i.mode=3;case 3:for(;y<32;){if(0===m)break t;m--,_+=u[p++]<>>8&255,U[2]=_>>>16&255,U[3]=_>>>24&255,i.check=n(i.check,U,4,0)),_=0,y=0,i.mode=4;case 4:for(;y<16;){if(0===m)break t;m--,_+=u[p++]<>8),512&i.flags&&(U[0]=255&_,U[1]=_>>>8&255,i.check=n(i.check,U,2,0)),_=0,y=0,i.mode=5;case 5:if(1024&i.flags){for(;y<16;){if(0===m)break t;m--,_+=u[p++]<>>8&255,i.check=n(i.check,U,2,0)),_=0,y=0}else i.head&&(i.head.extra=null);i.mode=6;case 6:if(1024&i.flags&&((C=i.length)>m&&(C=m),C&&(i.head&&(O=i.head.extra_len-i.length,i.head.extra||(i.head.extra=new Array(i.head.extra_len)),r.arraySet(i.head.extra,u,p,C,O)),512&i.flags&&(i.check=n(i.check,u,C,p)),m-=C,p+=C,i.length-=C),i.length))break t;i.length=0,i.mode=7;case 7:if(2048&i.flags){if(0===m)break t;C=0;do{O=u[p+C++],i.head&&O&&i.length<65536&&(i.head.name+=String.fromCharCode(O))}while(O&&C>9&1,i.head.done=!0),t.adler=i.check=0,i.mode=h;break;case 10:for(;y<32;){if(0===m)break t;m--,_+=u[p++]<>>=7&y,y-=7&y,i.mode=27;break}for(;y<3;){if(0===m)break t;m--,_+=u[p++]<>>=1)){case 0:i.mode=14;break;case 1:if(b(i),i.mode=20,6===e){_>>>=2,y-=2;break t}break;case 2:i.mode=17;break;case 3:t.msg="invalid block type",i.mode=c}_>>>=2,y-=2;break;case 14:for(_>>>=7&y,y-=7&y;y<32;){if(0===m)break t;m--,_+=u[p++]<>>16^65535)){t.msg="invalid stored block lengths",i.mode=c;break}if(i.length=65535&_,_=0,y=0,i.mode=15,6===e)break t;case 15:i.mode=16;case 16:if(C=i.length){if(C>m&&(C=m),C>v&&(C=v),0===C)break t;r.arraySet(f,u,p,C,g),m-=C,p+=C,v-=C,g+=C,i.length-=C;break}i.mode=h;break;case 17:for(;y<14;){if(0===m)break t;m--,_+=u[p++]<>>=5,y-=5,i.ndist=1+(31&_),_>>>=5,y-=5,i.ncode=4+(15&_),_>>>=4,y-=4,i.nlen>286||i.ndist>30){t.msg="too many length or distance symbols",i.mode=c;break}i.have=0,i.mode=18;case 18:for(;i.have>>=3,y-=3}for(;i.have<19;)i.lens[B[i.have++]]=0;if(i.lencode=i.lendyn,i.lenbits=7,k={bits:i.lenbits},D=o(0,i.lens,0,19,i.lencode,0,i.work,k),i.lenbits=k.bits,D){t.msg="invalid code lengths set",i.mode=c;break}i.have=0,i.mode=19;case 19:for(;i.have>>16&255,E=65535&P,!((z=P>>>24)<=y);){if(0===m)break t;m--,_+=u[p++]<>>=z,y-=z,i.lens[i.have++]=E;else{if(16===E){for(R=z+2;y>>=z,y-=z,0===i.have){t.msg="invalid bit length repeat",i.mode=c;break}O=i.lens[i.have-1],C=3+(3&_),_>>>=2,y-=2}else if(17===E){for(R=z+3;y>>=z)),_>>>=3,y-=3}else{for(R=z+7;y>>=z)),_>>>=7,y-=7}if(i.have+C>i.nlen+i.ndist){t.msg="invalid bit length repeat",i.mode=c;break}for(;C--;)i.lens[i.have++]=O}}if(i.mode===c)break;if(0===i.lens[256]){t.msg="invalid code -- missing end-of-block",i.mode=c;break}if(i.lenbits=9,k={bits:i.lenbits},D=o(1,i.lens,0,i.nlen,i.lencode,0,i.work,k),i.lenbits=k.bits,D){t.msg="invalid literal/lengths set",i.mode=c;break}if(i.distbits=6,i.distcode=i.distdyn,k={bits:i.distbits},D=o(2,i.lens,i.nlen,i.ndist,i.distcode,0,i.work,k),i.distbits=k.bits,D){t.msg="invalid distances set",i.mode=c;break}if(i.mode=20,6===e)break t;case 20:i.mode=21;case 21:if(m>=6&&v>=258){t.next_out=g,t.avail_out=v,t.next_in=p,t.avail_in=m,i.hold=_,i.bits=y,a(t,A),g=t.next_out,f=t.output,v=t.avail_out,p=t.next_in,u=t.input,m=t.avail_in,_=i.hold,y=i.bits,i.mode===h&&(i.back=-1);break}for(i.back=0;T=(P=i.lencode[_&(1<>>16&255,E=65535&P,!((z=P>>>24)<=y);){if(0===m)break t;m--,_+=u[p++]<>L)])>>>16&255,E=65535&P,!(L+(z=P>>>24)<=y);){if(0===m)break t;m--,_+=u[p++]<>>=L,y-=L,i.back+=L}if(_>>>=z,y-=z,i.back+=z,i.length=E,0===T){i.mode=26;break}if(32&T){i.back=-1,i.mode=h;break}if(64&T){t.msg="invalid literal/length code",i.mode=c;break}i.extra=15&T,i.mode=22;case 22:if(i.extra){for(R=i.extra;y>>=i.extra,y-=i.extra,i.back+=i.extra}i.was=i.length,i.mode=23;case 23:for(;T=(P=i.distcode[_&(1<>>16&255,E=65535&P,!((z=P>>>24)<=y);){if(0===m)break t;m--,_+=u[p++]<>L)])>>>16&255,E=65535&P,!(L+(z=P>>>24)<=y);){if(0===m)break t;m--,_+=u[p++]<>>=L,y-=L,i.back+=L}if(_>>>=z,y-=z,i.back+=z,64&T){t.msg="invalid distance code",i.mode=c;break}i.offset=E,i.extra=15&T,i.mode=24;case 24:if(i.extra){for(R=i.extra;y>>=i.extra,y-=i.extra,i.back+=i.extra}if(i.offset>i.dmax){t.msg="invalid distance too far back",i.mode=c;break}i.mode=25;case 25:if(0===v)break t;if(C=A-v,i.offset>C){if((C=i.offset-C)>i.whave&&i.sane){t.msg="invalid distance too far back",i.mode=c;break}C>i.wnext?(C-=i.wnext,S=i.wsize-C):S=i.wnext-C,C>i.length&&(C=i.length),M=i.window}else M=f,S=g-i.offset,C=i.length;C>v&&(C=v),v-=C,i.length-=C;do{f[g++]=M[S++]}while(--C);0===i.length&&(i.mode=21);break;case 26:if(0===v)break t;f[g++]=i.length,v--,i.mode=21;break;case 27:if(i.wrap){for(;y<32;){if(0===m)break t;m--,_|=u[p++]<{"use strict";var r=i(981),s=15,n=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],a=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],o=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],l=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];t.exports=function(t,e,i,h,c,d,u,f){var p,g,m,v,_,y,b,x,w,A=f.bits,C=0,S=0,M=0,z=0,T=0,E=0,L=0,F=0,I=0,O=0,D=null,k=0,R=new r.Buf16(16),P=new r.Buf16(16),U=null,B=0;for(C=0;C<=s;C++)R[C]=0;for(S=0;S=1&&0===R[z];z--);if(T>z&&(T=z),0===z)return c[d++]=20971520,c[d++]=20971520,f.bits=1,0;for(M=1;M0&&(0===t||1!==z))return-1;for(P[1]=0,C=1;C852||2===t&&I>592)return 1;for(;;){b=C-L,u[S]y?(x=U[B+u[S]],w=D[k+u[S]]):(x=96,w=0),p=1<>L)+(g-=p)]=b<<24|x<<16|w}while(0!==g);for(p=1<>=1;if(0!==p?(O&=p-1,O+=p):O=0,S++,0==--R[C]){if(C===z)break;C=e[i+u[S]]}if(C>T&&(O&v)!==m){for(0===L&&(L=T),_+=M,F=1<<(E=C-L);E+L852||2===t&&I>592)return 1;c[m=O&v]=T<<24|E<<16|_-d}}return 0!==O&&(c[_+O]=C-L<<24|64<<16),f.bits=T,0}},834:t=>{"use strict";t.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}},697:(t,e,i)=>{"use strict";var r=i(981);function s(t){for(var e=t.length;--e>=0;)t[e]=0}var n=256,a=286,o=30,l=15,h=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],c=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],d=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],u=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],f=new Array(576);s(f);var p=new Array(60);s(p);var g=new Array(512);s(g);var m=new Array(256);s(m);var v=new Array(29);s(v);var _,y,b,x=new Array(o);function w(t,e,i,r,s){this.static_tree=t,this.extra_bits=e,this.extra_base=i,this.elems=r,this.max_length=s,this.has_stree=t&&t.length}function A(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}function C(t){return t<256?g[t]:g[256+(t>>>7)]}function S(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function M(t,e,i){t.bi_valid>16-i?(t.bi_buf|=e<>16-t.bi_valid,t.bi_valid+=i-16):(t.bi_buf|=e<>>=1,i<<=1}while(--e>0);return i>>>1}function E(t,e,i){var r,s,n=new Array(16),a=0;for(r=1;r<=l;r++)n[r]=a=a+i[r-1]<<1;for(s=0;s<=e;s++){var o=t[2*s+1];0!==o&&(t[2*s]=T(n[o]++,o))}}function L(t){var e;for(e=0;e8?S(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0}function I(t,e,i,r){var s=2*e,n=2*i;return t[s]>1;i>=1;i--)O(t,n,i);s=h;do{i=t.heap[1],t.heap[1]=t.heap[t.heap_len--],O(t,n,1),r=t.heap[1],t.heap[--t.heap_max]=i,t.heap[--t.heap_max]=r,n[2*s]=n[2*i]+n[2*r],t.depth[s]=(t.depth[i]>=t.depth[r]?t.depth[i]:t.depth[r])+1,n[2*i+1]=n[2*r+1]=s,t.heap[1]=s++,O(t,n,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],function(t,e){var i,r,s,n,a,o,h=e.dyn_tree,c=e.max_code,d=e.stat_desc.static_tree,u=e.stat_desc.has_stree,f=e.stat_desc.extra_bits,p=e.stat_desc.extra_base,g=e.stat_desc.max_length,m=0;for(n=0;n<=l;n++)t.bl_count[n]=0;for(h[2*t.heap[t.heap_max]+1]=0,i=t.heap_max+1;i<573;i++)(n=h[2*h[2*(r=t.heap[i])+1]+1]+1)>g&&(n=g,m++),h[2*r+1]=n,r>c||(t.bl_count[n]++,a=0,r>=p&&(a=f[r-p]),o=h[2*r],t.opt_len+=o*(n+a),u&&(t.static_len+=o*(d[2*r+1]+a)));if(0!==m){do{for(n=g-1;0===t.bl_count[n];)n--;t.bl_count[n]--,t.bl_count[n+1]+=2,t.bl_count[g]--,m-=2}while(m>0);for(n=g;0!==n;n--)for(r=t.bl_count[n];0!==r;)(s=t.heap[--i])>c||(h[2*s+1]!==n&&(t.opt_len+=(n-h[2*s+1])*h[2*s],h[2*s+1]=n),r--)}}(t,e),E(n,c,t.bl_count)}function R(t,e,i){var r,s,n=-1,a=e[1],o=0,l=7,h=4;for(0===a&&(l=138,h=3),e[2*(i+1)+1]=65535,r=0;r<=i;r++)s=a,a=e[2*(r+1)+1],++o>=7;r0?(2===t.strm.data_type&&(t.strm.data_type=function(t){var e,i=4093624447;for(e=0;e<=31;e++,i>>>=1)if(1&i&&0!==t.dyn_ltree[2*e])return 0;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return 1;for(e=32;e=3&&0===t.bl_tree[2*u[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e}(t),s=t.opt_len+3+7>>>3,(a=t.static_len+3+7>>>3)<=s&&(s=a)):s=a=i+5,i+4<=s&&-1!==e?B(t,e,i,r):4===t.strategy||a===s?(M(t,2+(r?1:0),3),D(t,f,p)):(M(t,4+(r?1:0),3),function(t,e,i,r){var s;for(M(t,e-257,5),M(t,i-1,5),M(t,r-4,4),s=0;s>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&i,t.last_lit++,0===e?t.dyn_ltree[2*i]++:(t.matches++,e--,t.dyn_ltree[2*(m[i]+n+1)]++,t.dyn_dtree[2*C(e)]++),t.last_lit===t.lit_bufsize-1},e._tr_align=function(t){M(t,2,3),z(t,256,f),function(t){16===t.bi_valid?(S(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}(t)}},746:t=>{"use strict";t.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},185:()=>{$3Dmol.workerString=function(){self.onmessage=function(t){var e=t.data,i=e.type;if(i<0)self.atomData=e.atoms,self.volume=e.volume,self.ps=new ProteinSurface;else{var r=self.ps;r.initparm(e.expandedExtent,1!=i,self.volume),r.fillvoxels(self.atomData,e.extendedAtoms),r.buildboundary(),4!==i&&2!==i||(r.fastdistancemap(),r.boundingatom(!1),r.fillvoxelswaals(self.atomData,e.extendedAtoms)),r.marchingcube(i);var s=r.getFacesAndVertices(e.atomsToShow);self.postMessage(s)}}}.toString().replace(/(^.*?\{|\}$)/g,""),$3Dmol.workerString+=";\nfunction _classCallCheck() {};",$3Dmol.workerString+=";\n"+$3Dmol.Vector3.toString(),$3Dmol.workerString+=";\n"+$3Dmol.MarchingCubeInitializer.toString()+";\n\n",$3Dmol.workerString+=";\n"+$3Dmol.PointGrid.toString()+";\n",$3Dmol.workerString+=";\nvar ProteinSurface = "+$3Dmol.ProteinSurface.toString()+";\n",$3Dmol.SurfaceWorker=window.URL?window.URL.createObjectURL(new Blob([$3Dmol.workerString],{type:"text/javascript"})):void 0},307:t=>{"object"==typeof t.exports&&(t.exports=window.$3Dmol)},471:function(t,e){!function(t){"use strict";function e(t,e,i){for(var r=(t.byteLength,0),s=i.length;s>r;r++){var n=i.charCodeAt(r);if(128>n)t.setUint8(e++,n>>>0&127);else if(2048>n)t.setUint8(e++,n>>>6&31|192),t.setUint8(e++,n>>>0&63|128);else if(65536>n)t.setUint8(e++,n>>>12&15|224),t.setUint8(e++,n>>>6&63|128),t.setUint8(e++,n>>>0&63|128);else{if(!(1114112>n))throw new Error("bad codepoint "+n);t.setUint8(e++,n>>>18&7|240),t.setUint8(e++,n>>>12&63|128),t.setUint8(e++,n>>>6&63|128),t.setUint8(e++,n>>>0&63|128)}}}function i(t){for(var e=0,i=0,r=t.length;r>i;i++){var s=t.charCodeAt(i);if(128>s)e+=1;else if(2048>s)e+=2;else if(65536>s)e+=3;else{if(!(1114112>s))throw new Error("bad codepoint "+s);e+=4}}return e}function r(t,s,n){var a=typeof t;if("string"===a){if(32>(o=i(t)))return s.setUint8(n,160|o),e(s,n+1,t),1+o;if(256>o)return s.setUint8(n,217),s.setUint8(n+1,o),e(s,n+2,t),2+o;if(65536>o)return s.setUint8(n,218),s.setUint16(n+1,o),e(s,n+3,t),3+o;if(4294967296>o)return s.setUint8(n,219),s.setUint32(n+1,o),e(s,n+5,t),5+o}if(t instanceof Uint8Array){var o=t.byteLength,l=new Uint8Array(s.buffer);if(256>o)return s.setUint8(n,196),s.setUint8(n+1,o),l.set(t,n+2),2+o;if(65536>o)return s.setUint8(n,197),s.setUint16(n+1,o),l.set(t,n+3),3+o;if(4294967296>o)return s.setUint8(n,198),s.setUint32(n+1,o),l.set(t,n+5),5+o}if("number"===a){if(!isFinite(t))throw new Error("Number not finite: "+t);if(Math.floor(t)!==t)return s.setUint8(n,203),s.setFloat64(n+1,t),9;if(t>=0){if(128>t)return s.setUint8(n,t),1;if(256>t)return s.setUint8(n,204),s.setUint8(n+1,t),2;if(65536>t)return s.setUint8(n,205),s.setUint16(n+1,t),3;if(4294967296>t)return s.setUint8(n,206),s.setUint32(n+1,t),5;throw new Error("Number too big 0x"+t.toString(16))}if(t>=-32)return s.setInt8(n,t),1;if(t>=-128)return s.setUint8(n,208),s.setInt8(n+1,t),2;if(t>=-32768)return s.setUint8(n,209),s.setInt16(n+1,t),3;if(t>=-2147483648)return s.setUint8(n,210),s.setInt32(n+1,t),5;throw new Error("Number too small -0x"+(-t).toString(16).substr(1))}if(null===t)return s.setUint8(n,192),1;if("boolean"===a)return s.setUint8(n,t?195:194),1;if("object"===a){var h=0,c=Array.isArray(t);if(c)o=t.length;else{var d=Object.keys(t);o=d.length}if(16>o?(s.setUint8(n,o|(c?144:128)),h=1):65536>o?(s.setUint8(n,c?220:222),s.setUint16(n+1,o),h=3):4294967296>o&&(s.setUint8(n,c?221:223),s.setUint32(n+1,o),h=5),c)for(var u=0;o>u;u++)h+=r(t[u],s,n+h);else for(u=0;o>u;u++){var f=d[u];h+=r(f,s,n+h),h+=r(t[f],s,n+h)}return h}throw new Error("Unknown type "+a)}function s(t){var e=typeof t;if("string"===e){if(32>(r=i(t)))return 1+r;if(256>r)return 2+r;if(65536>r)return 3+r;if(4294967296>r)return 5+r}if(t instanceof Uint8Array){if(256>(r=t.byteLength))return 2+r;if(65536>r)return 3+r;if(4294967296>r)return 5+r}if("number"===e){if(Math.floor(t)!==t)return 9;if(t>=0){if(128>t)return 1;if(256>t)return 2;if(65536>t)return 3;if(4294967296>t)return 5;throw new Error("Number too big 0x"+t.toString(16))}if(t>=-32)return 1;if(t>=-128)return 2;if(t>=-32768)return 3;if(t>=-2147483648)return 5;throw new Error("Number too small -0x"+t.toString(16).substr(1))}if("boolean"===e||null===t)return 1;if("object"===e){var r,n=0;if(Array.isArray(t)){r=t.length;for(var a=0;r>a;a++)n+=s(t[a])}else{var o=Object.keys(t);for(r=o.length,a=0;r>a;a++){var l=o[a];n+=s(l)+s(t[l])}}if(16>r)return 1+n;if(65536>r)return 3+n;if(4294967296>r)return 5+n;throw new Error("Array or object too long 0x"+r.toString(16))}throw new Error("Unknown type "+e)}function n(t){var e=new ArrayBuffer(s(t));return r(t,new DataView(e),0),new Uint8Array(e)}function a(t,e,i){return e?new t(e.buffer,e.byteOffset,e.byteLength/(i||1)):void 0}function o(t){return a(DataView,t)}function l(t){return a(Uint8Array,t)}function h(t){return a(Int8Array,t)}function c(t){return a(Int32Array,t,4)}function d(t){return a(Float32Array,t,4)}function u(t,e){var i=t.length/2;e||(e=new Int16Array(i));for(var r=0,s=0;i>r;++r,s+=2)e[r]=t[s]<<8^t[s+1];return e}function f(t,e){var i=t.length;e||(e=new Uint8Array(2*i));for(var r=o(e),s=0;i>s;++s)r.setInt16(2*s,t[s]);return l(e)}function p(t,e){var i=t.length/4;e||(e=new Int32Array(i));for(var r=0,s=0;i>r;++r,s+=4)e[r]=t[s]<<24^t[s+1]<<16^t[s+2]<<8^t[s+3];return e}function g(t,e){var i=t.length;e||(e=new Uint8Array(4*i));for(var r=o(e),s=0;i>s;++s)r.setInt32(4*s,t[s]);return l(e)}function m(t,e){var i=t.length;e||(e=new Float32Array(i/4));for(var r=o(e),s=o(t),n=0,a=0,l=i/4;l>n;++n,a+=4)r.setFloat32(a,s.getFloat32(a),!0);return e}function v(t,e,i){var r=t.length,s=1/e;i||(i=new Float32Array(r));for(var n=0;r>n;++n)i[n]=t[n]*s;return i}function _(t,e,i){var r=t.length;i||(i=new Int32Array(r));for(var s=0;r>s;++s)i[s]=Math.round(t[s]*e);return i}function y(t,e){var i,r;if(!e){var s=0;for(i=0,r=t.length;r>i;i+=2)s+=t[i+1];e=new t.constructor(s)}var n=0;for(i=0,r=t.length;r>i;i+=2)for(var a=t[i],o=t[i+1],l=0;o>l;++l)e[n]=a,++n;return e}function b(t){if(0===t.length)return new Int32Array;var e,i,r=2;for(e=1,i=t.length;i>e;++e)t[e-1]!==t[e]&&(r+=2);var s=new Int32Array(r),n=0,a=1;for(e=1,i=t.length;i>e;++e)t[e-1]!==t[e]?(s[n]=t[e-1],s[n+1]=a,a=1,n+=2):++a;return s[n]=t[t.length-1],s[n+1]=a,s}function x(t,e){var i=t.length;e||(e=new t.constructor(i)),i&&(e[0]=t[0]);for(var r=1;i>r;++r)e[r]=t[r]+e[r-1];return e}function w(t,e){var i=t.length;e||(e=new t.constructor(i)),e[0]=t[0];for(var r=1;i>r;++r)e[r]=t[r]-t[r-1];return e}function A(t,e){var i,r,s=t instanceof Int8Array?127:32767,n=-s-1,a=t.length;if(!e){var o=0;for(i=0;a>i;++i)t[i]n&&++o;e=new Int32Array(o)}for(i=0,r=0;a>i;){for(var l=0;t[i]===s||t[i]===n;)l+=t[i],++i;l+=t[i],++i,e[r]=l,++r}return e}function C(t,e){var i,r=e?127:32767,s=-r-1,n=t.length,a=0;for(i=0;n>i;++i)0===(h=t[i])?++a:h>0?(a+=Math.ceil(h/r),h%r==0&&(a+=1)):(a+=Math.ceil(h/s),h%s==0&&(a+=1));var o=e?new Int8Array(a):new Int16Array(a),l=0;for(i=0;n>i;++i){var h;if((h=t[i])>=0)for(;h>=r;)o[l]=r,++l,h-=r;else for(;s>=h;)o[l]=s,++l,h-=s;o[l]=h,++l}return o}function S(t,e){return x(y(t),e)}function M(t){return b(w(t))}function z(t,e,i){return v(y(t,c(i)),e,i)}function T(t,e){return b(_(t,e))}function E(t,e,i){return v(x(t,c(i)),e,i)}function L(t,e,i){return w(_(t,e),i)}function F(t,e,i){return v(A(t,c(i)),e,i)}function I(t,e,i){var r=A(t,c(i));return E(r,e,d(r))}function O(t,e,i){return C(L(t,e),i)}function D(t){var e=o(t),i=e.getInt32(0),r=e.getInt32(4),s=t.subarray(8,12);return[i,t=t.subarray(12),r,s]}function k(t,e,i,r){var s=new ArrayBuffer(12+r.byteLength),n=new Uint8Array(s),a=new DataView(s);return a.setInt32(0,t),a.setInt32(4,e),i&&n.set(i,8),n.set(r,12),n}function R(t){return k(2,t.length,void 0,l(t))}function P(t){return k(4,t.length,void 0,g(t))}function U(t,e){return k(5,t.length/e,g([e]),l(t))}function B(t){return k(6,t.length,void 0,g(b(t)))}function N(t){return k(8,t.length,void 0,g(M(t)))}function G(t,e){return k(9,t.length,g([e]),g(T(t,e)))}function V(t,e){return k(10,t.length,g([e]),f(O(t,e)))}function j(t){var e={};return tt.forEach((function(i){void 0!==t[i]&&(e[i]=t[i])})),t.bondAtomList&&(e.bondAtomList=P(t.bondAtomList)),t.bondOrderList&&(e.bondOrderList=R(t.bondOrderList)),e.xCoordList=V(t.xCoordList,1e3),e.yCoordList=V(t.yCoordList,1e3),e.zCoordList=V(t.zCoordList,1e3),t.bFactorList&&(e.bFactorList=V(t.bFactorList,100)),t.atomIdList&&(e.atomIdList=N(t.atomIdList)),t.altLocList&&(e.altLocList=B(t.altLocList)),t.occupancyList&&(e.occupancyList=G(t.occupancyList,100)),e.groupIdList=N(t.groupIdList),e.groupTypeList=P(t.groupTypeList),t.secStructList&&(e.secStructList=R(t.secStructList)),t.insCodeList&&(e.insCodeList=B(t.insCodeList)),t.sequenceIndexList&&(e.sequenceIndexList=N(t.sequenceIndexList)),e.chainIdList=U(t.chainIdList,4),t.chainNameList&&(e.chainNameList=U(t.chainNameList,4)),e}function H(t){function e(t){for(var e={},i=0;t>i;i++)e[n()]=n();return e}function i(e){var i=t.subarray(a,a+e);return a+=e,i}function r(e){var i=t.subarray(a,a+e);a+=e;var r=65535;if(e>r){for(var s=[],n=0;ni;i++)e[i]=n();return e}function n(){var n,l,h=t[a];if(!(128&h))return a++,h;if(128==(240&h))return a++,e(l=15&h);if(144==(240&h))return a++,s(l=15&h);if(160==(224&h))return a++,r(l=31&h);if(!(224&~h))return n=o.getInt8(a),a++,n;switch(h){case 192:return a++,null;case 194:return a++,!1;case 195:return a++,!0;case 196:return l=o.getUint8(a+1),a+=2,i(l);case 197:return l=o.getUint16(a+1),a+=3,i(l);case 198:return l=o.getUint32(a+1),a+=5,i(l);case 202:return n=o.getFloat32(a+1),a+=5,n;case 203:return n=o.getFloat64(a+1),a+=9,n;case 204:return n=t[a+1],a+=2,n;case 205:return n=o.getUint16(a+1),a+=3,n;case 206:return n=o.getUint32(a+1),a+=5,n;case 208:return n=o.getInt8(a+1),a+=2,n;case 209:return n=o.getInt16(a+1),a+=3,n;case 210:return n=o.getInt32(a+1),a+=5,n;case 217:return l=o.getUint8(a+1),a+=2,r(l);case 218:return l=o.getUint16(a+1),a+=3,r(l);case 219:return l=o.getUint32(a+1),a+=5,r(l);case 220:return l=o.getUint16(a+1),a+=3,s(l);case 221:return l=o.getUint32(a+1),a+=5,s(l);case 222:return l=o.getUint16(a+1),a+=3,e(l);case 223:return l=o.getUint32(a+1),a+=5,e(l)}throw new Error("Unknown type 0x"+h.toString(16))}var a=0,o=new DataView(t.buffer);return n()}function W(t,e,i,r){switch(t){case 1:return m(e);case 2:return h(e);case 3:return u(e);case 4:return p(e);case 5:return l(e);case 6:return y(p(e),new Uint8Array(i));case 7:return y(p(e));case 8:return S(p(e));case 9:return z(p(e),p(r)[0]);case 10:return I(u(e),p(r)[0]);case 11:return v(u(e),p(r)[0]);case 12:return F(u(e),p(r)[0]);case 13:return F(h(e),p(r)[0]);case 14:return A(u(e));case 15:return A(h(e))}}function q(t,e){var i=(e=e||{}).ignoreFields,r={};return it.forEach((function(e){var s=!!i&&-1!==i.indexOf(e),n=t[e];s||void 0===n||(n instanceof Uint8Array?r[e]=W.apply(null,D(n)):r[e]=n)})),r}function Y(t){return String.fromCharCode.apply(null,t).replace(/\0/g,"")}function Z(t,e,i){var r,s,n,a,o,l,h=(i=i||{}).firstModelOnly,c=e.onModel,d=e.onChain,u=e.onGroup,f=e.onAtom,p=e.onBond,g=0,m=0,v=0,_=0,y=0,b=-1,x=t.chainNameList,w=t.secStructList,A=t.insCodeList,C=t.sequenceIndexList,S=t.atomIdList,M=t.bFactorList,z=t.altLocList,T=t.occupancyList,E=t.bondAtomList,L=t.bondOrderList;for(r=0,s=t.chainsPerModel.length;s>r&&!(h&&g>0);++r){var F=t.chainsPerModel[g];for(c&&c({chainCount:F,modelIndex:g}),n=0;F>n;++n){var I=t.groupsPerChain[m];if(d){var O=Y(t.chainIdList.subarray(4*m,4*m+4)),D=null;x&&(D=Y(x.subarray(4*m,4*m+4))),d({groupCount:I,chainIndex:m,modelIndex:g,chainId:O,chainName:D})}for(a=0;I>a;++a){var k=t.groupList[t.groupTypeList[v]],R=k.atomNameList.length;if(u){var P=null;w&&(P=w[v]);var U=null;t.insCodeList&&(U=String.fromCharCode(A[v]));var B=null;C&&(B=C[v]),u({atomCount:R,groupIndex:v,chainIndex:m,modelIndex:g,groupId:t.groupIdList[v],groupType:t.groupTypeList[v],groupName:k.groupName,singleLetterCode:k.singleLetterCode,chemCompType:k.chemCompType,secStruct:P,insCode:U,sequenceIndex:B})}for(o=0;R>o;++o){if(f){var N=null;S&&(N=S[_]);var G=null;M&&(G=M[_]);var V=null;z&&(V=String.fromCharCode(z[_]));var j=null;T&&(j=T[_]),f({atomIndex:_,groupIndex:v,chainIndex:m,modelIndex:g,atomId:N,element:k.elementList[o],atomName:k.atomNameList[o],formalCharge:k.formalChargeList[o],xCoord:t.xCoordList[_],yCoord:t.yCoordList[_],zCoord:t.zCoordList[_],bFactor:G,altLoc:V,occupancy:j})}_+=1}if(p){var H=k.bondAtomList;for(o=0,l=k.bondOrderList.length;l>o;++o)p({atomIndex1:_-R+H[2*o],atomIndex2:_-R+H[2*o+1],bondOrder:k.bondOrderList[o]})}v+=1}m+=1}if(y=b+1,b=_-1,p&&E)for(o=0,l=E.length;l>o;o+=2){var W=E[o],q=E[o+1];(W>=y&&b>=W||q>=y&&b>=q)&&p({atomIndex1:W,atomIndex2:q,bondOrder:L?L[o/2]:null})}g+=1}}function X(t){return n(j(t))}function K(t,e){return t instanceof ArrayBuffer&&(t=new Uint8Array(t)),q(t instanceof Uint8Array?H(t):t,e)}function Q(t,e,i,r){function s(){try{var t=K(n.response);i(t)}catch(t){r(t)}}var n=new XMLHttpRequest;n.addEventListener("load",s,!0),n.addEventListener("error",r,!0),n.responseType="arraybuffer",n.open("GET",e+t.toUpperCase()),n.send()}function $(t,e,i){Q(t,nt,e,i)}function J(t,e,i){Q(t,at,e,i)}var tt=["mmtfVersion","mmtfProducer","unitCell","spaceGroup","structureId","title","depositionDate","releaseDate","experimentalMethods","resolution","rFree","rWork","bioAssemblyList","ncsOperatorList","entityList","groupList","numBonds","numAtoms","numGroups","numChains","numModels","groupsPerChain","chainsPerModel"],et=["xCoordList","yCoordList","zCoordList","groupIdList","groupTypeList","chainIdList","bFactorList","atomIdList","altLocList","occupancyList","secStructList","insCodeList","sequenceIndexList","chainNameList","bondAtomList","bondOrderList"],it=tt.concat(et),rt="v1.0.1",st="//mmtf.rcsb.org/v1.0/",nt=st+"full/",at=st+"reduced/";t.encode=X,t.decode=K,t.traverse=Z,t.fetch=$,t.fetchReduced=J,t.version=rt,t.fetchUrl=nt,t.fetchReducedUrl=at,t.encodeMsgpack=n,t.encodeMmtf=j,t.decodeMsgpack=H,t.decodeMmtf=q}(e)},75:(t,e,i)=>{"use strict";i.r(e),i.d(e,{Deflate:()=>yi,Inflate:()=>Ai,constants:()=>zi,default:()=>Ti,deflate:()=>bi,deflateRaw:()=>xi,gzip:()=>wi,inflate:()=>Ci,inflateRaw:()=>Si,ungzip:()=>Mi});function r(t){let e=t.length;for(;--e>=0;)t[e]=0}const s=256,n=286,a=30,o=15,l=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),h=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),c=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),d=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),u=new Array(576);r(u);const f=new Array(60);r(f);const p=new Array(512);r(p);const g=new Array(256);r(g);const m=new Array(29);r(m);const v=new Array(a);function _(t,e,i,r,s){this.static_tree=t,this.extra_bits=e,this.extra_base=i,this.elems=r,this.max_length=s,this.has_stree=t&&t.length}let y,b,x;function w(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}r(v);const A=t=>t<256?p[t]:p[256+(t>>>7)],C=(t,e)=>{t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255},S=(t,e,i)=>{t.bi_valid>16-i?(t.bi_buf|=e<>16-t.bi_valid,t.bi_valid+=i-16):(t.bi_buf|=e<{S(t,i[2*e],i[2*e+1])},z=(t,e)=>{let i=0;do{i|=1&t,t>>>=1,i<<=1}while(--e>0);return i>>>1},T=(t,e,i)=>{const r=new Array(16);let s,n,a=0;for(s=1;s<=o;s++)a=a+i[s-1]<<1,r[s]=a;for(n=0;n<=e;n++){let e=t[2*n+1];0!==e&&(t[2*n]=z(r[e]++,e))}},E=t=>{let e;for(e=0;e{t.bi_valid>8?C(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0},F=(t,e,i,r)=>{const s=2*e,n=2*i;return t[s]{const r=t.heap[i];let s=i<<1;for(;s<=t.heap_len&&(s{let r,n,a,o,c=0;if(0!==t.sym_next)do{r=255&t.pending_buf[t.sym_buf+c++],r+=(255&t.pending_buf[t.sym_buf+c++])<<8,n=t.pending_buf[t.sym_buf+c++],0===r?M(t,n,e):(a=g[n],M(t,a+s+1,e),o=l[a],0!==o&&(n-=m[a],S(t,n,o)),r--,a=A(r),M(t,a,i),o=h[a],0!==o&&(r-=v[a],S(t,r,o)))}while(c{const i=e.dyn_tree,r=e.stat_desc.static_tree,s=e.stat_desc.has_stree,n=e.stat_desc.elems;let a,l,h,c=-1;for(t.heap_len=0,t.heap_max=573,a=0;a>1;a>=1;a--)I(t,i,a);h=n;do{a=t.heap[1],t.heap[1]=t.heap[t.heap_len--],I(t,i,1),l=t.heap[1],t.heap[--t.heap_max]=a,t.heap[--t.heap_max]=l,i[2*h]=i[2*a]+i[2*l],t.depth[h]=(t.depth[a]>=t.depth[l]?t.depth[a]:t.depth[l])+1,i[2*a+1]=i[2*l+1]=h,t.heap[1]=h++,I(t,i,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],((t,e)=>{const i=e.dyn_tree,r=e.max_code,s=e.stat_desc.static_tree,n=e.stat_desc.has_stree,a=e.stat_desc.extra_bits,l=e.stat_desc.extra_base,h=e.stat_desc.max_length;let c,d,u,f,p,g,m=0;for(f=0;f<=o;f++)t.bl_count[f]=0;for(i[2*t.heap[t.heap_max]+1]=0,c=t.heap_max+1;c<573;c++)d=t.heap[c],f=i[2*i[2*d+1]+1]+1,f>h&&(f=h,m++),i[2*d+1]=f,d>r||(t.bl_count[f]++,p=0,d>=l&&(p=a[d-l]),g=i[2*d],t.opt_len+=g*(f+p),n&&(t.static_len+=g*(s[2*d+1]+p)));if(0!==m){do{for(f=h-1;0===t.bl_count[f];)f--;t.bl_count[f]--,t.bl_count[f+1]+=2,t.bl_count[h]--,m-=2}while(m>0);for(f=h;0!==f;f--)for(d=t.bl_count[f];0!==d;)u=t.heap[--c],u>r||(i[2*u+1]!==f&&(t.opt_len+=(f-i[2*u+1])*i[2*u],i[2*u+1]=f),d--)}})(t,e),T(i,c,t.bl_count)},k=(t,e,i)=>{let r,s,n=-1,a=e[1],o=0,l=7,h=4;for(0===a&&(l=138,h=3),e[2*(i+1)+1]=65535,r=0;r<=i;r++)s=a,a=e[2*(r+1)+1],++o{let r,s,n=-1,a=e[1],o=0,l=7,h=4;for(0===a&&(l=138,h=3),r=0;r<=i;r++)if(s=a,a=e[2*(r+1)+1],!(++o{S(t,0+(r?1:0),3),L(t),C(t,i),C(t,~i),i&&t.pending_buf.set(t.window.subarray(e,e+i),t.pending),t.pending+=i};var B=(t,e,i,r)=>{let n,a,o=0;t.level>0?(2===t.strm.data_type&&(t.strm.data_type=(t=>{let e,i=4093624447;for(e=0;e<=31;e++,i>>>=1)if(1&i&&0!==t.dyn_ltree[2*e])return 0;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return 1;for(e=32;e{let e;for(k(t,t.dyn_ltree,t.l_desc.max_code),k(t,t.dyn_dtree,t.d_desc.max_code),D(t,t.bl_desc),e=18;e>=3&&0===t.bl_tree[2*d[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e})(t),n=t.opt_len+3+7>>>3,a=t.static_len+3+7>>>3,a<=n&&(n=a)):n=a=i+5,i+4<=n&&-1!==e?U(t,e,i,r):4===t.strategy||a===n?(S(t,2+(r?1:0),3),O(t,u,f)):(S(t,4+(r?1:0),3),((t,e,i,r)=>{let s;for(S(t,e-257,5),S(t,i-1,5),S(t,r-4,4),s=0;s{P||((()=>{let t,e,i,r,s;const d=new Array(16);for(i=0,r=0;r<28;r++)for(m[r]=i,t=0;t<1<>=7;r(t.pending_buf[t.sym_buf+t.sym_next++]=e,t.pending_buf[t.sym_buf+t.sym_next++]=e>>8,t.pending_buf[t.sym_buf+t.sym_next++]=i,0===e?t.dyn_ltree[2*i]++:(t.matches++,e--,t.dyn_ltree[2*(g[i]+s+1)]++,t.dyn_dtree[2*A(e)]++),t.sym_next===t.sym_end),_tr_align:t=>{S(t,2,3),M(t,256,u),(t=>{16===t.bi_valid?(C(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)})(t)}};var G=(t,e,i,r)=>{let s=65535&t,n=t>>>16&65535,a=0;for(;0!==i;){a=i>2e3?2e3:i,i-=a;do{s=s+e[r++]|0,n=n+s|0}while(--a);s%=65521,n%=65521}return s|n<<16};const V=new Uint32Array((()=>{let t,e=[];for(var i=0;i<256;i++){t=i;for(var r=0;r<8;r++)t=1&t?3988292384^t>>>1:t>>>1;e[i]=t}return e})());var j=(t,e,i,r)=>{const s=V,n=r+i;t^=-1;for(let i=r;i>>8^s[255&(t^e[i])];return~t},H={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"},W={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};const{_tr_init:q,_tr_stored_block:Y,_tr_flush_block:Z,_tr_tally:X,_tr_align:K}=N,{Z_NO_FLUSH:Q,Z_PARTIAL_FLUSH:$,Z_FULL_FLUSH:J,Z_FINISH:tt,Z_BLOCK:et,Z_OK:it,Z_STREAM_END:rt,Z_STREAM_ERROR:st,Z_DATA_ERROR:nt,Z_BUF_ERROR:at,Z_DEFAULT_COMPRESSION:ot,Z_FILTERED:lt,Z_HUFFMAN_ONLY:ht,Z_RLE:ct,Z_FIXED:dt,Z_DEFAULT_STRATEGY:ut,Z_UNKNOWN:ft,Z_DEFLATED:pt}=W,gt=258,mt=262,vt=42,_t=113,yt=666,bt=(t,e)=>(t.msg=H[e],e),xt=t=>2*t-(t>4?9:0),wt=t=>{let e=t.length;for(;--e>=0;)t[e]=0},At=t=>{let e,i,r,s=t.w_size;e=t.hash_size,r=e;do{i=t.head[--r],t.head[r]=i>=s?i-s:0}while(--e);e=s,r=e;do{i=t.prev[--r],t.prev[r]=i>=s?i-s:0}while(--e)};let Ct=(t,e,i)=>(e<{const e=t.state;let i=e.pending;i>t.avail_out&&(i=t.avail_out),0!==i&&(t.output.set(e.pending_buf.subarray(e.pending_out,e.pending_out+i),t.next_out),t.next_out+=i,e.pending_out+=i,t.total_out+=i,t.avail_out-=i,e.pending-=i,0===e.pending&&(e.pending_out=0))},Mt=(t,e)=>{Z(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,St(t.strm)},zt=(t,e)=>{t.pending_buf[t.pending++]=e},Tt=(t,e)=>{t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e},Et=(t,e,i,r)=>{let s=t.avail_in;return s>r&&(s=r),0===s?0:(t.avail_in-=s,e.set(t.input.subarray(t.next_in,t.next_in+s),i),1===t.state.wrap?t.adler=G(t.adler,e,s,i):2===t.state.wrap&&(t.adler=j(t.adler,e,s,i)),t.next_in+=s,t.total_in+=s,s)},Lt=(t,e)=>{let i,r,s=t.max_chain_length,n=t.strstart,a=t.prev_length,o=t.nice_match;const l=t.strstart>t.w_size-mt?t.strstart-(t.w_size-mt):0,h=t.window,c=t.w_mask,d=t.prev,u=t.strstart+gt;let f=h[n+a-1],p=h[n+a];t.prev_length>=t.good_match&&(s>>=2),o>t.lookahead&&(o=t.lookahead);do{if(i=e,h[i+a]===p&&h[i+a-1]===f&&h[i]===h[n]&&h[++i]===h[n+1]){n+=2,i++;do{}while(h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&h[++n]===h[++i]&&na){if(t.match_start=e,a=r,r>=o)break;f=h[n+a-1],p=h[n+a]}}}while((e=d[e&c])>l&&0!=--s);return a<=t.lookahead?a:t.lookahead},Ft=t=>{const e=t.w_size;let i,r,s;do{if(r=t.window_size-t.lookahead-t.strstart,t.strstart>=e+(e-mt)&&(t.window.set(t.window.subarray(e,e+e-r),0),t.match_start-=e,t.strstart-=e,t.block_start-=e,t.insert>t.strstart&&(t.insert=t.strstart),At(t),r+=e),0===t.strm.avail_in)break;if(i=Et(t.strm,t.window,t.strstart+t.lookahead,r),t.lookahead+=i,t.lookahead+t.insert>=3)for(s=t.strstart-t.insert,t.ins_h=t.window[s],t.ins_h=Ct(t,t.ins_h,t.window[s+1]);t.insert&&(t.ins_h=Ct(t,t.ins_h,t.window[s+3-1]),t.prev[s&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=s,s++,t.insert--,!(t.lookahead+t.insert<3)););}while(t.lookahead{let i,r,s,n=t.pending_buf_size-5>t.w_size?t.w_size:t.pending_buf_size-5,a=0,o=t.strm.avail_in;do{if(i=65535,s=t.bi_valid+42>>3,t.strm.avail_outr+t.strm.avail_in&&(i=r+t.strm.avail_in),i>s&&(i=s),i>8,t.pending_buf[t.pending-2]=~i,t.pending_buf[t.pending-1]=~i>>8,St(t.strm),r&&(r>i&&(r=i),t.strm.output.set(t.window.subarray(t.block_start,t.block_start+r),t.strm.next_out),t.strm.next_out+=r,t.strm.avail_out-=r,t.strm.total_out+=r,t.block_start+=r,i-=r),i&&(Et(t.strm,t.strm.output,t.strm.next_out,i),t.strm.next_out+=i,t.strm.avail_out-=i,t.strm.total_out+=i)}while(0===a);return o-=t.strm.avail_in,o&&(o>=t.w_size?(t.matches=2,t.window.set(t.strm.input.subarray(t.strm.next_in-t.w_size,t.strm.next_in),0),t.strstart=t.w_size,t.insert=t.strstart):(t.window_size-t.strstart<=o&&(t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,t.insert>t.strstart&&(t.insert=t.strstart)),t.window.set(t.strm.input.subarray(t.strm.next_in-o,t.strm.next_in),t.strstart),t.strstart+=o,t.insert+=o>t.w_size-t.insert?t.w_size-t.insert:o),t.block_start=t.strstart),t.high_waters&&t.block_start>=t.w_size&&(t.block_start-=t.w_size,t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,s+=t.w_size,t.insert>t.strstart&&(t.insert=t.strstart)),s>t.strm.avail_in&&(s=t.strm.avail_in),s&&(Et(t.strm,t.window,t.strstart,s),t.strstart+=s,t.insert+=s>t.w_size-t.insert?t.w_size-t.insert:s),t.high_water>3,s=t.pending_buf_size-s>65535?65535:t.pending_buf_size-s,n=s>t.w_size?t.w_size:s,r=t.strstart-t.block_start,(r>=n||(r||e===tt)&&e!==Q&&0===t.strm.avail_in&&r<=s)&&(i=r>s?s:r,a=e===tt&&0===t.strm.avail_in&&i===r?1:0,Y(t,t.block_start,i,a),t.block_start+=i,St(t.strm)),a?3:1)},Ot=(t,e)=>{let i,r;for(;;){if(t.lookahead=3&&(t.ins_h=Ct(t,t.ins_h,t.window[t.strstart+3-1]),i=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==i&&t.strstart-i<=t.w_size-mt&&(t.match_length=Lt(t,i)),t.match_length>=3)if(r=X(t,t.strstart-t.match_start,t.match_length-3),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=3){t.match_length--;do{t.strstart++,t.ins_h=Ct(t,t.ins_h,t.window[t.strstart+3-1]),i=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart}while(0!=--t.match_length);t.strstart++}else t.strstart+=t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=Ct(t,t.ins_h,t.window[t.strstart+1]);else r=X(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(r&&(Mt(t,!1),0===t.strm.avail_out))return 1}return t.insert=t.strstart<2?t.strstart:2,e===tt?(Mt(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(Mt(t,!1),0===t.strm.avail_out)?1:2},Dt=(t,e)=>{let i,r,s;for(;;){if(t.lookahead=3&&(t.ins_h=Ct(t,t.ins_h,t.window[t.strstart+3-1]),i=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=2,0!==i&&t.prev_length4096)&&(t.match_length=2)),t.prev_length>=3&&t.match_length<=t.prev_length){s=t.strstart+t.lookahead-3,r=X(t,t.strstart-1-t.prev_match,t.prev_length-3),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=s&&(t.ins_h=Ct(t,t.ins_h,t.window[t.strstart+3-1]),i=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart)}while(0!=--t.prev_length);if(t.match_available=0,t.match_length=2,t.strstart++,r&&(Mt(t,!1),0===t.strm.avail_out))return 1}else if(t.match_available){if(r=X(t,0,t.window[t.strstart-1]),r&&Mt(t,!1),t.strstart++,t.lookahead--,0===t.strm.avail_out)return 1}else t.match_available=1,t.strstart++,t.lookahead--}return t.match_available&&(r=X(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<2?t.strstart:2,e===tt?(Mt(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(Mt(t,!1),0===t.strm.avail_out)?1:2};function kt(t,e,i,r,s){this.good_length=t,this.max_lazy=e,this.nice_length=i,this.max_chain=r,this.func=s}const Rt=[new kt(0,0,0,0,It),new kt(4,4,8,4,Ot),new kt(4,5,16,8,Ot),new kt(4,6,32,32,Ot),new kt(4,4,16,16,Dt),new kt(8,16,32,32,Dt),new kt(8,16,128,128,Dt),new kt(8,32,128,256,Dt),new kt(32,128,258,1024,Dt),new kt(32,258,258,4096,Dt)];function Pt(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=pt,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new Uint16Array(1146),this.dyn_dtree=new Uint16Array(122),this.bl_tree=new Uint16Array(78),wt(this.dyn_ltree),wt(this.dyn_dtree),wt(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new Uint16Array(16),this.heap=new Uint16Array(573),wt(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new Uint16Array(573),wt(this.depth),this.sym_buf=0,this.lit_bufsize=0,this.sym_next=0,this.sym_end=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}const Ut=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||e.status!==vt&&57!==e.status&&69!==e.status&&73!==e.status&&91!==e.status&&103!==e.status&&e.status!==_t&&e.status!==yt?1:0},Bt=t=>{if(Ut(t))return bt(t,st);t.total_in=t.total_out=0,t.data_type=ft;const e=t.state;return e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=2===e.wrap?57:e.wrap?vt:_t,t.adler=2===e.wrap?0:1,e.last_flush=-2,q(e),it},Nt=t=>{const e=Bt(t);var i;return e===it&&((i=t.state).window_size=2*i.w_size,wt(i.head),i.max_lazy_match=Rt[i.level].max_lazy,i.good_match=Rt[i.level].good_length,i.nice_match=Rt[i.level].nice_length,i.max_chain_length=Rt[i.level].max_chain,i.strstart=0,i.block_start=0,i.lookahead=0,i.insert=0,i.match_length=i.prev_length=2,i.match_available=0,i.ins_h=0),e},Gt=(t,e,i,r,s,n)=>{if(!t)return st;let a=1;if(e===ot&&(e=6),r<0?(a=0,r=-r):r>15&&(a=2,r-=16),s<1||s>9||i!==pt||r<8||r>15||e<0||e>9||n<0||n>dt||8===r&&1!==a)return bt(t,st);8===r&&(r=9);const o=new Pt;return t.state=o,o.strm=t,o.status=vt,o.wrap=a,o.gzhead=null,o.w_bits=r,o.w_size=1<Gt(t,e,pt,15,8,ut),deflateInit2:Gt,deflateReset:Nt,deflateResetKeep:Bt,deflateSetHeader:(t,e)=>Ut(t)||2!==t.state.wrap?st:(t.state.gzhead=e,it),deflate:(t,e)=>{if(Ut(t)||e>et||e<0)return t?bt(t,st):st;const i=t.state;if(!t.output||0!==t.avail_in&&!t.input||i.status===yt&&e!==tt)return bt(t,0===t.avail_out?at:st);const r=i.last_flush;if(i.last_flush=e,0!==i.pending){if(St(t),0===t.avail_out)return i.last_flush=-1,it}else if(0===t.avail_in&&xt(e)<=xt(r)&&e!==tt)return bt(t,at);if(i.status===yt&&0!==t.avail_in)return bt(t,at);if(i.status===vt&&0===i.wrap&&(i.status=_t),i.status===vt){let e=pt+(i.w_bits-8<<4)<<8,r=-1;if(r=i.strategy>=ht||i.level<2?0:i.level<6?1:6===i.level?2:3,e|=r<<6,0!==i.strstart&&(e|=32),e+=31-e%31,Tt(i,e),0!==i.strstart&&(Tt(i,t.adler>>>16),Tt(i,65535&t.adler)),t.adler=1,i.status=_t,St(t),0!==i.pending)return i.last_flush=-1,it}if(57===i.status)if(t.adler=0,zt(i,31),zt(i,139),zt(i,8),i.gzhead)zt(i,(i.gzhead.text?1:0)+(i.gzhead.hcrc?2:0)+(i.gzhead.extra?4:0)+(i.gzhead.name?8:0)+(i.gzhead.comment?16:0)),zt(i,255&i.gzhead.time),zt(i,i.gzhead.time>>8&255),zt(i,i.gzhead.time>>16&255),zt(i,i.gzhead.time>>24&255),zt(i,9===i.level?2:i.strategy>=ht||i.level<2?4:0),zt(i,255&i.gzhead.os),i.gzhead.extra&&i.gzhead.extra.length&&(zt(i,255&i.gzhead.extra.length),zt(i,i.gzhead.extra.length>>8&255)),i.gzhead.hcrc&&(t.adler=j(t.adler,i.pending_buf,i.pending,0)),i.gzindex=0,i.status=69;else if(zt(i,0),zt(i,0),zt(i,0),zt(i,0),zt(i,0),zt(i,9===i.level?2:i.strategy>=ht||i.level<2?4:0),zt(i,3),i.status=_t,St(t),0!==i.pending)return i.last_flush=-1,it;if(69===i.status){if(i.gzhead.extra){let e=i.pending,r=(65535&i.gzhead.extra.length)-i.gzindex;for(;i.pending+r>i.pending_buf_size;){let s=i.pending_buf_size-i.pending;if(i.pending_buf.set(i.gzhead.extra.subarray(i.gzindex,i.gzindex+s),i.pending),i.pending=i.pending_buf_size,i.gzhead.hcrc&&i.pending>e&&(t.adler=j(t.adler,i.pending_buf,i.pending-e,e)),i.gzindex+=s,St(t),0!==i.pending)return i.last_flush=-1,it;e=0,r-=s}let s=new Uint8Array(i.gzhead.extra);i.pending_buf.set(s.subarray(i.gzindex,i.gzindex+r),i.pending),i.pending+=r,i.gzhead.hcrc&&i.pending>e&&(t.adler=j(t.adler,i.pending_buf,i.pending-e,e)),i.gzindex=0}i.status=73}if(73===i.status){if(i.gzhead.name){let e,r=i.pending;do{if(i.pending===i.pending_buf_size){if(i.gzhead.hcrc&&i.pending>r&&(t.adler=j(t.adler,i.pending_buf,i.pending-r,r)),St(t),0!==i.pending)return i.last_flush=-1,it;r=0}e=i.gzindexr&&(t.adler=j(t.adler,i.pending_buf,i.pending-r,r)),i.gzindex=0}i.status=91}if(91===i.status){if(i.gzhead.comment){let e,r=i.pending;do{if(i.pending===i.pending_buf_size){if(i.gzhead.hcrc&&i.pending>r&&(t.adler=j(t.adler,i.pending_buf,i.pending-r,r)),St(t),0!==i.pending)return i.last_flush=-1,it;r=0}e=i.gzindexr&&(t.adler=j(t.adler,i.pending_buf,i.pending-r,r))}i.status=103}if(103===i.status){if(i.gzhead.hcrc){if(i.pending+2>i.pending_buf_size&&(St(t),0!==i.pending))return i.last_flush=-1,it;zt(i,255&t.adler),zt(i,t.adler>>8&255),t.adler=0}if(i.status=_t,St(t),0!==i.pending)return i.last_flush=-1,it}if(0!==t.avail_in||0!==i.lookahead||e!==Q&&i.status!==yt){let r=0===i.level?It(i,e):i.strategy===ht?((t,e)=>{let i;for(;;){if(0===t.lookahead&&(Ft(t),0===t.lookahead)){if(e===Q)return 1;break}if(t.match_length=0,i=X(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,i&&(Mt(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===tt?(Mt(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(Mt(t,!1),0===t.strm.avail_out)?1:2})(i,e):i.strategy===ct?((t,e)=>{let i,r,s,n;const a=t.window;for(;;){if(t.lookahead<=gt){if(Ft(t),t.lookahead<=gt&&e===Q)return 1;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=3&&t.strstart>0&&(s=t.strstart-1,r=a[s],r===a[++s]&&r===a[++s]&&r===a[++s])){n=t.strstart+gt;do{}while(r===a[++s]&&r===a[++s]&&r===a[++s]&&r===a[++s]&&r===a[++s]&&r===a[++s]&&r===a[++s]&&r===a[++s]&&st.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=3?(i=X(t,1,t.match_length-3),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(i=X(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),i&&(Mt(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===tt?(Mt(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(Mt(t,!1),0===t.strm.avail_out)?1:2})(i,e):Rt[i.level].func(i,e);if(3!==r&&4!==r||(i.status=yt),1===r||3===r)return 0===t.avail_out&&(i.last_flush=-1),it;if(2===r&&(e===$?K(i):e!==et&&(Y(i,0,0,!1),e===J&&(wt(i.head),0===i.lookahead&&(i.strstart=0,i.block_start=0,i.insert=0))),St(t),0===t.avail_out))return i.last_flush=-1,it}return e!==tt?it:i.wrap<=0?rt:(2===i.wrap?(zt(i,255&t.adler),zt(i,t.adler>>8&255),zt(i,t.adler>>16&255),zt(i,t.adler>>24&255),zt(i,255&t.total_in),zt(i,t.total_in>>8&255),zt(i,t.total_in>>16&255),zt(i,t.total_in>>24&255)):(Tt(i,t.adler>>>16),Tt(i,65535&t.adler)),St(t),i.wrap>0&&(i.wrap=-i.wrap),0!==i.pending?it:rt)},deflateEnd:t=>{if(Ut(t))return st;const e=t.state.status;return t.state=null,e===_t?bt(t,nt):it},deflateSetDictionary:(t,e)=>{let i=e.length;if(Ut(t))return st;const r=t.state,s=r.wrap;if(2===s||1===s&&r.status!==vt||r.lookahead)return st;if(1===s&&(t.adler=G(t.adler,e,i,0)),r.wrap=0,i>=r.w_size){0===s&&(wt(r.head),r.strstart=0,r.block_start=0,r.insert=0);let t=new Uint8Array(r.w_size);t.set(e.subarray(i-r.w_size,i),0),e=t,i=r.w_size}const n=t.avail_in,a=t.next_in,o=t.input;for(t.avail_in=i,t.next_in=0,t.input=e,Ft(r);r.lookahead>=3;){let t=r.strstart,e=r.lookahead-2;do{r.ins_h=Ct(r,r.ins_h,r.window[t+3-1]),r.prev[t&r.w_mask]=r.head[r.ins_h],r.head[r.ins_h]=t,t++}while(--e);r.strstart=t,r.lookahead=2,Ft(r)}return r.strstart+=r.lookahead,r.block_start=r.strstart,r.insert=r.lookahead,r.lookahead=0,r.match_length=r.prev_length=2,r.match_available=0,t.next_in=a,t.input=o,t.avail_in=n,r.wrap=s,it},deflateInfo:"pako deflate (from Nodeca project)"};const jt=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var Ht=function(t){const e=Array.prototype.slice.call(arguments,1);for(;e.length;){const i=e.shift();if(i){if("object"!=typeof i)throw new TypeError(i+"must be non-object");for(const e in i)jt(i,e)&&(t[e]=i[e])}}return t},Wt=t=>{let e=0;for(let i=0,r=t.length;i=252?6:t>=248?5:t>=240?4:t>=224?3:t>=192?2:1;Yt[254]=Yt[254]=1;var Zt=t=>{if("function"==typeof TextEncoder&&TextEncoder.prototype.encode)return(new TextEncoder).encode(t);let e,i,r,s,n,a=t.length,o=0;for(s=0;s>>6,e[n++]=128|63&i):i<65536?(e[n++]=224|i>>>12,e[n++]=128|i>>>6&63,e[n++]=128|63&i):(e[n++]=240|i>>>18,e[n++]=128|i>>>12&63,e[n++]=128|i>>>6&63,e[n++]=128|63&i);return e},Xt=(t,e)=>{const i=e||t.length;if("function"==typeof TextDecoder&&TextDecoder.prototype.decode)return(new TextDecoder).decode(t.subarray(0,e));let r,s;const n=new Array(2*i);for(s=0,r=0;r4)n[s++]=65533,r+=a-1;else{for(e&=2===a?31:3===a?15:7;a>1&&r1?n[s++]=65533:e<65536?n[s++]=e:(e-=65536,n[s++]=55296|e>>10&1023,n[s++]=56320|1023&e)}}return((t,e)=>{if(e<65534&&t.subarray&&qt)return String.fromCharCode.apply(null,t.length===e?t:t.subarray(0,e));let i="";for(let r=0;r{(e=e||t.length)>t.length&&(e=t.length);let i=e-1;for(;i>=0&&128==(192&t[i]);)i--;return i<0||0===i?e:i+Yt[t[i]]>e?i:e};var Qt=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0};const $t=Object.prototype.toString,{Z_NO_FLUSH:Jt,Z_SYNC_FLUSH:te,Z_FULL_FLUSH:ee,Z_FINISH:ie,Z_OK:re,Z_STREAM_END:se,Z_DEFAULT_COMPRESSION:ne,Z_DEFAULT_STRATEGY:ae,Z_DEFLATED:oe}=W;function le(t){this.options=Ht({level:ne,method:oe,chunkSize:16384,windowBits:15,memLevel:8,strategy:ae},t||{});let e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Qt,this.strm.avail_out=0;let i=Vt.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(i!==re)throw new Error(H[i]);if(e.header&&Vt.deflateSetHeader(this.strm,e.header),e.dictionary){let t;if(t="string"==typeof e.dictionary?Zt(e.dictionary):"[object ArrayBuffer]"===$t.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,i=Vt.deflateSetDictionary(this.strm,t),i!==re)throw new Error(H[i]);this._dict_set=!0}}function he(t,e){const i=new le(e);if(i.push(t,!0),i.err)throw i.msg||H[i.err];return i.result}le.prototype.push=function(t,e){const i=this.strm,r=this.options.chunkSize;let s,n;if(this.ended)return!1;for(n=e===~~e?e:!0===e?ie:Jt,"string"==typeof t?i.input=Zt(t):"[object ArrayBuffer]"===$t.call(t)?i.input=new Uint8Array(t):i.input=t,i.next_in=0,i.avail_in=i.input.length;;)if(0===i.avail_out&&(i.output=new Uint8Array(r),i.next_out=0,i.avail_out=r),(n===te||n===ee)&&i.avail_out<=6)this.onData(i.output.subarray(0,i.next_out)),i.avail_out=0;else{if(s=Vt.deflate(i,n),s===se)return i.next_out>0&&this.onData(i.output.subarray(0,i.next_out)),s=Vt.deflateEnd(this.strm),this.onEnd(s),this.ended=!0,s===re;if(0!==i.avail_out){if(n>0&&i.next_out>0)this.onData(i.output.subarray(0,i.next_out)),i.avail_out=0;else if(0===i.avail_in)break}else this.onData(i.output)}return!0},le.prototype.onData=function(t){this.chunks.push(t)},le.prototype.onEnd=function(t){t===re&&(this.result=Wt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};var ce={Deflate:le,deflate:he,deflateRaw:function(t,e){return(e=e||{}).raw=!0,he(t,e)},gzip:function(t,e){return(e=e||{}).gzip=!0,he(t,e)},constants:W};const de=16209;var ue=function(t,e){let i,r,s,n,a,o,l,h,c,d,u,f,p,g,m,v,_,y,b,x,w,A,C,S;const M=t.state;i=t.next_in,C=t.input,r=i+(t.avail_in-5),s=t.next_out,S=t.output,n=s-(e-t.avail_out),a=s+(t.avail_out-257),o=M.dmax,l=M.wsize,h=M.whave,c=M.wnext,d=M.window,u=M.hold,f=M.bits,p=M.lencode,g=M.distcode,m=(1<>>24,u>>>=y,f-=y,y=_>>>16&255,0===y)S[s++]=65535&_;else{if(!(16&y)){if(64&y){if(32&y){M.mode=16191;break t}t.msg="invalid literal/length code",M.mode=de;break t}_=p[(65535&_)+(u&(1<>>=y,f-=y),f<15&&(u+=C[i++]<>>24,u>>>=y,f-=y,y=_>>>16&255,16&y){if(x=65535&_,y&=15,fo){t.msg="invalid distance too far back",M.mode=de;break t}if(u>>>=y,f-=y,y=s-n,x>y){if(y=x-y,y>h&&M.sane){t.msg="invalid distance too far back",M.mode=de;break t}if(w=0,A=d,0===c){if(w+=l-y,y2;)S[s++]=A[w++],S[s++]=A[w++],S[s++]=A[w++],b-=3;b&&(S[s++]=A[w++],b>1&&(S[s++]=A[w++]))}else{w=s-x;do{S[s++]=S[w++],S[s++]=S[w++],S[s++]=S[w++],b-=3}while(b>2);b&&(S[s++]=S[w++],b>1&&(S[s++]=S[w++]))}break}if(64&y){t.msg="invalid distance code",M.mode=de;break t}_=g[(65535&_)+(u&(1<>3,i-=b,f-=b<<3,u&=(1<{const l=o.bits;let h,c,d,u,f,p,g=0,m=0,v=0,_=0,y=0,b=0,x=0,w=0,A=0,C=0,S=null;const M=new Uint16Array(16),z=new Uint16Array(16);let T,E,L,F=null;for(g=0;g<=fe;g++)M[g]=0;for(m=0;m=1&&0===M[_];_--);if(y>_&&(y=_),0===_)return s[n++]=20971520,s[n++]=20971520,o.bits=1,0;for(v=1;v<_&&0===M[v];v++);for(y0&&(0===t||1!==_))return-1;for(z[1]=0,g=1;g852||2===t&&A>592)return 1;for(;;){T=g-x,a[m]+1=p?(E=F[a[m]-p],L=S[a[m]-p]):(E=96,L=0),h=1<>x)+c]=T<<24|E<<16|L}while(0!==c);for(h=1<>=1;if(0!==h?(C&=h-1,C+=h):C=0,m++,0==--M[g]){if(g===_)break;g=e[i+a[m]]}if(g>y&&(C&u)!==d){for(0===x&&(x=y),f+=v,b=g-x,w=1<852||2===t&&A>592)return 1;d=C&u,s[d]=y<<24|b<<16|f-n}}return 0!==C&&(s[f+C]=g-x<<24|64<<16),o.bits=y,0};const{Z_FINISH:ye,Z_BLOCK:be,Z_TREES:xe,Z_OK:we,Z_STREAM_END:Ae,Z_NEED_DICT:Ce,Z_STREAM_ERROR:Se,Z_DATA_ERROR:Me,Z_MEM_ERROR:ze,Z_BUF_ERROR:Te,Z_DEFLATED:Ee}=W,Le=16180,Fe=16190,Ie=16191,Oe=16192,De=16194,ke=16199,Re=16200,Pe=16206,Ue=16209,Be=t=>(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24);function Ne(){this.strm=null,this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}const Ge=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||e.mode16211?1:0},Ve=t=>{if(Ge(t))return Se;const e=t.state;return t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=Le,e.last=0,e.havedict=0,e.flags=-1,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new Int32Array(852),e.distcode=e.distdyn=new Int32Array(592),e.sane=1,e.back=-1,we},je=t=>{if(Ge(t))return Se;const e=t.state;return e.wsize=0,e.whave=0,e.wnext=0,Ve(t)},He=(t,e)=>{let i;if(Ge(t))return Se;const r=t.state;return e<0?(i=0,e=-e):(i=5+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?Se:(null!==r.window&&r.wbits!==e&&(r.window=null),r.wrap=i,r.wbits=e,je(t))},We=(t,e)=>{if(!t)return Se;const i=new Ne;t.state=i,i.strm=t,i.window=null,i.mode=Le;const r=He(t,e);return r!==we&&(t.state=null),r};let qe,Ye,Ze=!0;const Xe=t=>{if(Ze){qe=new Int32Array(512),Ye=new Int32Array(32);let e=0;for(;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(_e(1,t.lens,0,288,qe,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;_e(2,t.lens,0,32,Ye,0,t.work,{bits:5}),Ze=!1}t.lencode=qe,t.lenbits=9,t.distcode=Ye,t.distbits=5},Ke=(t,e,i,r)=>{let s;const n=t.state;return null===n.window&&(n.wsize=1<=n.wsize?(n.window.set(e.subarray(i-n.wsize,i),0),n.wnext=0,n.whave=n.wsize):(s=n.wsize-n.wnext,s>r&&(s=r),n.window.set(e.subarray(i-r,i-r+s),n.wnext),(r-=s)?(n.window.set(e.subarray(i-r,i),0),n.wnext=r,n.whave=n.wsize):(n.wnext+=s,n.wnext===n.wsize&&(n.wnext=0),n.whaveWe(t,15),inflateInit2:We,inflate:(t,e)=>{let i,r,s,n,a,o,l,h,c,d,u,f,p,g,m,v,_,y,b,x,w,A,C=0;const S=new Uint8Array(4);let M,z;const T=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(Ge(t)||!t.output||!t.input&&0!==t.avail_in)return Se;i=t.state,i.mode===Ie&&(i.mode=Oe),a=t.next_out,s=t.output,l=t.avail_out,n=t.next_in,r=t.input,o=t.avail_in,h=i.hold,c=i.bits,d=o,u=l,A=we;t:for(;;)switch(i.mode){case Le:if(0===i.wrap){i.mode=Oe;break}for(;c<16;){if(0===o)break t;o--,h+=r[n++]<>>8&255,i.check=j(i.check,S,2,0),h=0,c=0,i.mode=16181;break}if(i.head&&(i.head.done=!1),!(1&i.wrap)||(((255&h)<<8)+(h>>8))%31){t.msg="incorrect header check",i.mode=Ue;break}if((15&h)!==Ee){t.msg="unknown compression method",i.mode=Ue;break}if(h>>>=4,c-=4,w=8+(15&h),0===i.wbits&&(i.wbits=w),w>15||w>i.wbits){t.msg="invalid window size",i.mode=Ue;break}i.dmax=1<>8&1),512&i.flags&&4&i.wrap&&(S[0]=255&h,S[1]=h>>>8&255,i.check=j(i.check,S,2,0)),h=0,c=0,i.mode=16182;case 16182:for(;c<32;){if(0===o)break t;o--,h+=r[n++]<>>8&255,S[2]=h>>>16&255,S[3]=h>>>24&255,i.check=j(i.check,S,4,0)),h=0,c=0,i.mode=16183;case 16183:for(;c<16;){if(0===o)break t;o--,h+=r[n++]<>8),512&i.flags&&4&i.wrap&&(S[0]=255&h,S[1]=h>>>8&255,i.check=j(i.check,S,2,0)),h=0,c=0,i.mode=16184;case 16184:if(1024&i.flags){for(;c<16;){if(0===o)break t;o--,h+=r[n++]<>>8&255,i.check=j(i.check,S,2,0)),h=0,c=0}else i.head&&(i.head.extra=null);i.mode=16185;case 16185:if(1024&i.flags&&(f=i.length,f>o&&(f=o),f&&(i.head&&(w=i.head.extra_len-i.length,i.head.extra||(i.head.extra=new Uint8Array(i.head.extra_len)),i.head.extra.set(r.subarray(n,n+f),w)),512&i.flags&&4&i.wrap&&(i.check=j(i.check,r,f,n)),o-=f,n+=f,i.length-=f),i.length))break t;i.length=0,i.mode=16186;case 16186:if(2048&i.flags){if(0===o)break t;f=0;do{w=r[n+f++],i.head&&w&&i.length<65536&&(i.head.name+=String.fromCharCode(w))}while(w&&f>9&1,i.head.done=!0),t.adler=i.check=0,i.mode=Ie;break;case 16189:for(;c<32;){if(0===o)break t;o--,h+=r[n++]<>>=7&c,c-=7&c,i.mode=Pe;break}for(;c<3;){if(0===o)break t;o--,h+=r[n++]<>>=1,c-=1,3&h){case 0:i.mode=16193;break;case 1:if(Xe(i),i.mode=ke,e===xe){h>>>=2,c-=2;break t}break;case 2:i.mode=16196;break;case 3:t.msg="invalid block type",i.mode=Ue}h>>>=2,c-=2;break;case 16193:for(h>>>=7&c,c-=7&c;c<32;){if(0===o)break t;o--,h+=r[n++]<>>16^65535)){t.msg="invalid stored block lengths",i.mode=Ue;break}if(i.length=65535&h,h=0,c=0,i.mode=De,e===xe)break t;case De:i.mode=16195;case 16195:if(f=i.length,f){if(f>o&&(f=o),f>l&&(f=l),0===f)break t;s.set(r.subarray(n,n+f),a),o-=f,n+=f,l-=f,a+=f,i.length-=f;break}i.mode=Ie;break;case 16196:for(;c<14;){if(0===o)break t;o--,h+=r[n++]<>>=5,c-=5,i.ndist=1+(31&h),h>>>=5,c-=5,i.ncode=4+(15&h),h>>>=4,c-=4,i.nlen>286||i.ndist>30){t.msg="too many length or distance symbols",i.mode=Ue;break}i.have=0,i.mode=16197;case 16197:for(;i.have>>=3,c-=3}for(;i.have<19;)i.lens[T[i.have++]]=0;if(i.lencode=i.lendyn,i.lenbits=7,M={bits:i.lenbits},A=_e(0,i.lens,0,19,i.lencode,0,i.work,M),i.lenbits=M.bits,A){t.msg="invalid code lengths set",i.mode=Ue;break}i.have=0,i.mode=16198;case 16198:for(;i.have>>24,v=C>>>16&255,_=65535&C,!(m<=c);){if(0===o)break t;o--,h+=r[n++]<>>=m,c-=m,i.lens[i.have++]=_;else{if(16===_){for(z=m+2;c>>=m,c-=m,0===i.have){t.msg="invalid bit length repeat",i.mode=Ue;break}w=i.lens[i.have-1],f=3+(3&h),h>>>=2,c-=2}else if(17===_){for(z=m+3;c>>=m,c-=m,w=0,f=3+(7&h),h>>>=3,c-=3}else{for(z=m+7;c>>=m,c-=m,w=0,f=11+(127&h),h>>>=7,c-=7}if(i.have+f>i.nlen+i.ndist){t.msg="invalid bit length repeat",i.mode=Ue;break}for(;f--;)i.lens[i.have++]=w}}if(i.mode===Ue)break;if(0===i.lens[256]){t.msg="invalid code -- missing end-of-block",i.mode=Ue;break}if(i.lenbits=9,M={bits:i.lenbits},A=_e(1,i.lens,0,i.nlen,i.lencode,0,i.work,M),i.lenbits=M.bits,A){t.msg="invalid literal/lengths set",i.mode=Ue;break}if(i.distbits=6,i.distcode=i.distdyn,M={bits:i.distbits},A=_e(2,i.lens,i.nlen,i.ndist,i.distcode,0,i.work,M),i.distbits=M.bits,A){t.msg="invalid distances set",i.mode=Ue;break}if(i.mode=ke,e===xe)break t;case ke:i.mode=Re;case Re:if(o>=6&&l>=258){t.next_out=a,t.avail_out=l,t.next_in=n,t.avail_in=o,i.hold=h,i.bits=c,ue(t,u),a=t.next_out,s=t.output,l=t.avail_out,n=t.next_in,r=t.input,o=t.avail_in,h=i.hold,c=i.bits,i.mode===Ie&&(i.back=-1);break}for(i.back=0;C=i.lencode[h&(1<>>24,v=C>>>16&255,_=65535&C,!(m<=c);){if(0===o)break t;o--,h+=r[n++]<>y)],m=C>>>24,v=C>>>16&255,_=65535&C,!(y+m<=c);){if(0===o)break t;o--,h+=r[n++]<>>=y,c-=y,i.back+=y}if(h>>>=m,c-=m,i.back+=m,i.length=_,0===v){i.mode=16205;break}if(32&v){i.back=-1,i.mode=Ie;break}if(64&v){t.msg="invalid literal/length code",i.mode=Ue;break}i.extra=15&v,i.mode=16201;case 16201:if(i.extra){for(z=i.extra;c>>=i.extra,c-=i.extra,i.back+=i.extra}i.was=i.length,i.mode=16202;case 16202:for(;C=i.distcode[h&(1<>>24,v=C>>>16&255,_=65535&C,!(m<=c);){if(0===o)break t;o--,h+=r[n++]<>y)],m=C>>>24,v=C>>>16&255,_=65535&C,!(y+m<=c);){if(0===o)break t;o--,h+=r[n++]<>>=y,c-=y,i.back+=y}if(h>>>=m,c-=m,i.back+=m,64&v){t.msg="invalid distance code",i.mode=Ue;break}i.offset=_,i.extra=15&v,i.mode=16203;case 16203:if(i.extra){for(z=i.extra;c>>=i.extra,c-=i.extra,i.back+=i.extra}if(i.offset>i.dmax){t.msg="invalid distance too far back",i.mode=Ue;break}i.mode=16204;case 16204:if(0===l)break t;if(f=u-l,i.offset>f){if(f=i.offset-f,f>i.whave&&i.sane){t.msg="invalid distance too far back",i.mode=Ue;break}f>i.wnext?(f-=i.wnext,p=i.wsize-f):p=i.wnext-f,f>i.length&&(f=i.length),g=i.window}else g=s,p=a-i.offset,f=i.length;f>l&&(f=l),l-=f,i.length-=f;do{s[a++]=g[p++]}while(--f);0===i.length&&(i.mode=Re);break;case 16205:if(0===l)break t;s[a++]=i.length,l--,i.mode=Re;break;case Pe:if(i.wrap){for(;c<32;){if(0===o)break t;o--,h|=r[n++]<{if(Ge(t))return Se;let e=t.state;return e.window&&(e.window=null),t.state=null,we},inflateGetHeader:(t,e)=>{if(Ge(t))return Se;const i=t.state;return 2&i.wrap?(i.head=e,e.done=!1,we):Se},inflateSetDictionary:(t,e)=>{const i=e.length;let r,s,n;return Ge(t)?Se:(r=t.state,0!==r.wrap&&r.mode!==Fe?Se:r.mode===Fe&&(s=1,s=G(s,e,i,0),s!==r.check)?Me:(n=Ke(t,e,i,i),n?(r.mode=16210,ze):(r.havedict=1,we)))},inflateInfo:"pako inflate (from Nodeca project)"};var $e=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1};const Je=Object.prototype.toString,{Z_NO_FLUSH:ti,Z_FINISH:ei,Z_OK:ii,Z_STREAM_END:ri,Z_NEED_DICT:si,Z_STREAM_ERROR:ni,Z_DATA_ERROR:ai,Z_MEM_ERROR:oi}=W;function li(t){this.options=Ht({chunkSize:65536,windowBits:15,to:""},t||{});const e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&(15&e.windowBits||(e.windowBits|=15)),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Qt,this.strm.avail_out=0;let i=Qe.inflateInit2(this.strm,e.windowBits);if(i!==ii)throw new Error(H[i]);if(this.header=new $e,Qe.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=Zt(e.dictionary):"[object ArrayBuffer]"===Je.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(i=Qe.inflateSetDictionary(this.strm,e.dictionary),i!==ii)))throw new Error(H[i])}function hi(t,e){const i=new li(e);if(i.push(t),i.err)throw i.msg||H[i.err];return i.result}li.prototype.push=function(t,e){const i=this.strm,r=this.options.chunkSize,s=this.options.dictionary;let n,a,o;if(this.ended)return!1;for(a=e===~~e?e:!0===e?ei:ti,"[object ArrayBuffer]"===Je.call(t)?i.input=new Uint8Array(t):i.input=t,i.next_in=0,i.avail_in=i.input.length;;){for(0===i.avail_out&&(i.output=new Uint8Array(r),i.next_out=0,i.avail_out=r),n=Qe.inflate(i,a),n===si&&s&&(n=Qe.inflateSetDictionary(i,s),n===ii?n=Qe.inflate(i,a):n===ai&&(n=si));i.avail_in>0&&n===ri&&i.state.wrap>0&&0!==t[i.next_in];)Qe.inflateReset(i),n=Qe.inflate(i,a);switch(n){case ni:case ai:case si:case oi:return this.onEnd(n),this.ended=!0,!1}if(o=i.avail_out,i.next_out&&(0===i.avail_out||n===ri))if("string"===this.options.to){let t=Kt(i.output,i.next_out),e=i.next_out-t,s=Xt(i.output,t);i.next_out=e,i.avail_out=r-e,e&&i.output.set(i.output.subarray(t,t+e),0),this.onData(s)}else this.onData(i.output.length===i.next_out?i.output:i.output.subarray(0,i.next_out));if(n!==ii||0!==o){if(n===ri)return n=Qe.inflateEnd(this.strm),this.onEnd(n),this.ended=!0,!0;if(0===i.avail_in)break}}return!0},li.prototype.onData=function(t){this.chunks.push(t)},li.prototype.onEnd=function(t){t===ii&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=Wt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};var ci={Inflate:li,inflate:hi,inflateRaw:function(t,e){return(e=e||{}).raw=!0,hi(t,e)},ungzip:hi,constants:W};const{Deflate:di,deflate:ui,deflateRaw:fi,gzip:pi}=ce,{Inflate:gi,inflate:mi,inflateRaw:vi,ungzip:_i}=ci;var yi=di,bi=ui,xi=fi,wi=pi,Ai=gi,Ci=mi,Si=vi,Mi=_i,zi=W,Ti={Deflate:di,deflate:ui,deflateRaw:fi,gzip:pi,Inflate:gi,inflate:mi,inflateRaw:vi,ungzip:_i,constants:W}}},__webpack_module_cache__={};function __webpack_require__(t){var e=__webpack_module_cache__[t];if(void 0!==e)return e.exports;var i=__webpack_module_cache__[t]={exports:{}};return __webpack_modules__[t].call(i.exports,i,i.exports,__webpack_require__),i.exports}__webpack_require__.d=(t,e)=>{for(var i in e)__webpack_require__.o(e,i)&&!__webpack_require__.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})},__webpack_require__.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),__webpack_require__.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},__webpack_require__(421),__webpack_require__(185);var __webpack_exports__=__webpack_require__(307);return __webpack_exports__})())); \ No newline at end of file diff --git a/assets/variant_effect_inspection_tool/3Dmol-min.js.LICENSE.txt b/assets/variant_effect_inspection_tool/3Dmol-min.js.LICENSE.txt new file mode 100644 index 0000000..1b2a169 --- /dev/null +++ b/assets/variant_effect_inspection_tool/3Dmol-min.js.LICENSE.txt @@ -0,0 +1,90 @@ +3Dmol.js incorporates code from GLmol, Three.js, and jQuery and +is licensed under a BSD-3-Clause license. + +* 3Dmol.js + Copyright (c) 2014, University of Pittsburgh and contributors + 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. + +* GLmol + GLmol - Molecular Viewer on WebGL/Javascript (0.47) + (C) Copyright 2011-2012, biochem_fan + License: dual license of MIT or LGPL3 + + Contributors: + Robert Hanson for parseXYZ, deferred instantiation + + + +* Three.js + https://github.com/mrdoob/three.js + + Copyright (c) 2010-2012 three.js Authors. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +* jQuery + http://jquery.org/ + + Copyright (c) 2011 John Resig + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/assets/variant_effect_inspection_tool/viewer_template.html b/assets/variant_effect_inspection_tool/viewer_template.html new file mode 100644 index 0000000..c1e4f5a --- /dev/null +++ b/assets/variant_effect_inspection_tool/viewer_template.html @@ -0,0 +1,907 @@ + + + + + + + nf-core/deepmutscan Β· variant effect inspection tool Β· __SAMPLE__ + + + +
+ + variant effect inspection tool + + +
+ +
+ + +
+ + +
+ +
+
+
+
+ + + + + diff --git a/conf/base.config b/conf/base.config index 9aa66a0..6dd4679 100644 --- a/conf/base.config +++ b/conf/base.config @@ -1,6 +1,6 @@ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - nf-core/dmscore Nextflow base config file + nf-core/deepmutscan Nextflow base config file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A 'blank slate' config file, appropriate for general use on most high performance compute environments. Assumes that all software is installed and available on @@ -9,22 +9,25 @@ */ process { - - // TODO nf-core: Check the defaults for all processes cpus = { 1 * task.attempt } memory = { 6.GB * task.attempt } time = { 4.h * task.attempt } - errorStrategy = { task.exitStatus in ((130..145) + 104) ? 'retry' : 'finish' } + errorStrategy = { task.exitStatus in ((130..145) + 104 + (175..177)) ? 'retry' : 'finish' } maxRetries = 1 maxErrors = '-1' + // Disable 'noclobber' and pre-clean versions.yml so modules can overwrite it + beforeScript = ''' + set +o noclobber + rm -f versions.yml || true + ''' + // Process-specific resource requirements // NOTE - Please try and reuse the labels below as much as possible. // These labels are used and recognised by default in DSL2 files hosted on nf-core/modules. // If possible, it would be nice to keep the same label naming convention when // adding in your local modules too. - // TODO nf-core: Customise requirements for specific processes. // See https://www.nextflow.io/docs/latest/config.html#config-process-selectors withLabel:process_single { cpus = { 1 } diff --git a/conf/containers_conda_lock_files_amd64.config b/conf/containers_conda_lock_files_amd64.config new file mode 100644 index 0000000..41e234d --- /dev/null +++ b/conf/containers_conda_lock_files_amd64.config @@ -0,0 +1,2 @@ +process { withName: 'FASTQC' { container = 'modules/nf-core/fastqc/.conda-lock/linux_amd64-bd-5cb1a2fa2f18c7c2_1.txt' } } +process { withName: 'MULTIQC' { container = 'modules/nf-core/multiqc/.conda-lock/linux_amd64-bd-c17fb751507e9dfc_1.txt' } } diff --git a/conf/containers_conda_lock_files_arm64.config b/conf/containers_conda_lock_files_arm64.config new file mode 100644 index 0000000..5b5b9aa --- /dev/null +++ b/conf/containers_conda_lock_files_arm64.config @@ -0,0 +1,2 @@ +process { withName: 'FASTQC' { container = 'modules/nf-core/fastqc/.conda-lock/linux_arm64-bd-e455e32f745abe68_1.txt' } } +process { withName: 'MULTIQC' { container = 'modules/nf-core/multiqc/.conda-lock/linux_arm64-bd-5c84a5000a226ab5_1.txt' } } diff --git a/conf/containers_docker_amd64.config b/conf/containers_docker_amd64.config new file mode 100644 index 0000000..a66e3d1 --- /dev/null +++ b/conf/containers_docker_amd64.config @@ -0,0 +1,2 @@ +process { withName: 'FASTQC' { container = 'community.wave.seqera.io/library/fastqc:0.12.1--5cb1a2fa2f18c7c2' } } +process { withName: 'MULTIQC' { container = 'community.wave.seqera.io/library/multiqc:1.35--c17fb751507e9dfc' } } diff --git a/conf/containers_docker_arm64.config b/conf/containers_docker_arm64.config new file mode 100644 index 0000000..215f686 --- /dev/null +++ b/conf/containers_docker_arm64.config @@ -0,0 +1,2 @@ +process { withName: 'FASTQC' { container = 'community.wave.seqera.io/library/fastqc:0.12.1--e455e32f745abe68' } } +process { withName: 'MULTIQC' { container = 'community.wave.seqera.io/library/multiqc:1.35--5c84a5000a226ab5' } } diff --git a/conf/containers_singularity_https_amd64.config b/conf/containers_singularity_https_amd64.config new file mode 100644 index 0000000..2fb11a3 --- /dev/null +++ b/conf/containers_singularity_https_amd64.config @@ -0,0 +1,2 @@ +process { withName: 'FASTQC' { container = 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f2/f20b021476d1d87658820f971ebecc1e8cdbde0f338eb0d9cea2b0a8fc54a54b/data' } } +process { withName: 'MULTIQC' { container = 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/c8/c8e346f4f6080eadf1253505e6ff09ef004454fc18e8d672006fd7b222cc412e/data' } } diff --git a/conf/containers_singularity_https_arm64.config b/conf/containers_singularity_https_arm64.config new file mode 100644 index 0000000..5fa5b4f --- /dev/null +++ b/conf/containers_singularity_https_arm64.config @@ -0,0 +1,2 @@ +process { withName: 'FASTQC' { container = 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/46/46daf2dad0169afd2ae047c3e50ed3776259f664bf07e5e06b045dc23449e994/data' } } +process { withName: 'MULTIQC' { container = 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/e4/e48aa28aebc881254a499b24c3e1ce77b8df1b85a5432699ed6f72eb17ac7fb5/data' } } diff --git a/conf/containers_singularity_oras_amd64.config b/conf/containers_singularity_oras_amd64.config new file mode 100644 index 0000000..b334375 --- /dev/null +++ b/conf/containers_singularity_oras_amd64.config @@ -0,0 +1,2 @@ +process { withName: 'FASTQC' { container = 'oras://community.wave.seqera.io/library/fastqc:0.12.1--5c4bd442468d75dd' } } +process { withName: 'MULTIQC' { container = 'oras://community.wave.seqera.io/library/multiqc:1.35--c680f2aea25ccec2' } } diff --git a/conf/containers_singularity_oras_arm64.config b/conf/containers_singularity_oras_arm64.config new file mode 100644 index 0000000..661c656 --- /dev/null +++ b/conf/containers_singularity_oras_arm64.config @@ -0,0 +1,2 @@ +process { withName: 'FASTQC' { container = 'oras://community.wave.seqera.io/library/fastqc:0.12.1--127a87fc06499035' } } +process { withName: 'MULTIQC' { container = 'oras://community.wave.seqera.io/library/multiqc:1.35--c0468833d65b2f81' } } diff --git a/conf/modules.config b/conf/modules.config index d203d2b..7fb9b4b 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -7,6 +7,9 @@ ext.args2 = Second set of arguments appended to command in module (multi-tool modules). ext.args3 = Third set of arguments appended to command in module (multi-tool modules). ext.prefix = File name prefix for output files. + + Output layout: Outputs are published directly into functional directories + under `${outdir}/...` without sample-specific or reference parent folders. ---------------------------------------------------------------------------------------- */ @@ -20,6 +23,12 @@ process { withName: FASTQC { ext.args = '--quiet' + containerOptions = '' + publishDir = [ + path: { "${params.outdir}/fastqc" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + ] } withName: 'MULTIQC' { @@ -31,4 +40,236 @@ process { ] } + // --- Reference-derived, merged into intermediate_files --- + + withName: 'BWA_INDEX' { + cpus = 1 + memory = 1.GB + time = 30.min + publishDir = [ + path: "${params.outdir}/intermediate_files", + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'DMSANALYSIS_POSSIBLE_MUTATIONS' { + publishDir = [ + path: "${params.outdir}/intermediate_files", + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'DMSANALYSIS_AASEQ' { + publishDir = [ + path: "${params.outdir}/intermediate_files", + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + // --- Alignment / filtering / merging --- + + withName: 'BWA_MEM' { + publishDir = [ + path: { "${params.outdir}/intermediate_files/bam_files/bwa/mem" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'BAMFILTER_DMS' { + publishDir = [ + path: { "${params.outdir}/intermediate_files/bam_files/filtered" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'PREMERGE' { + publishDir = [ + path: { "${params.outdir}/intermediate_files/bam_files/premerged" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'SAMTOOLS_SORT' { + ext.prefix = { "${meta.id}.sorted" } + publishDir = [ + path: { "${params.outdir}/intermediate_files/bam_files/sorted" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'SAMTOOLS_INDEX' { + publishDir = [ + path: { "${params.outdir}/intermediate_files/bam_files/sorted" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + // --- Variant counting / processing / error correction (per-replicate meta.id subfolder) --- + + withName: 'VARIANTCOUNTING' { + publishDir = [ + path: { "${params.outdir}/intermediate_files/variant_counts" }, + mode: 'copy', + overwrite: false, + saveAs: { filename -> + if (filename == 'versions.yml') return null + "${meta.id}/${filename}" + } + ] + } + + withName: 'DMSANALYSIS_PROCESS_VARIANT_COUNTS' { + publishDir = [ + path: { "${params.outdir}/intermediate_files/processed_variant_counts" }, + mode: 'copy', + saveAs: { filename -> + if (filename == 'versions.yml') return null + "${meta.id}/${filename}" + } + ] + } + + withName: 'DMSANALYSIS_ERROR_CORRECTION_.*' { + publishDir = [ + path: { "${params.outdir}/intermediate_files/processed_variant_counts" }, + mode: 'copy', + saveAs: { filename -> + if (filename == 'versions.yml') return null + "${meta.id}/${filename}" + } + ] + } + + // Per-sample visualisations file under library_QC//. The negative lookahead excludes the two + // run-level reports, which have no per-sample `meta.id` to file under - Nextflow ACCUMULATES + // publishDir across matching selectors rather than letting a later `withName` override it, so if this + // selector also matched them its `${meta.id}` would still be evaluated (throwing for the EC report, + // and mis-filing the summary report under library_QC/run/). + withName: /.*VISUALIZATION_(?!ERROR_CORRECTION_REPORT|SUMMARY_REPORT).*/ { + publishDir = [ + path: { "${params.outdir}/library_QC" }, + mode: 'copy', + overwrite: false, + saveAs: { fn -> + if (fn == 'versions.yml') return null + "${meta.id}/${fn}" + } + ] + } + + // Run-level, so it has no meta.id to file itself under - published flat under library_QC. + withName: 'VISUALIZATION_ERROR_CORRECTION_REPORT' { + publishDir = [ + path: { "${params.outdir}/library_QC" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + // The all-in-one report is the flagship output - published at the results root, not buried. + withName: 'VISUALIZATION_SUMMARY_REPORT' { + publishDir = [ + path: { "${params.outdir}" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + // --- Fitness and Experimental Design --- + + withName: 'COUNTS_TO_FITNESS' { + publishDir = [ + path: { "${params.outdir}/fitness/DiMSum_results/single_rep_counts" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'MERGE_COUNTS' { + publishDir = [ + path: { "${params.outdir}/fitness" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'EXPDESIGN_FITNESS' { + publishDir = [ + path: "${params.outdir}/fitness", + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'FIND_SYNONYMOUS_MUTATION' { + publishDir = [ + path: { "${params.outdir}/fitness" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'FITNESS_CALCULATION' { + publishDir = [ + path: { "${params.outdir}/fitness/default_results" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'FITNESS_QC' { + publishDir = [ + path: { "${params.outdir}/fitness/default_results" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'FITNESS_HEATMAP' { + publishDir = [ + path: { "${params.outdir}/fitness/default_results" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'RUN_DIMSUM' { + publishDir = [ + path: { "${params.outdir}/fitness/DiMSum_results" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'RUN_MUTSCAN' { + publishDir = [ + path: { "${params.outdir}/fitness/mutscan_results" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'VISUALIZATION_SUMMARY_REPORT' { + publishDir = [ + path: { "${params.outdir}" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } + + withName: 'VARIANT_EFFECT_INSPECTION_TOOL' { + publishDir = [ + path: { "${params.outdir}/variant_effect_inspection_tool" }, + mode: 'copy', + saveAs: { filename -> filename == 'versions.yml' ? null : filename } + ] + } } diff --git a/conf/test.config b/conf/test.config index 8c566af..bc4667c 100644 --- a/conf/test.config +++ b/conf/test.config @@ -5,7 +5,7 @@ Defines input files and everything required to run a fast and simple pipeline test. Use as follows: - nextflow run nf-core/dmscore -profile test, --outdir + nextflow run nf-core/deepmutscan -profile test, --outdir ---------------------------------------------------------------------------------------- */ @@ -13,7 +13,7 @@ process { resourceLimits = [ cpus: 4, - memory: '15.GB', + memory: '8.GB', time: '1.h' ] } @@ -23,8 +23,17 @@ params { config_profile_description = 'Minimal test dataset to check pipeline function' // Input data - // TODO nf-core: Specify the paths to your test data on nf-core/test-datasets - // TODO nf-core: Give any required params for the test so that command line flags are not needed - input = params.pipelines_testdata_base_path + 'viralrecon/samplesheet/samplesheet_test_illumina_amplicon.csv'// Genome references - genome = 'R64-1-1' + input = params.pipelines_testdata_base_path + 'deepmutscan/samplesheet/GID1A_test.csv' + fasta = params.pipelines_testdata_base_path + 'deepmutscan/testdata/GID1A.fasta' + reading_frame = '352-1383' + min_counts = 2 + mutagenesis_type = 'nnk_nns' + run_seqdepth = true + fitness = true + mutscan = true + dimsum = true + + // Precomputed wildtype structure (AlphaFold DB model, GID1A / UniProt Q9MAA7, CC-BY-4.0) so the + // interactive variant effect inspection tool runs end-to-end without structure prediction. + pdb = params.pipelines_testdata_base_path + 'deepmutscan/testdata/GID1A_AFDB.pdb' } diff --git a/conf/test_full.config b/conf/test_full.config index b57a75c..ae592d4 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -5,7 +5,7 @@ Defines input files and everything required to run a full size pipeline test. Use as follows: - nextflow run nf-core/dmscore -profile test_full, --outdir + nextflow run nf-core/deepmutscan -profile test_full, --outdir ---------------------------------------------------------------------------------------- */ @@ -15,10 +15,18 @@ params { config_profile_description = 'Full test dataset to check pipeline function' // Input data for full size test - // TODO nf-core: Specify the paths to your full test data ( on nf-core/test-datasets or directly in repositories, e.g. SRA) - // TODO nf-core: Give any required params for the test so that command line flags are not needed - input = params.pipelines_testdata_base_path + 'viralrecon/samplesheet/samplesheet_full_illumina_amplicon.csv' + // GluePCA GID1A-GAI shotgun DMS (ENA PRJEB110196): 3 input and 3 output (2500 uM GA3) replicates, + // 2x150 bp, ~480M read pairs in total. The samplesheet links the FASTQs on ENA. + // The minimal `test` profile uses 50k read-pair subsamples of the same libraries. + input = params.pipelines_testdata_base_path + 'deepmutscan/samplesheet/GID1A_full.csv' + fasta = params.pipelines_testdata_base_path + 'deepmutscan/testdata/GID1A.fasta' + reading_frame = '352-1383' + mutagenesis_type = 'nnk_nns' + fitness = true + mutscan = true + dimsum = true - // Genome references - genome = 'R64-1-1' + // Precomputed wildtype structure (AlphaFold DB model, GID1A / UniProt Q9MAA7, CC-BY-4.0) so the + // interactive variant effect inspection tool runs end-to-end without structure prediction. + pdb = params.pipelines_testdata_base_path + 'deepmutscan/testdata/GID1A_AFDB.pdb' } diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..639617e --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,181 @@ +--- +title: Contributing +markdownPlugin: checklist +--- + +# `nf-core/deepmutscan`: Contributing guidelines + +Hi there! +Thanks for taking an interest in improving nf-core/deepmutscan. + +This page describes the recommended nf-core way to contribute to both nf-core/deepmutscan and nf-core pipelines in general, including: + +- [General contribution guidelines](#general-contribution-guidelines): common procedures or guides across all nf-core pipelines. +- [Pipeline-specific contribution guidelines](#pipeline-specific-contribution-guidelines): procedures or guides specific to the development conventions of nf-core/deepmutscan. + +> [!NOTE] +> If you need help using or modifying nf-core/deepmutscan, ask on the nf-core Slack [#deepmutscan](https://nfcore.slack.com/channels/deepmutscan) channel ([join our Slack here](https://nf-co.re/join/slack)). + +## General contribution guidelines + +### Contribution quick start + +To contribute code to any nf-core pipeline: + +- [ ] Ensure you have Nextflow, nf-core tools, and nf-test installed. See the [nf-core/tools repository](https://github.com/nf-core/tools) for instructions. +- [ ] Check whether a GitHub [issue](https://github.com/nf-core/deepmutscan/issues) about your idea already exists. If an issue does not exist, create one so that others are aware you are working on it. +- [ ] [Fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) the [nf-core/deepmutscan repository](https://github.com/nf-core/deepmutscan) to your GitHub account. +- [ ] Create a branch on your forked repository and make your changes following [pipeline conventions](#pipeline-contribution-conventions) (if applicable). +- [ ] To fix major bugs, name your branch `patch` and follow the [patch release](#patch-release) process. +- [ ] Update relevant documentation within the `docs/` folder, use nf-core/tools to update `nextflow_schema.json`, and update `CITATIONS.md`. +- [ ] Run and/or update tests. See [Testing](#testing) for more information. +- [ ] [Lint](#lint-tests) your code with nf-core/tools. +- [ ] Submit a pull request (PR) against the `dev` branch and request a review. + +If you are not used to this workflow with Git, see the [GitHub documentation](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests) or [Git resources](https://try.github.io/) for more information. + +## Use of AI and LLMs + +The nf-core stance on the use of AI and LLMs is that humans are still ultimately responsible for their submitted code, regardless of the tools they use. + +If you’re using AI tools, try to stick by these guidelines: + +- Keep PRs as small and focussed as possible +- Avoid any unnecessary changes, such as moving or refactoring code (unless that is the explicit intention of the PR) +- Review all generated code yourself before opening a PR, and ensure that you understand it +- Engage with the community review process and expect to make revisions + +For more detail, see the the [blog post](https://nf-co.re/blog/2026/statement-on-ai) for a statement from the nf-core/core team. + +### Getting help + +For further information and help, see the [nf-core/deepmutscan documentation](https://nf-co.re/deepmutscan/usage) or ask on the nf-core [#deepmutscan](https://nfcore.slack.com/channels/deepmutscan) Slack channel ([join our Slack here](https://nf-co.re/join/slack)). + +### GitHub Codespaces + +You can contribute to nf-core/deepmutscan without installing a local development environment on your machine by using [GitHub Codespaces](https://github.com/codespaces). + +[GitHub Codespaces](https://github.com/codespaces) is an online developer environment that runs in your browser, complete with VS Code and a terminal. +Most nf-core repositories include a devcontainer configuration, which creates a GitHub Codespaces environment specifically for Nextflow development. +The environment includes pre-installed nf-core tools, Nextflow, and a few other helpful utilities via a Docker container. + +To get started, open the repository in [Codespaces](https://github.com/nf-core/deepmutscan/codespaces). + +### Testing + +Once you have made your changes, run the pipeline with nf-test to test them locally. +For additional information, use the `--verbose` flag to view the Nextflow console log output. + +```bash +nf-test test --tag test --profile +docker --verbose +``` + +If you have added new functionality, ensure you update the test assertions in the `.nf.test` files in the `tests/` directory. +Update the snapshots with the following command: + +```bash +nf-test test --tag test --profile +docker --verbose --update-snapshots +``` + +When you create a pull request with changes, GitHub Actions will run automatic tests. +Pull requests are typically reviewed when these tests are passing. + +Two types of tests are typically run: + +#### Lint tests + +nf-core has a [set of guidelines](https://nf-co.re/docs/specifications/overview) which all pipelines must follow. +To enforce these, run linting with nf-core/tools: + +```bash +nf-core pipelines lint +``` + +If you encounter failures or warnings, follow the linked documentation printed to screen. +For more information about linting tests, see [nf-core/tools API documentation](https://nf-co.re/docs/nf-core-tools/api_reference/latest/pipeline_lint_tests/actions_awsfulltest). + +#### Pipeline tests + +Each nf-core pipeline should be set up with a minimal set of test data. +GitHub Actions runs the pipeline on this data to ensure it runs through and exits successfully. +If there are any failures then the automated tests fail. +These tests are run with the latest available version of Nextflow and the minimum required version specified in the pipeline code. + +### Patch release + +> [!WARNING] +> Only in the unlikely event of a release that contains a critical bug. + +- [ ] Create a new branch `patch` on your fork based on `upstream/main` or `upstream/master`. +- [ ] Fix the bug and use nf-core/tools to bump the version to the next semantic version, for example, `1.2.3` β†’ `1.2.4`. +- [ ] Open a Pull Request from `patch` directly to `main`/`master` with the changes. + +### Pipeline contribution conventions + +nf-core semi-standardises how you write code and other contributions to make the nf-core/deepmutscan code and processing logic more understandable for new contributors and to ensure quality. + +#### Add a new pipeline step + +To contribute a new step to the pipeline, follow the general nf-core coding procedure. +Please also refer to the [pipeline-specific contribution guidelines](#pipeline-specific-contribution-guidelines): + +- [ ] Define the corresponding [input channel](#channel-naming-schemes) into your new process from the expected previous process channel. +- [ ] Install a module with nf-core/tools, or write a local module (see [default processes resource requirements](#default-processes-resource-requirements)), and add it to the target `.nf`. +- [ ] Define the output channel if needed. Mix the version output channel into `ch_versions` and relevant files into `ch_multiqc`. +- [ ] Add new or updated parameters to `nextflow.config` with a [default value](#default-parameter-values). +- [ ] Add new or updated parameters and relevant help text to `nextflow_schema.json` with [nf-core/tools](#default-parameter-values). +- [ ] Add validation for relevant parameters to the pipeline utilisation section of `utils_nfcore_\_pipeline/main.nf` subworkflow. +- [ ] Perform local tests to validate that the new code works as expected. + - [ ] If applicable, add a new test in the `tests` directory. +- [ ] Update `usage.md`, `output.md`, and `citation.md` as appropriate. +- [ ] [Lint](lint) the code with nf-core/tools. +- [ ] Update any diagrams or pipeline images as necessary. +- [ ] Update MultiQC config `assets/multiqc_config.yml` so relevant suffixes, file name cleanup, and module plots are in the appropriate order. +- [ ] If applicable, create a [MultiQC](https://seqera.io/multiqc/) module. +- [ ] Add a description of the output files and, if relevant, images from the MultiQC report to `docs/output.md`. + +To update the minimum required Nextflow version, see the [Nextflow version bumping](#nextflow-version-bumping) section below. For more information about pipeline contributions, see [pipeline-specific contribution guidelines](#pipeline-specific-contribution-guidelines). + +#### Channel naming schemes + +Use the following naming schemes for channels to make the channel flow easier to understand: + +- Initial process channel: `ch_output_from_` +- Intermediate and terminal channels: `ch__for_` + +#### Default parameter values + +Parameters should be initialised and defined with default values within the `params` scope in `nextflow.config`. +They should also be documented in the pipeline JSON schema. + +To update `nextflow_schema.json`, run: + +```bash +nf-core pipelines schema build +``` + +The schema builder interface that loads in your browser should automatically update the defaults in the parameter documentation. + +#### Default processes resource requirements + +If you write a local module, specify a default set of resource requirements for the process. + +Sensible defaults for process resource requirements (CPUs, memory, time) should be defined in `conf/base.config`. +Specify these with generic `withLabel:` selectors, so they can be shared across multiple processes and steps of the pipeline. + +nf-core provides a set of standard labels that you should follow where possible, as seen in the [nf-core pipeline template](https://github.com/nf-core/tools/blob/main/nf_core/pipeline-template/conf/base.config). +These labels define resource defaults for single-core processes, modules that require a GPU, and different levels of multi-core configurations with increasing memory requirements. + +Values assigned within these labels can be dynamically passed to a tool using the the `${task.cpus}` and `${task.memory}` Nextflow variables in the `script:` block of a module (see an example in the [modules repository](https://github.com/nf-core/modules/blob/bd1b6a40f55933d94b8c9ca94ec8c1ea0eaf4b82/modules/nf-core/samtools/bam2fq/main.nf#L30)). + +#### Nextflow version bumping + +If you use a new feature from core Nextflow, bump the minimum required Nextflow version in the pipeline with: + +```bash +nf-core pipelines bump-version --nextflow . +``` + +#### Images and figures guidelines + +If you update images or graphics, follow the nf-core [style guidelines](https://nf-co.re/docs/community/brand/workflow-schematics). diff --git a/docs/README.md b/docs/README.md index 917d984..657c1b3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ -# nf-core/dmscore: Documentation +# nf-core/deepmutscan: Documentation -The nf-core/dmscore documentation is split into the following pages: +The nf-core/deepmutscan documentation is split into the following pages: - [Usage](usage.md) - An overview of how the pipeline works, how to run it and a description of all of the different command-line flags. diff --git a/docs/images/fastqc.png b/docs/images/fastqc.png new file mode 100644 index 0000000..32c30ff Binary files /dev/null and b/docs/images/fastqc.png differ diff --git a/docs/images/fitness_estimation_count_correlation.png b/docs/images/fitness_estimation_count_correlation.png new file mode 100644 index 0000000..98686bf Binary files /dev/null and b/docs/images/fitness_estimation_count_correlation.png differ diff --git a/docs/images/fitness_estimation_fitness_correlation.png b/docs/images/fitness_estimation_fitness_correlation.png new file mode 100644 index 0000000..81e30d8 Binary files /dev/null and b/docs/images/fitness_estimation_fitness_correlation.png differ diff --git a/docs/images/fitness_heatmap.png b/docs/images/fitness_heatmap.png new file mode 100644 index 0000000..26c19c1 Binary files /dev/null and b/docs/images/fitness_heatmap.png differ diff --git a/docs/images/fitness_structure.png b/docs/images/fitness_structure.png new file mode 100644 index 0000000..bc646b5 Binary files /dev/null and b/docs/images/fitness_structure.png differ diff --git a/docs/images/library_QC_SeqDepth.png b/docs/images/library_QC_SeqDepth.png new file mode 100644 index 0000000..c608496 Binary files /dev/null and b/docs/images/library_QC_SeqDepth.png differ diff --git a/docs/images/library_QC_counts_heatmap.png b/docs/images/library_QC_counts_heatmap.png new file mode 100644 index 0000000..850076e Binary files /dev/null and b/docs/images/library_QC_counts_heatmap.png differ diff --git a/docs/images/library_QC_counts_per_cov_heatmap.png b/docs/images/library_QC_counts_per_cov_heatmap.png new file mode 100644 index 0000000..245d515 Binary files /dev/null and b/docs/images/library_QC_counts_per_cov_heatmap.png differ diff --git a/docs/images/library_QC_logdiff_plot.png b/docs/images/library_QC_logdiff_plot.png new file mode 100644 index 0000000..c04d900 Binary files /dev/null and b/docs/images/library_QC_logdiff_plot.png differ diff --git a/docs/images/library_QC_logdiff_varying_bases.png b/docs/images/library_QC_logdiff_varying_bases.png new file mode 100644 index 0000000..e26b9fb Binary files /dev/null and b/docs/images/library_QC_logdiff_varying_bases.png differ diff --git a/docs/images/library_QC_rolling_counts.png b/docs/images/library_QC_rolling_counts.png new file mode 100644 index 0000000..e59411e Binary files /dev/null and b/docs/images/library_QC_rolling_counts.png differ diff --git a/docs/images/library_QC_rolling_coverage.png b/docs/images/library_QC_rolling_coverage.png new file mode 100644 index 0000000..9a0e014 Binary files /dev/null and b/docs/images/library_QC_rolling_coverage.png differ diff --git a/docs/images/multiqc1.png b/docs/images/multiqc1.png new file mode 100644 index 0000000..5308ad0 Binary files /dev/null and b/docs/images/multiqc1.png differ diff --git a/docs/images/multiqc2.png b/docs/images/multiqc2.png new file mode 100644 index 0000000..f8634ac Binary files /dev/null and b/docs/images/multiqc2.png differ diff --git a/docs/images/multiqc3.png b/docs/images/multiqc3.png new file mode 100644 index 0000000..7820346 Binary files /dev/null and b/docs/images/multiqc3.png differ diff --git a/docs/images/nf-core-deepmutscan_logo_dark.png b/docs/images/nf-core-deepmutscan_logo_dark.png new file mode 100644 index 0000000..398e0b2 Binary files /dev/null and b/docs/images/nf-core-deepmutscan_logo_dark.png differ diff --git a/docs/images/nf-core-deepmutscan_logo_light.png b/docs/images/nf-core-deepmutscan_logo_light.png new file mode 100644 index 0000000..ed93fd9 Binary files /dev/null and b/docs/images/nf-core-deepmutscan_logo_light.png differ diff --git a/docs/images/nf-core-dmscore_logo_dark.png b/docs/images/nf-core-dmscore_logo_dark.png deleted file mode 100644 index 7cbb4e1..0000000 Binary files a/docs/images/nf-core-dmscore_logo_dark.png and /dev/null differ diff --git a/docs/images/nf-core-dmscore_logo_light.png b/docs/images/nf-core-dmscore_logo_light.png deleted file mode 100644 index 569ba7e..0000000 Binary files a/docs/images/nf-core-dmscore_logo_light.png and /dev/null differ diff --git a/docs/images/pipeline.png b/docs/images/pipeline.png new file mode 100644 index 0000000..fa66090 Binary files /dev/null and b/docs/images/pipeline.png differ diff --git a/docs/images/run_overview.png b/docs/images/run_overview.png new file mode 100644 index 0000000..ea29344 Binary files /dev/null and b/docs/images/run_overview.png differ diff --git a/docs/images/seq_error1.png b/docs/images/seq_error1.png new file mode 100644 index 0000000..508e9e7 Binary files /dev/null and b/docs/images/seq_error1.png differ diff --git a/docs/images/seq_error2.png b/docs/images/seq_error2.png new file mode 100644 index 0000000..fdf9851 Binary files /dev/null and b/docs/images/seq_error2.png differ diff --git a/docs/images/seq_error3.png b/docs/images/seq_error3.png new file mode 100644 index 0000000..e639b18 Binary files /dev/null and b/docs/images/seq_error3.png differ diff --git a/docs/output.md b/docs/output.md index ff49a08..b493136 100644 --- a/docs/output.md +++ b/docs/output.md @@ -1,18 +1,43 @@ -# nf-core/dmscore: Output +# nf-core/deepmutscan: Output ## Introduction -This document describes the output produced by the pipeline. Most of the plots are taken from the MultiQC report, which summarises results at the end of the pipeline. - -The directories listed below will be created in the results directory after the pipeline has finished. All paths are relative to the top-level results directory. - - +This document describes the output produced by the `nf-core/deepmutscan` pipeline. + +The directories listed below will be created in the results directory after the pipeline has finished. All paths are relative to the top-level results directory. Library-specific files are named, or placed in subfolders named, after the library identifier in the samplesheet `___pe` (e.g. `GID1A_input_1_pe`). + +```tree title="nf-core/deepmutscan results" +results/ +β”œβ”€β”€ deepmutscan_report.html # all-in-one, self-contained run report +β”œβ”€β”€ fastqc/ # raw read QC per FASTQ file +β”œβ”€β”€ multiqc/ # MultiQC report across all FASTQ files +β”œβ”€β”€ intermediate_files/ # reference index, alignments, variant count tables, error correction +β”‚ β”œβ”€β”€ bwa/ +β”‚ β”œβ”€β”€ bam_files/{bwa/mem,filtered,premerged,sorted}/ +β”‚ β”œβ”€β”€ variant_counts// +β”‚ └── processed_variant_counts// +β”œβ”€β”€ library_QC/ # per-sample library QC plots + run-level error-correction report +β”‚ └── / +β”œβ”€β”€ fitness/ # fitness results, mostly only with --fitness +β”‚ β”œβ”€β”€ default_results/ +β”‚ β”œβ”€β”€ DiMSum_results/ # single_rep_counts/ always; dimsum_results/ only with --dimsum +β”‚ └── mutscan_results/ # only with --mutscan +β”œβ”€β”€ variant_effect_inspection_tool/ # only with --fitness and --pdb +└── pipeline_info/ # Nextflow and pipeline run information +``` ## Pipeline overview -The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes data using the following steps: +The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes data using the following key steps: -- [FastQC](#fastqc) - Raw read QC- [MultiQC](#multiqc) - Aggregate report describing results and QC from the whole pipeline +- [FastQC](#fastqc) - Raw read QC +- [MultiQC](#multiqc) - Aggregate report describing results and QC from the whole pipeline +- [Intermediate files](#intermediate-files) - Reference index, alignments and variant count tables +- [Error correction](#error-correction) - Sequencing-error corrected count tables and report +- [Library QC](#library-qc) - Per-sample visualisations of mutation efficiency, coverage and saturation +- [Fitness](#fitness) - Merged counts and fitness estimates (optional) +- [Variant effect inspection tool](#variant-effect-inspection-tool) - Interactive 3D structure viewer (optional) +- [All-in-one report](#all-in-one-report) - Self-contained HTML report of the whole run - [Pipeline information](#pipeline-information) - Report metrics generated during the workflow execution ### FastQC @@ -26,7 +51,11 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d -[FastQC](http://www.bioinformatics.babraham.ac.uk/projects/fastqc/) gives general quality metrics about your sequenced reads. It provides information about the quality score distribution across your reads, per base sequence content (%A/T/G/C), adapter contamination and overrepresented sequences. For further reading and documentation see the [FastQC help pages](http://www.bioinformatics.babraham.ac.uk/projects/fastqc/Help/).### MultiQC +[FastQC](http://www.bioinformatics.babraham.ac.uk/projects/fastqc/) gives general quality metrics about your sequenced reads. It provides information about the quality score distribution across your reads, per base sequence content (%A/T/G/C), adapter contamination estimates and overrepresented sequences. For further reading and documentation see the [FastQC help pages](http://www.bioinformatics.babraham.ac.uk/projects/fastqc/Help/). + +![FastQC report](images/fastqc.png) + +### MultiQC
Output files @@ -38,18 +67,148 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d
-[MultiQC](http://multiqc.info) is a visualization tool that generates a single HTML report summarising all samples in your project. Most of the pipeline QC results are visualised in the report and further statistics are available in the report data directory. +[MultiQC](http://multiqc.info) is a visualization tool that generates a single HTML report summarising all the sequenced samples in your project. Most of the pipeline QC results are visualised in the all-in-one report and further statistics are available in the report data directory. The pipeline has special steps which also allow the software versions to be reported in the MultiQC output for future traceability. For more information about how to use MultiQC reports, see . + +![MultiQC overview](images/multiqc1.png) +![MultiQC base-quality summary](images/multiqc2.png) +![MultiQC GC-content summary](images/multiqc3.png) + +### Intermediate files + +
+Output files + +- `intermediate_files/` + - `aa_seq.txt`: wildtype amino acid sequence translated from `--reading_frame`. + - `possible_mutations.csv`: all programmed codon variants per position, as defined by `--mutagenesis_type`; used for library filtering and visualisation. + - `bwa/`: BWA index of the reference FASTA. + - `bam_files/bwa/mem/.bam`: raw read alignments. + - `bam_files/filtered/_filtered.bam`: alignments after removal of unmapped, secondary, low mapping-quality (MAPQ < 30) and indel-containing reads. + - `bam_files/premerged/_merged.bam`: alignments of merged read pairs (consensus reads), used for variant counting. + - `bam_files/sorted/.sorted.bam` and `.bai`: coordinate-sorted and indexed merged alignments. + - `variant_counts//.variant_counts.tsv`: raw variant count table with one row per observed combination of nucleotide changes, its count, coverage and codon / amino acid annotation. + - `variant_counts//variant_counts_columns.tsv`: description of every column of the variant count table. + - `processed_variant_counts//`: + - `annotated_variantCounts.csv`: variant counts annotated with codon and amino acid changes and counts per coverage. + - `variantCounts_filtered_by_library.csv`: single-codon variants that match the programmed library. + - `library_completed_variantCounts.csv`: all programmed variants, including unobserved ones (with a near-zero placeholder count), used for the count distribution plots. + - `variantCounts_for_heatmaps.csv`: counts and counts per coverage aggregated per position and amino acid, used for the count heatmaps. + +
+ +These files document every step from raw reads to variant frequencies. + +### Error correction + +
+Output files + +- `intermediate_files/processed_variant_counts//` (unless `--error_correction none`) + - `variantCounts_filtered_by_library_error_corrected.csv`, `library_completed_variantCounts_error_corrected.csv`, `variantCounts_for_heatmaps_error_corrected.csv`: corrected versions of the tables above. The corrected counts are used by all downstream steps; in `variantCounts_filtered_by_library_error_corrected.csv`, the original counts are kept in the `counts_raw` and `counts_per_cov_raw` columns. + - `seq_error_rate.csv` (`--error_correction false_doubles` only): estimated sequencing error rate for every possible single-nucleotide substitution along the reference. +- `library_QC/` + - `error_correction_report.html`: interactive report of the correction across all libraries of the run. + +
+ +The error-correction report shows the single-nucleotide substitution error correlation across samples, raw versus corrected counts, the distribution of per-variant correction magnitudes, positional error bias along the ORF and a searchable per-variant table. Libraries can be selected and deselected in the report, for example to compare input and output libraries. + +![Error rate reproducibility](images/seq_error1.png) +![Raw vs. corrected count frequencies](images/seq_error2.png) +![Per-sample error profile](images/seq_error3.png) + +### Library QC + +
+Output files + +- `library_QC//` + - `counts_heatmap.pdf`: variant counts per position (x-axis) and mutant amino acid (y-axis). + - `counts_per_cov_heatmap.pdf`: as above, but as counts per sequencing coverage. + - `logdiff_plot.pdf`: sorted, log-scale distribution of counts per coverage of all programmed variants, with the log10 ratio of their 90th and 10th percentiles. + - `logdiff_varying_bases.pdf`: as above, stratified by the number of changed nucleotides per codon. + - `rolling_coverage.pdf`: sliding-window sequencing coverage along the ORF, with the coverage required to observe every amino acid variant `--aimed_cov` times. + - `rolling_counts.pdf`: sliding-window variant counts along the ORF, stratified by the number of changed nucleotides per codon. + - `rolling_counts_per_cov.pdf`: as above, but as counts per coverage. + - `SeqDepth.pdf` and `seqdepth_curve.csv` (with `--run_seqdepth`, default): sequencing-depth rarefaction curve, i.e. the expected fraction of programmed variants that remain detected when the library is subsampled to lower sequencing depths. + +
+ +These plots help to judge the success of the mutagenesis (are all positions and amino acids represented?), the evenness of the fragmentation and sequencing coverage along the ORF, and whether a library was sequenced deeply enough. + +![Counts per coverage heatmap](images/library_QC_counts_per_cov_heatmap.png) +![Counts per coverage distribution](images/library_QC_logdiff_plot.png) +![Counts per coverage distribution by number of changed bases](images/library_QC_logdiff_varying_bases.png) +![Rolling coverage](images/library_QC_rolling_coverage.png) +![Rolling counts](images/library_QC_rolling_counts.png) +![Sequencing depth rarefaction](images/library_QC_SeqDepth.png) + +### Fitness + +
+Output files + +- `fitness/` + - `counts_merged.tsv`: merged variant counts of all `input` and `output` libraries of a sample. + - `experimentalDesign.tsv`: experimental design derived from the original samplesheet (DiMSum input format). + - `synonymous_wt.txt`: nucleotide sequence of the synonymous variant used as wildtype proxy. + - `default_results/` + - `fitness_estimation.tsv`: per-variant counts, raw and rescaled fitness per replicate, and mean fitness and standard deviation across replicates. + - `fitness_estimation_count_correlation.pdf`: pairwise variant count correlations between all libraries. + - `fitness_estimation_fitness_correlation.pdf`: pairwise fitness correlations between replicates. + - `fitness_heatmap.pdf`: mean fitness per position (x-axis) and mutant amino acid (y-axis). + - `DiMSum_results/` + - `single_rep_counts/_fitness_input.tsv`: per-library count tables in DiMSum input format. These are written for every run, also without `--fitness`. + - `dimsum_results/` (with `--dimsum`): the complete [DiMSum](https://github.com/lehner-lab/DiMSum) default output, including its HTML `report.html`, fitness tables (`fitness_singles.txt`, `fitness_doubles.txt`, `fitness_synonymous.txt`, `fitness_wildtype.txt`, `fitness_singles_MaveDB.csv`) and `.RData` objects. + - `mutscan_results/` (with `--mutscan`) + - `fitness_estimation_mutscan_edgeR.tsv`, `fitness_estimation_mutscan_limma.tsv`: [mutscan](https://github.com/fmicompbio/mutscan) default variant enrichment estimates with edgeR and limma. + - `mutscan_counts_corr.pdf`, `mutscan_edgeR_volcano.pdf`, `mutscan_limma_volcano.pdf`: count correlation and volcano plots. + +
+ +Fitness is only estimated with `--fitness` and requires at least one `input` and one `output` library per sample. See the [usage documentation](usage.md#8-fitness-estimation-optional) for how fitness is calculated. Fitness values are scaled so that wild-type synonymous variants centre on 0 and stop codon (nonsense) variants on -1. + +![Variant count correlations](images/fitness_estimation_count_correlation.png) +![Fitness correlations](images/fitness_estimation_fitness_correlation.png) +![Fitness heatmap](images/fitness_heatmap.png) + +### Variant effect inspection tool + +
+Output files + +- `variant_effect_inspection_tool/` (with `--fitness` and `--pdb`) + - `variant_effect_inspection_tool.html`: self-contained, interactive 3D structure viewer. + - `structure_data.json`: the per-residue and per-variant data shown in the viewer. + +
+ +This tool projects the mean per-residue fitness, coverage, counts, counts per coverage and – when error correction is on – the positional error bias onto the supplied wildtype 3D structure. Selecting a residue shows the effect of each substitution. The file works offline and can be shared as a single file. + +![Fitness structure](images/fitness_structure.png) + +### All-in-one report + +
+Output files + +- `deepmutscan_report.html`: a single, self-contained HTML report of the run. + +
+ +The report brings all the important metrics and visual results of a run together in one file; it can be opened in any browser and shared without any accompanying files. Features are a complete `nf-core` run overview with parameters and execution statistics, interactive sequencing and library QC plots per library, the error-correction report, the fitness results and heatmap (with `--fitness`), the DiMSum and mutscan results (if run), a 3D structure view (with `--pdb`), the MultiQC report, and the citations of all the tools and major dependencies that were used in the run. + +![Run overview](images/run_overview.png) -Results generated by MultiQC collate pipeline QC from supported tools e.g. FastQC. The pipeline has special steps which also allow the software versions to be reported in the MultiQC output for future traceability. For more information about how to use MultiQC reports, see .### Pipeline information +### Pipeline information
Output files - `pipeline_info/` - - Reports generated by Nextflow: `execution_report.html`, `execution_timeline.html`, `execution_trace.txt` and `pipeline_dag.dot`/`pipeline_dag.svg`. - - Reports generated by the pipeline: `pipeline_report.html`, `pipeline_report.txt` and `software_versions.yml`. The `pipeline_report*` files will only be present if the `--email` / `--email_on_fail` parameter's are used when running the pipeline. - - Reformatted samplesheet files used as input to the pipeline: `samplesheet.valid.csv`. - - Parameters used by the pipeline run: `params.json`. + - Reports generated by Nextflow: `execution_report_.html`, `execution_timeline_.html`, `execution_trace_.txt` and `pipeline_dag_.html`. + - Reports generated by the pipeline: `pipeline_report.html`, `pipeline_report.txt` and `nf_core_deepmutscan_software_mqc_versions.yml`. The `pipeline_report*` files will only be present if the `--email` / `--email_on_fail` parameters are used when running the pipeline. + - Parameters used by the pipeline run: `params_.json`.
diff --git a/docs/usage.md b/docs/usage.md index e7ae0cb..77ea32e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,66 +1,72 @@ -# nf-core/dmscore: Usage +# nf-core/deepmutscan: Usage -## :warning: Please read this documentation on the nf-core website: [https://nf-co.re/dmscore/usage](https://nf-co.re/dmscore/usage) +## :warning: Please read this documentation on the nf-core website: [https://nf-co.re/deepmutscan/usage](https://nf-co.re/deepmutscan/usage) > _Documentation of pipeline parameters is generated automatically from the pipeline schema and can no longer be found in markdown files._ ## Introduction - +**nf-core/deepmutscan** is a workflow for the analysis of deep mutational scanning (DMS) data. It takes short-read next-generation DNA sequencing data of a site-saturation mutagenesis library – typically before (`input`) and after (`output`) a variant selection experiment – and turns them into quality-controlled, error-corrected variant frequencies and, optionally, relative variant fitness estimates. + +The pipeline was mainly designed for shotgun sequencing of long open reading frames (ORFs), where the mutated gene is randomly fragmented before sequencing and where resulting variant count signals are sparse, but it processes amplicon (tile) sequencing data in the same way. + +This page explains how to prepare the inputs, how to run the pipeline, and what each processing step does and why. It is intended both for new DMS users and for developers who want to understand the exact design choices. ## Samplesheet input -You will need to create a samplesheet with information about the samples you would like to analyse before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 columns, and a header row as shown in the examples below. +You will need to create a samplesheet with information about the sequencing libraries you would like to analyse before running the pipeline. Use the `--input` parameter to specify its location. It has to be a comma-separated file with 5 columns and a header row, as shown in the example below. ```bash --input '[path to samplesheet file]' ``` -### Multiple runs of the same sample - -The `sample` identifiers have to be the same when you have re-sequenced the same sample more than once e.g. to increase sequencing depth. The pipeline will concatenate the raw reads before performing any downstream analysis. Below is an example for the same sample sequenced across 3 lanes: - ```csv title="samplesheet.csv" -sample,fastq_1,fastq_2 -CONTROL_REP1,AEG588A1_S1_L002_R1_001.fastq.gz,AEG588A1_S1_L002_R2_001.fastq.gz -CONTROL_REP1,AEG588A1_S1_L003_R1_001.fastq.gz,AEG588A1_S1_L003_R2_001.fastq.gz -CONTROL_REP1,AEG588A1_S1_L004_R1_001.fastq.gz,AEG588A1_S1_L004_R2_001.fastq.gz +sample,type,replicate,file1,file2 +ORF1,input,1,/reads/input1_R1.fastq.gz,/reads/input1_R2.fastq.gz +ORF1,input,2,/reads/input2_R1.fastq.gz,/reads/input2_R2.fastq.gz +ORF1,output,1,/reads/output1_R1.fastq.gz,/reads/output1_R2.fastq.gz +ORF1,output,2,/reads/output2_R1.fastq.gz,/reads/output2_R2.fastq.gz ``` -### Full samplesheet +| Column | Description | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `sample` | Name of the biological sample or experiment (e.g. the gene or condition). Must not contain spaces or `/`. Rows with the same `sample` are grouped for fitness estimation and for `wildtype` error correction. | +| `type` | Role of the library: `input` (before selection), `output` (after selection) or `wildtype` (deep sequencing of the unmutated template, used by `--error_correction wildtype`). | +| `replicate` | Replicate number (positive integer). For fitness estimation, `input` and `output` libraries are paired in order of their replicate numbers, so please number replicates consecutively (1, 2, 3, …). | +| `file1` | Full path to the FASTQ file for read 1, with extension `.fastq.gz`, `.fq.gz`, `.fastq` or `.fq`. | +| `file2` | Full path to the FASTQ file for read 2, with extension `.fastq.gz`, `.fq.gz`, `.fastq` or `.fq`. | -The pipeline will auto-detect whether a sample is single- or paired-end using the information provided in the samplesheet. The samplesheet can have as many columns as you desire, however, there is a strict requirement for the first 3 columns to match those defined in the table below. +Each library gets an internal identifier `___pe`, which is used to name its output files and folders. -A final samplesheet file consisting of both single- and paired-end data may look something like the one below. This is for 6 samples, where `TREATMENT_REP3` has been sequenced twice. +## Reference and reading frame -```csv title="samplesheet.csv" -sample,fastq_1,fastq_2 -CONTROL_REP1,AEG588A1_S1_L002_R1_001.fastq.gz,AEG588A1_S1_L002_R2_001.fastq.gz -CONTROL_REP2,AEG588A2_S2_L002_R1_001.fastq.gz,AEG588A2_S2_L002_R2_001.fastq.gz -CONTROL_REP3,AEG588A3_S3_L002_R1_001.fastq.gz,AEG588A3_S3_L002_R2_001.fastq.gz -TREATMENT_REP1,AEG588A4_S4_L003_R1_001.fastq.gz, -TREATMENT_REP2,AEG588A5_S5_L003_R1_001.fastq.gz, -TREATMENT_REP3,AEG588A6_S6_L003_R1_001.fastq.gz, -TREATMENT_REP3,AEG588A6_S6_L004_R1_001.fastq.gz, -``` +As of v1.0.0, all libraries in one run of `nf-core/deepmutscan` must stem from the same gene. Provide its wildtype sequence as a FASTA file via `--fasta`. We recommend that the reference contains N- and C-terminal flanking sequences of approximately one read length (e.g. from surrounding vector regions amplified during the sequencing library preparation), which helps to align reads more uniformly at the ends of the ORF. -| Column | Description | -| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `sample` | Custom sample name. This entry will be identical for multiple sequencing libraries/runs from the same sample. Spaces in sample names are automatically converted to underscores (`_`). | -| `fastq_1` | Full path to FastQ file for Illumina short reads 1. File has to be gzipped and have the extension ".fastq.gz" or ".fq.gz". | -| `fastq_2` | Full path to FastQ file for Illumina short reads 2. File has to be gzipped and have the extension ".fastq.gz" or ".fq.gz". | +Use `--reading_frame` to give the position of the mutagenised ORF within that sequence, as 1-based, inclusive nucleotide coordinates in the format `start-stop`: `start` is the first nucleotide of the first codon and `stop` the last nucleotide of the last codon to be analysed, so the length of the range must be divisible by three. For example, `--reading_frame 151-450` analyses exactly 100 codons of the reference FASTA. -An [example samplesheet](../assets/samplesheet.csv) has been provided with the pipeline. +Use `--mutagenesis_type` to define which codons were programmed at each position (default `nnk`). This determines which observed variants count as library variants for filtering, QC and fitness estimation: + +- `nnk`, `nns`, `nnh`, `nnn`: degenerate codons, as used e.g. in nicking mutagenesis +- `nnk_nns`, `nnk_nns_nnh`: position-dependent degenerate codons, chosen by the third base of the wildtype codon: NNS where it is T, NNH where it is G (`nnk_nns_nnh` only), and NNK otherwise +- `custom`: a user-defined codon library, provided with `--custom_codon_library` as a `.csv` file. Either give one global comma-separated list of codons without a header (e.g. `AAA,AAC,AAG`), or a position-wise list: a header line containing the word `Position`, followed by one row per codon position with the position and its allowed codons (e.g. `1,ACG,AAA,ACA`). + +> [!IMPORTANT] +> Shotgun sequencing cannot distinguish reads from unmutated plasmids from wildtype-matching fragments of mutated plasmids, so exact wildtype reads are not used as the fitness reference. Instead, the pipeline uses a synonymous variant as a wildtype proxy. We therefore highly recommend that your library design choice includes at least some synonymous wildtype amino acid codons – degenerate NNK/NNS codons do so at almost every position. ## Running the pipeline -The typical command for running the pipeline is as follows: +The typical command for running the pipeline (here on a hypothetical gene of exactly 100 codons) is as follows: -```bash -nextflow run nf-core/dmscore --input ./samplesheet.csv --outdir ./results --genome GRCh37 -profile docker +```bash title="example_run.sh" +nextflow run nf-core/deepmutscan \ + -profile \ + --input ./samplesheet.csv \ + --fasta ./ref.fa \ + --reading_frame 151-450 \ + --outdir ./results ``` -This will launch the pipeline with the `docker` configuration profile. See below for more information about profiles. +This will launch the pipeline with the `docker` configuration profile (or the profile of your choice, see [below](#-profile)). It performs the default read QC, alignment, filtering, merging, variant counting, sequencing-error correction and library QC for every library in the samplesheet. Add `--fitness` to also estimate variant fitness from matched input and output libraries, and `--dimsum` and/or `--mutscan` for the additional fitness estimators. Note that the pipeline will create the following files in your working directory: @@ -81,7 +87,7 @@ Pipeline settings can be provided in a `yaml` or `json` file via `-params-file < The above pipeline run specified with a params file in yaml format: ```bash -nextflow run nf-core/dmscore -profile docker -params-file params.yaml +nextflow run nf-core/deepmutscan -profile docker -params-file params.yaml ``` with: @@ -89,32 +95,109 @@ with: ```yaml title="params.yaml" input: './samplesheet.csv' outdir: './results/' -genome: 'GRCh37' +fasta: './ref.fa' +reading_frame: '151-450' +fitness: true <...> ``` You can also generate such `YAML`/`JSON` files via [nf-core/launch](https://nf-co.re/launch). -### Updating the pipeline +## Pipeline steps in detail + +### 1. Alignment + +All paired-end raw reads are first quality-checked with [**FastQC**](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/) and then aligned to the reference with [**BWA-MEM**](https://github.com/lh3/bwa). BWA-MEM is a highly efficient mapping algorithm for reads β‰₯ 70 bp. + +### 2. Filtering + +Alignments are filtered with [**samtools view**](https://www.htslib.org/doc/samtools-view.html): only mapped, non-secondary alignments with a mapping quality β‰₯ 30 and without insertions, deletions or splice gaps are kept before read merging, as indel-containing reads are most likely artefacts in substitution libraries. + +For long ORF site-saturation mutagenesis libraries, most aligned shotgun sequencing reads match the reference exactly. These wildtype reads are retained: they are ignored by the variant counter but contribute to the overall sequencing coverage and sequencing-error estimation. + +### 3. Read merging + +Even the highest-quality sequencing platforms do not have perfect base accuracy. To minimise the effect of base errors, which would otherwise be counted as false mutations, `nf-core/deepmutscan` uses the overlap of each read pair. As base errors on the forward and reverse read are largely independent, [**vsearch --fastq_mergepairs**](https://github.com/torognes/vsearch) converts each read pair into a single consensus read with adjusted base qualities. Read pairs that cannot be merged (overlap shorter than 10 bp), and reads whose mate was removed during filtering, are discarded. Merged reads are then re-aligned with BWA-MEM, coordinate-sorted and indexed with samtools. + +> [!TIP] +> The merging benefit is largest if the average DNA fragment size matches the read length. For example, libraries sequenced with 150 bp paired-end reads should ideally also be sheared or tagmented to a mean size of about 150 bp. + +### 4. Variant counting + +Merged reads are screened for base-level mismatches against the reference. `nf-core/deepmutscan` uses a light-weight Python counter (built on [**pysam**](https://pysam.readthedocs.io) and [**polars**](https://pola.rs)) that counts all single, double, triple and higher-order nucleotide changes per read, together with the sequencing coverage of every position. This replaces the GATK `AnalyzeSaturationMutagenesis` tool used in earlier versions; a description of every column is written next to each count table (`variant_counts_columns.tsv`). + +Two parameters tune the counter: + +- `--base_qual` (default `40`): minimum Phred base quality for a mismatch to be counted, and for a base to contribute to coverage. +- `--min_flank` (default `2`): minimum distance (in nucleotides) of a mismatch or covered base from the read end. Set to `0` to disable. + +### 5. Library annotation and filtering + +Variant counts are annotated with their codon and amino acid changes within `--reading_frame`, and filtered for the variants that were programmed by the chosen `--mutagenesis_type`. In addition, a library-completed table lists every programmed variant, including those that were not observed (with a near-zero placeholder count), for the count distribution plots. + +### 6. Single-nucleotide variant error correction + +Sequencing errors occur at typically low, but position- and substitution-specific rates. With very deep sequencing, they inflate the counts of variants that differ from the wildtype by only a single nucleotide. Variants with two or three nucleotide changes in one codon, however, are hardly affected. Select a correction strategy with `--error_correction`: + +- `false_doubles` (**default**): the library only contains single-codon changes, so reads carrying a programmed multi-nucleotide codon variant plus an unprogrammed single-nucleotide change in a nearby codon ("false double mutants") reveal the sequencing error rate of that single-nucleotide change. The error rate is estimated for every single-nucleotide variant from these read-linked observations within `--false_doubles_codon_window` codons (default `40`), and the expected number of erroneous counts is subtracted. No extra data are required. If a library contains too few false double mutants to model their coverage along the read (e.g. very small or shallow libraries), no correction is applied and a warning is written to the log. Two estimators are available via `--false_doubles_method`: + - `mle` (**default**): a per-variant maximum-likelihood error rate. + - `eb`: an empirical-Bayes estimate that shrinks the per-variant rate towards the mean of its substitution class. It is steadier for sparsely observed variants, e.g. in shallowly sequenced libraries. +- `wildtype`: subtracts the variant-specific background measured by **additional deep sequencing of the unmutated template** from every library variant. Add these libraries to the samplesheet with `type` set to `wildtype` and the same `sample` name as the libraries they should correct. The pipeline stops with an error if a `sample` has no `wildtype` library. In this mode, the `wildtype` libraries themselves are not included in the library QC and fitness outputs. +- `none`: no correction; counts are used as observed. + +Corrected counts are used for every count-dependent step downstream (count heatmaps, positional QC and fitness). The uncorrected tables are kept next to the corrected ones, and an interactive HTML report shows the effect of the correction across all libraries of the run. + +### 7. DMS library quality control + +Based on the library-filtered (and error-corrected) counts, `nf-core/deepmutscan` produces per-library visualisations to assess mutation efficiency, coverage and saturation along the ORF: + +- heatmaps of variant counts and counts per coverage, per position and amino acid +- sorted, log-scale distributions of counts per coverage of all programmed variants, also stratified by the number of changed nucleotides, summarised by the log10 ratio of their 90th and 10th percentiles +- sliding-window profiles of coverage and counts along the ORF (window size set by `--sliding_window_size`, default `10` codons), with a reference line for the coverage needed to observe every amino acid variant `--aimed_cov` times (default `100`) +- a sequencing-depth rarefaction curve that estimates the fraction of programmed variants recovered at lower sequencing depths (`--run_seqdepth`, on by default) + +### 8. Fitness estimation (optional) + +With `--fitness`, libraries of each `sample` are grouped into a merged count table of all `input` and `output` replicates. Fitness is then estimated per replicate as the natural logarithm of each variant's output-to-input count ratio, relative to that of a synonymous wildtype proxy variant. As the proxy, the pipeline picks the synonymous variant with two nucleotide changes in one codon (or, if there is none, one nucleotide change) with the highest mean input count. Non-synonymous variants encoding the same amino acid sequence are aggregated. Per-replicate fitness values are linearly rescaled so that the median of synonymous variants is `0` and the median of stop codon variants is `-1` (if one of these anchors is missing, fitness values are only centred), and summarised as the mean and, with more than one replicate, the standard deviation across replicates. + +Two parameters control which variants are scored and how dropouts are treated: + +- `--min_counts` (default `10`): minimum number of reads a variant needs in every `input` replicate to receive a fitness estimate. Variants with fewer input reads are poorly measured and are excluded. +- `--output_pseudocount` (default `1`): pseudocount added to `output` replicates in which a variant observed in the input was not detected ("dropouts"), so that strongly depleted variants still receive a finite fitness value. Set it to `0` to disable; dropout variants then receive no fitness estimate in that replicate. + +Two established statistical frameworks can be run on the same merged counts: + +- `--dimsum`: [DiMSum](https://github.com/lehner-lab/DiMSum) fitness estimates (stages 4–5 of DiMSum, without its own read processing). `--min_counts` and `--output_pseudocount` are passed on to DiMSum (`--fitnessMinInputCountAll`, `--fitnessDropoutPseudocount`). +- `--mutscan`: [mutscan](https://github.com/fmicompbio/mutscan) enrichment estimates with edgeR and limma. Variants below `--min_counts` are removed before the analysis. + +### Interactive variant effect inspection tool (`--pdb`) + +When `--fitness` is set and a wildtype 3D structure is supplied via `--pdb ` (an experimental structure, an AlphaFold DB model, or any PDB file matching the wildtype ORF), the pipeline additionally builds a portable, interactive HTML tool. It projects per-residue fitness, coverage, counts, counts per coverage and – when error correction is on – the positional error bias onto the structure; clicking a residue shows the effect of each substitution. Structure prediction is not part of the v1.0.0 release, so a user-specified structure file is required to enable the tool. + +### 9. Reporting + +`nf-core/deepmutscan` writes a single, self-contained `deepmutscan_report.html` to the top of the results directory. It embeds the library QC plots, the error-correction report, the fitness results (and DiMSum/mutscan output, if run), the [MultiQC](https://multiqc.info) report, run statistics and the citations of all tools that were used, so it can be shared as a single file. + +## Updating the pipeline -When you run the above command, Nextflow automatically pulls the pipeline code from GitHub and stores it as a cached version. When running the pipeline after this, it will always use the cached version if available - even if the pipeline has been updated since. To make sure that you're running the latest version of the pipeline, make sure that you regularly update the cached version of the pipeline: +When you run the original command above, Nextflow automatically pulls the pipeline code from GitHub and stores it as a cached version. When running the pipeline after this, it will always use this cached version if available - even if the pipeline has been updated since. To make sure that you are running the latest version of the pipeline, make sure that you regularly update the cached version: ```bash -nextflow pull nf-core/dmscore +nextflow pull nf-core/deepmutscan ``` -### Reproducibility +## Reproducibility -It is a good idea to specify the pipeline version when running the pipeline on your data. This ensures that a specific version of the pipeline code and software are used when you run your pipeline. If you keep using the same tag, you'll be running the same version of the pipeline, even if there have been changes to the code since. +It is a good idea to specify the pipeline version when running the pipeline on your data. This ensures that a specific version of the pipeline code and software are used consistently. If you keep using the same tag, you'll be running the same version of the pipeline, even if there have been changes to the code since. -First, go to the [nf-core/dmscore releases page](https://github.com/nf-core/dmscore/releases) and find the latest pipeline version - numeric only (eg. `1.3.1`). Then specify this when running the pipeline with `-r` (one hyphen) - eg. `-r 1.3.1`. Of course, you can switch to another version by changing the number after the `-r` flag. +First, go to the [nf-core/deepmutscan releases page](https://github.com/nf-core/deepmutscan/releases) and find the latest pipeline version - numeric only (eg. `1.0.0`). Then specify this when running the pipeline with `-r` (one hyphen) - eg. `-r 1.0.0`. -This version number will be logged in reports when you run the pipeline, so that you'll know what you used when you look back in the future. For example, at the bottom of the MultiQC reports. +This version number will also be logged in reports when you run the pipeline, for example at the bottom of the MultiQC reports. -To further assist in reproducibility, you can use share and reuse [parameter files](#running-the-pipeline) to repeat pipeline runs with the same settings without having to write out a command with every single parameter. +To further assist in reproducibility, you can share and reuse [parameter files](#running-the-pipeline) to repeat pipeline runs with the same settings without having to write out a command with every single parameter. > [!TIP] -> If you wish to share such profile (such as upload as supplementary material for academic publications), make sure to NOT include cluster specific paths to files, nor institutional specific profiles. +> If you wish to share such a profile (e.g. providing it as supplementary material for academic publications), make sure to _not_ include your cluster specific file paths or institutional specific profiles. ## Core Nextflow arguments @@ -135,7 +218,7 @@ The pipeline also dynamically loads configurations from [https://github.com/nf-c Note that multiple profiles can be loaded, for example: `-profile test,docker` - the order of arguments is important! They are loaded in sequence, so later profiles can overwrite earlier profiles. -If `-profile` is not specified, the pipeline will run locally and expect all software to be installed and available on the `PATH`. This is _not_ recommended, since it can lead to different results on different machines dependent on the computer environment. +If `-profile` is not specified, the pipeline will run locally and expect all software to be installed and available on the `PATH`. This is _not_ recommended, since it may lead to varying or even irreproducible results across users' different computer environments. - `test` - A profile with a complete configuration for automated testing @@ -159,7 +242,7 @@ If `-profile` is not specified, the pipeline will run locally and expect all sof ### `-resume` -Specify this when restarting a pipeline. Nextflow will use cached results from any pipeline steps where the inputs are the same, continuing from where it got to previously. For input to be considered the same, not only the names must be identical but the files' contents as well. For more info about this parameter, see [this blog post](https://www.nextflow.io/blog/2019/demystifying-nextflow-resume.html). +Specify this when restarting a pipeline. Nextflow will use cached results (from within the `/work` directory) from any pipeline steps where the inputs are the same, continuing from where it got to previously. For input to be considered the same, not only the names must be identical but the files' contents as well. For more info about this parameter, see [this blog post](https://www.nextflow.io/blog/2019/demystifying-nextflow-resume.html). You can also supply a run name to resume a specific run: `-resume [run-name]`. Use the `nextflow log` command to show previous run names. @@ -173,19 +256,19 @@ Specify the path to a specific config file (this is a core Nextflow command). Se Whilst the default requirements set within the pipeline will hopefully work for most people and with most input data, you may find that you want to customise the compute resources that the pipeline requests. Each step in the pipeline has a default set of requirements for number of CPUs, memory and time. For most of the pipeline steps, if the job exits with any of the error codes specified [here](https://github.com/nf-core/rnaseq/blob/4c27ef5610c87db00c3c5a3eed10b1d161abf575/conf/base.config#L18) it will automatically be resubmitted with higher resources request (2 x original, then 3 x original). If it still fails after the third attempt then the pipeline execution is stopped. -To change the resource requests, please see the [max resources](https://nf-co.re/docs/usage/configuration#max-resources) and [tuning workflow resources](https://nf-co.re/docs/usage/configuration#tuning-workflow-resources) section of the nf-core website. +To change the resource requests, please see the [max resources](https://nf-co.re/docs/running/configuration/nextflow-for-your-system#set-max-resources) and [customise process resources](https://nf-co.re/docs/running/configuration/nextflow-for-your-system#customize-process-resources) section of the nf-core website. ### Custom Containers In some cases, you may wish to change the container or conda environment used by a pipeline steps for a particular tool. By default, nf-core pipelines use containers and software from the [biocontainers](https://biocontainers.pro/) or [bioconda](https://bioconda.github.io/) projects. However, in some cases the pipeline specified version maybe out of date. -To use a different container from the default container or conda environment specified in a pipeline, please see the [updating tool versions](https://nf-co.re/docs/usage/configuration#updating-tool-versions) section of the nf-core website. +To use a different container from the default container or conda environment specified in a pipeline, please see the [updating tool versions](https://nf-co.re/docs/running/configuration/nextflow-for-your-system#update-tool-versions) section of the nf-core website. ### Custom Tool Arguments A pipeline might not always support every possible argument or option of a particular tool used in pipeline. Fortunately, nf-core pipelines provide some freedom to users to insert additional parameters that the pipeline does not include by default. -To learn how to provide additional arguments to a particular tool of the pipeline, please see the [customising tool arguments](https://nf-co.re/docs/usage/configuration#customising-tool-arguments) section of the nf-core website. +To learn how to provide additional arguments to a particular tool of the pipeline, please see the [customising tool arguments](https://nf-co.re/docs/running/configuration/nextflow-for-your-system#modifying-tool-arguments) section of the nf-core website. ### nf-core/configs diff --git a/main.nf b/main.nf index 8c9943a..41a451c 100644 --- a/main.nf +++ b/main.nf @@ -1,11 +1,11 @@ #!/usr/bin/env nextflow /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - nf-core/dmscore + nf-core/deepmutscan ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Github : https://github.com/nf-core/dmscore - Website: https://nf-co.re/dmscore - Slack : https://nfcore.slack.com/channels/dmscore + Github : https://github.com/nf-core/deepmutscan + Website: https://nf-co.re/deepmutscan + Slack : https://nfcore.slack.com/channels/deepmutscan ---------------------------------------------------------------------------------------- */ @@ -15,10 +15,10 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { DMSCORE } from './workflows/dmscore' -include { PIPELINE_INITIALISATION } from './subworkflows/local/utils_nfcore_dmscore_pipeline' -include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_dmscore_pipeline' -include { getGenomeAttribute } from './subworkflows/local/utils_nfcore_dmscore_pipeline' +include { DEEPMUTSCAN } from './workflows/deepmutscan' +include { PIPELINE_INITIALISATION } from './subworkflows/local/utils_nfcore_deepmutscan_pipeline' +include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_deepmutscan_pipeline' +include { getGenomeAttribute } from './subworkflows/local/utils_nfcore_deepmutscan_pipeline' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -26,9 +26,6 @@ include { getGenomeAttribute } from './subworkflows/local/utils_nfcore_dmsc ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -// TODO nf-core: Remove this line if you don't need a FASTA file -// This is an example of how to use getGenomeAttribute() to fetch parameters -// from igenomes.config using `--genome` params.fasta = getGenomeAttribute('fasta') /* @@ -40,7 +37,7 @@ params.fasta = getGenomeAttribute('fasta') // // WORKFLOW: Run main analysis pipeline depending on type of input // -workflow NFCORE_DMSCORE { +workflow NFCORE_DEEPMUTSCAN { take: samplesheet // channel: samplesheet read in from --input @@ -50,12 +47,17 @@ workflow NFCORE_DMSCORE { // // WORKFLOW: Run pipeline // - DMSCORE ( - samplesheet + DEEPMUTSCAN ( + samplesheet, + params.multiqc_config, + params.multiqc_logo, + params.multiqc_methods_description, + params.outdir, ) emit: - multiqc_report = DMSCORE.out.multiqc_report // channel: /path/to/multiqc_report.html + multiqc_report = DEEPMUTSCAN.out.multiqc_report // channel: /path/to/multiqc_report.html } + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ RUN MAIN WORKFLOW @@ -80,7 +82,7 @@ workflow { // // WORKFLOW: Run main workflow // - NFCORE_DMSCORE ( + NFCORE_DEEPMUTSCAN ( PIPELINE_INITIALISATION.out.samplesheet ) // @@ -92,8 +94,7 @@ workflow { params.plaintext_email, params.outdir, params.monochrome_logs, - params.hook_url, - NFCORE_DMSCORE.out.multiqc_report + NFCORE_DEEPMUTSCAN.out.multiqc_report ) } diff --git a/modules.json b/modules.json index 910602b..d9f1096 100644 --- a/modules.json +++ b/modules.json @@ -1,18 +1,38 @@ { - "name": "nf-core/dmscore", - "homePage": "https://github.com/nf-core/dmscore", + "name": "nf-core/deepmutscan", + "homePage": "https://github.com/nf-core/deepmutscan", "repos": { "https://github.com/nf-core/modules.git": { "modules": { "nf-core": { + "bwa/index": { + "branch": "master", + "git_sha": "6d46786420b4d7bc88eba026eb389c0c5535d120", + "installed_by": ["modules"] + }, + "bwa/mem": { + "branch": "master", + "git_sha": "2fb127c8fd13de0adaa676df7169131e45c0b114", + "installed_by": ["modules"] + }, "fastqc": { "branch": "master", - "git_sha": "dc94b6ee04a05ddb9f7ae050712ff30a13149164", + "git_sha": "6d46786420b4d7bc88eba026eb389c0c5535d120", "installed_by": ["modules"] }, "multiqc": { "branch": "master", - "git_sha": "cf17ca47590cc578dfb47db1c2a44ef86f89976d", + "git_sha": "98403d15b0e50edae1f3fec5eae5e24982f1fade", + "installed_by": ["modules"] + }, + "samtools/index": { + "branch": "master", + "git_sha": "6d46786420b4d7bc88eba026eb389c0c5535d120", + "installed_by": ["modules"] + }, + "samtools/sort": { + "branch": "master", + "git_sha": "6d46786420b4d7bc88eba026eb389c0c5535d120", "installed_by": ["modules"] } } @@ -21,17 +41,17 @@ "nf-core": { "utils_nextflow_pipeline": { "branch": "master", - "git_sha": "c2b22d85f30a706a3073387f30380704fcae013b", + "git_sha": "1a545fcbd762911c21a64ced3dbef99b2b51ac75", "installed_by": ["subworkflows"] }, "utils_nfcore_pipeline": { "branch": "master", - "git_sha": "51ae5406a030d4da1e49e4dab49756844fdd6c7a", + "git_sha": "a3fb7351b1fdb2b1de282b765816bbea190e86a8", "installed_by": ["subworkflows"] }, "utils_nfschema_plugin": { "branch": "master", - "git_sha": "2fd2cd6d0e7b273747f32e465fdc6bcc3ae0814e", + "git_sha": "a7b27fd25bfa8dcc07d299e88bd790585901a436", "installed_by": ["subworkflows"] } } diff --git a/modules/local/bamprocessing/bam_filter/environment.yml b/modules/local/bamprocessing/bam_filter/environment.yml new file mode 100644 index 0000000..a5338f6 --- /dev/null +++ b/modules/local/bamprocessing/bam_filter/environment.yml @@ -0,0 +1,5 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::samtools=1.21 diff --git a/modules/local/bamprocessing/bam_filter/main.nf b/modules/local/bamprocessing/bam_filter/main.nf new file mode 100644 index 0000000..bed65dd --- /dev/null +++ b/modules/local/bamprocessing/bam_filter/main.nf @@ -0,0 +1,51 @@ + +process BAMFILTER_DMS { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/samtools:1.21--h96c455f_1': + 'biocontainers/samtools:1.21--h96c455f_1' }" + + input: + tuple val(meta), path(bam) + + output: + tuple val(meta), path("*.bam"), emit: bam + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + """ + # Keep primary, mapped, MAPQ>=30 reads and drop indel/splice alignments. + # Wildtype (NM:i:0) reads are intentionally retained (needed for downstream + # sequencing-error correction); the variant counter ignores them for calling. + samtools view -h -F 4 -F 256 -q 30 $bam | \ + samtools view -h | \ + awk '{if(\$6 !~ /I/ && \$6 !~ /D/ && \$6 !~ /N/) print \$0}' | \ + samtools view -bS > ${meta.id}_filtered.bam + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + samtools: \$(samtools --version |& sed '1!d ; s/samtools //') +END_VERSIONS + """ + + stub: + """ + # Must match the name the script block writes: the input bam is staged under + # \${meta.id}.bam, so touching that name here would just re-touch the input and + # leave the process with no output of its own. + touch ${meta.id}_filtered.bam + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + samtools: \$(samtools --version |& sed '1!d ; s/samtools //') +END_VERSIONS + """ +} diff --git a/modules/local/bamprocessing/bam_filter/meta.yml b/modules/local/bamprocessing/bam_filter/meta.yml new file mode 100644 index 0000000..78c92ea --- /dev/null +++ b/modules/local/bamprocessing/bam_filter/meta.yml @@ -0,0 +1,48 @@ +name: "bamfilter_dms" +description: Filters BAM files specifically for Deep Mutational Scanning (DMS) analysis by removing unmapped reads, low-quality alignments, secondary alignments, indels (CIGAR I/D/N), and perfectly matching wild-type reads. +keywords: + - dms + - bam + - filtering + - samtools + - indels +tools: + - "samtools": + description: "Tools for manipulating next-generation sequencing data" + homepage: "http://www.htslib.org/" + documentation: "http://www.htslib.org/doc/samtools.html" + tool_dev_url: "https://github.com/samtools/samtools" + doi: "10.1093/bioinformatics/btp352" + licence: ["MIT/Expat"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - bam: + type: file + description: Input BAM file to be filtered + pattern: "*.{bam}" + +output: + - bam: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*.bam": + type: file + description: Filtered BAM file containing only high-quality mutated reads without indels + pattern: "*_filtered.bam" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/bamprocessing/premerge/environment.yml b/modules/local/bamprocessing/premerge/environment.yml new file mode 100644 index 0000000..0e199ab --- /dev/null +++ b/modules/local/bamprocessing/premerge/environment.yml @@ -0,0 +1,7 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bwa=0.7.19 + - bioconda::samtools=1.21 + - bioconda::vsearch=2.30.0 diff --git a/modules/local/bamprocessing/premerge/main.nf b/modules/local/bamprocessing/premerge/main.nf new file mode 100644 index 0000000..fbf433f --- /dev/null +++ b/modules/local/bamprocessing/premerge/main.nf @@ -0,0 +1,51 @@ + +process PREMERGE { + tag "$meta.id" + label 'process_medium' + + conda "${moduleDir}/environment.yml" + container "community.wave.seqera.io/library/bwa_samtools_vsearch:28e8640725d3d8e9" + + input: + tuple val(meta), path(bam) + path wt_seq + + output: + tuple val(meta), path("*.bam"), emit: bam + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + # Convert BAM to paired FASTQ files + samtools fastq -1 forward_reads.fastq -2 reverse_reads.fastq -0 /dev/null -s /dev/null -n $bam + + # Merge paired reads + vsearch --fastq_mergepairs forward_reads.fastq --reverse reverse_reads.fastq --fastqout merged_reads.fastq --fastq_minovlen 10 --fastq_allowmergestagger + + # Re-align merged reads + bwa index $wt_seq + bwa mem $wt_seq merged_reads.fastq | samtools view -Sb - > ${prefix}_merged.bam + + # Save version information + cat <<-END_VERSIONS > versions.yml + "${task.process}": + premerge: \$(samtools --version |& sed '1!d ; s/samtools //') +END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + touch merged_reads.fastq + touch merged_reads.bam + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + premerge: dummy_version +END_VERSIONS + """ +} diff --git a/modules/local/bamprocessing/premerge/meta.yml b/modules/local/bamprocessing/premerge/meta.yml new file mode 100644 index 0000000..76186fe --- /dev/null +++ b/modules/local/bamprocessing/premerge/meta.yml @@ -0,0 +1,65 @@ +name: "premerge" +description: Processes paired-end BAM files by converting them to FASTQ, merging overlapping paired reads using VSEARCH, and re-aligning the merged single-end reads to the wild-type reference using BWA. +keywords: + - paired-end merging + - alignment + - bwa + - vsearch + - samtools + - dms +tools: + - "samtools": + description: "Tools for manipulating next-generation sequencing data" + homepage: "http://www.htslib.org/" + documentation: "http://www.htslib.org/doc/samtools.html" + tool_dev_url: "https://github.com/samtools/samtools" + doi: "10.1093/bioinformatics/btp352" + licence: ["MIT/Expat"] + - "vsearch": + description: "Versatile open-source tool for metagenomics" + homepage: "https://github.com/torognes/vsearch" + documentation: "https://github.com/torognes/vsearch/blob/master/README.md" + doi: "10.7717/peerj.2584" + licence: ["GPL-3.0-or-later", "BSD-2-Clause"] + - "bwa": + description: "Burrow-Wheeler Aligner for short-read alignment" + homepage: "http://bio-bwa.sourceforge.net/" + documentation: "http://bio-bwa.sourceforge.net/bwa.shtml" + doi: "10.1093/bioinformatics/btp324" + licence: ["GPL-3.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - bam: + type: file + description: Input paired-end BAM file + pattern: "*.{bam}" + - - wt_seq: + type: file + description: FASTA file containing the wild-type reference sequence + pattern: "*.{fasta,fa}" + +output: + - bam: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*.bam": + type: file + description: BAM file containing the merged and re-aligned reads + pattern: "*_merged.bam" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/dmsanalysis/aa_seq/environment.yml b/modules/local/dmsanalysis/aa_seq/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/dmsanalysis/aa_seq/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/dmsanalysis/aa_seq/main.nf b/modules/local/dmsanalysis/aa_seq/main.nf new file mode 100644 index 0000000..01ac974 --- /dev/null +++ b/modules/local/dmsanalysis/aa_seq/main.nf @@ -0,0 +1,31 @@ +process DMSANALYSIS_AASEQ { + tag "amino_acid_sequence" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(meta), path(wt_seq) + val pos_range + + output: + tuple val(meta), path("aa_seq.txt"), emit: aa_seq + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'aa_seq.R' + + stub: + """ + touch aa_seq.txt + echo "DMSANALYSIS_AASEQ:" > versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/dmsanalysis/aa_seq/meta.yml b/modules/local/dmsanalysis/aa_seq/meta.yml new file mode 100644 index 0000000..46a761d --- /dev/null +++ b/modules/local/dmsanalysis/aa_seq/meta.yml @@ -0,0 +1,54 @@ +name: "dmsanalysis_aaseq" +description: Translates a wild-type DNA sequence into an amino acid sequence based on a provided coding region (ORF) range. +keywords: + - translation + - amino acid + - dna + - dms + - biostrings +tools: + - "bioconductor-biostrings": + description: "Memory efficient string containers, string matching algorithms, and other utilities, for fast manipulation of large biological sequences or sets of sequences" + homepage: "https://bioconductor.org/packages/Biostrings" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/Biostrings/man/Biostrings.pdf" + licence: ["Artistic-2.0"] + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/mutscan" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'ref' ]` + - wt_seq: + type: file + description: FASTA file containing the wild-type DNA reference sequence + pattern: "*.{fasta,fa}" + - - pos_range: + type: string + description: Start and stop codon positions in the format 'start-stop', e.g., '352-1383' + +output: + - aa_seq: + - meta: + type: map + description: | + Groovy Map containing sample information + - aa_seq.txt: + type: file + description: Text file containing the translated amino acid sequence + pattern: "aa_seq.txt" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/dmsanalysis/aa_seq/templates/aa_seq.R b/modules/local/dmsanalysis/aa_seq/templates/aa_seq.R new file mode 100644 index 0000000..7c2e8e5 --- /dev/null +++ b/modules/local/dmsanalysis/aa_seq/templates/aa_seq.R @@ -0,0 +1,65 @@ +#!/usr/bin/env Rscript + +# input: wildtype-seq, start&stopp pos. +# output: amino acid sequence within the start-stop frame (.txt) + +# Load necessary libraries +suppressMessages(library(Biostrings)) + +# Define the function +aa_seq <- function(wt_seq_input, pos_range, output_file) { + # Parse the start and stop positions from the input format "start-stop" + positions <- unlist(strsplit(pos_range, "-")) + start_pos <- as.numeric(positions[1]) + stop_pos <- as.numeric(positions[2]) + + # Check if the input is a file or a string + if (file.exists(wt_seq_input)) { + # If it's a file, read the sequence from the fasta file + seq_data <- readDNAStringSet(filepath = wt_seq_input) + wt_seq <- seq_data[[1]] # Extract the sequence + } else { + # Otherwise, treat the input as a sequence string + wt_seq <- DNAString(wt_seq_input) + } + + # Extract the sequence between start and stop codons + coding_seq <- subseq(wt_seq, start = start_pos, end = stop_pos) + + # Translate the coding sequence into an amino acid sequence + aa_seq <- translate(coding_seq) + + # Write the amino acid sequence to a .txt file + write(as.character(aa_seq), file = output_file) +} + + +##### +# run function +##### +aa_seq( + wt_seq_input = "$wt_seq", + "$pos_range", + "aa_seq.txt" + ) + + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +Biostrings_version <- as.character(packageVersion("Biostrings")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(Biostrings_version) == 0) Biostrings_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-Biostrings:', Biostrings_version) + ), + f +) +close(f) diff --git a/modules/local/dmsanalysis/error_correction_false_doubles/environment.yml b/modules/local/dmsanalysis/error_correction_false_doubles/environment.yml new file mode 100644 index 0000000..07bd598 --- /dev/null +++ b/modules/local/dmsanalysis/error_correction_false_doubles/environment.yml @@ -0,0 +1,7 @@ +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::r-base=4.5.1 + - conda-forge::r-stringr=1.6.0 + - bioconda::bioconductor-biostrings=2.78.0 diff --git a/modules/local/dmsanalysis/error_correction_false_doubles/main.nf b/modules/local/dmsanalysis/error_correction_false_doubles/main.nf new file mode 100644 index 0000000..c43bd23 --- /dev/null +++ b/modules/local/dmsanalysis/error_correction_false_doubles/main.nf @@ -0,0 +1,43 @@ +process DMSANALYSIS_ERROR_CORRECTION_FALSE_DOUBLES { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(meta), path(raw_counts), path(filtered), path(completed) + path wt_fasta + path aa_seq + val min_counts + val method + val codon_window + + output: + tuple val(meta), + path("variantCounts_filtered_by_library_error_corrected.csv"), + path("variantCounts_for_heatmaps_error_corrected.csv"), + path("library_completed_variantCounts_error_corrected.csv"), + emit: corrected + tuple val(meta), path("seq_error_rate.csv"), emit: seq_error_rate + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'error_correction_false_doubles.R' + + stub: + """ + touch variantCounts_filtered_by_library_error_corrected.csv + touch variantCounts_for_heatmaps_error_corrected.csv + touch library_completed_variantCounts_error_corrected.csv + touch seq_error_rate.csv + echo "DMSANALYSIS_ERROR_CORRECTION_FALSE_DOUBLES:" > versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/dmsanalysis/error_correction_false_doubles/meta.yml b/modules/local/dmsanalysis/error_correction_false_doubles/meta.yml new file mode 100644 index 0000000..5840a90 --- /dev/null +++ b/modules/local/dmsanalysis/error_correction_false_doubles/meta.yml @@ -0,0 +1,66 @@ +name: "dmsanalysis_error_correction_false_doubles" +description: "Sequencing-error correction of single-codon variant counts using the false-double-mutant model: because the library only contains single-codon changes, observed multi-codon variants are treated as errors and used to estimate and subtract the per-nucleotide error rate." +keywords: + - deep mutational scanning + - error correction + - false doubles + - variant counting +tools: + - "r-base": + description: "The R programming language (base only)" + homepage: "https://www.r-project.org/" + licence: ["GPL-2.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', sample:'ORF1', type:'input', replicate:1 ]` + - raw_counts: + type: file + description: Raw (pre-library-filter) variant count table from VARIANTCOUNTING. + pattern: "*.variant_counts.tsv" + - filtered: + type: file + description: Library-filtered single-codon variant counts (annotated CSV). + pattern: "*.csv" + - completed: + type: file + description: Completed library table (all possible variants) to also correct. + pattern: "*.csv" + - - aa_seq: + type: file + description: Reference amino-acid sequence (one line). + - - min_counts: + type: integer + description: Minimum count threshold for the amino-acid-level heatmap table. + +output: + - corrected: + - meta: + type: map + description: Groovy Map containing sample information + - variantCounts_filtered_by_library_error_corrected.csv: + type: file + description: Library-filtered counts with added `counts_corrected` / `counts_per_cov_corrected` columns. + pattern: "variantCounts_filtered_by_library_error_corrected.csv" + - variantCounts_for_heatmaps_error_corrected.csv: + type: file + description: Amino-acid-level heatmap table built from corrected counts (canonical `total_counts` columns). + pattern: "variantCounts_for_heatmaps_error_corrected.csv" + - library_completed_variantCounts_error_corrected.csv: + type: file + description: Completed library table with corrected counts propagated (for logdiff etc.). + pattern: "library_completed_variantCounts_error_corrected.csv" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" +maintainers: + - "@BenjaminWehnert1008" diff --git a/modules/local/dmsanalysis/error_correction_false_doubles/templates/error_correction_false_doubles.R b/modules/local/dmsanalysis/error_correction_false_doubles/templates/error_correction_false_doubles.R new file mode 100644 index 0000000..4828a4c --- /dev/null +++ b/modules/local/dmsanalysis/error_correction_false_doubles/templates/error_correction_false_doubles.R @@ -0,0 +1,807 @@ +#!/usr/bin/env Rscript + +## false-double mutant codon sequencing-error correction in nf-core/deepmutscan (nucleotide level). +## +## Two estimators, ported VERBATIM from Maxi's scripts (24.06.2026): +## * MLE (default): per-SNV maximum-likelihood error rate e = F / m, applied per variant. +## * EB: pool F and m across SNVs, then a Gamma-Poisson empirical-Bayes fit with +## six-category shrinkage -> posterior-mean rate e_EB. +## `$method` (mle|eb) selects which one runs. Everything except I/O plumbing and the dispatch is +## kept exactly as written. +## +## Nextflow template: an R `\$` is a literal dollar; backslashes meant for R are doubled. + +suppressMessages({ + library(Biostrings) + library(stringr) +}) + +## ------------------------------------------------------------------------------------------------- +## MLE estimator (verbatim) +## ------------------------------------------------------------------------------------------------- +seq_error_correct_by_false_doubles_MLE <- function(wt_path, input_count_path_raw, input_count_path_processed, output_file_path, seq_error_rate_path, codon_window){ + + ## load data (nucleotide-level counts) + + # WT sequence + wt.seq <- readDNAStringSet(wt_path) + + # for the seq error rates, build a vector to be filled + seq.error.names <- c() + for(i in 1:nchar(wt.seq)){ + tmp.wt <- strsplit(as.character(wt.seq), "")[[1]][i] + seq.error.names <- c(seq.error.names, paste0(i, ":", tmp.wt, ">", c("A", "T", "G", "C"))[-match(tmp.wt, c("A", "T", "G", "C"))]) + } + seq.error.rate <- matrix(0, nrow = length(seq.error.names), ncol = 2) + rownames(seq.error.rate) <- seq.error.names + colnames(seq.error.rate) <- c("1nt counts/coverage", "1nt false counts/coverage") + + # Set the column names + colnames <- c("counts", "cov", "mean_length_variant_reads", "varying_bases", + "base_mut", "varying_codons", "codon_mut", "aa_mut", "pos_mut") + input.counts.raw <- read.table(input_count_path_raw, sep = "\\t", header = FALSE, fill = TRUE, col.names = colnames) + input.counts.processed <- read.csv(input_count_path_processed) + + ## append key columns + input.counts.processed\$counts_corrected <- input.counts.processed\$counts + input.counts.processed\$counts_per_cov_corrected <- input.counts.processed\$counts_per_cov + + ## calculate some things upfront + + ### highest accessible codon + max.codon <- max(as.numeric(str_split_fixed(input.counts.raw\$codon_mut, ":", 2)[,1]), na.rm = T) + + ### total distribution of false double mutant coverage decay (%) vs. codon-codon distance + cat("Estimating the distance-dependent coverage decay of false double mutants...\\n") + all.false.doubles <- input.counts.raw + all.false.doubles <- all.false.doubles[which(all.false.doubles[,"varying_bases"] >= 3 & all.false.doubles[,"varying_bases"] < 5 & all.false.doubles[,"varying_codons"] == 2),,drop = F] + all.false.doubles\$codon_dist <- vapply(regmatches(all.false.doubles[,"codon_mut"], gregexpr("\\\\d+(?=:)", all.false.doubles[,"codon_mut"], perl = TRUE)), function(z) abs(diff(as.integer(z))), integer(1)) + + ### additional filters: + + #### remove outlier double codon mutants with very high counts + all.false.doubles <- all.false.doubles[which(all.false.doubles\$counts <= quantile(all.false.doubles\$counts, c(0.995))),] + + #### only keep 2+1 nt and 3+1 nt double codon mutants + all.false.doubles.nt <- all.false.doubles\$codon_mut + all.false.doubles.nt <- str_split_fixed(all.false.doubles.nt, ", ", 2) + all.false.doubles.nt[,1] <- str_split_fixed(all.false.doubles.nt[,1], ":", 2)[,2] + all.false.doubles.nt[,2] <- str_split_fixed(all.false.doubles.nt[,2], ":", 2)[,2] + classify_double_codon_variant <- function(x) { + parts <- strsplit(x, ">", fixed = TRUE) + n_changes <- vapply(parts, function(p) { + if (length(p) != 2L || nchar(p[1]) != nchar(p[2])) { + return(NA_integer_) + } + sum(strsplit(p[1], "")[[1]] != strsplit(p[2], "")[[1]]) + }, integer(1)) + labels <- c(1, 2, 3) + ifelse(n_changes %in% 1:3, + labels[n_changes], + ifelse(n_changes == 0L, "no change", NA_character_)) + } + all.false.doubles.nt[,1] <- classify_double_codon_variant(all.false.doubles.nt[,1]) + all.false.doubles.nt[,2] <- classify_double_codon_variant(all.false.doubles.nt[,2]) + if(length(which(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 2) != 0)){ + all.false.doubles <- all.false.doubles[-which(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 2),] + all.false.doubles.nt <- all.false.doubles.nt[-which(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 2),] + } + if(length(which(all.false.doubles.nt[,1] == 1 & all.false.doubles.nt[,2] == 1) != 0)){ + all.false.doubles <- all.false.doubles[-which(all.false.doubles.nt[,1] == 1 & all.false.doubles.nt[,2] == 1),] + all.false.doubles.nt <- all.false.doubles.nt[-which(all.false.doubles.nt[,1] == 1 & all.false.doubles.nt[,2] == 1),] + } + if(length(which(c(all.false.doubles.nt[,1] == 3 & all.false.doubles.nt[,2] == 2) | c(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 3)) != 0)){ + all.false.doubles <- all.false.doubles[-which(c(all.false.doubles.nt[,1] == 3 & all.false.doubles.nt[,2] == 2) | c(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 3)),] + all.false.doubles.nt <- all.false.doubles.nt[-which(c(all.false.doubles.nt[,1] == 3 & all.false.doubles.nt[,2] == 2) | c(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 3)),] + } + if(length(which(is.na(all.false.doubles.nt[,1]) == T | is.na(all.false.doubles.nt[,2]) == T) != 0)){ + all.false.doubles <- all.false.doubles[-which(is.na(all.false.doubles.nt[,1]) == T | is.na(all.false.doubles.nt[,2]) == T),] + all.false.doubles.nt <- all.false.doubles.nt[-which(is.na(all.false.doubles.nt[,1]) == T | is.na(all.false.doubles.nt[,2]) == T),] + } + + all.false.doubles.codons <- str_split_fixed(all.false.doubles\$codon_mut, ", ", 2) + all.false.doubles.codons[,1] <- as.integer(sub(":.*", "", all.false.doubles.codons[,1])) + all.false.doubles.codons[,2] <- as.integer(sub(":.*", "", all.false.doubles.codons[,2])) + median.high.conf.coverage.per.pos <- rep(NA, max.codon) + names(median.high.conf.coverage.per.pos) <- 1:max.codon + for(i in 1:length(median.high.conf.coverage.per.pos)){ + median.high.conf.coverage.per.pos[i] <- median(input.counts.processed[which(names(median.high.conf.coverage.per.pos)[i] == str_split_fixed(input.counts.processed\$codon_mut, ":", 2)[,1]),"cov"]) + } + all.false.doubles.codons[,1] <- median.high.conf.coverage.per.pos[match(all.false.doubles.codons[,1], names(median.high.conf.coverage.per.pos))] + all.false.doubles.codons[,2] <- median.high.conf.coverage.per.pos[match(all.false.doubles.codons[,2], names(median.high.conf.coverage.per.pos))] + all.false.doubles\$max_cov_possible <- as.numeric(apply(all.false.doubles.codons, 1, min)) + all.false.doubles\$perc_cov_to_max <- 100 * all.false.doubles\$cov / all.false.doubles\$max_cov_possible + + ## deepmutscan guard (not in the verbatim script): a run can legitimately contain NO observed false + ## double mutants -- a small, shallow or tightly-filtered library, or the nf-core test dataset, where + ## every variant is single-codon. The verbatim code then hands lm() an empty frame and dies with + ## "0 (non-NA) cases". With no false doubles anywhere there is also no evidence of codon-level + ## sequencing error to subtract, so the correct answer is a no-op: corrected == raw. The quadratic + ## needs >= 3 distinct codon distances to be estimable at all, so require that too. + .usable <- is.finite(log(all.false.doubles\$perc_cov_to_max)) + if (nrow(all.false.doubles) == 0 || length(unique(all.false.doubles\$codon_dist[.usable])) < 3) { + cat("WARNING: only", sum(.usable), "usable false double mutant(s) found - the distance-dependent\\n", + "coverage decay is not estimable, so NO sequencing-error correction is applied and the\\n", + "corrected counts are identical to the raw counts.\\n") + write.csv(seq.error.rate, file = seq_error_rate_path, row.names = T) + write.csv(input.counts.processed, file = output_file_path, row.names = F) + return(invisible(NULL)) + } + + ## keep the full distance range before dropping rows, so the lookup below stays long enough + .max_dist <- max(c(all.false.doubles\$codon_dist, codon_window), na.rm = TRUE) + ## fit on the finite rows only - lm() errors on the -Inf that log() gives a zero-coverage double + all.false.doubles <- all.false.doubles[.usable, , drop = FALSE] + + fit <- lm(log(all.false.doubles\$perc_cov_to_max) ~ all.false.doubles\$codon_dist + I(all.false.doubles\$codon_dist^2)) + all.false.doubles\$pred_cov_perc <- exp(predict(fit)) + + ### convert this into a look-up table: "% max. possible coverage" depending on codon-codon distance + dual.codon.coverage.estimate <- matrix(NA, nrow = .max_dist, ncol = 2) + colnames(dual.codon.coverage.estimate) <- c("Codon distance", "Estimate % max. possible coverage") + dual.codon.coverage.estimate[,"Codon distance"] <- 1:nrow(dual.codon.coverage.estimate) + ## deepmutscan fix (Maxi-approved deviation from the verbatim script): fill the coverage-decay + ## lookup from the fitted quadratic evaluated at EVERY integer codon distance, instead of match()ing + ## only the distances that happen to have an observed double. `pred_cov_perc = exp(predict(fit))` + ## depends solely on codon_dist, so this is bit-identical to the original at every observed distance + ## and merely fills the previously-NA gaps -- those NAs otherwise crash MLE and leave NA corrected + ## counts in EB (a distance with no observed double, e.g. around codon 288 on the GID1A min_counts=10 + ## data). + .dccd <- dual.codon.coverage.estimate[,"Codon distance"] + .cf <- coef(fit) + dual.codon.coverage.estimate[,"Estimate % max. possible coverage"] <- exp(.cf[1] + .cf[2] * .dccd + .cf[3] * .dccd^2) + + ## Process the count file, only look at single nucleotide variants + cat("Sequencing error correction of raw counts...\\n") + for (i in grep("[,]", input.counts.processed[,"base_mut"], invert = T)){ + + ### algorithm: + ### 1.) isolate 1nt variant of interest + ### 2.) look for 2/3nt hits in a pre-specified window of codons upstream or downstream + ### -> if there are zero false double mutants: skip / set correction factor to zero + ### -> if there are any false double mutants: continue + ### 3.) generate a table for ALL possible 2/3nt variants in the selected window: + ### true high-conf. variant count | true high-conf. variant coverage | false double variant count (often 0 or 1) | false double variant coverage + ### 4.) Maximum likelihood estimation (MLE) over all events + ### 5.) Apply correction factor + + ## 1.) isolate 1nt variant of interest + tmp.single <- input.counts.processed[i,"base_mut"] + seq.error.rate[tmp.single, "1nt counts/coverage"] <- input.counts.processed[i,"counts_per_cov"] + + ## 2.) look for 2/3nt hits in up to 40 codons (120 bp) upstream or downstream + tmp.false.doubles <- all.false.doubles[grep(paste0("(?= tmp.min.from.ref & + as.numeric(str_split_fixed(tmp.summary.table.entries\$codon_mut, ":", 2)[,1]) <= tmp.max.from.ref),] + tmp.summary.table.entries <- tmp.summary.table.entries[which(tmp.summary.table.entries\$varying_bases > 1),] + + ### fill the table + tmp.summary.table <- matrix(NA, ncol = 5, nrow = nrow(tmp.summary.table.entries)) + colnames(tmp.summary.table) <- c("codon distance", + "true high-conf. variant count", + "true high-conf. variant coverage", + "false double variant count", + "false double variant coverage") + rownames(tmp.summary.table) <- paste0(c(tmp.summary.table.entries\$codon_mut),", ", c(tmp.summary.table.entries\$base_mut)) + + tmp.summary.table[,"codon distance"] <- abs(as.numeric(str_split_fixed(tmp.summary.table.entries\$codon_mut, ":", 2)[,1]) - tmp.ref.codon) + tmp.summary.table[,"true high-conf. variant count"] <- tmp.summary.table.entries\$counts + tmp.summary.table[,"true high-conf. variant coverage"] <- tmp.summary.table.entries\$cov + tmp.summary.table[,"false double variant count"] <- 0 + + ## input the actual hits + tmp.double.codon <- str_split_fixed(tmp.false.doubles\$codon_mut, ", ", 2) + tmp.match1 <- match(tmp.double.codon[,1], str_split_fixed(rownames(tmp.summary.table), ", ", 2)[,1]) + tmp.match1[is.na(tmp.match1)] <- 0 + tmp.match2 <- match(tmp.double.codon[,2], str_split_fixed(rownames(tmp.summary.table), ", ", 2)[,1]) + tmp.match2[is.na(tmp.match2)] <- 0 + tmp.match <- c(tmp.match1 + tmp.match2) + + ### remove those false doubles for which there are no matched true high confidence calls + if(any(tmp.match == 0)){ + tmp.false.doubles <- tmp.false.doubles[-which(tmp.match == 0),,drop = F] + tmp.match <- tmp.match[-which(tmp.match == 0)] + if(nrow(tmp.false.doubles) == 0){ + next + } + } + + tmp.summary.table[tmp.match,"false double variant count"] <- tmp.false.doubles[,"counts"] + tmp.summary.table[tmp.match,"false double variant coverage"] <- tmp.false.doubles[,"cov"] + tmp.summary.table <- as.data.frame(tmp.summary.table) + + ## 4.) Maximum likelihood estimation (MLE) over all events, iterating over codon positions + + ### summarise data by position + tmp.summary.table.pos <- matrix(NA, nrow = length(unique(str_split_fixed(rownames(tmp.summary.table), ">", 2)[,1])), ncol = 5) + colnames(tmp.summary.table.pos) <- colnames(tmp.summary.table) + rownames(tmp.summary.table.pos) <- unique(str_split_fixed(rownames(tmp.summary.table), ">", 2)[,1]) + tmp.summary.table.pos[,"codon distance"] <- abs(as.numeric(str_split_fixed(rownames(tmp.summary.table.pos), ":", 2)[,1]) - as.numeric(str_split_fixed(input.counts.processed[i,"codon_mut"], ":", 2)[,1])) + for(j in 1:nrow(tmp.summary.table.pos)){ + tmp.name <- rownames(tmp.summary.table.pos)[j] + tmp.summary.table.pos[j,"true high-conf. variant count"] <- sum(tmp.summary.table[grep(paste0("^",tmp.name), rownames(tmp.summary.table)),"true high-conf. variant count"]) + tmp.summary.table.pos[j,"true high-conf. variant coverage"] <- max(tmp.summary.table[grep(paste0("^",tmp.name), rownames(tmp.summary.table)),"true high-conf. variant coverage"]) + tmp.summary.table.pos[j,"false double variant count"] <- sum(tmp.summary.table[grep(paste0("^",tmp.name), rownames(tmp.summary.table)),"false double variant count"]) + tmp.summary.table.pos[j,"false double variant coverage"] <- max(tmp.summary.table[grep(paste0("^",tmp.name), rownames(tmp.summary.table)),"false double variant coverage"], na.rm = T) + + ## infer a good proxy for the false double variant coverage with 0 counts (based estimated distance-dependent % coverage decay observed across all observed double mutants) + if(tmp.summary.table.pos[j,"false double variant coverage"] == "-Inf"){ + tmp.summary.table.pos[j,"false double variant coverage"] <- c(dual.codon.coverage.estimate[abs(tmp.summary.table.pos[j,"codon distance"]),"Estimate % max. possible coverage"] / 100) * tmp.summary.table.pos[j,"true high-conf. variant coverage"] + tmp.summary.table.pos[j,"false double variant coverage"] <- round(tmp.summary.table.pos[j,"false double variant coverage"]) + } + } + tmp.summary.table.pos <- as.data.frame(tmp.summary.table.pos) + e_MLE <- sum(tmp.summary.table.pos\$`false double variant count`) / + sum(tmp.summary.table.pos\$`true high-conf. variant count` * c(tmp.summary.table.pos\$`false double variant coverage` / tmp.summary.table.pos\$`true high-conf. variant coverage`)) + seq.error.rate[tmp.single,"1nt false counts/coverage"] <- e_MLE + ## clamped here too - the guard below only tests the rounded count, so a subtraction landing in + ## (-0.5, 0] rounds to 0, skips the guard, and leaves a negative frequency behind + input.counts.processed[i,"counts_per_cov_corrected"] <- max(0, input.counts.processed[i,"counts_per_cov"] - e_MLE) + + ## 5.) Apply correction factor + input.counts.processed[i,"counts_corrected"] <- input.counts.processed[i,"counts"] - c(e_MLE * input.counts.processed[i,"cov"]) + + ## round to nearest integer, do not allow for negative counts + input.counts.processed[i,"counts_corrected"] <- round(input.counts.processed[i,"counts_corrected"]) + if(input.counts.processed[i,"counts_corrected"] < 0){ + input.counts.processed[i,"counts_corrected"] <- 0 + input.counts.processed[i,"counts_per_cov_corrected"] <- 0 + } + + } + + # Write the processed data + write.csv(seq.error.rate, file = seq_error_rate_path, row.names = T) + write.csv(input.counts.processed, file = output_file_path, row.names = F) + +} + +## ------------------------------------------------------------------------------------------------- +## EB estimator (verbatim) +## ------------------------------------------------------------------------------------------------- +seq_error_correct_by_false_doubles_EB <- function(wt_path, input_count_path_raw, input_count_path_processed, output_file_path, seq_error_rate_path, codon_window){ + + ## load data (nucleotide-level counts) + + # WT sequence + wt.seq <- readDNAStringSet(wt_path) + + # for the seq error rates, build a vector to be filled + seq.error.names <- c() + for(i in 1:nchar(wt.seq)){ + tmp.wt <- strsplit(as.character(wt.seq), "")[[1]][i] + seq.error.names <- c(seq.error.names, paste0(i, ":", tmp.wt, ">", c("A", "T", "G", "C"))[-match(tmp.wt, c("A", "T", "G", "C"))]) + } + seq.error.rate <- matrix(0, nrow = length(seq.error.names), ncol = 2) + rownames(seq.error.rate) <- seq.error.names + colnames(seq.error.rate) <- c("1nt counts/coverage", "1nt false counts/coverage") + + # Set the column names + colnames <- c("counts", "cov", "mean_length_variant_reads", "varying_bases", + "base_mut", "varying_codons", "codon_mut", "aa_mut", "pos_mut") + input.counts.raw <- read.table(input_count_path_raw, sep = "\\t", header = FALSE, fill = TRUE, col.names = colnames) + input.counts.processed <- read.csv(input_count_path_processed) + + ## append key columns + input.counts.processed\$counts_corrected <- input.counts.processed\$counts + input.counts.processed\$counts_per_cov_corrected <- input.counts.processed\$counts_per_cov + + ## calculate some things upfront + + ### highest accessible codon + max.codon <- max(as.numeric(str_split_fixed(input.counts.raw\$codon_mut, ":", 2)[,1]), na.rm = T) + + ### total distribution of false double mutant coverage decay (%) vs. codon-codon distance + cat("Estimating the distance-dependent coverage decay of false double mutants...\\n") + all.false.doubles <- input.counts.raw + all.false.doubles <- all.false.doubles[which(all.false.doubles[,"varying_bases"] >= 3 & all.false.doubles[,"varying_bases"] < 5 & all.false.doubles[,"varying_codons"] == 2),,drop = F] + all.false.doubles\$codon_dist <- vapply(regmatches(all.false.doubles[,"codon_mut"], gregexpr("\\\\d+(?=:)", all.false.doubles[,"codon_mut"], perl = TRUE)), function(z) abs(diff(as.integer(z))), integer(1)) + + ### additional filters: + + #### remove outlier double codon mutants with very high counts + all.false.doubles <- all.false.doubles[which(all.false.doubles\$counts <= quantile(all.false.doubles\$counts, c(0.995))),] + + #### only keep 2+1 nt and 3+1 nt double codon mutants + all.false.doubles.nt <- all.false.doubles\$codon_mut + all.false.doubles.nt <- str_split_fixed(all.false.doubles.nt, ", ", 2) + all.false.doubles.nt[,1] <- str_split_fixed(all.false.doubles.nt[,1], ":", 2)[,2] + all.false.doubles.nt[,2] <- str_split_fixed(all.false.doubles.nt[,2], ":", 2)[,2] + classify_double_codon_variant <- function(x) { + parts <- strsplit(x, ">", fixed = TRUE) + n_changes <- vapply(parts, function(p) { + if (length(p) != 2L || nchar(p[1]) != nchar(p[2])) { + return(NA_integer_) + } + sum(strsplit(p[1], "")[[1]] != strsplit(p[2], "")[[1]]) + }, integer(1)) + labels <- c(1, 2, 3) + ifelse(n_changes %in% 1:3, + labels[n_changes], + ifelse(n_changes == 0L, "no change", NA_character_)) + } + all.false.doubles.nt[,1] <- classify_double_codon_variant(all.false.doubles.nt[,1]) + all.false.doubles.nt[,2] <- classify_double_codon_variant(all.false.doubles.nt[,2]) + if(length(which(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 2) != 0)){ + all.false.doubles <- all.false.doubles[-which(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 2),] + all.false.doubles.nt <- all.false.doubles.nt[-which(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 2),] + } + if(length(which(all.false.doubles.nt[,1] == 1 & all.false.doubles.nt[,2] == 1) != 0)){ + all.false.doubles <- all.false.doubles[-which(all.false.doubles.nt[,1] == 1 & all.false.doubles.nt[,2] == 1),] + all.false.doubles.nt <- all.false.doubles.nt[-which(all.false.doubles.nt[,1] == 1 & all.false.doubles.nt[,2] == 1),] + } + if(length(which(c(all.false.doubles.nt[,1] == 3 & all.false.doubles.nt[,2] == 2) | c(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 3)) != 0)){ + all.false.doubles <- all.false.doubles[-which(c(all.false.doubles.nt[,1] == 3 & all.false.doubles.nt[,2] == 2) | c(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 3)),] + all.false.doubles.nt <- all.false.doubles.nt[-which(c(all.false.doubles.nt[,1] == 3 & all.false.doubles.nt[,2] == 2) | c(all.false.doubles.nt[,1] == 2 & all.false.doubles.nt[,2] == 3)),] + } + if(length(which(is.na(all.false.doubles.nt[,1]) == T | is.na(all.false.doubles.nt[,2]) == T) != 0)){ + all.false.doubles <- all.false.doubles[-which(is.na(all.false.doubles.nt[,1]) == T | is.na(all.false.doubles.nt[,2]) == T),] + all.false.doubles.nt <- all.false.doubles.nt[-which(is.na(all.false.doubles.nt[,1]) == T | is.na(all.false.doubles.nt[,2]) == T),] + } + + all.false.doubles.codons <- str_split_fixed(all.false.doubles\$codon_mut, ", ", 2) + all.false.doubles.codons[,1] <- as.integer(sub(":.*", "", all.false.doubles.codons[,1])) + all.false.doubles.codons[,2] <- as.integer(sub(":.*", "", all.false.doubles.codons[,2])) + median.high.conf.coverage.per.pos <- rep(NA, max(as.numeric(str_split_fixed(input.counts.processed\$codon_mut, ":", 2)[,1]))) + names(median.high.conf.coverage.per.pos) <- 1:max(as.numeric(str_split_fixed(input.counts.processed\$codon_mut, ":", 2)[,1])) + for(i in 1:length(median.high.conf.coverage.per.pos)){ + median.high.conf.coverage.per.pos[i] <- median(input.counts.processed[which(names(median.high.conf.coverage.per.pos)[i] == str_split_fixed(input.counts.processed\$codon_mut, ":", 2)[,1]),"cov"]) + } + all.false.doubles.codons[,1] <- median.high.conf.coverage.per.pos[match(all.false.doubles.codons[,1], names(median.high.conf.coverage.per.pos))] + all.false.doubles.codons[,2] <- median.high.conf.coverage.per.pos[match(all.false.doubles.codons[,2], names(median.high.conf.coverage.per.pos))] + all.false.doubles\$max_cov_possible <- as.numeric(apply(all.false.doubles.codons, 1, min)) + all.false.doubles\$perc_cov_to_max <- 100 * all.false.doubles\$cov / all.false.doubles\$max_cov_possible + + ## deepmutscan guard (not in the verbatim script) - see the identical guard in the MLE estimator. + ## With no estimable coverage decay there is no evidence of codon-level sequencing error, so the + ## correct answer is a no-op: corrected == raw, rather than lm() dying on an empty frame. + .usable <- is.finite(log(all.false.doubles\$perc_cov_to_max)) + if (nrow(all.false.doubles) == 0 || length(unique(all.false.doubles\$codon_dist[.usable])) < 3) { + cat("WARNING: only", sum(.usable), "usable false double mutant(s) found - the distance-dependent\\n", + "coverage decay is not estimable, so NO sequencing-error correction is applied and the\\n", + "corrected counts are identical to the raw counts.\\n") + write.csv(seq.error.rate, file = seq_error_rate_path, row.names = T) + write.csv(input.counts.processed, file = output_file_path, row.names = F) + return(invisible(NULL)) + } + + ## keep the full distance range before dropping rows, so the lookup below stays long enough + .max_dist <- max(c(all.false.doubles\$codon_dist, codon_window), na.rm = TRUE) + ## fit on the finite rows only - lm() errors on the -Inf that log() gives a zero-coverage double + all.false.doubles <- all.false.doubles[.usable, , drop = FALSE] + + fit <- lm(log(all.false.doubles\$perc_cov_to_max) ~ all.false.doubles\$codon_dist + I(all.false.doubles\$codon_dist^2)) + all.false.doubles\$pred_cov_perc <- exp(predict(fit)) + + ### convert this into a look-up table: "% max. possible coverage" depending on codon-codon distance + dual.codon.coverage.estimate <- matrix(NA, nrow = .max_dist, ncol = 2) + colnames(dual.codon.coverage.estimate) <- c("Codon distance", "Estimate % max. possible coverage") + dual.codon.coverage.estimate[,"Codon distance"] <- 1:nrow(dual.codon.coverage.estimate) + ## deepmutscan fix (Maxi-approved deviation from the verbatim script): fill the coverage-decay + ## lookup from the fitted quadratic evaluated at EVERY integer codon distance, instead of match()ing + ## only the distances that happen to have an observed double. `pred_cov_perc = exp(predict(fit))` + ## depends solely on codon_dist, so this is bit-identical to the original at every observed distance + ## and merely fills the previously-NA gaps -- those NAs otherwise crash MLE and leave NA corrected + ## counts in EB (a distance with no observed double, e.g. around codon 288 on the GID1A min_counts=10 + ## data). + .dccd <- dual.codon.coverage.estimate[,"Codon distance"] + .cf <- coef(fit) + dual.codon.coverage.estimate[,"Estimate % max. possible coverage"] <- exp(.cf[1] + .cf[2] * .dccd + .cf[3] * .dccd^2) + + ## Process the count file, only look at single nucleotide variants + cat("Sequencing error correction of raw counts...\\n") + out.summary <- matrix(NA, ncol = 7, nrow = length(grep("[,]", input.counts.processed[,"base_mut"], invert = T))) + colnames(out.summary) <- c("SNV", "Category", + "False_double_variant_count", "False_double_variant_coverage", + "True_high_conf_variant_count", "True_high_conf_variant_coverage", + "Exposure_m") + out.summary[,"SNV"] <- input.counts.processed[grep("[,]", input.counts.processed[,"base_mut"], invert = T),"base_mut"] + out.summary[,"Category"] <- str_split_fixed(out.summary[,"SNV"], ":", 2)[,2] + category.translate <- cbind(c("C>A", "C>G", "C>T", "T>A", "T>C", "T>G"), + c("G>T", "G>C", "G>A", "A>T", "A>G", "A>C")) + for(i in 1:nrow(out.summary)){ + if(out.summary[i,"Category"] %in% category.translate[,2]){ + out.summary[i,"Category"] <- category.translate[match(out.summary[i,"Category"],category.translate[,2]),1] + } + } + + for (i in grep("[,]", input.counts.processed[,"base_mut"], invert = T)){ + + ### algorithm: + ### 1.) isolate 1nt variant of interest + ### 2.) look for 2/3nt hits in a pre-specified window of codons upstream or downstream + ### -> if there are zero false double mutants: skip / set correction factor to zero + ### -> if there are any false double mutants: continue + ### 3.) generate a table for ALL possible 2/3nt variants in the selected window: + ### true high-conf. variant count | true high-conf. variant coverage | false double variant count (often 0 or 1) | false double variant coverage + ### 4.) Iterate of all positions + ### 5.) Calculate the empirical Bayes + ### 6.) Apply correction factor + + ## 1.) isolate 1nt variant of interest + tmp.single <- input.counts.processed[i,"base_mut"] + seq.error.rate[tmp.single, "1nt counts/coverage"] <- input.counts.processed[i,"counts_per_cov"] + + ## 2.) look for 2/3nt hits in up to 40 codons (120 bp) upstream or downstream + tmp.false.doubles <- all.false.doubles[grep(paste0("(?= tmp.min.from.ref & + as.numeric(str_split_fixed(tmp.summary.table.entries\$codon_mut, ":", 2)[,1]) <= tmp.max.from.ref),] + tmp.summary.table.entries <- tmp.summary.table.entries[which(tmp.summary.table.entries\$varying_bases > 1),] + + ### fill the table + tmp.summary.table <- matrix(NA, ncol = 5, nrow = nrow(tmp.summary.table.entries)) + colnames(tmp.summary.table) <- c("codon distance", + "true high-conf. variant count", + "true high-conf. variant coverage", + "false double variant count", + "false double variant coverage") + rownames(tmp.summary.table) <- paste0(c(tmp.summary.table.entries\$codon_mut),", ", c(tmp.summary.table.entries\$base_mut)) + + tmp.summary.table[,"codon distance"] <- abs(as.numeric(str_split_fixed(tmp.summary.table.entries\$codon_mut, ":", 2)[,1]) - tmp.ref.codon) + tmp.summary.table[,"true high-conf. variant count"] <- tmp.summary.table.entries\$counts + tmp.summary.table[,"true high-conf. variant coverage"] <- tmp.summary.table.entries\$cov + tmp.summary.table[,"false double variant count"] <- 0 + + ## input the actual hits + tmp.double.codon <- str_split_fixed(tmp.false.doubles\$codon_mut, ", ", 2) + tmp.match1 <- match(tmp.double.codon[,1], str_split_fixed(rownames(tmp.summary.table), ", ", 2)[,1]) + tmp.match1[is.na(tmp.match1)] <- 0 + tmp.match2 <- match(tmp.double.codon[,2], str_split_fixed(rownames(tmp.summary.table), ", ", 2)[,1]) + tmp.match2[is.na(tmp.match2)] <- 0 + tmp.match <- c(tmp.match1 + tmp.match2) + + ### remove those false doubles for which there are no matched true high confidence calls + if(any(tmp.match == 0)){ + tmp.false.doubles <- tmp.false.doubles[-which(tmp.match == 0),,drop = F] + tmp.match <- tmp.match[-which(tmp.match == 0)] + if(nrow(tmp.false.doubles) == 0){ + next + } + } + + tmp.summary.table[tmp.match,"false double variant count"] <- tmp.false.doubles[,"counts"] + tmp.summary.table[tmp.match,"false double variant coverage"] <- tmp.false.doubles[,"cov"] + tmp.summary.table <- as.data.frame(tmp.summary.table) + + ## 4.) Iterate over codon positions + + ### summarise data by position + tmp.summary.table.pos <- matrix(NA, nrow = length(unique(str_split_fixed(rownames(tmp.summary.table), ">", 2)[,1])), ncol = 5) + colnames(tmp.summary.table.pos) <- colnames(tmp.summary.table) + rownames(tmp.summary.table.pos) <- unique(str_split_fixed(rownames(tmp.summary.table), ">", 2)[,1]) + tmp.summary.table.pos[,"codon distance"] <- abs(as.numeric(str_split_fixed(rownames(tmp.summary.table.pos), ":", 2)[,1]) - as.numeric(str_split_fixed(input.counts.processed[i,"codon_mut"], ":", 2)[,1])) + for(j in 1:nrow(tmp.summary.table.pos)){ + tmp.name <- rownames(tmp.summary.table.pos)[j] + tmp.summary.table.pos[j,"true high-conf. variant count"] <- sum(tmp.summary.table[grep(paste0("^",tmp.name), rownames(tmp.summary.table)),"true high-conf. variant count"]) + tmp.summary.table.pos[j,"true high-conf. variant coverage"] <- max(tmp.summary.table[grep(paste0("^",tmp.name), rownames(tmp.summary.table)),"true high-conf. variant coverage"]) + tmp.summary.table.pos[j,"false double variant count"] <- sum(tmp.summary.table[grep(paste0("^",tmp.name), rownames(tmp.summary.table)),"false double variant count"]) + tmp.summary.table.pos[j,"false double variant coverage"] <- max(tmp.summary.table[grep(paste0("^",tmp.name), rownames(tmp.summary.table)),"false double variant coverage"], na.rm = T) + + ## infer a good proxy for the false double variant coverage with 0 counts (based estimated distance-dependent % coverage decay observed across all observed double mutants) + if(tmp.summary.table.pos[j,"false double variant coverage"] == "-Inf"){ + tmp.summary.table.pos[j,"false double variant coverage"] <- c(dual.codon.coverage.estimate[abs(tmp.summary.table.pos[j,"codon distance"]),"Estimate % max. possible coverage"] / 100) * tmp.summary.table.pos[j,"true high-conf. variant coverage"] + tmp.summary.table.pos[j,"false double variant coverage"] <- round(tmp.summary.table.pos[j,"false double variant coverage"]) + } + } + tmp.summary.table.pos <- as.data.frame(tmp.summary.table.pos) + + ### fill the master table + out.summary[match(tmp.single,out.summary[,"SNV"]),"False_double_variant_count"] <- sum(tmp.summary.table.pos\$`false double variant count`) + out.summary[match(tmp.single,out.summary[,"SNV"]),"False_double_variant_coverage"] <- sum(tmp.summary.table.pos\$`false double variant coverage`) + out.summary[match(tmp.single,out.summary[,"SNV"]),"True_high_conf_variant_count"] <- sum(tmp.summary.table.pos\$`true high-conf. variant count`) + out.summary[match(tmp.single,out.summary[,"SNV"]),"True_high_conf_variant_coverage"] <- sum(tmp.summary.table.pos\$`true high-conf. variant coverage`) + out.summary[match(tmp.single,out.summary[,"SNV"]),"Exposure_m"] <- sum(tmp.summary.table.pos\$`true high-conf. variant count` * c(tmp.summary.table.pos\$`false double variant coverage` / tmp.summary.table.pos\$`true high-conf. variant coverage`)) + + } + + ## 5.) Calculate the empirical-Bayes sequencing error estimates + if(length(which(is.na(out.summary[,"False_double_variant_count"]) == T)) != 0){ + out.summary <- out.summary[-which(is.na(out.summary[,"False_double_variant_count"]) == T),] + } + out.summary <- as.data.frame(out.summary) + class(out.summary\$False_double_variant_count) <- "numeric" + class(out.summary\$False_double_variant_coverage) <- "numeric" + class(out.summary\$True_high_conf_variant_count) <- "numeric" + class(out.summary\$True_high_conf_variant_coverage) <- "numeric" + class(out.summary\$Exposure_m) <- "numeric" + + ### calculate e_MLE across all + out.summary\$e_MLE <- out.summary\$False_double_variant_count / out.summary\$Exposure_m + + ### calculate all the necessary rates, priors, etc. + + #### global background rate + mu_global <- sum(out.summary\$False_double_variant_count) / sum(out.summary\$Exposure_m) + mu_global <- max(mu_global, 1e-12) ## + + ##### marginal log-likelihood under Gamma-Poisson with fixed mean mu (ChatGPT function, adapted) + ## prior: e ~ Gamma(shape = nu * mu, rate = nu) + ## mean = mu, variance = mu / nu + gp_loglik_nu <- function(log_nu, y, m, mu) { + nu <- exp(log_nu) + a <- max(nu * mu, 1e-12) + + # Negative-binomial marginal: + # y ~ NB(size = a, prob = nu / (nu + m)) + ll <- lgamma(y + a) - lgamma(a) - lgamma(y + 1) + + a * log(nu / (nu + m)) + + y * log(m / (nu + m)) + + -sum(ll) + } + opt_global <- optimize(f = gp_loglik_nu, + interval = c(log(1e-6), log(1e6)), + y = out.summary\$False_double_variant_count, + m = out.summary\$Exposure_m, + mu = mu_global) + nu_global <- exp(opt_global\$minimum) + + #### category-level rates, shrunk to global rate + mu_cat <- vector(mode = "list", length = 6) + names(mu_cat) <- category.translate[,1] + mu_cat_raw <- mu_cat + for(i in 1:length(mu_cat)){ + mu_cat[[i]] <- out.summary[which(out.summary\$Category == names(mu_cat)[i]),] + mu_cat_raw[[i]] <- sum(mu_cat[[i]]\$False_double_variant_count) / sum(mu_cat[[i]]\$Exposure_m) + mu_cat_raw[[i]] <- pmax(mu_cat_raw[[i]], 1e-12) + mu_cat[[i]] <- c(sum(mu_cat[[i]]\$False_double_variant_count) + nu_global * mu_global) / c(sum(mu_cat[[i]]\$Exposure_m) + nu_global) + mu_cat[[i]] <- pmax(mu_cat[[i]], 1e-12) + } + mu_cat_raw <- do.call(c, mu_cat_raw) + mu_cat <- do.call(c, mu_cat) + + ##### category-specific prior strength nu_cat (ChatGPT function, adapted) + ## with fallback to global if category is too small + nu_cat <- rep(NA, 6) + names(nu_cat) <- names(mu_cat) + for (i in 1:length(mu_cat)) { + cat_name <- names(mu_cat)[i] + idx <- out.summary\$Category == cat_name + + yk <- out.summary\$False_double_variant_count[idx] + mk <- out.summary\$Exposure_m[idx] + mu_k <- mu_cat[i] + + opt_k <- try(optimize(f = gp_loglik_nu, + interval = c(log(1e-6), log(1e6)), + y = yk, + m = mk, + mu = mu_k), + silent = TRUE) + + if (inherits(opt_k, "try-error")) { + nu_cat[i] <- nu_global + } else { + nu_cat[i] <- exp(opt_k\$minimum) + } + } + + #### posterior mean for each specific SNV + out.summary\$mu_category <- rep(NA, nrow(out.summary)) + out.summary\$nu_category <- rep(NA, nrow(out.summary)) + for(i in 1:6){ + out.summary\$mu_category[which(out.summary\$Category == names(mu_cat)[i])] <- mu_cat[i] + out.summary\$nu_category[which(out.summary\$Category == names(nu_cat)[i])] <- nu_cat[i] + } + + ##### prior: e_j ~ Gamma(shape = nu_cat * mu_cat, rate = nu_cat) + out.summary\$prior_shape <- out.summary\$mu_category * out.summary\$nu_category + out.summary\$prior_rate <- out.summary\$nu_category + + ##### posterior: e_j | data ~ Gamma(shape = prior_shape + F, rate = prior_rate [= nu_cat] + M) + out.summary\$post_shape <- out.summary\$prior_shape + out.summary\$False_double_variant_count + out.summary\$post_rate <- out.summary\$nu_category + out.summary\$Exposure_m + + ##### posterior mean = shrunk EB estimate + out.summary\$e_EB <- out.summary\$post_shape / out.summary\$post_rate + + ## 6.) Apply correction factors systematically (use upper 95% percentile to correct, a.k.a. "conservative") + seq.error.rate[match(out.summary\$SNV, rownames(seq.error.rate)),"1nt false counts/coverage"] <- out.summary\$e_EB + ## clamped here too - the guard below only tests the rounded count (see the MLE branch) + input.counts.processed[match(out.summary\$SNV, input.counts.processed\$base_mut),"counts_per_cov_corrected"] <- pmax(0, input.counts.processed[match(out.summary\$SNV, input.counts.processed\$base_mut),"counts_per_cov"] - out.summary\$e_EB) + input.counts.processed[match(out.summary\$SNV, input.counts.processed\$base_mut),"counts_corrected"] <- input.counts.processed[match(out.summary\$SNV, input.counts.processed\$base_mut),"counts"] - c(out.summary\$e_EB * input.counts.processed[match(out.summary\$SNV, input.counts.processed\$base_mut),"cov"]) + + ## round to nearest integer, do not allow for negative counts + input.counts.processed[match(out.summary\$SNV, input.counts.processed\$base_mut),"counts_corrected"] <- round(input.counts.processed[match(out.summary\$SNV, input.counts.processed\$base_mut),"counts_corrected"]) + if(any(which(input.counts.processed[,"counts_corrected"] < 0))){ + input.counts.processed[which(input.counts.processed[,"counts_corrected"] < 0),"counts_per_cov_corrected"] <- 0 + input.counts.processed[which(input.counts.processed[,"counts_corrected"] < 0),"counts_corrected"] <- 0 + } + + # Write the processed data + write.csv(seq.error.rate, file = seq_error_rate_path, row.names = T) + write.csv(input.counts.processed, file = output_file_path, row.names = F) + +} + +## ------------------------------------------------------------------------------------------------- +## I/O wrappers preserved from the pipeline module (not part of Maxi's estimator logic) +## ------------------------------------------------------------------------------------------------- + +# propagate corrected counts onto the completed library table (used by e.g. logdiff) +correct_library_completed <- function(completed_path, corrected_filtered_path, output_file_path){ + completed <- read.csv(completed_path) + corrected <- read.csv(corrected_filtered_path) + idx <- match(completed\$codon_mut, corrected\$codon_mut) + has <- !is.na(idx) + completed\$counts[has] <- corrected\$counts[idx[has]] + completed\$counts_per_cov[has] <- corrected\$counts_per_cov[idx[has]] + write.csv(completed, file = output_file_path, row.names = FALSE) +} + +# re-make the AA-level heatmap table from corrected counts (canonical column names) +seq_error_correct_counts_for_heatmaps <- function(input_csv_path, aa_seq_file_path, output_csv_path, threshold = 3) { + + raw_counts <- read.table(input_csv_path, sep = ",", header = TRUE, stringsAsFactors = FALSE) + + wt_seq <- readLines(aa_seq_file_path) + wt_seq <- unlist(strsplit(wt_seq, "")) + wt_seq[wt_seq == "X"] <- "*" + + # summarise corrected counts for each unique pos_mut + aggregated_counts_per_cov <- aggregate( + counts_per_cov_corrected ~ pos_mut, + data = raw_counts, + FUN = function(x) sum(x, na.rm = TRUE) + ) + aggregated_counts <- aggregate( + counts_corrected ~ pos_mut, + data = raw_counts, + FUN = function(x) sum(x, na.rm = TRUE) + ) + aggregated_data <- merge(aggregated_counts_per_cov, aggregated_counts, by = "pos_mut", all = TRUE) + + # canonical output names so the count heatmaps consume this drop-in + names(aggregated_data)[names(aggregated_data) == "counts_per_cov_corrected"] <- "total_counts_per_cov" + names(aggregated_data)[names(aggregated_data) == "counts_corrected"] <- "total_counts" + + aggregated_data\$wt_aa <- sub("(\\\\D)(\\\\d+)(\\\\D)", "\\\\1", aggregated_data\$pos_mut) + aggregated_data\$position <- as.numeric(sub("(\\\\D)(\\\\d+)(\\\\D)", "\\\\2", aggregated_data\$pos_mut)) + aggregated_data\$mut_aa <- sub("(\\\\D)(\\\\d+)(\\\\D)", "\\\\3", aggregated_data\$pos_mut) + aggregated_data\$mut_aa[aggregated_data\$mut_aa == "X"] <- "*" + + all_amino_acids <- c("A", "C", "D", "E", "F", "G", "H", "I", "K", "L", + "M", "N", "P", "Q", "R", "S", "T", "V", "W", "Y", "*") + all_positions <- seq_along(wt_seq) + complete_data <- expand.grid(mut_aa = all_amino_acids, position = all_positions, stringsAsFactors = FALSE) + + heatmap_data <- merge(complete_data, aggregated_data, by = c("mut_aa", "position"), all.x = TRUE, sort = FALSE) + + heatmap_data\$total_counts_per_cov[is.na(heatmap_data\$total_counts_per_cov)] <- 0 + heatmap_data\$wt_aa <- wt_seq[heatmap_data\$position] + + low_count <- is.na(heatmap_data\$total_counts) | heatmap_data\$total_counts < threshold + heatmap_data\$total_counts_per_cov[low_count] <- NA + heatmap_data\$total_counts[low_count] <- NA + + missing_pos_mut <- is.na(heatmap_data\$pos_mut) + heatmap_data\$pos_mut[missing_pos_mut] <- paste0( + heatmap_data\$wt_aa[missing_pos_mut], + heatmap_data\$position[missing_pos_mut], + heatmap_data\$mut_aa[missing_pos_mut] + ) + + out.order <- paste0(rep(1:max(heatmap_data\$position), each = 21), + rep(c("A", "C", "D", "E", "F", "G", "H", + "I", "K", "L","M", "N", "P", "Q", + "R", "S", "T", "V", "W", "Y", "*"), + max(heatmap_data\$position))) + heatmap_data <- heatmap_data[match(out.order, paste0(heatmap_data\$position, heatmap_data\$mut_aa)),] + rownames(heatmap_data) <- 1:nrow(heatmap_data) + + write.csv(heatmap_data, file = output_csv_path, row.names = FALSE) + +} + +# Defensive coordinate-contract check (general, not GID1A-specific): base_mut positions must be +# 1-based within the supplied reference, and the stated WT base must match the reference base there. +# A mismatch means the reference and the counts are in different frames -> stop rather than +# silently mis-index seq.error.rate. +check_reference_frame <- function(wt_path, processed_path){ + ref <- strsplit(as.character(readDNAStringSet(wt_path)[[1]]), "")[[1]] + proc <- read.csv(processed_path) + snv <- proc[grep("[,]", proc[,"base_mut"], invert = TRUE), "base_mut"] + if(length(snv) == 0) return(invisible(NULL)) + pos <- suppressWarnings(as.integer(str_split_fixed(snv, ":", 2)[,1])) + wtbase <- substr(str_split_fixed(snv, ":", 2)[,2], 1, 1) + ok <- !is.na(pos) & pos >= 1 & pos <= length(ref) + if(any(!ok)){ + stop("Reference/counts coordinate mismatch: ", sum(!ok), " single-nucleotide base_mut positions ", + "fall outside the reference (length ", length(ref), "). The reference fasta must be the same ", + "one the reads were aligned to, in the same coordinate frame as base_mut.") + } + mism <- ref[pos] != wtbase + if(any(mism, na.rm = TRUE)){ + stop("Reference/counts coordinate mismatch: ", sum(mism, na.rm = TRUE), " single-nucleotide ", + "base_mut entries state a wild-type base that disagrees with the reference at that position ", + "(e.g. ", snv[which(mism)[1]], "). Check that --fasta matches the counts.") + } + invisible(NULL) +} + +## ------------------------------------------------------------------------------------------------- +## Run +## ------------------------------------------------------------------------------------------------- +method <- "$method" +codon_window <- $codon_window + +# fail loudly if the supplied reference and the counts are in different coordinate frames +check_reference_frame("$wt_fasta", "$filtered") + +intermediate <- "variantCounts_intermediate_corrected.csv" +if (method == "eb") { + seq_error_correct_by_false_doubles_EB( + wt_path = "$wt_fasta", + input_count_path_raw = "$raw_counts", + input_count_path_processed = "$filtered", + output_file_path = intermediate, + seq_error_rate_path = "seq_error_rate.csv", + codon_window = codon_window + ) +} else { + seq_error_correct_by_false_doubles_MLE( + wt_path = "$wt_fasta", + input_count_path_raw = "$raw_counts", + input_count_path_processed = "$filtered", + output_file_path = intermediate, + seq_error_rate_path = "seq_error_rate.csv", + codon_window = codon_window + ) +} + +## corrected counts become the canonical columns so every count-dependent step uses them; +## the originals are preserved as *_raw for the error-correction report +ic <- read.csv(intermediate) +ic\$counts_raw <- ic\$counts +ic\$counts_per_cov_raw <- ic\$counts_per_cov +ic\$counts <- ic\$counts_corrected +ic\$counts_per_cov <- ic\$counts_per_cov_corrected +write.csv(ic, file = "variantCounts_filtered_by_library_error_corrected.csv", row.names = FALSE) + +seq_error_correct_counts_for_heatmaps( + input_csv_path = "variantCounts_filtered_by_library_error_corrected.csv", + aa_seq_file_path = "$aa_seq", + output_csv_path = "variantCounts_for_heatmaps_error_corrected.csv", + threshold = $min_counts +) + +correct_library_completed( + completed_path = "$completed", + corrected_filtered_path = "variantCounts_filtered_by_library_error_corrected.csv", + output_file_path = "library_completed_variantCounts_error_corrected.csv" +) + +## --- versions --- +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +if (is.null(r_version)) r_version <- "unknown" +stringr_version <- as.character(packageVersion("stringr")) +biostrings_version <- as.character(packageVersion("Biostrings")) +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-stringr:', stringr_version), + paste(' r-Biostrings:', biostrings_version) + ), + f +) +close(f) diff --git a/modules/local/dmsanalysis/error_correction_wildtype/environment.yml b/modules/local/dmsanalysis/error_correction_wildtype/environment.yml new file mode 100644 index 0000000..9db6b17 --- /dev/null +++ b/modules/local/dmsanalysis/error_correction_wildtype/environment.yml @@ -0,0 +1,4 @@ +channels: + - conda-forge +dependencies: + - conda-forge::r-base=4.5.1 diff --git a/modules/local/dmsanalysis/error_correction_wildtype/main.nf b/modules/local/dmsanalysis/error_correction_wildtype/main.nf new file mode 100644 index 0000000..0937129 --- /dev/null +++ b/modules/local/dmsanalysis/error_correction_wildtype/main.nf @@ -0,0 +1,38 @@ +process DMSANALYSIS_ERROR_CORRECTION_WILDTYPE { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(meta), path(filtered, stageAs: 'input_counts.csv'), path(wt_counts, stageAs: 'wildtype_counts.csv'), path(completed) + path aa_seq + val min_counts + + output: + tuple val(meta), + path("variantCounts_filtered_by_library_error_corrected.csv"), + path("variantCounts_for_heatmaps_error_corrected.csv"), + path("library_completed_variantCounts_error_corrected.csv"), + emit: corrected + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'error_correction_wildtype.R' + + stub: + """ + touch variantCounts_filtered_by_library_error_corrected.csv + touch variantCounts_for_heatmaps_error_corrected.csv + touch library_completed_variantCounts_error_corrected.csv + echo "DMSANALYSIS_ERROR_CORRECTION_WILDTYPE:" > versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/dmsanalysis/error_correction_wildtype/meta.yml b/modules/local/dmsanalysis/error_correction_wildtype/meta.yml new file mode 100644 index 0000000..8a38af2 --- /dev/null +++ b/modules/local/dmsanalysis/error_correction_wildtype/meta.yml @@ -0,0 +1,66 @@ +name: "dmsanalysis_error_correction_wildtype" +description: "Sequencing-error correction of single-codon variant counts using an additional deep wildtype-only sequencing sample to measure the position-specific error profile, which is then subtracted from each library sample." +keywords: + - deep mutational scanning + - error correction + - wildtype sequencing + - variant counting +tools: + - "r-base": + description: "The R programming language (base only)" + homepage: "https://www.r-project.org/" + licence: ["GPL-2.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', sample:'ORF1', type:'input', replicate:1 ]` + - filtered: + type: file + description: Library-filtered single-codon variant counts (annotated CSV) for this sample. + pattern: "*.csv" + - wt_counts: + type: file + description: Library-filtered variant counts of the matching wildtype-only sample. + pattern: "*.csv" + - completed: + type: file + description: Completed library table (all possible variants) to also correct. + pattern: "*.csv" + - - aa_seq: + type: file + description: Reference amino-acid sequence (one line). + - - min_counts: + type: integer + description: Minimum count threshold for the amino-acid-level heatmap table. + +output: + - corrected: + - meta: + type: map + description: Groovy Map containing sample information + - variantCounts_filtered_by_library_error_corrected.csv: + type: file + description: Library-filtered counts with added `counts_corrected` / `counts_per_cov_corrected` columns. + pattern: "variantCounts_filtered_by_library_error_corrected.csv" + - variantCounts_for_heatmaps_error_corrected.csv: + type: file + description: Amino-acid-level heatmap table built from corrected counts (canonical `total_counts` columns). + pattern: "variantCounts_for_heatmaps_error_corrected.csv" + - library_completed_variantCounts_error_corrected.csv: + type: file + description: Completed library table with corrected counts propagated (for logdiff etc.). + pattern: "library_completed_variantCounts_error_corrected.csv" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" +maintainers: + - "@BenjaminWehnert1008" diff --git a/modules/local/dmsanalysis/error_correction_wildtype/templates/error_correction_wildtype.R b/modules/local/dmsanalysis/error_correction_wildtype/templates/error_correction_wildtype.R new file mode 100644 index 0000000..1e82965 --- /dev/null +++ b/modules/local/dmsanalysis/error_correction_wildtype/templates/error_correction_wildtype.R @@ -0,0 +1,159 @@ +#!/usr/bin/env Rscript + +## Wildtype-based sequencing error correction in nf-core/deepmutscan +## An additional deep wildtype-only sequencing sample measures the position-specific error +## profile, which is subtracted from each library sample's per-base coverage. + +## --- Helper functions --- + +# WT-based sequencing error correction (nucleotide level) +seq_error_correct_by_WT_nt <- function(wt_seq_count_path, input_count_path, output_file_path){ + + WT.counts <- read.csv(wt_seq_count_path) + input.counts <- read.csv(input_count_path) + + ## append key columns + input.counts\$counts_corrected <- input.counts\$counts + input.counts\$counts_per_cov_corrected <- input.counts\$counts_per_cov + + for (i in 1:nrow(WT.counts)){ + + ## only look at variants observed in both the input and WT sequencing + tmp.id <- match(WT.counts[i,"base_mut"], input.counts[,"base_mut"]) + if(is.na(tmp.id) == T){ + next + } + + ## subtract the observed per-base coverage in the WT sequencing + input.counts[tmp.id,"counts_per_cov_corrected"] <- input.counts[tmp.id,"counts_per_cov"] - WT.counts[i,"counts_per_cov"] + + ## adjust the total counts by cross-multiplication + input.counts[tmp.id,"counts_corrected"] <- input.counts[tmp.id,"counts"] * c(input.counts[tmp.id,"counts_per_cov_corrected"] / input.counts[tmp.id,"counts_per_cov"]) + + ## round to nearest integer, do not allow for negative counts + input.counts[tmp.id,"counts_corrected"] <- round(input.counts[tmp.id,"counts_corrected"]) + if(input.counts[tmp.id,"counts_corrected"] < 0){ + input.counts[tmp.id,"counts_corrected"] <- 0 + input.counts[tmp.id,"counts_per_cov_corrected"] <- 0 + } + + } + + ## corrected counts become the canonical columns so every count-dependent step uses them; + ## the originals are preserved as *_raw for the error-correction report + input.counts\$counts_raw <- input.counts\$counts + input.counts\$counts_per_cov_raw <- input.counts\$counts_per_cov + input.counts\$counts <- input.counts\$counts_corrected + input.counts\$counts_per_cov <- input.counts\$counts_per_cov_corrected + + write.csv(input.counts, file = output_file_path, row.names = F) + +} + +# propagate corrected counts onto the completed library table (used by e.g. logdiff) +correct_library_completed <- function(completed_path, corrected_filtered_path, output_file_path){ + completed <- read.csv(completed_path) + corrected <- read.csv(corrected_filtered_path) + idx <- match(completed\$codon_mut, corrected\$codon_mut) + has <- !is.na(idx) + completed\$counts[has] <- corrected\$counts[idx[has]] + completed\$counts_per_cov[has] <- corrected\$counts_per_cov[idx[has]] + write.csv(completed, file = output_file_path, row.names = FALSE) +} + +# re-make the AA-level heatmap table from corrected counts (canonical column names) +seq_error_correct_counts_for_heatmaps <- function(input_csv_path, aa_seq_file_path, output_csv_path, threshold = 3) { + + raw_counts <- read.table(input_csv_path, sep = ",", header = TRUE, stringsAsFactors = FALSE) + + wt_seq <- readLines(aa_seq_file_path) + wt_seq <- unlist(strsplit(wt_seq, "")) + wt_seq[wt_seq == "X"] <- "*" + + # summarise corrected counts for each unique pos_mut + aggregated_counts_per_cov <- aggregate( + counts_per_cov_corrected ~ pos_mut, + data = raw_counts, + FUN = function(x) sum(x, na.rm = TRUE) + ) + aggregated_counts <- aggregate( + counts_corrected ~ pos_mut, + data = raw_counts, + FUN = function(x) sum(x, na.rm = TRUE) + ) + aggregated_data <- merge(aggregated_counts_per_cov, aggregated_counts, by = "pos_mut", all = TRUE) + + # canonical output names so the count heatmaps consume this drop-in + names(aggregated_data)[names(aggregated_data) == "counts_per_cov_corrected"] <- "total_counts_per_cov" + names(aggregated_data)[names(aggregated_data) == "counts_corrected"] <- "total_counts" + + aggregated_data\$wt_aa <- sub("(\\\\D)(\\\\d+)(\\\\D)", "\\\\1", aggregated_data\$pos_mut) + aggregated_data\$position <- as.numeric(sub("(\\\\D)(\\\\d+)(\\\\D)", "\\\\2", aggregated_data\$pos_mut)) + aggregated_data\$mut_aa <- sub("(\\\\D)(\\\\d+)(\\\\D)", "\\\\3", aggregated_data\$pos_mut) + aggregated_data\$mut_aa[aggregated_data\$mut_aa == "X"] <- "*" + + all_amino_acids <- c("A", "C", "D", "E", "F", "G", "H", "I", "K", "L", + "M", "N", "P", "Q", "R", "S", "T", "V", "W", "Y", "*") + all_positions <- seq_along(wt_seq) + complete_data <- expand.grid(mut_aa = all_amino_acids, position = all_positions, stringsAsFactors = FALSE) + + heatmap_data <- merge(complete_data, aggregated_data, by = c("mut_aa", "position"), all.x = TRUE, sort = FALSE) + + heatmap_data\$total_counts_per_cov[is.na(heatmap_data\$total_counts_per_cov)] <- 0 + heatmap_data\$wt_aa <- wt_seq[heatmap_data\$position] + + low_count <- is.na(heatmap_data\$total_counts) | heatmap_data\$total_counts < threshold + heatmap_data\$total_counts_per_cov[low_count] <- NA + heatmap_data\$total_counts[low_count] <- NA + + missing_pos_mut <- is.na(heatmap_data\$pos_mut) + heatmap_data\$pos_mut[missing_pos_mut] <- paste0( + heatmap_data\$wt_aa[missing_pos_mut], + heatmap_data\$position[missing_pos_mut], + heatmap_data\$mut_aa[missing_pos_mut] + ) + + out.order <- paste0(rep(1:max(heatmap_data\$position), each = 21), + rep(c("A", "C", "D", "E", "F", "G", "H", + "I", "K", "L","M", "N", "P", "Q", + "R", "S", "T", "V", "W", "Y", "*"), + max(heatmap_data\$position))) + heatmap_data <- heatmap_data[match(out.order, paste0(heatmap_data\$position, heatmap_data\$mut_aa)),] + rownames(heatmap_data) <- 1:nrow(heatmap_data) + + write.csv(heatmap_data, file = output_csv_path, row.names = FALSE) + +} + +## --- Run --- +seq_error_correct_by_WT_nt( + wt_seq_count_path = "$wt_counts", + input_count_path = "$filtered", + output_file_path = "variantCounts_filtered_by_library_error_corrected.csv" +) + +seq_error_correct_counts_for_heatmaps( + input_csv_path = "variantCounts_filtered_by_library_error_corrected.csv", + aa_seq_file_path = "$aa_seq", + output_csv_path = "variantCounts_for_heatmaps_error_corrected.csv", + threshold = $min_counts +) + +correct_library_completed( + completed_path = "$completed", + corrected_filtered_path = "variantCounts_filtered_by_library_error_corrected.csv", + output_file_path = "library_completed_variantCounts_error_corrected.csv" +) + +## --- versions --- +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +if (is.null(r_version)) r_version <- "unknown" +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version) + ), + f +) +close(f) diff --git a/modules/local/dmsanalysis/possible_mutations/environment.yml b/modules/local/dmsanalysis/possible_mutations/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/dmsanalysis/possible_mutations/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/dmsanalysis/possible_mutations/main.nf b/modules/local/dmsanalysis/possible_mutations/main.nf new file mode 100644 index 0000000..e98eb0f --- /dev/null +++ b/modules/local/dmsanalysis/possible_mutations/main.nf @@ -0,0 +1,33 @@ +process DMSANALYSIS_POSSIBLE_MUTATIONS { + tag "table /w all possible variants" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(meta), path(wt_seq) + val pos_range + val mutagenesis_type + path custom_codon_library + + output: + tuple val(meta), path("possible_mutations.csv"), emit: possible_mutations + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'possible_mutations.R' + + stub: + """ + touch possible_mutations.csv + echo "DMSANALYSIS_POSSIBLE_MUTATIONS:" > versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/dmsanalysis/possible_mutations/meta.yml b/modules/local/dmsanalysis/possible_mutations/meta.yml new file mode 100644 index 0000000..4fbd8c3 --- /dev/null +++ b/modules/local/dmsanalysis/possible_mutations/meta.yml @@ -0,0 +1,63 @@ +name: "dmsanalysis_possible_mutations" +description: | + Generates a comprehensive table of all theoretically possible variants based on the + mutagenesis strategy (e.g., NNK, NNS) and the reference sequence. This table acts + as the ground truth for downstream filtering and analysis. +keywords: + - deep mutational scanning + - dms + - mutagenesis + - library design + - variants +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'ref' ]` + - wt_seq: + type: file + description: FASTA file containing the wild-type reference DNA sequence + pattern: "*.{fasta,fa}" + - - pos_range: + type: string + description: Start and stop codon positions (ORF) in the format 'start-stop', e.g., '352-1383' + - - mutagenesis_type: + type: string + description: | + Type of mutagenic primers used. + Supported types: nnk, nns, nnh, nnn, nnk_nns, nnk_nns_nnh, or custom. + - - custom_codon_library: + type: file + description: | + Optional CSV file defining a custom codon library. + Only required if mutagenesis_type is set to 'custom'. + +output: + - possible_mutations: + - meta: + type: map + description: | + Groovy Map containing sample information + - possible_mutations.csv: + type: file + description: CSV file containing all theoretically possible mutations for the experiment + pattern: "possible_mutations.csv" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/dmsanalysis/possible_mutations/templates/possible_mutations.R b/modules/local/dmsanalysis/possible_mutations/templates/possible_mutations.R new file mode 100644 index 0000000..13a0702 --- /dev/null +++ b/modules/local/dmsanalysis/possible_mutations/templates/possible_mutations.R @@ -0,0 +1,197 @@ +#!/usr/bin/env Rscript + +# ------------------------------------------------------------------------------ +# Script: Generate Programmed Codon Variants +# Description: Generates all possible programmed codon mutations for a given +# wild-type sequence based on a specified mutagenesis strategy. +# Input: +# - wt_seq_input: Wild-type sequence (string or path to FASTA file). +# - start_stop_pos: Target sequence range format "start-stop". +# - mutagenesis_type: Strategy ('nnk', 'nns', 'nnh', 'nnn', 'nnk_nns', 'nnk_nns_nnh', 'custom'). +# - custom_codon_library_path: Path to custom library. Automatically detects +# if the file is a global list ("AAA, AAC...") or a position-wise CSV +# (requires a "Position" header). +# - output_file: Desired name/path for the output CSV. +# Output: A CSV file containing all possible mutated codons per position. +# ------------------------------------------------------------------------------ + +suppressMessages(library(Biostrings)) +suppressMessages(library(methods)) + +generate_possible_variants <- function(wt_seq_input, start_stop_pos, mutagenesis_type, + custom_codon_library_path, output_file) { + + # Parse the start and stop positions from the input format "start-stop" + positions <- unlist(strsplit(start_stop_pos, "-")) + start_pos <- as.numeric(positions[1]) + stop_pos <- as.numeric(positions[2]) + + # Load sequence from file or process as a direct string + if (file.exists(wt_seq_input)) { + seq_data <- Biostrings::readDNAStringSet(filepath = wt_seq_input) + wt_seq <- seq_data[[1]] + } else { + wt_seq <- Biostrings::DNAString(wt_seq_input) + } + + # Extract the target coding sequence + coding_seq <- Biostrings::subseq(wt_seq, start = start_pos, end = stop_pos) + coding_seq <- as.character(coding_seq) + + # Predefined codon dictionaries + nnk_codons <- c('AAG', 'AAT', 'ATG', 'ATT', 'AGG', 'AGT', 'ACG', 'ACT', 'TAG', 'TAT', 'TTG', 'TTT', 'TGG', 'TGT', 'TCG', 'TCT', 'GAG', 'GAT', 'GTG', 'GTT', 'GGG', 'GGT', 'GCG', 'GCT', 'CAG', 'CAT', 'CTG', 'CTT', 'CGG', 'CGT', 'CCG', 'CCT') + nns_codons <- c('AAG', 'AAC', 'ATG', 'ATC', 'AGG', 'AGC', 'ACG', 'ACC', 'TAG', 'TAC', 'TTG', 'TTC', 'TGG', 'TGC', 'TCG', 'TCC', 'GAG', 'GAC', 'GTG', 'GTC', 'GGG', 'GGC', 'GCG', 'GCC', 'CAG', 'CAC', 'CTG', 'CTC', 'CGG', 'CGC', 'CCG', 'CCC') + nnh_codons <- c('AAA', 'AAC', 'AAT', 'ATA', 'ATC', 'ATT', 'AGA', 'AGC', 'AGT', 'ACA', 'ACC', 'ACT', 'TAA', 'TAC', 'TAT', 'TTA', 'TTC', 'TTT', 'TGA', 'TGC', 'TGT', 'TCA', 'TCC', 'TCT', 'GAA', 'GAC', 'GAT', 'GTA', 'GTC', 'GTT', 'GGA', 'GGC', 'GGT', 'GCA', 'GCC', 'GCT', 'CAA', 'CAC', 'CAT', 'CTA', 'CTC', 'CTT', 'CGA', 'CGC', 'CGT', 'CCA', 'CCC', 'CCT') + nnn_codons <- c('AAA', 'AAC', 'AAG', 'AAT', 'ATA', 'ATC', 'ATG', 'ATT', 'AGA', 'AGC', 'AGG', 'AGT', 'ACA', 'ACC', 'ACG', 'ACT', 'TAA', 'TAC', 'TAG', 'TAT', 'TTA', 'TTC', 'TTG', 'TTT', 'TGA', 'TGC', 'TGG', 'TGT', 'TCA', 'TCC', 'TCG', 'TCT', 'GAA', 'GAC', 'GAG', 'GAT', 'GTA', 'GTC', 'GTG', 'GTT', 'GGA', 'GGC', 'GGG', 'GGT', 'GCA', 'GCC', 'GCG', 'GCT', 'CAA', 'CAC', 'CAG', 'CAT', 'CTA', 'CTC', 'CTG', 'CTT', 'CGA', 'CGC', 'CGG', 'CGT', 'CCA', 'CCC', 'CCG', 'CCT') + + # -------------------------------------------------------------------------- + # Custom Library Parsing with Auto-Detection + # -------------------------------------------------------------------------- + is_position_wise <- FALSE + position_lookup <- list() + custom_codons <- NULL + + if (mutagenesis_type == "custom") { + if (!file.exists(custom_codon_library_path) || is.null(custom_codon_library_path)) { + stop("Custom codons file must be provided and valid when using 'custom' mutagenesis_type.") + } + + # Auto-detect format by inspecting the first line + first_line <- readLines(custom_codon_library_path, n = 1) + + if (grepl("Position", first_line, ignore.case = TRUE)) { + # Format 1: Position-wise CSV file + is_position_wise <- TRUE + + # Read line-by-line instead of read.csv to avoid strict column matching errors + lines <- readLines(custom_codon_library_path) + + # Loop through lines, skipping the header (index 1) + for (line in lines[-1]) { + # Skip empty lines + if (trimws(line) == "") next + + # Split the line by commas + parts <- trimws(unlist(strsplit(line, ","))) + + # The first part is the position, everything else are the codons + pos_idx <- parts[1] + codon_vec <- parts[-1] + + # Remove any accidental empty strings (e.g., from trailing commas) + codon_vec <- codon_vec[codon_vec != ""] + + position_lookup[[pos_idx]] <- codon_vec + } + } else { + # Format 2: Global comma-separated list (Legacy compatibility) + custom_codons <- unlist(strsplit(readLines(custom_codon_library_path), ",")) + custom_codons <- trimws(custom_codons) + } + } + + # Helper function to split a DNA sequence into nucleotide triplets + split_into_codons <- function(seq) { + # Note: Double escaping is required for Perl regular expressions here + return(strsplit(seq, "(?<=.{3})", perl = TRUE)[[1]]) + } + + wt_codons <- split_into_codons(coding_seq) + + # Initialize dataframe to store final variant results + # Note: \$ escaping is maintained for Nextflow compatibility + result <- data.frame(Codon_Number = integer(), wt_codon = character(), Variant = character(), stringsAsFactors = FALSE) + + # Helper function to determine the target codon list per position + get_codon_list <- function(wt_codon, codon_index) { + if (mutagenesis_type == "nnk") { + return(nnk_codons) + } else if (mutagenesis_type == "nns") { + return(nns_codons) + } else if (mutagenesis_type == "nnh") { + return(nnh_codons) + } else if (mutagenesis_type == "nnn") { + return(nnn_codons) + } else if (mutagenesis_type == "nnk_nns") { + if (substr(wt_codon, 3, 3) == "T") return(nns_codons) else return(nnk_codons) + } else if (mutagenesis_type == "nnk_nns_nnh") { + if (substr(wt_codon, 3, 3) == "T") { + return(nns_codons) + } else if (substr(wt_codon, 3, 3) == "G"){ + return(nnh_codons) + } else { + return(nnk_codons) + } + } else if (mutagenesis_type == "custom") { + if (is_position_wise) { + idx_str <- as.character(codon_index) + if (!is.null(position_lookup[[idx_str]])) { + return(position_lookup[[idx_str]]) + } else { + return(NULL) # Skip positions not explicitly defined in the CSV + } + } else { + return(custom_codons) + } + } else { + stop("Invalid mutagenesis_type. Choose from 'nnk', 'nns', 'nnh', 'nnn', 'nnk_nns', 'nnk_nns_nnh', or 'custom'.") + } + } + + # Iterate over each wild-type codon to assign programmed variants + for (i in seq_along(wt_codons)) { + wt_codon <- wt_codons[i] + codon_list <- get_codon_list(wt_codon, i) + + # Skip iteration if no custom codons were assigned to this specific position + if (is.null(codon_list)) next + + # Filter out the wild-type codon from the mutation list + possible_variants <- codon_list[codon_list != wt_codon] + + for (variant in possible_variants) { + # Note: \$ escaping is maintained for Nextflow compatibility + result <- rbind(result, data.frame(Codon_Number = i, wt_codon = wt_codon, Variant = variant, stringsAsFactors = FALSE)) + } + } + + write.csv(result, output_file, row.names = FALSE) +} + +# ------------------------------------------------------------------------------ +# Main Execution Block (Nextflow variable substitution) +# ------------------------------------------------------------------------------ + +# Replaces bash if/else logic. If Nextflow omits the optional file, +# it passes "/NULL", which R translates to an actual NULL object. +custom_lib_arg <- if ("$custom_codon_library" == "/NULL") { + NULL +} else { + "$custom_codon_library" +} + +generate_possible_variants( + wt_seq_input = "$wt_seq", + start_stop_pos = "$pos_range", + mutagenesis_type = "$mutagenesis_type", + custom_codon_library_path = "$custom_codon_library", + output_file = "possible_mutations.csv" +) + +# ------------------------------------------------------------------------------ +# Versioning Generation +# ------------------------------------------------------------------------------ + +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +biostrings_version <- as.character(packageVersion("Biostrings")) + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' biostrings:', biostrings_version) + ), + f +) +close(f) diff --git a/modules/local/dmsanalysis/process_variant_counts/environment.yml b/modules/local/dmsanalysis/process_variant_counts/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/dmsanalysis/process_variant_counts/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/dmsanalysis/process_variant_counts/main.nf b/modules/local/dmsanalysis/process_variant_counts/main.nf new file mode 100644 index 0000000..c8a6347 --- /dev/null +++ b/modules/local/dmsanalysis/process_variant_counts/main.nf @@ -0,0 +1,41 @@ +process DMSANALYSIS_PROCESS_VARIANT_COUNTS { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + publishDir "${params.outdir}/intermediate_files", mode: 'copy' + + input: + tuple val(meta), path(variantCounts) + path possible_mutations + path aa_seq + val min_counts + + output: + tuple val(meta), + path("annotated_variantCounts.csv"), + path("variantCounts_filtered_by_library.csv"), + path("library_completed_variantCounts.csv"), + path("variantCounts_for_heatmaps.csv"), + emit: processed_variantCounts + + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'process_variant_counts.R' + + stub: + """ + touch annotated_variantCounts.csv variantCounts_filtered_by_library.csv library_completed_variantCounts.csv variantCounts_for_heatmaps.csv + echo "DMSANALYSIS_PROCESS_VARIANT_COUNTS:" > versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/dmsanalysis/process_variant_counts/meta.yml b/modules/local/dmsanalysis/process_variant_counts/meta.yml new file mode 100644 index 0000000..c72060b --- /dev/null +++ b/modules/local/dmsanalysis/process_variant_counts/meta.yml @@ -0,0 +1,69 @@ +name: "dmsanalysis_process_variant_counts" +description: "Processes the raw variant count table, filters it against expected libraries, and prepares data for downstream visualization and fitness calculation." +keywords: + - deep mutational scanning + - variant counting + - variant filtering + - dms +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - variantCounts: + type: file + description: TSV file containing the raw variant counts + pattern: "*.{tsv,csv}" + - - possible_mutations: + type: file + description: CSV file containing all theoretically possible mutations based on library design + pattern: "*.{csv}" + - - aa_seq: + type: file + description: FASTA or text file containing the reference amino acid sequence + - - min_counts: + type: integer + description: Minimum count threshold to consider a variant valid + +output: + - processed_variantCounts: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - annotated_variantCounts.csv: + type: file + description: Raw variant counts annotated with mutation details + pattern: "annotated_variantCounts.csv" + - variantCounts_filtered_by_library.csv: + type: file + description: Variant counts filtered to include only those present in the provided library + pattern: "variantCounts_filtered_by_library.csv" + - library_completed_variantCounts.csv: + type: file + description: Full library table including zero-count variants + pattern: "library_completed_variantCounts.csv" + - variantCounts_for_heatmaps.csv: + type: file + description: Processed counts formatted specifically for heatmap visualization + pattern: "variantCounts_for_heatmaps.csv" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/dmsanalysis/process_variant_counts/templates/process_variant_counts.R b/modules/local/dmsanalysis/process_variant_counts/templates/process_variant_counts.R new file mode 100644 index 0000000..43b0093 --- /dev/null +++ b/modules/local/dmsanalysis/process_variant_counts/templates/process_variant_counts.R @@ -0,0 +1,366 @@ +#!/usr/bin/env Rscript + +# four parts condensed into one R script + +######### +# process raw counts +######### + +# input: counts variantcounts tsv file, output_path +# output: csv with column names. Creates additional counts_per_cov column. Fills pos_mut column for synonymous mutations. Sorted out variants that have mutations, but do not show up in the specifying columns -> was affecting roughly 30 low-count variants out of over 15000 in Taylor's data + +library("dplyr") +process_raw_counts <- function(counts_file_path, output_csv_path) { + + # Set the column names + colnames <- c("counts", "cov", "mean_length_variant_reads", "varying_bases", + "base_mut", "varying_codons", "codon_mut", "aa_mut", "pos_mut") + + # Read the variant-count file into a data frame + counts_raw <- read.table(counts_file_path, sep = "\\t", header = FALSE, fill = TRUE, col.names = colnames) + + # Filter out rows where 'aa_mut' is empty or NA + counts_raw <- counts_raw[!(counts_raw\$aa_mut == "" | is.na(counts_raw\$aa_mut)), ] + + # Handle synonymous mutations: where aa_mut starts with "S" and pos_mut is either NA or "" + counts_raw <- counts_raw %>% + rowwise() %>% + mutate( + pos_mut = ifelse( + (is.na(pos_mut) | pos_mut == "") & grepl("^S:", aa_mut), + # Construct the new 'pos_mut' entry for synonymous mutations + paste0( + sub("S:([A-Z])>[A-Z]", "\\\\1", aa_mut), # Get the original amino acid from 'aa_mut' + sub("^(\\\\d+):.*", "\\\\1", codon_mut), # Get the position from 'codon_mut' + sub("S:[A-Z]>([A-Z])", "\\\\1", aa_mut) # Get the mutated amino acid from 'aa_mut' + ), + pos_mut # Keep the existing 'pos_mut' if it's not NA or "" + ) + ) %>% + ungroup() %>% + mutate(counts_per_cov = counts / cov) + + # Write the cleaned data frame to a CSV file + write.csv(counts_raw, file = output_csv_path, row.names = FALSE) +} + +##### +# run function +##### +process_raw_counts( + counts_file_path = "$variantCounts", + output_csv_path = "annotated_variantCounts.csv" +) + + + + + + + + +######### +# filter counts by codon library +######### + +# Input: pre_processed_raw_counts_path, mutation library .csv path (former "possible_NNK_mutations.csv"), output_path +# Output: counts table filtered for only single-codon mutations that are part of the library + +library("dplyr") +library("stringr") + +filter_counts_by_codon_library <- function(counts_file_path, codon_library_path, output_file_path) { + # Load the variant-count table from the provided file path + counts_table <- read.csv(counts_file_path) + + # Load the codon library from the provided .csv file + codon_library <- read.csv(codon_library_path) + + # Ensure the codon library has the expected columns + if (!all(c("Codon_Number", "wt_codon", "Variant") %in% colnames(codon_library))) { + stop("Codon library must contain columns 'Codon_Number', 'wt_codon', and 'Variant'.") + } + + # Filter the variant-count table + counts_table\$codon_mut <- toupper(counts_table\$codon_mut) + filtered_counts <- counts_table %>% + filter(varying_codons == 1) %>% # Keep rows with single-codon mutations + rowwise() %>% + filter({ + # Extract the position and mutated codon + codon_position <- as.numeric(sub(":.*", "", codon_mut)) # Extract position before ':' + mutated_codon <- sub(".*>", "", codon_mut) # Extract codon after '>' + + # Check if the position and codon are valid + is_in_library <- any( + codon_library\$Codon_Number == codon_position & + (codon_library\$Variant == mutated_codon | # Check Variant column + codon_library\$wt_codon == mutated_codon) # Check wt_codon column + ) + is_in_library + }) %>% + ungroup() %>% + # Apply additional filtering based on mutation distances + rowwise() %>% + filter({ + # Split base_mut into individual mutations + mutations <- unlist(strsplit(base_mut, ",\\\\s*")) # Splits by comma and removes extra spaces + # Extract numeric positions from each mutation string + positions <- as.numeric(str_extract(mutations, "^[0-9]+")) + + # Calculate the distance between the first and last position + distance <- max(positions, na.rm = TRUE) - min(positions, na.rm = TRUE) + + # Keep rows where the distance is <= 2 + distance <= 2 + }) %>% + ungroup() + + # Write the filtered variant-count table to the output file path + write.csv(filtered_counts, file = output_file_path, row.names = FALSE) +} + +##### +# run function +##### +filter_counts_by_codon_library( + counts_file_path = "annotated_variantCounts.csv", + codon_library_path = "$possible_mutations", + output_file_path = "variantCounts_filtered_by_library.csv" +) + + + + +######### +# complete filtered counts +######### + +# input: NNK_codon_library_filtered_counts.csv-path, prefiltered_counts.csv-path (containing only NNK mutations), output_folder-path +# output: completed counts_file with all possible variants (even if not measured in sequencing) -> NA in counts and counts_per_cov to 0.0000001 to deal with log-scale in following calculations + +library(dplyr) +library(Biostrings) # Required for codon-to-amino-acid translation + +# Function to calculate Hamming distance (varying_bases) +hamming_distance <- function(wt_codon, variant_codon) { + sum(strsplit(wt_codon, "")[[1]] != strsplit(variant_codon, "")[[1]]) +} + +# Function to get amino acid from codon +get_amino_acid <- function(codon) { + codon_table <- GENETIC_CODE + aa <- codon_table[[toupper(codon)]] + if (is.null(aa)) { + return(NA) # Handle cases where codon is not valid + } + return(aa) +} + +# Function to calculate mutation type (aa_mut) and pos_mut +mutation_details <- function(wt_codon, variant_codon, codon_number) { + wt_aa <- get_amino_acid(wt_codon) + variant_aa <- get_amino_acid(variant_codon) + + # If amino acids are different, it's a missense mutation; otherwise, synonymous + if (wt_aa != variant_aa) { + mutation_type <- "M" # Missense mutation + } else { + mutation_type <- "S" # Synonymous mutation + } + + # aa_mut: Type of mutation and amino acid changes (e.g., M:D>S) + aa_mut <- paste0(mutation_type, ":", wt_aa, ">", variant_aa) + + # pos_mut: Wild-type AA, codon position, mutated AA (e.g., D2Q) + pos_mut <- paste0(wt_aa, codon_number, variant_aa) + + return(list(aa_mut = aa_mut, pos_mut = pos_mut)) +} + +complete_prefiltered_counts <- function(possible_nnk_path, prefiltered_counts_path, output_file_path) { + + # Load the possible NNK mutations CSV + possible_nnk <- read.csv(possible_nnk_path) + + # Load the prefiltered variant-count CSV + prefiltered_counts <- read.csv(prefiltered_counts_path) + + # Create codon_mut column in possible_NNK_mutations in the format 'Codon_Number:wt_codon>Variant' + possible_nnk <- possible_nnk %>% + mutate(codon_mut = paste0(Codon_Number, ":", wt_codon, ">", Variant)) + + # Merge both dataframes based on the codon_mut column (full join to include all) + prefiltered_counts\$codon_mut <- toupper(prefiltered_counts\$codon_mut) + merged_data <- full_join(prefiltered_counts, possible_nnk, by = "codon_mut") + + # Fill missing values in counts_per_cov and counts with 0.0000001 + merged_data <- merged_data %>% + mutate(counts_per_cov = ifelse(is.na(counts_per_cov), 0.0000001, counts_per_cov), + counts = ifelse(is.na(counts), 0.000001, counts)) + + # Calculate Hamming distance (varying_bases) and mutation details (aa_mut, pos_mut) + merged_data <- merged_data %>% + rowwise() %>% + mutate(varying_bases = hamming_distance(wt_codon, Variant), + mutation_info = list(mutation_details(wt_codon, Variant, Codon_Number))) %>% + mutate(aa_mut = mutation_info\$aa_mut, # Extract aa_mut + pos_mut = mutation_info\$pos_mut) %>% # Extract pos_mut + ungroup() %>% + select(-mutation_info) # Remove the temporary list column + + # Save the merged data to a new CSV file + write.csv(merged_data, file = output_file_path, row.names = FALSE) +} + +##### +# run function +##### +complete_prefiltered_counts( + possible_nnk_path = "$possible_mutations", + prefiltered_counts_path = "variantCounts_filtered_by_library.csv", + output_file_path = "library_completed_variantCounts.csv" +) + + + + + + + + + +######### +# prepare counts data for counts heatmap +######### + +# input: prefiltered variant-count path (filtered for codon library), aa-seq file path, output path, threshold (for minimum counts to recognize variant) +# output: csv file serving as basis for counts_per_cov_heatmap function + +suppressMessages(library(dplyr)) +suppressMessages(library(ggplot2)) +suppressMessages(library(tidyr)) +suppressMessages(library(reshape2)) +suppressMessages(library(scales)) + +prepare_counts_data_for_counts_heatmaps <- function(counts_file_path, aa_seq_file_path, output_csv_path, threshold = 3) { + # Load the raw variant-count data + raw_counts <- read.table(counts_file_path, sep = ",", header = TRUE) + + # Read the wild-type amino acid sequence from the text file + wt_seq <- readLines(aa_seq_file_path) + wt_seq <- unlist(strsplit(wt_seq, "")) # Split the sequence into individual amino acids + + # Summarize counts-per-cov for each unique aa mutation in pos_mut + aggregated_data <- raw_counts %>% + group_by(pos_mut) %>% + summarize(total_counts_per_cov = sum(counts_per_cov, na.rm = TRUE), + total_counts = sum(counts, na.rm = TRUE)) # Also sum the counts + + # Extract the wild-type position and mutations from 'pos_mut' + aggregated_data <- aggregated_data %>% + mutate( + wt_aa = sub("(\\\\D)(\\\\d+)(\\\\D)", "\\\\1", pos_mut), # Wild-type amino acid (e.g., S) + position = as.numeric(sub("(\\\\D)(\\\\d+)(\\\\D)", "\\\\2", pos_mut)), # Position (e.g., 3) + mut_aa = sub("(\\\\D)(\\\\d+)(\\\\D)", "\\\\3", pos_mut) # Mutant amino acid (e.g., R) + ) + + # Replace 'X' with '*', indicating the stop codon + aggregated_data <- aggregated_data %>% + mutate(mut_aa = ifelse(mut_aa == "X", "*", mut_aa)) + + # Replace 'X' with '*' in the wild-type amino acid sequence as well + wt_seq <- ifelse(wt_seq == "X", "*", wt_seq) + + # Define all 20 standard amino acids and the stop codon "*" + all_amino_acids <- c("A", "C", "D", "E", "F", "G", "H", "I", "K", "L", + "M", "N", "P", "Q", "R", "S", "T", "V", "W", "Y", "*") + + # Create a list of all positions in the wild-type sequence + all_positions <- 1:length(wt_seq) + + # Create a complete grid of all possible combinations of positions and amino acids + complete_data <- expand.grid(mut_aa = all_amino_acids, position = all_positions) + + # Merge the summarized data with the complete grid (filling missing entries with 0) + heatmap_data <- complete_data %>% + left_join(aggregated_data, by = c("mut_aa", "position")) %>% + mutate(total_counts_per_cov = ifelse(is.na(total_counts_per_cov), 0, total_counts_per_cov), + wt_aa = wt_seq[position]) # Assign the wild-type amino acid + + # Set variants with counts < threshold to NA + heatmap_data <- heatmap_data %>% + mutate( + total_counts_per_cov = ifelse(total_counts < threshold, NA, total_counts_per_cov), + total_counts = ifelse(total_counts < threshold, NA, total_counts) + ) + + # Fill pos_mut column + heatmap_data <- heatmap_data %>% + mutate( + pos_mut = ifelse(is.na(pos_mut), + paste0(wt_aa, position, mut_aa), + pos_mut) + ) + + # Save the aggregated data to a CSV file + write.csv(heatmap_data, file = output_csv_path, row.names = FALSE) + print(paste("Aggregated data saved to:", output_csv_path)) +} + + +##### +# run function +##### +prepare_counts_data_for_counts_heatmaps( + counts_file_path = "variantCounts_filtered_by_library.csv", + aa_seq_file_path = "$aa_seq", + output_csv_path = "variantCounts_for_heatmaps.csv", + threshold = $min_counts +) + + + + + + + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +dplyr_version <- as.character(packageVersion("dplyr")) +Biostrings_version <- as.character(packageVersion("Biostrings")) +stringr_version <- as.character(packageVersion("stringr")) +ggplot2_version <- as.character(packageVersion("ggplot2")) +tidyr_version <- as.character(packageVersion("tidyr")) +reshape2_version <- as.character(packageVersion("reshape2")) +scales_version <- as.character(packageVersion("scales")) + + +if (is.null(r_version)) r_version <- "unknown" +if (length(dplyr_version) == 0) dplyr_version <- "unknown" +if (length(Biostrings_version) == 0) Biostrings_version <- "unknown" +if (length(stringr_version) == 0) stringr_version <- "unknown" +if (length(ggplot2_version) == 0) ggplot2_version <- "unknown" +if (length(tidyr_version) == 0) tidyr_version <- "unknown" +if (length(reshape2_version) == 0) reshape2_version <- "unknown" +if (length(scales_version) == 0) scales_version <- "unknown" + + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-dplyr:', dplyr_version), + paste(' r-Biostrings:', Biostrings_version), + paste(' r-stringr:', stringr_version), + paste(' r-ggplot2:', ggplot2_version), + paste(' r-tidyr:', tidyr_version), + paste(' r-reshape2:', reshape2_version), + paste(' r-scales:', scales_version) + ), + f +) +close(f) diff --git a/modules/local/fitness/counts_to_fitness/environment.yml b/modules/local/fitness/counts_to_fitness/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/fitness/counts_to_fitness/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/fitness/counts_to_fitness/main.nf b/modules/local/fitness/counts_to_fitness/main.nf new file mode 100644 index 0000000..1423e99 --- /dev/null +++ b/modules/local/fitness/counts_to_fitness/main.nf @@ -0,0 +1,32 @@ +process COUNTS_TO_FITNESS { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(meta), path(variantCounts_filtered_by_library) + path wt_seq + val pos_range + + output: + tuple val(meta), path("${meta.id}_fitness_input.tsv"), emit: fitness_input + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'counts_to_fitness.R' + + stub: + """ + touch ${meta.id}_fitness_input.tsv + echo "COUNTS_TO_FITNESS:" > versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/fitness/counts_to_fitness/meta.yml b/modules/local/fitness/counts_to_fitness/meta.yml new file mode 100644 index 0000000..e0aae49 --- /dev/null +++ b/modules/local/fitness/counts_to_fitness/meta.yml @@ -0,0 +1,57 @@ +name: "counts_to_fitness" +description: Reformats the library-filtered variant count table into a standardized TSV compatible with downstream fitness calculation tools, ensuring mutation nomenclature is consistent with the reference sequence. +keywords: + - deep mutational scanning + - dms + - variant counting + - format conversion + - fitness +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + - "bioconductor-biostrings": + description: "Efficient manipulation of biological strings" + homepage: "https://bioconductor.org/packages/Biostrings" + licence: ["Artistic-2.0"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - variantCounts_filtered_by_library: + type: file + description: CSV file containing variant counts already filtered against the design library + pattern: "*.{csv}" + - - wt_seq: + type: file + description: FASTA file containing the wild-type DNA reference sequence + pattern: "*.{fasta,fa}" + - - pos_range: + type: string + description: Start and stop codon positions (ORF) in the format 'start-stop' + +output: + - fitness_input: + - meta: + type: map + description: Groovy Map containing sample information + - "${meta.id}_fitness_input.tsv": + type: file + description: A standardized TSV file ready for input into fitness calculation modules + pattern: "*_fitness_input.tsv" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/fitness/counts_to_fitness/templates/counts_to_fitness.R b/modules/local/fitness/counts_to_fitness/templates/counts_to_fitness.R new file mode 100644 index 0000000..b234058 --- /dev/null +++ b/modules/local/fitness/counts_to_fitness/templates/counts_to_fitness.R @@ -0,0 +1,103 @@ +#!/usr/bin/env Rscript + +suppressMessages(library(Biostrings)) + +generate_fitness_input <- function(wt_seq_path, counts_file, pos_range, output_file_path) { + # Parse the position range + positions <- unlist(strsplit(pos_range, "-")) + start_pos <- as.numeric(positions[1]) + stop_pos <- as.numeric(positions[2]) + + # Load the wild-type sequence + seq_data <- Biostrings::readDNAStringSet(filepath = wt_seq_path) + wt_seq <- seq_data[[1]] # Extract the sequence + wt_seq <- subseq(wt_seq, start = start_pos, end = stop_pos) + + # Convert wt_seq to a character string + wt_seq <- as.character(wt_seq) + + # Split the wild-type sequence into codons (groups of 3 bases) + wt_codons <- substring(wt_seq, seq(1, nchar(wt_seq), 3), seq(3, nchar(wt_seq), 3)) + + # Helper function to process variant-count CSVs into count data + process_counts_file <- function(counts_csv) { + # Load the input variant-count CSV file + counts_data <- read.csv(counts_csv, stringsAsFactors = FALSE) + + # Initialize a data frame for results + results <- data.frame( + nt_seq = character(), + count = numeric(), + stringsAsFactors = FALSE + ) + + # Iterate over each row in the input data + for (i in 1:nrow(counts_data)) { + # Extract the mutation info + codon_mut <- counts_data\$codon_mut[i] + + if("counts_corrected" %in% colnames(counts_data)){ + counts <- counts_data\$counts_corrected[i] + }else{ + counts <- counts_data\$counts[i] + } + + # Create a mutable copy of the wild-type codons + mutated_codons <- wt_codons + + # Apply the mutation + mutations <- strsplit(codon_mut, ", ")[[1]] + for (mutation in mutations) { + codon_position <- as.numeric(sub(":.*", "", mutation)) + new_codon <- sub(".*>", "", mutation) + # Replace the codon at the specified position + mutated_codons[codon_position] <- new_codon + } + + # Convert the mutated codons back to a sequence string + mutated_seq_string <- paste(mutated_codons, collapse = "") + + # Add the result to the data frame + results <- rbind(results, data.frame(nt_seq = mutated_seq_string, count = counts)) + } + + return(results) + } + + # Process the variant-count file + cat("Processing variant-count file...\\n") + processed_data <- process_counts_file(counts_file) + + # Write the processed data to a file without column names + write.table(processed_data, file = output_file_path, sep = "\\t", row.names = FALSE, col.names = FALSE, quote = FALSE) +} + +##### +# run function +##### +generate_fitness_input( + wt_seq_path = "$wt_seq", + counts_file = "$variantCounts_filtered_by_library", + pos_range = "$pos_range", + output_file_path = "${meta.id}_fitness_input.tsv" +) + +##### +# create versions.yml +##### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +Biostrings_version <- as.character(packageVersion("Biostrings")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(Biostrings_version) == 0) Biostrings_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-Biostrings:', Biostrings_version) + ), + f +) +close(f) diff --git a/modules/local/fitness/find_synonymous_mutation/environment.yml b/modules/local/fitness/find_synonymous_mutation/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/fitness/find_synonymous_mutation/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/fitness/find_synonymous_mutation/main.nf b/modules/local/fitness/find_synonymous_mutation/main.nf new file mode 100644 index 0000000..2ea78e6 --- /dev/null +++ b/modules/local/fitness/find_synonymous_mutation/main.nf @@ -0,0 +1,31 @@ +process FIND_SYNONYMOUS_MUTATION { + tag { sample.sample } + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(sample), path(counts_merged) // from MERGE_COUNTS.out.merged_counts + path wt_fasta // broadcast singleton + val pos_range // "start-end", broadcast singleton + + output: + tuple val(sample), path("synonymous_wt.txt"), emit: synonymous_wt + path "versions.yml", emit: versions + + script: + template 'find_syn_mutation.R' + + stub: + """ + touch synonymous_wt.txt + cat <<-END_VERSIONS > versions.yml + "${task.process}": + r-base: "0.0.0" + END_VERSIONS + """ +} diff --git a/modules/local/fitness/find_synonymous_mutation/meta.yml b/modules/local/fitness/find_synonymous_mutation/meta.yml new file mode 100644 index 0000000..8ec3ab3 --- /dev/null +++ b/modules/local/fitness/find_synonymous_mutation/meta.yml @@ -0,0 +1,58 @@ +name: "find_synonymous_mutation" +description: Identifies synonymous mutations within the merged count table, prioritizing those with high counts (typically 2nt changes) to serve as a reference for normalizing fitness scores. +keywords: + - deep mutational scanning + - dms + - synonymous mutation + - fitness calculation + - normalization +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + - "bioconductor-biostrings": + description: "Memory efficient string containers, string matching algorithms, and other utilities, for fast manipulation of large biological sequences or sets of sequences" + homepage: "https://bioconductor.org/packages/Biostrings" + licence: ["Artistic-2.0"] + +input: + - - sample: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test' ]` + - counts_merged: + type: file + description: TSV/CSV file containing variant counts merged across samples or replicates + pattern: "*.{tsv,csv}" + - - wt_fasta: + type: file + description: FASTA file containing the wild-type reference DNA sequence + pattern: "*.{fasta,fa}" + - - pos_range: + type: string + description: Start and stop codon positions (ORF) in the format 'start-stop', e.g., '352-1383' + +output: + - synonymous_wt: + - sample: + type: map + description: | + Groovy Map containing sample information + - synonymous_wt.txt: + type: file + description: Text file identifying the chosen synonymous mutation to be used as a wild-type reference + pattern: "synonymous_wt.txt" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/fitness/find_synonymous_mutation/templates/find_syn_mutation.R b/modules/local/fitness/find_synonymous_mutation/templates/find_syn_mutation.R new file mode 100644 index 0000000..a0ee07a --- /dev/null +++ b/modules/local/fitness/find_synonymous_mutation/templates/find_syn_mutation.R @@ -0,0 +1,119 @@ +#!/usr/bin/env Rscript + +suppressMessages(library(Biostrings)) + +# Pick a synonymous "WT substitute" for DiMSum normalization using a fixed coding window. +# Inputs: +# wt_fasta : path to FASTA (single WT sequence) +# counts_merged_tsv : path to merged counts (columns: nt_seq, input1..N, output1..M) +# pos_range : "start-end" (1-based, inclusive), e.g. "352-1383" +# Returns: +# character scalar: chosen nt sequence +# +# Preference: +# 1) AA-identical (fully synonymous) AND exactly 2 nt mismatches vs WT, both within ONE codon. +# 2) If none, AA-identical AND exactly 1 nt mismatch vs WT. +# 3) If more than one, pick highest mean of input counts. +# 4) If still none, stop with an error. + +pick_synonymous_wt_from_range <- function(wt_fasta, counts_merged_tsv, pos_range) { + ## ---- parse range ---- + pr <- strsplit(as.character(pos_range), "-", fixed = TRUE)[[1]] + if (length(pr) != 2L) stop("pos_range must be 'start-end', got: ", pos_range) + start_pos <- as.integer(pr[1]); end_pos <- as.integer(pr[2]) + if (is.na(start_pos) || is.na(end_pos) || start_pos < 1L || end_pos < start_pos) + stop("Invalid pos_range: ", pos_range) + + ## ---- WT window ---- + wt_set <- Biostrings::readDNAStringSet(wt_fasta) + if (length(wt_set) != 1L) stop("WT FASTA must contain exactly one sequence.") + wt_subseq <- Biostrings::subseq(wt_set[[1]], start = start_pos, end = end_pos) + wt_seq_chr <- as.character(wt_subseq) + wt_len <- nchar(wt_seq_chr) + if ((wt_len %% 3) != 0) stop("Provided window length is not divisible by 3: ", wt_len) + wt_aa <- Biostrings::translate(wt_subseq, if.fuzzy.codon = "X") + wt_chars <- strsplit(wt_seq_chr, "", fixed = TRUE)[[1]] + + ## ---- counts ---- + df <- utils::read.delim(counts_merged_tsv, sep = "\\t", header = TRUE, + stringsAsFactors = FALSE, check.names = FALSE) + if (!"nt_seq" %in% names(df)) stop("counts_merged_tsv must have a 'nt_seq' column.") + + df\$nt_seq <- toupper(df\$nt_seq) + keep_len <- nchar(df\$nt_seq) == wt_len + if (!any(keep_len)) stop("No sequences match WT window length (", wt_len, ").") + if (!all(keep_len)) df <- df[keep_len, , drop = FALSE] + + # input columns & mean (works with 1+ replicates) + input_cols <- grep("^input", names(df), value = TRUE) + if (length(input_cols) == 0L) stop("No input columns found (expect names starting with 'input').") + input_mat <- as.data.frame(lapply(df[, input_cols, drop = FALSE], function(x) as.numeric(as.character(x)))) + input_mean <- if (length(input_cols) == 1L) input_mat[[1]] else rowMeans(as.matrix(input_mat), na.rm = TRUE) + + ## ---- synonymous filter ---- + var_set <- Biostrings::DNAStringSet(df\$nt_seq) + var_aa <- Biostrings::translate(var_set, if.fuzzy.codon = "X") + syn_idx <- which(as.character(var_aa) == as.character(wt_aa)) + if (length(syn_idx) == 0L) stop("No fully-synonymous variants found relative to WT translation.") + + # helpers + mismatch_positions <- function(seq_nt_chars) which(seq_nt_chars != wt_chars) # 1-based positions + codon_index <- function(pos_vec) floor((pos_vec - 1L) / 3L) # 0-based codon bin + + # preference 1: exactly 2 mismatches, both within the same codon + cand_two_one <- Filter(function(i) { + vchars <- strsplit(df\$nt_seq[i], "", fixed = TRUE)[[1]] + pos <- mismatch_positions(vchars) + length(pos) == 2L && length(unique(codon_index(pos))) == 1L + }, syn_idx) + + choose_best <- function(idx_vec) idx_vec[ which.max(input_mean[idx_vec]) ] + + if (length(cand_two_one) > 0L) { + best_i <- choose_best(cand_two_one) + return(as.character(df\$nt_seq[best_i])) + } + + # preference 2 (fallback): exactly 1 mismatch (still synonymous) + cand_one <- Filter(function(i) { + vchars <- strsplit(df\$nt_seq[i], "", fixed = TRUE)[[1]] + length(mismatch_positions(vchars)) == 1L + }, syn_idx) + + if (length(cand_one) > 0L) { + best_i <- choose_best(cand_one) + return(as.character(df\$nt_seq[best_i])) + } + + stop("No suitable synonymous variant found: neither 2-in-1-codon nor 1-nt synonymous candidates present.") +} + +##### +# run function +##### +seq <- pick_synonymous_wt_from_range( + wt_fasta = "$wt_fasta", + counts_merged_tsv = "$counts_merged", + pos_range = "$pos_range" + ) +write(seq, file='synonymous_wt.txt') + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +Biostrings_version <- as.character(packageVersion("Biostrings")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(Biostrings_version) == 0) Biostrings_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-Biostrings:', Biostrings_version) + ), + f +) +close(f) diff --git a/modules/local/fitness/fitness_QC/environment.yml b/modules/local/fitness/fitness_QC/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/fitness/fitness_QC/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/fitness/fitness_QC/main.nf b/modules/local/fitness/fitness_QC/main.nf new file mode 100644 index 0000000..78420eb --- /dev/null +++ b/modules/local/fitness/fitness_QC/main.nf @@ -0,0 +1,31 @@ +process FITNESS_QC { + tag { sample.sample } + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(sample), path(fitness_estimation_tsv) // from FITNESS_CALCULATION + + output: + tuple val(sample), path("fitness_estimation_count_correlation.pdf"), emit: counts_corr_pdf + tuple val(sample), path("fitness_estimation_fitness_correlation.pdf"), emit: fitness_corr_pdf + path "versions.yml", emit: versions + + script: + template 'fitness_QC.R' + + stub: + """ + touch fitness_estimation_count_correlation.pdf + touch fitness_estimation_fitness_correlation.pdf + cat > versions.yml <<'EOF' + FITNESS_PLOTS: + stub-version: "0.0.0" + EOF + """ +} diff --git a/modules/local/fitness/fitness_QC/meta.yml b/modules/local/fitness/fitness_QC/meta.yml new file mode 100644 index 0000000..309e225 --- /dev/null +++ b/modules/local/fitness/fitness_QC/meta.yml @@ -0,0 +1,54 @@ +name: "fitness_qc" +description: Generates quality control visualizations for fitness estimation results, including correlation plots of raw counts and calculated fitness scores to assess experimental reproducibility. +keywords: + - deep mutational scanning + - dms + - quality control + - correlation + - fitness +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + +input: + - - sample: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test' ]` + - fitness_estimation_tsv: + type: file + description: TSV file containing calculated fitness estimates and associated statistics + pattern: "*.{tsv}" + +output: + - counts_corr_pdf: + - sample: + type: map + description: Groovy Map containing sample information + - fitness_estimation_count_correlation.pdf: + type: file + description: PDF plot showing the correlation of variant counts between samples/replicates + pattern: "fitness_estimation_count_correlation.pdf" + - fitness_corr_pdf: + - sample: + type: map + description: Groovy Map containing sample information + - fitness_estimation_fitness_correlation.pdf: + type: file + description: PDF plot showing the correlation of fitness scores between samples/replicates + pattern: "fitness_estimation_fitness_correlation.pdf" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/fitness/fitness_QC/templates/fitness_QC.R b/modules/local/fitness/fitness_QC/templates/fitness_QC.R new file mode 100644 index 0000000..6df7dfe --- /dev/null +++ b/modules/local/fitness/fitness_QC/templates/fitness_QC.R @@ -0,0 +1,128 @@ +#!/usr/bin/env Rscript + +## fitness QC plots for nf-core/deepmutscan +## 28.10.2025 +## maximilian.stammnitz@crg.eu + +## lower panels: scatter + x=y (log-log version for counts) +panel_xy_abline_counts <- function(x, y, ...) { + op <- par("xpd"); on.exit(par(xpd = op), add = TRUE) + par(xpd = FALSE) + points(x, y, pch = 16, cex = 0.1, ...) + abline(a = 0, b = 1, lty = 2, col = "grey50") +} + +## upper panels: Pearson r (log-log-transformed) +panel_cor_counts <- function(x, y, digits = 2, prefix = "r = ", cex.text = 1.4, ...) { + r <- suppressWarnings(cor(log(x), log(y), use = "pairwise.complete.obs", method = "pearson")) + lab <- if (is.finite(r)) bquote(italic(r) == .(round(r, digits))) else bquote(italic(r) == NA) + + ## Save/restore full graphics state we touch + op <- par(c("usr", "xpd", "xlog", "ylog")) + on.exit(par(op), add = TRUE) + + ## Draw in normalized 0..1 panel coords with logs OFF so text is visible + par(xlog = FALSE, ylog = FALSE, xpd = FALSE, usr = c(0, 1, 0, 1)) + text(0.5, 0.5, labels = lab, cex = cex.text, font = 1, col = "black") +} + +## lower panels: scatter + x=y (linear version for fitness) +panel_xy_abline_fitness <- function(x, y, ...) { + op <- par("xpd"); on.exit(par(xpd = op), add = TRUE) + par(xpd = FALSE) + points(x, y, pch = 16, cex = 0.5, ...) + abline(a = 0, b = 1, lty = 2, col = "grey50") +} + +## upper panels: Pearson r (linear) +panel_cor_fitness <- function(x, y, digits = 2, prefix = "r = ", cex.text = 1.4, ...) { + r <- suppressWarnings(cor(x, y, use = "pairwise.complete.obs", method = "pearson")) + lab <- if (is.finite(r)) bquote(italic(r) == .(round(r, digits))) else bquote(italic(r) == NA) + + ## Save/restore full graphics state we touch + op <- par(c("usr", "xpd", "xlog", "ylog")) + on.exit(par(op), add = TRUE) + + ## Draw in normalized 0..1 panel coords with logs OFF so text is visible + par(xlog = FALSE, ylog = FALSE, xpd = FALSE, usr = c(0, 1, 0, 1)) + text(0.5, 0.5, labels = lab, cex = cex.text, font = 1, col = "black") +} + +#' Plot input/output count correlations and fitness replicate correlations +#' +#' @param fitness_table_path Path to the input table (fitness_estimation.tsv) +#' @param out_counts_corr_pdf Path to write the counts correlation PDF +#' @param out_fitness_corr_pdf Path to write the fitness correlation PDF +#' +#' @return Invisibly returns TRUE; writes the two PDFs. +run_fitness_plots <- function(fitness_table_path, + out_counts_corr_pdf, + out_fitness_corr_pdf) { + + merged.counts.fitness <- read.table(fitness_table_path, sep = "\\t", header = TRUE, check.names = FALSE) + + ## identify the right samples + inputs <- grep("input", colnames(merged.counts.fitness)) + outputs <- grep("output", colnames(merged.counts.fitness)) + + ## 5. Plot input vs. output counts ## + ##################################### + pdf(out_counts_corr_pdf, height = 9, width = 14) + pairs(merged.counts.fitness[, c(inputs, outputs)] + 1, ## use a pseudo-count + lower.panel = panel_xy_abline_counts, + upper.panel = panel_cor_counts, + cex.text = 2, + log = "xy") + dev.off() + + ## 6. Plot fitness correlations ## + ################################## + fitness.repl <- grep("rescaled_fitness", colnames(merged.counts.fitness)) + + if (length(fitness.repl) > 1) { + pdf(out_fitness_corr_pdf, height = 9, width = 14) + pairs(merged.counts.fitness[, fitness.repl], + lower.panel = panel_xy_abline_fitness, + upper.panel = panel_cor_fitness, + cex.text = 2, + xlim = c(-3, 1), + ylim = c(-3, 1)) + dev.off() + } else { + ## If only one (or zero) rescaled_fitness columns exist, still create an empty placeholder + ## so Nextflow finds the declared output. + pdf(out_fitness_corr_pdf, height = 9, width = 14) + plot.new() + title("No replicate fitness columns found (need β‰₯2 'rescaled_fitness...')\\nCreated placeholder PDF.") + dev.off() + } + + invisible(TRUE) +} + + +##### +# run function +##### +run_fitness_plots( + fitness_table_path = "$fitness_estimation_tsv", + out_counts_corr_pdf = "fitness_estimation_count_correlation.pdf", + out_fitness_corr_pdf = "fitness_estimation_fitness_correlation.pdf" +) + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] + +if (is.null(r_version)) r_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version) + ), + f +) +close(f) diff --git a/modules/local/fitness/fitness_calculation/environment.yml b/modules/local/fitness/fitness_calculation/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/fitness/fitness_calculation/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/fitness/fitness_calculation/main.nf b/modules/local/fitness/fitness_calculation/main.nf new file mode 100644 index 0000000..5bf6208 --- /dev/null +++ b/modules/local/fitness/fitness_calculation/main.nf @@ -0,0 +1,29 @@ +process FITNESS_CALCULATION { + tag { sample.sample } + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(sample), path(counts_merged) + path(exp_design) + path(syn_wt_txt) + + output: + tuple val(sample), path("fitness_estimation.tsv"), emit: fitness_estimation + path "versions.yml", emit: versions + + script: + template 'fitness_calculation.R' + + stub: + """ + touch fitness_estimation.tsv + echo "FITNESS_CALCULATION:" > versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/fitness/fitness_calculation/meta.yml b/modules/local/fitness/fitness_calculation/meta.yml new file mode 100644 index 0000000..6be779d --- /dev/null +++ b/modules/local/fitness/fitness_calculation/meta.yml @@ -0,0 +1,54 @@ +name: "fitness_calculation" +description: Calculates fitness scores for Deep Mutational Scanning (DMS) variants. It typically uses a log-enrichment ratio approach, comparing variant frequencies in selection (output) versus baseline (input) samples, normalized to a synonymous wild-type reference. +keywords: + - deep mutational scanning + - dms + - fitness calculation + - log-ratio + - enrichment +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + +input: + - - sample: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test' ]` + - counts_merged: + type: file + description: TSV or CSV file containing the consolidated variant counts across all replicates + pattern: "*.{tsv,csv}" + - - exp_design: + type: file + description: CSV or text file defining the experimental design, including input/output mappings and replicates + pattern: "*.{csv,txt}" + - - syn_wt_txt: + type: file + description: Text file identifying the synonymous wild-type variant used as the normalization baseline + pattern: "*.txt" + +output: + - fitness_estimation: + - sample: + type: map + description: Groovy Map containing sample information + - fitness_estimation.tsv: + type: file + description: TSV file containing the final calculated fitness scores for all variants + pattern: "fitness_estimation.tsv" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/fitness/fitness_calculation/templates/fitness_calculation.R b/modules/local/fitness/fitness_calculation/templates/fitness_calculation.R new file mode 100644 index 0000000..4b0966b --- /dev/null +++ b/modules/local/fitness/fitness_calculation/templates/fitness_calculation.R @@ -0,0 +1,358 @@ +#!/usr/bin/env Rscript + +## default fitness estimation for nf-core/deepmutscan +## 18.02.2026 +## maximilian.stammnitz@crg.eu + +## 0. Libraries ## +################## + +suppressPackageStartupMessages({ + library(Biostrings) +}) + +## --- Helper functions --- + +# calculate nt hamming distances from the specified WT +compute_nt_hamming <- function(merged.counts, wt.seq) { + merged.counts <- cbind("nt_ham" = rep(NA, nrow(merged.counts)), merged.counts) + for (i in 1:nrow(merged.counts)){ + tmp.wt <- strsplit(as.character(wt.seq), "")[[1]] + tmp.mut <- strsplit(as.character(merged.counts\$nt_seq[i]), "")[[1]] + if(length(which(tmp.mut != tmp.wt)) == 0){ + merged.counts\$nt_ham[i] <- 0 + rm(tmp.mut, tmp.wt) + next + }else{ + merged.counts\$nt_ham[i] <- length(which(tmp.mut != tmp.wt)) + rm(tmp.mut, tmp.wt) + next + } + } + merged.counts +} + +# translate sequences and add aa_seq +add_aa_seq <- function(merged.counts) { + merged.counts <- cbind("aa_seq" = as.character(translate(DNAStringSet(merged.counts\$nt_seq))), merged.counts) + merged.counts +} + +# calculate AA hamming distances from the WT +compute_aa_hamming <- function(merged.counts, wt.seq.aa) { + merged.counts <- cbind("aa_ham" = rep(NA, nrow(merged.counts)), merged.counts) + for (i in 1:nrow(merged.counts)){ + tmp.wt <- strsplit(as.character(wt.seq.aa), "")[[1]] + tmp.mut <- strsplit(as.character(merged.counts\$aa_seq[i]), "")[[1]] + if(length(which(tmp.mut != tmp.wt)) == 0){ + merged.counts\$aa_ham[i] <- 0 + rm(tmp.mut, tmp.wt) + next + }else{ + merged.counts\$aa_ham[i] <- length(which(tmp.mut != tmp.wt)) + rm(tmp.mut, tmp.wt) + next + } + } + merged.counts +} + +# name the mutations +name_mutations <- function(merged.counts, wt.seq.aa) { + merged.counts <- cbind("wt_aa" = rep(NA, nrow(merged.counts)), + "pos" = rep(NA, nrow(merged.counts)), + "mut_aa" = rep(NA, nrow(merged.counts)), merged.counts) + for (i in 1:nrow(merged.counts)){ + if(merged.counts\$aa_ham[i] == 0){ + next + }else{ + tmp.wt <- strsplit(as.character(wt.seq.aa), "")[[1]] + tmp.mut <- strsplit(as.character(merged.counts\$aa_seq[i]), "")[[1]] + merged.counts\$pos[i] <- which(tmp.mut != tmp.wt) + merged.counts\$'wt_aa'[i] <- tmp.wt[merged.counts\$pos[i]] + merged.counts\$'mut_aa'[i] <- tmp.mut[merged.counts\$pos[i]] + rm(tmp.mut, tmp.wt) + } + } + merged.counts +} + +# find stops, WT and WT; aggregate counts of variants which are identical on the aa (but not nt) level +aggregate_by_aa <- function(merged.counts) { + ## find stops, WT and WT + merged.counts <- cbind(merged.counts, + "wt" = rep(NA, nrow(merged.counts)), + "stop" = rep(NA, nrow(merged.counts))) + merged.counts\$wt[which(merged.counts\$nt_ham == 0)] <- TRUE + merged.counts\$stop[which(merged.counts\$'mut_aa' == "*")] <- TRUE + + ## aggregate counts of variants which are identical on the aa (but not nt) level + ## exception: wildtype ones + ## thereby shrinking the matrix + uniq.aa.vars <- unique(merged.counts\$aa_seq) + uniq.aa.vars <- uniq.aa.vars[-which(uniq.aa.vars == merged.counts\$aa_seq[which(merged.counts\$wt == TRUE)])] + for(i in 1:length(uniq.aa.vars)){ + tmp.aa_seq <- uniq.aa.vars[i] + hits <- which(as.character(merged.counts\$aa_seq) == tmp.aa_seq) + if(length(hits) == 1){ + rm(tmp.aa_seq, hits) + next + }else{ + for(j in grep("input|output", colnames(merged.counts))){ + merged.counts[hits[1],j] <- sum(merged.counts[hits,j], na.rm = TRUE) + } + merged.counts[hits[1], "nt_seq"] <- paste(merged.counts[hits, "nt_seq"], collapse = ", ") + merged.counts <- merged.counts[-hits[-1],] + rm(tmp.aa_seq, hits) + next + } + } + merged.counts +} + +# calculate bimodal fitness distribution +density_peaks <- function(x, adjust = 1, ...) { + + ## obtain density distribution + d <- density(x, adjust = adjust, n = 5000, na.rm = T, ...) + y <- d\$y + idx <- which(diff(sign(diff(y))) == -2) + 1 + if (length(idx) == 0) return(numeric(0)) + + ## order density peaks by height (y value); take top 2 + idx <- idx[order(y[idx], decreasing = TRUE)] + peaks_x <- d\$x[idx] + peaks_x[seq_len(min(2, length(peaks_x)))] + +} + +# 3. Raw fitness calculations ## +calc_raw_fitness <- function(merged.counts, exp.design) { + ## how many fitness replicates are there + reps <- length(unique(exp.design\$experiment_replicate)) + for (i in 1:reps){ + merged.counts <- cbind(merged.counts, rep(NA, nrow(merged.counts))) + colnames(merged.counts)[ncol(merged.counts)] <- paste0("raw_fitness_rep", i) + } + + ## calculate raw fitness of all variants vs. WT variant + for (i in 1:reps){ + + ### collect counts + tmp.input.counts <- merged.counts[,paste0("input", i)] + tmp.output.counts <- merged.counts[,paste0("output", i)] + + ### add pseudo-count to zero-outputs (if the corresponding input count is non-zero) + tmp.output.counts[which(tmp.output.counts == 0 & tmp.input.counts != 0)] <- 1 + + ### take logs + tmp.wt.log.ratio <- log(tmp.output.counts[which(merged.counts\$wt == TRUE)] / + tmp.input.counts[which(merged.counts\$wt == TRUE)]) + tmp.fitness <- log(tmp.output.counts / + tmp.input.counts) - tmp.wt.log.ratio + + ### uncertain values to NA + tmp.fitness[which(is.na(tmp.fitness) == TRUE)] <- NA + tmp.fitness[which(tmp.fitness == "Inf")] <- NA + + ### add to table + merged.counts[,c(ncol(merged.counts) - reps + i)] <- tmp.fitness + + ### clean up + rm(tmp.fitness, tmp.wt.log.ratio, tmp.output.counts, tmp.input.counts) + } + + list(merged.counts = merged.counts, reps = reps) +} + +# 4. Fitness and error refinements ## +rescale_and_summarize <- function(merged.counts, reps) { + ## center the raw fitness distributions on 0 (median of wildtype synonymous) and -1 (median of stops) + for (i in 1:reps){ + + merged.counts <- cbind(merged.counts, rep(NA, nrow(merged.counts))) + colnames(merged.counts)[ncol(merged.counts)] <- paste0("rescaled_fitness_rep", i) + + ### anchors: median fitness of synonymous (aa_ham == 0) -> 0, of stops -> -1 + raw_col <- ncol(merged.counts) - reps + tmp.raw <- merged.counts[,raw_col] + tmp.wt.fitness.med <- median(tmp.raw[which(merged.counts\$aa_ham == 0)], na.rm = TRUE) + tmp.stop.fitness.med <- median(tmp.raw[which(merged.counts\$stop == TRUE)], na.rm = TRUE) + + ### if an anchor is missing, or the two anchors coincide (e.g. on sparse data, + ### where lm() would return an NA slope), fall back to the modes of the + ### (bimodal) fitness distribution to define the missing/degenerate anchor + if(is.na(tmp.wt.fitness.med) | is.na(tmp.stop.fitness.med) | + (!is.na(tmp.wt.fitness.med) & !is.na(tmp.stop.fitness.med) & tmp.wt.fitness.med == tmp.stop.fitness.med)){ + tmp.peaks <- sort(density_peaks(x = tmp.raw)) + tmp.hi <- if(!is.na(tmp.wt.fitness.med)) tmp.wt.fitness.med else if(length(tmp.peaks) >= 1) tmp.peaks[length(tmp.peaks)] else NA + if(!is.na(tmp.stop.fitness.med) & (is.na(tmp.hi) || tmp.stop.fitness.med != tmp.hi)){ + tmp.lo <- tmp.stop.fitness.med + }else{ + tmp.cand <- tmp.peaks[!is.na(tmp.peaks) & tmp.peaks != tmp.hi] + tmp.lo <- if(length(tmp.cand) >= 1) tmp.cand[1] else NA + } + tmp.wt.fitness.med <- tmp.hi + tmp.stop.fitness.med <- tmp.lo + rm(tmp.peaks, tmp.hi) + } + + ### two distinct anchors -> linear rescale; a single usable anchor -> WT-centre only + if(!is.na(tmp.wt.fitness.med) & !is.na(tmp.stop.fitness.med) & tmp.wt.fitness.med != tmp.stop.fitness.med){ + lm.rescale <- lm(c(0, -1) ~ c(tmp.wt.fitness.med, tmp.stop.fitness.med)) + merged.counts[,ncol(merged.counts)] <- tmp.raw * lm.rescale\$coefficients[[2]] + lm.rescale\$coefficients[[1]] + rm(lm.rescale) + }else if(!is.na(tmp.wt.fitness.med)){ + merged.counts[,ncol(merged.counts)] <- tmp.raw - tmp.wt.fitness.med + } + + rm(raw_col, tmp.raw, tmp.wt.fitness.med, tmp.stop.fitness.med) + } + + ## calculate fitness mean and standard deviation across replicates + merged.counts <- cbind(merged.counts, + "mean fitness" = rep(NA, nrow(merged.counts)), + "fitness sd" = rep(NA, nrow(merged.counts))) + + if(reps == 1){ + + merged.counts\$'mean fitness' <- merged.counts[,ncol(merged.counts) - 2] + + }else if(reps > 1){ + + merged.counts\$'mean fitness' <- apply(merged.counts[,c(ncol(merged.counts) - 1 - reps):c(ncol(merged.counts) - 2)], + 1, + mean, + na.rm = TRUE) + merged.counts\$'fitness sd' <- apply(merged.counts[,c(ncol(merged.counts) - 1 - reps):c(ncol(merged.counts) - 2)], + 1, + sd, + na.rm = TRUE) + + } + + merged.counts +} + +## --- Main function --- + +#' Run default fitness estimation with configurable I/O paths +#' +#' @param counts_path Path to counts_merged.tsv +#' @param design_path Path to experimentalDesign.tsv +#' @param wt_seq_path Path to synonymous_wt.txt (single line DNA sequence) +#' @param output_path Path to write fitness_estimation.tsv +#' +#' @return Invisibly returns the final data.frame; writes the output to output_path. +run_fitness_estimation <- function(counts_path, + design_path, + wt_seq_path, + output_path) { + ## 1. Import key files ## + ######################### + + merged.counts <- read.table(counts_path, sep = "\t", header = TRUE, check.names = FALSE) + exp.design <- read.table(design_path, sep = "\t", header = TRUE, check.names = FALSE) + wt.seq <- DNAString(as.character(read.table(wt_seq_path))) + wt.seq.aa <- translate(wt.seq) + + ## 2. Pre-processing the count table ## + ####################################### + + ## calculate nt hamming distances from the specified WT + merged.counts <- compute_nt_hamming(merged.counts, wt.seq) + + ## translate sequences + merged.counts <- add_aa_seq(merged.counts) + + ## calculate AA hamming distances from the WT + merged.counts <- compute_aa_hamming(merged.counts, wt.seq.aa) + + ## name the mutations + merged.counts <- name_mutations(merged.counts, wt.seq.aa) + + ## find stops, WT and WT; aggregate AA-identical variants (except WT) + merged.counts <- aggregate_by_aa(merged.counts) + + ## 3. Raw fitness calculations ## + ################################# + fitness_res <- calc_raw_fitness(merged.counts, exp.design) + merged.counts <- fitness_res\$merged.counts + reps <- fitness_res\$reps + + ## 4. Fitness and error refinements ## + ###################################### + merged.counts <- rescale_and_summarize(merged.counts, reps) + + ## clean up + rm(reps) + + ## export + write.table(merged.counts, output_path, + col.names = TRUE, row.names = FALSE, quote = FALSE, sep = "\t", na = "") + + invisible(merged.counts) +} + + +## 5. Version ## +################ + +# sessionInfo() +# R version 4.5.1 (2025-06-13) +# Platform: aarch64-apple-darwin20 +# Running under: macOS Sonoma 14.6.1 +# +# Matrix products: default +# BLAS: /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib +# LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1 +# +# locale: +# [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8 +# +# time zone: Europe/Madrid +# tzcode source: internal +# +# attached base packages: +# [1] stats4 stats graphics grDevices utils datasets methods base +# +# other attached packages: +# [1] Biostrings_2.76.0 GenomeInfoDb_1.44.2 XVector_0.48.0 IRanges_2.42.0 S4Vectors_0.46.0 +# [6] BiocGenerics_0.54.0 generics_0.1.4 +# +# loaded via a namespace (and not attached): +# [1] httr_1.4.7 compiler_4.5.1 R6_2.6.1 tools_4.5.1 +# [5] GenomeInfoDbData_1.2.14 rstudioapi_0.17.1 crayon_1.5.3 UCSC.utils_1.4.0 +# [9] jsonlite_2.0.0 + + + +##### +# run function +##### +run_fitness_estimation( + counts_path = "$counts_merged", + design_path = "$exp_design", + wt_seq_path = "$syn_wt_txt", + output_path = "fitness_estimation.tsv" +) + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +Biostrings_version <- as.character(packageVersion("Biostrings")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(Biostrings_version) == 0) Biostrings_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-Biostrings:', Biostrings_version) + ), + f +) +close(f) diff --git a/modules/local/fitness/fitness_experimental_design/environment.yml b/modules/local/fitness/fitness_experimental_design/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/fitness/fitness_experimental_design/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/fitness/fitness_experimental_design/main.nf b/modules/local/fitness/fitness_experimental_design/main.nf new file mode 100644 index 0000000..b1793ba --- /dev/null +++ b/modules/local/fitness/fitness_experimental_design/main.nf @@ -0,0 +1,29 @@ +process EXPDESIGN_FITNESS { + tag "experimentalDesign" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + path samplesheet_csv + + output: + path "experimentalDesign.tsv", emit: experimental_design + path "versions.yml", emit: versions + + script: + template 'dimsum_experimentalDesign.R' + + stub: + """ + touch experimentalDesign.tsv + cat <<-END_VERSIONS > versions.yml + "${task.process}": + r-base: "0.0.0" + END_VERSIONS + """ +} diff --git a/modules/local/fitness/fitness_experimental_design/meta.yml b/modules/local/fitness/fitness_experimental_design/meta.yml new file mode 100644 index 0000000..74d5c93 --- /dev/null +++ b/modules/local/fitness/fitness_experimental_design/meta.yml @@ -0,0 +1,37 @@ +name: "expdesign_fitness" +description: Transforms a standard nf-core samplesheet into a tab-separated experimental design file that defines replicates, selection conditions, and input/output relationships for fitness calculation. +keywords: + - deep mutational scanning + - dms + - experimental design + - samplesheet + - dimsum + - mutscan +tools: + - "r-base": + description: "R is a free software environment for statistical computing and graphics" + homepage: "https://www.r-project.org/" + documentation: "https://cran.r-project.org/manuals.html" + licence: ["GPL-2.0-or-later"] + +input: + - - samplesheet_csv: + type: file + description: The primary nf-core pipeline samplesheet containing metadata for all sequencing libraries + pattern: "*.{csv}" + +output: + - experimental_design: + - experimentalDesign.tsv: + type: file + description: A TSV file formatted specifically for DiMSum or mutscan experimental design requirements + pattern: "experimentalDesign.tsv" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/fitness/fitness_experimental_design/templates/dimsum_experimentalDesign.R b/modules/local/fitness/fitness_experimental_design/templates/dimsum_experimentalDesign.R new file mode 100644 index 0000000..475353d --- /dev/null +++ b/modules/local/fitness/fitness_experimental_design/templates/dimsum_experimentalDesign.R @@ -0,0 +1,111 @@ +#!/usr/bin/env Rscript + +# Make a DiMSum experimental design from a deepmutscan samplesheet. +# - samplesheet_csv: path to CSV with columns sample,type,replicate,file1,file2 +# - out_path: where to write the TSV (default "experimentalDesign.tsv") +# Returns: the experimental design as a data.frame +make_dimsum_experimental_design <- function(samplesheet_csv, out_path = "experimentalDesign.tsv") { + # ---- read & normalize ---- + ss <- read.csv(samplesheet_csv, stringsAsFactors = FALSE, check.names = FALSE) + names(ss) <- tolower(names(ss)) + + # tolerate missing file2 column (single-end) + if (!"file2" %in% names(ss)) ss\$file2 <- "" + + required <- c("sample", "type", "replicate", "file1", "file2") + missing <- setdiff(required, names(ss)) + if (length(missing) > 0) stop("Samplesheet missing columns: ", paste(missing, collapse = ", ")) + + # coerce types + ss\$replicate <- as.integer(ss\$replicate) + + # ---- keep only the samples DiMSum scores ---- + # A samplesheet legitimately carries other row types: `wildtype` (required by + # --error_correction wildtype) and `quality`. They are real samples, but they are not part of the + # selection design - they feed error correction and QC - and MERGE_COUNTS drops them too. Leaving + # them in gave them an empty `selection_id`, which DiMSum reads back as NA and then dies in + # dimsum__check_experiment_design on `sum(exp_design[,"selection_id"] < 0)` with the opaque + # "missing value where TRUE/FALSE needed". + ss <- ss[ss\$type %in% c("input", "output"), , drop = FALSE] + if (nrow(ss) == 0) { + stop("Samplesheet has no 'input'/'output' rows - DiMSum needs both to compute fitness.") + } + + # ---- order rows: by sample (if multiple), input before output, then replicate ---- + # Sorting happens before naming, because the names are derived from the row order below. + multi_base <- length(unique(ss\$sample)) > 1 + type_rank <- match(ss\$type, c("input", "output")) + ord <- if (multi_base) { + order(ss\$sample, type_rank, ss\$replicate) + } else { + order(type_rank, ss\$replicate) + } + ss <- ss[ord, , drop = FALSE] + + # ---- sample_name: must equal the count column MERGE_COUNTS writes for this row ---- + # merge_counts.R names its columns positionally - paste0("input", seq_len(n_in)) over the files + # sorted by replicate - so the name has to come from each row's POSITION within its type, not from + # the samplesheet's `replicate` value. The two only coincide when the replicates happen to be a + # contiguous 1..N; replicates 1,2,4 would otherwise produce "input4" for a column named "input3" + # and DiMSum could not match them. DiMSum also requires sample_name to be alphanumeric and to + # start with a letter, so the parts are joined without a separator. + idx <- stats::ave(seq_len(nrow(ss)), ss\$sample, ss\$type, FUN = seq_along) + # NOTE: for multiple biological samples these names still will not match the counts file, because + # one design is built from the whole samplesheet and broadcast to every per-sample DiMSum run + # (see subworkflows/local/calculate_fitness/main.nf). Multi-sample + DiMSum is a separate, + # known-broken case; single-sample runs are correct. + sample_name <- if (multi_base) paste0(ss\$sample, ss\$type, idx) else paste0(ss\$type, idx) + + # ---- build DiMSum columns ---- + experiment_replicate <- ss\$replicate # pairs an input with an output; need not be 1..N + selection_id <- ifelse(ss\$type == "input", 0L, 1L) + # assume one selection batch; DiMSum requires this to be blank for input samples + selection_replicate <- ifelse(ss\$type == "output", 1L, NA_integer_) + # assume one technical batch + technical_replicate <- rep(1L, nrow(ss)) + + pair1 <- basename(ss\$file1) + # keep empty string for single-end / missing file2 + pair2 <- ifelse(is.na(ss\$file2) | ss\$file2 == "", "", basename(ss\$file2)) + + ed <- data.frame( + sample_name = sample_name, + experiment_replicate = experiment_replicate, + selection_id = selection_id, + selection_replicate = selection_replicate, + technical_replicate = technical_replicate, + pair1 = pair1, + pair2 = pair2, + stringsAsFactors = FALSE + ) + rownames(ed) <- NULL + + # ---- write & return ---- + write.table(ed, file = out_path, sep = "\\t", row.names = FALSE, col.names = TRUE, quote = FALSE, na = "") + return(ed) +} + +##### +# run function +##### +make_dimsum_experimental_design( + samplesheet_csv = "$samplesheet_csv", + out_path = "experimentalDesign.tsv" +) + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] + +if (is.null(r_version)) r_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version) + ), + f +) +close(f) diff --git a/modules/local/fitness/fitness_heatmap/environment.yml b/modules/local/fitness/fitness_heatmap/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/fitness/fitness_heatmap/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/fitness/fitness_heatmap/main.nf b/modules/local/fitness/fitness_heatmap/main.nf new file mode 100644 index 0000000..4106ba6 --- /dev/null +++ b/modules/local/fitness/fitness_heatmap/main.nf @@ -0,0 +1,30 @@ +process FITNESS_HEATMAP { + tag { sample.sample } + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(sample), path(fitness_estimation_tsv) // from FITNESS_CALCULATION + tuple val(sample2), path(wt_seq) // WT sequence + + output: + tuple val(sample), path("fitness_heatmap.pdf"), emit: fitness_heatmap + path "versions.yml", emit: versions + + script: + template 'fitness_heatmap.R' + + stub: + """ + touch fitness_heatmap.pdf + cat > versions.yml <<'EOF' + FITNESS_HEATMAP: + stub-version: "0.0.0" + EOF + """ +} diff --git a/modules/local/fitness/fitness_heatmap/meta.yml b/modules/local/fitness/fitness_heatmap/meta.yml new file mode 100644 index 0000000..7b22869 --- /dev/null +++ b/modules/local/fitness/fitness_heatmap/meta.yml @@ -0,0 +1,59 @@ +name: "fitness_heatmap" +description: Generates a fitness landscape heatmap from calculated fitness scores, displaying the functional impact of amino acid substitutions at each position of the reference sequence. +keywords: + - deep mutational scanning + - dms + - fitness + - visualization + - heatmap + - mutation landscape +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + - "bioconductor-biostrings": + description: "Efficient manipulation of biological strings in R" + homepage: "https://bioconductor.org/packages/Biostrings" + licence: ["Artistic-2.0"] + +input: + - - sample: + type: map + description: | + Groovy Map containing sample information for the fitness data + e.g. `[ id:'test' ]` + - fitness_estimation_tsv: + type: file + description: TSV file containing calculated fitness scores and statistics + pattern: "*.{tsv}" + - - sample2: + type: map + description: | + Groovy Map containing metadata for the reference sequence + - wt_seq: + type: file + description: FASTA file or text file containing the wild-type reference sequence + pattern: "*.{fasta,fa,txt}" + +output: + - fitness_heatmap: + - sample: + type: map + description: Groovy Map containing sample information + - fitness_heatmap.pdf: + type: file + description: PDF heatmap visualizing the fitness scores across the protein sequence + pattern: "fitness_heatmap.pdf" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/fitness/fitness_heatmap/templates/fitness_heatmap.R b/modules/local/fitness/fitness_heatmap/templates/fitness_heatmap.R new file mode 100644 index 0000000..e860382 --- /dev/null +++ b/modules/local/fitness/fitness_heatmap/templates/fitness_heatmap.R @@ -0,0 +1,312 @@ +#!/usr/bin/env Rscript + +suppressPackageStartupMessages({ + library(methods) + library(dplyr) + library(ggplot2) + library(grid) # for unit() +}) + +# ---------- helper functions ---------- +find_col <- function(df, candidates) { + norm <- function(x) gsub("[^a-z0-9]+", "_", tolower(x)) + nms <- colnames(df); nn <- norm(nms) + for (cand in candidates) { + hit <- which(nn == norm(cand)) + if (length(hit) == 1) return(nms[hit]) + } + stop(sprintf("Could not find any of columns: %s", paste(candidates, collapse = ", "))) +} + +get_rescaled_cols <- function(df) { + nms <- colnames(df) + hits <- grep("^rescaled[_ ]?fitness", nms, ignore.case = TRUE, value = TRUE) + if (!length(hits)) stop("No 'rescaled_fitness' columns found.") + idx <- suppressWarnings(as.integer(gsub(".*?([0-9]+)\$", "\\\\1", hits))) # additional backslashs to make it groovy readable + hits[order(is.na(idx), idx, hits)] +} + +# Find "mean fitness" column (if present) +find_mean_col <- function(df) { + nms <- colnames(df) + key <- tolower(gsub("[^a-z0-9]+", "_", nms)) + hit <- which(key == "mean_fitness") + if (length(hit) == 1) nms[hit] else NULL +} + +# NEW: read WT amino acid sequence from .txt file (single line) +read_wt_seq_aa_txt <- function(path) { + if (is.null(path)) stop("wt_seq_aa_txt_path must be provided.") + x <- readLines(path, warn = FALSE) + x <- x[nzchar(x)] + if (!length(x)) stop("WT AA TXT is empty.") + aa <- toupper(gsub("\\\\s+", "", x[which.max(nchar(x))])) + # Keep only valid amino acid letters (including stop '*') + aa <- gsub("[^ACDEFGHIKLMNPQRSTVWY*]", "", aa) + if (!nchar(aa)) stop("WT AA TXT contains no valid AA letters.") + aa +} + +# Build full AAΓ—position grid for positions 1..wt_len (from WT sequence), +# join fitness values; any missing combos stay NA (-> grey). +# Positions > wt_len are considered padded and will be white. +build_heatmap_long <- function(df, + wt_aa_col, + pos_col, + mut_aa_col, + fitness_col, + positions_per_row = 75, + wt_seq_aa, + fill_missing_as_zero = FALSE) { + + # authoritative WT length from provided sequence + letters <- strsplit(wt_seq_aa, "", fixed = TRUE)[[1]] + wt_len <- length(letters) + + # normalize data + df0 <- df %>% + transmute( + position = suppressWarnings(as.numeric(.data[[pos_col]])), + wt_aa_in = .data[[wt_aa_col]], + mut_aa = .data[[mut_aa_col]], + fitness = suppressWarnings(as.numeric(.data[[fitness_col]])) + ) %>% + filter(is.finite(position)) + + # drop any rows that claim positions beyond WT length + if (nrow(df0) && any(df0\$position > wt_len, na.rm = TRUE)) { + dropped <- sum(df0\$position > wt_len, na.rm = TRUE) + warning(sprintf("Dropping %d row(s) with position > WT length (%d).", dropped, wt_len)) + df0 <- df0 %>% filter(position <= wt_len) + } + + # pad to next multiple of 75 (by rows) + rem <- wt_len %% positions_per_row + pad_need <- if (rem == 0) 0 else positions_per_row - rem + max_paded <- wt_len + pad_need + + # full grid: positions 1..max_paded (so the tail exists), AA set of 21 + all_positions <- seq_len(max_paded) + all_amino_acids <- c("A","C","D","E","F","G","H","I","K","L", + "M","N","P","Q","R","S","T","V","W","Y","*") + + grid_df <- expand.grid(position = all_positions, + mut_aa = all_amino_acids, + KEEP.OUT.ATTRS = FALSE, stringsAsFactors = FALSE) %>% + mutate(is_padded = position > wt_len) + + # join fitness only for real positions (<= wt_len) + fit_df <- df0 %>% select(position, mut_aa, fitness) + d <- grid_df %>% + left_join(fit_df, by = c("position","mut_aa")) + + if (fill_missing_as_zero) { + d\$fitness[is.na(d\$fitness) & d\$position <= wt_len] <- 0 + } + + # authoritative WT AA per real position; tail gets placeholder 'Y' + wt_map <- tibble(position = seq_len(wt_len), wt_aa = letters) + d <- d %>% + left_join(wt_map, by = "position") %>% + mutate(wt_aa = ifelse(is.na(wt_aa) & position > wt_len, "Y", wt_aa)) + + # layout fields + d <- d %>% + mutate( + row_group = ((position - 1) %/% positions_per_row) + 1, + wt_aa_pos = paste0(wt_aa, position), + wt_aa_pos = factor(wt_aa_pos, levels = unique(wt_aa_pos)), + synonymous = mut_aa == wt_aa + ) + + # IMPORTANT: use WT length as the true end of the protein + d\$max_pos <- wt_len + d +} + +syn_segments <- function(d, positions_per_row = 75) { + amino_order <- rev(c("G", "A", "V", "L", "M", "I", "F", + "Y", "W", "K", "R", "H", "D", "E", + "S", "T", "C", "N", "Q", "P", "*")) + d %>% + mutate( + mut_aa = factor(mut_aa, levels = amino_order), + x = as.numeric(factor(wt_aa_pos, levels = levels(wt_aa_pos))) - + ((row_group - 1) * positions_per_row), + y = as.numeric(factor(mut_aa, levels = amino_order)) + ) %>% + filter(synonymous, position <= max_pos) +} + +# Draw one solid white rectangle per row group covering the padded tail region +white_tail_rects <- function(d, positions_per_row = 75) { + wt_len <- unique(d\$max_pos)[1] + if (!is.finite(wt_len)) return(dplyr::tibble()[0,]) + + # if perfectly divisible by 75, there is no tail to cover + if (wt_len %% positions_per_row == 0) return(dplyr::tibble()[0,]) + + # which facet (row group) contains the last real position? + last_group <- ((wt_len - 1) %/% positions_per_row) + 1 + last_local_idx <- ((wt_len - 1) %% positions_per_row) + 1 # 1..75 within the facet + + tibble::tibble( + row_group = last_group, + xmin = last_local_idx + 0.5 - 0.025, # tiny epsilon to avoid hairlines + xmax = positions_per_row + 0.5 + 0.025, + ymin = 0.5 - 0.025, + ymax = 21.5 + 0.025 + ) +} + +plot_heatmap <- function(d, title_text, positions_per_row = 75) { + amino_order <- rev(c("G", "A", "V", "L", "M", "I", "F", + "Y", "W", "K", "R", "H", "D", "E", + "S", "T", "C", "N", "Q", "P", "*")) + d <- d %>% mutate(mut_aa = factor(mut_aa, levels = amino_order)) + + min_f <- suppressWarnings(min(d\$fitness, na.rm = TRUE)); if (!is.finite(min_f)) min_f <- 0 + max_f <- suppressWarnings(max(d\$fitness, na.rm = TRUE)); if (!is.finite(max_f)) max_f <- 0 + max_orig_pos <- unique(d\$max_pos)[1] + + syn <- syn_segments(d, positions_per_row) + rect <- white_tail_rects(d, positions_per_row) + + ggplot(d, aes(x = wt_aa_pos, y = mut_aa, fill = fitness)) + + scale_fill_gradientn( + colours = c("#D73027", "#F0F0F0", "#4575B4"), + values = if ((abs(min_f) + max_f) > 0) c(0, abs(min_f)/(abs(min_f)+max_f), 1) else c(0, 0.5, 1), + na.value = "grey35", + limits = c(min_f, max_f) + ) + + scale_x_discrete( + labels = function(x) { + num <- suppressWarnings(as.numeric(gsub("[^0-9]", "", x))) + ifelse(num > max_orig_pos, " ", x) + }, + expand = expansion(mult = c(0, 0)) # no extra margin area + ) + + geom_tile() + + # Solid white block covering the tail (no pattern / no seams) + { if (nrow(rect)) geom_rect(data = rect, inherit.aes = FALSE, + aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax), + fill = "white", color = NA) } + + geom_segment( + data = syn, + aes(x = x - 0.485, xend = x + 0.485, y = y - 0.485, yend = y + 0.485), + linewidth = 0.2, inherit.aes = FALSE, color = "grey10" + ) + + theme_minimal() + + labs(title = title_text, x = "Wild-type amino acid", y = "Mutant amino acid", fill = "Fitness") + + theme( + plot.title = element_text(size = 16, face = "bold"), + axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.5, size = 10), + axis.text.y = element_text(size = 10), + axis.title.x = element_text(size = 14), + axis.title.y = element_text(size = 14), + legend.title = element_text(size = 12), + legend.text = element_text(size = 10), + panel.grid.major = element_blank(), + panel.grid.minor = element_blank(), + strip.text = element_blank(), + strip.background = element_blank(), + panel.spacing = grid::unit(0.2, "lines") + ) + + facet_wrap(~ row_group, scales = "free_x", ncol = 1) +} + +# ---------- main callable ---------- +# fitness_table_path : path to fitness_estimation.tsv +# wt_seq_aa_txt_path : path to TXT file containing WT AA sequence (one line) +# output_pdf_path : output PDF (default "fitness_heatmap.pdf") +# positions_per_row : default 75 +run_fitness_rescaled_heatmaps <- function(fitness_table_path, + wt_seq_aa_txt_path, + output_pdf_path = "fitness_heatmap.pdf", + positions_per_row = 75) { + + df <- read.table( + fitness_table_path, sep = "\t", header = TRUE, + check.names = FALSE, quote = "", comment.char = "" + ) + + wt_aa_col <- find_col(df, c("wt aa", "wt_aa", "wt")) + pos_col <- find_col(df, c("pos", "position")) + mut_aa_col <- find_col(df, c("mut aa", "mut_aa", "aa")) + rescaled_cols <- get_rescaled_cols(df) + + wt_seq_aa <- read_wt_seq_aa_txt(wt_seq_aa_txt_path) + + plots <- list() + + ## 1) Mean first – use existing "mean fitness" column if available + mean_col <- find_mean_col(df) + if (is.null(mean_col)) { + df\$`rescaled_fitness_mean` <- if (length(rescaled_cols) == 1) df[[rescaled_cols[1]]] else rowMeans(df[, rescaled_cols], na.rm = TRUE) + mean_col <- "rescaled_fitness_mean" + } + long_df_mean <- build_heatmap_long(df, wt_aa_col, pos_col, mut_aa_col, mean_col, + positions_per_row, wt_seq_aa) + plots[[length(plots) + 1]] <- list( + title = sprintf("Fitness β€” mean of %d replicate(s)", length(rescaled_cols)), + data = long_df_mean + ) + + ## 2) Then individual replicates + for (i in seq_along(rescaled_cols)) { + col <- rescaled_cols[i] + long_df <- build_heatmap_long(df, wt_aa_col, pos_col, mut_aa_col, col, + positions_per_row, wt_seq_aa) + plots[[length(plots) + 1]] <- list( + title = sprintf("Fitness β€” rep%d", i), + data = long_df + ) + } + + # Device height: (#row groups Γ— 4) + page_heights <- vapply(plots, function(p) max(p\$data\$row_group, na.rm = TRUE), numeric(1)) + device_height <- max(4, as.numeric(page_heights) * 4, na.rm = TRUE) + + grDevices::pdf(output_pdf_path, width = 16, height = device_height) + on.exit(try(grDevices::dev.off(), silent = TRUE), add = TRUE) + for (p in plots) print(plot_heatmap(p\$data, p\$title, positions_per_row)) + invisible(TRUE) +} + +#### +# run function +#### +run_fitness_rescaled_heatmaps( + fitness_table_path = "$fitness_estimation_tsv", + wt_seq_aa_txt_path = "$wt_seq", + output_pdf_path = "fitness_heatmap.pdf" +) + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +dplyr_version <- as.character(packageVersion("dplyr")) +ggplot2_version <- as.character(packageVersion("ggplot2")) +methods_version <- as.character(packageVersion("methods")) +grid_version <- as.character(packageVersion("grid")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(dplyr_version) == 0) dplyr_version <- "unknown" +if (length(ggplot2_version) == 0) ggplot2_version <- "unknown" +if (length(methods_version) == 0) methods_version <- "unknown" +if (length(grid_version) == 0) grid_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-dplyr:', dplyr_version), + paste(' r-ggplot2:', ggplot2_version), + paste(' r-methods:', methods_version), + paste(' r-grid:', grid_version) + ), + f +) +close(f) diff --git a/modules/local/fitness/merge_counts/environment.yml b/modules/local/fitness/merge_counts/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/fitness/merge_counts/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/fitness/merge_counts/main.nf b/modules/local/fitness/merge_counts/main.nf new file mode 100644 index 0000000..c85d2f4 --- /dev/null +++ b/modules/local/fitness/merge_counts/main.nf @@ -0,0 +1,29 @@ +process MERGE_COUNTS { + tag "${sample.sample}" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(sample), path(input_counts), path(output_counts) + + output: + tuple val(sample), path("counts_merged.tsv"), emit: merged_counts + path "versions.yml", emit: versions + + script: + template 'merge_counts.R' + + stub: + """ + touch counts_merged.tsv + cat <<-END_VERSIONS > versions.yml + "${task.process}": + r-base: "0.0.0" + END_VERSIONS + """ +} diff --git a/modules/local/fitness/merge_counts/meta.yml b/modules/local/fitness/merge_counts/meta.yml new file mode 100644 index 0000000..87e035a --- /dev/null +++ b/modules/local/fitness/merge_counts/meta.yml @@ -0,0 +1,51 @@ +name: "merge_counts" +description: Consolidates variant counts from multiple input (baseline) and output (selection) files into a single merged TSV table, organized by sample or replicate. +keywords: + - deep mutational scanning + - dms + - count merging + - variant table + - preprocessing +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + +input: + - - sample: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test' ]` + - metas: + type: list + description: A list of metadata maps corresponding to the input and output count files + - input_counts: + type: list + description: A list of paths to count files representing the baseline/input population + - output_counts: + type: list + description: A list of paths to count files representing the selection/output population + +output: + - merged_counts: + - sample: + type: map + description: Groovy Map containing sample information + - counts_merged.tsv: + type: file + description: A consolidated TSV file containing merged counts for all provided inputs and outputs + pattern: "counts_merged.tsv" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/fitness/merge_counts/templates/merge_counts.R b/modules/local/fitness/merge_counts/templates/merge_counts.R new file mode 100644 index 0000000..8e39be8 --- /dev/null +++ b/modules/local/fitness/merge_counts/templates/merge_counts.R @@ -0,0 +1,112 @@ +#!/usr/bin/env Rscript + +# -------------------------------------------------------------- +# 1. SETUP & INPUTS +# -------------------------------------------------------------- + +raw_inputs_str <- "$input_counts" +raw_outputs_str <- "$output_counts" + +parse_paths <- function(x) { + if (x == "" || x == "[]") return(character(0)) + # Split at spaces + parts <- strsplit(x, "\\\\s+")[[1]] + # Remove empty strings + parts[parts != ""] +} + +input_paths <- parse_paths(raw_inputs_str) +output_paths <- parse_paths(raw_outputs_str) + +message("Found ", length(input_paths), " input files.") +message("Found ", length(output_paths), " output files.") + +# -------------------------------------------------------------- +# 2. MERGE LOGIC +# -------------------------------------------------------------- + +# Helper to read a 2-col TSV without header +read_counts <- function(fp) { + utils::read.table( + fp, header = FALSE, sep = "\\t", quote = "", + col.names = c("nt_seq", "count"), + colClasses = c("character", "numeric"), + comment.char = "", check.names = FALSE + ) +} + +# Read all inputs / outputs +input_list <- lapply(input_paths, read_counts) +output_list <- lapply(output_paths, read_counts) + +# Collect universe of sequences +all_seqs <- unique(c( + unlist(lapply(input_list, function(x) x\$nt_seq)), + unlist(lapply(output_list, function(x) x\$nt_seq)) +)) + +# Pre-allocate output frame +n_in <- length(input_list) +n_out <- length(output_list) + +col_names <- c( + "nt_seq", + if (n_in > 0) paste0("input", seq_len(n_in)) else character(0), + if (n_out > 0) paste0("output", seq_len(n_out)) else character(0) +) + +# Initialize dataframe with 0 counts +out <- data.frame( + nt_seq = all_seqs, + matrix(0, nrow = length(all_seqs), ncol = n_in + n_out), + stringsAsFactors = FALSE, check.names = FALSE +) +names(out) <- col_names + +# Fill inputs +if (n_in > 0) { + for (i in seq_len(n_in)) { + df <- input_list[[i]] + # Match sequences: WICHTIG \$ escaping + idx <- match(df\$nt_seq, out\$nt_seq) + # Assign counts: WICHTIG \$ escaping + out[idx, paste0("input", i)] <- df\$count + } +} + +# Fill outputs +if (n_out > 0) { + for (j in seq_len(n_out)) { + df <- output_list[[j]] + # WICHTIG \$ escaping + idx <- match(df\$nt_seq, out\$nt_seq) + out[idx, paste0("output", j)] <- df\$count + } +} + +# Write Output +utils::write.table( + out, + file = "counts_merged.tsv", + sep = "\\t", + row.names = FALSE, + col.names = TRUE, + quote = FALSE +) + +# -------------------------------------------------------------- +# 3. VERSIONING +# -------------------------------------------------------------- + +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +if (is.null(r_version)) r_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version) + ), + f +) +close(f) diff --git a/modules/local/fitness/run_dimsum/environment.yml b/modules/local/fitness/run_dimsum/environment.yml new file mode 100644 index 0000000..1c93270 --- /dev/null +++ b/modules/local/fitness/run_dimsum/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + # Every pin here mirrors the paired container exactly. r-base must be pinned: with r-dimsum alone the + # solver picks an ancient R (4.0.5), not the container's 4.4.2. ggplot2/GGally must ALSO be pinned: + # DiMSum 1.4 builds its merge/diagnostics reports through GGally, which breaks under ggplot2 4.0 (the + # solver floats to 4.0.3 on R 4.4.2) with "object 'dimsum_meta_new_report' not found" and aborts the + # run; the container holds ggplot2 at 3.5.1 / GGally at 2.2.1, so we do the same. + - bioconda::r-dimsum=1.4 + - conda-forge::r-base=4.4.2 + - conda-forge::r-data.table=1.16.4 + - bioconda::bioconductor-biostrings=2.74.0 + - conda-forge::r-ggplot2=3.5.1 + - conda-forge::r-ggally=2.2.1 diff --git a/modules/local/fitness/run_dimsum/main.nf b/modules/local/fitness/run_dimsum/main.nf new file mode 100644 index 0000000..9c7d11c --- /dev/null +++ b/modules/local/fitness/run_dimsum/main.nf @@ -0,0 +1,54 @@ +process RUN_DIMSUM { + tag { sample.sample } + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/92/9298c391f285d7f2c1155f6a519a96c4f7591971780ba4f10569558282f40b6f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_r-dimsum_r-base_r-data.table_pruned:cca13eed371a9d84' }" + + input: + tuple val(sample), path(counts_merged) + path(wt_txt) + path(exp_design) + + output: + path "dimsum_results**", emit: results_dir + path "versions.yml", emit: versions + + script: + """ + set -euo pipefail + + # DiMSum expects the sequence string, not a file path + WT=\$(tr -d ' \r\\n\\t' < "$wt_txt") + + DiMSum \ + --experimentDesignPath "$exp_design" \ + --wildtypeSequence "\$WT" \ + --countPath "$counts_merged" \ + --startStage 4 \ + --stopStage 5 \ + --fitnessErrorModel F \ + --retainIntermediateFiles T \ + --projectName "dimsum_results" \ + --fastqFileDir . \ + + R_VERSION=\$(R --version | head -n 1 | sed -E 's/^R version ([0-9.]+).*/\\1/') + cat <<-END_VERSIONS > versions.yml + DIMSUM_RUN: + r-base: \$R_VERSION +END_VERSIONS + """ + + stub: + """ + mkdir -p dimsum_results + touch dimsum_results/report.html + cat <<-END_VERSIONS > versions.yml + "${task.process}": + r-base: "0.0.0" + END_VERSIONS + """ +} diff --git a/modules/local/fitness/run_dimsum/meta.yml b/modules/local/fitness/run_dimsum/meta.yml new file mode 100644 index 0000000..3e889f9 --- /dev/null +++ b/modules/local/fitness/run_dimsum/meta.yml @@ -0,0 +1,49 @@ +name: "run_dimsum" +description: Runs DiMSum to estimate fitness and functionality scores from Deep Mutational Scanning (DMS) count data. +keywords: + - deep mutational scanning + - dms + - fitness + - dimsum +tools: + - "dimsum": + description: "An error-model-enriched pipeline for analyzing deep mutational scanning data" + homepage: "https://github.com/lehner-lab/DiMSum" + documentation: "https://github.com/lehner-lab/DiMSum/blob/master/README.md" + doi: "10.1186/s12859-020-03709-3" + licence: ["GPL-3.0-or-later"] + +input: + - - sample: + type: map + description: | + Groovy Map containing sample information. + Matches the structure used in the pipeline (e.g., [ id:'test' ]) + - counts_merged: + type: file + description: A TSV file containing variant counts merged across samples + pattern: "*.{tsv,csv}" + - - wt_txt: + type: file + description: A text file containing the wild-type DNA/amino acid sequence string + pattern: "*.{txt}" + - - exp_design: + type: file + description: DiMSum-specific experimental design file describing replicates and conditions + pattern: "*.{txt,csv}" + +output: + - results_dir: + - dimsum_results**: + type: directory + description: Directory containing the full suite of DiMSum output files, including fitness estimates and plots + pattern: "dimsum_results*" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/fitness/run_mutscan/environment.yml b/modules/local/fitness/run_mutscan/environment.yml new file mode 100644 index 0000000..a8b1c61 --- /dev/null +++ b/modules/local/fitness/run_mutscan/environment.yml @@ -0,0 +1,7 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - bioconda::bioconductor-mutscan=1.0.0 + - conda-forge::r-base=4.5.1 diff --git a/modules/local/fitness/run_mutscan/main.nf b/modules/local/fitness/run_mutscan/main.nf new file mode 100644 index 0000000..3024b0b --- /dev/null +++ b/modules/local/fitness/run_mutscan/main.nf @@ -0,0 +1,33 @@ +process RUN_MUTSCAN { + tag "${sample.sample}" + label 'process_medium' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(sample), path(counts_merged) + path(syn_wt_txt) + path(exp_design) + + output: + tuple val(sample), path("fitness_estimation_mutscan_edgeR.tsv"), emit: fitness_mutscan_edgeR + tuple val(sample), path("fitness_estimation_mutscan_limma.tsv"), emit: fitness_mutscan_limma + tuple val(sample), path("*.pdf"), emit: qc_plots + path "versions.yml", emit: versions + + script: + template 'fitness_calculation_mutscan.R' + + stub: + """ + touch fitness_estimation_mutscan_edgeR.tsv fitness_estimation_mutscan_limma.tsv mutscan_stub.pdf + cat <<-END_VERSIONS > versions.yml + "${task.process}": + r-base: "0.0.0" + END_VERSIONS + """ +} diff --git a/modules/local/fitness/run_mutscan/meta.yml b/modules/local/fitness/run_mutscan/meta.yml new file mode 100644 index 0000000..f80e038 --- /dev/null +++ b/modules/local/fitness/run_mutscan/meta.yml @@ -0,0 +1,81 @@ +name: "run_mutscan" +description: Calculates fitness and functionality scores using the mutscan R package, employing edgeR and limma-voom statistical frameworks for robust estimation from input and output variant counts. +keywords: + - deep mutational scanning + - dms + - fitness estimation + - mutscan + - edger + - limma +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + - "edger": + description: "Empirical Analysis of Digital Gene Expression Data in R" + homepage: "https://bioconductor.org/packages/release/bioc/html/edgeR.html" + doi: "10.1093/bioinformatics/btp616" + licence: ["GPL-2.0-or-later"] + - "limma": + description: "Linear Models for Microarray Data" + homepage: "https://bioconductor.org/packages/release/bioc/html/limma.html" + doi: "10.1093/nar/gkv007" + licence: ["GPL-2.0-or-later"] + +input: + - - sample: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test' ]` + - counts_merged: + type: file + description: TSV/CSV file containing variant counts merged across samples or replicates + pattern: "*.{tsv,csv}" + - - syn_wt_txt: + type: file + description: Text file identifying the synonymous wild-type mutation used as the reference baseline + pattern: "*.txt" + - - exp_design: + type: file + description: Experimental design file defining replicates, conditions, and groupings for statistical analysis + pattern: "*.{csv,txt}" + +output: + - fitness_mutscan_edgeR: + - sample: + type: map + description: Groovy Map containing sample information + - fitness_estimation_mutscan_edgeR.tsv: + type: file + description: Fitness estimation results generated using the edgeR framework + pattern: "fitness_estimation_mutscan_edgeR.tsv" + - fitness_mutscan_limma: + - sample: + type: map + description: Groovy Map containing sample information + - fitness_estimation_mutscan_limma.tsv: + type: file + description: Fitness estimation results generated using the limma framework + pattern: "fitness_estimation_mutscan_limma.tsv" + - qc_plots: + - sample: + type: map + description: Groovy Map containing sample information + - "*.pdf": + type: file + description: Quality control plots generated by mutscan during fitness estimation + pattern: "*.pdf" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/fitness/run_mutscan/templates/fitness_calculation_mutscan.R b/modules/local/fitness/run_mutscan/templates/fitness_calculation_mutscan.R new file mode 100644 index 0000000..8a196ff --- /dev/null +++ b/modules/local/fitness/run_mutscan/templates/fitness_calculation_mutscan.R @@ -0,0 +1,371 @@ +#!/usr/bin/env Rscript + +## mutscan (Soneson et al., Genome Biology 2023) fitness estimation for nf-core/deepmutscan +## 19.03.2026 + +## 0. Libraries ## +################## + +suppressPackageStartupMessages({ + library(mutscan) + library(Biostrings) +}) + +## --- Helper functions --- + +# calculate nt hamming distances from the specified WT +nbrMutBases <- function(x, wt.seq) { + x <- cbind(x, "nbrMutBases" = rep(NA, nrow(x))) + for (i in 1:nrow(x)){ + tmp.wt <- strsplit(as.character(wt.seq), "")[[1]] + tmp.mut <- strsplit(as.character(x\$sequence[i]), "")[[1]] + if(length(which(tmp.mut != tmp.wt)) == 0){ + x\$nbrMutBases[i] <- 0 + rm(tmp.mut, tmp.wt) + next + }else{ + x\$nbrMutBases[i] <- length(which(tmp.mut != tmp.wt)) + rm(tmp.mut, tmp.wt) + next + } + } + x\$nbrMutBases <- as.character(x\$nbrMutBases) + return(x) +} + +# calculate codon hamming distances from the specified WT +nbrMutCodons <- function(x, wt.seq) { + x <- cbind(x, "nbrMutCodons" = rep(NA, nrow(x))) + for (i in 1:nrow(x)){ + tmp.wt <- strsplit(as.character(wt.seq), "")[[1]] + tmp.mut <- strsplit(as.character(x\$sequence[i]), "")[[1]] + if(length(which(tmp.mut != tmp.wt)) == 0){ + x\$nbrMutCodons[i] <- 0 + rm(tmp.mut, tmp.wt) + next + }else if(length(which(tmp.mut != tmp.wt)) == 1){ + x\$nbrMutCodons[i] <- 1 + rm(tmp.mut, tmp.wt) + }else if(length(which(tmp.mut != tmp.wt)) > 1){ + mut.pos <- which(tmp.mut != tmp.wt) + x\$nbrMutCodons[i] <- length(unique(ceiling(mut.pos / 3))) + rm(tmp.mut, tmp.wt) + next + } + } + x\$nbrMutCodons <- as.character(x\$nbrMutCodons) + return(x) +} + +# translate sequences and add aa_seq +sequenceAA <- function(x) { + x <- cbind(x, "sequenceAA" = as.character(translate(DNAStringSet(x\$sequence)))) + return(x) +} + +# calculate AA hamming distances from the WT +nbrMutAAs <- function(x, wt.seq) { + x <- cbind(x, "nbrMutAAs" = rep(NA, nrow(x))) + for (i in 1:nrow(x)){ + tmp.wt <- strsplit(as.character(translate(wt.seq)), "")[[1]] + tmp.mut <- strsplit(as.character(x\$sequenceAA[i]), "")[[1]] + if(length(which(tmp.mut != tmp.wt)) == 0){ + x\$nbrMutAAs[i] <- 0 + rm(tmp.mut, tmp.wt) + next + }else{ + x\$nbrMutAAs[i] <- length(which(tmp.mut != tmp.wt)) + rm(tmp.mut, tmp.wt) + next + } + } + x\$nbrMutAAs <- as.character(x\$nbrMutAAs) + return(x) +} + +# mutant base labels +mutantNameBase <- function(x, wt.seq){ + x <- cbind(x, "mutantNameBase" = rep(NA, nrow(x))) + for(i in 1:nrow(x)){ + tmp.wt <- strsplit(as.character(wt.seq), "")[[1]] + tmp.mut <- strsplit(as.character(x\$sequence[i]), "")[[1]] + if(length(which(tmp.mut != tmp.wt)) == 0){ + x\$mutantNameBase[i] <- "f.0.WT" + rm(tmp.mut, tmp.wt) + next + }else if(length(which(tmp.mut != tmp.wt)) > 0){ + mut.pos <- which(tmp.mut != tmp.wt) + x\$mutantNameBase[i] <- paste0("f.", mut.pos, ".", tmp.mut[mut.pos], collapse = "_") + rm(tmp.mut, tmp.wt, mut.pos) + next + } + } + return(x) +} + +# mutant codon labels +mutantNameCodon <- function(x, wt.seq){ + x <- cbind(x, "mutantNameCodon" = rep(NA, nrow(x))) + for(i in 1:nrow(x)){ + tmp.wt <- strsplit(as.character(wt.seq), "(?<=.{3})", perl = TRUE)[[1]] + tmp.mut <- strsplit(as.character(x\$sequence[i]), "(?<=.{3})", perl = TRUE)[[1]] + if(length(which(tmp.mut != tmp.wt)) == 0){ + x\$mutantNameCodon[i] <- "f.0.WT" + rm(tmp.mut, tmp.wt) + next + }else if(length(which(tmp.mut != tmp.wt)) > 0){ + mut.pos <- which(tmp.mut != tmp.wt) + x\$mutantNameCodon[i] <- paste0("f.", mut.pos, ".", tmp.mut[mut.pos], collapse = "_") + rm(tmp.mut, tmp.wt, mut.pos) + next + } + } + return(x) +} + +# name the mutations (AA level) +mutantNameAA <- function(x, wt.seq) { + x <- cbind(x, "mutantNameAA" = rep(NA, nrow(x))) + for (i in 1:nrow(x)){ + if(x\$nbrMutAAs[i] == 0){ + x\$mutantNameAA[i] <- "f.0.WT" + }else{ + tmp.wt <- strsplit(as.character(translate(wt.seq)), "")[[1]] + tmp.mut <- strsplit(as.character(x\$sequenceAA[i]), "")[[1]] + tmp.pos <- which(tmp.mut != tmp.wt) + x\$mutantNameAA[i] <- paste0("f.", tmp.pos, ".", tmp.mut[tmp.pos]) + rm(tmp.mut, tmp.wt,tmp.pos) + } + } + return(x) +} + +# categorise the mutation types +mutationTypes <- function(x){ + x <- cbind(x, "mutationTypes" = rep(NA, nrow(x))) + x[grep("WT", x\$mutantNameAA), "mutationTypes"] <- "silent" ## silent/synonymous + x[grep("[*]", x\$mutantNameAA), "mutationTypes"] <- "stop" ## stop + x[-grep("[*]|WT", x\$mutantNameAA), "mutationTypes"] <- "nonsynonymous" ## nonsynonymous + x +} + +# mutant name +mutantName <- function(x, wt.seq){ + x <- cbind("mutantName" = rep(NA, nrow(x)), x) + for (i in 1:nrow(x)){ + if(x\$nbrMutAAs[i] == 0){ + x\$mutantName[i] <- "WT" + }else{ + tmp.wt <- strsplit(as.character(translate(wt.seq)), "")[[1]] + tmp.mut <- strsplit(as.character(x\$sequenceAA[i]), "")[[1]] + tmp.pos <- which(tmp.mut != tmp.wt) + x\$mutantName[i] <- paste0(tmp.wt[tmp.pos], tmp.pos,tmp.mut[tmp.pos]) + rm(tmp.mut, tmp.wt,tmp.pos) + } + } + return(x) +} + +# mutscan 'summaryTable' building (from merged counts TSV file) +mutscan.summaryTable.from.counts <- function(x, sample, wt.seq){ + + ### pre-process + x <- x[,c(1, grep(sample, colnames(x)))] + colnames(x) <- c("sequence", "nbrReads") + x <- cbind(x, "maxNbrReads" = x[,"nbrReads"], "nbrUmis" = x[,"nbrReads"]) + + ### nbrMutBases + x <- nbrMutBases(x, wt.seq) + + ### nbrMutCodons + x <- nbrMutCodons(x, wt.seq) + + ### sequenceAA + x <- sequenceAA(x) + + ### nbrMutAAs + x <- nbrMutAAs(x, wt.seq) + + ### varLengths + x <- cbind(x, "varLengths" = as.character(nchar(x\$sequence))) + + ### mutantNameBase + x <- mutantNameBase(x, wt.seq) + + ### mutantNameCodon + x <- mutantNameCodon(x, wt.seq) + + ### mutantNameBaseHGVS (would ideally look like this: "f:c", "f:c.32_33delinsAC", etc.) + x <- cbind(x, "mutantNameBaseHGVS" = x\$mutantNameCodon) + + ### mutantNameAA + x <- mutantNameAA(x, wt.seq) + + ### mutantNameAAHGVS (would ideally look like this: "f:p" or "f:p.(Leu11His)") + x <- cbind(x, "mutantNameAAHGVS" = x\$mutantNameAA) + + ### mutationTypes: silent, stop, nonsynonymous + x <- mutationTypes(x) + + ### mutantName + x <- mutantName(x, wt.seq) + + ### re-order columns + x <- x[,c(1:7,9:16,8)] + + ### output + return(x) +} + +## --- Main function --- + +#' Run mutscan fitness estimation with configurable I/O paths +#' +#' @param counts_path Path to counts_merged.tsv +#' @param design_path Path to experimentalDesign.tsv +#' @param wt_seq_path Path to synonymous_wt.txt (single line DNA sequence) +#' @param output_path Path to write fitness_estimation.tsv +#' +#' @return Invisibly returns the final data.frame; writes the output to output_path. +run_mutscan_fitness_estimation <- function(counts_path, + design_path, + wt_seq_path, + output_path_edgeR, + output_path_limma){ + + ## 1. Import key files ## + ######################### + + merged.counts <- read.table(counts_path, sep = "\t", header = T, check.names = F) + exp.design <- read.table(design_path, sep = "\t", header = T, check.names = F) + wt.seq <- DNAString(as.character(read.table(wt_seq_path))) + + ## 2. Variant count matrix reformatting ## + ########################################## + + var.tables <- vector(mode = "list", length = nrow(exp.design)) + names(var.tables) <- exp.design[,"sample_name"] + var.tables <- lapply(var.tables, function(x){x <- vector(mode = "list", length = 4); + names(x) <- c("summaryTable", "filterSummary", "errorStatistics", "parameters"); + return(x)}) + + # mutscan 'summaryTable' + for(i in 1:length(var.tables)){ + print(i) + var.tables[[i]]\$summaryTable <- mutscan.summaryTable.from.counts(merged.counts, names(var.tables)[i], wt.seq) + } + + # mutscan 'filterSummary' (fill with minimal decoy) + var.tables <- lapply(var.tables, function(x){x\$filterSummary <- data.frame(NA); return(x)}) + + # mutscan 'errorStatistics' (fill with minimal decoy) + var.tables <- lapply(var.tables, function(x){x\$errorStatistics <- NA; return(x)}) + + # mutscan 'parameters' (fill with minimal decoy) + var.tables <- lapply(var.tables, function(x){x\$parameters\$mutNameDelimiter <- '.'; return(x)}) + + ## 3. summarizeExperiment object ## + ################################### + + # mutscan 'coldata' object (from experimental design TSV file) + condition <- exp.design\$selection_id + condition[which(condition == '0')] <- "input" + condition[which(condition == '1')] <- "output" + coldata <- as.data.frame(cbind("Name" = exp.design\$sample_name, + "Condition" = condition, + "Replicate" = exp.design\$experiment_replicate)) + class(coldata\$Replicate) <- "integer" + + # mutscan 'summarizeExperiment' object + se <- summarizeExperiment(x = var.tables, + coldata = coldata, + countType = "reads") + + ## 4. logFC calculations ## + ########################### + + # mutscan 'model.matrix' object to calculate logFC values + model.design <- model.matrix(~ Replicate + Condition, data = se@colData) + + # edgeR + logFC.edgeR <- calculateRelativeFC(se = se, + design = model.design, + coef = "Conditionoutput", + WTrows = "WT", + selAssay = "counts", + pseudocount = 1, + method = "edgeR") + + # limma + logFC.limma <- calculateRelativeFC(se = se, + design = model.design, + coef = "Conditionoutput", + WTrows = "WT", + selAssay = "counts", + pseudocount = 1, + method = "limma") + + ## 5. QC plots ## + ################# + + # raw counts comparison + pdf('mutscan_counts_corr.pdf', height = 9, width = 14) + print(plotPairs(se, selAssay = "counts", addIdentityLine = TRUE)) + dev.off() + + # edgeR volcano plot + pdf('mutscan_edgeR_volcano.pdf', height = 9, width = 14) + print(plotVolcano(logFC.edgeR, pointSize = "large")) + dev.off() + + # limma volcano plot + pdf('mutscan_limma_volcano.pdf', height = 9, width = 14) + print(plotVolcano(logFC.limma, pointSize = "large")) + dev.off() + + ## 6. Data export ## + #################### + + write.table(logFC.edgeR, output_path_edgeR, + col.names = TRUE, row.names = FALSE, quote = FALSE, sep = "\t", na = "") + write.table(logFC.limma, output_path_limma, + col.names = TRUE, row.names = FALSE, quote = FALSE, sep = "\t", na = "") + + invisible(logFC.edgeR) + invisible(logFC.limma) + +} + +##### +# run function +##### +run_mutscan_fitness_estimation( + counts_path = "$counts_merged", + design_path = "$exp_design", + wt_seq_path = "$syn_wt_txt", + output_path_edgeR = "fitness_estimation_mutscan_edgeR.tsv", + output_path_limma = "fitness_estimation_mutscan_limma.tsv" +) + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +mutscan_version <- as.character(packageVersion("mutscan")) +Biostrings_version <- as.character(packageVersion("Biostrings")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(mutscan_version) == 0) mutscan_version <- "unknown" +if (length(Biostrings_version) == 0) Biostrings_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-mutscan:', mutscan_version), + paste(' r-Biostrings:', Biostrings_version) + ), + f +) +close(f) diff --git a/modules/local/structure/variant_effect_inspection_tool/environment.yml b/modules/local/structure/variant_effect_inspection_tool/environment.yml new file mode 100644 index 0000000..a72f7ec --- /dev/null +++ b/modules/local/structure/variant_effect_inspection_tool/environment.yml @@ -0,0 +1,7 @@ +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::python=3.12.3 + - conda-forge::pandas=2.2.1 + - conda-forge::numpy=1.26.4 diff --git a/modules/local/structure/variant_effect_inspection_tool/main.nf b/modules/local/structure/variant_effect_inspection_tool/main.nf new file mode 100644 index 0000000..8935225 --- /dev/null +++ b/modules/local/structure/variant_effect_inspection_tool/main.nf @@ -0,0 +1,42 @@ +// Build the single, self-contained interactive variant effect inspection tool (HTML). +// Aggregates per-residue metrics (fitness, pLDDT accuracy, coverage, counts, counts/cov, error bias), +// aligns the wildtype sequence onto the structure, and inlines 3Dmol.js + PDB + data into +// one portable HTML file (plus a structure_data.json sidecar). +process VARIANT_EFFECT_INSPECTION_TOOL { + tag "${meta.sample ?: meta.id}" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container + ? 'https://depot.galaxyproject.org/singularity/pandas:2.2.1' + : 'quay.io/biocontainers/pandas:2.2.1' }" + + input: + tuple val(meta), path(pdb), path(fitness_tsv) + path(variant_counts, stageAs: 'vc*/*') // per-sample files share a basename -> stage in numbered dirs + path aa_seq + path threedmol_js + path template_html + + output: + tuple val(meta), path("variant_effect_inspection_tool.html"), emit: viewer + tuple val(meta), path("structure_data.json") , emit: data + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'build_variant_effect_inspection_tool.py' + + stub: + """ + touch variant_effect_inspection_tool.html structure_data.json + cat <<-END_VERSIONS > versions.yml + "${task.process}": + python: "0.0.0" + pandas: "0.0.0" + END_VERSIONS + """ +} diff --git a/modules/local/structure/variant_effect_inspection_tool/meta.yml b/modules/local/structure/variant_effect_inspection_tool/meta.yml new file mode 100644 index 0000000..5582e00 --- /dev/null +++ b/modules/local/structure/variant_effect_inspection_tool/meta.yml @@ -0,0 +1,75 @@ +name: "variant_effect_inspection_tool" +description: Build a single, self-contained interactive variant effect inspection tool (HTML) that projects per-residue fitness, counts, coverage and error-correction bias onto the wildtype 3D structure using 3Dmol.js. +keywords: + - deep mutational scanning + - variant effect + - 3D structure + - visualisation +tools: + - "3Dmol.js": + description: "Object-oriented, WebGL-based JavaScript library for molecular visualisation." + homepage: "https://3dmol.csb.pitt.edu/" + documentation: "https://3dmol.csb.pitt.edu/doc/" + licence: ["BSD-3-Clause"] + - "pandas": + description: "Fast, powerful data structures for data analysis in Python." + homepage: "https://pandas.pydata.org/" + licence: ["BSD-3-Clause"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', sample:'ORF1' ]` + - pdb: + type: file + description: Wildtype 3D structure (PDB); per-residue confidence read from the B-factor column. + pattern: "*.pdb" + - fitness_tsv: + type: file + description: Per-sample default fitness estimation table. + pattern: "*.tsv" + - - variant_counts: + type: file + description: Per-sample (error-corrected) library-filtered variant count tables. + pattern: "*.csv" + - - aa_seq: + type: file + description: Reference amino-acid sequence (one line). + - - threedmol_js: + type: file + description: Bundled 3Dmol.js library (inlined into the HTML). + pattern: "*.js" + - - template_html: + type: file + description: HTML template into which the structure, data and 3Dmol.js are injected. + pattern: "*.html" + +output: + - viewer: + - meta: + type: map + description: Groovy Map containing sample information + - variant_effect_inspection_tool.html: + type: file + description: Self-contained interactive variant effect inspection tool. + pattern: "variant_effect_inspection_tool.html" + - data: + - meta: + type: map + description: Groovy Map containing sample information + - structure_data.json: + type: file + description: Sidecar with the per-residue/per-variant data projected onto the structure. + pattern: "structure_data.json" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" +maintainers: + - "@BenjaminWehnert1008" diff --git a/modules/local/structure/variant_effect_inspection_tool/templates/build_variant_effect_inspection_tool.py b/modules/local/structure/variant_effect_inspection_tool/templates/build_variant_effect_inspection_tool.py new file mode 100755 index 0000000..227a56e --- /dev/null +++ b/modules/local/structure/variant_effect_inspection_tool/templates/build_variant_effect_inspection_tool.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +""" +Build a single self-contained, interactive variant effect inspection tool (HTML) for nf-core/deepmutscan. + +Nextflow ``template`` script for the VARIANT_EFFECT_INSPECTION_TOOL module (staged from this module's +``templates/`` folder). Reads the wildtype structure (PDB), the fitness estimation table and the +per-sample variant-count tables, aggregates per-residue/per-variant metrics, and writes one portable +HTML file with 3Dmol.js, the PDB coordinates and all data inlined (no external/CDN/sidecar +dependency), plus a structure_data.json sidecar and versions.yml. + +Note: this is a Nextflow template, so input paths / the sample name are filled in as Nextflow +variables and every backslash meant for Python is written doubled (as in the pipeline's R templates). +""" +import json +import platform +import re +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +THREE_TO_ONE = { + "ALA": "A", "ARG": "R", "ASN": "N", "ASP": "D", "CYS": "C", "GLN": "Q", "GLU": "E", + "GLY": "G", "HIS": "H", "ILE": "I", "LEU": "L", "LYS": "K", "MET": "M", "PHE": "F", + "PRO": "P", "SER": "S", "THR": "T", "TRP": "W", "TYR": "Y", "VAL": "V", +} +AA_ORDER = list("ACDEFGHIKLMNPQRSTVWY") +MUT_RE = re.compile(r"^([A-Z])(\\d+)([A-Z*])\$") + + +def parse_pdb(pdb_path): + """Return (pdb_text, residues) where residues = [{resnum, aa, plddt}] from CA atoms.""" + text = Path(pdb_path).read_text() + residues = [] + seen = set() + for line in text.splitlines(): + if not line.startswith("ATOM"): + continue + atom_name = line[12:16].strip() + if atom_name != "CA": + continue + try: + resnum = int(line[22:26]) + bfac = float(line[60:66]) + except ValueError: + continue + if resnum in seen: + continue + seen.add(resnum) + resname = line[17:20].strip().upper() + residues.append({"resnum": resnum, "aa": THREE_TO_ONE.get(resname, "X"), "plddt": bfac}) + residues.sort(key=lambda r: r["resnum"]) + return text, residues + + +def best_alignment(wt_seq, struct_residues): + """Find offset so that wt position i (1-based) maps to struct_residues[off + i - 1]. + + Scans all offsets, maximising identity. Returns (offset0, identity_fraction) where the + structure index for wt index j (0-based) is off0 + j. + """ + struct_seq = "".join(r["aa"] for r in struct_residues) + n, m = len(wt_seq), len(struct_seq) + best_off, best_matches = 0, -1 + for off in range(-n + 1, m): + matches = 0 + for j in range(n): + k = off + j + if 0 <= k < m and struct_seq[k] == wt_seq[j]: + matches += 1 + if matches > best_matches: + best_matches, best_off = matches, off + identity = best_matches / max(1, n) + return best_off, identity + + +def r6(x): + """Round to 6 dp so output is byte-stable regardless of floating-point summation order.""" + return round(x, 6) if isinstance(x, (int, float)) else x + + +def to_float(x): + try: + f = float(x) + return f if f == f else None # drop NaN + except (TypeError, ValueError): + return None + + +REP_RE = re.compile(r"^rescaled_fitness_rep(\\d+)\$", re.I) +OUT_RE = re.compile(r"^output(\\d+)\$", re.I) + + +def load_fitness(path): + """Per-position variant records from the default fitness_estimation.tsv. + + Captures the mean fitness, its SD and every per-replicate (rescaled) fitness, so the viewer can + recompute the mean/SD over a user-chosen subset of replicates. That subsetting is exact rather + than approximate: the fitness step derives each `rescaled_fitness_rep` from that replicate's + own input/output counts and its own synonymous/stop anchors, so a replicate's value never + depends on which other replicates were in the run. + + Returns (per_pos, rep_ids). Any number of replicates is handled; missing values are tolerated. + """ + df = pd.read_csv(path, sep="\\t", dtype=str, keep_default_na=False) + + def col(name): + return next((c for c in df.columns if c.strip().lower() == name), None) + + def numbered(rx): + return sorted((c for c in df.columns if rx.match(c.strip())), + key=lambda c: int(rx.match(c.strip()).group(1))) + + fit_col, sd_col = col("mean fitness"), col("fitness sd") + rep_cols = numbered(REP_RE) + out_cols = numbered(OUT_RE) + rep_ids = [REP_RE.match(c.strip()).group(1) for c in rep_cols] + + per_pos = {} + for _, row in df.iterrows(): + pos = to_float(row.get("pos", "")) + wt = (row.get("wt_aa", "") or "").strip() + mut = (row.get("mut_aa", "") or "").strip() + if pos is None or not wt or not mut: + continue + pos = int(pos) + is_stop = mut == "*" or (row.get("stop", "").strip() not in ("", "0")) + per_pos.setdefault(pos, {"wt": wt, "variants": []})["variants"].append({ + "mut": mut, + "fitness": to_float(row.get(fit_col, "")) if fit_col else None, + "sd": to_float(row.get(sd_col, "")) if sd_col else None, + "reps": [to_float(row.get(c, "")) for c in rep_cols], + "n_out": sum((to_float(row.get(c, "")) or 0) for c in out_cols), + "syn": mut == wt, "stop": bool(is_stop), + }) + return per_pos, rep_ids + + +def load_counts(paths): + """Aggregate coverage / counts / counts_per_cov across all sample CSVs. + + Returns two dicts: per position ``{pos: {...}}`` (used to colour the structure) and per variant + ``{(pos, mut): {...}}`` (used for the substitution-specific bee-swarm distributions). + """ + frames = [] + for p in paths: + try: + frames.append(pd.read_csv(p)) + except Exception as exc: # pragma: no cover - defensive + print(f"WARN: could not read {p}: {exc}", file=sys.stderr) + if not frames: + return {}, {} + df = pd.concat(frames, ignore_index=True) + # The "A1M"-style WT/pos/mut label lives in the `pos_mut` column + # (`aa_mut` holds e.g. "M:A>M"). Fall back to aa_mut only if pos_mut is absent. + label_col = "pos_mut" if "pos_mut" in df.columns else ("aa_mut" if "aa_mut" in df.columns else None) + if label_col is None: + return {}, {} + cols = (("coverage", "cov"), ("counts", "counts"), ("counts_per_cov", "counts_per_cov"), + ("counts_raw", "counts_raw")) + pos_acc, var_acc = {}, {} + for _, row in df.iterrows(): + m = MUT_RE.match(str(row.get(label_col, "")).strip()) + if not m: + continue + pos, mut = int(m.group(2)), m.group(3) + pa = pos_acc.setdefault(pos, {k: [] for k, _ in cols}) + va = var_acc.setdefault((pos, mut), {k: [] for k, _ in cols}) + for key, src in cols: + v = to_float(row.get(src, "")) + if v is not None: + pa[key].append(v) + va[key].append(v) + + def meaned(acc): + return {k: {kk: (sum(vv) / len(vv) if vv else None) for kk, vv in a.items()} + for k, a in acc.items()} + + return meaned(pos_acc), meaned(var_acc) + + +class args: + # Filled in by Nextflow when this template is staged and run by the VARIANT_EFFECT_INSPECTION_TOOL process. + pdb = "$pdb" + fitness = "$fitness_tsv" + aa_seq = "$aa_seq" + counts = "$variant_counts".split() # collected per-sample CSVs (staged as vc*/*) + threedmol = "$threedmol_js" + template = "$template_html" + sample = "${meta.sample ?: meta.id}" + out_html = "variant_effect_inspection_tool.html" + out_json = "structure_data.json" + + +def main(): + + wt_seq = Path(args.aa_seq).read_text().strip().replace("\\n", "").replace("\\r", "") + pdb_text, struct_residues = parse_pdb(args.pdb) + off0, identity = best_alignment(wt_seq, struct_residues) + struct_by_index = struct_residues # 0-based list + + fit_per_pos, rep_ids = load_fitness(args.fitness) + # per-position aggregate (structure colour) and per-variant values (substitution-specific swarm) + counts_per_pos, counts_per_var = load_counts(sorted(args.counts)) # sorted -> deterministic + + # ---- candidate metrics (dropped automatically when a dataset has no data for them) ---- + # scope="variant": has a per-substitution value (used in the bee-swarm); the position mean is + # still what colours the structure. reverse=True flips the colour ramp (low -> red, high -> blue). + METRIC_SPECS = [ + ("fitness", dict(label="Fitness (nf-core/deepmutscan)", type="diverging", scope="variant", + reverse=True, + desc="Fitness per substitution; the position mean is shown on the structure")), + ("fitness_sd", dict(label="Fitness uncertainty (SD)", type="sequential", scope="variant", + desc="Standard deviation of the fitness estimate (lower = more confident)")), + ("accuracy", dict(label="Prediction accuracy (pLDDT)", type="plddt", scope="residue", fixed=[0, 100], + desc="Per-residue model confidence from the structure B-factor")), + ("coverage", dict(label="Coverage", type="sequential", scope="variant", + desc="Sequencing coverage of this residue (the same for all substitutions at one residue)")), + ("counts", dict(label="Counts", type="sequential", scope="variant", + desc="Variant read counts per substitution (position mean shown on the structure)")), + ("counts_per_cov", dict(label="Counts / coverage", type="sequential", scope="variant", + desc="Counts normalised by coverage, per substitution (position mean shown on the structure)")), + ("n_substitutions", dict(label="Measured substitutions", type="sequential", scope="residue", + desc="Number of substitutions with a measured fitness at the position")), + ("error_bias", dict(label="Positional error bias", type="sequential", scope="residue", + desc="Fraction of counts removed by sequencing-error correction at this position")), + ] + variant_metrics = {k for k, s in METRIC_SPECS if s["scope"] == "variant"} + + def mean(xs): + return (sum(xs) / len(xs)) if xs else None + + residues = [] + for i, wt_aa in enumerate(wt_seq): # i is 0-based; position = i + 1 + pos = i + 1 + k = off0 + i + srec = struct_by_index[k] if 0 <= k < len(struct_by_index) else None + variants = fit_per_pos.get(pos, {}).get("variants", []) + miss = [v for v in variants if not v["syn"] and not v["stop"]] + fit_vals = [v["fitness"] for v in miss if v["fitness"] is not None] + sd_vals = [v["sd"] for v in miss if v["sd"] is not None] + cp = counts_per_pos.get(pos, {}) + craw, cnow = cp.get("counts_raw"), cp.get("counts") + error_bias = (1.0 - cnow / craw) if (craw not in (None, 0) and cnow is not None) else None + res_metrics = { + "fitness": r6(mean(fit_vals)), + "fitness_sd": r6(mean(sd_vals)), + "accuracy": r6(srec["plddt"]) if srec else None, + "coverage": r6(cp.get("coverage")), + "counts": r6(cp.get("counts")), + "counts_per_cov": r6(cp.get("counts_per_cov")), + "n_substitutions": (len(fit_vals) or None), + "error_bias": r6(error_bias), + } + vlist = [] + for v in sorted(variants, key=lambda v: AA_ORDER.index(v["mut"]) if v["mut"] in AA_ORDER else 99): + vc = counts_per_var.get((pos, v["mut"]), {}) + vlist.append({ + "mut": v["mut"], "syn": v["syn"], "stop": v["stop"], "n_out": r6(v["n_out"]), + # per-replicate fitness, so the viewer can re-derive mean/SD for a chosen subset + "reps": [r6(x) if x is not None else None for x in v["reps"]], + "metrics": { + "fitness": r6(v["fitness"]), "fitness_sd": r6(v["sd"]), + "coverage": r6(vc.get("coverage")), "counts": r6(vc.get("counts")), + "counts_per_cov": r6(vc.get("counts_per_cov")), + }, + }) + residues.append({ + "pos": pos, "wt": wt_aa, + "resnum": srec["resnum"] if srec else None, + "mapped": bool(srec and srec["aa"] == wt_aa), + "metrics": res_metrics, "variants": vlist, + }) + + # keep only metrics that actually have data (generality across diverse datasets) + metrics = {} + for key, spec in METRIC_SPECS: + vals = [r["metrics"].get(key) for r in residues if r["metrics"].get(key) is not None] + if not vals: + continue + if "fixed" in spec: + mrange = spec["fixed"] + elif spec["type"] == "diverging": + a = max(abs(min(vals)), abs(max(vals))) or 1 + mrange = [r6(-a), r6(a)] + else: + lo, hi = min(vals), max(vals) + mrange = [r6(lo), r6(hi if hi != lo else lo + 1)] + metrics[key] = {"label": spec["label"], "type": spec["type"], "scope": spec["scope"], + "range": mrange, "desc": spec["desc"]} + if spec.get("reverse"): + metrics[key]["reverse"] = True + + present = set(metrics) + for r in residues: + r["metrics"] = {k: v for k, v in r["metrics"].items() if k in present} + for v in r["variants"]: + v["metrics"] = {k: val for k, val in v["metrics"].items() + if k in present and k in variant_metrics} + + data = { + "sample": args.sample, + "sequence": wt_seq, + "alignment": {"offset": off0, "identity": round(identity, 4), + "n_residues_structure": len(struct_residues)}, + "replicates": rep_ids, + "metrics": metrics, + "residues": residues, + } + + Path(args.out_json).write_text(json.dumps(data, indent=2)) + + threedmol_js = Path(args.threedmol).read_text() + template = Path(args.template).read_text() + html = (template + .replace("/*__THREEDMOL_JS__*/", threedmol_js) + .replace('"__PDB_TEXT__"', json.dumps(pdb_text)) + .replace("/*__DATA_JSON__*/", json.dumps(data)) + .replace("__SAMPLE__", args.sample)) + Path(args.out_html).write_text(html) + + Path("versions.yml").write_text( + '"${task.process}":\\n' + + " python: " + platform.python_version() + "\\n" + + " pandas: " + pd.__version__ + "\\n" + + " numpy: " + np.__version__ + "\\n" + ) + print(f"Wrote {args.out_html} (alignment offset={off0}, identity={identity:.2%}, " + f"{len(residues)} residues)") + + +if __name__ == "__main__": + main() diff --git a/modules/local/variantcounting/environment.yml b/modules/local/variantcounting/environment.yml new file mode 100644 index 0000000..9397a3f --- /dev/null +++ b/modules/local/variantcounting/environment.yml @@ -0,0 +1,14 @@ +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::python=3.12.13 + - bioconda::pysam=0.24.0 + - conda-forge::pyarrow=24.0.0 + - conda-forge::numpy=2.5.1 + - conda-forge::biopython=1.87 + # polars-lts-cpu (no AVX2 requirement) so the amd64 image also runs under + # emulation on Apple Silicon; the mutscan container is amd64-only. + - pip + - pip: + - polars-lts-cpu==1.33.1 diff --git a/modules/local/variantcounting/main.nf b/modules/local/variantcounting/main.nf new file mode 100644 index 0000000..5ee7eec --- /dev/null +++ b/modules/local/variantcounting/main.nf @@ -0,0 +1,40 @@ +process VARIANTCOUNTING { + tag "$meta.id" + label 'process_high' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/a6/a66af652f7b1c6d9dab9f081f5676bd9452d653eed43034f20a2cf172921a4cf/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_pysam_biopython_pruned:fb9a2095922ddd59' }" + + input: + tuple val(meta), path(bam), path(bai) + path fasta + val reading_frame + val min_counts + val base_qual + val min_flank + + output: + tuple val(meta), path("*.variant_counts.tsv"), emit: variant_counts + path "variant_counts_columns.tsv" , emit: columns + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'count_variants.py' + + stub: + """ + touch ${meta.id}.variant_counts.tsv + touch variant_counts_columns.tsv + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + variantcounting: stub + END_VERSIONS + """ +} diff --git a/modules/local/variantcounting/meta.yml b/modules/local/variantcounting/meta.yml new file mode 100644 index 0000000..5669b02 --- /dev/null +++ b/modules/local/variantcounting/meta.yml @@ -0,0 +1,74 @@ +name: "variantcounting" +description: Count single- and multi-nucleotide variants per read against a reference ORF, producing a GATK-AnalyzeSaturationMutagenesis-compatible variant count table. +keywords: + - deep mutational scanning + - variant calling + - saturation mutagenesis + - counting +tools: + - pysam: + description: Python interface for reading/manipulating SAM/BAM alignment files (htslib). + homepage: https://pysam.readthedocs.io + licence: ["MIT"] + - polars: + description: Fast DataFrame library. + homepage: https://pola.rs + licence: ["MIT"] + - biopython: + description: Tools for biological computation (sequence translation). + homepage: https://biopython.org + licence: ["BSD-3-Clause"] + +input: + - - meta: + type: map + description: Groovy Map containing sample information, e.g. `[ id:'sample1' ]` + - bam: + type: file + description: Coordinate-sorted, indexed BAM of premerged reads aligned to the reference. + pattern: "*.bam" + - bai: + type: file + description: BAM index (staged alongside the BAM). + pattern: "*.{bai,csi}" + - - fasta: + type: file + description: Reference FASTA containing the ORF. + pattern: "*.{fa,fasta}" + - - reading_frame: + type: string + description: ORF range as 1-based inclusive `start-stop` (GATK `--orf` convention), e.g. `352-1383`. + - - min_counts: + type: integer + description: Minimum number of observations for a variant to be reported (GATK `--min-variant-obs`). + - - base_qual: + type: integer + description: Minimum base quality for a mutation to be counted. + - - min_flank: + type: integer + description: Minimum distance (nt) of a mutation/covered base from a read edge; 0 disables edge trimming. + +output: + - variant_counts: + - meta: + type: map + description: Groovy Map containing sample information + - "*.variant_counts.tsv": + type: file + description: Tab-separated variant count table (no header), GATK-compatible 9 columns. + pattern: "*.variant_counts.tsv" + - columns: + - variant_counts_columns.tsv: + type: file + description: Concise explanation of each output column. + pattern: "variant_counts_columns.tsv" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" +maintainers: + - "@BenjaminWehnert1008" diff --git a/modules/local/variantcounting/templates/count_variants.py b/modules/local/variantcounting/templates/count_variants.py new file mode 100644 index 0000000..73e460b --- /dev/null +++ b/modules/local/variantcounting/templates/count_variants.py @@ -0,0 +1,522 @@ +#!/usr/bin/env python + +# Nextflow template: values below are interpolated by Nextflow. The whole file is compiled +# as one Groovy GString, so every backslash must be doubled and no triple-quoted strings +# are allowed anywhere in this file, including comments. + +#-- import modules --# +import os +import sys +import re +import gc +import platform +import pysam +import numpy as np +import pyarrow as pa +import polars as pl +import Bio +from Bio import SeqIO +from Bio.Seq import Seq +from datetime import datetime +from concurrent.futures import ProcessPoolExecutor, as_completed +from collections import defaultdict + +#-- functions --# +def init_bam(bam_path): + global bam_file_cov + bam_file_cov = pysam.AlignmentFile(bam_path, "rb") + +def get_base_cov(chrom, start, end, qual, min_flank): + if min_flank and min_flank > 0: + cov = defaultdict(int) + for read in bam_file_cov.fetch(chrom, start, end): + if read.is_unmapped: + continue + quals = read.query_qualities + qlen = read.query_length + if quals is None or qlen is None: + continue + lo = min_flank + hi = qlen - min_flank + for qpos, refpos in read.get_aligned_pairs(matches_only=True): + if refpos is None or qpos is None: + continue + if refpos < start or refpos >= end: + continue + if qpos < lo or qpos >= hi: + continue + if quals[qpos] < qual: + continue + cov[refpos] += 1 + return dict(cov) + + A, C, G, T = bam_file_cov.count_coverage(chrom, start, end, quality_threshold = qual) + coverage_array = np.array(A, dtype=np.uint32) + \\ + np.array(C, dtype=np.uint32) + \\ + np.array(G, dtype=np.uint32) + \\ + np.array(T, dtype=np.uint32) + + # key on 0-based ref position to match ref_pos lookups in gatk_formating + return {start + pos: int(cov) for pos, cov in enumerate(coverage_array)} + +def chunk_ranges(start, end, sub_region_size): + for s in range(start, end, sub_region_size): + yield s, min(s + sub_region_size, end) + +def get_base_cov_in_chunk(bam_path, chrom, start, end, qual, sub_region_size, threads, min_flank): + chunks = list(chunk_ranges(start, end, sub_region_size)) + dicts = [] + + with ProcessPoolExecutor(max_workers = threads, initializer = init_bam, initargs = (bam_path,)) as executor: + futures = [ + executor.submit(get_base_cov, chrom, s, e, qual, min_flank) + for s, e in chunks + ] + + for f in as_completed(futures): + dicts.append(f.result()) + + merged_dict = {} + for d in dicts: + merged_dict.update(d) + + return merged_dict + +def init_worker(): + global global_dict_base_cov + global_dict_base_cov = dict_base_cov + +def extract_read_info(read): + read_seq = read.query_sequence + read_qual = read.query_qualities + + return { + 'ref': read.reference_name, + 'pos': read.reference_start, + 'seq': read_seq, + 'qual': read_qual, + 'md': read.get_tag('MD'), + 'cigar': read.cigartuples + } + +def parse_md(read): + read_pos = read.get('pos') + read_md = read.get('md') + md_pattern = re.finditer(r'(\\d+)|([A-Z]+)|(\\^[A-Z]+)', read_md) + + base_ref_pos = read_pos + + variants = [] + for match in md_pattern: + if match.group(1): + num_matches = int(match.group(1)) + base_ref_pos += num_matches + elif match.group(2): + for base in match.group(2): + variants.append(('X', base_ref_pos, base)) + base_ref_pos += 1 + elif match.group(3): + deleted_bases = match.group(3)[1:] + for base in deleted_bases: + variants.append(('D', base_ref_pos, base)) + base_ref_pos += 1 + + return variants + +def gatk_formating(variants): + if not variants: + return 0, 0, "", 0, "", "", "" + + varying_bases = 0 + varying_codons = 0 + base_mut = "" + codon_mut = "" + aa_mut = "" + pos_mut = "" + + base_pos = [ v['ref_pos'] for v in variants if v['var_type'] == 'X' ] + base_cov = [ global_dict_base_cov.get(pos, 0) for pos in base_pos ] + base_cov_avg = int(np.mean(base_cov)) if base_cov else 0 + + base_variants = [ v for v in variants if v['var_type'] == 'X' ] + varying_bases = len(base_variants) + + # base_mut positions are 1-based to match GATK AnalyzeSaturationMutagenesis + base_mut = ", ".join( + f"{v['ref_pos'] + 1}:{v['ref_base']}>{v['alt_base']}" + for v in base_variants + ) + + codon_changes = defaultdict(list) + for v in base_variants: + codon_idx = ((v['ref_pos'] - orf_start) // 3) + 1 + codon_changes[codon_idx].append(v) + + varying_codons = len(codon_changes) + + codon_mut_list = [] + aa_mut_list = [] + pos_mut_list = [] + + for codon_idx in sorted(codon_changes): + vars_in_codon = codon_changes[codon_idx] + ref_codon = str(codon_dict[chrom][codon_idx]) + codon_list = list(ref_codon) + + for v in vars_in_codon: + pos_in_codon = (v['ref_pos'] - orf_start) % 3 + codon_list[pos_in_codon] = v['alt_base'] + + alt_codon = "".join(codon_list) + codon_mut_list.append(f"{codon_idx}:{ref_codon}>{alt_codon}") + + ref_aa = str(Seq(ref_codon).translate()) + alt_aa = str(Seq(alt_codon).translate()) + + if alt_aa == ref_aa: + mut_type = "S" + elif alt_aa == "*": + mut_type = "N" + else: + mut_type = "M" + + # GATK writes the stop codon as X, not * + ref_aa_s = "X" if ref_aa == "*" else ref_aa + alt_aa_s = "X" if alt_aa == "*" else alt_aa + + aa_mut_list.append(f"{mut_type}:{ref_aa_s}>{alt_aa_s}") + pos_mut_list.append(f"{ref_aa_s}{codon_idx}{alt_aa_s}") + + codon_mut = ", ".join(codon_mut_list) + aa_mut = ", ".join(aa_mut_list) + pos_mut = ";".join(pos_mut_list) + + return base_cov_avg, varying_bases, base_mut, varying_codons, codon_mut, aa_mut, pos_mut + +def parse_read(read, orf_start, orf_end, base_qual, min_flank): + read_ref = read.get('ref') + read_pos = read.get('pos') + read_seq = read.get('seq') + read_qual = read.get('qual') + read_cigar = read.get('cigar') + read_md_variants = parse_md(read) + + if not read_md_variants: + return [] + + qlen = len(read_seq) + base_ref_pos = read_pos + base_read_pos = 0 + md_index = 0 + variants = [] + for op, op_len in read_cigar: + if op == 0: # M, match + for i in range(op_len): + if md_index < len(read_md_variants) and read_md_variants[md_index][1] == base_ref_pos: + var_type, _, ref_base = read_md_variants[md_index] + alt_base = read_seq[base_read_pos] + alt_qual = read_qual[base_read_pos] + alt_idx = (base_ref_pos - orf_start) % 3 + 1 + codon_idx = ((base_ref_pos - orf_start) // 3) + 1 + variants.append({ 'var_type': var_type, + 'ref_name': read_ref, + 'ref_pos': base_ref_pos, + 'ref_base': ref_base, + 'alt_base': alt_base, + 'alt_qual': alt_qual, + 'alt_idx' : alt_idx, + 'read_idx': base_read_pos, + 'codon_idx': codon_idx }) + md_index += 1 + base_ref_pos += 1 + base_read_pos += 1 + elif op == 1: # I, insertion (indel-containing reads are dropped upstream in bam_filter) + base_read_pos += op_len + elif op == 2: # D, deletion + for i in range(op_len): + if md_index < len(read_md_variants) and read_md_variants[md_index][1] == base_ref_pos: + var_type, _, ref_base = read_md_variants[md_index] + alt_idx = (base_ref_pos - orf_start) % 3 + 1 + codon_idx = ((base_ref_pos - orf_start) // 3) + 1 + variants.append({ 'var_type': 'D', + 'ref_name': read_ref, + 'ref_pos': base_ref_pos, + 'ref_base': ref_base, + 'alt_base': '-', + 'alt_qual': 99, + 'alt_idx' : alt_idx, + 'read_idx': base_read_pos, + 'codon_idx': codon_idx }) + md_index += 1 + base_ref_pos += 1 + elif op == 3: # N, skip + base_ref_pos += op_len + elif op == 4: # S, softclip + base_read_pos += op_len + elif op == 5: # H, hardclip + continue + + if not variants: + return [] + + variants_filtered = [] + for var in variants: + if var['ref_pos'] >= orf_start and var['ref_pos'] <= orf_end: + if var['alt_qual'] >= base_qual: + # read-edge (min-flanking-length) filter + if min_flank > 0: + read_idx = var['read_idx'] + if read_idx < min_flank or read_idx >= qlen - min_flank: + continue + codons_tmp = codon_dict[var['ref_name']] + ref_codon = codons_tmp[var['codon_idx']] + variants_filtered.append({ 'var_type': var['var_type'], + 'ref_name': var['ref_name'], + 'ref_pos': var['ref_pos'], + 'ref_base': var['ref_base'], + 'alt_base': var['alt_base'], + 'alt_qual': var['alt_qual'], + 'alt_idx' : var['alt_idx'], + 'codon_idx': var['codon_idx'], + 'ref_codon': ref_codon }) + + return gatk_formating(variants_filtered) + +def batch_parse_reads(batch_reads, orf_start, orf_end, base_qual, min_flank): + cols = { + "base_cov_avg": [], + "varying_bases": [], + "base_mut": [], + "varying_codons": [], + "codon_mut": [], + "aa_mut": [], + "pos_mut": [] + } + + for read in batch_reads: + result = parse_read(read, orf_start, orf_end, base_qual, min_flank) + + if not result or result[2] == "": + continue + + cols["base_cov_avg"].append(result[0]) + cols["varying_bases"].append(result[1]) + cols["base_mut"].append(result[2]) + cols["varying_codons"].append(result[3]) + cols["codon_mut"].append(result[4]) + cols["aa_mut"].append(result[5]) + cols["pos_mut"].append(result[6]) + + if not cols["base_mut"]: + return None + + return pa.table(cols) + +def function_for_processpool(args_tuple): + return batch_parse_reads(*args_tuple) + +def empty_yield_df(): + return pl.DataFrame([], schema={ + "base_cov_avg": pl.Int64, + "varying_bases": pl.Int64, + "base_mut": pl.Utf8, + "varying_codons": pl.Int64, + "codon_mut": pl.Utf8, + "aa_mut": pl.Utf8, + "pos_mut": pl.Utf8, + "counts": pl.Int64 + }, orient = "row") + +def build_yield_df(results): + if results: + df_yield = pl.from_arrow(pa.concat_tables(results)) + df_yield = df_yield.filter(pl.col("base_mut") != "") + df_yield = df_yield.with_columns(pl.lit(1).alias("counts")) + return df_yield + return empty_yield_df() + +def read_bam_in_chunk(bam_path, orf_start, orf_end, base_qual, min_flank, chunk_size, threads): + with pysam.AlignmentFile(bam_path, "rb", threads = threads) as bam_file, \\ + ProcessPoolExecutor(max_workers = threads, initializer = init_worker) as executor: + + batch_size = min(chunk_size, 5000) + + read_chunk = [] + for read in bam_file.fetch(until_eof=True): + if read.is_unmapped or not read.has_tag('MD'): + continue + + # perfectly-aligned (wildtype) reads carry no variants + if re.fullmatch(r'\\d+', read.get_tag('MD')): + continue + + read_chunk.append(extract_read_info(read)) + + if len(read_chunk) >= chunk_size: + read_batches = [ + read_chunk[i:i + batch_size] + for i in range(0, len(read_chunk), batch_size) + ] + + futures = [ + executor.submit(function_for_processpool, (batch, orf_start, orf_end, base_qual, min_flank)) + for batch in read_batches + ] + + results = [] + for f in as_completed(futures): + batch_result = f.result() + if batch_result: + results.append(batch_result) + + df_yield = build_yield_df(results) + read_chunk = [] + del read_batches, futures, results + gc.collect() + + yield df_yield + + if read_chunk: + read_batches = [ + read_chunk[i:i + batch_size] + for i in range(0, len(read_chunk), batch_size) + ] + + futures = [ + executor.submit(function_for_processpool, (batch, orf_start, orf_end, base_qual, min_flank)) + for batch in read_batches + ] + + results = [] + for f in as_completed(futures): + batch_result = f.result() + if batch_result: + results.append(batch_result) + + df_yield = build_yield_df(results) + del read_chunk, read_batches, futures, results + gc.collect() + + yield df_yield + +def write_column_sheet(path): + rows = [ + ("column", "description"), + ("counts", "Number of reads supporting this variant (observations)"), + ("cov", "Mean sequencing coverage across the variant's mutated base positions"), + ("mean_length_variant_reads", "Placeholder (0); mean read length of supporting reads is not computed"), + ("varying_bases", "Number of nucleotide substitutions in the variant"), + ("base_mut", "Nucleotide change(s), 1-based reference position(s), e.g. 352:G>A"), + ("varying_codons", "Number of codons altered by the variant"), + ("codon_mut", "Codon change(s), 1-based codon index, e.g. 1:GCT>ACT"), + ("aa_mut", "Amino-acid change per codon; type S=synonymous, M=missense, N=nonsense; stop=X, e.g. M:A>T"), + ("pos_mut", "Compact per-codon amino-acid change, e.g. A1T (stop written as X)"), + ] + with open(path, "w") as fh: + for name, desc in rows: + fh.write(name + "\\t" + desc + "\\n") + +#-- Nextflow-interpolated arguments --# +class args: + input_bam = "$bam" + reference = "$fasta" + orf_range = "$reading_frame" # 1-based inclusive (GATK --orf convention) + min_counts = int("$min_counts") + base_qual = int("$base_qual") + min_flank = int("$min_flank") + threads = max(1, int("$task.cpus")) + chunk_size = 1000 + out_file = "${meta.id}.variant_counts.tsv" + +#-- main execution --# +if __name__ == "__main__": + write_column_sheet("variant_counts_columns.tsv") + + # orf_range is 1-based inclusive (GATK --orf convention); convert to 0-based + orf_start_1, orf_end_1 = map(int, args.orf_range.split('-')) + orf_start = orf_start_1 - 1 + orf_end = orf_end_1 - 1 + + ref_dict = SeqIO.to_dict(SeqIO.parse(args.reference, "fasta")) + + orf_dict = {} + codon_dict = {} + + if os.path.exists(args.out_file): + os.remove(args.out_file) + + print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Get the reference codons, please wait...", flush = True) + for chrom, record in ref_dict.items(): + orf_seq = record.seq[orf_start : orf_end + 1] + orf_dict[chrom] = orf_seq + ref_codons = {} + for i in range(0, len(orf_seq) - 2, 3): + codon_idx = i // 3 + 1 # 1-based codon index + ref_codon = orf_seq[i:i + 3] + if len(ref_codon) == 3: + ref_codons[codon_idx] = ref_codon + codon_dict[chrom] = ref_codons + + del ref_dict + gc.collect() + + print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Get the base coverage, please wait...", flush = True) + sub_region_size = max(1, (orf_end - orf_start + 1) // args.threads) + dict_base_cov = get_base_cov_in_chunk(args.input_bam, chrom, orf_start, orf_end + 1, args.base_qual, sub_region_size, args.threads, args.min_flank) + + print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Extracting read information, please wait...", flush = True) + list_results = [] + for i, chunk_result in enumerate(read_bam_in_chunk(args.input_bam, orf_start, orf_end, args.base_qual, args.min_flank, args.chunk_size, args.threads)): + print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} |--> Processed chunk {i+1}", flush = True) + list_results.append(chunk_result) + print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} |--> Finished.", flush = True) + + print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Creating the variant matrix, please wait...", flush = True) + list_results_filtered = [df for df in list_results if df.height > 0] + + if list_results_filtered: + df_variants = pl.concat(list_results_filtered, how = "vertical") + df_variants = df_variants.filter(pl.col("base_mut") != "") + df_variants_counts = ( df_variants.group_by("base_mut") + .agg([pl.col("counts").sum().alias("counts"), + pl.all().exclude(["base_mut", "counts"]).first()]) ) + del list_results, list_results_filtered, df_variants + gc.collect() + else: + df_variants_counts = pl.DataFrame([], schema={ + "base_mut": pl.Utf8, + "counts": pl.Int64, + "base_cov_avg": pl.Int64, + "varying_bases": pl.Int64, + "varying_codons": pl.Int64, + "codon_mut": pl.Utf8, + "aa_mut": pl.Utf8, + "pos_mut": pl.Utf8 + }, orient = "row") + + # matches GATK --min-variant-obs + df_variants_counts = df_variants_counts.filter(pl.col("counts") >= args.min_counts) + + print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Creating the output file, please wait...", flush = True) + df_variants_counts = df_variants_counts.select([pl.col("counts"), + pl.col("base_cov_avg").alias("cov"), + pl.lit(0).alias("mean_length_variant_reads"), + pl.col("varying_bases"), + pl.col("base_mut"), + pl.col("varying_codons"), + pl.col("codon_mut"), + pl.col("aa_mut"), + pl.col("pos_mut")]) + # GATK variantCounts has no header line + df_variants_counts.write_csv(args.out_file, separator = "\\t", include_header = False) + print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Done.", flush = True) + + #-- versions --# + with open("versions.yml", "w") as vf: + vf.write('"${task.process}":\\n') + vf.write(" python: " + platform.python_version() + "\\n") + vf.write(" pysam: " + pysam.__version__ + "\\n") + vf.write(" polars: " + pl.__version__ + "\\n") + vf.write(" pyarrow: " + pa.__version__ + "\\n") + vf.write(" numpy: " + np.__version__ + "\\n") + vf.write(" biopython: " + Bio.__version__ + "\\n") diff --git a/modules/local/visualization/counts_heatmap/environment.yml b/modules/local/visualization/counts_heatmap/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/visualization/counts_heatmap/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/visualization/counts_heatmap/main.nf b/modules/local/visualization/counts_heatmap/main.nf new file mode 100644 index 0000000..4ef05dd --- /dev/null +++ b/modules/local/visualization/counts_heatmap/main.nf @@ -0,0 +1,31 @@ +process VISUALIZATION_COUNTS_HEATMAP { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(meta), path(variantCounts_for_heatmaps) + val min_counts + + output: + tuple val(meta), path("counts_heatmap.pdf"), emit: counts_heatmap + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'counts_heatmap.R' + + stub: + """ + touch counts_heatmap.pdf + echo "VISUALIZATION_COUNTS_HEATMAP:" > versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/visualization/counts_heatmap/meta.yml b/modules/local/visualization/counts_heatmap/meta.yml new file mode 100644 index 0000000..41ac5d7 --- /dev/null +++ b/modules/local/visualization/counts_heatmap/meta.yml @@ -0,0 +1,51 @@ +name: "visualization_counts_heatmap" +description: Generates a comprehensive heatmap of variant counts, visualizing the mutation landscape across the entire reference sequence. +keywords: + - deep mutational scanning + - dms + - visualization + - heatmap + - mutation landscape +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - variantCounts_for_heatmaps: + type: file + description: CSV file containing processed variant counts formatted for heatmap generation + pattern: "*.{csv}" + - - min_counts: + type: integer + description: Minimum count threshold used to filter variants before plotting + +output: + - counts_heatmap: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - counts_heatmap.pdf: + type: file + description: PDF heatmap visualizing mutation frequencies across the sequence positions + pattern: "counts_heatmap.pdf" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/visualization/counts_heatmap/templates/counts_heatmap.R b/modules/local/visualization/counts_heatmap/templates/counts_heatmap.R new file mode 100644 index 0000000..44ecb8d --- /dev/null +++ b/modules/local/visualization/counts_heatmap/templates/counts_heatmap.R @@ -0,0 +1,187 @@ +#!/usr/bin/env Rscript + +# Input: prepared GATK data path, output_path, threshold (same as used for prepare_gatk_data_for_counts_per_cov_heatmap function) +# Output: counts_per_cov_heatmap.pdf + +library(dplyr) +library(ggplot2) + +counts_heatmap <- function(input_csv_path, threshold = 3, output_pdf_path, img_format = "pdf") { + + # Inner function to add padding to the last row, adding 21 amino acids per position + pad_heatmap_data_long <- function(heatmap_data_long, min_non_na_value, num_positions_per_row = 75) { + all_amino_acids <- c("G", "A", "V", "L", "M", "I", "F", + "Y", "W", "K", "R", "H", "D", "E", + "S", "T", "C", "N", "Q", "P", "*") + + max_position <- max(heatmap_data_long\$position) + num_missing_positions <- num_positions_per_row - (max_position %% num_positions_per_row) + + if (num_missing_positions < num_positions_per_row) { + new_positions <- (max_position + 1):(max_position + num_missing_positions) + + # Add all 21 amino acid variants for each new position + padding_data <- expand.grid( + mut_aa = all_amino_acids, # All possible amino acids + position = new_positions # New positions to be padded + ) + + # Set placeholder values for the added positions to the exact smallest non-NA value + padding_data\$total_counts <- min_non_na_value # Set to the smallest non-NA value + padding_data\$wt_aa <- "Y" # Set wild-type amino acid to 'Y' + padding_data\$wt_aa_pos <- paste0("Y", padding_data\$position) # Create wt_aa_pos with correct positions + padding_data\$row_group <- max(heatmap_data_long\$row_group) # Set row group to the current last group + + # Add the new padding rows to heatmap_data_long + heatmap_data_long <- dplyr::bind_rows(heatmap_data_long, padding_data) + } + + return(heatmap_data_long) + } + + # Load the CSV data + heatmap_data <- read.csv(input_csv_path) + + # Check if the necessary column exists in the data + if (!"total_counts" %in% colnames(heatmap_data)) { + stop("The column 'total_counts' is not found in the data.") + } + + # Create heatmap_data_long by selecting necessary columns + heatmap_data_long <- heatmap_data %>% + select(mut_aa, position, total_counts, wt_aa) # Use 'total_counts' + + # Find the smallest non-NA value in total_counts + min_non_na_value <- min(heatmap_data_long\$total_counts, na.rm = TRUE) + + # Group positions by rows (75 positions per row) and calculate row_group + heatmap_data_long <- heatmap_data_long %>% + mutate(row_group = ((position - 1) %/% 75) + 1) # Grouping positions into rows + + # Apply padding to add missing positions at the end of the last row, using the calculated min value + heatmap_data_long <- pad_heatmap_data_long(heatmap_data_long, min_non_na_value) + + # Convert positions to numeric, sort them, and create wt_aa_pos for the plot + heatmap_data_long <- heatmap_data_long %>% + mutate(position = as.numeric(position)) %>% # Ensure position is numeric + arrange(position) %>% # Sort by position + mutate(wt_aa_pos = factor(paste0(wt_aa, position), levels = unique(paste0(wt_aa, position)))) # Create sorted factor levels for wt_aa_pos + + # Add a column to identify synonymous mutations (where mut_aa == wt_aa) + heatmap_data_long <- heatmap_data_long %>% + mutate(synonymous = mut_aa == wt_aa) + + # Definiere die korrekte Reihenfolge der AminosΓ€uren + amino_acid_order <- rev(c("G", "A", "V", "L", "M", "I", "F", + "Y", "W", "K", "R", "H", "D", "E", + "S", "T", "C", "N", "Q", "P", "*")) + + heatmap_data_long <- heatmap_data_long %>% + mutate(mut_aa = factor(mut_aa, levels = amino_acid_order)) + + # Bearbeite heatmap_data_long und erstelle syn_positions gleichzeitig + syn_positions <- heatmap_data_long %>% + mutate(mut_aa = factor(mut_aa, levels = amino_acid_order), + # Berechne die x-Koordinate, die pro Gruppe immer von 1 bis 75 verlΓ€uft + x = as.numeric(factor(wt_aa_pos, levels = unique(wt_aa_pos))) - ((row_group - 1) * 75), + y = as.numeric(factor(mut_aa, levels = amino_acid_order))) %>% + filter(synonymous == TRUE) + + # Calculate the number of row groups and adjust plot height dynamically + num_row_groups <- max(heatmap_data_long\$row_group) + plot_height <- num_row_groups * 4 + + # Set the limits for the color scale, ignoring NA (negative values are now NA) + min_count <- min(heatmap_data_long\$total_counts, na.rm = TRUE) + max_count <- max(heatmap_data_long\$total_counts, na.rm = TRUE) + max_position <- max(heatmap_data\$position) + + # Create the heatmap plot with explicit handling for positions > max_position + heatmap_plot <- ggplot(heatmap_data_long, aes(x = wt_aa_pos, y = mut_aa, fill = total_counts)) + + scale_fill_gradientn(colours = c(alpha("blue", 0), "blue"), na.value = "grey35", trans = "log", # Apply log transformation to the scale + limits = c(min_count, max_count), + breaks = scales::trans_breaks("log10", function(x) 10^x), # Logarithmic scale breaks + labels = scales::trans_format("log10", scales::math_format(10^.x))) + + scale_x_discrete(labels = function(x) { + numeric_pos <- as.numeric(gsub("[^0-9]", "", x)) + ifelse(numeric_pos > max_position, " ", x) + }) + + geom_tile() + + + # Add diagonal lines for synonymous mutations using geom_segment + geom_segment(data = syn_positions[syn_positions\$position <= max_position, ], + aes(x = x - 0.485, xend = x + 0.485, + y = y - 0.485, yend = y + 0.485, color = synonymous), + size = 0.2) + + + # Manuelle Farbskala fΓΌr die diagonalen Linien + scale_color_manual(values = c("TRUE" = "grey10"), labels = c("TRUE" = "")) + + + theme_minimal() + + labs(title = "Heatmap of Counts per Variant", x = "Wild-type Amino Acid", y = "Mutant Amino Acid", fill = "Counts", color = "Synonymous Mutation") + + theme(plot.title = element_text(size = 16, face = "bold"), + axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.5, size = 12), + axis.text.y = element_text(size = 12), # Larger y-axis labels + axis.title.x = element_text(size = 16), + axis.title.y = element_text(size = 16), + legend.title = element_text(size = 14), # Larger legend title + legend.text = element_text(size = 12), # Larger legend text + panel.spacing = unit(0.1, "lines"), # Adjust panel spacing + strip.text = element_blank(), # Remove row group labels (facet numbers) + strip.background = element_blank(), + panel.grid.major = element_blank(), # Remove major grid lines + panel.grid.minor = element_blank()) + # Remove minor grid lines + facet_wrap(~ row_group, scales = "free_x", ncol = 1) + # Group by 75 positions per row + theme(panel.spacing = unit(0.2, "lines")) + + heatmap_plot <- heatmap_plot + + geom_point(data = heatmap_data_long, aes(size = ""), colour = "black", alpha = 0) # Invisible points for legend + heatmap_plot <- heatmap_plot + + guides(size = guide_legend(paste("Dropout (Counts <", threshold, ")"), override.aes = list(shape = 15, size = 8, colour = "grey35", alpha = 1))) # Define Legend for Dropouts + + # Save the heatmap plot + if (img_format == "pdf") { + ggsave(output_pdf_path, plot = heatmap_plot, width = 16, height = plot_height, dpi = 150, device = cairo_pdf) + } else { + ggsave(output_pdf_path, plot = heatmap_plot, width = 16, height = plot_height, dpi = 150) + } + + if (file.exists(output_pdf_path)) { + print("Heatmap image successfully created!") + } else { + print("Error: Heatmap image was not created.") + } +} + +##### +# run function +##### +counts_heatmap( + input_csv_path = "$variantCounts_for_heatmaps", + threshold = $min_counts, + output_pdf_path = "counts_heatmap.pdf", + img_format = "pdf" +) + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +dplyr_version <- as.character(packageVersion("dplyr")) +ggplot2_version <- as.character(packageVersion("ggplot2")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(dplyr_version) == 0) dplyr_version <- "unknown" +if (length(ggplot2_version) == 0) ggplot2_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-dplyr:', dplyr_version), + paste(' r-ggplot2:', ggplot2_version) + ), + f +) +close(f) diff --git a/modules/local/visualization/counts_per_cov/environment.yml b/modules/local/visualization/counts_per_cov/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/visualization/counts_per_cov/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/visualization/counts_per_cov/main.nf b/modules/local/visualization/counts_per_cov/main.nf new file mode 100644 index 0000000..e19bd40 --- /dev/null +++ b/modules/local/visualization/counts_per_cov/main.nf @@ -0,0 +1,31 @@ +process VISUALIZATION_COUNTS_PER_COV { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(meta), path(variantCounts_for_heatmaps) + val min_counts + + output: + tuple val(meta), path("counts_per_cov_heatmap.pdf"), emit: counts_per_cov_heatmap + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'counts_per_cov_heatmap.R' + + stub: + """ + touch counts_per_cov_heatmap.pdf + echo "VISUALIZATION_COUNTS_PER_COV:" > versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/visualization/counts_per_cov/meta.yml b/modules/local/visualization/counts_per_cov/meta.yml new file mode 100644 index 0000000..354351c --- /dev/null +++ b/modules/local/visualization/counts_per_cov/meta.yml @@ -0,0 +1,49 @@ +name: "visualization_counts_per_cov" +description: "Generates a heatmap visualizing the distribution of variant counts per coverage depth in Deep Mutational Scanning data." +keywords: + - deep mutational scanning + - dms + - visualization + - heatmap + - coverage +tools: + - "r-base": + description: "R is a free software environment for statistical computing and graphics" + homepage: "https://www.r-project.org/" + documentation: "https://cran.r-project.org/manuals.html" + licence: ["GPL-2.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - variantCounts_for_heatmaps: + type: file + description: CSV file containing processed variant counts formatted for heatmap generation + pattern: "*.{csv}" + - - min_counts: + type: integer + description: Minimum count threshold used to filter variants for visualization + +output: + - counts_per_cov_heatmap: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - counts_per_cov_heatmap.pdf: + type: file + description: PDF heatmap showing variant counts distributed across coverage depths + pattern: "counts_per_cov_heatmap.pdf" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/visualization/counts_per_cov/templates/counts_per_cov_heatmap.R b/modules/local/visualization/counts_per_cov/templates/counts_per_cov_heatmap.R new file mode 100644 index 0000000..77518b7 --- /dev/null +++ b/modules/local/visualization/counts_per_cov/templates/counts_per_cov_heatmap.R @@ -0,0 +1,187 @@ +#!/usr/bin/env Rscript + +# Input: prefiltered GATK data path, output_path, threshold (same as used for prepare_gatk_data_for_counts_per_cov_heatmap function) +# Output: counts_per_cov_heatmap.pdf + +library(dplyr) +library(ggplot2) + +counts_per_cov_heatmap <- function(input_csv_path, threshold = 3, output_pdf_path, img_format = "pdf") { + + # Inner function to add padding to the last row, adding 21 amino acids per position + pad_heatmap_data_long <- function(heatmap_data_long, min_non_na_value, num_positions_per_row = 75) { + all_amino_acids <- c("G", "A", "V", "L", "M", "I", "F", + "Y", "W", "K", "R", "H", "D", "E", + "S", "T", "C", "N", "Q", "P", "*") + + max_position <- max(heatmap_data_long\$position) + num_missing_positions <- num_positions_per_row - (max_position %% num_positions_per_row) + + if (num_missing_positions < num_positions_per_row) { + new_positions <- (max_position + 1):(max_position + num_missing_positions) + + # Add all 21 amino acid variants for each new position + padding_data <- expand.grid( + mut_aa = all_amino_acids, # All possible amino acids + position = new_positions # New positions to be padded + ) + + # Set placeholder values for the added positions to the exact smallest non-NA value + padding_data\$total_counts_per_cov <- min_non_na_value # Set to the smallest non-NA value + padding_data\$wt_aa <- "Y" # Set wild-type amino acid to 'Y' + padding_data\$wt_aa_pos <- paste0("Y", padding_data\$position) # Create wt_aa_pos with correct positions + padding_data\$row_group <- max(heatmap_data_long\$row_group) # Set row group to the current last group + + # Add the new padding rows to heatmap_data_long + heatmap_data_long <- dplyr::bind_rows(heatmap_data_long, padding_data) + } + + return(heatmap_data_long) + } + + # Load the CSV data + heatmap_data <- read.csv(input_csv_path) + + # Check if the necessary column exists in the data + if (!"total_counts_per_cov" %in% colnames(heatmap_data)) { + stop("The column 'total_counts_per_cov' is not found in the data.") + } + + # Create heatmap_data_long by selecting necessary columns + heatmap_data_long <- heatmap_data %>% + select(mut_aa, position, total_counts_per_cov, wt_aa) # Use 'total_counts_per_cov' + + # Find the smallest non-NA value in total_counts_per_cov + min_non_na_value <- min(heatmap_data_long\$total_counts_per_cov, na.rm = TRUE) + + # Group positions by rows (75 positions per row) and calculate row_group + heatmap_data_long <- heatmap_data_long %>% + mutate(row_group = ((position - 1) %/% 75) + 1) # Grouping positions into rows + + # Apply padding to add missing positions at the end of the last row, using the calculated min value + heatmap_data_long <- pad_heatmap_data_long(heatmap_data_long, min_non_na_value) + + # Convert positions to numeric, sort them, and create wt_aa_pos for the plot + heatmap_data_long <- heatmap_data_long %>% + mutate(position = as.numeric(position)) %>% # Ensure position is numeric + arrange(position) %>% # Sort by position + mutate(wt_aa_pos = factor(paste0(wt_aa, position), levels = unique(paste0(wt_aa, position)))) # Create sorted factor levels for wt_aa_pos + + # Add a column to identify synonymous mutations (where mut_aa == wt_aa) + heatmap_data_long <- heatmap_data_long %>% + mutate(synonymous = mut_aa == wt_aa) + + # Definiere die korrekte Reihenfolge der AminosΓ€uren + amino_acid_order <- rev(c("G", "A", "V", "L", "M", "I", "F", + "Y", "W", "K", "R", "H", "D", "E", + "S", "T", "C", "N", "Q", "P", "*")) + + heatmap_data_long <- heatmap_data_long %>% + mutate(mut_aa = factor(mut_aa, levels = amino_acid_order)) + + # Bearbeite heatmap_data_long und erstelle syn_positions gleichzeitig + syn_positions <- heatmap_data_long %>% + mutate(mut_aa = factor(mut_aa, levels = amino_acid_order), + # Berechne die x-Koordinate, die pro Gruppe immer von 1 bis 75 verlΓ€uft + x = as.numeric(factor(wt_aa_pos, levels = unique(wt_aa_pos))) - ((row_group - 1) * 75), + y = as.numeric(factor(mut_aa, levels = amino_acid_order))) %>% + filter(synonymous == TRUE) + + # Calculate the number of row groups and adjust plot height dynamically + num_row_groups <- max(heatmap_data_long\$row_group) + plot_height <- num_row_groups * 4 + + # Set the limits for the color scale, ignoring NA (negative values are now NA) + min_count <- min(heatmap_data_long\$total_counts_per_cov, na.rm = TRUE) + max_count <- max(heatmap_data_long\$total_counts_per_cov, na.rm = TRUE) + max_position <- max(heatmap_data\$position) + + # Create the heatmap plot with explicit handling for positions > max_position + heatmap_plot <- ggplot(heatmap_data_long, aes(x = wt_aa_pos, y = mut_aa, fill = total_counts_per_cov)) + + scale_fill_gradientn(colours = c(alpha("blue", 0), "blue"), na.value = "grey35", trans = "log", # Apply log transformation to the scale + limits = c(min_count, max_count), + breaks = scales::trans_breaks("log10", function(x) 10^x), # Logarithmic scale breaks + labels = scales::trans_format("log10", scales::math_format(10^.x))) + + scale_x_discrete(labels = function(x) { + numeric_pos <- as.numeric(gsub("[^0-9]", "", x)) + ifelse(numeric_pos > max_position, " ", x) + }) + + geom_tile() + + + # Add diagonal lines for synonymous mutations using geom_segment + geom_segment(data = syn_positions[syn_positions\$position <= max_position, ], + aes(x = x - 0.485, xend = x + 0.485, + y = y - 0.485, yend = y + 0.485, color = synonymous), + size = 0.2) + + + # Manuelle Farbskala fΓΌr die diagonalen Linien + scale_color_manual(values = c("TRUE" = "grey10"), labels = c("TRUE" = "")) + + + theme_minimal() + + labs(title = "Heatmap of Counts per Coverage for Mutations", x = "Wild-type Amino Acid", y = "Mutant Amino Acid", fill = "Counts per \\n Coverage", color = "Synonymous Mutation") + + theme(plot.title = element_text(size = 16, face = "bold"), + axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.5, size = 12), + axis.text.y = element_text(size = 12), # Larger y-axis labels + axis.title.x = element_text(size = 16), + axis.title.y = element_text(size = 16), + legend.title = element_text(size = 14), # Larger legend title + legend.text = element_text(size = 12), # Larger legend text + panel.spacing = unit(0.1, "lines"), # Adjust panel spacing + strip.text = element_blank(), # Remove row group labels (facet numbers) + strip.background = element_blank(), + panel.grid.major = element_blank(), # Remove major grid lines + panel.grid.minor = element_blank()) + # Remove minor grid lines + facet_wrap(~ row_group, scales = "free_x", ncol = 1) + # Group by 75 positions per row + theme(panel.spacing = unit(0.2, "lines")) + + heatmap_plot <- heatmap_plot + + geom_point(data = heatmap_data_long, aes(size = ""), colour = "black", alpha = 0) # Invisible points for legend + heatmap_plot <- heatmap_plot + + guides(size = guide_legend(paste("Dropout (Counts <", threshold, ")"), override.aes = list(shape = 15, size = 8, colour = "grey35", alpha = 1))) # Define Legend for Dropouts + + # Save the heatmap plot + if (img_format == "pdf") { + ggsave(output_pdf_path, plot = heatmap_plot, width = 16, height = plot_height, dpi = 150, device = cairo_pdf) + } else { + ggsave(output_pdf_path, plot = heatmap_plot, width = 16, height = plot_height, dpi = 150) + } + + if (file.exists(output_pdf_path)) { + print("Heatmap image successfully created!") + } else { + print("Error: Heatmap image was not created.") + } +} + +##### +# run function +##### +counts_per_cov_heatmap( + input_csv_path = "$variantCounts_for_heatmaps", + threshold = $min_counts, + output_pdf_path = "counts_per_cov_heatmap.pdf", + img_format = "pdf" +) + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +dplyr_version <- as.character(packageVersion("dplyr")) +ggplot2_version <- as.character(packageVersion("ggplot2")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(dplyr_version) == 0) dplyr_version <- "unknown" +if (length(ggplot2_version) == 0) ggplot2_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-dplyr:', dplyr_version), + paste(' r-ggplot2:', ggplot2_version) + ), + f +) +close(f) diff --git a/modules/local/visualization/error_correction_report/environment.yml b/modules/local/visualization/error_correction_report/environment.yml new file mode 100644 index 0000000..a72f7ec --- /dev/null +++ b/modules/local/visualization/error_correction_report/environment.yml @@ -0,0 +1,7 @@ +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::python=3.12.3 + - conda-forge::pandas=2.2.1 + - conda-forge::numpy=1.26.4 diff --git a/modules/local/visualization/error_correction_report/main.nf b/modules/local/visualization/error_correction_report/main.nf new file mode 100644 index 0000000..6038ef7 --- /dev/null +++ b/modules/local/visualization/error_correction_report/main.nf @@ -0,0 +1,43 @@ +// Build the self-contained error-correction HTML report for the whole run. +// +// One report, not one per file: every sequencing file's numbers travel in the page and each panel +// recomputes in the browser from whichever files the reader selects, so a single-file view is still +// one click away while cross-replicate spread (the error bars) becomes possible at all. +process VISUALIZATION_ERROR_CORRECTION_REPORT { + tag "${sample}" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container + ? 'https://depot.galaxyproject.org/singularity/pandas:2.2.1' + : 'quay.io/biocontainers/pandas:2.2.1' }" + + input: + val sample // run-level name, for the heading + val ids_b64 // base64(JSON) [file id, ...] aligned with `corrected` + path corrected, stageAs: 'c*/*' // per-file tables share a basename -> numbered dirs + val method + val false_doubles_method // 'mle' | 'eb' - names the estimator in the report heading + path template_html + + output: + path "error_correction_report.html", emit: report + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'build_ec_report.py' + + stub: + """ + touch error_correction_report.html + cat <<-END_VERSIONS > versions.yml + "${task.process}": + python: "0.0.0" + pandas: "0.0.0" + END_VERSIONS + """ +} diff --git a/modules/local/visualization/error_correction_report/meta.yml b/modules/local/visualization/error_correction_report/meta.yml new file mode 100644 index 0000000..9cb45dc --- /dev/null +++ b/modules/local/visualization/error_correction_report/meta.yml @@ -0,0 +1,53 @@ +name: "visualization_error_correction_report" +description: Build a self-contained, per-sample HTML report visualising the effect of the sequencing-error correction (positional error bias, correction magnitude, and per-variant raw-vs-corrected counts). +keywords: + - deep mutational scanning + - error correction + - report + - visualisation +tools: + - "pandas": + description: "Fast, powerful data structures for data analysis in Python." + homepage: "https://pandas.pydata.org/" + licence: ["BSD-3-Clause"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', sample:'ORF1' ]` + - corrected: + type: file + description: Error-corrected, library-filtered counts (with `counts` corrected and `counts_raw` preserved). + pattern: "*.csv" + - - method: + type: string + description: Error-correction method label (e.g. `false_doubles`, `wildtype`). + - - false_doubles_method: + type: string + description: False-doubles estimator actually used (`mle` or `eb`); named in the report heading. + - - template_html: + type: file + description: HTML template into which the report data is injected. + pattern: "*.html" + +output: + - report: + - meta: + type: map + description: Groovy Map containing sample information + - "*_error_correction_report.html": + type: file + description: Self-contained error-correction report. + pattern: "*_error_correction_report.html" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" +maintainers: + - "@BenjaminWehnert1008" diff --git a/modules/local/visualization/error_correction_report/templates/build_ec_report.py b/modules/local/visualization/error_correction_report/templates/build_ec_report.py new file mode 100644 index 0000000..e6719a9 --- /dev/null +++ b/modules/local/visualization/error_correction_report/templates/build_ec_report.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python + +# Build the self-contained error-correction HTML report for a whole run. +# +# One report covers every sequencing file: the page carries each file's numbers separately and +# recomputes every panel in the browser from whichever files are selected, so deselecting down to a +# single file reproduces exactly what a per-file report used to show. +# +# Nextflow template: values below are interpolated; backslashes meant for Python are doubled. + +import base64 +import json +import platform +import re + +import numpy as np +import pandas as pd + +sample = "$sample" +method = "$method" +# The false-doubles estimator actually used (mle|eb), so the report can name it. Empty/other for the +# wildtype method, where it does not apply. +estimator = "$false_doubles_method" +# Each entry is {id, type, replicate} - type/replicate come straight from the samplesheet (general +# for any protein, not hard-coded here) and drive the input-vs-output split in the report. +files = json.loads(base64.b64decode("$ids_b64").decode("utf-8")) +paths = "$corrected".split() + +if len(files) != len(paths): + raise SystemExit(f"{len(files)} ids but {len(paths)} files staged") + +MUT = re.compile(r"^([A-Za-z*]+)(\\d+)([A-Za-z*]+)") + +# `base_mut` holds the nucleotide change(s) of a variant as ":>", comma-separated when +# the codon carries 2-3 changes (the NNK case). Positions are reference coordinates. +SNV = re.compile(r"^(\\d+):([ACGTacgt])>([ACGTacgt])\$") + +COMPLEMENT = {"A": "T", "C": "G", "G": "C", "T": "A"} + +# The six strand-symmetric substitution classes, pyrimidine-centric: a purine reference is +# reverse-complemented, so C>A and G>T are one and the same event. Order is canonical (as used for +# mutational signatures) and fixed - the colour of a class never depends on the data. +SNV_CLASSES = ["C>A", "C>G", "C>T", "T>A", "T>C", "T>G"] +SNV_LABELS = { + "C>A": "C>A / G>T", "C>G": "C>G / G>C", "C>T": "C>T / G>A", + "T>A": "T>A / A>T", "T>C": "T>C / A>G", "T>G": "T>G / A>C", +} + + +def parse_pos(s): + m = MUT.match(str(s)) + return (m.group(1), int(m.group(2)), m.group(3)) if m else (None, None, None) + + +def parse_snvs(base_mut): + """"1220:T>G, 1221:T>G" -> [(1220,'T','G'), (1221,'T','G')]""" + out = [] + for part in str(base_mut).split(","): + m = SNV.match(part.strip()) + if m: + out.append((int(m.group(1)), m.group(2).upper(), m.group(3).upper())) + return out + + +def classify_snv(ref, alt): + """Collapse a substitution onto its pyrimidine-centric class key.""" + if ref not in COMPLEMENT or alt not in COMPLEMENT or ref == alt: + return None + if ref in ("A", "G"): + ref, alt = COMPLEMENT[ref], COMPLEMENT[alt] + key = ref + ">" + alt + return key if key in SNV_LABELS else None + + +def numcol(df, name): + return (pd.to_numeric(df[name], errors="coerce") if name in df.columns + else pd.Series([np.nan] * len(df))) + + +def f(x, nd): + return None if x is None or x != x else round(float(x), nd) + + +# Variants are keyed on `codon_mut`, not `pos_mut`. `pos_mut` is NOT unique - one amino-acid +# substitution is reachable through up to three codons (6898 unique pos_mut across 10254 rows on the +# GID1A run), so keying on it would silently merge distinct nucleotide variants. `codon_mut` is +# unique (10254/10254). +# +# The files do not share a variant set either (union 10771 vs intersection 6886 on GID1A), so this is +# a union: a variant absent from a file gets None there, never 0. Absent means "not observed above +# threshold in that file", which is missing data, not a measured zero - averaging it in as 0 would +# drag every frequency down. The browser skips Nones and reports how many files contributed. +variants = {} +n_files = len(files) + +for fi, (fid, path) in enumerate(zip(files, paths)): + df = pd.read_csv(path) + raw = numcol(df, "counts_raw") + cor = numcol(df, "counts") + cpc_raw = numcol(df, "counts_per_cov_raw") + cpc_cor = numcol(df, "counts_per_cov") + posmut = df["pos_mut"].astype(str) + basemut = df["base_mut"].astype(str) if "base_mut" in df.columns else pd.Series([""] * len(df)) + codonmut = df["codon_mut"].astype(str) if "codon_mut" in df.columns else posmut + + for i in range(len(df)): + r = raw.iloc[i] + if pd.isna(r): + continue + key = codonmut.iloc[i].strip() + v = variants.get(key) + if v is None: + wt, pos, _mut = parse_pos(posmut.iloc[i]) + nt = basemut.iloc[i].strip() + snvs = parse_snvs(nt) + # A class is only meaningful when a single nucleotide changed; NNK codon variants + # (2-3 changes) mix classes and stay unclassified by design. + kls = classify_snv(snvs[0][1], snvs[0][2]) if len(snvs) == 1 else None + v = variants[key] = { + "key": key, "pos_mut": posmut.iloc[i], "pos": pos, "wt_aa": wt, + "nt": nt, "cls": kls, + "ntpos": (snvs[0][0] if len(snvs) == 1 else None), + "raw": [None] * n_files, "cor": [None] * n_files, + "f_raw": [None] * n_files, "f_cor": [None] * n_files, + } + c = cor.iloc[i] + v["raw"][fi] = f(r, 3) + v["cor"][fi] = 0.0 if pd.isna(c) else f(c, 3) + v["f_raw"][fi] = f(cpc_raw.iloc[i], 10) + v["f_cor"][fi] = 0.0 if pd.isna(cpc_cor.iloc[i]) else f(cpc_cor.iloc[i], 10) + +data = { + "sample": sample, + "method": method, + "estimator": estimator, + "files": files, + "classes": [{"cls": k, "label": SNV_LABELS[k]} for k in SNV_CLASSES], + "variants": sorted(variants.values(), key=lambda v: (v["pos"] if v["pos"] is not None else 0, v["key"])), +} + +data_json = json.dumps(data, separators=(",", ":")).replace(" versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/visualization/global_pos_biases_counts/meta.yml b/modules/local/visualization/global_pos_biases_counts/meta.yml new file mode 100644 index 0000000..8a02b08 --- /dev/null +++ b/modules/local/visualization/global_pos_biases_counts/meta.yml @@ -0,0 +1,61 @@ +name: "visualization_global_pos_biases_counts" +description: Calculates and visualizes the rolling mean of variant counts and counts normalized by coverage across the reference sequence to identify regional biases or mutation hotspots. +keywords: + - deep mutational scanning + - dms + - visualization + - rolling mean + - sliding window + - bias +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - variantCounts_filtered_by_library: + type: file + description: CSV file containing variant counts filtered against the target library + pattern: "*.{csv}" + - - aa_seq: + type: file + description: Text or FASTA file containing the reference amino acid sequence + - - sliding_window_size: + type: integer + description: The size of the window (number of amino acids) used for calculating the rolling mean + +output: + - rolling_counts: + - meta: + type: map + description: Groovy Map containing sample information + - rolling_counts.pdf: + type: file + description: PDF plot showing the rolling mean of raw variant counts across positions + pattern: "rolling_counts.pdf" + - rolling_counts_per_cov: + - meta: + type: map + description: Groovy Map containing sample information + - rolling_counts_per_cov.pdf: + type: file + description: PDF plot showing the rolling mean of counts normalized by local coverage + pattern: "rolling_counts_per_cov.pdf" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/visualization/global_pos_biases_counts/templates/global_position_biases_counts_and_counts_per_cov.R b/modules/local/visualization/global_pos_biases_counts/templates/global_position_biases_counts_and_counts_per_cov.R new file mode 100644 index 0000000..2ec9d91 --- /dev/null +++ b/modules/local/visualization/global_pos_biases_counts/templates/global_position_biases_counts_and_counts_per_cov.R @@ -0,0 +1,186 @@ +#!/usr/bin/env Rscript + +# input: prefiltered gatk path, aa seq path, window_size (sliding window), output_path_folder +# output: two lineplots showing counts & counts per coverage divided in types of variants (single-/double-/triple-base exchange) + +library(zoo) # sliding window +library(dplyr) +library(ggplot2) +library(scales) + +position_biases <- function(prefiltered_gatk_path, aa_seq_path, window_size = 10) { + + # Load and process the data + prefiltered_gatk <- read.table(prefiltered_gatk_path, sep = ",", fill = NA, header = TRUE) + prefiltered_gatk\$pos <- as.numeric(sub("(\\\\D)(\\\\d+)(\\\\D)", "\\\\2", prefiltered_gatk\$pos_mut)) + unique_pos <- unique(as.numeric(prefiltered_gatk\$pos)) + aa_seq <- readLines(aa_seq_path, warn = FALSE) + aa_seq_length <- nchar(aa_seq) + aa_positions <- seq(nchar(aa_seq)) + + means_counts_single <- rep(NA, nchar(aa_seq)) + means_counts_double <- rep(NA, nchar(aa_seq)) + means_counts_triple <- rep(NA, nchar(aa_seq)) + means_counts_per_cov_single <- rep(NA, nchar(aa_seq)) + means_counts_per_cov_double <- rep(NA, nchar(aa_seq)) + means_counts_per_cov_triple <- rep(NA, nchar(aa_seq)) + + + # Loop through each position in the amino acid sequence + for (i in 1:(nchar(aa_seq))) { + + # Filter the data for the current position in aa_positions + window_data <- prefiltered_gatk %>% filter(prefiltered_gatk\$pos %in% aa_positions[i]) + + # Calculate mean for Single mutations (where varying_bases == 1) + window_data_single <- window_data %>% filter(varying_bases == 1) + means_counts_single[i] <- mean(window_data_single\$counts, na.rm = FALSE) + means_counts_per_cov_single[i] <- mean(window_data_single\$counts_per_cov, na.rm = FALSE) + + # Calculate mean for Double mutations (where varying_bases == 2) + window_data_double <- window_data %>% filter(varying_bases == 2) + means_counts_double[i] <- mean(window_data_double\$counts, na.rm = FALSE) + means_counts_per_cov_double[i] <- mean(window_data_double\$counts_per_cov, na.rm = FALSE) + + # Calculate mean for Triple mutations (where varying_bases == 3) + window_data_triple <- window_data %>% filter(varying_bases == 3) + means_counts_triple[i] <- mean(window_data_triple\$counts, na.rm = FALSE) + means_counts_per_cov_triple[i] <- mean(window_data_triple\$counts_per_cov, na.rm = FALSE) + } + + pos_bias_df <- data.frame(pos = seq(nchar(aa_seq))) + + + pos_bias_df\$rolling_mean_counts_single <- rollapply(means_counts_single, width = window_size, + FUN = function(x) (mean(x, na.rm = TRUE) + 0.00001), fill = "extend") + pos_bias_df\$rolling_mean_counts_double <- rollapply(means_counts_double, width = window_size, + FUN = function(x) (mean(x, na.rm = TRUE) + 0.00001), fill = "extend") + pos_bias_df\$rolling_mean_counts_triple <- rollapply(means_counts_triple, width = window_size, + FUN = function(x) (mean(x, na.rm = TRUE) + 0.00001), fill = "extend") + + pos_bias_df\$rolling_mean_counts_per_cov_single <- rollapply(means_counts_per_cov_single, width = window_size, + FUN = function(x) (mean(x, na.rm = TRUE) + 0.00001), fill = "extend") + pos_bias_df\$rolling_mean_counts_per_cov_double <- rollapply(means_counts_per_cov_double, width = window_size, + FUN = function(x) (mean(x, na.rm = TRUE) + 0.00001), fill = "extend") + pos_bias_df\$rolling_mean_counts_per_cov_triple <- rollapply(means_counts_per_cov_triple, width = window_size, + FUN = function(x) (mean(x, na.rm = TRUE) + 0.00001), fill = "extend") + + + # Replace NAs with 0 for rolling means and SE + pos_bias_df\$rolling_mean_counts_single[is.na(pos_bias_df\$rolling_mean_counts_single)] <- 0.00001 + pos_bias_df\$rolling_mean_counts_double[is.na(pos_bias_df\$rolling_mean_counts_double)] <- 0.00001 + pos_bias_df\$rolling_mean_counts_triple[is.na(pos_bias_df\$rolling_mean_counts_triple)] <- 0.00001 + pos_bias_df\$rolling_mean_counts_per_cov_single[is.na(pos_bias_df\$rolling_mean_counts_per_cov_single)] <- 0.000001 + pos_bias_df\$rolling_mean_counts_per_cov_double[is.na(pos_bias_df\$rolling_mean_counts_per_cov_double)] <- 0.000001 + pos_bias_df\$rolling_mean_counts_per_cov_triple[is.na(pos_bias_df\$rolling_mean_counts_per_cov_triple)] <- 0.000001 + + + + plots_theme <- list( + + # Add the minimal theme to the list + theme_minimal(), + + # Customize legend title and appearance + theme(legend.title = element_text(size = 10, face = "bold"), + legend.position = "right"), + + # Customize guides for the legend elements + guides( + color = guide_legend(title = "Mutation Type", order = 1), # Title for line color + fill = guide_legend(title = "Standard Deviation", order = 2), # Title for ribbon fill + linetype = guide_legend(title = "Required Coverage", order = 3) # Title for line type + ) + ) + + + # Placeholder name for the linetype + linetype_placeholder <- "Required coverage" + + rolling_counts_plot <- ggplot(pos_bias_df, aes(x = pos)) + + + # Add line for rolling mean coverage + geom_line(aes(y = rolling_mean_counts_single, color = "One Varying Base")) + + geom_line(aes(y = rolling_mean_counts_double, color = "Two Varying Bases")) + + geom_line(aes(y = rolling_mean_counts_triple, color = "Three Varying Bases")) + + + # Individual axis labels for this plot + xlab("Amino Acid Position") + + ylab("Counts") + + + # Manually set color and fill labels + scale_color_manual(name = "Mutation Type", + values = c("One Varying Base" = "chocolate", "Two Varying Bases" = "darkolivegreen3", "Three Varying Bases" = "deepskyblue1"), + limits = c("One Varying Base", "Two Varying Bases", "Three Varying Bases")) + # Color for line + + # Apply the saved theme and design elements at the end + plots_theme + + + scale_y_continuous(trans = 'log10', labels = scales::comma) + + + + + rolling_counts_per_cov_plot <- ggplot(pos_bias_df, aes(x = pos)) + + + # Add line for rolling mean coverage + geom_line(aes(y = rolling_mean_counts_per_cov_single, color = "One Varying Base")) + + geom_line(aes(y = rolling_mean_counts_per_cov_double, color = "Two Varying Bases")) + + geom_line(aes(y = rolling_mean_counts_per_cov_triple, color = "Three Varying Bases")) + + + # Individual axis labels for this plot + xlab("Amino Acid Position") + + ylab("Counts") + + + # Manually set color and fill labels + scale_color_manual(name = "Mutation Type", + values = c("One Varying Base" = "chocolate", "Two Varying Bases" = "darkolivegreen3", "Three Varying Bases" = "deepskyblue1"), + limits = c("One Varying Base", "Two Varying Bases", "Three Varying Bases")) + # Color and legend order for lines + + # Apply the saved theme and design elements at the end + plots_theme + + + scale_y_continuous(trans = 'log10', labels = scales::comma) + + + ggsave(filename = "rolling_counts.pdf", plot = rolling_counts_plot, device = "pdf", width = 10, height = 6) + ggsave(filename = "rolling_counts_per_cov.pdf", plot = rolling_counts_per_cov_plot, device = "pdf", width = 10, height = 6) +} + +##### +# run pipeline +##### +position_biases( + prefiltered_gatk_path = "$variantCounts_filtered_by_library", + aa_seq_path = "$aa_seq", + window_size = $sliding_window_size +) + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +dplyr_version <- as.character(packageVersion("dplyr")) +ggplot2_version <- as.character(packageVersion("ggplot2")) +scales_version <- as.character(packageVersion("scales")) +zoo_version <- as.character(packageVersion("zoo")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(dplyr_version) == 0) dplyr_version <- "unknown" +if (length(ggplot2_version) == 0) ggplot2_version <- "unknown" +if (length(scales_version) == 0) scales_version <- "unknown" +if (length(zoo_version) == 0) zoo_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-dplyr:', dplyr_version), + paste(' r-ggplot2:', ggplot2_version), + paste(' r-scales:', scales_version), + paste(' r-zoo:', zoo_version) + ), + f +) +close(f) diff --git a/modules/local/visualization/global_pos_biases_cov/environment.yml b/modules/local/visualization/global_pos_biases_cov/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/visualization/global_pos_biases_cov/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/visualization/global_pos_biases_cov/main.nf b/modules/local/visualization/global_pos_biases_cov/main.nf new file mode 100644 index 0000000..b6b18e3 --- /dev/null +++ b/modules/local/visualization/global_pos_biases_cov/main.nf @@ -0,0 +1,33 @@ +process VISUALIZATION_GLOBAL_POS_BIASES_COV { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(meta), path(variantCounts_filtered_by_library) + path aa_seq + val sliding_window_size + val aimed_cov + + output: + tuple val(meta), path("rolling_coverage.pdf"), emit: rolling_coverage + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'global_position_biases_cov.R' + + stub: + """ + touch rolling_coverage.pdf + echo "VISUALIZATION_COUNTS_HEATMAP:" > versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/visualization/global_pos_biases_cov/meta.yml b/modules/local/visualization/global_pos_biases_cov/meta.yml new file mode 100644 index 0000000..c0b6216 --- /dev/null +++ b/modules/local/visualization/global_pos_biases_cov/meta.yml @@ -0,0 +1,56 @@ +name: "visualization_global_pos_biases_cov" +description: Calculates and plots the rolling mean coverage across the protein sequence to identify positional biases, regions of low coverage, or potential experimental dropouts. +keywords: + - deep mutational scanning + - dms + - visualization + - coverage + - rolling window + - bias +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - variantCounts_filtered_by_library: + type: file + description: CSV file containing variant counts filtered against the target library + pattern: "*.{csv}" + - - aa_seq: + type: file + description: Text file containing the reference amino acid sequence + - - sliding_window_size: + type: integer + description: The size of the window (in amino acids) used for calculating the rolling mean coverage + - - aimed_cov: + type: integer + description: The target coverage threshold to visualize as a horizontal reference line in the plot + +output: + - rolling_coverage: + - meta: + type: map + description: Groovy Map containing sample information + - rolling_coverage.pdf: + type: file + description: PDF plot showing the rolling mean coverage across sequence positions + pattern: "rolling_coverage.pdf" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/visualization/global_pos_biases_cov/templates/global_position_biases_cov.R b/modules/local/visualization/global_pos_biases_cov/templates/global_position_biases_cov.R new file mode 100644 index 0000000..0a2fda1 --- /dev/null +++ b/modules/local/visualization/global_pos_biases_cov/templates/global_position_biases_cov.R @@ -0,0 +1,121 @@ +#!/usr/bin/env Rscript + +# input: prefiltered gatk path, aa seq path, window_size (sliding window), output_path_folder, aimed counts per aa variant +# output: lineplot showing coverage over positions and dotted line: Assuming all potential 21 variants are equally distributed, this is the coverage one need to get the aimed counts per variant. + +library(zoo) # sliding window +library(dplyr) +library(ggplot2) +library(scales) +position_biases <- function(prefiltered_gatk_path, aa_seq_path, window_size = 10, output_file_path, targeted_counts_per_aa_variant = 100) { + + # Load and process the data + prefiltered_gatk <- read.table(prefiltered_gatk_path, sep = ",", fill = NA, header = TRUE) + prefiltered_gatk\$pos <- as.numeric(sub("(\\\\D)(\\\\d+)(\\\\D)", "\\\\2", prefiltered_gatk\$pos_mut)) + unique_pos <- unique(as.numeric(prefiltered_gatk\$pos)) + aa_seq <- readLines(aa_seq_path, warn = FALSE) + aa_seq_length <- nchar(aa_seq) + aa_positions <- seq(nchar(aa_seq)) + + means_cov <- rep(NA, nchar(aa_seq)) + + # Calculate means for cov (should be the same over all variants in one position) + for (i in 1:(nchar(aa_seq))) { + window_data <- prefiltered_gatk %>% filter(prefiltered_gatk\$pos %in% aa_positions[i]) + means_cov[i] <- mean(window_data\$cov, na.rm = FALSE) + } + + pos_bias_df <- data.frame(pos = seq(nchar(aa_seq))) + + # Log-transform the rolling means to avoid issues with zeros (log(y + 0.001)) + pos_bias_df\$rolling_mean_cov <- rollapply(means_cov, width = window_size, + FUN = function(x) (mean(x, na.rm = TRUE)), fill = "extend") + + # Generate the plot + plots_theme <- list( + + # Customize legend appearance (leave titles blank) + guides( + color = guide_legend(title = NULL, order = 1), # Title for line color + fill = guide_legend(title = NULL, order = 2), # Title for ribbon fill + linetype = guide_legend(title = NULL, order = 3) # Title for line type + ), + + # Apply minimal theme + theme_minimal() + ) + + + linetype_placeholder <- "Required coverage" + + rolling_cov_plot <- ggplot(pos_bias_df, aes(x = pos, y = rolling_mean_cov)) + + + # Add line for rolling mean coverage + geom_line(aes(color = "Coverage")) + + + # Add a horizontal line with a mapped linetype so it appears in the legend + geom_hline(aes(yintercept = ((21 * nchar(aa_seq) * targeted_counts_per_aa_variant)), + linetype = linetype_placeholder), + color = "black", linewidth = 0.3) + + + # Individual axis labels for this plot + xlab("Amino Acid Position") + + ylab("Coverage") + + + # Manually set color and fill labels + scale_color_manual(values = c("Coverage" = "black")) + # Color for line + + # Set the linetype with a label that uses the targeted threshold dynamically + scale_linetype_manual(values = c("Required coverage" = "dotted"), + labels = paste("Required coverage \\n for", as.character(targeted_counts_per_aa_variant), "counts per variant \\n if equally present")) + + + # Apply the saved theme and design elements at the end + plots_theme + + + # Set y-axis to log scale and apply comma formatting + scale_y_continuous(trans = 'log10', labels = scales::comma) + + + # Return the plot + ggsave(filename = output_file_path, plot = rolling_cov_plot, device = "pdf", width = 10, height = 6) +} + +##### +# run function +##### +position_biases( + prefiltered_gatk_path = "$variantCounts_filtered_by_library", + aa_seq_path = "$aa_seq", + window_size = $sliding_window_size, + output_file_path = "rolling_coverage.pdf", + targeted_counts_per_aa_variant = $aimed_cov +) + +##### +# create versions.yml +##### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +dplyr_version <- as.character(packageVersion("dplyr")) +ggplot2_version <- as.character(packageVersion("ggplot2")) +scales_version <- as.character(packageVersion("scales")) +zoo_version <- as.character(packageVersion("zoo")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(dplyr_version) == 0) dplyr_version <- "unknown" +if (length(ggplot2_version) == 0) ggplot2_version <- "unknown" +if (length(scales_version) == 0) scales_version <- "unknown" +if (length(zoo_version) == 0) zoo_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-dplyr:', dplyr_version), + paste(' r-ggplot2:', ggplot2_version), + paste(' r-scales:', scales_version), + paste(' r-zoo:', zoo_version) + ), + f +) +close(f) diff --git a/modules/local/visualization/logdiff/environment.yml b/modules/local/visualization/logdiff/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/visualization/logdiff/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/visualization/logdiff/main.nf b/modules/local/visualization/logdiff/main.nf new file mode 100644 index 0000000..3b733c5 --- /dev/null +++ b/modules/local/visualization/logdiff/main.nf @@ -0,0 +1,32 @@ +process VISUALIZATION_LOGDIFF { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(meta), path(library_completed_variantCounts) + + output: + tuple val(meta), path("logdiff_plot.pdf"), emit: logdiff_plot + tuple val(meta), path("logdiff_varying_bases.pdf"), emit: logdiff_varying_bases + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'logdiff.R' + + stub: + """ + touch logdiff_plot.pdf + touch logdiff_varying_bases.pdf + echo "VISUALIZATION_COUNTS_HEATMAP:" > versions.yml + echo " stub-version: 0.0.0" >> versions.yml + """ +} diff --git a/modules/local/visualization/logdiff/meta.yml b/modules/local/visualization/logdiff/meta.yml new file mode 100644 index 0000000..dd4e343 --- /dev/null +++ b/modules/local/visualization/logdiff/meta.yml @@ -0,0 +1,54 @@ +name: "visualization_logdiff" +description: Generates diagnostic plots to visualize the logarithmic differences in variant counts, specifically focusing on the distribution of variants across different nucleotide compositions. +keywords: + - deep mutational scanning + - dms + - visualization + - diagnostics + - logdiff +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - library_completed_variantCounts: + type: file + description: CSV file containing processed variant counts, including zero-count entries from the library + pattern: "*.{csv}" + +output: + - logdiff_plot: + - meta: + type: map + description: Groovy Map containing sample information + - logdiff_plot.pdf: + type: file + description: PDF plot showing the overall logarithmic differences in variant counts + pattern: "logdiff_plot.pdf" + - logdiff_varying_bases: + - meta: + type: map + description: Groovy Map containing sample information + - logdiff_varying_bases.pdf: + type: file + description: PDF plot showing logarithmic differences categorized by varying nucleotide bases + pattern: "logdiff_varying_bases.pdf" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/visualization/logdiff/templates/logdiff.R b/modules/local/visualization/logdiff/templates/logdiff.R new file mode 100644 index 0000000..752e574 --- /dev/null +++ b/modules/local/visualization/logdiff/templates/logdiff.R @@ -0,0 +1,115 @@ +#!/usr/bin/env Rscript + +# Maybe develop additional ideas to characterise variants below 10 percentile -> Do we find patterns that help the user to understand why certain variants are hard to count? + +# input: completed_prefiltered_gatk path, output folder path +# output: two logdiff-plots (counts_per_cov): 1st lineplot 2nd dotplot showing type of variant (varying bases - 1/2/3) + +library(dplyr) +library(ggplot2) +library(scales) + + logdiff_plot <- function(prefiltered_gatk_path) { + + # Load the data + prefiltered_gatk <- read.table(prefiltered_gatk_path, sep = ",", fill = NA, header = TRUE) + + # Sort by counts_per_cov while keeping corresponding varying_bases + sorted_counts <- prefiltered_gatk %>% + arrange(counts_per_cov) # Sort by counts_per_cov + + # Create a new column for the sorted index (1, 2, 3, ...) + sorted_counts\$ids <- 1:nrow(sorted_counts) + + # Calculate Q1 (10% quantile) and Q3 (90% quantile) + Q1 <- quantile(sorted_counts\$counts_per_cov, 0.10, na.rm = TRUE) + Q3 <- quantile(sorted_counts\$counts_per_cov, 0.90, na.rm = TRUE) + + # Calculate the LogDiff + LogDiff <- log10(Q3) - log10(Q1) + + # Create the first plot with a line + line_plot <- ggplot(sorted_counts, aes(x = ids, y = counts_per_cov)) + + geom_line(color = "black") + + + # Set axis labels + xlab("Variants") + + ylab("Counts per Coverage") + + + # Apply logarithmic scale to the y-axis + scale_y_continuous(trans = 'log10') + + + # Add horizontal dotted lines at Q1 and Q3 + geom_hline(yintercept = Q1, linetype = "dotted", color = "black") + + geom_hline(yintercept = Q3, linetype = "dotted", color = "black") + + + # Add the LogDiff value to the top left corner + annotate("text", x = 0, y = max(sorted_counts\$counts_per_cov), label = paste("LogDiff =", round(LogDiff, 2)), hjust = 0, vjust = 1, size = 5, color = "black") + + + # Apply the minimal theme + theme_minimal() + + # Save the line plot as a PDF + ggsave(filename = "logdiff_plot.pdf", plot = line_plot, device = "pdf", width = 10, height = 6) + + # Create the second plot with colored dots based on varying_bases + colored_plot <- ggplot(sorted_counts, aes(x = ids, y = counts_per_cov, color = as.factor(varying_bases))) + + + # Add horizontal dotted lines at Q1 and Q3 + geom_hline(yintercept = Q1, linetype = "dotted", color = "black") + + geom_hline(yintercept = Q3, linetype = "dotted", color = "black") + + + geom_point(size = 0.9) + # Use points instead of lines + + # Set axis labels + xlab("Variants") + + ylab("Counts per Coverage") + + + # Apply logarithmic scale to the y-axis + scale_y_continuous(trans = 'log10') + + + # Add the LogDiff value to the top left corner + annotate("text", x = 1, y = max(sorted_counts\$counts_per_cov), label = paste("LogDiff =", round(LogDiff, 2)), hjust = 0, vjust = 1, size = 5, color = "black") + + + # Add color scale for varying_bases + scale_color_manual(values = c("1" = "chocolate", "2" = "darkolivegreen3", "3" = "deepskyblue1"), name = "Varying \\n Bases") + + + # Apply the minimal theme + theme_minimal() + + # Save the colored plot as a PDF + ggsave(filename = "logdiff_varying_bases.pdf", plot = colored_plot, device = "pdf", width = 10, height = 6) + } + +##### +# run function +##### +logdiff_plot( + prefiltered_gatk_path = "$library_completed_variantCounts" +) + +#### +# create versions.yml +#### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +dplyr_version <- as.character(packageVersion("dplyr")) +ggplot2_version <- as.character(packageVersion("ggplot2")) +scales_version <- as.character(packageVersion("scales")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(dplyr_version) == 0) dplyr_version <- "unknown" +if (length(ggplot2_version) == 0) ggplot2_version <- "unknown" +if (length(scales_version) == 0) scales_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-dplyr:', dplyr_version), + paste(' r-ggplot2:', ggplot2_version), + paste(' r-scales:', scales_version) + ), + f +) +close(f) diff --git a/modules/local/visualization/seqdepth/environment.yml b/modules/local/visualization/seqdepth/environment.yml new file mode 100644 index 0000000..197283a --- /dev/null +++ b/modules/local/visualization/seqdepth/environment.yml @@ -0,0 +1,15 @@ +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::bioconductor-biostrings=2.78.0 + - conda-forge::r-base=4.5.1 + - conda-forge::r-biocmanager=1.30.27 + - conda-forge::r-dplyr=1.1.4 + - conda-forge::r-ggplot2=4.0.2 + - conda-forge::r-reshape2=1.4.4 + - conda-forge::r-scales=1.4.0 + - conda-forge::r-stringr=1.6.0 + - conda-forge::r-tidyr=1.3.1 + - conda-forge::r-tidyverse=2.0.0 + - conda-forge::r-zoo=1.8_15 diff --git a/modules/local/visualization/seqdepth/main.nf b/modules/local/visualization/seqdepth/main.nf new file mode 100644 index 0000000..9d338c4 --- /dev/null +++ b/modules/local/visualization/seqdepth/main.nf @@ -0,0 +1,38 @@ +process VISUALIZATION_SEQDEPTH { + tag "$meta.id" + // Closed-form hypergeometric rarefaction - fast, so no longer a heavy process. + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/73/73a72ec77725aeb67678a74228938fdd6827b669d01a8c96951b1a8ef96eeb0f/data' + : 'community.wave.seqera.io/library/bioconductor-biostrings_bioconductor-mutscan_r-base_r-biocmanager_pruned:c65036d76406f342' }" + + input: + tuple val(meta), path(variantCounts_filtered_by_library) + path possible_mutations + val min_counts + + output: + tuple val(meta), path("SeqDepth.pdf"), emit: SeqDepth + tuple val(meta), path("seqdepth_curve.csv"), emit: curve + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'SeqDepth_simulation.R' + + stub: + """ + touch SeqDepth.pdf + touch seqdepth_curve.csv + cat <<-END_VERSIONS > versions.yml + "${task.process}": + r-base: "0.0.0" + r-ggplot2: "0.0.0" + END_VERSIONS + """ +} diff --git a/modules/local/visualization/seqdepth/meta.yml b/modules/local/visualization/seqdepth/meta.yml new file mode 100644 index 0000000..e3a35c6 --- /dev/null +++ b/modules/local/visualization/seqdepth/meta.yml @@ -0,0 +1,55 @@ +name: "visualization_seqdepth" +description: Performs a sequencing depth simulation to assess library coverage and saturation, helping to determine if the sequencing effort was sufficient to capture all intended variants. +keywords: + - deep mutational scanning + - dms + - visualization + - sequencing depth + - saturation +tools: + - "mutscan": + description: "R package for analysis of deep mutational scanning data" + homepage: "https://bioconductor.org/packages/release/bioc/html/mutscan.html" + documentation: "https://bioconductor.org/packages/release/bioc/manuals/mutscan/man/mutscan.pdf" + tool_dev_url: "https://github.com/csoneson/mutscan" + doi: "10.1186/s12859-023-05187-y" + licence: ["GPL-3.0-or-later"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - variantCounts_filtered_by_library: + type: file + description: CSV file containing variant counts filtered against the target library + pattern: "*.{csv}" + - - possible_mutations: + type: file + description: CSV file defining all theoretically possible mutations for the library design + pattern: "*.{csv}" + - - min_counts: + type: integer + description: Minimum count threshold used in the simulation logic + +output: + - SeqDepth: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - SeqDepth.pdf: + type: file + description: PDF plot showing the simulation of variant detection as a function of sequencing depth + pattern: "SeqDepth.pdf" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/modules/local/visualization/seqdepth/templates/SeqDepth_simulation.R b/modules/local/visualization/seqdepth/templates/SeqDepth_simulation.R new file mode 100644 index 0000000..41add3f --- /dev/null +++ b/modules/local/visualization/seqdepth/templates/SeqDepth_simulation.R @@ -0,0 +1,125 @@ +#!/usr/bin/env Rscript + +# Sequencing-depth rarefaction: what fraction of the possible library variants stay detectable +# (>= `threshold` reads) as the sequencing depth is subsampled from full down to zero. +# +# This is a closed-form rarefaction, not a stochastic simulation. Subsampling reads without +# replacement makes each variant's surviving read count hypergeometric, so the expected number of +# variants still at or above the threshold at depth n is a sum of hypergeometric tail probabilities: +# +# E[detected | n reads] = sum_i P(Hypergeom(N, c_i, n) >= threshold) +# = sum_i phyper(threshold - 1, c_i, N - c_i, n, lower.tail = FALSE) +# +# where N is the total read count and c_i the reads on variant i. It is exact (no Monte-Carlo noise), +# needs no random sampling, and is orders of magnitude faster than draw-one-read-at-a-time loops - so +# the step count can be fine without the runtime blowing up. Being exact at every depth, a finer grid +# only resolves the curve better; it cannot change it. +# +# Depth grid: the step size grows by a constant factor from 0.01% at the bottom to 5% at the top +# (0.95 -> 1). Each step is then a fixed fraction (~5%) of the depth it starts from, so the steep +# low-depth rise is resolved as finely as the flat top, whatever depth a given library saturates at. +# With these defaults that is 123 steps. + +library(ggplot2) + +# 0, then the cumulative sum of K geometrically growing steps that ends at exactly 1 with a final step +# of `last_step`. K comes from the closed-form ratio (1 - first_step) / (1 - last_step), rounded up to +# a whole number of steps; the ratio is then re-solved for that K so both ends stay exact. +depth_grid <- function(first_step = 1e-4, last_step = 0.05) { + K <- ceiling(1 + log(last_step / first_step) / log((1 - first_step) / (1 - last_step))) + r <- uniroot(function(r) last_step * (1 - r^-K) / (1 - 1 / r) - 1, + c(1 + 1e-9, 2), tol = 1e-12)\$root + c(0, cumsum(last_step * r^-((K - 1):0))) +} + +SeqDepth_simulation_plot <- function(prefiltered_gatk_path, possible_mutations_path, + output_file_path, curve_csv_path, + first_step = 1e-4, last_step = 0.05, + threshold = 3) { + + data <- read.csv(prefiltered_gatk_path) + counts <- as.numeric(data\$counts) + counts <- counts[!is.na(counts) & counts > 0] # only observed variants carry reads + possible_mutations <- read.csv(possible_mutations_path) + total_possible <- nrow(possible_mutations) # denominator: the whole designed library + + N <- sum(counts) + if (N <= 0 || total_possible <= 0) { + stop("SeqDepth: no counts or no possible mutations to rarefy") + } + + # Depth fractions from depth_grid(), as integer read totals. unique() drops the collisions that + # rounding produces at the bottom of very small libraries. + depths <- unique(round(depth_grid(first_step, last_step) * N)) + + # Expected detected variants at each depth. phyper is vectorised over the per-variant counts, so each + # depth is one vectorised call; `lower.tail = FALSE` gives P(X > threshold-1) = P(X >= threshold). + expected_detected <- vapply(depths, function(n) { + if (n <= 0) return(0) + sum(phyper(threshold - 1, counts, N - counts, n, lower.tail = FALSE)) + }, numeric(1)) + + curve <- data.frame( + depth_fold = round(depths / N, 6), # x: fraction of full sequencing depth + variants_pct = round(expected_detected / total_possible * 100, 4) # y: % of the possible library + ) + write.csv(curve, curve_csv_path, row.names = FALSE) + + plot <- ggplot(curve, aes(x = depth_fold, y = variants_pct)) + + geom_line(color = "black", linewidth = 0.4) + + geom_hline(yintercept = 100, linetype = "dotted", color = "black") + + scale_y_continuous( + labels = scales::percent_format(scale = 1), + limits = c(0, 100), + breaks = seq(0, 100, by = 5) + ) + + scale_x_continuous( + limits = c(0, 1), + breaks = seq(0, 1, length.out = 20), + labels = scales::number_format(accuracy = 0.01) + ) + + labs(x = "Fraction of sequencing depth", y = "Variants detected (% of possible library)") + + theme_minimal() + + theme( + panel.border = element_rect(color = "black", fill = NA), + panel.grid.major = element_line(linewidth = 0.2, linetype = "solid", color = "grey80"), + panel.grid.minor = element_blank(), + axis.text.x = element_text(angle = 45, hjust = 1) + ) + + ggsave(output_file_path, plot = plot, device = "pdf", width = 8, height = 6) +} + + +##### +# run function +##### +SeqDepth_simulation_plot( + prefiltered_gatk_path = "$variantCounts_filtered_by_library", + possible_mutations_path = "$possible_mutations", + output_file_path = "SeqDepth.pdf", + curve_csv_path = "seqdepth_curve.csv", + first_step = 1e-4, + last_step = 0.05, + threshold = $min_counts + ) + +##### +# create versions.yml +##### +r_version <- strsplit(version[['version.string']], ' ')[[1]][3] +ggplot2_version <- as.character(packageVersion("ggplot2")) + +if (is.null(r_version)) r_version <- "unknown" +if (length(ggplot2_version) == 0) ggplot2_version <- "unknown" + +f <- file("versions.yml", "w") +writeLines( + c( + '"${task.process}":', + paste(' r-base:', r_version), + paste(' r-ggplot2:', ggplot2_version) + ), + f +) +close(f) diff --git a/modules/local/visualization/summary_report/environment.yml b/modules/local/visualization/summary_report/environment.yml new file mode 100644 index 0000000..b4d8e52 --- /dev/null +++ b/modules/local/visualization/summary_report/environment.yml @@ -0,0 +1,8 @@ +name: visualization_summary_report +channels: + - conda-forge + - bioconda +dependencies: + - python=3.12.3 + - pandas=2.2.1 + - numpy=1.26.4 diff --git a/modules/local/visualization/summary_report/main.nf b/modules/local/visualization/summary_report/main.nf new file mode 100644 index 0000000..25acef6 --- /dev/null +++ b/modules/local/visualization/summary_report/main.nf @@ -0,0 +1,43 @@ +// Build the single, self-contained all-in-one HTML report for a run. +// +// Every artefact the report shows is passed through one generic pair of inputs: a base64-encoded +// JSON manifest and the matching staged files, in the same order. That keeps the process signature +// stable no matter which optional parts of the pipeline ran - a section simply disappears when its +// entries are absent from the manifest, so no channel juggling is needed for --fitness/--pdb/EC. +process VISUALIZATION_SUMMARY_REPORT { + tag "${meta.sample ?: meta.id}" + label 'process_single' + + conda "${moduleDir}/environment.yml" + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container + ? 'https://depot.galaxyproject.org/singularity/pandas:2.2.1' + : 'quay.io/biocontainers/pandas:2.2.1' }" + + input: + tuple val(meta), val(run_b64) // base64(JSON) run metadata (params, samples, versions) + val manifest_b64 // base64(JSON) [{kind,group,name}, ...] aligned with `assets` + path assets, stageAs: 'a*/*' // pdf/csv/tsv/html artefacts; numbered dirs avoid basename clashes + path template_html + path threedmol_js // 3Dmol.js library, inlined only when the run has a structure + + output: + tuple val(meta), path("deepmutscan_report.html"), emit: report + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + template 'build_summary_report.py' + + stub: + """ + touch deepmutscan_report.html + cat <<-END_VERSIONS > versions.yml + "${task.process}": + python: "0.0.0" + pandas: "0.0.0" + END_VERSIONS + """ +} diff --git a/modules/local/visualization/summary_report/meta.yml b/modules/local/visualization/summary_report/meta.yml new file mode 100644 index 0000000..0b23fca --- /dev/null +++ b/modules/local/visualization/summary_report/meta.yml @@ -0,0 +1,60 @@ +name: "visualization_summary_report" +description: Build the single self-contained all-in-one HTML report for a run (QC to fitness) +keywords: + - report + - html + - quality control + - deep mutational scanning +tools: + - "pandas": + description: "Powerful data structures for data analysis, time series, and statistics" + homepage: "https://pandas.pydata.org/" + licence: ["BSD-3-Clause"] + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', sample:'ORF1' ]` + - run_b64: + type: string + description: Base64-encoded JSON with run-level metadata (parameters, samples, versions). + - - manifest_b64: + type: string + description: | + Base64-encoded JSON array describing every staged asset as `{kind, group, name}`, + in the same order as `assets`. Base64 keeps the JSON free of quoting problems when it is + interpolated into the template. + - - assets: + type: file + description: | + Every artefact embedded in the report (plot PDFs, count tables, the fitness table and the + detail HTML reports). Staged into numbered directories because basenames repeat across + samples. + - - template_html: + type: file + description: The report's HTML shell; data, plots and assets are injected into it. + pattern: "*.html" + +output: + - report: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', sample:'ORF1' ]` + - "deepmutscan_report.html": + type: file + description: Self-contained all-in-one report; no sidecar files are needed to view or share it. + pattern: "deepmutscan_report.html" + - versions: + - "versions.yml": + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@BenjaminWehnert1008" +maintainers: + - "@BenjaminWehnert1008" diff --git a/modules/local/visualization/summary_report/templates/build_summary_report.py b/modules/local/visualization/summary_report/templates/build_summary_report.py new file mode 100644 index 0000000..8b817a5 --- /dev/null +++ b/modules/local/visualization/summary_report/templates/build_summary_report.py @@ -0,0 +1,785 @@ +#!/usr/bin/env python3 +"""Build the self-contained all-in-one HTML report for nf-core/deepmutscan. + +Nextflow ``template`` script for the VISUALIZATION_SUMMARY_REPORT module. Everything the report +shows arrives through one generic mechanism: a base64 JSON manifest describing each staged asset +(`kind|group|name`) plus the assets themselves, in the same order. Sections that have no entries +are simply omitted, so the same code serves runs with or without fitness / error correction / a PDB. + +Two kinds of payload end up in the HTML: + * data - count tables and the fitness table, aggregated down to what the plots actually need, + then rendered natively as interactive SVG in the browser. + * blobs - the original R-generated plot PDFs and the standalone detail reports, embedded as + base64 data URIs so the single file needs no sidecars to view, download or share. + +Note: this is a Nextflow template, so every backslash meant for Python is doubled and literal +dollar signs are escaped. +""" +import base64 +import json +import platform +import re +from pathlib import Path + +import numpy as np +import pandas as pd + +MUT = re.compile(r"^([A-Za-z*]+)(\\d+)([A-Za-z*]+)") +REP_RE = re.compile(r"^rescaled_fitness_rep(\\d+)\$", re.I) +IN_RE = re.compile(r"^input(\\d+)\$", re.I) +OUT_RE = re.compile(r"^output(\\d+)\$", re.I) + +# Reference data for the citation page. Every entry is transcribed from CITATIONS.md, which is the +# pipeline's own maintained list - the point is that the report and the repo can never drift apart, +# and that nothing here is invented. `pkgs` are the names these tools use in the run's versions.yml, +# which is what decides whether a tool actually ran and therefore appears. +CITATIONS = [ + {"id": "deepmutscan", "always": True, "pkgs": [], + "authors": "Wehnert B, et al.", "title": "nf-core/deepmutscan", + "journal": "", "year": "", "key": "deepmutscan", + "note": "bioRxiv preprint, coming soon."}, + {"id": "nfcore", "always": True, "pkgs": [], + "authors": "Ewels PA, Peltzer A, Fillinger S, Patel H, Alneberg J, Wilm A, Garcia MU, Di Tommaso P, Nahnsen S", + "title": "The nf-core framework for community-curated bioinformatics pipelines", + "journal": "Nature Biotechnology", "year": "2020", "volume": "38", "number": "3", "pages": "276--278", + "doi": "10.1038/s41587-020-0439-x", "key": "Ewels2020"}, + {"id": "nextflow", "always": True, "pkgs": ["nextflow"], + "authors": "Di Tommaso P, Chatzou M, Floden EW, Barja PP, Palumbo E, Notredame C", + "title": "Nextflow enables reproducible computational workflows", + "journal": "Nature Biotechnology", "year": "2017", "volume": "35", "number": "4", "pages": "316--319", + "doi": "10.1038/nbt.3820", "key": "DiTommaso2017"}, + {"id": "fastqc", "pkgs": ["fastqc"], + "authors": "Andrews S", "title": "FastQC: A Quality Control Tool for High Throughput Sequence Data", + "year": "2010", "url": "https://www.bioinformatics.babraham.ac.uk/projects/fastqc/", "key": "Andrews2010"}, + {"id": "multiqc", "pkgs": ["multiqc"], + "authors": "Ewels P, Magnusson M, Lundin S, K\\u00e4ller M", + "title": "MultiQC: summarize analysis results for multiple tools and samples in a single report", + "journal": "Bioinformatics", "year": "2016", "volume": "32", "number": "19", "pages": "3047--3048", + "doi": "10.1093/bioinformatics/btw354", "key": "Ewels2016"}, + {"id": "samtools", "pkgs": ["samtools"], + "authors": "Danecek P, Bonfield JK, Liddle J, Marshall J, Ohan V, Pollard MO, Whitwham A, Keane T, McCarthy SA, Davies RM, Li H", + "title": "Twelve years of SAMtools and BCFtools", + "journal": "GigaScience", "year": "2021", "volume": "10", "number": "2", "pages": "giab008", + "doi": "10.1093/gigascience/giab008", "key": "Danecek2021"}, + # BWA-MEM, not BWA-backtrack: the pipeline runs `bwa mem`, whose citation is the 2013 arXiv + # preprint - the same one nf-core's own bwa/mem module points at. Li & Durbin 2009 describes + # `bwa aln`, a different algorithm this pipeline never calls. + {"id": "bwa", "pkgs": ["bwa"], + "authors": "Li H", "title": "Aligning sequence reads, clone sequences and assembly contigs with BWA-MEM", + "journal": "arXiv", "year": "2013", "eprint": "1303.3997", "primaryclass": "q-bio.GN", + "url": "https://arxiv.org/abs/1303.3997", "key": "Li2013"}, + {"id": "pysam", "pkgs": ["pysam"], + "authors": "Heger A, et al.", "title": "pysam: a Python module for reading, manipulating and writing genomic data sets", + "year": "", "url": "https://github.com/pysam-developers/pysam", "key": "pysam"}, + {"id": "polars", "pkgs": ["polars"], + "authors": "Vink R, et al.", "title": "Polars: Lightning-fast DataFrame library for Rust and Python", + "year": "", "url": "https://github.com/pola-rs/polars", "key": "polars"}, + {"id": "biopython", "pkgs": ["biopython"], + "authors": "Cock PJA, Antao T, Chang JT, Chapman BA, Cox CJ, Dalke A, Friedberg I, Hamelryck T, Kauff F, Wilczynski B, de Hoon MJL", + "title": "Biopython: freely available Python tools for computational molecular biology and bioinformatics", + "journal": "Bioinformatics", "year": "2009", "volume": "25", "number": "11", "pages": "1422--1423", + "doi": "10.1093/bioinformatics/btp163", "key": "Cock2009"}, + {"id": "threedmol", "pkgs": ["3dmol"], + "authors": "Rego N, Koes D", "title": "3Dmol.js: molecular visualization with WebGL", + "journal": "Bioinformatics", "year": "2015", "volume": "31", "number": "8", "pages": "1322--1324", + "doi": "10.1093/bioinformatics/btu829", "key": "Rego2015", + "note": "Bundled in the interactive variant effect inspection tool (BSD-3-Clause)."}, + {"id": "dimsum", "pkgs": ["r-dimsum", "dimsum"], + "authors": "Faure AJ, Schmiedel JM, Baeza-Centurion P, Lehner B", + "title": "DiMSum: an error model and pipeline for analyzing deep mutational scanning data and diagnosing common experimental pathologies", + "journal": "Genome Biology", "year": "2020", "volume": "21", "number": "1", "pages": "207", + "doi": "10.1186/s13059-020-02091-3", "key": "Faure2020"}, + {"id": "mutscan", "pkgs": ["r-mutscan", "mutscan"], + "authors": "Soneson C, Bendel AM, Diss G, Stadler MB", + "title": "mutscan - a flexible R package for efficient end-to-end analysis of multiplexed assays of variant effect data", + "journal": "Genome Biology", "year": "2023", "volume": "24", "number": "1", "pages": "132", + "doi": "10.1186/s13059-023-02967-0", "key": "Soneson2023"}, +] + + +class args: + run = "$run_b64" + manifest = "$manifest_b64" + assets = "$assets".split() + template = "$template_html" + threedmol = "$threedmol_js" # 3Dmol.js, inlined only when a structure is shown + out_html = "deepmutscan_report.html" + + +def b64json(s): + return json.loads(base64.b64decode(s).decode("utf-8")) + + +def data_uri(path, mime): + return "data:" + mime + ";base64," + base64.b64encode(Path(path).read_bytes()).decode("ascii") + + +# Embedded reports run in a frame whose origin is opaque, and in an opaque origin `localStorage` and +# `document.cookie` do not return empty - they *throw* SecurityError. MultiQC v1.35 reads both while +# bootstrapping without a try/catch, so the exception unwinds its ready handler and the report is left +# frozen on its "Loading report.." placeholder with no plots. Handing it a memory-backed store that +# never throws is enough; the only thing lost is persistence of the reader's MultiQC preferences. +# This prepends to the file we embed and does not touch the published multiqc_report.html. +STORAGE_SHIM = """""" + +# A srcdoc frame resolves relative URLs against the PARENT document's URL, so an in-page link such as +# MultiQC's sidebar `href="#fastqc"` does not scroll the frame: it navigates the frame to +# deepmutscan_report.html#fastqc, i.e. loads the whole report inside itself, again on every click. +# Catch same-page links before the browser follows them and scroll to the target instead. Links that +# only toggle a widget (Bootstrap collapse/tab) keep their own handlers; they just lose the navigation. +ANCHOR_SHIM = """""" + +HEAD_RE = re.compile(r"]*>", re.I) + + +def html_for_frame(path): + """Read an embedded report and make it work inside a srcdoc frame.""" + text = Path(path).read_text(encoding="utf-8", errors="surrogateescape") + m = HEAD_RE.search(text) + if m: + text = text[: m.end()] + STORAGE_SHIM + ANCHOR_SHIM + text[m.end():] + return text + + +def r6(x): + return round(float(x), 6) if x is not None and x == x else None + + +def to_float(x): + try: + v = float(x) + return round(v, 6) if v == v else None + except (TypeError, ValueError): + return None + + +def wt_orf(fasta_path, reading_frame): + """Wildtype ORF nucleotides plus the reference offset they start at. + + --reading_frame is the 1-based inclusive range the pipeline uses everywhere else (e.g. + "352-1383"), converted to a 0-based half-open slice here. `start` is kept so nucleotide positions + can be reported in reference coordinates, which is what base_mut uses. + + Returns None rather than guessing whenever anything fails to line up: a wrong offset would + mislabel every nucleotide change while still looking entirely plausible. + """ + if not fasta_path or not reading_frame: + return None + try: + lo_s, hi_s = str(reading_frame).split("-") + lo, hi = int(lo_s), int(hi_s) + except (TypeError, ValueError): + return None + seq = "".join(l.strip() for l in Path(fasta_path).read_text().splitlines() + if l.strip() and not l.startswith(">")).upper() + if lo < 1 or hi > len(seq) or hi < lo: + return None + orf = seq[lo - 1:hi] + if len(orf) % 3: + return None + return {"seq": orf, "start": lo} + + +def parse_pos(s): + m = MUT.match(str(s)) + return (m.group(1), int(m.group(2)), m.group(3)) if m else (None, None, None) + + +def num(df, name): + return (pd.to_numeric(df[name], errors="coerce") if name in df.columns + else pd.Series([np.nan] * len(df))) + + +def per_position(path, orf_len): + """Aggregate a filtered-by-library table to per-ORF-position series. + + Deliberately mirrors `global_position_biases_{counts,cov}.R` so the interactive curves and the + pipeline's published PDFs describe the same quantity: + + * coverage - a property of the position, so it is averaged over the substitutions measured + there (the R comment claims it is constant; on real data it varies by ~0.25% + because reads must span every varying base plus --min_flank, which is small + enough that the mean is the right summary). + * counts / counts_per_cov + - averaged *within* a varying-bases class and never pooled across classes. The + classes differ several-fold in both counts and frequency, and their mix changes + along the ORF, so a pooled curve would track composition rather than the library. + * R uses na.rm=FALSE for these per-position means, hence skipna=False here. + + Series run over every ORF position 1..orf_len, with None where a class has no variants, so that + the rolling mean in the browser sees the same gaps rollapply() does. + + `counts_raw` is only present once error correction has run, and drives the per-sample summary. + """ + df = pd.read_csv(path) + pos = df["pos_mut"].astype(str).map(lambda s: parse_pos(s)[1]) + cov, cnt = num(df, "cov"), num(df, "counts") + cpc, raw = num(df, "counts_per_cov"), num(df, "counts_raw") + vb = num(df, "varying_bases") + d = pd.DataFrame({"pos": pos, "cov": cov, "counts": cnt, "cpc": cpc, "raw": raw, "vb": vb}) + d = d.dropna(subset=["pos"]) + d["pos"] = d["pos"].astype(int) + + n = int(orf_len) if orf_len else (int(d["pos"].max()) if len(d) else 0) + axis = list(range(1, n + 1)) + + cov_by = d.groupby("pos")["cov"].mean(numeric_only=False) + positions = [{"pos": p, "cov": r6(cov_by.get(p))} for p in axis] + + classes = {} + for k in (1, 2, 3): + sub = d[d["vb"] == k] + cm = sub.groupby("pos")["counts"].apply(lambda s: s.mean(skipna=False)) + fm = sub.groupby("pos")["cpc"].apply(lambda s: s.mean(skipna=False)) + classes[str(k)] = [{"pos": p, "counts": r6(cm.get(p)), "cpc": r6(fm.get(p))} for p in axis] + + ec = None + if raw.notna().any(): + tr, tc = float(np.nansum(raw.values)), float(np.nansum(cnt.values)) + ec = { + "n_variants": int(len(d)), + "n_corrected": int((raw.fillna(0) != cnt.fillna(0)).sum()), + "total_raw": round(tr, 1), + "total_corrected": round(tc, 1), + "pct_removed": round((tr - tc) / tr * 100.0, 2) if tr > 0 else 0.0, + } + return positions, classes, ec + + +def heatmap(path): + """Counts-per-coverage grid (position x substituted amino acid) for one sample.""" + df = pd.read_csv(path) + if "position" not in df.columns or "mut_aa" not in df.columns: + return None + pos = pd.to_numeric(df["position"], errors="coerce") + val = num(df, "total_counts_per_cov") + cells = [{"p": int(p), "a": str(a), "v": r6(v)} + for p, a, v in zip(pos, df["mut_aa"].astype(str), val) + if p == p and v == v] + wt = {} + if "wt_aa" in df.columns: + for p, w in zip(pos, df["wt_aa"].astype(str)): + if p == p and w and w != "nan": + wt[int(p)] = w + return {"cells": cells, "wt": wt} + + +def codon_changes(nt_seq_field, pos, wt_nt): + """Nucleotide changes behind one amino-acid substitution. + + `nt_seq` holds the full ORF for every distinct coding variant that produces this amino-acid + change, comma-separated - an NNK library typically reaches one residue through several codons. + Each is diffed against the wildtype ORF over this residue's codon only: changes elsewhere belong + to other residues and would be double counted. Returns one entry per distinct codon. + + Reported positions are in REFERENCE coordinates, not ORF-relative ones, because that is the + convention `base_mut` uses everywhere else in the pipeline - checked against variantCounts, where + 4000/4000 variants agree with the reference convention and 0/4000 with the ORF-relative one. + Emitting ORF-relative numbers here would be off by the reading frame's start and would silently + disagree with the error-correction report. + """ + if not wt_nt or not wt_nt.get("seq") or not nt_seq_field: + return [] + seq_wt, rf_lo = wt_nt["seq"], wt_nt["start"] + lo = (int(pos) - 1) * 3 + if lo + 3 > len(seq_wt): + return [] + wt_codon = seq_wt[lo:lo + 3] + out, seen = [], set() + for seq in (s.strip() for s in str(nt_seq_field).split(",")): + if len(seq) < lo + 3: + continue + mut_codon = seq[lo:lo + 3] + if mut_codon == wt_codon or mut_codon in seen: + continue + seen.add(mut_codon) + changes, positions = [], [] + for k in range(3): + if wt_codon[k] != mut_codon[k]: + changes.append(f"{wt_codon[k]}>{mut_codon[k]}") + positions.append(rf_lo + lo + k) # reference coords, as base_mut reports them + out.append({"wt_codon": wt_codon, "mut_codon": mut_codon, + "changes": changes, "positions": positions}) + return out + + +def load_fitness(path, wt_nt=None): + """Per-substitution fitness plus its per-position mean, from the default fitness table.""" + df = pd.read_csv(path, sep="\\t", dtype=str, keep_default_na=False) + + def col(n): + return next((c for c in df.columns if c.strip().lower() == n), None) + + fc, sc = col("mean fitness"), col("fitness sd") + if not fc: + return None + # Replicate count is a per-run property, so discover the columns instead of assuming rep1/rep2. + rep_cols = sorted((c for c in df.columns if REP_RE.match(c.strip())), + key=lambda c: int(REP_RE.match(c.strip()).group(1))) + in_cols = [c for c in df.columns if IN_RE.match(c.strip())] + out_cols = [c for c in df.columns if OUT_RE.match(c.strip())] + ntc = col("nt_seq") + rows, per_pos = [], {} + for _, r in df.iterrows(): + try: + p = int(float(r.get("pos", "") or "nan")) + except (TypeError, ValueError): + continue + wt, mut = (r.get("wt_aa", "") or "").strip(), (r.get("mut_aa", "") or "").strip() + if not wt or not mut: + continue + try: + f = float(r.get(fc, "")) + except (TypeError, ValueError): + continue + if f != f: + continue + sd = None + try: + sd = float(r.get(sc, "")) if sc else None + if sd != sd: + sd = None + except (TypeError, ValueError): + sd = None + stop = mut == "*" or (r.get("stop", "").strip() not in ("", "0")) + + def tot(cols): + s = 0.0 + got = False + for c in cols: + try: + s += float(r.get(c, "") or 0) + got = True + except (TypeError, ValueError): + pass + return s if got else None + + cell = {"p": p, "wt": wt, "a": mut, "f": r6(f), "sd": r6(sd), + "syn": mut == wt, "stop": bool(stop)} + if rep_cols: + cell["reps"] = [to_float(r.get(c, "")) for c in rep_cols] + cell["n_in"], cell["n_out"] = tot(in_cols), tot(out_cols) + if ntc and wt_nt: + nt = codon_changes(r.get(ntc, ""), p, wt_nt) + if nt: + cell["nt"] = nt + rows.append(cell) + if mut != wt and not stop: + per_pos.setdefault(p, []).append(f) + return {"cells": rows} + + +THREE_TO_ONE = { + "ALA": "A", "ARG": "R", "ASN": "N", "ASP": "D", "CYS": "C", "GLN": "Q", "GLU": "E", + "GLY": "G", "HIS": "H", "ILE": "I", "LEU": "L", "LYS": "K", "MET": "M", "PHE": "F", + "PRO": "P", "SER": "S", "THR": "T", "TRP": "W", "TYR": "Y", "VAL": "V", +} + + +def parse_pdb(pdb_path): + """(pdb_text, residues) with residues = [{resnum, aa}] from the CA atoms, ordered by residue number.""" + text = Path(pdb_path).read_text() + residues, seen = [], set() + for line in text.splitlines(): + if not line.startswith("ATOM") or line[12:16].strip() != "CA": + continue + try: + resnum = int(line[22:26]) + except ValueError: + continue + if resnum in seen: + continue + seen.add(resnum) + residues.append({"resnum": resnum, "aa": THREE_TO_ONE.get(line[17:20].strip().upper(), "X")}) + residues.sort(key=lambda r: r["resnum"]) + return text, residues + + +def best_alignment(wt_seq, struct_residues): + """Offset mapping wt position j (0-based) to struct_residues[off0 + j], maximising identity. + + The PDB's residue numbering need not start at the ORF's first residue (AFDB models often carry a + signal peptide or a different frame), so the mapping is found by sliding the sequences, exactly as + the inspection tool does - the two must agree or the structure would be coloured off by a frame. + """ + struct_seq = "".join(r["aa"] for r in struct_residues) + n, m = len(wt_seq), len(struct_seq) + best_off, best_matches = 0, -1 + for off in range(-n + 1, m): + matches = sum(1 for j in range(n) if 0 <= off + j < m and struct_seq[off + j] == wt_seq[j]) + if matches > best_matches: + best_matches, best_off = matches, off + return best_off, best_matches / max(1, n) + + +def structure_data(pdb_path, wt_seq, fitness): + """PDB text plus per-residue mean fitness, keyed by PDB residue number, for the spinning viewer. + + The colour at a residue is the mean fitness over the missense substitutions measured there - the + same position summary the fitness heatmap collapses to - so the structure and the heatmap tell one + story. Returns None (viewer simply hidden) whenever a structure cannot be trusted: no PDB, no + fitness, or an alignment that never lines up. + """ + if not pdb_path or not wt_seq or not fitness or not fitness.get("cells"): + return None + pdb_text, residues = parse_pdb(pdb_path) + if not residues: + return None + off0, identity = best_alignment(wt_seq, residues) + by_pos = {} + for c in fitness["cells"]: + if c.get("syn") or c.get("stop") or c.get("f") is None: + continue + by_pos.setdefault(c["p"], []).append(c["f"]) + pos_mean = {p: sum(v) / len(v) for p, v in by_pos.items() if v} + resi_fit = {} + for j in range(len(wt_seq)): + k = off0 + j + if 0 <= k < len(residues) and (j + 1) in pos_mean: + resi_fit[residues[k]["resnum"]] = round(pos_mean[j + 1], 4) + if not resi_fit: + return None + fmax = max(abs(v) for v in resi_fit.values()) or 1.0 + return {"resi_fit": resi_fit, "fmax": round(fmax, 4), + "identity": round(identity, 4), "n_residues": len(residues), "pdb": pdb_text} + + +def _dur_s(s): + """Nextflow-formatted duration ('7m 37s', '746ms', '1h 2m') to seconds.""" + s = str(s).strip() + if not s or s == "-": + return 0.0 + tot = 0.0 + for num, unit in re.findall(r"([\\d.]+)\\s*(ms|s|m|h|d)", s): + tot += float(num) * {"ms": 0.001, "s": 1, "m": 60, "h": 3600, "d": 86400}[unit] + return tot + + +def _size_b(s): + """Nextflow-formatted size ('3 GB', '4.9 MB') to bytes.""" + m = re.match(r"([\\d.]+)\\s*(B|KB|MB|GB|TB)", str(s).strip(), re.I) + if not m: + return 0.0 + return float(m.group(1)) * {"B": 1, "KB": 1e3, "MB": 1e6, "GB": 1e9, "TB": 1e12}[m.group(2).upper()] + + +def run_stats(path): + """Per-process compute stats from Nextflow's execution trace. + + The trace is written continuously, so at report time it holds every task finished so far - i.e. + everything but the report itself and the version collation that follows it. That is the whole + compute-heavy body of the run, which is what the page is about; it is labelled 'as of report + generation' rather than pretended to be the final total. CACHED tasks are counted separately + because they consumed no compute this run. + """ + df = pd.read_csv(path, sep="\\t", dtype=str, keep_default_na=False) + if "name" not in df.columns: + return None + + def proc(n): + return re.sub(r"\\s*\\(.*\\)\\s*\$", "", n).split(":")[-1] # drop "(sample)" and the module path + + rows = {} + for _, r in df.iterrows(): + p = proc(r.get("name", "")) + d = rows.setdefault(p, {"n": 0, "cached": 0, "cpu_time": 0.0, "rss": 0.0, "cpu": [], "submit": ""}) + d["n"] += 1 + if r.get("status", "") == "CACHED": + d["cached"] += 1 + d["cpu_time"] += _dur_s(r.get("realtime", "")) + d["rss"] = max(d["rss"], _size_b(r.get("peak_rss", ""))) + # earliest submission time, so the table can read top-to-bottom in run order. The trace timestamp + # is "YYYY-MM-DD HH:MM:SS.mmm", which sorts lexically = chronologically, so no parsing is needed. + sub = r.get("submit", "") + if sub and (not d["submit"] or sub < d["submit"]): + d["submit"] = sub + c = r.get("%cpu", "").replace("%", "").strip() + try: + d["cpu"].append(float(c)) + except ValueError: + pass + procs = [{ + "process": p, "tasks": d["n"], "cached": d["cached"], + "cpu_time": round(d["cpu_time"], 1), "peak_rss": round(d["rss"]), + "cpu_pct": round(sum(d["cpu"]) / len(d["cpu"])) if d["cpu"] else None, + "submit": d["submit"], + } for p, d in rows.items()] + # chronological: first-submitted process first (falls back to name for any row with no timestamp) + procs.sort(key=lambda x: (x["submit"] or "~", x["process"])) + status = df["status"] if "status" in df.columns else pd.Series([], dtype=str) + total = { + "tasks": int(df.shape[0]), + "cached": int((status == "CACHED").sum()), + "cpu_time": round(sum(p["cpu_time"] for p in procs), 1), + "peak_rss": round(max((p["peak_rss"] for p in procs), default=0)), + } + return {"processes": procs, "total": total} + + +def software_versions(path): + """Parse pipeline_info's software versions YAML into {process: {package: version}}. + + Hand-parsed rather than via a YAML library: the file is two levels of trivial `key: value` and + pandas' container carries no yaml. Duplicate process keys are merged rather than overwritten - + they occur in practice when a module fails to interpolate its own name. + """ + out = {} + cur = None + for line in Path(path).read_text().splitlines(): + if not line.strip() or line.lstrip().startswith("#"): + continue + if not line.startswith((" ", "\\t")): + cur = line.strip().rstrip(":").strip('"') + out.setdefault(cur, {}) + elif cur: + k, _, v = line.strip().partition(":") + if k: + out[cur][k.strip()] = v.strip() + return out + + +def citations(versions, extra=()): + """The subset of CITATIONS whose tools actually ran, plus the pipeline and frameworks always. + + Package names in versions.yml are the primary signal, but they are not always sufficient: DiMSum, + for instance, reports only `r-base`, so it is invisible at the package level. `extra` carries the + tokens that a run's own flags settle unambiguously (--dimsum, --mutscan, a --pdb structure). A tool + outside both signals is never cited - the page is about this run, not the pipeline's full + repertoire. + """ + seen = {p.strip().lower() for pkgs in versions.values() for p in pkgs} + seen |= {e.lower() for e in extra} + out = [] + for c in CITATIONS: + if c.get("always") or any(p.lower() in seen for p in c["pkgs"]): + out.append({k: v for k, v in c.items() if k != "pkgs"}) + return out + + +def biblatex(entries): + """A biblatex source block for the given entries.""" + # The braces are assembled from these two names instead of being written literally: nf-core's + # `template_strings` lint reads any doubled-brace pair in the source as a leftover Jinja + # placeholder, and biblatex needs exactly such a pair around the title to protect its + # capitalisation. + ob, cb = "{", "}" + lines = [] + for c in entries: + kind = "@article" + if c.get("eprint"): + kind = "@misc" + elif not c.get("journal"): + kind = "@misc" + lines.append(kind + ob + c["key"] + ",") + lines.append(" author = " + ob + c["authors"].replace(", ", " and ") + cb + ",") + lines.append(" title = " + ob + ob + c["title"] + cb + cb + ",") + for fld, key in (("journaltitle", "journal"), ("year", "year"), ("volume", "volume"), + ("number", "number"), ("pages", "pages"), ("doi", "doi"), + ("eprint", "eprint"), ("eprintclass", "primaryclass"), ("url", "url")): + if c.get(key): + lines.append(f" {fld:<12} = " + ob + str(c[key]) + cb + ",") + if c.get("note"): + lines.append(" note = " + ob + c["note"] + cb + ",") + lines.append("}") + lines.append("") + return "\\n".join(lines) + + +def main(): + run = b64json(args.run) + manifest = b64json(args.manifest) + if len(manifest) != len(args.assets): + raise SystemExit(f"manifest has {len(manifest)} entries but {len(args.assets)} files were staged") + + samples, plots, reports, fitness, dl, versions = {}, {}, [], None, [], {} + + def sample(sid): + # type/replicate drive the input-vs-output colour palette and the seqdepth default selection. + # Pipeline ids are "___", so the type is a delimited token - matching it + # that way is robust for any protein name (which is a separate, earlier segment). + m = re.search(r"_(input|output|wildtype|quality)_(\\d+)", sid) + stype = m.group(1) if m else None + srep = int(m.group(2)) if m else None + return samples.setdefault(sid, {"id": sid, "type": stype, "replicate": srep, + "positions": [], "classes": {}, "heatmap": None, "ec": None, "seqdepth": None}) + + # The ORF length has to come from the wildtype protein sequence, not from the highest position that + # happens to carry a variant, or trailing residues with no library coverage would silently shorten + # the axis and shift every rolling window. + orf_len, wt_nt, wt_aa_seq, pdb_path = 0, None, None, None + for entry, path in zip(manifest, args.assets): + if entry["kind"] == "aa_seq": + wt_aa_seq = Path(path).read_text().strip().split("\\n")[0].strip() + orf_len = len(wt_aa_seq) + elif entry["kind"] == "fasta": + wt_nt = wt_orf(path, run.get("reading_frame")) + elif entry["kind"] == "pdb": + pdb_path = path + # A frame that disagrees with the protein length means the codon diffs would be off by a whole + # residue, so drop the nucleotide detail rather than print something plausible and wrong. + if wt_nt and orf_len and len(wt_nt["seq"]) // 3 not in (orf_len, orf_len + 1): + print(f"WARNING: ORF is {len(wt_nt['seq'])} nt ({len(wt_nt['seq'])//3} codons) but the protein is " + f"{orf_len} aa; skipping nucleotide-level detail") + wt_nt = None + + for entry, path in zip(manifest, args.assets): + kind, group, name = entry["kind"], entry["group"], entry["name"] + + if kind == "qc": # per-sample library-QC plot (original R PDF) + # Deliberately NOT embedded. The report re-plots these natively and offers the live chart + # as SVG/PNG, so carrying ~25 MB of base64 PDF bought nothing but weight - and the weight + # is not free: it is what stops the embedded MultiQC report from finishing its render. + # Only the location is kept, for the "open the results folder" links. + plots.setdefault(group, {})[Path(name).stem] = None + + elif kind == "filtered": # per-sample counts -> per-position curves + EC summary + positions, classes, ec = per_position(path, orf_len) + s = sample(group) + s["positions"], s["classes"], s["ec"] = positions, classes, ec + + elif kind == "heatmap": # per-sample counts-per-cov grid + sample(group)["heatmap"] = heatmap(path) + + elif kind == "seqdepth": # per-sample rarefaction curve (depth fraction -> % library) + sd = pd.read_csv(path) + sample(group)["seqdepth"] = [ + {"x": r6(x), "y": r6(y)} + for x, y in zip(num(sd, "depth_fold"), num(sd, "variants_pct")) + if x == x and y == y + ] + + elif kind == "fitness_table": + fitness = load_fitness(path, wt_nt) + + elif kind in ("fitness_pdf", "mutscan_pdf"): + # Same reasoning as the QC PDFs: recorded by name so the page can link to them in the + # results folder, not inlined. + dl.append({"section": "fitness", "group": group, "name": name}) + + elif kind == "report": # standalone detail report, embedded for a lazy iframe + # Carried as srcdoc text rather than a data: URI. srcdoc inherits this document's origin + # instead of minting an opaque one, and it is ~25% smaller than the base64 equivalent. + reports.append({"group": group, "name": name, "html": html_for_frame(path)}) + + elif kind == "versions": # pipeline_info software-versions YAML + versions = software_versions(path) + + elif kind == "trace": # Nextflow execution trace -> per-process compute stats + try: + run["stats"] = run_stats(path) + except Exception as exc: # a malformed/partial trace must never sink the whole report + print(f"WARNING: could not parse execution trace: {exc}") + + # Coverage needed for every residue outcome to reach --aimed_cov counts, assuming all 21 outcomes + # are equally represented. Same expression as the reference line in global_position_biases_cov.R; + # it is a per-position number because each read spans the whole ORF in this assay. + try: + run["required_cov"] = 21 * orf_len * float(run.get("aimed_cov") or 0) or None + except (TypeError, ValueError): + run["required_cov"] = None + run["orf_length"] = orf_len + + # Flags settle the tools that the version strings alone cannot: DiMSum reports only r-base, and + # 3Dmol.js has no package entry at all - the structure viewer's presence is the tell. + def truthy(x): + return str(x).strip().lower() in ("true", "1", "yes") + + extra = set() + if truthy(run.get("dimsum")): + extra.add("dimsum") + if truthy(run.get("mutscan")): + extra.add("mutscan") + if any(r["group"] == "viewer" for r in reports): + extra.add("3dmol") + if pdb_path: + extra.add("3dmol") + cites = citations(versions, extra) + # The spinning structure needs a PDB, fitness, and the wildtype protein sequence to align them; any + # missing and the viewer is simply left out (structure = None) so the page still renders. + structure = structure_data(pdb_path, wt_aa_seq, fitness) + data = { + "run": run, + "samples": [samples[k] for k in sorted(samples)], + "plots": plots, + "reports": reports, + "fitness": fitness, + "structure": structure, + "downloads": dl, + # Two citation groups for the page: the pipeline itself (always) and the tools that actually + # ran. Each carries its own biblatex block so the "get biblatex" button has nothing to compute. + # Section 1 is the pipeline and the frameworks it is built on; section 2 is the analysis tools + # this particular run actually invoked. + "citations": (lambda pipe: { + "pipeline": [c for c in cites if c["id"] in pipe], + "tools": [c for c in cites if c["id"] not in pipe], + "biblatex_pipeline": biblatex([c for c in cites if c["id"] in pipe]), + "biblatex_tools": biblatex([c for c in cites if c["id"] not in pipe]), + })({"deepmutscan", "nfcore", "nextflow"}), + "software": versions, + } + + # 3Dmol.js is ~0.5 MB, so inline it only when a structure will actually be shown; otherwise the + # placeholder collapses to nothing and a no-PDB run carries no dead weight. + threedmol_js = "" + if structure and args.threedmol: + try: + threedmol_js = Path(args.threedmol).read_text(encoding="utf-8", errors="surrogateescape") + except OSError: + threedmol_js = "" + + html = (Path(args.template).read_text(encoding="utf-8") + .replace("__REPORT_DATA__", json.dumps(data, separators=(",", ":")).replace("&1 | sed -n "s/^Version: //p"'), topic: versions, emit: versions_bwa + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${fasta.baseName}" + def args = task.ext.args ?: '' + """ + mkdir bwa + bwa \\ + index \\ + $args \\ + -p bwa/${prefix} \\ + $fasta + """ + + stub: + def prefix = task.ext.prefix ?: "${fasta.baseName}" + """ + mkdir bwa + touch bwa/${prefix}.amb + touch bwa/${prefix}.ann + touch bwa/${prefix}.bwt + touch bwa/${prefix}.pac + touch bwa/${prefix}.sa + """ +} diff --git a/modules/nf-core/bwa/index/meta.yml b/modules/nf-core/bwa/index/meta.yml new file mode 100644 index 0000000..f5bf7f5 --- /dev/null +++ b/modules/nf-core/bwa/index/meta.yml @@ -0,0 +1,71 @@ +name: bwa_index +description: Create BWA index for reference genome +keywords: + - index + - fasta + - genome + - reference +tools: + - bwa: + description: | + BWA is a software package for mapping DNA sequences against + a large reference genome, such as the human genome. + homepage: http://bio-bwa.sourceforge.net/ + documentation: https://bio-bwa.sourceforge.net/bwa.shtml + arxiv: arXiv:1303.3997 + licence: ["GPL-3.0-or-later"] + identifier: "biotools:bwa" +input: + - - meta: + type: map + description: | + Groovy Map containing reference information. + e.g. [ id:'test', single_end:false ] + - fasta: + type: file + description: Input genome fasta file + ontologies: + - edam: "http://edamontology.org/data_2044" # Sequence + - edam: "http://edamontology.org/format_1929" # FASTA +output: + index: + - - meta: + type: map + description: | + Groovy Map containing reference information. + e.g. [ id:'test', single_end:false ] + - bwa: + type: map + description: | + Groovy Map containing reference information. + e.g. [ id:'test', single_end:false ] + pattern: "*.{amb,ann,bwt,pac,sa}" + ontologies: + - edam: "http://edamontology.org/data_3210" # Genome index + versions_bwa: + - - ${task.process}: + type: string + description: The process the versions were collected from + - bwa: + type: string + description: The tool name + - 'bwa 2>&1 | sed -n "s/^Version: //p"': + type: string + description: The command used to generate the version of the tool +topics: + versions: + - - ${task.process}: + type: string + description: The process the versions were collected from + - bwa: + type: string + description: The tool name + - 'bwa 2>&1 | sed -n "s/^Version: //p"': + type: string + description: The command used to generate the version of the tool +authors: + - "@drpatelh" + - "@maxulysse" +maintainers: + - "@maxulysse" + - "@gallvp" diff --git a/modules/nf-core/bwa/index/tests/main.nf.test b/modules/nf-core/bwa/index/tests/main.nf.test new file mode 100644 index 0000000..f0fba82 --- /dev/null +++ b/modules/nf-core/bwa/index/tests/main.nf.test @@ -0,0 +1,57 @@ +nextflow_process { + + name "Test Process BWA_INDEX" + tag "modules_nfcore" + tag "modules" + tag "bwa" + tag "bwa/index" + script "../main.nf" + process "BWA_INDEX" + + test("BWA index") { + + when { + process { + """ + input[0] = [ + [id: 'test'], + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true) + ] + """ + } + } + + then { + assert process.success + assertAll( + { assert snapshot(process.out).match() } + ) + } + + } + + test("BWA index - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ + [id: 'test'], + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true) + ] + """ + } + } + + then { + assert process.success + assertAll( + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-core/bwa/index/tests/main.nf.test.snap b/modules/nf-core/bwa/index/tests/main.nf.test.snap new file mode 100644 index 0000000..21a6f73 --- /dev/null +++ b/modules/nf-core/bwa/index/tests/main.nf.test.snap @@ -0,0 +1,108 @@ +{ + "BWA index - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + [ + "genome.amb:md5,d41d8cd98f00b204e9800998ecf8427e", + "genome.ann:md5,d41d8cd98f00b204e9800998ecf8427e", + "genome.bwt:md5,d41d8cd98f00b204e9800998ecf8427e", + "genome.pac:md5,d41d8cd98f00b204e9800998ecf8427e", + "genome.sa:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "1": [ + [ + "BWA_INDEX", + "bwa", + "0.7.19-r1273" + ] + ], + "index": [ + [ + { + "id": "test" + }, + [ + "genome.amb:md5,d41d8cd98f00b204e9800998ecf8427e", + "genome.ann:md5,d41d8cd98f00b204e9800998ecf8427e", + "genome.bwt:md5,d41d8cd98f00b204e9800998ecf8427e", + "genome.pac:md5,d41d8cd98f00b204e9800998ecf8427e", + "genome.sa:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "versions_bwa": [ + [ + "BWA_INDEX", + "bwa", + "0.7.19-r1273" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.2" + }, + "timestamp": "2026-01-23T16:58:59.966558606" + }, + "BWA index": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + [ + "genome.amb:md5,3a68b8b2287e07dd3f5f95f4344ba76e", + "genome.ann:md5,c32e11f6c859f166c7525a9c1d583567", + "genome.bwt:md5,0469c30a1e239dd08f68afe66fde99da", + "genome.pac:md5,983e3d2cd6f36e2546e6d25a0da78d66", + "genome.sa:md5,ab3952cabf026b48cd3eb5bccbb636d1" + ] + ] + ], + "1": [ + [ + "BWA_INDEX", + "bwa", + "0.7.19-r1273" + ] + ], + "index": [ + [ + { + "id": "test" + }, + [ + "genome.amb:md5,3a68b8b2287e07dd3f5f95f4344ba76e", + "genome.ann:md5,c32e11f6c859f166c7525a9c1d583567", + "genome.bwt:md5,0469c30a1e239dd08f68afe66fde99da", + "genome.pac:md5,983e3d2cd6f36e2546e6d25a0da78d66", + "genome.sa:md5,ab3952cabf026b48cd3eb5bccbb636d1" + ] + ] + ], + "versions_bwa": [ + [ + "BWA_INDEX", + "bwa", + "0.7.19-r1273" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.2" + }, + "timestamp": "2026-01-23T16:58:53.330725134" + } +} \ No newline at end of file diff --git a/modules/nf-core/bwa/mem/environment.yml b/modules/nf-core/bwa/mem/environment.yml new file mode 100644 index 0000000..54e6794 --- /dev/null +++ b/modules/nf-core/bwa/mem/environment.yml @@ -0,0 +1,13 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda + +dependencies: + # renovate: datasource=conda depName=bioconda/bwa + - bioconda::bwa=0.7.19 + # renovate: datasource=conda depName=bioconda/htslib + - bioconda::htslib=1.22.1 + # renovate: datasource=conda depName=bioconda/samtools + - bioconda::samtools=1.22.1 diff --git a/modules/nf-core/bwa/mem/main.nf b/modules/nf-core/bwa/mem/main.nf new file mode 100644 index 0000000..bde6a9a --- /dev/null +++ b/modules/nf-core/bwa/mem/main.nf @@ -0,0 +1,73 @@ +process BWA_MEM { + tag "$meta.id" + label 'process_high' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/d7/d7e24dc1e4d93ca4d3a76a78d4c834a7be3985b0e1e56fddd61662e047863a8a/data' : + 'community.wave.seqera.io/library/bwa_htslib_samtools:83b50ff84ead50d0' }" + + input: + tuple val(meta) , path(reads) + tuple val(meta2), path(index) + tuple val(meta3), path(fasta) + val sort_bam + + output: + tuple val(meta), path("*.bam") , emit: bam, optional: true + tuple val(meta), path("*.cram") , emit: cram, optional: true + tuple val(meta), path("*.sam") , emit: sam, optional: true + tuple val(meta), path("*.csi") , emit: csi, optional: true + tuple val(meta), path("*.crai") , emit: crai, optional: true + tuple val("${task.process}"), val('bwa'), eval('bwa 2>&1 | sed -n "s/^Version: //p"'), topic: versions, emit: versions_bwa + tuple val("${task.process}"), val('samtools'), eval("samtools version | sed '1!d;s/.* //'"), topic: versions, emit: versions_samtools + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def args2 = task.ext.args2 ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + def samtools_command = sort_bam ? 'sort' : 'view' + def extension = args2.contains("--output-fmt sam") ? "sam" : + args2.contains("--output-fmt cram") ? "cram": + sort_bam && args2.contains("-O cram")? "cram": + !sort_bam && args2.contains("-C") ? "cram": + "bam" + def reference = fasta && extension=="cram" ? "--reference ${fasta}" : "" + if (!fasta && extension=="cram") error "Fasta reference is required for CRAM output" + // + // For SAM output we can skip samtools view + // + def pipe_command = "" + if (extension == "sam") { + pipe_command = "> ${prefix}.${extension}" + } else { + pipe_command = "| samtools $samtools_command $args2 ${reference} --threads $task.cpus -o ${prefix}.${extension} -" + } + """ + INDEX=`find -L ./ -name "*.amb" | sed 's/\\.amb\$//'` + + bwa mem \\ + $args \\ + -t $task.cpus \\ + \$INDEX \\ + $reads \\ + $pipe_command + """ + + stub: + def args2 = task.ext.args2 ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + def extension = args2.contains("--output-fmt sam") ? "sam" : + args2.contains("--output-fmt cram") ? "cram": + sort_bam && args2.contains("-O cram")? "cram": + !sort_bam && args2.contains("-C") ? "cram": + "bam" + """ + touch ${prefix}.${extension} + touch ${prefix}.csi + touch ${prefix}.crai + """ +} diff --git a/modules/nf-core/bwa/mem/meta.yml b/modules/nf-core/bwa/mem/meta.yml new file mode 100644 index 0000000..1c4ee88 --- /dev/null +++ b/modules/nf-core/bwa/mem/meta.yml @@ -0,0 +1,159 @@ +name: bwa_mem +description: Performs fastq alignment to a fasta reference using BWA +keywords: + - mem + - bwa + - alignment + - map + - fastq + - bam + - sam +tools: + - bwa: + description: | + BWA is a software package for mapping DNA sequences against + a large reference genome, such as the human genome. + homepage: http://bio-bwa.sourceforge.net/ + documentation: https://bio-bwa.sourceforge.net/bwa.shtml + arxiv: arXiv:1303.3997 + licence: + - "GPL-3.0-or-later" + identifier: "biotools:bwa" +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - reads: + type: file + description: | + List of input FastQ files of size 1 and 2 for single-end and paired-end data, + respectively. + ontologies: + - edam: "http://edamontology.org/data_2044" + - edam: "http://edamontology.org/format_1930" + - - meta2: + type: map + description: | + Groovy Map containing reference information. + e.g. [ id:'test', single_end:false ] + - index: + type: file + description: BWA genome index files + pattern: "Directory containing BWA index *.{amb,ann,bwt,pac,sa}" + ontologies: + - edam: "http://edamontology.org/data_3210" + - - meta3: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - fasta: + type: file + description: Reference genome in FASTA format + pattern: "*.{fasta,fa}" + ontologies: + - edam: "http://edamontology.org/data_2044" + - edam: "http://edamontology.org/format_1929" + - sort_bam: + type: boolean + description: use samtools sort (true) or samtools view (false) + pattern: "true or false" +output: + bam: + - - meta: + type: map + description: Groovy Map containing sample information + - "*.bam": + type: file + description: Output BAM file containing read alignments + pattern: "*.{bam}" + ontologies: + - edam: "http://edamontology.org/format_2572" + cram: + - - meta: + type: map + description: Groovy Map containing sample information + - "*.cram": + type: file + description: Output CRAM file containing read alignments + pattern: "*.{cram}" + ontologies: + - edam: "http://edamontology.org/format_3462" + sam: + - - meta: + type: map + description: Groovy Map containing sample information + - "*.sam": + type: file + description: Output SAM file containing read alignments + pattern: "*.{sam}" + ontologies: + - edam: "http://edamontology.org/format_2573" + csi: + - - meta: + type: map + description: Groovy Map containing sample information + - "*.csi": + type: file + description: Optional index file for BAM file + pattern: "*.{csi}" + ontologies: [] + crai: + - - meta: + type: map + description: Groovy Map containing sample information + - "*.crai": + type: file + description: Optional index file for CRAM file + pattern: "*.{crai}" + ontologies: [] + versions_bwa: + - - ${task.process}: + type: string + description: The name of the process + - bwa: + type: string + description: The name of the tool + - 'bwa 2>&1 | sed -n "s/^Version: //p"': + type: eval + description: The expression to obtain the version of the tool + versions_samtools: + - - ${task.process}: + type: string + description: The name of the process + - samtools: + type: string + description: The name of the tool + - samtools version | sed '1!d;s/.* //': + type: eval + description: The expression to obtain the version of the tool +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - bwa: + type: string + description: The name of the tool + - 'bwa 2>&1 | sed -n "s/^Version: //p"': + type: eval + description: The expression to obtain the version of the tool + - - ${task.process}: + type: string + description: The name of the process + - samtools: + type: string + description: The name of the tool + - samtools version | sed '1!d;s/.* //': + type: eval + description: The expression to obtain the version of the tool +authors: + - "@drpatelh" + - "@jeremy1805" + - "@matthdsm" +maintainers: + - "@drpatelh" + - "@jeremy1805" + - "@matthdsm" diff --git a/modules/nf-core/bwa/mem/tests/main.nf.test b/modules/nf-core/bwa/mem/tests/main.nf.test new file mode 100644 index 0000000..e284e2e --- /dev/null +++ b/modules/nf-core/bwa/mem/tests/main.nf.test @@ -0,0 +1,343 @@ +nextflow_process { + + name "Test Process BWA_MEM" + tag "modules_nfcore" + tag "modules" + tag "bwa" + tag "bwa/mem" + tag "bwa/index" + script "../main.nf" + process "BWA_MEM" + + setup { + run("BWA_INDEX") { + script "../../index/main.nf" + process { + """ + input[0] = [ + [id: 'test'], + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true) + ] + """ + } + } + } + + test("Single-End") { + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:true ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true) + ] + ] + input[1] = BWA_INDEX.out.index + input[2] = [[id: 'test'],file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true)] + input[3] = false + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.cram, + process.out.sam, + process.out.csi, + process.out.crai, + process.out.findAll { key, val -> key.startsWith("versions") }, + bam(process.out.bam[0][1]).getReadsMD5() + ).match() + } + ) + } + + } + + test("Single-End Sort") { + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:true ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true) + ] + ] + input[1] = BWA_INDEX.out.index + input[2] = [[id: 'test'],file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true)] + input[3] = true + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.cram, + process.out.sam, + process.out.csi, + process.out.crai, + process.out.findAll { key, val -> key.startsWith("versions") }, + bam(process.out.bam[0][1]).getReadsMD5() + ).match() + } + ) + } + + } + + test("Single-End - SAM output") { + + config "./nextflow_sam.config" + + when { + params { + module_args2 = "--output-fmt sam" + } + + process { + """ + input[0] = [ + [ id:'test', single_end:true ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true) + ] + ] + input[1] = BWA_INDEX.out.index + input[2] = [[id: 'test'],file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true)] + input[3] = false + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.cram, + process.out.csi, + process.out.crai, + process.out.findAll { key, val -> key.startsWith("versions") }, + bam(process.out.sam[0][1]).getReadsMD5() + ).match() + } + ) + } + + } + + test("Paired-End") { + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_2.fastq.gz', checkIfExists: true) + ] + ] + input[1] = BWA_INDEX.out.index + input[2] = [[id: 'test'],file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true)] + input[3] = false + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.cram, + process.out.sam, + process.out.csi, + process.out.crai, + process.out.findAll { key, val -> key.startsWith("versions") }, + bam(process.out.bam[0][1]).getReadsMD5() + ).match() + } + ) + } + + } + + test("Paired-End Sort") { + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_2.fastq.gz', checkIfExists: true) + ] + ] + input[1] = BWA_INDEX.out.index + input[2] = [[id: 'test'],file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true)] + input[3] = true + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.cram, + process.out.sam, + process.out.csi, + process.out.crai, + process.out.findAll { key, val -> key.startsWith("versions") }, + bam(process.out.bam[0][1]).getReadsMD5() + ).match() + } + ) + } + + } + + test("Paired-End - no fasta") { + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_2.fastq.gz', checkIfExists: true) + ] + ] + input[1] = BWA_INDEX.out.index + input[2] = [[:],[]] + input[3] = false + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.cram, + process.out.sam, + process.out.csi, + process.out.crai, + process.out.findAll { key, val -> key.startsWith("versions") }, + bam(process.out.bam[0][1]).getReadsMD5() + ).match() + } + ) + } + + } + + test ("Paired-end - SAM output") { + + config "./nextflow_sam.config" + + when { + params { + module_args2 = "--output-fmt sam" + } + + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_2.fastq.gz', checkIfExists: true) + ] + ] + input[1] = BWA_INDEX.out.index + input[2] = [[:],[]] + input[3] = false + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.cram, + process.out.csi, + process.out.crai, + process.out.findAll { key, val -> key.startsWith("versions") }, + bam(process.out.sam[0][1]).getReadsMD5() + ).match() + } + ) + } + + } + + test("Single-end - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:true ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true) + ] + ] + input[1] = BWA_INDEX.out.index + input[2] = [[id: 'test'],file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true)] + input[3] = false + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("Paired-end - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastq/test_2.fastq.gz', checkIfExists: true) + ] + ] + input[1] = BWA_INDEX.out.index + input[2] = [[id: 'test'],file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true)] + input[3] = false + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-core/bwa/mem/tests/main.nf.test.snap b/modules/nf-core/bwa/mem/tests/main.nf.test.snap new file mode 100644 index 0000000..6f9031d --- /dev/null +++ b/modules/nf-core/bwa/mem/tests/main.nf.test.snap @@ -0,0 +1,478 @@ +{ + "Single-End - SAM output": { + "content": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + { + "versions_bwa": [ + [ + "BWA_MEM", + "bwa", + "0.7.19-r1273" + ] + ], + "versions_samtools": [ + [ + "BWA_MEM", + "samtools", + "1.22.1" + ] + ] + }, + "798439cbd7fd81cbcc5078022dc5479d" + ], + "timestamp": "2026-05-11T12:09:32.334359515", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } + }, + "Single-End": { + "content": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + { + "versions_bwa": [ + [ + "BWA_MEM", + "bwa", + "0.7.19-r1273" + ] + ], + "versions_samtools": [ + [ + "BWA_MEM", + "samtools", + "1.22.1" + ] + ] + }, + "798439cbd7fd81cbcc5078022dc5479d" + ], + "timestamp": "2026-05-11T12:07:21.233636979", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } + }, + "Single-End Sort": { + "content": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + { + "versions_bwa": [ + [ + "BWA_MEM", + "bwa", + "0.7.19-r1273" + ] + ], + "versions_samtools": [ + [ + "BWA_MEM", + "samtools", + "1.22.1" + ] + ] + }, + "94fcf617f5b994584c4e8d4044e16b4f" + ], + "timestamp": "2026-05-11T12:07:28.74614221", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } + }, + "Paired-End": { + "content": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + { + "versions_bwa": [ + [ + "BWA_MEM", + "bwa", + "0.7.19-r1273" + ] + ], + "versions_samtools": [ + [ + "BWA_MEM", + "samtools", + "1.22.1" + ] + ] + }, + "57aeef88ed701a8ebc8e2f0a381b2a6" + ], + "timestamp": "2026-05-11T12:07:42.612131595", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } + }, + "Paired-End Sort": { + "content": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + { + "versions_bwa": [ + [ + "BWA_MEM", + "bwa", + "0.7.19-r1273" + ] + ], + "versions_samtools": [ + [ + "BWA_MEM", + "samtools", + "1.22.1" + ] + ] + }, + "af8628d9df18b2d3d4f6fd47ef2bb872" + ], + "timestamp": "2026-05-11T12:09:45.938323098", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } + }, + "Single-end - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": true + }, + "test.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + [ + { + "id": "test", + "single_end": true + }, + "test.csi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "4": [ + [ + { + "id": "test", + "single_end": true + }, + "test.crai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "5": [ + [ + "BWA_MEM", + "bwa", + "0.7.19-r1273" + ] + ], + "6": [ + [ + "BWA_MEM", + "samtools", + "1.22.1" + ] + ], + "bam": [ + [ + { + "id": "test", + "single_end": true + }, + "test.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "crai": [ + [ + { + "id": "test", + "single_end": true + }, + "test.crai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "cram": [ + + ], + "csi": [ + [ + { + "id": "test", + "single_end": true + }, + "test.csi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "sam": [ + + ], + "versions_bwa": [ + [ + "BWA_MEM", + "bwa", + "0.7.19-r1273" + ] + ], + "versions_samtools": [ + [ + "BWA_MEM", + "samtools", + "1.22.1" + ] + ] + } + ], + "timestamp": "2026-05-11T12:10:09.92486753", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } + }, + "Paired-End - no fasta": { + "content": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + { + "versions_bwa": [ + [ + "BWA_MEM", + "bwa", + "0.7.19-r1273" + ] + ], + "versions_samtools": [ + [ + "BWA_MEM", + "samtools", + "1.22.1" + ] + ] + }, + "57aeef88ed701a8ebc8e2f0a381b2a6" + ], + "timestamp": "2026-05-11T12:09:52.820539909", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } + }, + "Paired-end - SAM output": { + "content": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + { + "versions_bwa": [ + [ + "BWA_MEM", + "bwa", + "0.7.19-r1273" + ] + ], + "versions_samtools": [ + [ + "BWA_MEM", + "samtools", + "1.22.1" + ] + ] + }, + "57aeef88ed701a8ebc8e2f0a381b2a6" + ], + "timestamp": "2026-05-11T12:10:00.199968933", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } + }, + "Paired-end - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + [ + { + "id": "test", + "single_end": false + }, + "test.csi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "4": [ + [ + { + "id": "test", + "single_end": false + }, + "test.crai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "5": [ + [ + "BWA_MEM", + "bwa", + "0.7.19-r1273" + ] + ], + "6": [ + [ + "BWA_MEM", + "samtools", + "1.22.1" + ] + ], + "bam": [ + [ + { + "id": "test", + "single_end": false + }, + "test.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "crai": [ + [ + { + "id": "test", + "single_end": false + }, + "test.crai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "cram": [ + + ], + "csi": [ + [ + { + "id": "test", + "single_end": false + }, + "test.csi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "sam": [ + + ], + "versions_bwa": [ + [ + "BWA_MEM", + "bwa", + "0.7.19-r1273" + ] + ], + "versions_samtools": [ + [ + "BWA_MEM", + "samtools", + "1.22.1" + ] + ] + } + ], + "timestamp": "2026-05-11T12:10:16.940291647", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } + } +} \ No newline at end of file diff --git a/modules/nf-core/bwa/mem/tests/nextflow_sam.config b/modules/nf-core/bwa/mem/tests/nextflow_sam.config new file mode 100644 index 0000000..831332e --- /dev/null +++ b/modules/nf-core/bwa/mem/tests/nextflow_sam.config @@ -0,0 +1,5 @@ +process { + withName: BWA_MEM { + ext.args2 = params.module_args2 + } +} diff --git a/modules/nf-core/fastqc/.conda-lock/linux_amd64-bd-5cb1a2fa2f18c7c2_1.txt b/modules/nf-core/fastqc/.conda-lock/linux_amd64-bd-5cb1a2fa2f18c7c2_1.txt new file mode 100644 index 0000000..7770ccd --- /dev/null +++ b/modules/nf-core/fastqc/.conda-lock/linux_amd64-bd-5cb1a2fa2f18c7c2_1.txt @@ -0,0 +1,822 @@ + +version: 6 +environments: +default: +channels: +- url: https://conda.anaconda.org/conda-forge/ +- url: https://conda.anaconda.org/bioconda/ +- url: https://conda.anaconda.org/bioconda/ +options: +pypi-prerelease-mode: if-necessary-or-explicit +packages: +linux-64: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.15.3-hb03c661_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-he90730b_1.conda +- conda: https://conda.anaconda.org/bioconda/noarch/fastqc-0.12.1-hdfd78af_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.17.1-h27c8c51_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.2-hd590300_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-13.2.1-h6083320_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.18-h0c24ade_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-hecca717_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.4-h6548e54_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.2-hb03c661_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.55-h421ea60_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/openjdk-25.0.2-ha668962_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.32.1-7_hd590300_perl5.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/procps-ng-4.0.6-h18c060e_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.7-hb03c661_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.5-hb03c661_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda +build_number: 20 +sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 +md5: a9f577daf3de00bca7c3c76c0ecbd1de +depends: +- __glibc >=2.17,<3.0.a0 +- libgomp >=7.5.0 +constrains: +- openmp_impl <0.0a0 +license: BSD-3-Clause +license_family: BSD +size: 28948 +timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.15.3-hb03c661_0.conda +sha256: d88aa7ae766cf584e180996e92fef2aa7d8e0a0a5ab1d4d49c32390c1b5fff31 +md5: dcdc58c15961dbf17a0621312b01f5cb +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: LGPL-2.1-or-later +license_family: GPL +size: 584660 +timestamp: 1768327524772 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda +sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 +md5: d2ffd7602c02f2b316fd921d39876885 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: bzip2-1.0.6 +license_family: BSD +size: 260182 +timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda +sha256: 67cc7101b36421c5913a1687ef1b99f85b5d6868da3abbf6ec1a4181e79782fc +md5: 4492fd26db29495f0ba23f146cd5638d +depends: +- __unix +license: ISC +size: 147413 +timestamp: 1772006283803 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-he90730b_1.conda +sha256: 06525fa0c4e4f56e771a3b986d0fdf0f0fc5a3270830ee47e127a5105bde1b9a +md5: bb6c4808bfa69d6f7f6b07e5846ced37 +depends: +- __glibc >=2.17,<3.0.a0 +- fontconfig >=2.15.0,<3.0a0 +- fonts-conda-ecosystem +- icu >=78.1,<79.0a0 +- libexpat >=2.7.3,<3.0a0 +- libfreetype >=2.14.1 +- libfreetype6 >=2.14.1 +- libgcc >=14 +- libglib >=2.86.3,<3.0a0 +- libpng >=1.6.53,<1.7.0a0 +- libstdcxx >=14 +- libxcb >=1.17.0,<2.0a0 +- libzlib >=1.3.1,<2.0a0 +- pixman >=0.46.4,<1.0a0 +- xorg-libice >=1.1.2,<2.0a0 +- xorg-libsm >=1.2.6,<2.0a0 +- xorg-libx11 >=1.8.12,<2.0a0 +- xorg-libxext >=1.3.6,<2.0a0 +- xorg-libxrender >=0.9.12,<0.10.0a0 +license: LGPL-2.1-only or MPL-1.1 +size: 989514 +timestamp: 1766415934926 +- conda: https://conda.anaconda.org/bioconda/noarch/fastqc-0.12.1-hdfd78af_0.tar.bz2 +sha256: 7cc26225d590540ae95cd24940ff42f2da7479dd4cd22ae9ab9298665d06790c +md5: c9f6a4b12229f7331f79c9a00dd6e240 +depends: +- font-ttf-dejavu-sans-mono +- fontconfig +- openjdk >=8.0.144 +- perl +license: GPL >=3 +size: 11664291 +timestamp: 1677946722445 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 +sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b +md5: 0c96522c6bdaed4b1566d11387caaf45 +license: BSD-3-Clause +license_family: BSD +size: 397370 +timestamp: 1566932522327 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 +sha256: c52a29fdac682c20d252facc50f01e7c2e7ceac52aa9817aaf0bb83f7559ec5c +md5: 34893075a5c9e55cdafac56607368fc6 +license: OFL-1.1 +license_family: Other +size: 96530 +timestamp: 1620479909603 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 +sha256: 00925c8c055a2275614b4d983e1df637245e19058d79fc7dd1a93b8d9fb4b139 +md5: 4d59c254e01d9cde7957100457e2d5fb +license: OFL-1.1 +license_family: Other +size: 700814 +timestamp: 1620479612257 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda +sha256: 2821ec1dc454bd8b9a31d0ed22a7ce22422c0aef163c59f49dfdf915d0f0ca14 +md5: 49023d73832ef61042f6a237cb2687e7 +license: LicenseRef-Ubuntu-Font-Licence-Version-1.0 +license_family: Other +size: 1620504 +timestamp: 1727511233259 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.17.1-h27c8c51_0.conda +sha256: aa4a44dba97151221100a637c7f4bde619567afade9c0265f8e1c8eed8d7bd8c +md5: 867127763fbe935bab59815b6e0b7b5c +depends: +- __glibc >=2.17,<3.0.a0 +- libexpat >=2.7.4,<3.0a0 +- libfreetype >=2.14.1 +- libfreetype6 >=2.14.1 +- libgcc >=14 +- libuuid >=2.41.3,<3.0a0 +- libzlib >=1.3.1,<2.0a0 +license: MIT +license_family: MIT +size: 270705 +timestamp: 1771382710863 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 +sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 +md5: fee5683a3f04bd15cbd8318b096a27ab +depends: +- fonts-conda-forge +license: BSD-3-Clause +license_family: BSD +size: 3667 +timestamp: 1566974674465 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda +sha256: 54eea8469786bc2291cc40bca5f46438d3e062a399e8f53f013b6a9f50e98333 +md5: a7970cd949a077b7cb9696379d338681 +depends: +- font-ttf-ubuntu +- font-ttf-inconsolata +- font-ttf-dejavu-sans-mono +- font-ttf-source-code-pro +license: BSD-3-Clause +license_family: BSD +size: 4059 +timestamp: 1762351264405 +- conda: https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.2-hd590300_0.conda +sha256: aac402a8298f0c0cc528664249170372ef6b37ac39fdc92b40601a6aed1e32ff +md5: 3bf7b9fd5a7136126e0234db4b87c8b6 +depends: +- libgcc-ng >=12 +license: MIT +license_family: MIT +size: 77248 +timestamp: 1712692454246 +- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda +sha256: 25ba37da5c39697a77fce2c9a15e48cf0a84f1464ad2aafbe53d8357a9f6cc8c +md5: 2cd94587f3a401ae05e03a6caf09539d +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libstdcxx >=14 +license: LGPL-2.0-or-later +license_family: LGPL +size: 99596 +timestamp: 1755102025473 +- conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-13.2.1-h6083320_0.conda +sha256: 477f2c553f72165020d3c56740ba354be916c2f0b76fd9f535e83d698277d5ec +md5: 14470902326beee192e33719a2e8bb7f +depends: +- __glibc >=2.17,<3.0.a0 +- cairo >=1.18.4,<2.0a0 +- graphite2 >=1.3.14,<2.0a0 +- icu >=78.3,<79.0a0 +- libexpat >=2.7.4,<3.0a0 +- libfreetype >=2.14.2 +- libfreetype6 >=2.14.2 +- libgcc >=14 +- libglib >=2.86.4,<3.0a0 +- libstdcxx >=14 +- libzlib >=1.3.2,<2.0a0 +license: MIT +license_family: MIT +size: 2384060 +timestamp: 1774276284520 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda +sha256: fbf86c4a59c2ed05bbffb2ba25c7ed94f6185ec30ecb691615d42342baa1a16a +md5: c80d8a3b84358cb967fa81e7075fbc8a +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libstdcxx >=14 +license: MIT +license_family: MIT +size: 12723451 +timestamp: 1773822285671 +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda +sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 +md5: b38117a3c920364aff79f870c984b4a3 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +license: LGPL-2.1-or-later +size: 134088 +timestamp: 1754905959823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda +sha256: 3e307628ca3527448dd1cb14ad7bb9d04d1d28c7d4c5f97ba196ae984571dd25 +md5: fb53fb07ce46a575c5d004bbc96032c2 +depends: +- __glibc >=2.17,<3.0.a0 +- keyutils >=1.6.3,<2.0a0 +- libedit >=3.1.20250104,<3.2.0a0 +- libedit >=3.1.20250104,<4.0a0 +- libgcc >=14 +- libstdcxx >=14 +- openssl >=3.5.5,<4.0a0 +license: MIT +license_family: MIT +size: 1386730 +timestamp: 1769769569681 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.18-h0c24ade_0.conda +sha256: 836ec4b895352110335b9fdcfa83a8dcdbe6c5fb7c06c4929130600caea91c0a +md5: 6f2e2c8f58160147c4d1c6f4c14cbac4 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libjpeg-turbo >=3.1.2,<4.0a0 +- libtiff >=4.7.1,<4.8.0a0 +license: MIT +license_family: MIT +size: 249959 +timestamp: 1768184673131 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda +sha256: f84cb54782f7e9cea95e810ea8fef186e0652d0fa73d3009914fa2c1262594e1 +md5: a752488c68f2e7c456bcbd8f16eec275 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libstdcxx >=14 +license: Apache-2.0 +license_family: Apache +size: 261513 +timestamp: 1773113328888 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda +sha256: 205c4f19550f3647832ec44e35e6d93c8c206782bdd620c1d7cf66237580ff9c +md5: 49c553b47ff679a6a1e9fc80b9c5a2d4 +depends: +- __glibc >=2.17,<3.0.a0 +- krb5 >=1.22.2,<1.23.0a0 +- libgcc >=14 +- libstdcxx >=14 +- libzlib >=1.3.1,<2.0a0 +license: Apache-2.0 +license_family: Apache +size: 4518030 +timestamp: 1770902209173 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda +sha256: aa8e8c4be9a2e81610ddf574e05b64ee131fab5e0e3693210c9d6d2fba32c680 +md5: 6c77a605a7a689d17d4819c0f8ac9a00 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: MIT +license_family: MIT +size: 73490 +timestamp: 1761979956660 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda +sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 +md5: c277e0a4d549b03ac1e9d6cbbe3d017b +depends: +- ncurses +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +- ncurses >=6.5,<7.0a0 +license: BSD-2-Clause +license_family: BSD +size: 134676 +timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-hecca717_0.conda +sha256: d78f1d3bea8c031d2f032b760f36676d87929b18146351c4464c66b0869df3f5 +md5: e7f7ce06ec24cfcfb9e36d28cf82ba57 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +constrains: +- expat 2.7.4.* +license: MIT +license_family: MIT +size: 76798 +timestamp: 1771259418166 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda +sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 +md5: a360c33a5abe61c07959e449fa1453eb +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: MIT +license_family: MIT +size: 58592 +timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda +sha256: 38f014a7129e644636e46064ecd6b1945e729c2140e21d75bb476af39e692db2 +md5: e289f3d17880e44b633ba911d57a321b +depends: +- libfreetype6 >=2.14.3 +license: GPL-2.0-only OR FTL +size: 8049 +timestamp: 1774298163029 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda +sha256: 16f020f96da79db1863fcdd8f2b8f4f7d52f177dd4c58601e38e9182e91adf1d +md5: fb16b4b69e3f1dcfe79d80db8fd0c55d +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libpng >=1.6.55,<1.7.0a0 +- libzlib >=1.3.2,<2.0a0 +constrains: +- freetype >=2.14.3 +license: GPL-2.0-only OR FTL +size: 384575 +timestamp: 1774298162622 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda +sha256: faf7d2017b4d718951e3a59d081eb09759152f93038479b768e3d612688f83f5 +md5: 0aa00f03f9e39fb9876085dee11a85d4 +depends: +- __glibc >=2.17,<3.0.a0 +- _openmp_mutex >=4.5 +constrains: +- libgcc-ng ==15.2.0=*_18 +- libgomp 15.2.0 he0feb66_18 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 1041788 +timestamp: 1771378212382 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda +sha256: e318a711400f536c81123e753d4c797a821021fb38970cebfb3f454126016893 +md5: d5e96b1ed75ca01906b3d2469b4ce493 +depends: +- libgcc 15.2.0 he0feb66_18 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 27526 +timestamp: 1771378224552 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.4-h6548e54_1.conda +sha256: a27e44168a1240b15659888ce0d9b938ed4bdb49e9ea68a7c1ff27bcea8b55ce +md5: bb26456332b07f68bf3b7622ed71c0da +depends: +- __glibc >=2.17,<3.0.a0 +- libffi >=3.5.2,<3.6.0a0 +- libgcc >=14 +- libiconv >=1.18,<2.0a0 +- libzlib >=1.3.1,<2.0a0 +- pcre2 >=10.47,<10.48.0a0 +constrains: +- glib 2.86.4 *_1 +license: LGPL-2.1-or-later +size: 4398701 +timestamp: 1771863239578 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda +sha256: 21337ab58e5e0649d869ab168d4e609b033509de22521de1bfed0c031bfc5110 +md5: 239c5e9546c38a1e884d69effcf4c882 +depends: +- __glibc >=2.17,<3.0.a0 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 603262 +timestamp: 1771378117851 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda +sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f +md5: 915f5995e94f60e9a4826e0b0920ee88 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: LGPL-2.1-only +size: 790176 +timestamp: 1754908768807 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.2-hb03c661_0.conda +sha256: cc9aba923eea0af8e30e0f94f2ad7156e2984d80d1e8e7fe6be5a1f257f0eb32 +md5: 8397539e3a0bbd1695584fb4f927485a +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +constrains: +- jpeg <0.0.0a +license: IJG AND BSD-3-Clause AND Zlib +size: 633710 +timestamp: 1762094827865 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda +sha256: 755c55ebab181d678c12e49cced893598f2bab22d582fbbf4d8b83c18be207eb +md5: c7c83eecbb72d88b940c249af56c8b17 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +constrains: +- xz 5.8.2.* +license: 0BSD +size: 113207 +timestamp: 1768752626120 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.55-h421ea60_0.conda +sha256: 36ade759122cdf0f16e2a2562a19746d96cf9c863ffaa812f2f5071ebbe9c03c +md5: 5f13ffc7d30ffec87864e678df9957b4 +depends: +- libgcc >=14 +- __glibc >=2.17,<3.0.a0 +- libzlib >=1.3.1,<2.0a0 +license: zlib-acknowledgement +size: 317669 +timestamp: 1770691470744 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda +sha256: 78668020064fdaa27e9ab65cd2997e2c837b564ab26ce3bf0e58a2ce1a525c6e +md5: 1b08cd684f34175e4514474793d44bcb +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc 15.2.0 he0feb66_18 +constrains: +- libstdcxx-ng ==15.2.0=*_18 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 5852330 +timestamp: 1771378262446 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda +sha256: e5f8c38625aa6d567809733ae04bb71c161a42e44a9fa8227abe61fa5c60ebe0 +md5: cd5a90476766d53e901500df9215e927 +depends: +- __glibc >=2.17,<3.0.a0 +- lerc >=4.0.0,<5.0a0 +- libdeflate >=1.25,<1.26.0a0 +- libgcc >=14 +- libjpeg-turbo >=3.1.0,<4.0a0 +- liblzma >=5.8.1,<6.0a0 +- libstdcxx >=14 +- libwebp-base >=1.6.0,<2.0a0 +- libzlib >=1.3.1,<2.0a0 +- zstd >=1.5.7,<1.6.0a0 +license: HPND +size: 435273 +timestamp: 1762022005702 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda +sha256: 1a7539cfa7df00714e8943e18de0b06cceef6778e420a5ee3a2a145773758aee +md5: db409b7c1720428638e7c0d509d3e1b5 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: BSD-3-Clause +license_family: BSD +size: 40311 +timestamp: 1766271528534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda +sha256: 3aed21ab28eddffdaf7f804f49be7a7d701e8f0e46c856d801270b470820a37b +md5: aea31d2e5b1091feca96fcfe945c3cf9 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +constrains: +- libwebp 1.6.0 +license: BSD-3-Clause +license_family: BSD +size: 429011 +timestamp: 1752159441324 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda +sha256: 666c0c431b23c6cec6e492840b176dde533d48b7e6fb8883f5071223433776aa +md5: 92ed62436b625154323d40d5f2f11dd7 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +- pthread-stubs +- xorg-libxau >=1.0.11,<2.0a0 +- xorg-libxdmcp +license: MIT +license_family: MIT +size: 395888 +timestamp: 1727278577118 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda +sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c +md5: 5aa797f8787fe7a17d1b0821485b5adc +depends: +- libgcc-ng >=12 +license: LGPL-2.1-or-later +size: 100393 +timestamp: 1702724383534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda +sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 +md5: d87ff7921124eccd67248aa483c23fec +depends: +- __glibc >=2.17,<3.0.a0 +constrains: +- zlib 1.3.2 *_2 +license: Zlib +license_family: Other +size: 63629 +timestamp: 1774072609062 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda +sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 +md5: 47e340acb35de30501a76c7c799c41d7 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +license: X11 AND BSD-3-Clause +size: 891641 +timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openjdk-25.0.2-ha668962_0.conda +sha256: 3825a4c84676a8a5cc23b397a2911e4efa4a805daf2af764153bd904e142ec41 +md5: a41092b0177362dbe5eb2a18501e86c0 +depends: +- xorg-libx11 +- xorg-libxext +- xorg-libxi +- xorg-libxrender +- xorg-libxtst +- libstdcxx >=14 +- libgcc >=14 +- __glibc >=2.17,<3.0.a0 +- libfreetype >=2.14.1 +- libfreetype6 >=2.14.1 +- xorg-libxrender >=0.9.12,<0.10.0a0 +- libjpeg-turbo >=3.1.2,<4.0a0 +- giflib >=5.2.2,<5.3.0a0 +- xorg-libxrandr >=1.5.5,<2.0a0 +- harfbuzz >=12.3.2 +- fontconfig >=2.17.1,<3.0a0 +- fonts-conda-ecosystem +- xorg-libxtst >=1.2.5,<2.0a0 +- xorg-libxi >=1.8.2,<2.0a0 +- lcms2 >=2.18,<3.0a0 +- alsa-lib >=1.2.15.3,<1.3.0a0 +- libpng >=1.6.55,<1.7.0a0 +- xorg-libxt >=1.3.1,<2.0a0 +- libzlib >=1.3.1,<2.0a0 +- xorg-libxext >=1.3.7,<2.0a0 +- xorg-libx11 >=1.8.13,<2.0a0 +- libcups >=2.3.3,<2.4.0a0 +license: GPL-2.0-or-later WITH Classpath-exception-2.0 +license_family: GPL +size: 122465031 +timestamp: 1771443671180 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda +sha256: 44c877f8af015332a5d12f5ff0fb20ca32f896526a7d0cdb30c769df1144fb5c +md5: f61eb8cd60ff9057122a3d338b99c00f +depends: +- __glibc >=2.17,<3.0.a0 +- ca-certificates +- libgcc >=14 +license: Apache-2.0 +license_family: Apache +size: 3164551 +timestamp: 1769555830639 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda +sha256: 5e6f7d161356fefd981948bea5139c5aa0436767751a6930cb1ca801ebb113ff +md5: 7a3bff861a6583f1889021facefc08b1 +depends: +- __glibc >=2.17,<3.0.a0 +- bzip2 >=1.0.8,<2.0a0 +- libgcc >=14 +- libzlib >=1.3.1,<2.0a0 +license: BSD-3-Clause +license_family: BSD +size: 1222481 +timestamp: 1763655398280 +- conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.32.1-7_hd590300_perl5.conda +build_number: 7 +sha256: 9ec32b6936b0e37bcb0ed34f22ec3116e75b3c0964f9f50ecea5f58734ed6ce9 +md5: f2cfec9406850991f4e3d960cc9e3321 +depends: +- libgcc-ng >=12 +- libxcrypt >=4.4.36 +license: GPL-1.0-or-later OR Artistic-1.0-Perl +size: 13344463 +timestamp: 1703310653947 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda +sha256: 43d37bc9ca3b257c5dd7bf76a8426addbdec381f6786ff441dc90b1a49143b6a +md5: c01af13bdc553d1a8fbfff6e8db075f0 +depends: +- libgcc >=14 +- libstdcxx >=14 +- libgcc >=14 +- __glibc >=2.17,<3.0.a0 +license: MIT +license_family: MIT +size: 450960 +timestamp: 1754665235234 +- conda: https://conda.anaconda.org/conda-forge/linux-64/procps-ng-4.0.6-h18c060e_0.conda +sha256: 4ce2e1ee31a6217998f78c31ce7dc0a3e0557d9238b51d49dd20c52d467a126d +md5: f2c23a77b25efcad57d377b34bd84941 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- ncurses >=6.5,<7.0a0 +license: GPL-2.0-or-later AND LGPL-2.0-or-later +license_family: GPL +size: 593603 +timestamp: 1769710381284 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda +sha256: 9c88f8c64590e9567c6c80823f0328e58d3b1efb0e1c539c0315ceca764e0973 +md5: b3c17d95b5a10c6e64a21fa17573e70e +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +license: MIT +license_family: MIT +size: 8252 +timestamp: 1726802366959 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda +sha256: c12396aabb21244c212e488bbdc4abcdef0b7404b15761d9329f5a4a39113c4b +md5: fb901ff28063514abb6046c9ec2c4a45 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +license: MIT +license_family: MIT +size: 58628 +timestamp: 1734227592886 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda +sha256: 277841c43a39f738927145930ff963c5ce4c4dacf66637a3d95d802a64173250 +md5: 1c74ff8c35dcadf952a16f752ca5aa49 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +- libuuid >=2.38.1,<3.0a0 +- xorg-libice >=1.1.2,<2.0a0 +license: MIT +license_family: MIT +size: 27590 +timestamp: 1741896361728 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda +sha256: 516d4060139dbb4de49a4dcdc6317a9353fb39ebd47789c14e6fe52de0deee42 +md5: 861fb6ccbc677bb9a9fb2468430b9c6a +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libxcb >=1.17.0,<2.0a0 +license: MIT +license_family: MIT +size: 839652 +timestamp: 1770819209719 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda +sha256: 6bc6ab7a90a5d8ac94c7e300cc10beb0500eeba4b99822768ca2f2ef356f731b +md5: b2895afaf55bf96a8c8282a2e47a5de0 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: MIT +license_family: MIT +size: 15321 +timestamp: 1762976464266 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda +sha256: 25d255fb2eef929d21ff660a0c687d38a6d2ccfbcbf0cc6aa738b12af6e9d142 +md5: 1dafce8548e38671bea82e3f5c6ce22f +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: MIT +license_family: MIT +size: 20591 +timestamp: 1762976546182 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.7-hb03c661_0.conda +sha256: 79c60fc6acfd3d713d6340d3b4e296836a0f8c51602327b32794625826bd052f +md5: 34e54f03dfea3e7a2dcf1453a85f1085 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- xorg-libx11 >=1.8.12,<2.0a0 +license: MIT +license_family: MIT +size: 50326 +timestamp: 1769445253162 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda +sha256: 83c4c99d60b8784a611351220452a0a85b080668188dce5dfa394b723d7b64f4 +md5: ba231da7fccf9ea1e768caf5c7099b84 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- xorg-libx11 >=1.8.12,<2.0a0 +license: MIT +license_family: MIT +size: 20071 +timestamp: 1759282564045 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda +sha256: 1a724b47d98d7880f26da40e45f01728e7638e6ec69f35a3e11f92acd05f9e7a +md5: 17dcc85db3c7886650b8908b183d6876 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +- xorg-libx11 >=1.8.10,<2.0a0 +- xorg-libxext >=1.3.6,<2.0a0 +- xorg-libxfixes >=6.0.1,<7.0a0 +license: MIT +license_family: MIT +size: 47179 +timestamp: 1727799254088 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.5-hb03c661_0.conda +sha256: 80ed047a5cb30632c3dc5804c7716131d767089f65877813d4ae855ee5c9d343 +md5: e192019153591938acf7322b6459d36e +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- xorg-libx11 >=1.8.12,<2.0a0 +- xorg-libxext >=1.3.6,<2.0a0 +- xorg-libxrender >=0.9.12,<0.10.0a0 +license: MIT +license_family: MIT +size: 30456 +timestamp: 1769445263457 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda +sha256: 044c7b3153c224c6cedd4484dd91b389d2d7fd9c776ad0f4a34f099b3389f4a1 +md5: 96d57aba173e878a2089d5638016dc5e +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +- xorg-libx11 >=1.8.10,<2.0a0 +license: MIT +license_family: MIT +size: 33005 +timestamp: 1734229037766 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda +sha256: a8afba4a55b7b530eb5c8ad89737d60d60bc151a03fbef7a2182461256953f0e +md5: 279b0de5f6ba95457190a1c459a64e31 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +- xorg-libice >=1.1.1,<2.0a0 +- xorg-libsm >=1.2.4,<2.0a0 +- xorg-libx11 >=1.8.10,<2.0a0 +license: MIT +license_family: MIT +size: 379686 +timestamp: 1731860547604 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda +sha256: 752fdaac5d58ed863bbf685bb6f98092fe1a488ea8ebb7ed7b606ccfce08637a +md5: 7bbe9a0cc0df0ac5f5a8ad6d6a11af2f +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +- xorg-libx11 >=1.8.10,<2.0a0 +- xorg-libxext >=1.3.6,<2.0a0 +- xorg-libxi >=1.7.10,<2.0a0 +license: MIT +license_family: MIT +size: 32808 +timestamp: 1727964811275 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda +sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 +md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 +depends: +- __glibc >=2.17,<3.0.a0 +- libzlib >=1.3.1,<2.0a0 +license: BSD-3-Clause +license_family: BSD +size: 601375 +timestamp: 1764777111296 diff --git a/modules/nf-core/fastqc/.conda-lock/linux_arm64-bd-e455e32f745abe68_1.txt b/modules/nf-core/fastqc/.conda-lock/linux_arm64-bd-e455e32f745abe68_1.txt new file mode 100644 index 0000000..cdc434c --- /dev/null +++ b/modules/nf-core/fastqc/.conda-lock/linux_arm64-bd-e455e32f745abe68_1.txt @@ -0,0 +1,769 @@ + +version: 6 +environments: +default: +channels: +- url: https://conda.anaconda.org/conda-forge/ +- url: https://conda.anaconda.org/bioconda/ +- url: https://conda.anaconda.org/bioconda/ +options: +pypi-prerelease-mode: if-necessary-or-explicit +packages: +linux-aarch64: +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.15.3-he30d5cf_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.4-h0b6afd8_1.conda +- conda: https://conda.anaconda.org/bioconda/noarch/fastqc-0.12.1-hdfd78af_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.17.1-hba86a56_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/giflib-5.2.2-h31becfc_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.14-hfae3067_2.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-13.2.1-h1134a53_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.3-hcab7f73_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.22.2-hfd895c2_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lcms2-2.18-h9d5b58d_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.1.0-h52b7260_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcups-2.3.3-h4f2b762_6.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.4-hfae3067_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.3-h8af1aa0_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.3-hdae7a39_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_18.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_18.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.86.4-hf53f6bf_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_18.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.1.2-he30d5cf_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.2-he30d5cf_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.55-h1abf092_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_18.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.1-hdb009f0_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.3-h1022ec0_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openjdk-25.0.2-h488f50d_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.47-hf841c20_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/perl-5.32.1-7_h31becfc_perl5.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/procps-ng-4.0.6-h1779866_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.2-h86ecc28_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.6-h0808dbd_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.13-h63a1b12_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-he30d5cf_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-he30d5cf_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.7-he30d5cf_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.2-he30d5cf_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxi-1.8.2-h57736b2_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrandr-1.5.5-he30d5cf_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.12-h86ecc28_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxt-1.3.1-h57736b2_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxtst-1.2.5-h57736b2_3.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda +build_number: 20 +sha256: a2527b1d81792a0ccd2c05850960df119c2b6d8f5fdec97f2db7d25dc23b1068 +md5: 468fd3bb9e1f671d36c2cbc677e56f1d +depends: +- libgomp >=7.5.0 +constrains: +- openmp_impl <0.0a0 +license: BSD-3-Clause +license_family: BSD +size: 28926 +timestamp: 1770939656741 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.15.3-he30d5cf_0.conda +sha256: ea2233e2db9908c2e5f29d3ca420a546b4583253f4f70abb5494cdd676866d42 +md5: 4a98cbc4ade694520227402ff8880630 +depends: +- libgcc >=14 +license: LGPL-2.1-or-later +license_family: GPL +size: 615729 +timestamp: 1768327548407 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda +sha256: b3495077889dde6bb370938e7db82be545c73e8589696ad0843a32221520ad4c +md5: 840d8fc0d7b3209be93080bc20e07f2d +depends: +- libgcc >=14 +license: bzip2-1.0.6 +license_family: BSD +size: 192412 +timestamp: 1771350241232 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda +sha256: 67cc7101b36421c5913a1687ef1b99f85b5d6868da3abbf6ec1a4181e79782fc +md5: 4492fd26db29495f0ba23f146cd5638d +depends: +- __unix +license: ISC +size: 147413 +timestamp: 1772006283803 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.4-h0b6afd8_1.conda +sha256: 675db823f3d6fb6bf747fab3b0170ba99b269a07cf6df1e49fff2f9972be9cd1 +md5: 043c13ed3a18396994be9b4fab6572ad +depends: +- fontconfig >=2.15.0,<3.0a0 +- fonts-conda-ecosystem +- icu >=78.1,<79.0a0 +- libexpat >=2.7.3,<3.0a0 +- libfreetype >=2.14.1 +- libfreetype6 >=2.14.1 +- libgcc >=14 +- libglib >=2.86.3,<3.0a0 +- libpng >=1.6.53,<1.7.0a0 +- libstdcxx >=14 +- libxcb >=1.17.0,<2.0a0 +- libzlib >=1.3.1,<2.0a0 +- pixman >=0.46.4,<1.0a0 +- xorg-libice >=1.1.2,<2.0a0 +- xorg-libsm >=1.2.6,<2.0a0 +- xorg-libx11 >=1.8.12,<2.0a0 +- xorg-libxext >=1.3.6,<2.0a0 +- xorg-libxrender >=0.9.12,<0.10.0a0 +license: LGPL-2.1-only or MPL-1.1 +size: 927045 +timestamp: 1766416003626 +- conda: https://conda.anaconda.org/bioconda/noarch/fastqc-0.12.1-hdfd78af_0.tar.bz2 +sha256: 7cc26225d590540ae95cd24940ff42f2da7479dd4cd22ae9ab9298665d06790c +md5: c9f6a4b12229f7331f79c9a00dd6e240 +depends: +- font-ttf-dejavu-sans-mono +- fontconfig +- openjdk >=8.0.144 +- perl +license: GPL >=3 +size: 11664291 +timestamp: 1677946722445 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 +sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b +md5: 0c96522c6bdaed4b1566d11387caaf45 +license: BSD-3-Clause +license_family: BSD +size: 397370 +timestamp: 1566932522327 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 +sha256: c52a29fdac682c20d252facc50f01e7c2e7ceac52aa9817aaf0bb83f7559ec5c +md5: 34893075a5c9e55cdafac56607368fc6 +license: OFL-1.1 +license_family: Other +size: 96530 +timestamp: 1620479909603 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 +sha256: 00925c8c055a2275614b4d983e1df637245e19058d79fc7dd1a93b8d9fb4b139 +md5: 4d59c254e01d9cde7957100457e2d5fb +license: OFL-1.1 +license_family: Other +size: 700814 +timestamp: 1620479612257 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda +sha256: 2821ec1dc454bd8b9a31d0ed22a7ce22422c0aef163c59f49dfdf915d0f0ca14 +md5: 49023d73832ef61042f6a237cb2687e7 +license: LicenseRef-Ubuntu-Font-Licence-Version-1.0 +license_family: Other +size: 1620504 +timestamp: 1727511233259 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.17.1-hba86a56_0.conda +sha256: 835aff8615dd8d8fff377679710ce81b8a2c47b6404e21a92fb349fda193a15c +md5: 0fed1ff55f4938a65907f3ecf62609db +depends: +- libexpat >=2.7.4,<3.0a0 +- libfreetype >=2.14.1 +- libfreetype6 >=2.14.1 +- libgcc >=14 +- libuuid >=2.41.3,<3.0a0 +- libzlib >=1.3.1,<2.0a0 +license: MIT +license_family: MIT +size: 279044 +timestamp: 1771382728182 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 +sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 +md5: fee5683a3f04bd15cbd8318b096a27ab +depends: +- fonts-conda-forge +license: BSD-3-Clause +license_family: BSD +size: 3667 +timestamp: 1566974674465 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda +sha256: 54eea8469786bc2291cc40bca5f46438d3e062a399e8f53f013b6a9f50e98333 +md5: a7970cd949a077b7cb9696379d338681 +depends: +- font-ttf-ubuntu +- font-ttf-inconsolata +- font-ttf-dejavu-sans-mono +- font-ttf-source-code-pro +license: BSD-3-Clause +license_family: BSD +size: 4059 +timestamp: 1762351264405 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/giflib-5.2.2-h31becfc_0.conda +sha256: a79dc3bd54c4fb1f249942ee2d5b601a76ecf9614774a4cff9af49adfa458db2 +md5: 2f809afaf0ba1ea4135dce158169efac +depends: +- libgcc-ng >=12 +license: MIT +license_family: MIT +size: 82124 +timestamp: 1712692444545 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.14-hfae3067_2.conda +sha256: c9b1781fe329e0b77c5addd741e58600f50bef39321cae75eba72f2f381374b7 +md5: 4aa540e9541cc9d6581ab23ff2043f13 +depends: +- libgcc >=14 +- libstdcxx >=14 +license: LGPL-2.0-or-later +license_family: LGPL +size: 102400 +timestamp: 1755102000043 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-13.2.1-h1134a53_0.conda +sha256: e22f485fddaaea3ff4b6cae98e0197b9dccd2ed2770337ad6ff38a92afe04e59 +md5: 05d65a2cf410adc331c9ea61f59f1013 +depends: +- cairo >=1.18.4,<2.0a0 +- graphite2 >=1.3.14,<2.0a0 +- icu >=78.3,<79.0a0 +- libexpat >=2.7.4,<3.0a0 +- libfreetype >=2.14.2 +- libfreetype6 >=2.14.2 +- libgcc >=14 +- libglib >=2.86.4,<3.0a0 +- libstdcxx >=14 +- libzlib >=1.3.2,<2.0a0 +license: MIT +license_family: MIT +size: 2345732 +timestamp: 1774281448329 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.3-hcab7f73_0.conda +sha256: 49ba6aed2c6b482bb0ba41078057555d29764299bc947b990708617712ef6406 +md5: 546da38c2fa9efacf203e2ad3f987c59 +depends: +- libgcc >=14 +- libstdcxx >=14 +license: MIT +license_family: MIT +size: 12837286 +timestamp: 1773822650615 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda +sha256: 5ce830ca274b67de11a7075430a72020c1fb7d486161a82839be15c2b84e9988 +md5: e7df0aab10b9cbb73ab2a467ebfaf8c7 +depends: +- libgcc >=13 +license: LGPL-2.1-or-later +size: 129048 +timestamp: 1754906002667 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.22.2-hfd895c2_0.conda +sha256: b53999d888dda53c506b264e8c02b5f5c8e022c781eda0718f007339e6bc90ba +md5: d9ca108bd680ea86a963104b6b3e95ca +depends: +- keyutils >=1.6.3,<2.0a0 +- libedit >=3.1.20250104,<3.2.0a0 +- libedit >=3.1.20250104,<4.0a0 +- libgcc >=14 +- libstdcxx >=14 +- openssl >=3.5.5,<4.0a0 +license: MIT +license_family: MIT +size: 1517436 +timestamp: 1769773395215 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lcms2-2.18-h9d5b58d_0.conda +sha256: 379ef5e91a587137391a6149755d0e929f1a007d2dcb211318ac670a46c8596f +md5: bb960f01525b5e001608afef9d47b79c +depends: +- libgcc >=14 +- libjpeg-turbo >=3.1.2,<4.0a0 +- libtiff >=4.7.1,<4.8.0a0 +license: MIT +license_family: MIT +size: 293039 +timestamp: 1768184778398 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.1.0-h52b7260_0.conda +sha256: 8957fd460c1c132c8031f65fd5f56ec3807fd71b7cab2c5e2b0937b13404ab36 +md5: d13423b06447113a90b5b1366d4da171 +depends: +- libgcc >=14 +- libstdcxx >=14 +license: Apache-2.0 +license_family: Apache +size: 240444 +timestamp: 1773114901155 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcups-2.3.3-h4f2b762_6.conda +sha256: 41b04f995c9f63af8c4065a35931e46cbc2fdd6b9bf7e4c19f90d53cbb2bc8e5 +md5: 67828c963b17db7dc989fe5d509ef04a +depends: +- krb5 >=1.22.2,<1.23.0a0 +- libgcc >=14 +- libstdcxx >=14 +- libzlib >=1.3.1,<2.0a0 +license: Apache-2.0 +license_family: Apache +size: 4553739 +timestamp: 1770903929794 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda +sha256: 48814b73bd462da6eed2e697e30c060ae16af21e9fbed30d64feaf0aad9da392 +md5: a9138815598fe6b91a1d6782ca657b0c +depends: +- libgcc >=14 +license: MIT +license_family: MIT +size: 71117 +timestamp: 1761979776756 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda +sha256: c0b27546aa3a23d47919226b3a1635fccdb4f24b94e72e206a751b33f46fd8d6 +md5: fb640d776fc92b682a14e001980825b1 +depends: +- ncurses +- libgcc >=13 +- ncurses >=6.5,<7.0a0 +license: BSD-2-Clause +license_family: BSD +size: 148125 +timestamp: 1738479808948 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.4-hfae3067_0.conda +sha256: 995ce3ad96d0f4b5ed6296b051a0d7b6377718f325bc0e792fbb96b0e369dad7 +md5: 57f3b3da02a50a1be2a6fe847515417d +depends: +- libgcc >=14 +constrains: +- expat 2.7.4.* +license: MIT +license_family: MIT +size: 76564 +timestamp: 1771259530958 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda +sha256: 3df4c539449aabc3443bbe8c492c01d401eea894603087fca2917aa4e1c2dea9 +md5: 2f364feefb6a7c00423e80dcb12db62a +depends: +- libgcc >=14 +license: MIT +license_family: MIT +size: 55952 +timestamp: 1769456078358 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.3-h8af1aa0_0.conda +sha256: 752e4f66283d7deb4c6fd47d88df644d8daa2aaa825a54f3bf350a625190192a +md5: a229e22d4d8814a07702b0919d8e6701 +depends: +- libfreetype6 >=2.14.3 +license: GPL-2.0-only OR FTL +size: 8125 +timestamp: 1774301094057 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.3-hdae7a39_0.conda +sha256: 8e6b27fe4eec4c2fa7b7769a21973734c8dba1de80086fb0213e58375ac09f4c +md5: b99ed99e42dafb27889483b3098cace7 +depends: +- libgcc >=14 +- libpng >=1.6.55,<1.7.0a0 +- libzlib >=1.3.2,<2.0a0 +constrains: +- freetype >=2.14.3 +license: GPL-2.0-only OR FTL +size: 422941 +timestamp: 1774301093473 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_18.conda +sha256: 43df385bedc1cab11993c4369e1f3b04b4ca5d0ea16cba6a0e7f18dbc129fcc9 +md5: 552567ea2b61e3a3035759b2fdb3f9a6 +depends: +- _openmp_mutex >=4.5 +constrains: +- libgcc-ng ==15.2.0=*_18 +- libgomp 15.2.0 h8acb6b2_18 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 622900 +timestamp: 1771378128706 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_18.conda +sha256: 83bb0415f59634dccfa8335d4163d1f6db00a27b36666736f9842b650b92cf2f +md5: 4feebd0fbf61075a1a9c2e9b3936c257 +depends: +- libgcc 15.2.0 h8acb6b2_18 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 27568 +timestamp: 1771378136019 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.86.4-hf53f6bf_1.conda +sha256: afc503dbd04a5bf2709aa9d8318a03a8c4edb389f661ff280c3494bfef4341ec +md5: 4ac4372fc4d7f20630a91314cdac8afd +depends: +- libffi >=3.5.2,<3.6.0a0 +- libgcc >=14 +- libiconv >=1.18,<2.0a0 +- libzlib >=1.3.1,<2.0a0 +- pcre2 >=10.47,<10.48.0a0 +constrains: +- glib 2.86.4 *_1 +license: LGPL-2.1-or-later +size: 4512186 +timestamp: 1771863220969 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_18.conda +sha256: fc716f11a6a8525e27a5d332ef6a689210b0d2a4dd1133edc0f530659aa9faa6 +md5: 4faa39bf919939602e594253bd673958 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 588060 +timestamp: 1771378040807 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda +sha256: 1473451cd282b48d24515795a595801c9b65b567fe399d7e12d50b2d6cdb04d9 +md5: 5a86bf847b9b926f3a4f203339748d78 +depends: +- libgcc >=14 +license: LGPL-2.1-only +size: 791226 +timestamp: 1754910975665 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.1.2-he30d5cf_0.conda +sha256: 84064c7c53a64291a585d7215fe95ec42df74203a5bf7615d33d49a3b0f08bb6 +md5: 5109d7f837a3dfdf5c60f60e311b041f +depends: +- libgcc >=14 +constrains: +- jpeg <0.0.0a +license: IJG AND BSD-3-Clause AND Zlib +size: 691818 +timestamp: 1762094728337 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.2-he30d5cf_0.conda +sha256: 843c46e20519651a3e357a8928352b16c5b94f4cd3d5481acc48be2e93e8f6a3 +md5: 96944e3c92386a12755b94619bae0b35 +depends: +- libgcc >=14 +constrains: +- xz 5.8.2.* +license: 0BSD +size: 125916 +timestamp: 1768754941722 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.55-h1abf092_0.conda +sha256: c7378c6b79de4d571d00ad1caf0a4c19d43c9c94077a761abb6ead44d891f907 +md5: be4088903b94ea297975689b3c3aeb27 +depends: +- libgcc >=14 +- libzlib >=1.3.1,<2.0a0 +license: zlib-acknowledgement +size: 340156 +timestamp: 1770691477245 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_18.conda +sha256: 31fdb9ffafad106a213192d8319b9f810e05abca9c5436b60e507afb35a6bc40 +md5: f56573d05e3b735cb03efeb64a15f388 +depends: +- libgcc 15.2.0 h8acb6b2_18 +constrains: +- libstdcxx-ng ==15.2.0=*_18 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 5541411 +timestamp: 1771378162499 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.1-hdb009f0_1.conda +sha256: 7ff79470db39e803e21b8185bc8f19c460666d5557b1378d1b1e857d929c6b39 +md5: 8c6fd84f9c87ac00636007c6131e457d +depends: +- lerc >=4.0.0,<5.0a0 +- libdeflate >=1.25,<1.26.0a0 +- libgcc >=14 +- libjpeg-turbo >=3.1.0,<4.0a0 +- liblzma >=5.8.1,<6.0a0 +- libstdcxx >=14 +- libwebp-base >=1.6.0,<2.0a0 +- libzlib >=1.3.1,<2.0a0 +- zstd >=1.5.7,<1.6.0a0 +license: HPND +size: 488407 +timestamp: 1762022048105 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.3-h1022ec0_0.conda +sha256: c37a8e89b700646f3252608f8368e7eb8e2a44886b92776e57ad7601fc402a11 +md5: cf2861212053d05f27ec49c3784ff8bb +depends: +- libgcc >=14 +license: BSD-3-Clause +license_family: BSD +size: 43453 +timestamp: 1766271546875 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda +sha256: b03700a1f741554e8e5712f9b06dd67e76f5301292958cd3cb1ac8c6fdd9ed25 +md5: 24e92d0942c799db387f5c9d7b81f1af +depends: +- libgcc >=14 +constrains: +- libwebp 1.6.0 +license: BSD-3-Clause +license_family: BSD +size: 359496 +timestamp: 1752160685488 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda +sha256: 461cab3d5650ac6db73a367de5c8eca50363966e862dcf60181d693236b1ae7b +md5: cd14ee5cca2464a425b1dbfc24d90db2 +depends: +- libgcc >=13 +- pthread-stubs +- xorg-libxau >=1.0.11,<2.0a0 +- xorg-libxdmcp +license: MIT +license_family: MIT +size: 397493 +timestamp: 1727280745441 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda +sha256: 6b46c397644091b8a26a3048636d10b989b1bf266d4be5e9474bf763f828f41f +md5: b4df5d7d4b63579d081fd3a4cf99740e +depends: +- libgcc-ng >=12 +license: LGPL-2.1-or-later +size: 114269 +timestamp: 1702724369203 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda +sha256: eb111e32e5a7313a5bf799c7fb2419051fa2fe7eff74769fac8d5a448b309f7f +md5: 502006882cf5461adced436e410046d1 +constrains: +- zlib 1.3.2 *_2 +license: Zlib +license_family: Other +size: 69833 +timestamp: 1774072605429 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda +sha256: 91cfb655a68b0353b2833521dc919188db3d8a7f4c64bea2c6a7557b24747468 +md5: 182afabe009dc78d8b73100255ee6868 +depends: +- libgcc >=13 +license: X11 AND BSD-3-Clause +size: 926034 +timestamp: 1738196018799 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openjdk-25.0.2-h488f50d_0.conda +sha256: 6fd2c872b275fa5d42a61a4b6dc28a819cde29f9048adb547363597432e0720e +md5: 27fdd5d67e235c20d23b2d66406497d3 +depends: +- xorg-libx11 +- xorg-libxext +- xorg-libxi +- xorg-libxrender +- xorg-libxtst +- libstdcxx >=14 +- libgcc >=14 +- libzlib >=1.3.1,<2.0a0 +- xorg-libxtst >=1.2.5,<2.0a0 +- libpng >=1.6.55,<1.7.0a0 +- alsa-lib >=1.2.15.3,<1.3.0a0 +- xorg-libx11 >=1.8.13,<2.0a0 +- xorg-libxi >=1.8.2,<2.0a0 +- xorg-libxrandr >=1.5.5,<2.0a0 +- lcms2 >=2.18,<3.0a0 +- xorg-libxrender >=0.9.12,<0.10.0a0 +- libcups >=2.3.3,<2.4.0a0 +- libfreetype >=2.14.1 +- libfreetype6 >=2.14.1 +- harfbuzz >=12.3.2 +- xorg-libxext >=1.3.7,<2.0a0 +- giflib >=5.2.2,<5.3.0a0 +- xorg-libxt >=1.3.1,<2.0a0 +- libjpeg-turbo >=3.1.2,<4.0a0 +- fontconfig >=2.17.1,<3.0a0 +- fonts-conda-ecosystem +license: GPL-2.0-or-later WITH Classpath-exception-2.0 +license_family: GPL +size: 106988620 +timestamp: 1771443741031 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda +sha256: 7f8048c0e75b2620254218d72b4ae7f14136f1981c5eb555ef61645a9344505f +md5: 25f5885f11e8b1f075bccf4a2da91c60 +depends: +- ca-certificates +- libgcc >=14 +license: Apache-2.0 +license_family: Apache +size: 3692030 +timestamp: 1769557678657 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.47-hf841c20_0.conda +sha256: 04df2cee95feba440387f33f878e9f655521e69f4be33a0cd637f07d3d81f0f9 +md5: 1a30c42e32ca0ea216bd0bfe6f842f0b +depends: +- bzip2 >=1.0.8,<2.0a0 +- libgcc >=14 +- libzlib >=1.3.1,<2.0a0 +license: BSD-3-Clause +license_family: BSD +size: 1166552 +timestamp: 1763655534263 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/perl-5.32.1-7_h31becfc_perl5.conda +build_number: 7 +sha256: d78296134263b5bf476cad838ded65451e7162db756f9997c5d06b08122572ed +md5: 17d019cb2a6c72073c344e98e40dfd61 +depends: +- libgcc-ng >=12 +- libxcrypt >=4.4.36 +license: GPL-1.0-or-later OR Artistic-1.0-Perl +size: 13338804 +timestamp: 1703310557094 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_1.conda +sha256: e6b0846a998f2263629cfeac7bca73565c35af13251969f45d385db537a514e4 +md5: 1587081d537bd4ae77d1c0635d465ba5 +depends: +- libgcc >=14 +- libstdcxx >=14 +- libgcc >=14 +license: MIT +license_family: MIT +size: 357913 +timestamp: 1754665583353 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/procps-ng-4.0.6-h1779866_0.conda +sha256: e9cbcbc94e151ada3d6dc365380aaaf591f65012c16d9a2abaea4b9b90adc402 +md5: ab7288cc39545556d1bc5e71ab2df9a9 +depends: +- libgcc >=14 +- ncurses >=6.5,<7.0a0 +license: GPL-2.0-or-later AND LGPL-2.0-or-later +license_family: GPL +size: 636733 +timestamp: 1769712412683 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda +sha256: 977dfb0cb3935d748521dd80262fe7169ab82920afd38ed14b7fee2ea5ec01ba +md5: bb5a90c93e3bac3d5690acf76b4a6386 +depends: +- libgcc >=13 +license: MIT +license_family: MIT +size: 8342 +timestamp: 1726803319942 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.2-h86ecc28_0.conda +sha256: a2ba1864403c7eb4194dacbfe2777acf3d596feae43aada8d1b478617ce45031 +md5: c8d8ec3e00cd0fd8a231789b91a7c5b7 +depends: +- libgcc >=13 +license: MIT +license_family: MIT +size: 60433 +timestamp: 1734229908988 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.6-h0808dbd_0.conda +sha256: b86a819cd16f90c01d9d81892155126d01555a20dabd5f3091da59d6309afd0a +md5: 2d1409c50882819cb1af2de82e2b7208 +depends: +- libgcc >=13 +- libuuid >=2.38.1,<3.0a0 +- xorg-libice >=1.1.2,<2.0a0 +license: MIT +license_family: MIT +size: 28701 +timestamp: 1741897678254 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.13-h63a1b12_0.conda +sha256: cf886160e2ff580d77f7eb8ec1a77c41c2c5b05343e329bc35f0ddf40b8d92ab +md5: 22dd10425ef181e80e130db50675d615 +depends: +- libgcc >=14 +- libxcb >=1.17.0,<2.0a0 +license: MIT +license_family: MIT +size: 869058 +timestamp: 1770819244991 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-he30d5cf_1.conda +sha256: e9f6e931feeb2f40e1fdbafe41d3b665f1ab6cb39c5880a1fcf9f79a3f3c84a5 +md5: 1c246e1105000c3660558459e2fd6d43 +depends: +- libgcc >=14 +license: MIT +license_family: MIT +size: 16317 +timestamp: 1762977521691 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-he30d5cf_1.conda +sha256: 128d72f36bcc8d2b4cdbec07507542e437c7d67f677b7d77b71ed9eeac7d6df1 +md5: bff06dcde4a707339d66d45d96ceb2e2 +depends: +- libgcc >=14 +license: MIT +license_family: MIT +size: 21039 +timestamp: 1762979038025 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.7-he30d5cf_0.conda +sha256: db2188bc0d844d4e9747bac7f6c1d067e390bd769c5ad897c93f1df759dc5dba +md5: fb42b683034619915863d68dd9df03a3 +depends: +- libgcc >=14 +- xorg-libx11 >=1.8.12,<2.0a0 +license: MIT +license_family: MIT +size: 52409 +timestamp: 1769446753771 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.2-he30d5cf_0.conda +sha256: 8cb9c88e25c57e47419e98f04f9ef3154ad96b9f858c88c570c7b91216a64d0e +md5: e8b4056544341daf1d415eaeae7a040c +depends: +- libgcc >=14 +- xorg-libx11 >=1.8.12,<2.0a0 +license: MIT +license_family: MIT +size: 20704 +timestamp: 1759284028146 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxi-1.8.2-h57736b2_0.conda +sha256: 7b587407ecb9ccd2bbaf0fb94c5dbdde4d015346df063e9502dc0ce2b682fb5e +md5: eeee3bdb31c6acde2b81ad1b8c287087 +depends: +- libgcc >=13 +- xorg-libx11 >=1.8.9,<2.0a0 +- xorg-libxext >=1.3.6,<2.0a0 +- xorg-libxfixes >=6.0.1,<7.0a0 +license: MIT +license_family: MIT +size: 48197 +timestamp: 1727801059062 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrandr-1.5.5-he30d5cf_0.conda +sha256: 9f5196665a8d72f4f119c40dcc4bafeb0b540b102cc7b8b299c2abf599e7919f +md5: 1f64c613f0b8d67e9fb0e165d898fb6b +depends: +- libgcc >=14 +- xorg-libx11 >=1.8.12,<2.0a0 +- xorg-libxext >=1.3.6,<2.0a0 +- xorg-libxrender >=0.9.12,<0.10.0a0 +license: MIT +license_family: MIT +size: 31122 +timestamp: 1769445286951 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.12-h86ecc28_0.conda +sha256: ffd77ee860c9635a28cfda46163dcfe9224dc6248c62404c544ae6b564a0be1f +md5: ae2c2dd0e2d38d249887727db2af960e +depends: +- libgcc >=13 +- xorg-libx11 >=1.8.10,<2.0a0 +license: MIT +license_family: MIT +size: 33649 +timestamp: 1734229123157 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxt-1.3.1-h57736b2_0.conda +sha256: 7c109792b60720809a580612aba7f8eb2a0bd425b9fc078748a9d6ffc97cbfa8 +md5: a9e4852c8e0b68ee783e7240030b696f +depends: +- libgcc >=13 +- xorg-libice >=1.1.1,<2.0a0 +- xorg-libsm >=1.2.4,<2.0a0 +- xorg-libx11 >=1.8.9,<2.0a0 +license: MIT +license_family: MIT +size: 384752 +timestamp: 1731860572314 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxtst-1.2.5-h57736b2_3.conda +sha256: 6eaffce5a34fc0a16a21ddeaefb597e792a263b1b0c387c1ce46b0a967d558e1 +md5: c05698071b5c8e0da82a282085845860 +depends: +- libgcc >=13 +- xorg-libx11 >=1.8.9,<2.0a0 +- xorg-libxext >=1.3.6,<2.0a0 +- xorg-libxi >=1.7.10,<2.0a0 +license: MIT +license_family: MIT +size: 33786 +timestamp: 1727964907993 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda +sha256: 569990cf12e46f9df540275146da567d9c618c1e9c7a0bc9d9cfefadaed20b75 +md5: c3655f82dcea2aa179b291e7099c1fcc +depends: +- libzlib >=1.3.1,<2.0a0 +license: BSD-3-Clause +license_family: BSD +size: 614429 +timestamp: 1764777145593 diff --git a/modules/nf-core/fastqc/environment.yml b/modules/nf-core/fastqc/environment.yml index 691d4c7..f9f54ee 100644 --- a/modules/nf-core/fastqc/environment.yml +++ b/modules/nf-core/fastqc/environment.yml @@ -1,3 +1,5 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json channels: - conda-forge - bioconda diff --git a/modules/nf-core/fastqc/main.nf b/modules/nf-core/fastqc/main.nf index 752c3a1..1085126 100644 --- a/modules/nf-core/fastqc/main.nf +++ b/modules/nf-core/fastqc/main.nf @@ -1,19 +1,19 @@ process FASTQC { - tag "$meta.id" - label 'process_medium' + tag "${meta.id}" + label 'process_low' conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/fastqc:0.12.1--hdfd78af_0' : - 'biocontainers/fastqc:0.12.1--hdfd78af_0' }" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container + ? 'https://depot.galaxyproject.org/singularity/fastqc:0.12.1--hdfd78af_0' + : 'quay.io/biocontainers/fastqc:0.12.1--hdfd78af_0'}" input: - tuple val(meta), path(reads) + tuple val(meta), path(reads, stageAs: '?/*') output: tuple val(meta), path("*.html"), emit: html - tuple val(meta), path("*.zip") , emit: zip - path "versions.yml" , emit: versions + tuple val(meta), path("*.zip"), emit: zip + tuple val("${task.process}"), val('fastqc'), eval('fastqc --version | sed "/FastQC v/!d; s/.*v//"'), emit: versions_fastqc, topic: versions when: task.ext.when == null || task.ext.when @@ -22,32 +22,30 @@ process FASTQC { def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" // Make list of old name and new name pairs to use for renaming in the bash while loop - def old_new_pairs = reads instanceof Path || reads.size() == 1 ? [[ reads, "${prefix}.${reads.extension}" ]] : reads.withIndex().collect { entry, index -> [ entry, "${prefix}_${index + 1}.${entry.extension}" ] } + def old_new_pairs = reads instanceof Path || reads.size() == 1 ? [[reads, "${prefix}.${reads.extension}"]] : reads.withIndex().collect { entry, index -> [entry, "${prefix}_${index + 1}.${entry.extension}"] } def rename_to = old_new_pairs*.join(' ').join(' ') - def renamed_files = old_new_pairs.collect{ _old_name, new_name -> new_name }.join(' ') + def renamed_files = old_new_pairs.collect { _old_name, new_name -> new_name }.join(' ') // The total amount of allocated RAM by FastQC is equal to the number of threads defined (--threads) time the amount of RAM defined (--memory) // https://github.com/s-andrews/FastQC/blob/1faeea0412093224d7f6a07f777fad60a5650795/fastqc#L211-L222 - // Dividing the task.memory by task.cpu allows to stick to requested amount of RAM in the label - def memory_in_mb = MemoryUnit.of("${task.memory}").toUnit('MB') / task.cpus + // Dividing the task.memory by task.cpus allows to stick to requested amount of RAM in the label + def memory_in_mb = task.memory + ? (task.memory.toUnit('MB') / task.cpus).intValue() + : null // FastQC memory value allowed range (100 - 10000) def fastqc_memory = memory_in_mb > 10000 ? 10000 : (memory_in_mb < 100 ? 100 : memory_in_mb) + def fastqc_memory_arg = fastqc_memory ? "--memory ${fastqc_memory}" : '' """ - printf "%s %s\\n" $rename_to | while read old_name new_name; do + printf "%s %s\\n" ${rename_to} | while read old_name new_name; do [ -f "\${new_name}" ] || ln -s \$old_name \$new_name done fastqc \\ - $args \\ - --threads $task.cpus \\ - --memory $fastqc_memory \\ - $renamed_files - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - fastqc: \$( fastqc --version | sed '/FastQC v/!d; s/.*v//' ) - END_VERSIONS + ${args} \\ + --threads ${task.cpus} \\ + ${fastqc_memory_arg} \\ + ${renamed_files} """ stub: @@ -55,10 +53,5 @@ process FASTQC { """ touch ${prefix}.html touch ${prefix}.zip - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - fastqc: \$( fastqc --version | sed '/FastQC v/!d; s/.*v//' ) - END_VERSIONS """ } diff --git a/modules/nf-core/fastqc/meta.yml b/modules/nf-core/fastqc/meta.yml index 2b2e62b..2f6cfef 100644 --- a/modules/nf-core/fastqc/meta.yml +++ b/modules/nf-core/fastqc/meta.yml @@ -29,9 +29,10 @@ input: description: | List of input FastQ files of size 1 and 2 for single-end and paired-end data, respectively. + ontologies: [] output: - - html: - - meta: + html: + - - meta: type: map description: | Groovy Map containing sample information @@ -40,8 +41,9 @@ output: type: file description: FastQC report pattern: "*_{fastqc.html}" - - zip: - - meta: + ontologies: [] + zip: + - - meta: type: map description: | Groovy Map containing sample information @@ -50,11 +52,29 @@ output: type: file description: FastQC report archive pattern: "*_{fastqc.zip}" - - versions: - - versions.yml: - type: file - description: File containing software versions - pattern: "versions.yml" + ontologies: [] + versions_fastqc: + - - ${task.process}: + type: string + description: The process the versions were collected from + - fastqc: + type: string + description: The tool name + - fastqc --version | sed "/FastQC v/!d; s/.*v//": + type: eval + description: The expression to obtain the version of the tool + +topics: + versions: + - - ${task.process}: + type: string + description: The process the versions were collected from + - fastqc: + type: string + description: The tool name + - fastqc --version | sed "/FastQC v/!d; s/.*v//": + type: eval + description: The expression to obtain the version of the tool authors: - "@drpatelh" - "@grst" @@ -65,3 +85,27 @@ maintainers: - "@grst" - "@ewels" - "@FelixKrueger" +containers: + docker: + linux/arm64: + name: community.wave.seqera.io/library/fastqc:0.12.1--e455e32f745abe68 + build_id: bd-e455e32f745abe68_1 + scan_id: sc-f102f736465af88c_1 + linux/amd64: + name: community.wave.seqera.io/library/fastqc:0.12.1--5cb1a2fa2f18c7c2 + build_id: bd-5cb1a2fa2f18c7c2_1 + scan_id: sc-0c0466326b6b77d2_1 + singularity: + linux/amd64: + name: oras://community.wave.seqera.io/library/fastqc:0.12.1--5c4bd442468d75dd + build_id: bd-5c4bd442468d75dd_1 + https: https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f2/f20b021476d1d87658820f971ebecc1e8cdbde0f338eb0d9cea2b0a8fc54a54b/data + linux/arm64: + name: oras://community.wave.seqera.io/library/fastqc:0.12.1--127a87fc06499035 + build_id: bd-127a87fc06499035_1 + https: https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/46/46daf2dad0169afd2ae047c3e50ed3776259f664bf07e5e06b045dc23449e994/data + conda: + linux/amd64: + lock_file: modules/nf-core/fastqc/.conda-lock/linux_amd64-bd-5cb1a2fa2f18c7c2_1.txt + linux/arm64: + lock_file: modules/nf-core/fastqc/.conda-lock/linux_arm64-bd-e455e32f745abe68_1.txt diff --git a/modules/nf-core/fastqc/tests/main.nf.test b/modules/nf-core/fastqc/tests/main.nf.test index e9d79a0..66c44da 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test +++ b/modules/nf-core/fastqc/tests/main.nf.test @@ -30,7 +30,7 @@ nextflow_process { { assert process.out.html[0][1] ==~ ".*/test_fastqc.html" }, { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, - { assert snapshot(process.out.versions).match() } + { assert snapshot(sanitizeOutput(process.out).findAll { key, val -> key != 'html' && key != 'zip' }).match() } ) } } @@ -58,7 +58,7 @@ nextflow_process { { assert process.out.zip[0][1][1] ==~ ".*/test_2_fastqc.zip" }, { assert path(process.out.html[0][1][0]).text.contains("File typeConventional base calls") }, { assert path(process.out.html[0][1][1]).text.contains("File typeConventional base calls") }, - { assert snapshot(process.out.versions).match() } + { assert snapshot(sanitizeOutput(process.out).findAll { key, val -> key != 'html' && key != 'zip' }).match() } ) } } @@ -82,7 +82,7 @@ nextflow_process { { assert process.out.html[0][1] ==~ ".*/test_fastqc.html" }, { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, - { assert snapshot(process.out.versions).match() } + { assert snapshot(sanitizeOutput(process.out).findAll { key, val -> key != 'html' && key != 'zip' }).match() } ) } } @@ -106,7 +106,7 @@ nextflow_process { { assert process.out.html[0][1] ==~ ".*/test_fastqc.html" }, { assert process.out.zip[0][1] ==~ ".*/test_fastqc.zip" }, { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, - { assert snapshot(process.out.versions).match() } + { assert snapshot(sanitizeOutput(process.out).findAll { key, val -> key != 'html' && key != 'zip' }).match() } ) } } @@ -142,7 +142,7 @@ nextflow_process { { assert path(process.out.html[0][1][1]).text.contains("File typeConventional base calls") }, { assert path(process.out.html[0][1][2]).text.contains("File typeConventional base calls") }, { assert path(process.out.html[0][1][3]).text.contains("File typeConventional base calls") }, - { assert snapshot(process.out.versions).match() } + { assert snapshot(sanitizeOutput(process.out).findAll { key, val -> key != 'html' && key != 'zip' }).match() } ) } } @@ -166,7 +166,7 @@ nextflow_process { { assert process.out.html[0][1] ==~ ".*/mysample_fastqc.html" }, { assert process.out.zip[0][1] ==~ ".*/mysample_fastqc.zip" }, { assert path(process.out.html[0][1]).text.contains("File typeConventional base calls") }, - { assert snapshot(process.out.versions).match() } + { assert snapshot(sanitizeOutput(process.out).findAll { key, val -> key != 'html' && key != 'zip' }).match() } ) } } diff --git a/modules/nf-core/fastqc/tests/main.nf.test.snap b/modules/nf-core/fastqc/tests/main.nf.test.snap index d5db309..c8ee120 100644 --- a/modules/nf-core/fastqc/tests/main.nf.test.snap +++ b/modules/nf-core/fastqc/tests/main.nf.test.snap @@ -1,15 +1,21 @@ { "sarscov2 custom_prefix": { "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] + { + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] + ] + } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:02:16.374038" + "timestamp": "2025-10-28T16:39:14.518503" }, "sarscov2 single-end [fastq] - stub": { "content": [ @@ -33,7 +39,11 @@ ] ], "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "html": [ [ @@ -44,8 +54,12 @@ "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "zip": [ [ @@ -59,10 +73,10 @@ } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:02:24.993809" + "timestamp": "2025-10-28T16:39:19.309008" }, "sarscov2 custom_prefix - stub": { "content": [ @@ -86,7 +100,11 @@ ] ], "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "html": [ [ @@ -97,8 +115,12 @@ "mysample.html:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "zip": [ [ @@ -112,58 +134,82 @@ } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:03:10.93942" + "timestamp": "2025-10-28T16:39:44.94888" }, "sarscov2 interleaved [fastq]": { "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] + { + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] + ] + } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:01:42.355718" + "timestamp": "2025-10-28T16:38:45.168496" }, "sarscov2 paired-end [bam]": { "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] + { + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] + ] + } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:01:53.276274" + "timestamp": "2025-10-28T16:38:53.268919" }, "sarscov2 multiple [fastq]": { "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] + { + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] + ] + } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:02:05.527626" + "timestamp": "2025-10-28T16:39:05.050305" }, "sarscov2 paired-end [fastq]": { "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] + { + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] + ] + } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:01:31.188871" + "timestamp": "2025-10-28T16:38:37.2373" }, "sarscov2 paired-end [fastq] - stub": { "content": [ @@ -187,7 +233,11 @@ ] ], "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "html": [ [ @@ -198,8 +248,12 @@ "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "zip": [ [ @@ -213,10 +267,10 @@ } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:02:34.273566" + "timestamp": "2025-10-28T16:39:24.450398" }, "sarscov2 multiple [fastq] - stub": { "content": [ @@ -240,7 +294,11 @@ ] ], "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "html": [ [ @@ -251,8 +309,12 @@ "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "zip": [ [ @@ -266,22 +328,28 @@ } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:03:02.304411" + "timestamp": "2025-10-28T16:39:39.758762" }, "sarscov2 single-end [fastq]": { "content": [ - [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" - ] + { + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] + ] + } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:01:19.095607" + "timestamp": "2025-10-28T16:38:29.555068" }, "sarscov2 interleaved [fastq] - stub": { "content": [ @@ -305,7 +373,11 @@ ] ], "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "html": [ [ @@ -316,8 +388,12 @@ "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "zip": [ [ @@ -331,10 +407,10 @@ } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:02:44.640184" + "timestamp": "2025-10-28T16:39:29.193136" }, "sarscov2 paired-end [bam] - stub": { "content": [ @@ -358,7 +434,11 @@ ] ], "2": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "html": [ [ @@ -369,8 +449,12 @@ "test.html:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "versions": [ - "versions.yml:md5,e1cc25ca8af856014824abd842e93978" + "versions_fastqc": [ + [ + "FASTQC", + "fastqc", + "0.12.1" + ] ], "zip": [ [ @@ -384,9 +468,9 @@ } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.3" + "nf-test": "0.9.2", + "nextflow": "25.10.0" }, - "timestamp": "2024-07-22T11:02:53.550742" + "timestamp": "2025-10-28T16:39:34.144919" } } \ No newline at end of file diff --git a/modules/nf-core/fastqc/tests/tags.yml b/modules/nf-core/fastqc/tests/tags.yml deleted file mode 100644 index 7834294..0000000 --- a/modules/nf-core/fastqc/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -fastqc: - - modules/nf-core/fastqc/** diff --git a/modules/nf-core/multiqc/.conda-lock/linux_amd64-bd-c17fb751507e9dfc_1.txt b/modules/nf-core/multiqc/.conda-lock/linux_amd64-bd-c17fb751507e9dfc_1.txt new file mode 100644 index 0000000..2a91c22 --- /dev/null +++ b/modules/nf-core/multiqc/.conda-lock/linux_amd64-bd-c17fb751507e9dfc_1.txt @@ -0,0 +1,1526 @@ + +version: 6 +environments: +default: +channels: +- url: https://conda.anaconda.org/conda-forge/ +- url: https://conda.anaconda.org/bioconda/ +- url: https://conda.anaconda.org/bioconda/ +options: +pypi-prerelease-mode: if-necessary-or-explicit +packages: +linux-64: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.5.20-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.4.0-pyhc90fa1f_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/coloredlogs-15.0.1-pyhd8ed1ab_4.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/colormath-3.0.0-pyhd8ed1ab_4.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.5-py314hd8ed1ab_100.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/expat-2.8.1-hecca717_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.0-h27c8c51_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh707e725_8.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/humanize-4.15.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.15-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/kaleido-core-0.2.1-h3644ca4_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.19.1-h0c24ade_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-7_h4a7cf45_openblas.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-7_h0358290_openblas.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-7_h47877c9_openblas.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.1-h5347b49_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.10.2-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/mathjax-2.7.7-ha770c72_3.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda +- conda: https://conda.anaconda.org/bioconda/noarch/multiqc-1.35-pyhdfd78af_1.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.21.2-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/natsort-8.4.0-pyhcf101f3_2.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/nspr-4.38-h29cc59b_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/nss-3.118-h445c969_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.6-py314h2b28147_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.2.0-py314h8ec4b1a_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/polars-1.41.0-pyh58ad624_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-32-1.41.0-py310h49dadd8_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-compat-1.41.0-py310hcbd6021_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/procps-ng-4.0.6-h18c060e_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/pyaml-env-1.2.2-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py314h2e6c369_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.5-habeac84_100_cp314.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.5-h4df99d1_100.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/python-kaleido-0.2.1-pyhd8ed1ab_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/regex-2026.5.9-py314h5bd0f2a_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-click-1.9.7-pyh8f84b5b_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py314h2e6c369_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/spectra-0.0.11-pyhd8ed1ab_2.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.53.1-hbc0de68_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/tiktoken-0.12.0-py314h67fec18_3.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/typeguard-4.5.2-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda +build_number: 20 +sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 +md5: a9f577daf3de00bca7c3c76c0ecbd1de +depends: +- __glibc >=2.17,<3.0.a0 +- libgomp >=7.5.0 +constrains: +- openmp_impl <0.0a0 +license: BSD-3-Clause +license_family: BSD +size: 28948 +timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda +sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 +md5: aaa2a381ccc56eac91d63b6c1240312f +depends: +- cpython +- python-gil +license: MIT +license_family: MIT +size: 8191 +timestamp: 1744137672556 +- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda +sha256: e0ea1ba78fbb64f17062601edda82097fcf815012cf52bb704150a2668110d48 +md5: 2934f256a8acfe48f6ebb4fce6cde29c +depends: +- python >=3.9 +- typing-extensions >=4.0.0 +license: MIT +license_family: MIT +size: 18074 +timestamp: 1733247158254 +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda +sha256: 1b6124230bb4e571b1b9401537ecff575b7b109cc3a21ee019f65e083b8399ab +md5: c6b0543676ecb1fb2d7643941fe375f2 +depends: +- python >=3.10 +- python +license: MIT +license_family: MIT +size: 64927 +timestamp: 1773935801332 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda +noarch: generic +sha256: a1c97297e867776760489537bc5ae36fa83a154be30e3b79385a39ca4cb058fe +md5: 1133126d840e75287d83947be3fc3e71 +depends: +- python >=3.14 +license: BSD-3-Clause AND MIT AND EPL-2.0 +size: 7533 +timestamp: 1778594057496 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda +sha256: 3ad3500bff54a781c29f16ce1b288b36606e2189d0b0ef2f67036554f47f12b0 +md5: 8910d2c46f7e7b519129f486e0fe927a +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libstdcxx >=14 +- python >=3.14,<3.15.0a0 +- python_abi 3.14.* *_cp314 +constrains: +- libbrotlicommon 1.2.0 hb03c661_1 +license: MIT +license_family: MIT +size: 367376 +timestamp: 1764017265553 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda +sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 +md5: d2ffd7602c02f2b316fd921d39876885 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: bzip2-1.0.6 +license_family: BSD +size: 260182 +timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda +sha256: 9812a303a1395e1dafbd92e5bc8a1ff6013bcbba0a09c7f03a8d23e43560aa9b +md5: 489b8e97e666c93f68fdb35c3c9b957f +depends: +- __unix +license: ISC +size: 129868 +timestamp: 1779289852439 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.5.20-pyhd8ed1ab_0.conda +sha256: 645655a3510e38e625da136595f3f16f2130c3263630cc3bc8f60f619ddbe490 +md5: 9fefff2f745ea1cc2ef15211a20c054a +depends: +- python >=3.10 +license: ISC +size: 134201 +timestamp: 1779285131141 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda +sha256: 3f9483d62ce24ecd063f8a5a714448445dc8d9e201147c46699fc0033e824457 +md5: a9167b9571f3baa9d448faa2139d1089 +depends: +- python >=3.10 +license: MIT +license_family: MIT +size: 58872 +timestamp: 1775127203018 +- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.4.0-pyhc90fa1f_0.conda +sha256: 99ab8ef815c4520cce3a7482c2513f377c14348206857661d84c76a55e030f97 +md5: 003767c47f1f0a474c4de268b57839c3 +depends: +- __unix +- python +- python >=3.10 +license: BSD-3-Clause +license_family: BSD +size: 104631 +timestamp: 1779108494556 +- conda: https://conda.anaconda.org/conda-forge/noarch/coloredlogs-15.0.1-pyhd8ed1ab_4.conda +sha256: 8021c76eeadbdd5784b881b165242db9449783e12ce26d6234060026fd6a8680 +md5: b866ff7007b934d564961066c8195983 +depends: +- humanfriendly >=9.1 +- python >=3.9 +license: MIT +license_family: MIT +size: 43758 +timestamp: 1733928076798 +- conda: https://conda.anaconda.org/conda-forge/noarch/colormath-3.0.0-pyhd8ed1ab_4.conda +sha256: 59c9e29800b483b390467f90e82b0da3a4fbf0612efe1c90813fca232780e160 +md5: 071cf7b0ce333c81718b054066c15102 +depends: +- networkx >=2.0 +- numpy +- python >=3.9 +license: BSD-3-Clause +license_family: BSD +size: 39326 +timestamp: 1735759976140 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.5-py314hd8ed1ab_100.conda +noarch: generic +sha256: 777882d2685f368417f31bbe1b28f73687fc6c8f6a5768bda20ffeefa6b07f5b +md5: a749029ce5d0632a913db19d17f944ab +depends: +- python >=3.14,<3.15.0a0 +- python_abi * *_cp314 +license: Python-2.0 +size: 50212 +timestamp: 1779236682725 +- conda: https://conda.anaconda.org/conda-forge/linux-64/expat-2.8.1-hecca717_0.conda +sha256: 29a10599d56d93bd750914888ebe6822d47722070762b4647b34d12df9f4476e +md5: d0757fd84af06f065eba49d39af6c546 +depends: +- __glibc >=2.17,<3.0.a0 +- libexpat 2.8.1 hecca717_0 +- libgcc >=14 +license: MIT +license_family: MIT +size: 148238 +timestamp: 1779278694477 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 +sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b +md5: 0c96522c6bdaed4b1566d11387caaf45 +license: BSD-3-Clause +license_family: BSD +size: 397370 +timestamp: 1566932522327 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 +sha256: c52a29fdac682c20d252facc50f01e7c2e7ceac52aa9817aaf0bb83f7559ec5c +md5: 34893075a5c9e55cdafac56607368fc6 +license: OFL-1.1 +license_family: Other +size: 96530 +timestamp: 1620479909603 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 +sha256: 00925c8c055a2275614b4d983e1df637245e19058d79fc7dd1a93b8d9fb4b139 +md5: 4d59c254e01d9cde7957100457e2d5fb +license: OFL-1.1 +license_family: Other +size: 700814 +timestamp: 1620479612257 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda +sha256: 2821ec1dc454bd8b9a31d0ed22a7ce22422c0aef163c59f49dfdf915d0f0ca14 +md5: 49023d73832ef61042f6a237cb2687e7 +license: LicenseRef-Ubuntu-Font-Licence-Version-1.0 +license_family: Other +size: 1620504 +timestamp: 1727511233259 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.0-h27c8c51_0.conda +sha256: e798086d8a65d55dc4c51f5746705639c9a5f2eeb0b8fc50e6152cfc0d69a4e8 +md5: 06965b2f9854d0b15e0443ee81fe83dc +depends: +- __glibc >=2.17,<3.0.a0 +- libexpat >=2.8.1,<3.0a0 +- libfreetype >=2.14.3 +- libfreetype6 >=2.14.3 +- libgcc >=14 +- libuuid >=2.42.1,<3.0a0 +- libzlib >=1.3.2,<2.0a0 +license: MIT +license_family: MIT +size: 280882 +timestamp: 1779421631622 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda +sha256: 54eea8469786bc2291cc40bca5f46438d3e062a399e8f53f013b6a9f50e98333 +md5: a7970cd949a077b7cb9696379d338681 +depends: +- font-ttf-ubuntu +- font-ttf-inconsolata +- font-ttf-dejavu-sans-mono +- font-ttf-source-code-pro +license: BSD-3-Clause +license_family: BSD +size: 4059 +timestamp: 1762351264405 +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda +sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 +md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 +depends: +- python >=3.10 +- hyperframe >=6.1,<7 +- hpack >=4.1,<5 +- python +license: MIT +license_family: MIT +size: 95967 +timestamp: 1756364871835 +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda +sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba +md5: 0a802cb9888dd14eeefc611f05c40b6e +depends: +- python >=3.9 +license: MIT +license_family: MIT +size: 30731 +timestamp: 1737618390337 +- conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh707e725_8.conda +sha256: fa2071da7fab758c669e78227e6094f6b3608228740808a6de5d6bce83d9e52d +md5: 7fe569c10905402ed47024fc481bb371 +depends: +- __unix +- python >=3.9 +license: MIT +license_family: MIT +size: 73563 +timestamp: 1733928021866 +- conda: https://conda.anaconda.org/conda-forge/noarch/humanize-4.15.0-pyhd8ed1ab_0.conda +sha256: 6c4343b376d0b12a4c75ab992640970d36c933cad1fd924f6a1181fa91710e80 +md5: daddf757c3ecd6067b9af1df1f25d89e +depends: +- python >=3.10 +license: MIT +license_family: MIT +size: 67994 +timestamp: 1766267728652 +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda +sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 +md5: 8e6923fc12f1fe8f8c4e5c9f343256ac +depends: +- python >=3.9 +license: MIT +license_family: MIT +size: 17397 +timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.15-pyhcf101f3_0.conda +sha256: 3d25f9f6f7ab3e1ce6429fc8c8aae0335cf446692e715068488536d220cc43de +md5: 1b9083b7f00609605d1483dbc6071a81 +depends: +- python >=3.10 +- python +license: BSD-3-Clause +license_family: BSD +size: 62642 +timestamp: 1779294335905 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda +sha256: 43e2a5497cad1598ff88a3e69f69bc88b7b8f141fa63c60eab5db296317318b8 +md5: ffc17e785d64e12fc311af9184221839 +depends: +- python >=3.10 +- zipp >=3.20 +- python +license: Apache-2.0 +size: 34766 +timestamp: 1779714582554 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda +sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b +md5: 04558c96691bed63104678757beb4f8d +depends: +- markupsafe >=2.0 +- python >=3.10 +- python +license: BSD-3-Clause +license_family: BSD +size: 120685 +timestamp: 1764517220861 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda +sha256: db973a37d75db8e19b5f44bbbdaead0c68dde745407f281e2a7fe4db74ec51d7 +md5: ada41c863af263cc4c5fcbaff7c3e4dc +depends: +- attrs >=22.2.0 +- jsonschema-specifications >=2023.3.6 +- python >=3.10 +- referencing >=0.28.4 +- rpds-py >=0.25.0 +- python +license: MIT +license_family: MIT +size: 82356 +timestamp: 1767839954256 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda +sha256: 0a4f3b132f0faca10c89fdf3b60e15abb62ded6fa80aebfc007d05965192aa04 +md5: 439cd0f567d697b20a8f45cb70a1005a +depends: +- python >=3.10 +- referencing >=0.31.0 +- python +license: MIT +license_family: MIT +size: 19236 +timestamp: 1757335715225 +- conda: https://conda.anaconda.org/conda-forge/linux-64/kaleido-core-0.2.1-h3644ca4_0.tar.bz2 +sha256: 7f243680ca03eba7457b7a48f93a9440ba8181a8eac20a3eb5ef165ab6c96664 +md5: b3723b235b0758abaae8c82ce4d80146 +depends: +- __glibc >=2.17,<3.0.a0 +- expat >=2.2.10,<3.0.0a0 +- fontconfig +- fonts-conda-forge +- libgcc-ng >=9.3.0 +- mathjax 2.7.* +- nspr >=4.29,<5.0a0 +- nss >=3.62,<4.0a0 +- sqlite >=3.34.0,<4.0a0 +license: MIT +license_family: MIT +size: 62099926 +timestamp: 1615199463039 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.19.1-h0c24ade_0.conda +sha256: eb89c6c39f2f6a93db55723dbb2f6bba8c8e63e6312bf1abf13e6e9ff45849c8 +md5: f92f984b558e6e6204014b16d212b271 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libjpeg-turbo >=3.1.4.1,<4.0a0 +- libtiff >=4.7.1,<4.8.0a0 +license: MIT +license_family: MIT +size: 251086 +timestamp: 1778079286384 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda +sha256: 3d584956604909ff5df353767f3a2a2f60e07d070b328d109f30ac40cd62df6c +md5: 18335a698559cdbcd86150a48bf54ba6 +depends: +- __glibc >=2.17,<3.0.a0 +- zstd >=1.5.7,<1.6.0a0 +constrains: +- binutils_impl_linux-64 2.45.1 +license: GPL-3.0-only +license_family: GPL +size: 728002 +timestamp: 1774197446916 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda +sha256: f84cb54782f7e9cea95e810ea8fef186e0652d0fa73d3009914fa2c1262594e1 +md5: a752488c68f2e7c456bcbd8f16eec275 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libstdcxx >=14 +license: Apache-2.0 +license_family: Apache +size: 261513 +timestamp: 1773113328888 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-7_h4a7cf45_openblas.conda +build_number: 7 +sha256: 081c850f99bc355821fac9c6e3727d40b3f8ce3beb50a5437cf03726b611ff39 +md5: 955b44e8b00b7f7ef4ce0130cef12394 +depends: +- libopenblas >=0.3.33,<0.3.34.0a0 +- libopenblas >=0.3.33,<1.0a0 +constrains: +- libcblas 3.11.0 7*_openblas +- blas 2.307 openblas +- liblapack 3.11.0 7*_openblas +- liblapacke 3.11.0 7*_openblas +- mkl <2027 +license: BSD-3-Clause +license_family: BSD +size: 18716 +timestamp: 1778489854108 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-7_h0358290_openblas.conda +build_number: 7 +sha256: 956ae0bb1ec8b0c3663d75b151aceb0521b54e513bf97f621a035f9c87037970 +md5: 0675639dc24cb0032f199e7ff68e4633 +depends: +- libblas 3.11.0 7_h4a7cf45_openblas +constrains: +- liblapacke 3.11.0 7*_openblas +- blas 2.307 openblas +- liblapack 3.11.0 7*_openblas +license: BSD-3-Clause +license_family: BSD +size: 18675 +timestamp: 1778489861559 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda +sha256: aa8e8c4be9a2e81610ddf574e05b64ee131fab5e0e3693210c9d6d2fba32c680 +md5: 6c77a605a7a689d17d4819c0f8ac9a00 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: MIT +license_family: MIT +size: 73490 +timestamp: 1761979956660 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_0.conda +sha256: 363018b25fdb5534c79783d912bd4b685a3547f4fc5996357ad548899b0ee8e7 +md5: 93764a5ca80616e9c10106cdaec92f74 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +constrains: +- expat 2.8.1.* +license: MIT +license_family: MIT +size: 77294 +timestamp: 1779278686680 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda +sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 +md5: a360c33a5abe61c07959e449fa1453eb +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: MIT +license_family: MIT +size: 58592 +timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda +sha256: 38f014a7129e644636e46064ecd6b1945e729c2140e21d75bb476af39e692db2 +md5: e289f3d17880e44b633ba911d57a321b +depends: +- libfreetype6 >=2.14.3 +license: GPL-2.0-only OR FTL +size: 8049 +timestamp: 1774298163029 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda +sha256: 16f020f96da79db1863fcdd8f2b8f4f7d52f177dd4c58601e38e9182e91adf1d +md5: fb16b4b69e3f1dcfe79d80db8fd0c55d +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libpng >=1.6.55,<1.7.0a0 +- libzlib >=1.3.2,<2.0a0 +constrains: +- freetype >=2.14.3 +license: GPL-2.0-only OR FTL +size: 384575 +timestamp: 1774298162622 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda +sha256: 8e0a3b5e41272e5678499b5dfc4cddb673f9e935de01eb0767ce857001229f46 +md5: 57736f29cc2b0ec0b6c2952d3f101b6a +depends: +- __glibc >=2.17,<3.0.a0 +- _openmp_mutex >=4.5 +constrains: +- libgcc-ng ==15.2.0=*_19 +- libgomp 15.2.0 he0feb66_19 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 1041084 +timestamp: 1778269013026 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda +sha256: 9dcf54adfaa5e861123c2da4f2f0451a685464ea7e5a41ad91cf67b31d658d98 +md5: 331ee9b72b9dff570d56b1302c5ab37d +depends: +- libgcc 15.2.0 he0feb66_19 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 27694 +timestamp: 1778269016987 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda +sha256: 561a42758ef25b9ce308c4e2cf56daee4f06138385a17e29a492cd928e00be6f +md5: 42bf7eca1a951735fa06c0e3c0d5c8e6 +depends: +- libgfortran5 15.2.0 h68bc16d_19 +constrains: +- libgfortran-ng ==15.2.0=*_19 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 27655 +timestamp: 1778269042954 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda +sha256: 057978bb69fea29ed715a9b98adf71015c31baecc4aeb2bfc20d4fd5d83579d4 +md5: 85072b0ad177c966294f129b7c04a2d5 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=15.2.0 +constrains: +- libgfortran 15.2.0 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 2483673 +timestamp: 1778269025089 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda +sha256: 5abe4ab9d93f6c9757d654f1969ae2267d4505315c1f2f8fe705fd60af084f1b +md5: faac990cb7aedc7f3a2224f2c9b0c26c +depends: +- __glibc >=2.17,<3.0.a0 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 603817 +timestamp: 1778268942614 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda +sha256: 10056646c28115b174de81a44e23e3a0a3b95b5347d2e6c45cc6d49d35294256 +md5: 6178c6f2fb254558238ef4e6c56fb782 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +constrains: +- jpeg <0.0.0a +license: IJG AND BSD-3-Clause AND Zlib +size: 633831 +timestamp: 1775962768273 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-7_h47877c9_openblas.conda +build_number: 7 +sha256: 96962084921f197c9ad13fb7f8b324f2351d50ff3d8d962148751ad532f54a01 +md5: 6569b4f273740e25dc0dc7e3232c2a6c +depends: +- libblas 3.11.0 7_h4a7cf45_openblas +constrains: +- liblapacke 3.11.0 7*_openblas +- libcblas 3.11.0 7*_openblas +- blas 2.307 openblas +license: BSD-3-Clause +license_family: BSD +size: 18694 +timestamp: 1778489869038 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda +sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d +md5: b88d90cad08e6bc8ad540cb310a761fb +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +constrains: +- xz 5.8.3.* +license: 0BSD +size: 113478 +timestamp: 1775825492909 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda +sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 +md5: 2c21e66f50753a083cbe6b80f38268fa +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: BSD-2-Clause +license_family: BSD +size: 92400 +timestamp: 1769482286018 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda +sha256: 3d9aa85648e5e18a6d66db98b8c4317cc426721ad7a220aa86330d1ccedc8903 +md5: 2d3278b721e40468295ca755c3b84070 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libgfortran +- libgfortran5 >=14.3.0 +constrains: +- openblas >=0.3.33,<0.3.34.0a0 +license: BSD-3-Clause +license_family: BSD +size: 5931919 +timestamp: 1776993658641 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda +sha256: 377cfe037f3eeb3b1bf3ad333f724a64d32f315ee1958581fc671891d63d3f89 +md5: eba48a68a1a2b9d3c0d9511548db85db +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libzlib >=1.3.2,<2.0a0 +license: zlib-acknowledgement +size: 317729 +timestamp: 1776315175087 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda +sha256: 54cdcd3214313b62c2a8ee277e6f42150d9b748264c1b70d958bf735e420ef8d +md5: 7dc38adcbf71e6b38748e919e16e0dce +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libzlib >=1.3.2,<2.0a0 +license: blessing +size: 954962 +timestamp: 1777986471789 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda +sha256: dff1058c76ec6b8759e41cefa2508162d00e4a5e6721aa68ec3fd10094e702dc +md5: 5794b3bdc38177caf969dabd3af08549 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc 15.2.0 he0feb66_19 +constrains: +- libstdcxx-ng ==15.2.0=*_19 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 5852044 +timestamp: 1778269036376 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda +sha256: e5f8c38625aa6d567809733ae04bb71c161a42e44a9fa8227abe61fa5c60ebe0 +md5: cd5a90476766d53e901500df9215e927 +depends: +- __glibc >=2.17,<3.0.a0 +- lerc >=4.0.0,<5.0a0 +- libdeflate >=1.25,<1.26.0a0 +- libgcc >=14 +- libjpeg-turbo >=3.1.0,<4.0a0 +- liblzma >=5.8.1,<6.0a0 +- libstdcxx >=14 +- libwebp-base >=1.6.0,<2.0a0 +- libzlib >=1.3.1,<2.0a0 +- zstd >=1.5.7,<1.6.0a0 +license: HPND +size: 435273 +timestamp: 1762022005702 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.1-h5347b49_0.conda +sha256: 3f0edf1280e2f6684a986f821eaa3e123d2694a00b31b96ca0d4a4c12c129231 +md5: 7d0a66598195ef00b6efc55aefc7453b +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: BSD-3-Clause +license_family: BSD +size: 40163 +timestamp: 1779118517630 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda +sha256: 3aed21ab28eddffdaf7f804f49be7a7d701e8f0e46c856d801270b470820a37b +md5: aea31d2e5b1091feca96fcfe945c3cf9 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +constrains: +- libwebp 1.6.0 +license: BSD-3-Clause +license_family: BSD +size: 429011 +timestamp: 1752159441324 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda +sha256: 666c0c431b23c6cec6e492840b176dde533d48b7e6fb8883f5071223433776aa +md5: 92ed62436b625154323d40d5f2f11dd7 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +- pthread-stubs +- xorg-libxau >=1.0.11,<2.0a0 +- xorg-libxdmcp +license: MIT +license_family: MIT +size: 395888 +timestamp: 1727278577118 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda +sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 +md5: d87ff7921124eccd67248aa483c23fec +depends: +- __glibc >=2.17,<3.0.a0 +constrains: +- zlib 1.3.2 *_2 +license: Zlib +license_family: Other +size: 63629 +timestamp: 1774072609062 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.10.2-pyhcf101f3_0.conda +sha256: 20e0892592a3e7c683e3d66df704a9425d731486a97c34fc56af4da1106b2b6b +md5: ba0a9221ce1063f31692c07370d062f3 +depends: +- importlib-metadata >=4.4 +- python >=3.10 +- python +license: BSD-3-Clause +license_family: BSD +size: 85893 +timestamp: 1770694658918 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda +sha256: 0c4c35376fe920714390d46e4b8d31c876d65f18e1655899e0763ec25f2a902f +md5: 6d03368f2b2b0a5fb6839df53b2eb5e0 +depends: +- mdurl >=0.1,<1 +- python >=3.10 +license: MIT +license_family: MIT +size: 69017 +timestamp: 1778169663339 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda +sha256: c279be85b59a62d5c52f5dd9a4cd43ebd08933809a8416c22c3131595607d4cf +md5: 9a17c4307d23318476d7fbf0fedc0cde +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- python >=3.14,<3.15.0a0 +- python_abi 3.14.* *_cp314 +constrains: +- jinja2 >=3.0.0 +license: BSD-3-Clause +license_family: BSD +size: 27424 +timestamp: 1772445227915 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mathjax-2.7.7-ha770c72_3.tar.bz2 +sha256: 02fef69bde69db264a12f21386612262f545b6e3e68d8f1ccec19f3eaae58edf +md5: 86e69bd82c2a2c6fd29f5ab7e02b3691 +license: Apache-2.0 +license_family: Apache +size: 22281629 +timestamp: 1662784498331 +- conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda +sha256: 78c1bbe1723449c52b7a9df1af2ee5f005209f67e40b6e1d3c7619127c43b1c7 +md5: 592132998493b3ff25fd7479396e8351 +depends: +- python >=3.9 +license: MIT +license_family: MIT +size: 14465 +timestamp: 1733255681319 +- conda: https://conda.anaconda.org/bioconda/noarch/multiqc-1.35-pyhdfd78af_1.conda +sha256: e86033aa55a9e915e2d0957e770bdb81e3feb26a227d1adb17f9d6c528da6a71 +md5: cdb20309681ba3ce8f52c110e214d4f3 +depends: +- click +- coloredlogs +- humanize +- importlib-metadata +- jinja2 >=3.0.0 +- jsonschema +- markdown +- natsort +- numpy +- packaging +- pillow >=10.2.0 +- plotly >=5.18 +- polars >=1.34.0 +- polars-runtime-compat >=1.34.0 +- pyaml-env +- pydantic >=2.7.1 +- python >=3.9,!=3.14.1 +- python-dotenv +- python-kaleido 0.2.1 +- pyyaml >=4 +- requests +- rich >=10 +- rich-click +- spectra >=0.0.10 +- tiktoken +- tqdm +- typeguard >=4 +license: GPL-3.0-or-later +license_family: GPL3 +size: 4282188 +timestamp: 1779465338806 +- conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.21.2-pyhcf101f3_0.conda +sha256: 70f43d62450927d51673eecd8823e14f5b3cfebdb43cda1d502eba97162bab42 +md5: 6687827c332121727ce383919e1ec8c2 +depends: +- python >=3.10 +- python +license: MIT +license_family: MIT +size: 284323 +timestamp: 1778929680962 +- conda: https://conda.anaconda.org/conda-forge/noarch/natsort-8.4.0-pyhcf101f3_2.conda +sha256: aeb1548eb72e4f198e72f19d242fb695b35add2ac7b2c00e0d83687052867680 +md5: e941e85e273121222580723010bd4fa2 +depends: +- python >=3.9 +- python +license: MIT +license_family: MIT +size: 39262 +timestamp: 1770905275632 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda +sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3 +md5: fc21868a1a5aacc937e7a18747acb8a5 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: X11 AND BSD-3-Clause +size: 918956 +timestamp: 1777422145199 +- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda +sha256: f6a82172afc50e54741f6f84527ef10424326611503c64e359e25a19a8e4c1c6 +md5: a2c1eeadae7a309daed9d62c96012a2b +depends: +- python >=3.11 +- python +constrains: +- numpy >=1.25 +- scipy >=1.11.2 +- matplotlib-base >=3.8 +- pandas >=2.0 +license: BSD-3-Clause +license_family: BSD +size: 1587439 +timestamp: 1765215107045 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nspr-4.38-h29cc59b_0.conda +sha256: e3664264bd936c357523b55c71ed5a30263c6ba278d726a75b1eb112e6fb0b64 +md5: e235d5566c9cc8970eb2798dd4ecf62f +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libstdcxx >=14 +license: MPL-2.0 +license_family: MOZILLA +size: 228588 +timestamp: 1762348634537 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nss-3.118-h445c969_0.conda +sha256: 44dd98ffeac859d84a6dcba79a2096193a42fc10b29b28a5115687a680dd6aea +md5: 567fbeed956c200c1db5782a424e58ee +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libsqlite >=3.51.0,<4.0a0 +- libstdcxx >=14 +- libzlib >=1.3.1,<2.0a0 +- nspr >=4.38,<5.0a0 +license: MPL-2.0 +license_family: MOZILLA +size: 2057773 +timestamp: 1763485556350 +- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.6-py314h2b28147_0.conda +sha256: bc61ae892973751a6b0e6ecea57ed6d7053224bddcb007165d6ceb1d7344ad47 +md5: f49b5f950379e0b97c35ca97682f7c6a +depends: +- python +- libstdcxx >=14 +- libgcc >=14 +- __glibc >=2.17,<3.0.a0 +- liblapack >=3.9.0,<4.0a0 +- python_abi 3.14.* *_cp314 +- libblas >=3.9.0,<4.0a0 +- libcblas >=3.9.0,<4.0a0 +constrains: +- numpy-base <0a0 +license: BSD-3-Clause +license_family: BSD +size: 8928909 +timestamp: 1779169198391 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda +sha256: 3900f9f2dbbf4129cf3ad6acf4e4b6f7101390b53843591c53b00f034343bc4d +md5: 11b3379b191f63139e29c0d19dee24cd +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libpng >=1.6.50,<1.7.0a0 +- libstdcxx >=14 +- libtiff >=4.7.1,<4.8.0a0 +- libzlib >=1.3.1,<2.0a0 +license: BSD-2-Clause +license_family: BSD +size: 355400 +timestamp: 1758489294972 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda +sha256: c0ef482280e38c71a08ad6d71448194b719630345b0c9c60744a2010e8a8e0cb +md5: da1b85b6a87e141f5140bb9924cecab0 +depends: +- __glibc >=2.17,<3.0.a0 +- ca-certificates +- libgcc >=14 +license: Apache-2.0 +license_family: Apache +size: 3167099 +timestamp: 1775587756857 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda +sha256: 3906abfb6511a3bb309e39b9b1b7bc38f50a723971de2395489fd1f379255890 +md5: 4c06a92e74452cfa53623a81592e8934 +depends: +- python >=3.8 +- python +license: Apache-2.0 +license_family: APACHE +size: 91574 +timestamp: 1777103621679 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.2.0-py314h8ec4b1a_0.conda +sha256: 123d8a7c16c88658b4f29e9f115a047598c941708dade74fbaff373a32dbec5e +md5: 76c4757c0ec9d11f969e8eb44899307b +depends: +- python +- libgcc >=14 +- __glibc >=2.17,<3.0.a0 +- libtiff >=4.7.1,<4.8.0a0 +- openjpeg >=2.5.4,<3.0a0 +- libxcb >=1.17.0,<2.0a0 +- libwebp-base >=1.6.0,<2.0a0 +- zlib-ng >=2.3.3,<2.4.0a0 +- libjpeg-turbo >=3.1.2,<4.0a0 +- python_abi 3.14.* *_cp314 +- libfreetype >=2.14.3 +- libfreetype6 >=2.14.3 +- lcms2 >=2.18,<3.0a0 +- tk >=8.6.13,<8.7.0a0 +license: HPND +size: 1082797 +timestamp: 1775060059882 +- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda +sha256: c418d325359fc7a0074cea7f081ef1bce26e114d2da8a0154c5d27ecc87a08e7 +md5: 3e9427ee186846052e81fadde8ebe96a +depends: +- narwhals >=1.15.1 +- packaging +- python >=3.10 +constrains: +- ipywidgets >=7.6 +license: MIT +license_family: MIT +size: 5251872 +timestamp: 1772628857717 +- conda: https://conda.anaconda.org/conda-forge/noarch/polars-1.41.0-pyh58ad624_0.conda +sha256: 70fc56877c4a095ee658d61924d8019768fbae4a48437058d181fc94b0a7c4d8 +md5: 25a883fed9f1f3f21ff317a3e7c92ac4 +depends: +- polars-runtime-32 ==1.41.0 +- python >=3.10 +- python +constrains: +- numpy >=1.16.0 +- pyarrow >=7.0.0 +- fastexcel >=0.9 +- openpyxl >=3.0.0 +- xlsx2csv >=0.8.0 +- connectorx >=0.3.2 +- deltalake >=1.0.0 +- pyiceberg >=0.7.1 +- altair >=5.4.0 +- great_tables >=0.8.0 +- polars-runtime-32 ==1.41.0 +- polars-runtime-64 ==1.41.0 +- polars-runtime-compat ==1.41.0 +license: MIT +size: 539656 +timestamp: 1779630790562 +- conda: https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-32-1.41.0-py310h49dadd8_0.conda +noarch: python +sha256: e51ee3fe5259f2e115b2f78f8fbe3554e419c7c82b0c110878e12a5ff95ce3ab +md5: 7682765a1588e5ac887c99736d297c93 +depends: +- python +- __glibc >=2.17,<3.0.a0 +- libstdcxx >=14 +- libgcc >=14 +- _python_abi3_support 1.* +- cpython >=3.10 +constrains: +- __glibc >=2.17 +license: MIT +size: 42578921 +timestamp: 1779630790562 +- conda: https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-compat-1.41.0-py310hcbd6021_0.conda +noarch: python +sha256: 29c3831c92394af11d9f7d04882dda9479ffbb76a3d36ba155d52159d67805fa +md5: cb0b620c9914a07a9022cb8b183ea9ee +depends: +- python +- libstdcxx >=14 +- libgcc >=14 +- __glibc >=2.17,<3.0.a0 +- _python_abi3_support 1.* +- cpython >=3.10 +constrains: +- __glibc >=2.17 +license: MIT +size: 41864944 +timestamp: 1779630722548 +- conda: https://conda.anaconda.org/conda-forge/linux-64/procps-ng-4.0.6-h18c060e_0.conda +sha256: 4ce2e1ee31a6217998f78c31ce7dc0a3e0557d9238b51d49dd20c52d467a126d +md5: f2c23a77b25efcad57d377b34bd84941 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- ncurses >=6.5,<7.0a0 +license: GPL-2.0-or-later AND LGPL-2.0-or-later +license_family: GPL +size: 593603 +timestamp: 1769710381284 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda +sha256: 9c88f8c64590e9567c6c80823f0328e58d3b1efb0e1c539c0315ceca764e0973 +md5: b3c17d95b5a10c6e64a21fa17573e70e +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=13 +license: MIT +license_family: MIT +size: 8252 +timestamp: 1726802366959 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyaml-env-1.2.2-pyhd8ed1ab_0.conda +sha256: 58994e0d2ea8584cb399546e6f6896d771995e6121d1a7b6a2c9948388358932 +md5: e17be1016bcc3516827b836cd3e4d9dc +depends: +- python >=3.9 +- pyyaml >=5.0,<=7.0 +license: MIT +license_family: MIT +size: 14645 +timestamp: 1736766960536 +- conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda +sha256: 69700e31165df070e9716315e042196aa92525dae5deb5107785847ab9f4189f +md5: 729843edafc0899b3348bd3f19525b9d +depends: +- typing-inspection >=0.4.2 +- typing_extensions >=4.14.1 +- python >=3.10 +- annotated-types >=0.6.0 +- pydantic-core ==2.46.4 +- python +license: MIT +license_family: MIT +size: 346511 +timestamp: 1778103405862 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py314h2e6c369_0.conda +sha256: 802e216c39f1359aed60823b6e11d8ccd812b0ae1c81ae5ac1c81f99446409ab +md5: 0c96993dbeadf3a277cf757b9f1c9412 +depends: +- python +- typing-extensions >=4.6.0,!=4.7.0 +- libgcc >=14 +- __glibc >=2.17,<3.0.a0 +- python_abi 3.14.* *_cp314 +constrains: +- __glibc >=2.17 +license: MIT +license_family: MIT +size: 1895020 +timestamp: 1778084229247 +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda +sha256: cf70b2f5ad9ae472b71235e5c8a736c9316df3705746de419b59d442e8348e86 +md5: 16c18772b340887160c79a6acc022db0 +depends: +- python >=3.10 +license: BSD-2-Clause +license_family: BSD +size: 893031 +timestamp: 1774796815820 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda +sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 +md5: 461219d1a5bd61342293efa2c0c90eac +depends: +- __unix +- python >=3.9 +license: BSD-3-Clause +license_family: BSD +size: 21085 +timestamp: 1733217331982 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.5-habeac84_100_cp314.conda +build_number: 100 +sha256: 55eed9bf2a3f6e90311276f0834737fe7c2d9ec3e5e2e557507858df4c7521e6 +md5: da92e59ff92f2d5ede4f612af20f583f +depends: +- __glibc >=2.17,<3.0.a0 +- bzip2 >=1.0.8,<2.0a0 +- ld_impl_linux-64 >=2.36.1 +- libexpat >=2.8.0,<3.0a0 +- libffi >=3.5.2,<3.6.0a0 +- libgcc >=14 +- liblzma >=5.8.3,<6.0a0 +- libmpdec >=4.0.0,<5.0a0 +- libsqlite >=3.53.1,<4.0a0 +- libuuid >=2.42.1,<3.0a0 +- libzlib >=1.3.2,<2.0a0 +- ncurses >=6.6,<7.0a0 +- openssl >=3.5.6,<4.0a0 +- python_abi 3.14.* *_cp314 +- readline >=8.3,<9.0a0 +- tk >=8.6.13,<8.7.0a0 +- tzdata +- zstd >=1.5.7,<1.6.0a0 +license: Python-2.0 +size: 36745188 +timestamp: 1779236923603 +python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda +sha256: 74e417a768f59f02a242c25e7db0aa796627b5bc8c818863b57786072aeb85e5 +md5: 130584ad9f3a513cdd71b1fdc1244e9c +depends: +- python >=3.10 +license: BSD-3-Clause +license_family: BSD +size: 27848 +timestamp: 1772388605021 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.5-h4df99d1_100.conda +sha256: 41dd7da285d71d519257fa7dacb1cae060d5ebfaa5f92cba5994899d2978e943 +md5: 41954747ba952ec4b01e16c2c9e8d8ff +depends: +- cpython 3.14.5.* +- python_abi * *_cp314 +license: Python-2.0 +size: 50212 +timestamp: 1779236703009 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-kaleido-0.2.1-pyhd8ed1ab_0.tar.bz2 +sha256: e17bf63a30aec33432f1ead86e15e9febde9fc40a7f869c0e766be8d2db44170 +md5: 310259a5b03ff02289d7705f39e2b1d2 +depends: +- kaleido-core 0.2.1.* +- python >=3.5 +license: MIT +license_family: MIT +size: 18320 +timestamp: 1615204747600 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda +build_number: 8 +sha256: ad6d2e9ac39751cc0529dd1566a26751a0bf2542adb0c232533d32e176e21db5 +md5: 0539938c55b6b1a59b560e843ad864a4 +constrains: +- python 3.14.* *_cp314 +license: BSD-3-Clause +license_family: BSD +size: 6989 +timestamp: 1752805904792 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda +sha256: b318fb070c7a1f89980ef124b80a0b5ccf3928143708a85e0053cde0169c699d +md5: 2035f68f96be30dc60a5dfd7452c7941 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- python >=3.14,<3.15.0a0 +- python_abi 3.14.* *_cp314 +- yaml >=0.2.5,<0.3.0a0 +license: MIT +license_family: MIT +size: 202391 +timestamp: 1770223462836 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda +sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 +md5: d7d95fc8287ea7bf33e0e7116d2b95ec +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- ncurses >=6.5,<7.0a0 +license: GPL-3.0-only +license_family: GPL +size: 345073 +timestamp: 1765813471974 +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda +sha256: 0577eedfb347ff94d0f2fa6c052c502989b028216996b45c7f21236f25864414 +md5: 870293df500ca7e18bedefa5838a22ab +depends: +- attrs >=22.2.0 +- python >=3.10 +- rpds-py >=0.7.0 +- typing_extensions >=4.4.0 +- python +license: MIT +license_family: MIT +size: 51788 +timestamp: 1760379115194 +- conda: https://conda.anaconda.org/conda-forge/linux-64/regex-2026.5.9-py314h5bd0f2a_0.conda +sha256: c7a4aca4977c15c82d053b06cbc676460974c1b25757cfeea8a9a2497ac911f8 +md5: 9dd235b6ac69a0198080dac39f9891aa +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- python >=3.14,<3.15.0a0 +- python_abi 3.14.* *_cp314 +license: Apache-2.0 AND CNRI-Python +license_family: PSF +size: 413611 +timestamp: 1778374155646 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda +sha256: 1715246b19c9f85ee022933b4845f2fc14ac9184981b7b7d9b728bec8e9588da +md5: 4a85203c1d80c1059086ae860836ffb9 +depends: +- python >=3.10 +- certifi >=2023.5.7 +- charset-normalizer >=2,<4 +- idna >=2.5,<4 +- urllib3 >=1.26,<3 +- python +constrains: +- chardet >=3.0.2,<8 +license: Apache-2.0 +license_family: APACHE +size: 68709 +timestamp: 1778851103479 +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda +sha256: 3d6ba2c0fcdac3196ba2f0615b4104e532525ffa1335b50a2878be5ff488814a +md5: 0242025a3c804966bf71aa04eee82f66 +depends: +- markdown-it-py >=2.2.0 +- pygments >=2.13.0,<3.0.0 +- python >=3.10 +- typing_extensions >=4.0.0,<5.0.0 +- python +license: MIT +license_family: MIT +size: 208577 +timestamp: 1775991661559 +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-click-1.9.7-pyh8f84b5b_0.conda +sha256: aa3fcb167321bae51998de2e94d199109c9024f25a5a063cb1c28d8f1af33436 +md5: 0c20a8ebcddb24a45da89d5e917e6cb9 +depends: +- python >=3.10 +- rich >=12 +- click >=8 +- typing-extensions >=4 +- __unix +- python +license: MIT +license_family: MIT +size: 64356 +timestamp: 1769850479089 +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py314h2e6c369_0.conda +sha256: e53b0cbf3b324eaa03ca1fe1a688fdf4ab42cea9c25270b0a7307d8aaaa4f446 +md5: c1c368b5437b0d1a68f372ccf01cb133 +depends: +- python +- libgcc >=14 +- __glibc >=2.17,<3.0.a0 +- python_abi 3.14.* *_cp314 +constrains: +- __glibc >=2.17 +license: MIT +license_family: MIT +size: 376121 +timestamp: 1764543122774 +- conda: https://conda.anaconda.org/conda-forge/noarch/spectra-0.0.11-pyhd8ed1ab_2.conda +sha256: 7c65782d2511738e62c70462e89d65da4fa54d5a7e47c46667bcd27a59f81876 +md5: 472239e4eb7b5a84bb96b3ed7e3a596a +depends: +- colormath >=3.0.0 +- python >=3.9 +license: MIT +license_family: MIT +size: 22284 +timestamp: 1735770589188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.53.1-hbc0de68_0.conda +sha256: d167fa92781bcdcd3b9aaa6bb1cd50c5b108f6190c170098a118b5cf5df2f881 +md5: 8e0b8654ead18e50af552e54b5a08a61 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libsqlite 3.53.1 h0c1763c_0 +- libzlib >=1.3.2,<2.0a0 +- ncurses >=6.6,<7.0a0 +- readline >=8.3,<9.0a0 +license: blessing +size: 205399 +timestamp: 1777986477546 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tiktoken-0.12.0-py314h67fec18_3.conda +sha256: 7e395d67fd249d901beb1ae269057763c0d8c3ee5f7a348694bdb16d158a37d9 +md5: d705f9d8a1185a2b01cced191177a028 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libstdcxx >=14 +- python >=3.14,<3.15.0a0 +- python_abi 3.14.* *_cp314 +- regex >=2022.1.18 +- requests >=2.26.0 +constrains: +- __glibc >=2.17 +license: MIT +license_family: MIT +size: 939648 +timestamp: 1764028306357 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda +sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac +md5: cffd3bdd58090148f4cfcd831f4b26ab +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libzlib >=1.3.1,<2.0a0 +constrains: +- xorg-libx11 >=1.8.12,<2.0a0 +license: TCL +license_family: BSD +size: 3301196 +timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda +sha256: 9ef8e47cf00e4d6dcc114eb32a1504cc18206300572ef14d76634ba29dfe1eb6 +md5: e5ce43272193b38c2e9037446c1d9206 +depends: +- python >=3.10 +- __unix +- python +license: MPL-2.0 and MIT +size: 94132 +timestamp: 1770153424136 +- conda: https://conda.anaconda.org/conda-forge/noarch/typeguard-4.5.2-pyhcf101f3_0.conda +sha256: 59d7851d32fddb5b510272e6557aa982edeb927d349648dac27f5bf01d18bb26 +md5: 4460f039b7dedf15f7df086446ca75ae +depends: +- typing_extensions >=4.14.0 +- python >=3.10 +- importlib-metadata >=3.6 +- python +constrains: +- pytest >=7 +license: MIT +license_family: MIT +size: 38297 +timestamp: 1778779291237 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda +sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c +md5: edd329d7d3a4ab45dcf905899a7a6115 +depends: +- typing_extensions ==4.15.0 pyhcf101f3_0 +license: PSF-2.0 +license_family: PSF +size: 91383 +timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda +sha256: 8b90d2f19f9458b8c58a55e1fcdc1d90c1603a847a47654d8a454549413ba60a +md5: 53f5409c5cfd6c5a66417d68e3f0a864 +depends: +- python >=3.10 +- typing_extensions >=4.12.0 +- python +license: MIT +license_family: MIT +size: 20935 +timestamp: 1777105465795 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda +sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 +md5: 0caa1af407ecff61170c9437a808404d +depends: +- python >=3.10 +- python +license: PSF-2.0 +license_family: PSF +size: 51692 +timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda +sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c +md5: ad659d0a2b3e47e38d829aa8cad2d610 +license: LicenseRef-Public-Domain +size: 119135 +timestamp: 1767016325805 +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda +sha256: feff959a816f7988a0893201aa9727bbb7ee1e9cec2c4f0428269b489eb93fb4 +md5: cbb88288f74dbe6ada1c6c7d0a97223e +depends: +- backports.zstd >=1.0.0 +- brotli-python >=1.2.0 +- h2 >=4,<5 +- pysocks >=1.5.6,<2.0,!=1.5.7 +- python >=3.10 +license: MIT +license_family: MIT +size: 103560 +timestamp: 1778188657149 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda +sha256: 6bc6ab7a90a5d8ac94c7e300cc10beb0500eeba4b99822768ca2f2ef356f731b +md5: b2895afaf55bf96a8c8282a2e47a5de0 +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: MIT +license_family: MIT +size: 15321 +timestamp: 1762976464266 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda +sha256: 25d255fb2eef929d21ff660a0c687d38a6d2ccfbcbf0cc6aa738b12af6e9d142 +md5: 1dafce8548e38671bea82e3f5c6ce22f +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +license: MIT +license_family: MIT +size: 20591 +timestamp: 1762976546182 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda +sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad +md5: a77f85f77be52ff59391544bfe73390a +depends: +- libgcc >=14 +- __glibc >=2.17,<3.0.a0 +license: MIT +license_family: MIT +size: 85189 +timestamp: 1753484064210 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda +sha256: 210bd31c22bb88f5e2a167df24c95bb5f152b2ada7502f9b8c49d1f5366db423 +md5: ba3dcdc8584155c97c648ae9c044b7a3 +depends: +- python >=3.10 +- python +license: MIT +license_family: MIT +size: 24190 +timestamp: 1779159948016 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda +sha256: ea4e50c465d70236408cb0bfe0115609fd14db1adcd8bd30d8918e0291f8a75f +md5: 2aadb0d17215603a82a2a6b0afd9a4cb +depends: +- __glibc >=2.17,<3.0.a0 +- libgcc >=14 +- libstdcxx >=14 +license: Zlib +license_family: Other +size: 122618 +timestamp: 1770167931827 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda +sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 +md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 +depends: +- __glibc >=2.17,<3.0.a0 +- libzlib >=1.3.1,<2.0a0 +license: BSD-3-Clause +license_family: BSD +size: 601375 +timestamp: 1764777111296 diff --git a/modules/nf-core/multiqc/.conda-lock/linux_arm64-bd-5c84a5000a226ab5_1.txt b/modules/nf-core/multiqc/.conda-lock/linux_arm64-bd-5c84a5000a226ab5_1.txt new file mode 100644 index 0000000..3d5b93d --- /dev/null +++ b/modules/nf-core/multiqc/.conda-lock/linux_arm64-bd-5c84a5000a226ab5_1.txt @@ -0,0 +1,1476 @@ + +version: 6 +environments: +default: +channels: +- url: https://conda.anaconda.org/conda-forge/ +- url: https://conda.anaconda.org/bioconda/ +- url: https://conda.anaconda.org/bioconda/ +options: +pypi-prerelease-mode: if-necessary-or-explicit +packages: +linux-aarch64: +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.2.0-py314h352cb57_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.5.20-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.4.0-pyhc90fa1f_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/coloredlogs-15.0.1-pyhd8ed1ab_4.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/colormath-3.0.0-pyhd8ed1ab_4.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.5-py314hd8ed1ab_100.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/expat-2.8.1-hfae3067_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.18.0-hba86a56_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh707e725_8.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/humanize-4.15.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.15-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/kaleido-core-0.2.1-he5a581e_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lcms2-2.19.1-h9d5b58d_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45.1-default_h1979696_102.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.1.0-h52b7260_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libblas-3.11.0-7_haddc8a3_openblas.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.11.0-7_hd72aa62_openblas.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.8.1-hfae3067_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.3-h8af1aa0_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.3-hdae7a39_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-15.2.0-he9431aa_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-15.2.0-h1b7bec0_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.1.4.1-he30d5cf_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblapack-3.11.0-7_h88aeb00_openblas.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.3-he30d5cf_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-he30d5cf_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenblas-0.3.33-pthreads_h9d3fd7e_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.58-h1abf092_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.53.1-h022381a_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_19.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.1-hdb009f0_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42.1-h1022ec0_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.10.2-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.3-py314hb76de3f_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mathjax-2.7.7-h8af1aa0_3.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda +- conda: https://conda.anaconda.org/bioconda/noarch/multiqc-1.35-pyhdfd78af_1.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.21.2-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/natsort-8.4.0-pyhcf101f3_2.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.6-hf8d1292_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nspr-4.38-h3ad9384_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nss-3.118-h544fa81_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/numpy-2.4.6-py314he1698a1_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openjpeg-2.5.4-h5da879a_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.2-h546c87b_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pillow-12.2.0-py314hac3e5ec_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/polars-1.41.0-pyh58ad624_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/polars-runtime-32-1.41.0-py310h32c7c23_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/polars-runtime-compat-1.41.0-py310hc0e61be_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/procps-ng-4.0.6-h1779866_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/pyaml-env-1.2.2-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pydantic-core-2.46.4-py314h451b6cc_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.14.5-hfd9ac0a_100_cp314.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.5-h4df99d1_100.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/python-kaleido-0.2.1-pyhd8ed1ab_0.tar.bz2 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py314h807365f_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/regex-2026.5.9-py314h51f160d_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-click-1.9.7-pyh8f84b5b_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.30.0-py314h02b7a91_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/spectra-0.0.11-pyhd8ed1ab_2.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sqlite-3.53.1-he8854b5_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tiktoken-0.12.0-py314h6a36e60_3.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/typeguard-4.5.2-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-he30d5cf_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-he30d5cf_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-ng-2.3.3-ha7cb516_1.conda +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda +build_number: 20 +sha256: a2527b1d81792a0ccd2c05850960df119c2b6d8f5fdec97f2db7d25dc23b1068 +md5: 468fd3bb9e1f671d36c2cbc677e56f1d +depends: +- libgomp >=7.5.0 +constrains: +- openmp_impl <0.0a0 +license: BSD-3-Clause +license_family: BSD +size: 28926 +timestamp: 1770939656741 +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda +sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 +md5: aaa2a381ccc56eac91d63b6c1240312f +depends: +- cpython +- python-gil +license: MIT +license_family: MIT +size: 8191 +timestamp: 1744137672556 +- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda +sha256: e0ea1ba78fbb64f17062601edda82097fcf815012cf52bb704150a2668110d48 +md5: 2934f256a8acfe48f6ebb4fce6cde29c +depends: +- python >=3.9 +- typing-extensions >=4.0.0 +license: MIT +license_family: MIT +size: 18074 +timestamp: 1733247158254 +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda +sha256: 1b6124230bb4e571b1b9401537ecff575b7b109cc3a21ee019f65e083b8399ab +md5: c6b0543676ecb1fb2d7643941fe375f2 +depends: +- python >=3.10 +- python +license: MIT +license_family: MIT +size: 64927 +timestamp: 1773935801332 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda +noarch: generic +sha256: a1c97297e867776760489537bc5ae36fa83a154be30e3b79385a39ca4cb058fe +md5: 1133126d840e75287d83947be3fc3e71 +depends: +- python >=3.14 +license: BSD-3-Clause AND MIT AND EPL-2.0 +size: 7533 +timestamp: 1778594057496 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.2.0-py314h352cb57_1.conda +sha256: 5a5b0cdcd7ed89c6a8fb830924967f6314a2b71944bc1ebc2c105781ba97aa75 +md5: a1b5c571a0923a205d663d8678df4792 +depends: +- libgcc >=14 +- libstdcxx >=14 +- python >=3.14,<3.15.0a0 +- python >=3.14,<3.15.0a0 *_cp314 +- python_abi 3.14.* *_cp314 +constrains: +- libbrotlicommon 1.2.0 he30d5cf_1 +license: MIT +license_family: MIT +size: 373193 +timestamp: 1764017486851 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda +sha256: b3495077889dde6bb370938e7db82be545c73e8589696ad0843a32221520ad4c +md5: 840d8fc0d7b3209be93080bc20e07f2d +depends: +- libgcc >=14 +license: bzip2-1.0.6 +license_family: BSD +size: 192412 +timestamp: 1771350241232 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda +sha256: 9812a303a1395e1dafbd92e5bc8a1ff6013bcbba0a09c7f03a8d23e43560aa9b +md5: 489b8e97e666c93f68fdb35c3c9b957f +depends: +- __unix +license: ISC +size: 129868 +timestamp: 1779289852439 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.5.20-pyhd8ed1ab_0.conda +sha256: 645655a3510e38e625da136595f3f16f2130c3263630cc3bc8f60f619ddbe490 +md5: 9fefff2f745ea1cc2ef15211a20c054a +depends: +- python >=3.10 +license: ISC +size: 134201 +timestamp: 1779285131141 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda +sha256: 3f9483d62ce24ecd063f8a5a714448445dc8d9e201147c46699fc0033e824457 +md5: a9167b9571f3baa9d448faa2139d1089 +depends: +- python >=3.10 +license: MIT +license_family: MIT +size: 58872 +timestamp: 1775127203018 +- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.4.0-pyhc90fa1f_0.conda +sha256: 99ab8ef815c4520cce3a7482c2513f377c14348206857661d84c76a55e030f97 +md5: 003767c47f1f0a474c4de268b57839c3 +depends: +- __unix +- python +- python >=3.10 +license: BSD-3-Clause +license_family: BSD +size: 104631 +timestamp: 1779108494556 +- conda: https://conda.anaconda.org/conda-forge/noarch/coloredlogs-15.0.1-pyhd8ed1ab_4.conda +sha256: 8021c76eeadbdd5784b881b165242db9449783e12ce26d6234060026fd6a8680 +md5: b866ff7007b934d564961066c8195983 +depends: +- humanfriendly >=9.1 +- python >=3.9 +license: MIT +license_family: MIT +size: 43758 +timestamp: 1733928076798 +- conda: https://conda.anaconda.org/conda-forge/noarch/colormath-3.0.0-pyhd8ed1ab_4.conda +sha256: 59c9e29800b483b390467f90e82b0da3a4fbf0612efe1c90813fca232780e160 +md5: 071cf7b0ce333c81718b054066c15102 +depends: +- networkx >=2.0 +- numpy +- python >=3.9 +license: BSD-3-Clause +license_family: BSD +size: 39326 +timestamp: 1735759976140 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.5-py314hd8ed1ab_100.conda +noarch: generic +sha256: 777882d2685f368417f31bbe1b28f73687fc6c8f6a5768bda20ffeefa6b07f5b +md5: a749029ce5d0632a913db19d17f944ab +depends: +- python >=3.14,<3.15.0a0 +- python_abi * *_cp314 +license: Python-2.0 +size: 50212 +timestamp: 1779236682725 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/expat-2.8.1-hfae3067_0.conda +sha256: a9cd5eb1700e11cc39acc36630a2d72a4e317943bd7c5695cd8804419f04ff42 +md5: 89f0247b3cea528d8ad1a6664a313153 +depends: +- libexpat 2.8.1 hfae3067_0 +- libgcc >=14 +license: MIT +license_family: MIT +size: 140114 +timestamp: 1779278679081 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 +sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b +md5: 0c96522c6bdaed4b1566d11387caaf45 +license: BSD-3-Clause +license_family: BSD +size: 397370 +timestamp: 1566932522327 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 +sha256: c52a29fdac682c20d252facc50f01e7c2e7ceac52aa9817aaf0bb83f7559ec5c +md5: 34893075a5c9e55cdafac56607368fc6 +license: OFL-1.1 +license_family: Other +size: 96530 +timestamp: 1620479909603 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 +sha256: 00925c8c055a2275614b4d983e1df637245e19058d79fc7dd1a93b8d9fb4b139 +md5: 4d59c254e01d9cde7957100457e2d5fb +license: OFL-1.1 +license_family: Other +size: 700814 +timestamp: 1620479612257 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda +sha256: 2821ec1dc454bd8b9a31d0ed22a7ce22422c0aef163c59f49dfdf915d0f0ca14 +md5: 49023d73832ef61042f6a237cb2687e7 +license: LicenseRef-Ubuntu-Font-Licence-Version-1.0 +license_family: Other +size: 1620504 +timestamp: 1727511233259 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.18.0-hba86a56_0.conda +sha256: 1805f4ab3d9e1734a5a17abccc2cb0fdade51d4d5f29bdc410600ea0115ec050 +md5: b660d59a9d0fb3297327418624acaec3 +depends: +- libexpat >=2.8.1,<3.0a0 +- libfreetype >=2.14.3 +- libfreetype6 >=2.14.3 +- libgcc >=14 +- libuuid >=2.42.1,<3.0a0 +- libzlib >=1.3.2,<2.0a0 +license: MIT +license_family: MIT +size: 293348 +timestamp: 1779421661332 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda +sha256: 54eea8469786bc2291cc40bca5f46438d3e062a399e8f53f013b6a9f50e98333 +md5: a7970cd949a077b7cb9696379d338681 +depends: +- font-ttf-ubuntu +- font-ttf-inconsolata +- font-ttf-dejavu-sans-mono +- font-ttf-source-code-pro +license: BSD-3-Clause +license_family: BSD +size: 4059 +timestamp: 1762351264405 +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda +sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 +md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 +depends: +- python >=3.10 +- hyperframe >=6.1,<7 +- hpack >=4.1,<5 +- python +license: MIT +license_family: MIT +size: 95967 +timestamp: 1756364871835 +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda +sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba +md5: 0a802cb9888dd14eeefc611f05c40b6e +depends: +- python >=3.9 +license: MIT +license_family: MIT +size: 30731 +timestamp: 1737618390337 +- conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh707e725_8.conda +sha256: fa2071da7fab758c669e78227e6094f6b3608228740808a6de5d6bce83d9e52d +md5: 7fe569c10905402ed47024fc481bb371 +depends: +- __unix +- python >=3.9 +license: MIT +license_family: MIT +size: 73563 +timestamp: 1733928021866 +- conda: https://conda.anaconda.org/conda-forge/noarch/humanize-4.15.0-pyhd8ed1ab_0.conda +sha256: 6c4343b376d0b12a4c75ab992640970d36c933cad1fd924f6a1181fa91710e80 +md5: daddf757c3ecd6067b9af1df1f25d89e +depends: +- python >=3.10 +license: MIT +license_family: MIT +size: 67994 +timestamp: 1766267728652 +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda +sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 +md5: 8e6923fc12f1fe8f8c4e5c9f343256ac +depends: +- python >=3.9 +license: MIT +license_family: MIT +size: 17397 +timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.15-pyhcf101f3_0.conda +sha256: 3d25f9f6f7ab3e1ce6429fc8c8aae0335cf446692e715068488536d220cc43de +md5: 1b9083b7f00609605d1483dbc6071a81 +depends: +- python >=3.10 +- python +license: BSD-3-Clause +license_family: BSD +size: 62642 +timestamp: 1779294335905 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda +sha256: 43e2a5497cad1598ff88a3e69f69bc88b7b8f141fa63c60eab5db296317318b8 +md5: ffc17e785d64e12fc311af9184221839 +depends: +- python >=3.10 +- zipp >=3.20 +- python +license: Apache-2.0 +size: 34766 +timestamp: 1779714582554 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda +sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b +md5: 04558c96691bed63104678757beb4f8d +depends: +- markupsafe >=2.0 +- python >=3.10 +- python +license: BSD-3-Clause +license_family: BSD +size: 120685 +timestamp: 1764517220861 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda +sha256: db973a37d75db8e19b5f44bbbdaead0c68dde745407f281e2a7fe4db74ec51d7 +md5: ada41c863af263cc4c5fcbaff7c3e4dc +depends: +- attrs >=22.2.0 +- jsonschema-specifications >=2023.3.6 +- python >=3.10 +- referencing >=0.28.4 +- rpds-py >=0.25.0 +- python +license: MIT +license_family: MIT +size: 82356 +timestamp: 1767839954256 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda +sha256: 0a4f3b132f0faca10c89fdf3b60e15abb62ded6fa80aebfc007d05965192aa04 +md5: 439cd0f567d697b20a8f45cb70a1005a +depends: +- python >=3.10 +- referencing >=0.31.0 +- python +license: MIT +license_family: MIT +size: 19236 +timestamp: 1757335715225 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/kaleido-core-0.2.1-he5a581e_0.tar.bz2 +sha256: d3c7f4797566e6f983d16c2a87063a18e4b2d819a66230190a21584d70042755 +md5: 4f0d284f5d11e04277b552eb1c172c7f +depends: +- __glibc >=2.17,<3.0.a0 +- expat >=2.2.10,<3.0.0a0 +- fontconfig +- fonts-conda-forge +- libgcc-ng >=9.3.0 +- mathjax 2.7.* +- nspr >=4.29,<5.0a0 +- nss >=3.62,<4.0a0 +- sqlite >=3.34.0,<4.0a0 +license: MIT +license_family: MIT +size: 65750397 +timestamp: 1615199465742 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lcms2-2.19.1-h9d5b58d_0.conda +sha256: 1e5f68e4b36a0e1a278c6dc026bc3d7775518a15832cbc9d7fc1c0e4c47784b1 +md5: b1f8bee3c53a6d2c103fb4a1ae44f5c4 +depends: +- libgcc >=14 +- libjpeg-turbo >=3.1.4.1,<4.0a0 +- libtiff >=4.7.1,<4.8.0a0 +license: MIT +license_family: MIT +size: 296899 +timestamp: 1778079402392 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45.1-default_h1979696_102.conda +sha256: 7abd913d81a9bf00abb699e8987966baa2065f5132e37e815f92d90fc6bba530 +md5: a21644fc4a83da26452a718dc9468d5f +depends: +- zstd >=1.5.7,<1.6.0a0 +constrains: +- binutils_impl_linux-aarch64 2.45.1 +license: GPL-3.0-only +license_family: GPL +size: 875596 +timestamp: 1774197520746 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.1.0-h52b7260_0.conda +sha256: 8957fd460c1c132c8031f65fd5f56ec3807fd71b7cab2c5e2b0937b13404ab36 +md5: d13423b06447113a90b5b1366d4da171 +depends: +- libgcc >=14 +- libstdcxx >=14 +license: Apache-2.0 +license_family: Apache +size: 240444 +timestamp: 1773114901155 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libblas-3.11.0-7_haddc8a3_openblas.conda +build_number: 7 +sha256: f27ba323c2f1e1731b5e880fe520f178f55047f25be94f77e649605b2343c066 +md5: e8d07b777f6ff1fab69665336561910b +depends: +- libopenblas >=0.3.33,<0.3.34.0a0 +- libopenblas >=0.3.33,<1.0a0 +constrains: +- liblapack 3.11.0 7*_openblas +- libcblas 3.11.0 7*_openblas +- mkl <2027 +- blas 2.307 openblas +- liblapacke 3.11.0 7*_openblas +license: BSD-3-Clause +license_family: BSD +size: 18696 +timestamp: 1778489796402 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.11.0-7_hd72aa62_openblas.conda +build_number: 7 +sha256: c8f0192362966df0828419f042d6f94c079e5df00ad6bd05b5e84c12b42f8cc7 +md5: 90ac57b82c055faa9be25031864b7d8f +depends: +- libblas 3.11.0 7_haddc8a3_openblas +constrains: +- liblapack 3.11.0 7*_openblas +- blas 2.307 openblas +- liblapacke 3.11.0 7*_openblas +license: BSD-3-Clause +license_family: BSD +size: 18664 +timestamp: 1778489802790 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda +sha256: 48814b73bd462da6eed2e697e30c060ae16af21e9fbed30d64feaf0aad9da392 +md5: a9138815598fe6b91a1d6782ca657b0c +depends: +- libgcc >=14 +license: MIT +license_family: MIT +size: 71117 +timestamp: 1761979776756 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.8.1-hfae3067_0.conda +sha256: 1fc392b997c6ee2bd3226a7cd870d0edbcbb367e25f9f18dd4a7025fced6efc0 +md5: 513dd884361dfb8a554298ed69b58823 +depends: +- libgcc >=14 +constrains: +- expat 2.8.1.* +license: MIT +license_family: MIT +size: 77140 +timestamp: 1779278671302 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda +sha256: 3df4c539449aabc3443bbe8c492c01d401eea894603087fca2917aa4e1c2dea9 +md5: 2f364feefb6a7c00423e80dcb12db62a +depends: +- libgcc >=14 +license: MIT +license_family: MIT +size: 55952 +timestamp: 1769456078358 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.3-h8af1aa0_0.conda +sha256: 752e4f66283d7deb4c6fd47d88df644d8daa2aaa825a54f3bf350a625190192a +md5: a229e22d4d8814a07702b0919d8e6701 +depends: +- libfreetype6 >=2.14.3 +license: GPL-2.0-only OR FTL +size: 8125 +timestamp: 1774301094057 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.3-hdae7a39_0.conda +sha256: 8e6b27fe4eec4c2fa7b7769a21973734c8dba1de80086fb0213e58375ac09f4c +md5: b99ed99e42dafb27889483b3098cace7 +depends: +- libgcc >=14 +- libpng >=1.6.55,<1.7.0a0 +- libzlib >=1.3.2,<2.0a0 +constrains: +- freetype >=2.14.3 +license: GPL-2.0-only OR FTL +size: 422941 +timestamp: 1774301093473 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_19.conda +sha256: 4592b096e553f67799ae70d4b6167eeda3ec74587d68c7aecbf4e7b1df136681 +md5: f35b3f52d0a2ec4ffe3c89ba135cdb9a +depends: +- _openmp_mutex >=4.5 +constrains: +- libgomp 15.2.0 h8acb6b2_19 +- libgcc-ng ==15.2.0=*_19 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 622462 +timestamp: 1778268755949 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_19.conda +sha256: 1137f93f477f56199ded24117430045a0c02cbe8b10031beac3b9ad2138539d3 +md5: 770cf892e5530f43e63cadc673e85653 +depends: +- libgcc 15.2.0 h8acb6b2_19 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 27738 +timestamp: 1778268759211 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-15.2.0-he9431aa_19.conda +sha256: e5ad94be72634233510b33ba792a3339921bd468f0b8bc6961ea05eded251d9b +md5: c7a5b5decf969ead5ecada83654164cf +depends: +- libgfortran5 15.2.0 h1b7bec0_19 +constrains: +- libgfortran-ng ==15.2.0=*_19 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 27728 +timestamp: 1778268784621 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-15.2.0-h1b7bec0_19.conda +sha256: af8e9bdcaa77f133a8ee4c1ef57ef564d9c45aa262abf9f5ef9b50eb99d96407 +md5: 779dbb494de6d3d6477cab52eb34285a +depends: +- libgcc >=15.2.0 +constrains: +- libgfortran 15.2.0 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 1487244 +timestamp: 1778268767295 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_19.conda +sha256: 2370ef0ffcbae5bede3c4bf136add4abc257245eb91f724c99bb4a43116c5a83 +md5: c5e8a379c4a2ec2aea4ba22758c001d9 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 587387 +timestamp: 1778268674393 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.1.4.1-he30d5cf_0.conda +sha256: e97ec2af5f09f8f6ea8ecd550055c95ae80fae22015fcfadaa94eafe025c9ccc +md5: a85ba48648f6868016f2741fd9170250 +depends: +- libgcc >=14 +constrains: +- jpeg <0.0.0a +license: IJG AND BSD-3-Clause AND Zlib +size: 693143 +timestamp: 1775962625956 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblapack-3.11.0-7_h88aeb00_openblas.conda +build_number: 7 +sha256: 20b38a0156200ac65f597bf0a93914c565435f2cc58b1042581854231a99ac35 +md5: 5899cbd743cc74fd655c1ed2af7120f3 +depends: +- libblas 3.11.0 7_haddc8a3_openblas +constrains: +- libcblas 3.11.0 7*_openblas +- blas 2.307 openblas +- liblapacke 3.11.0 7*_openblas +license: BSD-3-Clause +license_family: BSD +size: 18685 +timestamp: 1778489809140 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.3-he30d5cf_0.conda +sha256: d61962b9cd54c3554361550203c64d5b65b71e3058a285b66e4b04b9769f0a5c +md5: 76298a9e6d71ee6e832a8d0d7373b261 +depends: +- libgcc >=14 +constrains: +- xz 5.8.3.* +license: 0BSD +size: 126102 +timestamp: 1775828008518 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-he30d5cf_1.conda +sha256: 57c0dd12d506e84541c4e877898bd2a59cca141df493d34036f18b2751e0a453 +md5: 7b9813e885482e3ccb1fa212b86d7fd0 +depends: +- libgcc >=14 +license: BSD-2-Clause +license_family: BSD +size: 114056 +timestamp: 1769482343003 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenblas-0.3.33-pthreads_h9d3fd7e_0.conda +sha256: b018ecfb05e75a8eea3f21f6b5c5c2a54b5178bdcf19e2e2df2735740214a8c8 +md5: 58a66cd95e9692f08abe89f55a6f3f12 +depends: +- libgcc >=14 +- libgfortran +- libgfortran5 >=14.3.0 +constrains: +- openblas >=0.3.33,<0.3.34.0a0 +license: BSD-3-Clause +license_family: BSD +size: 5121336 +timestamp: 1776993423004 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.58-h1abf092_0.conda +sha256: 483eaa53da40a6a3e558709d9f7b1ca388735364ae21a1ba58cf942514649c92 +md5: f51503ac45a4888bce71af9027a2ecc9 +depends: +- libgcc >=14 +- libzlib >=1.3.2,<2.0a0 +license: zlib-acknowledgement +size: 341202 +timestamp: 1776315188425 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.53.1-h022381a_0.conda +sha256: ad03b7d8e4d08001f0df88ee7a56108bb35bae4795a42b9a04cc1abfa822bd07 +md5: 2ec1119217d8f0d086e9a62f3cb0e5ea +depends: +- libgcc >=14 +- libzlib >=1.3.2,<2.0a0 +license: blessing +size: 955361 +timestamp: 1777986487553 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_19.conda +sha256: 1dadc45e599f510dd5f97141dddcdbb9844d9f1430c1f3a38075cf1c58f87b4e +md5: 543fbc8d71f2a0baf04cf88ce96cb8bb +depends: +- libgcc 15.2.0 h8acb6b2_19 +constrains: +- libstdcxx-ng ==15.2.0=*_19 +license: GPL-3.0-only WITH GCC-exception-3.1 +license_family: GPL +size: 5546559 +timestamp: 1778268777463 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.1-hdb009f0_1.conda +sha256: 7ff79470db39e803e21b8185bc8f19c460666d5557b1378d1b1e857d929c6b39 +md5: 8c6fd84f9c87ac00636007c6131e457d +depends: +- lerc >=4.0.0,<5.0a0 +- libdeflate >=1.25,<1.26.0a0 +- libgcc >=14 +- libjpeg-turbo >=3.1.0,<4.0a0 +- liblzma >=5.8.1,<6.0a0 +- libstdcxx >=14 +- libwebp-base >=1.6.0,<2.0a0 +- libzlib >=1.3.1,<2.0a0 +- zstd >=1.5.7,<1.6.0a0 +license: HPND +size: 488407 +timestamp: 1762022048105 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42.1-h1022ec0_0.conda +sha256: 1628839b062e98b2192857d4da8496ac9ac6b0dbb77aa040c34efc9192c440ee +md5: 0f42f9fedd2a32d798de95a7f65c456f +depends: +- libgcc >=14 +license: BSD-3-Clause +license_family: BSD +size: 43453 +timestamp: 1779118526838 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda +sha256: b03700a1f741554e8e5712f9b06dd67e76f5301292958cd3cb1ac8c6fdd9ed25 +md5: 24e92d0942c799db387f5c9d7b81f1af +depends: +- libgcc >=14 +constrains: +- libwebp 1.6.0 +license: BSD-3-Clause +license_family: BSD +size: 359496 +timestamp: 1752160685488 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda +sha256: 461cab3d5650ac6db73a367de5c8eca50363966e862dcf60181d693236b1ae7b +md5: cd14ee5cca2464a425b1dbfc24d90db2 +depends: +- libgcc >=13 +- pthread-stubs +- xorg-libxau >=1.0.11,<2.0a0 +- xorg-libxdmcp +license: MIT +license_family: MIT +size: 397493 +timestamp: 1727280745441 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda +sha256: eb111e32e5a7313a5bf799c7fb2419051fa2fe7eff74769fac8d5a448b309f7f +md5: 502006882cf5461adced436e410046d1 +constrains: +- zlib 1.3.2 *_2 +license: Zlib +license_family: Other +size: 69833 +timestamp: 1774072605429 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.10.2-pyhcf101f3_0.conda +sha256: 20e0892592a3e7c683e3d66df704a9425d731486a97c34fc56af4da1106b2b6b +md5: ba0a9221ce1063f31692c07370d062f3 +depends: +- importlib-metadata >=4.4 +- python >=3.10 +- python +license: BSD-3-Clause +license_family: BSD +size: 85893 +timestamp: 1770694658918 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda +sha256: 0c4c35376fe920714390d46e4b8d31c876d65f18e1655899e0763ec25f2a902f +md5: 6d03368f2b2b0a5fb6839df53b2eb5e0 +depends: +- mdurl >=0.1,<1 +- python >=3.10 +license: MIT +license_family: MIT +size: 69017 +timestamp: 1778169663339 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.3-py314hb76de3f_1.conda +sha256: 383c188496d13a55658c06e61e7d4cdff2c9f9d5a0648769fca8250bece7e0ef +md5: e5de3c36dd548b35ff2a8aa49208dcb3 +depends: +- libgcc >=14 +- python >=3.14,<3.15.0a0 +- python_abi 3.14.* *_cp314 +constrains: +- jinja2 >=3.0.0 +license: BSD-3-Clause +license_family: BSD +size: 27913 +timestamp: 1772446407659 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mathjax-2.7.7-h8af1aa0_3.tar.bz2 +sha256: 8fd4c79d6eda3d4cba73783114305a53a154ada4d1e334d4e02cb3521429599b +md5: 7b08314a6867a9d5648a1c3265e9eb8e +license: Apache-2.0 +license_family: Apache +size: 22257008 +timestamp: 1662784555011 +- conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda +sha256: 78c1bbe1723449c52b7a9df1af2ee5f005209f67e40b6e1d3c7619127c43b1c7 +md5: 592132998493b3ff25fd7479396e8351 +depends: +- python >=3.9 +license: MIT +license_family: MIT +size: 14465 +timestamp: 1733255681319 +- conda: https://conda.anaconda.org/bioconda/noarch/multiqc-1.35-pyhdfd78af_1.conda +sha256: e86033aa55a9e915e2d0957e770bdb81e3feb26a227d1adb17f9d6c528da6a71 +md5: cdb20309681ba3ce8f52c110e214d4f3 +depends: +- click +- coloredlogs +- humanize +- importlib-metadata +- jinja2 >=3.0.0 +- jsonschema +- markdown +- natsort +- numpy +- packaging +- pillow >=10.2.0 +- plotly >=5.18 +- polars >=1.34.0 +- polars-runtime-compat >=1.34.0 +- pyaml-env +- pydantic >=2.7.1 +- python >=3.9,!=3.14.1 +- python-dotenv +- python-kaleido 0.2.1 +- pyyaml >=4 +- requests +- rich >=10 +- rich-click +- spectra >=0.0.10 +- tiktoken +- tqdm +- typeguard >=4 +license: GPL-3.0-or-later +license_family: GPL3 +size: 4282188 +timestamp: 1779465338806 +- conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.21.2-pyhcf101f3_0.conda +sha256: 70f43d62450927d51673eecd8823e14f5b3cfebdb43cda1d502eba97162bab42 +md5: 6687827c332121727ce383919e1ec8c2 +depends: +- python >=3.10 +- python +license: MIT +license_family: MIT +size: 284323 +timestamp: 1778929680962 +- conda: https://conda.anaconda.org/conda-forge/noarch/natsort-8.4.0-pyhcf101f3_2.conda +sha256: aeb1548eb72e4f198e72f19d242fb695b35add2ac7b2c00e0d83687052867680 +md5: e941e85e273121222580723010bd4fa2 +depends: +- python >=3.9 +- python +license: MIT +license_family: MIT +size: 39262 +timestamp: 1770905275632 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.6-hf8d1292_0.conda +sha256: 369db85c5cd8d99dde364ce70725d76511d9c8199e5b820c740414091bf5bcca +md5: b2a43456aa56fe80c2477a5094899eff +depends: +- libgcc >=14 +license: X11 AND BSD-3-Clause +size: 960036 +timestamp: 1777422174534 +- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda +sha256: f6a82172afc50e54741f6f84527ef10424326611503c64e359e25a19a8e4c1c6 +md5: a2c1eeadae7a309daed9d62c96012a2b +depends: +- python >=3.11 +- python +constrains: +- numpy >=1.25 +- scipy >=1.11.2 +- matplotlib-base >=3.8 +- pandas >=2.0 +license: BSD-3-Clause +license_family: BSD +size: 1587439 +timestamp: 1765215107045 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nspr-4.38-h3ad9384_0.conda +sha256: 78a06e89285fef242e272998b292c1e621e3ee3dd4fba62ec014e503c7ec118f +md5: 6dd4f07147774bf720075a210f8026b9 +depends: +- libgcc >=14 +- libstdcxx >=14 +license: MPL-2.0 +license_family: MOZILLA +size: 235140 +timestamp: 1762350120355 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nss-3.118-h544fa81_0.conda +sha256: 48942696889367ffd448f8dccfc080fb7e130b9938a4a3b6b20ef8e6af856463 +md5: 4540f9570d12db2150f42ba036154552 +depends: +- libgcc >=14 +- libsqlite >=3.51.0,<4.0a0 +- libstdcxx >=14 +- libzlib >=1.3.1,<2.0a0 +- nspr >=4.38,<5.0a0 +license: MPL-2.0 +license_family: MOZILLA +size: 2061869 +timestamp: 1763490303490 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/numpy-2.4.6-py314he1698a1_0.conda +sha256: 04af718b911f8a3a0095481c7e283aa081a175fe626eccbc2c5644bcb2aba9a1 +md5: 8b173772deea177b45d2a133b509b3f7 +depends: +- python +- libstdcxx >=14 +- libgcc >=14 +- python_abi 3.14.* *_cp314 +- libblas >=3.9.0,<4.0a0 +- liblapack >=3.9.0,<4.0a0 +- libcblas >=3.9.0,<4.0a0 +constrains: +- numpy-base <0a0 +license: BSD-3-Clause +license_family: BSD +size: 8002900 +timestamp: 1779169206742 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openjpeg-2.5.4-h5da879a_0.conda +sha256: bd1bc8bdde5e6c5cbac42d462b939694e40b59be6d0698f668515908640c77b8 +md5: cea962410e327262346d48d01f05936c +depends: +- libgcc >=14 +- libpng >=1.6.50,<1.7.0a0 +- libstdcxx >=14 +- libtiff >=4.7.1,<4.8.0a0 +- libzlib >=1.3.1,<2.0a0 +license: BSD-2-Clause +license_family: BSD +size: 392636 +timestamp: 1758489353577 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.2-h546c87b_0.conda +sha256: 348cb74c1530ac241215d047ef65d134cf797af935c97a68655319362b7e6a01 +md5: 3b129669089e4d6a5c6871dbb4669b99 +depends: +- ca-certificates +- libgcc >=14 +license: Apache-2.0 +license_family: Apache +size: 3706406 +timestamp: 1775589602258 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda +sha256: 3906abfb6511a3bb309e39b9b1b7bc38f50a723971de2395489fd1f379255890 +md5: 4c06a92e74452cfa53623a81592e8934 +depends: +- python >=3.8 +- python +license: Apache-2.0 +license_family: APACHE +size: 91574 +timestamp: 1777103621679 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pillow-12.2.0-py314hac3e5ec_0.conda +sha256: 96b26c2657275ffe84ab510edf0865e21999d791485d12794edd4a71b837beb6 +md5: 87d58d103b47c4a8567b3d7666647684 +depends: +- python +- libgcc >=14 +- python 3.14.* *_cp314 +- openjpeg >=2.5.4,<3.0a0 +- libxcb >=1.17.0,<2.0a0 +- libwebp-base >=1.6.0,<2.0a0 +- zlib-ng >=2.3.3,<2.4.0a0 +- python_abi 3.14.* *_cp314 +- lcms2 >=2.18,<3.0a0 +- tk >=8.6.13,<8.7.0a0 +- libtiff >=4.7.1,<4.8.0a0 +- libjpeg-turbo >=3.1.2,<4.0a0 +- libfreetype >=2.14.3 +- libfreetype6 >=2.14.3 +license: HPND +size: 1062080 +timestamp: 1775060067775 +- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda +sha256: c418d325359fc7a0074cea7f081ef1bce26e114d2da8a0154c5d27ecc87a08e7 +md5: 3e9427ee186846052e81fadde8ebe96a +depends: +- narwhals >=1.15.1 +- packaging +- python >=3.10 +constrains: +- ipywidgets >=7.6 +license: MIT +license_family: MIT +size: 5251872 +timestamp: 1772628857717 +- conda: https://conda.anaconda.org/conda-forge/noarch/polars-1.41.0-pyh58ad624_0.conda +sha256: 70fc56877c4a095ee658d61924d8019768fbae4a48437058d181fc94b0a7c4d8 +md5: 25a883fed9f1f3f21ff317a3e7c92ac4 +depends: +- polars-runtime-32 ==1.41.0 +- python >=3.10 +- python +constrains: +- numpy >=1.16.0 +- pyarrow >=7.0.0 +- fastexcel >=0.9 +- openpyxl >=3.0.0 +- xlsx2csv >=0.8.0 +- connectorx >=0.3.2 +- deltalake >=1.0.0 +- pyiceberg >=0.7.1 +- altair >=5.4.0 +- great_tables >=0.8.0 +- polars-runtime-32 ==1.41.0 +- polars-runtime-64 ==1.41.0 +- polars-runtime-compat ==1.41.0 +license: MIT +size: 539656 +timestamp: 1779630790562 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/polars-runtime-32-1.41.0-py310h32c7c23_0.conda +noarch: python +sha256: d903b774ec09189e164207328aac157eee82fed8cc5c9ace46aeb5d1c15cb5b3 +md5: 8c08c506ed1ea8ce0ca37af5e918c58d +depends: +- python +- libgcc >=14 +- libstdcxx >=14 +- _python_abi3_support 1.* +- cpython >=3.10 +constrains: +- __glibc >=2.17 +license: MIT +size: 38704429 +timestamp: 1779630794932 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/polars-runtime-compat-1.41.0-py310hc0e61be_0.conda +noarch: python +sha256: 101696adff43a654146376c62ef9611bf7946b95fa46f604fe247d77eefc6267 +md5: 65b73e4260677ee5162bdbb252e28e06 +depends: +- python +- libstdcxx >=14 +- libgcc >=14 +- _python_abi3_support 1.* +- cpython >=3.10 +constrains: +- __glibc >=2.17 +license: MIT +size: 38651498 +timestamp: 1779630714016 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/procps-ng-4.0.6-h1779866_0.conda +sha256: e9cbcbc94e151ada3d6dc365380aaaf591f65012c16d9a2abaea4b9b90adc402 +md5: ab7288cc39545556d1bc5e71ab2df9a9 +depends: +- libgcc >=14 +- ncurses >=6.5,<7.0a0 +license: GPL-2.0-or-later AND LGPL-2.0-or-later +license_family: GPL +size: 636733 +timestamp: 1769712412683 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda +sha256: 977dfb0cb3935d748521dd80262fe7169ab82920afd38ed14b7fee2ea5ec01ba +md5: bb5a90c93e3bac3d5690acf76b4a6386 +depends: +- libgcc >=13 +license: MIT +license_family: MIT +size: 8342 +timestamp: 1726803319942 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyaml-env-1.2.2-pyhd8ed1ab_0.conda +sha256: 58994e0d2ea8584cb399546e6f6896d771995e6121d1a7b6a2c9948388358932 +md5: e17be1016bcc3516827b836cd3e4d9dc +depends: +- python >=3.9 +- pyyaml >=5.0,<=7.0 +license: MIT +license_family: MIT +size: 14645 +timestamp: 1736766960536 +- conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda +sha256: 69700e31165df070e9716315e042196aa92525dae5deb5107785847ab9f4189f +md5: 729843edafc0899b3348bd3f19525b9d +depends: +- typing-inspection >=0.4.2 +- typing_extensions >=4.14.1 +- python >=3.10 +- annotated-types >=0.6.0 +- pydantic-core ==2.46.4 +- python +license: MIT +license_family: MIT +size: 346511 +timestamp: 1778103405862 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pydantic-core-2.46.4-py314h451b6cc_0.conda +sha256: 1a7c6b18e404c13c4d959888ecb48a9ed9de0e41be2872932b83a35278088df0 +md5: 9c3ace6aba6df14b943256095ac1281e +depends: +- python +- typing-extensions >=4.6.0,!=4.7.0 +- libgcc >=14 +- python 3.14.* *_cp314 +- python_abi 3.14.* *_cp314 +constrains: +- __glibc >=2.17 +license: MIT +license_family: MIT +size: 1780773 +timestamp: 1778084251775 +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda +sha256: cf70b2f5ad9ae472b71235e5c8a736c9316df3705746de419b59d442e8348e86 +md5: 16c18772b340887160c79a6acc022db0 +depends: +- python >=3.10 +license: BSD-2-Clause +license_family: BSD +size: 893031 +timestamp: 1774796815820 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda +sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 +md5: 461219d1a5bd61342293efa2c0c90eac +depends: +- __unix +- python >=3.9 +license: BSD-3-Clause +license_family: BSD +size: 21085 +timestamp: 1733217331982 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.14.5-hfd9ac0a_100_cp314.conda +build_number: 100 +sha256: d37bad5447365346166c72950ea8f49689aa49cecc1b0623d00458427627b8df +md5: d956e09feb806f5974675ce92ad81d45 +depends: +- bzip2 >=1.0.8,<2.0a0 +- ld_impl_linux-aarch64 >=2.36.1 +- libexpat >=2.8.0,<3.0a0 +- libffi >=3.5.2,<3.6.0a0 +- libgcc >=14 +- liblzma >=5.8.3,<6.0a0 +- libmpdec >=4.0.0,<5.0a0 +- libsqlite >=3.53.1,<4.0a0 +- libuuid >=2.42.1,<3.0a0 +- libzlib >=1.3.2,<2.0a0 +- ncurses >=6.6,<7.0a0 +- openssl >=3.5.6,<4.0a0 +- python_abi 3.14.* *_cp314 +- readline >=8.3,<9.0a0 +- tk >=8.6.13,<8.7.0a0 +- tzdata +- zstd >=1.5.7,<1.6.0a0 +license: Python-2.0 +size: 37510439 +timestamp: 1779236267040 +python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda +sha256: 74e417a768f59f02a242c25e7db0aa796627b5bc8c818863b57786072aeb85e5 +md5: 130584ad9f3a513cdd71b1fdc1244e9c +depends: +- python >=3.10 +license: BSD-3-Clause +license_family: BSD +size: 27848 +timestamp: 1772388605021 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.5-h4df99d1_100.conda +sha256: 41dd7da285d71d519257fa7dacb1cae060d5ebfaa5f92cba5994899d2978e943 +md5: 41954747ba952ec4b01e16c2c9e8d8ff +depends: +- cpython 3.14.5.* +- python_abi * *_cp314 +license: Python-2.0 +size: 50212 +timestamp: 1779236703009 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-kaleido-0.2.1-pyhd8ed1ab_0.tar.bz2 +sha256: e17bf63a30aec33432f1ead86e15e9febde9fc40a7f869c0e766be8d2db44170 +md5: 310259a5b03ff02289d7705f39e2b1d2 +depends: +- kaleido-core 0.2.1.* +- python >=3.5 +license: MIT +license_family: MIT +size: 18320 +timestamp: 1615204747600 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda +build_number: 8 +sha256: ad6d2e9ac39751cc0529dd1566a26751a0bf2542adb0c232533d32e176e21db5 +md5: 0539938c55b6b1a59b560e843ad864a4 +constrains: +- python 3.14.* *_cp314 +license: BSD-3-Clause +license_family: BSD +size: 6989 +timestamp: 1752805904792 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py314h807365f_1.conda +sha256: 496b5e65dfdd0aaaaa5de0dcaaf3bceea00fcb4398acf152f89e567c82ec1046 +md5: 9ae2c92975118058bd720e9ba2bb7c58 +depends: +- libgcc >=14 +- python >=3.14,<3.15.0a0 +- python >=3.14,<3.15.0a0 *_cp314 +- python_abi 3.14.* *_cp314 +- yaml >=0.2.5,<0.3.0a0 +license: MIT +license_family: MIT +size: 195678 +timestamp: 1770223441816 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda +sha256: fe695f9d215e9a2e3dd0ca7f56435ab4df24f5504b83865e3d295df36e88d216 +md5: 3d49cad61f829f4f0e0611547a9cda12 +depends: +- libgcc >=14 +- ncurses >=6.5,<7.0a0 +license: GPL-3.0-only +license_family: GPL +size: 357597 +timestamp: 1765815673644 +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda +sha256: 0577eedfb347ff94d0f2fa6c052c502989b028216996b45c7f21236f25864414 +md5: 870293df500ca7e18bedefa5838a22ab +depends: +- attrs >=22.2.0 +- python >=3.10 +- rpds-py >=0.7.0 +- typing_extensions >=4.4.0 +- python +license: MIT +license_family: MIT +size: 51788 +timestamp: 1760379115194 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/regex-2026.5.9-py314h51f160d_0.conda +sha256: 05ef55f09f31eabd0a205f6b065e13fc746675f41924620977692ef0ffe5aad8 +md5: 34ed7bc9febeca70f55b757ca09c354d +depends: +- libgcc >=14 +- python >=3.14,<3.15.0a0 +- python >=3.14,<3.15.0a0 *_cp314 +- python_abi 3.14.* *_cp314 +license: Apache-2.0 AND CNRI-Python +license_family: PSF +size: 409780 +timestamp: 1778374195988 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda +sha256: 1715246b19c9f85ee022933b4845f2fc14ac9184981b7b7d9b728bec8e9588da +md5: 4a85203c1d80c1059086ae860836ffb9 +depends: +- python >=3.10 +- certifi >=2023.5.7 +- charset-normalizer >=2,<4 +- idna >=2.5,<4 +- urllib3 >=1.26,<3 +- python +constrains: +- chardet >=3.0.2,<8 +license: Apache-2.0 +license_family: APACHE +size: 68709 +timestamp: 1778851103479 +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda +sha256: 3d6ba2c0fcdac3196ba2f0615b4104e532525ffa1335b50a2878be5ff488814a +md5: 0242025a3c804966bf71aa04eee82f66 +depends: +- markdown-it-py >=2.2.0 +- pygments >=2.13.0,<3.0.0 +- python >=3.10 +- typing_extensions >=4.0.0,<5.0.0 +- python +license: MIT +license_family: MIT +size: 208577 +timestamp: 1775991661559 +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-click-1.9.7-pyh8f84b5b_0.conda +sha256: aa3fcb167321bae51998de2e94d199109c9024f25a5a063cb1c28d8f1af33436 +md5: 0c20a8ebcddb24a45da89d5e917e6cb9 +depends: +- python >=3.10 +- rich >=12 +- click >=8 +- typing-extensions >=4 +- __unix +- python +license: MIT +license_family: MIT +size: 64356 +timestamp: 1769850479089 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.30.0-py314h02b7a91_0.conda +sha256: a587240f16eac7c6a80f9585cef679cd1cb9a287b8dfcdd36dcef1f7e7db15dc +md5: e7f6ed9e60043bb5cbcc527764897f0d +depends: +- python +- libgcc >=14 +- python_abi 3.14.* *_cp314 +constrains: +- __glibc >=2.17 +license: MIT +license_family: MIT +size: 376332 +timestamp: 1764543345455 +- conda: https://conda.anaconda.org/conda-forge/noarch/spectra-0.0.11-pyhd8ed1ab_2.conda +sha256: 7c65782d2511738e62c70462e89d65da4fa54d5a7e47c46667bcd27a59f81876 +md5: 472239e4eb7b5a84bb96b3ed7e3a596a +depends: +- colormath >=3.0.0 +- python >=3.9 +license: MIT +license_family: MIT +size: 22284 +timestamp: 1735770589188 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sqlite-3.53.1-he8854b5_0.conda +sha256: 27467e4bfb0681546f149718c33b806fec078185fbaa6a4d17d440bc8f56185c +md5: 46009bdca2315a99e0a3a7d0ba1af3b9 +depends: +- libgcc >=14 +- libsqlite 3.53.1 h022381a_0 +- libzlib >=1.3.2,<2.0a0 +- ncurses >=6.6,<7.0a0 +- readline >=8.3,<9.0a0 +license: blessing +size: 209964 +timestamp: 1777986493350 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tiktoken-0.12.0-py314h6a36e60_3.conda +sha256: c1da41c79262b27efa168407cfecc47b20270e5fc071a8307f95a2c85fb94170 +md5: 55bf7b559202236157b14323b40f19e6 +depends: +- libgcc >=14 +- libstdcxx >=14 +- python >=3.14,<3.15.0a0 +- python_abi 3.14.* *_cp314 +- regex >=2022.1.18 +- requests >=2.26.0 +constrains: +- __glibc >=2.17 +license: MIT +license_family: MIT +size: 914402 +timestamp: 1764030357702 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda +sha256: e25c314b52764219f842b41aea2c98a059f06437392268f09b03561e4f6e5309 +md5: 7fc6affb9b01e567d2ef1d05b84aa6ed +depends: +- libgcc >=14 +- libzlib >=1.3.1,<2.0a0 +constrains: +- xorg-libx11 >=1.8.12,<2.0a0 +license: TCL +license_family: BSD +size: 3368666 +timestamp: 1769464148928 +- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda +sha256: 9ef8e47cf00e4d6dcc114eb32a1504cc18206300572ef14d76634ba29dfe1eb6 +md5: e5ce43272193b38c2e9037446c1d9206 +depends: +- python >=3.10 +- __unix +- python +license: MPL-2.0 and MIT +size: 94132 +timestamp: 1770153424136 +- conda: https://conda.anaconda.org/conda-forge/noarch/typeguard-4.5.2-pyhcf101f3_0.conda +sha256: 59d7851d32fddb5b510272e6557aa982edeb927d349648dac27f5bf01d18bb26 +md5: 4460f039b7dedf15f7df086446ca75ae +depends: +- typing_extensions >=4.14.0 +- python >=3.10 +- importlib-metadata >=3.6 +- python +constrains: +- pytest >=7 +license: MIT +license_family: MIT +size: 38297 +timestamp: 1778779291237 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda +sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c +md5: edd329d7d3a4ab45dcf905899a7a6115 +depends: +- typing_extensions ==4.15.0 pyhcf101f3_0 +license: PSF-2.0 +license_family: PSF +size: 91383 +timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda +sha256: 8b90d2f19f9458b8c58a55e1fcdc1d90c1603a847a47654d8a454549413ba60a +md5: 53f5409c5cfd6c5a66417d68e3f0a864 +depends: +- python >=3.10 +- typing_extensions >=4.12.0 +- python +license: MIT +license_family: MIT +size: 20935 +timestamp: 1777105465795 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda +sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 +md5: 0caa1af407ecff61170c9437a808404d +depends: +- python >=3.10 +- python +license: PSF-2.0 +license_family: PSF +size: 51692 +timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda +sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c +md5: ad659d0a2b3e47e38d829aa8cad2d610 +license: LicenseRef-Public-Domain +size: 119135 +timestamp: 1767016325805 +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda +sha256: feff959a816f7988a0893201aa9727bbb7ee1e9cec2c4f0428269b489eb93fb4 +md5: cbb88288f74dbe6ada1c6c7d0a97223e +depends: +- backports.zstd >=1.0.0 +- brotli-python >=1.2.0 +- h2 >=4,<5 +- pysocks >=1.5.6,<2.0,!=1.5.7 +- python >=3.10 +license: MIT +license_family: MIT +size: 103560 +timestamp: 1778188657149 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-he30d5cf_1.conda +sha256: e9f6e931feeb2f40e1fdbafe41d3b665f1ab6cb39c5880a1fcf9f79a3f3c84a5 +md5: 1c246e1105000c3660558459e2fd6d43 +depends: +- libgcc >=14 +license: MIT +license_family: MIT +size: 16317 +timestamp: 1762977521691 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-he30d5cf_1.conda +sha256: 128d72f36bcc8d2b4cdbec07507542e437c7d67f677b7d77b71ed9eeac7d6df1 +md5: bff06dcde4a707339d66d45d96ceb2e2 +depends: +- libgcc >=14 +license: MIT +license_family: MIT +size: 21039 +timestamp: 1762979038025 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda +sha256: 66265e943f32ce02396ad214e27cb35f5b0490b3bd4f064446390f9d67fa5d88 +md5: 032d8030e4a24fe1f72c74423a46fb88 +depends: +- libgcc >=14 +license: MIT +license_family: MIT +size: 88088 +timestamp: 1753484092643 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda +sha256: 210bd31c22bb88f5e2a167df24c95bb5f152b2ada7502f9b8c49d1f5366db423 +md5: ba3dcdc8584155c97c648ae9c044b7a3 +depends: +- python >=3.10 +- python +license: MIT +license_family: MIT +size: 24190 +timestamp: 1779159948016 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-ng-2.3.3-ha7cb516_1.conda +sha256: 638a3a41a4fbfed52d3c60c8ef5a3693b3f12a5b1a3f58fa29f5698d0a0702e2 +md5: f731af71c723065d91b4c01bb822641b +depends: +- libgcc >=14 +- libstdcxx >=14 +license: Zlib +license_family: Other +size: 121046 +timestamp: 1770167944449 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda +sha256: 569990cf12e46f9df540275146da567d9c618c1e9c7a0bc9d9cfefadaed20b75 +md5: c3655f82dcea2aa179b291e7099c1fcc +depends: +- libzlib >=1.3.1,<2.0a0 +license: BSD-3-Clause +license_family: BSD +size: 614429 +timestamp: 1764777145593 diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml index 6f5b867..7a970e2 100644 --- a/modules/nf-core/multiqc/environment.yml +++ b/modules/nf-core/multiqc/environment.yml @@ -1,5 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json channels: - conda-forge - bioconda dependencies: - - bioconda::multiqc=1.25.1 + - bioconda::multiqc=1.35 diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index cc0643e..c4bc715 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -1,24 +1,21 @@ process MULTIQC { + tag "${meta.id}" label 'process_single' conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://depot.galaxyproject.org/singularity/multiqc:1.25.1--pyhdfd78af_0' : - 'biocontainers/multiqc:1.25.1--pyhdfd78af_0' }" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/c8/c8e346f4f6080eadf1253505e6ff09ef004454fc18e8d672006fd7b222cc412e/data' + : 'community.wave.seqera.io/library/multiqc:1.35--c17fb751507e9dfc'}" input: - path multiqc_files, stageAs: "?/*" - path(multiqc_config) - path(extra_multiqc_config) - path(multiqc_logo) - path(replace_names) - path(sample_names) + tuple val(meta), path(multiqc_files, stageAs: "?/*"), path(multiqc_config, stageAs: "?/*"), path(multiqc_logo), path(replace_names), path(sample_names) output: - path "*multiqc_report.html", emit: report - path "*_data" , emit: data - path "*_plots" , optional:true, emit: plots - path "versions.yml" , emit: versions + tuple val(meta), path("*.html"), emit: report + tuple val(meta), path("*_data"), emit: data + tuple val(meta), path("*_plots"), emit: plots, optional: true + // MultiQC should not push its versions to the `versions` topic. Its input depends on the versions topic to be resolved thus outputting to the topic will let the pipeline hang forever + tuple val("${task.process}"), val('multiqc'), eval('multiqc --version | sed "s/.* //g"'), emit: versions when: task.ext.when == null || task.ext.when @@ -26,38 +23,28 @@ process MULTIQC { script: def args = task.ext.args ?: '' def prefix = task.ext.prefix ? "--filename ${task.ext.prefix}.html" : '' - def config = multiqc_config ? "--config $multiqc_config" : '' - def extra_config = extra_multiqc_config ? "--config $extra_multiqc_config" : '' + def config = multiqc_config ? multiqc_config instanceof List ? "--config ${multiqc_config.join(' --config ')}" : "--config ${multiqc_config}" : "" def logo = multiqc_logo ? "--cl-config 'custom_logo: \"${multiqc_logo}\"'" : '' def replace = replace_names ? "--replace-names ${replace_names}" : '' def samples = sample_names ? "--sample-names ${sample_names}" : '' """ multiqc \\ --force \\ - $args \\ - $config \\ - $prefix \\ - $extra_config \\ - $logo \\ - $replace \\ - $samples \\ + ${args} \\ + ${config} \\ + ${prefix} \\ + ${logo} \\ + ${replace} \\ + ${samples} \\ . - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - multiqc: \$( multiqc --version | sed -e "s/multiqc, version //g" ) - END_VERSIONS """ stub: """ mkdir multiqc_data + touch multiqc_data/.stub mkdir multiqc_plots + touch multiqc_plots/.stub touch multiqc_report.html - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - multiqc: \$( multiqc --version | sed -e "s/multiqc, version //g" ) - END_VERSIONS """ } diff --git a/modules/nf-core/multiqc/meta.yml b/modules/nf-core/multiqc/meta.yml index b16c187..27ce18d 100644 --- a/modules/nf-core/multiqc/meta.yml +++ b/modules/nf-core/multiqc/meta.yml @@ -1,6 +1,6 @@ name: multiqc -description: Aggregate results from bioinformatics analyses across many samples into - a single report +description: Aggregate results from bioinformatics analyses across many samples + into a single report keywords: - QC - bioinformatics tools @@ -12,60 +12,91 @@ tools: It's a general use tool, perfect for summarising the output from numerous bioinformatics tools. homepage: https://multiqc.info/ documentation: https://multiqc.info/docs/ - licence: ["GPL-3.0-or-later"] + licence: + - "GPL-3.0-or-later" identifier: biotools:multiqc input: - - - multiqc_files: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample1', single_end:false ] + - multiqc_files: type: file description: | List of reports / files recognised by MultiQC, for example the html and zip output of FastQC - - - multiqc_config: + ontologies: [] + - multiqc_config: type: file description: Optional config yml for MultiQC pattern: "*.{yml,yaml}" - - - extra_multiqc_config: - type: file - description: Second optional config yml for MultiQC. Will override common sections - in multiqc_config. - pattern: "*.{yml,yaml}" - - - multiqc_logo: + ontologies: + - edam: http://edamontology.org/format_3750 + - multiqc_logo: type: file description: Optional logo file for MultiQC pattern: "*.{png}" - - - replace_names: + ontologies: [] + - replace_names: type: file description: | Optional two-column sample renaming file. First column a set of patterns, second column a set of corresponding replacements. Passed via MultiQC's `--replace-names` option. pattern: "*.{tsv}" - - - sample_names: + ontologies: + - edam: http://edamontology.org/format_3475 + - sample_names: type: file description: | Optional TSV file with headers, passed to the MultiQC --sample_names argument. pattern: "*.{tsv}" + ontologies: + - edam: http://edamontology.org/format_3475 output: - - report: - - "*multiqc_report.html": + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample1', single_end:false ] + - "*.html": type: file description: MultiQC report file - pattern: "multiqc_report.html" - - data: + pattern: ".html" + ontologies: [] + data: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample1', single_end:false ] - "*_data": type: directory description: MultiQC data dir pattern: "multiqc_data" - - plots: + plots: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample1', single_end:false ] - "*_plots": type: file description: Plots created by MultiQC - pattern: "*_data" - - versions: - - versions.yml: - type: file - description: File containing software versions - pattern: "versions.yml" + pattern: "*_plots" + ontologies: [] + versions: + - - ${task.process}: + type: string + description: The process the versions were collected from + - multiqc: + type: string + description: The tool name + - multiqc --version | sed "s/.* //g": + type: eval + description: The expression to obtain the version of the tool authors: - "@abhi18av" - "@bunop" @@ -76,3 +107,27 @@ maintainers: - "@bunop" - "@drpatelh" - "@jfy133" +containers: + conda: + linux/amd64: + lock_file: modules/nf-core/multiqc/.conda-lock/linux_amd64-bd-c17fb751507e9dfc_1.txt + linux/arm64: + lock_file: modules/nf-core/multiqc/.conda-lock/linux_arm64-bd-5c84a5000a226ab5_1.txt + docker: + linux/amd64: + name: community.wave.seqera.io/library/multiqc:1.35--c17fb751507e9dfc + build_id: bd-c17fb751507e9dfc_1 + scan_id: sc-3b1b3932f9846892_1 + linux/arm64: + name: community.wave.seqera.io/library/multiqc:1.35--5c84a5000a226ab5 + build_id: bd-5c84a5000a226ab5_1 + scan_id: sc-0d39df41e9737bbd_1 + singularity: + linux/amd64: + name: oras://community.wave.seqera.io/library/multiqc:1.35--c680f2aea25ccec2 + build_id: bd-c680f2aea25ccec2_1 + https: https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/c8/c8e346f4f6080eadf1253505e6ff09ef004454fc18e8d672006fd7b222cc412e/data + linux/arm64: + name: oras://community.wave.seqera.io/library/multiqc:1.35--c0468833d65b2f81 + build_id: bd-c0468833d65b2f81_1 + https: https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/e4/e48aa28aebc881254a499b24c3e1ce77b8df1b85a5432699ed6f72eb17ac7fb5/data diff --git a/modules/nf-core/multiqc/tests/custom_prefix.config b/modules/nf-core/multiqc/tests/custom_prefix.config new file mode 100644 index 0000000..b30b135 --- /dev/null +++ b/modules/nf-core/multiqc/tests/custom_prefix.config @@ -0,0 +1,5 @@ +process { + withName: 'MULTIQC' { + ext.prefix = "custom_prefix" + } +} diff --git a/modules/nf-core/multiqc/tests/main.nf.test b/modules/nf-core/multiqc/tests/main.nf.test index 33316a7..4cbdb95 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test +++ b/modules/nf-core/multiqc/tests/main.nf.test @@ -15,25 +15,84 @@ nextflow_process { when { process { """ - input[0] = Channel.of(file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true)) - input[1] = [] - input[2] = [] - input[3] = [] - input[4] = [] - input[5] = [] + input[0] = channel.of([ + [ id: 'FASTQC' ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true), + [], + [], + [], + [] + ]) """ } } then { - assertAll( - { assert process.success }, - { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, - { assert process.out.data[0] ==~ ".*/multiqc_data" }, - { assert snapshot(process.out.versions).match("multiqc_versions_single") } - ) + assert process.success + assert snapshot( + sanitizeOutput(process.out).collectEntries { key, val -> + if (key == "data") { + return [key, val.collect { [path(it[1]).list().collect { file(it.toString()).name }] }] + } + else if (key == "plots") { + return [key, val.collect { [ + "pdf", + path("${it[1]}/pdf").list().collect { file(it.toString()).name }, + "png", + path("${it[1]}/png").list().collect { file(it.toString()).name }, + "svg", + path("${it[1]}/svg").list().collect { file(it.toString()).name }] }] + } + else if (key == "report") { + return [key, file(val[0][1].toString()).name] + } + return [key, val] + } + ).match() + } + } + + test("sarscov2 single-end [fastqc] - custom prefix") { + config "./custom_prefix.config" + + when { + process { + """ + input[0] = channel.of([ + [ id: 'FASTQC' ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true), + [], + [], + [], + [] + ]) + """ + } } + then { + assert process.success + assert snapshot( + sanitizeOutput(process.out).collectEntries { key, val -> + if (key == "data") { + return [key, val.collect { [path(it[1]).list().collect { file(it.toString()).name }] }] + } + else if (key == "plots") { + return [key, val.collect { [ + "pdf", + path("${it[1]}/pdf").list().collect { file(it.toString()).name }, + "png", + path("${it[1]}/png").list().collect { file(it.toString()).name }, + "svg", + path("${it[1]}/svg").list().collect { file(it.toString()).name }] }] + } + else if (key == "report") { + return [key, file(val[0][1].toString()).name] + } + return [key, val] + } + ).match() + } } test("sarscov2 single-end [fastqc] [config]") { @@ -41,23 +100,85 @@ nextflow_process { when { process { """ - input[0] = Channel.of(file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true)) - input[1] = Channel.of(file("https://github.com/nf-core/tools/raw/dev/nf_core/pipeline-template/assets/multiqc_config.yml", checkIfExists: true)) - input[2] = [] - input[3] = [] - input[4] = [] - input[5] = [] + input[0] = channel.of([ + [ id: 'FASTQC' ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true), + file("https://raw.githubusercontent.com/nf-core/seqinspector/1.0.0/assets/multiqc_config.yml", checkIfExists: true), + [], + [], + [] + ]) """ } } then { - assertAll( - { assert process.success }, - { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, - { assert process.out.data[0] ==~ ".*/multiqc_data" }, - { assert snapshot(process.out.versions).match("multiqc_versions_config") } - ) + assert process.success + assert snapshot( + sanitizeOutput(process.out).collectEntries { key, val -> + if (key == "data") { + return [key, val.collect { [path(it[1]).list().collect { file(it.toString()).name }] }] + } + else if (key == "plots") { + return [key, val.collect { [ + "pdf", + path("${it[1]}/pdf").list().collect { file(it.toString()).name }, + "png", + path("${it[1]}/png").list().collect { file(it.toString()).name }, + "svg", + path("${it[1]}/svg").list().collect { file(it.toString()).name }] }] + } + else if (key == "report") { + return [key, file(val[0][1].toString()).name] + } + return [key, val] + } + ).match() + } + } + + test("sarscov2 single-end [fastqc] [multiple configs]") { + + when { + process { + """ + input[0] = channel.of([ + [ id: 'FASTQC' ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true), + [ + file("https://raw.githubusercontent.com/nf-core/seqinspector/1.0.0/assets/multiqc_config.yml", checkIfExists: true), + file("https://raw.githubusercontent.com/nf-core/seqinspector/1.0.0/assets/multiqc_config.yml", checkIfExists: true) + ], + [], + [], + [] + ]) + """ + } + } + + then { + assert process.success + assert snapshot( + sanitizeOutput(process.out).collectEntries { key, val -> + if (key == "data") { + return [key, val.collect { [path(it[1]).list().collect { file(it.toString()).name }] }] + } + else if (key == "plots") { + return [key, val.collect { [ + "pdf", + path("${it[1]}/pdf").list().collect { file(it.toString()).name }, + "png", + path("${it[1]}/png").list().collect { file(it.toString()).name }, + "svg", + path("${it[1]}/svg").list().collect { file(it.toString()).name }] }] + } + else if (key == "report") { + return [key, file(val[0][1].toString()).name] + } + return [key, val] + } + ).match() } } @@ -68,25 +189,23 @@ nextflow_process { when { process { """ - input[0] = Channel.of(file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true)) - input[1] = [] - input[2] = [] - input[3] = [] - input[4] = [] - input[5] = [] + input[0] = channel.of([ + [ id: 'FASTQC' ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true), + [], + [], + [], + [] + ]) """ } } then { + assert process.success assertAll( - { assert process.success }, - { assert snapshot(process.out.report.collect { file(it).getName() } + - process.out.data.collect { file(it).getName() } + - process.out.plots.collect { file(it).getName() } + - process.out.versions ).match("multiqc_stub") } + { assert snapshot(sanitizeOutput(process.out)).match() } ) } - } } diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap index 2fcbb5f..4489921 100644 --- a/modules/nf-core/multiqc/tests/main.nf.test.snap +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -1,41 +1,422 @@ { - "multiqc_versions_single": { + "sarscov2 single-end [fastqc] [multiple configs]": { "content": [ - [ - "versions.yml:md5,41f391dcedce7f93ca188f3a3ffa0916" - ] + { + "data": [ + [ + [ + "fastqc-status-check-heatmap.txt", + "fastqc_overrepresented_sequences_plot.txt", + "fastqc_per_base_n_content_plot.txt", + "fastqc_per_base_sequence_quality_plot.txt", + "fastqc_per_sequence_gc_content_plot_Counts.txt", + "fastqc_per_sequence_gc_content_plot_Percentages.txt", + "fastqc_per_sequence_quality_scores_plot.txt", + "fastqc_sequence_counts_plot.txt", + "fastqc_sequence_duplication_levels_plot.txt", + "fastqc_sequence_length_distribution_plot.txt", + "fastqc_top_overrepresented_sequences_table.txt", + "llms-full.txt", + "multiqc.log", + "multiqc.parquet", + "multiqc_citations.txt", + "multiqc_data.json", + "multiqc_fastqc.txt", + "multiqc_general_stats.txt", + "multiqc_sources.txt" + ] + ] + ], + "plots": [ + [ + "pdf", + [ + "fastqc-status-check-heatmap.pdf", + "fastqc_overrepresented_sequences_plot.pdf", + "fastqc_per_base_n_content_plot.pdf", + "fastqc_per_base_sequence_quality_plot.pdf", + "fastqc_per_sequence_gc_content_plot_Counts.pdf", + "fastqc_per_sequence_gc_content_plot_Percentages.pdf", + "fastqc_per_sequence_quality_scores_plot.pdf", + "fastqc_sequence_counts_plot-cnt.pdf", + "fastqc_sequence_counts_plot-pct.pdf", + "fastqc_sequence_duplication_levels_plot.pdf", + "fastqc_sequence_length_distribution_plot.pdf", + "fastqc_top_overrepresented_sequences_table.pdf" + ], + "png", + [ + "fastqc-status-check-heatmap.png", + "fastqc_overrepresented_sequences_plot.png", + "fastqc_per_base_n_content_plot.png", + "fastqc_per_base_sequence_quality_plot.png", + "fastqc_per_sequence_gc_content_plot_Counts.png", + "fastqc_per_sequence_gc_content_plot_Percentages.png", + "fastqc_per_sequence_quality_scores_plot.png", + "fastqc_sequence_counts_plot-cnt.png", + "fastqc_sequence_counts_plot-pct.png", + "fastqc_sequence_duplication_levels_plot.png", + "fastqc_sequence_length_distribution_plot.png", + "fastqc_top_overrepresented_sequences_table.png" + ], + "svg", + [ + "fastqc-status-check-heatmap.svg", + "fastqc_overrepresented_sequences_plot.svg", + "fastqc_per_base_n_content_plot.svg", + "fastqc_per_base_sequence_quality_plot.svg", + "fastqc_per_sequence_gc_content_plot_Counts.svg", + "fastqc_per_sequence_gc_content_plot_Percentages.svg", + "fastqc_per_sequence_quality_scores_plot.svg", + "fastqc_sequence_counts_plot-cnt.svg", + "fastqc_sequence_counts_plot-pct.svg", + "fastqc_sequence_duplication_levels_plot.svg", + "fastqc_sequence_length_distribution_plot.svg", + "fastqc_top_overrepresented_sequences_table.svg" + ] + ] + ], + "report": "multiqc_report.html", + "versions": [ + [ + "MULTIQC", + "multiqc", + "1.35" + ] + ] + } ], + "timestamp": "2026-03-17T16:15:42.577775492", "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.4" - }, - "timestamp": "2024-10-02T17:51:46.317523" + "nf-test": "0.9.4", + "nextflow": "25.10.4" + } }, - "multiqc_stub": { + "sarscov2 single-end [fastqc]": { "content": [ - [ - "multiqc_report.html", - "multiqc_data", - "multiqc_plots", - "versions.yml:md5,41f391dcedce7f93ca188f3a3ffa0916" - ] + { + "data": [ + [ + [ + "fastqc-status-check-heatmap.txt", + "fastqc_overrepresented_sequences_plot.txt", + "fastqc_per_base_n_content_plot.txt", + "fastqc_per_base_sequence_quality_plot.txt", + "fastqc_per_sequence_gc_content_plot_Counts.txt", + "fastqc_per_sequence_gc_content_plot_Percentages.txt", + "fastqc_per_sequence_quality_scores_plot.txt", + "fastqc_sequence_counts_plot.txt", + "fastqc_sequence_duplication_levels_plot.txt", + "fastqc_sequence_length_distribution_plot.txt", + "fastqc_top_overrepresented_sequences_table.txt", + "llms-full.txt", + "multiqc.log", + "multiqc.parquet", + "multiqc_citations.txt", + "multiqc_data.json", + "multiqc_fastqc.txt", + "multiqc_general_stats.txt", + "multiqc_software_versions.txt", + "multiqc_sources.txt" + ] + ] + ], + "plots": [ + [ + "pdf", + [ + "fastqc-status-check-heatmap.pdf", + "fastqc_overrepresented_sequences_plot.pdf", + "fastqc_per_base_n_content_plot.pdf", + "fastqc_per_base_sequence_quality_plot.pdf", + "fastqc_per_sequence_gc_content_plot_Counts.pdf", + "fastqc_per_sequence_gc_content_plot_Percentages.pdf", + "fastqc_per_sequence_quality_scores_plot.pdf", + "fastqc_sequence_counts_plot-cnt.pdf", + "fastqc_sequence_counts_plot-pct.pdf", + "fastqc_sequence_duplication_levels_plot.pdf", + "fastqc_sequence_length_distribution_plot.pdf", + "fastqc_top_overrepresented_sequences_table.pdf" + ], + "png", + [ + "fastqc-status-check-heatmap.png", + "fastqc_overrepresented_sequences_plot.png", + "fastqc_per_base_n_content_plot.png", + "fastqc_per_base_sequence_quality_plot.png", + "fastqc_per_sequence_gc_content_plot_Counts.png", + "fastqc_per_sequence_gc_content_plot_Percentages.png", + "fastqc_per_sequence_quality_scores_plot.png", + "fastqc_sequence_counts_plot-cnt.png", + "fastqc_sequence_counts_plot-pct.png", + "fastqc_sequence_duplication_levels_plot.png", + "fastqc_sequence_length_distribution_plot.png", + "fastqc_top_overrepresented_sequences_table.png" + ], + "svg", + [ + "fastqc-status-check-heatmap.svg", + "fastqc_overrepresented_sequences_plot.svg", + "fastqc_per_base_n_content_plot.svg", + "fastqc_per_base_sequence_quality_plot.svg", + "fastqc_per_sequence_gc_content_plot_Counts.svg", + "fastqc_per_sequence_gc_content_plot_Percentages.svg", + "fastqc_per_sequence_quality_scores_plot.svg", + "fastqc_sequence_counts_plot-cnt.svg", + "fastqc_sequence_counts_plot-pct.svg", + "fastqc_sequence_duplication_levels_plot.svg", + "fastqc_sequence_length_distribution_plot.svg", + "fastqc_top_overrepresented_sequences_table.svg" + ] + ] + ], + "report": "multiqc_report.html", + "versions": [ + [ + "MULTIQC", + "multiqc", + "1.35" + ] + ] + } ], + "timestamp": "2026-03-17T16:21:17.072841555", "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.4" - }, - "timestamp": "2024-10-02T17:52:20.680978" + "nf-test": "0.9.4", + "nextflow": "25.10.4" + } }, - "multiqc_versions_config": { + "sarscov2 single-end [fastqc] - stub": { "content": [ - [ - "versions.yml:md5,41f391dcedce7f93ca188f3a3ffa0916" - ] + { + "data": [ + [ + { + "id": "FASTQC" + }, + [ + ".stub:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "plots": [ + [ + { + "id": "FASTQC" + }, + [ + ".stub:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "report": [ + [ + { + "id": "FASTQC" + }, + "multiqc_report.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + [ + "MULTIQC", + "multiqc", + "1.35" + ] + ] + } ], + "timestamp": "2026-02-26T15:14:39.789193051", "meta": { - "nf-test": "0.9.0", - "nextflow": "24.04.4" - }, - "timestamp": "2024-10-02T17:52:09.185842" + "nf-test": "0.9.4", + "nextflow": "25.10.4" + } + }, + "sarscov2 single-end [fastqc] [config]": { + "content": [ + { + "data": [ + [ + [ + "fastqc-status-check-heatmap.txt", + "fastqc_overrepresented_sequences_plot.txt", + "fastqc_per_base_n_content_plot.txt", + "fastqc_per_base_sequence_quality_plot.txt", + "fastqc_per_sequence_gc_content_plot_Counts.txt", + "fastqc_per_sequence_gc_content_plot_Percentages.txt", + "fastqc_per_sequence_quality_scores_plot.txt", + "fastqc_sequence_counts_plot.txt", + "fastqc_sequence_duplication_levels_plot.txt", + "fastqc_sequence_length_distribution_plot.txt", + "fastqc_top_overrepresented_sequences_table.txt", + "llms-full.txt", + "multiqc.log", + "multiqc.parquet", + "multiqc_citations.txt", + "multiqc_data.json", + "multiqc_fastqc.txt", + "multiqc_general_stats.txt", + "multiqc_sources.txt" + ] + ] + ], + "plots": [ + [ + "pdf", + [ + "fastqc-status-check-heatmap.pdf", + "fastqc_overrepresented_sequences_plot.pdf", + "fastqc_per_base_n_content_plot.pdf", + "fastqc_per_base_sequence_quality_plot.pdf", + "fastqc_per_sequence_gc_content_plot_Counts.pdf", + "fastqc_per_sequence_gc_content_plot_Percentages.pdf", + "fastqc_per_sequence_quality_scores_plot.pdf", + "fastqc_sequence_counts_plot-cnt.pdf", + "fastqc_sequence_counts_plot-pct.pdf", + "fastqc_sequence_duplication_levels_plot.pdf", + "fastqc_sequence_length_distribution_plot.pdf", + "fastqc_top_overrepresented_sequences_table.pdf" + ], + "png", + [ + "fastqc-status-check-heatmap.png", + "fastqc_overrepresented_sequences_plot.png", + "fastqc_per_base_n_content_plot.png", + "fastqc_per_base_sequence_quality_plot.png", + "fastqc_per_sequence_gc_content_plot_Counts.png", + "fastqc_per_sequence_gc_content_plot_Percentages.png", + "fastqc_per_sequence_quality_scores_plot.png", + "fastqc_sequence_counts_plot-cnt.png", + "fastqc_sequence_counts_plot-pct.png", + "fastqc_sequence_duplication_levels_plot.png", + "fastqc_sequence_length_distribution_plot.png", + "fastqc_top_overrepresented_sequences_table.png" + ], + "svg", + [ + "fastqc-status-check-heatmap.svg", + "fastqc_overrepresented_sequences_plot.svg", + "fastqc_per_base_n_content_plot.svg", + "fastqc_per_base_sequence_quality_plot.svg", + "fastqc_per_sequence_gc_content_plot_Counts.svg", + "fastqc_per_sequence_gc_content_plot_Percentages.svg", + "fastqc_per_sequence_quality_scores_plot.svg", + "fastqc_sequence_counts_plot-cnt.svg", + "fastqc_sequence_counts_plot-pct.svg", + "fastqc_sequence_duplication_levels_plot.svg", + "fastqc_sequence_length_distribution_plot.svg", + "fastqc_top_overrepresented_sequences_table.svg" + ] + ] + ], + "report": "multiqc_report.html", + "versions": [ + [ + "MULTIQC", + "multiqc", + "1.35" + ] + ] + } + ], + "timestamp": "2026-03-17T16:15:30.372239611", + "meta": { + "nf-test": "0.9.4", + "nextflow": "25.10.4" + } + }, + "sarscov2 single-end [fastqc] - custom prefix": { + "content": [ + { + "data": [ + [ + [ + "fastqc-status-check-heatmap.txt", + "fastqc_overrepresented_sequences_plot.txt", + "fastqc_per_base_n_content_plot.txt", + "fastqc_per_base_sequence_quality_plot.txt", + "fastqc_per_sequence_gc_content_plot_Counts.txt", + "fastqc_per_sequence_gc_content_plot_Percentages.txt", + "fastqc_per_sequence_quality_scores_plot.txt", + "fastqc_sequence_counts_plot.txt", + "fastqc_sequence_duplication_levels_plot.txt", + "fastqc_sequence_length_distribution_plot.txt", + "fastqc_top_overrepresented_sequences_table.txt", + "llms-full.txt", + "multiqc.log", + "multiqc.parquet", + "multiqc_citations.txt", + "multiqc_data.json", + "multiqc_fastqc.txt", + "multiqc_general_stats.txt", + "multiqc_software_versions.txt", + "multiqc_sources.txt" + ] + ] + ], + "plots": [ + [ + "pdf", + [ + "fastqc-status-check-heatmap.pdf", + "fastqc_overrepresented_sequences_plot.pdf", + "fastqc_per_base_n_content_plot.pdf", + "fastqc_per_base_sequence_quality_plot.pdf", + "fastqc_per_sequence_gc_content_plot_Counts.pdf", + "fastqc_per_sequence_gc_content_plot_Percentages.pdf", + "fastqc_per_sequence_quality_scores_plot.pdf", + "fastqc_sequence_counts_plot-cnt.pdf", + "fastqc_sequence_counts_plot-pct.pdf", + "fastqc_sequence_duplication_levels_plot.pdf", + "fastqc_sequence_length_distribution_plot.pdf", + "fastqc_top_overrepresented_sequences_table.pdf" + ], + "png", + [ + "fastqc-status-check-heatmap.png", + "fastqc_overrepresented_sequences_plot.png", + "fastqc_per_base_n_content_plot.png", + "fastqc_per_base_sequence_quality_plot.png", + "fastqc_per_sequence_gc_content_plot_Counts.png", + "fastqc_per_sequence_gc_content_plot_Percentages.png", + "fastqc_per_sequence_quality_scores_plot.png", + "fastqc_sequence_counts_plot-cnt.png", + "fastqc_sequence_counts_plot-pct.png", + "fastqc_sequence_duplication_levels_plot.png", + "fastqc_sequence_length_distribution_plot.png", + "fastqc_top_overrepresented_sequences_table.png" + ], + "svg", + [ + "fastqc-status-check-heatmap.svg", + "fastqc_overrepresented_sequences_plot.svg", + "fastqc_per_base_n_content_plot.svg", + "fastqc_per_base_sequence_quality_plot.svg", + "fastqc_per_sequence_gc_content_plot_Counts.svg", + "fastqc_per_sequence_gc_content_plot_Percentages.svg", + "fastqc_per_sequence_quality_scores_plot.svg", + "fastqc_sequence_counts_plot-cnt.svg", + "fastqc_sequence_counts_plot-pct.svg", + "fastqc_sequence_duplication_levels_plot.svg", + "fastqc_sequence_length_distribution_plot.svg", + "fastqc_top_overrepresented_sequences_table.svg" + ] + ] + ], + "report": "custom_prefix.html", + "versions": [ + [ + "MULTIQC", + "multiqc", + "1.35" + ] + ] + } + ], + "timestamp": "2026-03-17T16:15:18.189023981", + "meta": { + "nf-test": "0.9.4", + "nextflow": "25.10.4" + } } } \ No newline at end of file diff --git a/modules/nf-core/multiqc/tests/nextflow.config b/modules/nf-core/multiqc/tests/nextflow.config index c537a6a..374dfef 100644 --- a/modules/nf-core/multiqc/tests/nextflow.config +++ b/modules/nf-core/multiqc/tests/nextflow.config @@ -1,5 +1,6 @@ process { withName: 'MULTIQC' { ext.prefix = null + ext.args = '-p' } } diff --git a/modules/nf-core/multiqc/tests/tags.yml b/modules/nf-core/multiqc/tests/tags.yml deleted file mode 100644 index bea6c0d..0000000 --- a/modules/nf-core/multiqc/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -multiqc: - - modules/nf-core/multiqc/** diff --git a/modules/nf-core/samtools/index/environment.yml b/modules/nf-core/samtools/index/environment.yml new file mode 100644 index 0000000..946bb36 --- /dev/null +++ b/modules/nf-core/samtools/index/environment.yml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + # renovate: datasource=conda depName=bioconda/htslib + - bioconda::htslib=1.23.1 + # renovate: datasource=conda depName=bioconda/samtools + - bioconda::samtools=1.23.1 diff --git a/modules/nf-core/samtools/index/main.nf b/modules/nf-core/samtools/index/main.nf new file mode 100644 index 0000000..5b5756b --- /dev/null +++ b/modules/nf-core/samtools/index/main.nf @@ -0,0 +1,38 @@ +process SAMTOOLS_INDEX { + tag "${meta.id}" + label 'process_low' + + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/8c/8c5d2818c8b9f58e1fba77ce219fdaf32087ae53e857c4a496402978af26e78c/data' + : 'community.wave.seqera.io/library/htslib_samtools:1.23.1--5b6bb4ede7e612e5'}" + + input: + tuple val(meta), path(input) + + output: + tuple val(meta), path("*.{bai,csi,crai}"), emit: index + tuple val("${task.process}"), val('samtools'), eval("samtools version | sed '1!d;s/.* //'"), emit: versions_samtools, topic: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + """ + samtools \\ + index \\ + -@ ${task.cpus} \\ + ${args} \\ + ${input} + """ + + stub: + def args = task.ext.args ?: '' + def extension = file(input).getExtension() == 'cram' + ? "crai" + : args.contains("-c") ? "csi" : "bai" + """ + touch ${input}.${extension} + """ +} diff --git a/modules/nf-core/samtools/index/meta.yml b/modules/nf-core/samtools/index/meta.yml new file mode 100644 index 0000000..d4938bc --- /dev/null +++ b/modules/nf-core/samtools/index/meta.yml @@ -0,0 +1,70 @@ +name: samtools_index +description: Index SAM/BAM/CRAM file +keywords: + - index + - bam + - sam + - cram +tools: + - samtools: + description: | + SAMtools is a set of utilities for interacting with and post-processing + short DNA sequence read alignments in the SAM, BAM and CRAM formats, written by Heng Li. + These files are generated as output by short read aligners like BWA. + homepage: http://www.htslib.org/ + documentation: http://www.htslib.org/doc/samtools.html + doi: 10.1093/bioinformatics/btp352 + licence: + - "MIT" + identifier: biotools:samtools +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - input: + type: file + description: input file + ontologies: [] +output: + index: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.{bai,csi,crai}": + type: file + description: BAM/CRAM/SAM index file + pattern: "*.{bai,csi,crai}" + ontologies: [] + versions_samtools: + - - ${task.process}: + type: string + description: The name of the process + - samtools: + type: string + description: The name of the tool + - samtools version | sed '1!d;s/.* //': + type: eval + description: The expression to obtain the version of the tool +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - samtools: + type: string + description: The name of the tool + - samtools version | sed '1!d;s/.* //': + type: eval + description: The expression to obtain the version of the tool +authors: + - "@drpatelh" + - "@ewels" + - "@maxulysse" +maintainers: + - "@ewels" + - "@maxulysse" + - "@matthdsm" diff --git a/modules/nf-core/samtools/index/tests/csi.nextflow.config b/modules/nf-core/samtools/index/tests/csi.nextflow.config new file mode 100644 index 0000000..4af6d82 --- /dev/null +++ b/modules/nf-core/samtools/index/tests/csi.nextflow.config @@ -0,0 +1,6 @@ +process { + + withName: SAMTOOLS_INDEX { + ext.args = '-c' + } +} diff --git a/modules/nf-core/samtools/index/tests/main.nf.test b/modules/nf-core/samtools/index/tests/main.nf.test new file mode 100644 index 0000000..f9d3922 --- /dev/null +++ b/modules/nf-core/samtools/index/tests/main.nf.test @@ -0,0 +1,155 @@ +nextflow_process { + + name "Test Process SAMTOOLS_INDEX" + script "../main.nf" + process "SAMTOOLS_INDEX" + tag "modules" + tag "modules_nfcore" + tag "samtools" + tag "samtools/index" + + test("bai") { + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.index, + process.out.findAll { key, val -> key.startsWith('versions') } + ).match() } + ) + } + } + + test("crai") { + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/cram/test.paired_end.recalibrated.sorted.cram', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.index, + process.out.findAll { key, val -> key.startsWith('versions') } + ).match() } + ) + } + } + + test("csi") { + config "./csi.nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + file(process.out.index[0][1]).name, + process.out.findAll { key, val -> key.startsWith('versions') } + ).match() } + ) + } + } + + test("bai - stub") { + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.index, + process.out.findAll { key, val -> key.startsWith('versions') } + ).match() } + ) + } + } + + test("crai - stub") { + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/cram/test.paired_end.recalibrated.sorted.cram', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.index, + process.out.findAll { key, val -> key.startsWith('versions') } + ).match() } + ) + } + } + + test("csi - stub") { + options "-stub" + config "./csi.nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.index, + process.out.findAll { key, val -> key.startsWith('versions') } + ).match() } + ) + } + } +} diff --git a/modules/nf-core/samtools/index/tests/main.nf.test.snap b/modules/nf-core/samtools/index/tests/main.nf.test.snap new file mode 100644 index 0000000..337dec7 --- /dev/null +++ b/modules/nf-core/samtools/index/tests/main.nf.test.snap @@ -0,0 +1,156 @@ +{ + "csi - stub": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.sorted.bam.csi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_INDEX", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:00:39.171613" + }, + "crai - stub": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.recalibrated.sorted.cram.crai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_INDEX", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:00:32.838795" + }, + "bai - stub": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.sorted.bam.bai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_INDEX", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:00:25.255379" + }, + "csi": { + "content": [ + "test.paired_end.sorted.bam.csi", + { + "versions_samtools": [ + [ + "SAMTOOLS_INDEX", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:00:18.414839" + }, + "crai": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.recalibrated.sorted.cram.crai:md5,14bc3bd5c89cacc8f4541f9062429029" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_INDEX", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:00:13.571297" + }, + "bai": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.sorted.bam.bai:md5,704c10dd1326482448ca3073fdebc2f4" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_INDEX", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:00:06.767362" + } +} \ No newline at end of file diff --git a/modules/nf-core/samtools/sort/environment.yml b/modules/nf-core/samtools/sort/environment.yml new file mode 100644 index 0000000..946bb36 --- /dev/null +++ b/modules/nf-core/samtools/sort/environment.yml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + # renovate: datasource=conda depName=bioconda/htslib + - bioconda::htslib=1.23.1 + # renovate: datasource=conda depName=bioconda/samtools + - bioconda::samtools=1.23.1 diff --git a/modules/nf-core/samtools/sort/main.nf b/modules/nf-core/samtools/sort/main.nf new file mode 100644 index 0000000..e75ce91 --- /dev/null +++ b/modules/nf-core/samtools/sort/main.nf @@ -0,0 +1,97 @@ +process SAMTOOLS_SORT { + tag "${meta.id}" + label 'process_medium' + + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/8c/8c5d2818c8b9f58e1fba77ce219fdaf32087ae53e857c4a496402978af26e78c/data' + : 'community.wave.seqera.io/library/htslib_samtools:1.23.1--5b6bb4ede7e612e5'}" + + input: + tuple val(meta), path(bam) + tuple val(meta2), path(fasta), path(fai) + val index_format + + output: + tuple val(meta), path("${prefix}.bam"), emit: bam, optional: true + tuple val(meta), path("${prefix}.cram"), emit: cram, optional: true + tuple val(meta), path("${prefix}.sam"), emit: sam, optional: true + tuple val(meta), path("${prefix}.${extension}.{crai,csi,bai}"), emit: index, optional: true + tuple val("${task.process}"), val('samtools'), eval("samtools version | sed '1!d;s/.* //'"), topic: versions, emit: versions_samtools + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}" + extension = args.contains("--output-fmt sam") + ? "sam" + : args.contains("--output-fmt cram") + ? "cram" + : "bam" + def reference = fasta ? "--reference ${fasta}" : "" + //setting default values + def write_index = "" + def output_file = "${prefix}.${extension}" + + // Update if index is requested + if (index_format != '' && index_format) { + write_index = "--write-index" + output_file = "${prefix}.${extension}##idx##${prefix}.${extension}.${index_format}" + } + def is_sam = (bam instanceof List ? bam[0] : bam).name.endsWith('.sam') + if (index_format) { + if (!index_format.matches('bai|csi|crai')) { + error("Index format not one of bai, csi, crai.") + } + else if (extension == "sam") { + error("Indexing not compatible with SAM output") + } + } + if ("${bam}" == "${prefix}.bam") { + error("Input and output names are the same, use \"task.ext.prefix\" to disambiguate!") + } + if ("${bam}" == "${prefix}.bam") { + error("Input and output names are the same, use \"task.ext.prefix\" to disambiguate!") + } + + def input_source = is_sam ? "${bam}" : "-" + def pre_command = is_sam ? "" : "samtools cat ${bam} | " + + """ + ${pre_command}samtools sort \\ + ${args} \\ + -T ${prefix} \\ + --threads ${task.cpus} \\ + ${reference} \\ + -o ${output_file} \\ + ${write_index} \\ + ${input_source} + """ + + stub: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}" + extension = args.contains("--output-fmt sam") + ? "sam" + : args.contains("--output-fmt cram") + ? "cram" + : "bam" + + if (index_format) { + if (!index_format.matches('bai|csi|crai')) { + error("Index format not one of bai, csi, crai.") + } + else if (extension == "sam") { + error("Indexing not compatible with SAM output") + } + } + + index = index_format ? "touch ${prefix}.${extension}.${index_format}" : "" + + """ + touch ${prefix}.${extension} + ${index} + """ +} diff --git a/modules/nf-core/samtools/sort/meta.yml b/modules/nf-core/samtools/sort/meta.yml new file mode 100644 index 0000000..0447a95 --- /dev/null +++ b/modules/nf-core/samtools/sort/meta.yml @@ -0,0 +1,126 @@ +name: samtools_sort +description: Sort SAM/BAM/CRAM file +keywords: + - sort + - bam + - sam + - cram +tools: + - samtools: + description: | + SAMtools is a set of utilities for interacting with and post-processing + short DNA sequence read alignments in the SAM, BAM and CRAM formats, written by Heng Li. + These files are generated as output by short read aligners like BWA. + homepage: http://www.htslib.org/ + documentation: http://www.htslib.org/doc/samtools.html + doi: 10.1093/bioinformatics/btp352 + licence: ["MIT"] + identifier: biotools:samtools +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - bam: + type: file + description: BAM/CRAM/SAM file(s) + pattern: "*.{bam,cram,sam}" + ontologies: [] + - - meta2: + type: map + description: | + Groovy Map containing reference information + e.g. [ id:'genome' ] + - fasta: + type: file + description: Reference genome FASTA file + pattern: "*.{fa,fasta,fna}" + optional: true + ontologies: [] + - fai: + type: file + description: Reference genome FASTA index file + pattern: "*.{fai}" + optional: true + ontologies: [] + - index_format: + type: string + description: Index format to use (optional) + pattern: "bai|csi|crai" +output: + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "${prefix}.bam": + type: file + description: Sorted BAM file + pattern: "*.{bam}" + ontologies: [] + cram: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "${prefix}.cram": + type: file + description: Sorted CRAM file + pattern: "*.{cram}" + ontologies: [] + sam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "${prefix}.sam": + type: file + description: Sorted SAM file + pattern: "*.{sam}" + ontologies: [] + index: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "${prefix}.${extension}.{crai,csi,bai}": + type: file + description: CRAM index file (optional) + pattern: "*.{crai,csi,bai}" + ontologies: [] + versions_samtools: + - - ${task.process}: + type: string + description: The process the versions were collected from + - samtools: + type: string + description: The tool name + - "samtools version | sed '1!d;s/.* //'": + type: string + description: The command used to generate the version of the tool + +topics: + versions: + - - ${task.process}: + type: string + description: The process the versions were collected from + - samtools: + type: string + description: The tool name + - "samtools version | sed '1!d;s/.* //'": + type: string + description: The command used to generate the version of the tool + +authors: + - "@drpatelh" + - "@ewels" + - "@matthdsm" +maintainers: + - "@drpatelh" + - "@ewels" + - "@matthdsm" diff --git a/modules/nf-core/samtools/sort/tests/main.nf.test b/modules/nf-core/samtools/sort/tests/main.nf.test new file mode 100644 index 0000000..b60edf8 --- /dev/null +++ b/modules/nf-core/samtools/sort/tests/main.nf.test @@ -0,0 +1,410 @@ +nextflow_process { + + name "Test Process SAMTOOLS_SORT" + script "../main.nf" + process "SAMTOOLS_SORT" + tag "modules" + tag "modules_nfcore" + tag "samtools" + tag "samtools/sort" + + test("bam_no_index") { + + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.bam', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.index, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("bam_bai_index") { + + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.bam', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = 'bai' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.index, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("bam_csi_index") { + + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.bam', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = 'csi' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.index, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("multiple bam") { + + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test2.paired_end.sorted.bam', checkIfExists: true) + ] + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.index.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("multiple bam bai index") { + + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test2.paired_end.sorted.bam', checkIfExists: true) + ] + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = 'bai' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.index.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("multiple bam csi index") { + + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test2.paired_end.sorted.bam', checkIfExists: true) + ] + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = 'csi' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.index.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("cram") { + + config "./nextflow_cram.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/cram/test.paired_end.sorted.cram', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.cram.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.index.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("bam - stub") { + + options "-stub" + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.bam', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out.findAll { key, val -> key.startsWith("versions") }).match() } + ) + } + } + + test("multiple bam - stub") { + + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test2.paired_end.sorted.bam', checkIfExists: true) + ] + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out.findAll { key, val -> key.startsWith("versions") }).match() } + ) + } + } + + test("cram - stub") { + + options "-stub" + config "./nextflow_cram.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/cram/test.paired_end.sorted.cram', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out.findAll { key, val -> key.startsWith("versions") }).match() } + ) + } + } + + test("multi_sam") { + + config "./nextflow_sam.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pairtools/mock.sam', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.index.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + + test("sam") { + + config "./nextflow_sam.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pairtools/mock.sam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pairtools/mock.sam', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.index.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } +} diff --git a/modules/nf-core/samtools/sort/tests/main.nf.test.snap b/modules/nf-core/samtools/sort/tests/main.nf.test.snap new file mode 100644 index 0000000..5ce05c3 --- /dev/null +++ b/modules/nf-core/samtools/sort/tests/main.nf.test.snap @@ -0,0 +1,344 @@ +{ + "cram": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.cram" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.cram.crai" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:04:36.491063" + }, + "bam_csi_index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,53aea06779611856bc481c60beabecaa" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam.csi:md5,f77a4adb3dde616d7eafd28db2ed147c" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:04:14.341977" + }, + "bam - stub": { + "content": [ + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:04:43.558376" + }, + "multiple bam bai index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,a15f775c655d4a3b080812a8aae84d34" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam.bai" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:04:25.647565" + }, + "cram - stub": { + "content": [ + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:04:54.684578" + }, + "multiple bam": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,f4343475d9ed2c261f31e1e49d67c1b6" + ] + ], + [ + + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:04:20.2368" + }, + "multiple bam - stub": { + "content": [ + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:04:48.874947" + }, + "bam_no_index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,9277ba4bea590ae1b84e6ab06d11d79b" + ] + ], + [ + + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:04:03.721646" + }, + "multi_sam": { + "content": [ + [ + + ], + [ + + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:05:00.624092" + }, + "multiple bam csi index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,f168809dc154156c40548c06d0f46791" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam.csi" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:04:31.11865" + }, + "sam": { + "content": [ + [ + + ], + [ + + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:05:06.309319" + }, + "bam_bai_index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,2ca2d7f2368251d3f06f84afa49865a5" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam.bai:md5,66dca3dc2e314029035799f6f44f60d1" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.23.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.4" + }, + "timestamp": "2026-03-19T09:04:09.147615" + } +} \ No newline at end of file diff --git a/modules/nf-core/samtools/sort/tests/nextflow.config b/modules/nf-core/samtools/sort/tests/nextflow.config new file mode 100644 index 0000000..ca694ca --- /dev/null +++ b/modules/nf-core/samtools/sort/tests/nextflow.config @@ -0,0 +1,6 @@ +process { + + withName: SAMTOOLS_SORT { + ext.prefix = { "${meta.id}.sorted" } + } +} diff --git a/modules/nf-core/samtools/sort/tests/nextflow_cram.config b/modules/nf-core/samtools/sort/tests/nextflow_cram.config new file mode 100644 index 0000000..8ebc9d9 --- /dev/null +++ b/modules/nf-core/samtools/sort/tests/nextflow_cram.config @@ -0,0 +1,7 @@ +process { + + withName: SAMTOOLS_SORT { + ext.prefix = { "${meta.id}.sorted" } + ext.args = "--write-index --output-fmt cram" + } +} diff --git a/modules/nf-core/samtools/sort/tests/nextflow_sam.config b/modules/nf-core/samtools/sort/tests/nextflow_sam.config new file mode 100644 index 0000000..29ee6a8 --- /dev/null +++ b/modules/nf-core/samtools/sort/tests/nextflow_sam.config @@ -0,0 +1,7 @@ +process { + + withName: SAMTOOLS_SORT { + ext.prefix = { "${meta.id}.sorted" } + ext.args = "--output-fmt sam" + } +} diff --git a/nextflow.config b/nextflow.config index 3fd3e96..3bfffba 100644 --- a/nextflow.config +++ b/nextflow.config @@ -1,6 +1,6 @@ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - nf-core/dmscore Nextflow config file + nf-core/deepmutscan Nextflow config file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default config options for all compute environments ---------------------------------------------------------------------------------------- @@ -9,14 +9,29 @@ // Global default params, used in configs params { - // TODO nf-core: Specify your pipeline's command line flags - // Input options input = null + min_counts = 1 + base_qual = 40 + min_flank = 2 + error_correction = 'false_doubles' + false_doubles_method = 'mle' + false_doubles_codon_window = 40 + pdb = null + mutagenesis_type = 'nnk' + run_seqdepth = true + reading_frame = null + custom_codon_library = '/NULL' + sliding_window_size = 10 + aimed_cov = 100 + fitness = false + dimsum = false + mutscan = false + // References genome = null igenomes_base = 's3://ngi-igenomes/igenomes/' - igenomes_ignore = false + igenomes_ignore = true // MultiQC options multiqc_config = null @@ -32,13 +47,14 @@ params { email_on_fail = null plaintext_email = false monochrome_logs = false - hook_url = null help = false help_full = false show_hidden = false version = false pipelines_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/' - trace_report_suffix = new java.util.Date().format( 'yyyy-MM-dd_HH-mm-ss')// Config options + trace_report_suffix = new java.util.Date().format( 'yyyy-MM-dd_HH-mm-ss') + + // Config options config_profile_name = null config_profile_description = null @@ -51,6 +67,10 @@ params { validate_params = true } +// Backwards compatibility for publishDir syntax +outputDir = params.outdir +workflow.output.mode = params.publish_dir_mode + // Load base.config by default for all pipelines includeConfig 'conf/base.config' @@ -91,7 +111,17 @@ profiles { apptainer.enabled = false docker.runOptions = '-u $(id -u):$(id -g)' } - arm { + arm64 { + process.arch = 'arm64' + // For now if you're using arm64 you have to use wave for the sake of the maintainers + // wave profile + apptainer.ociAutoPull = true + singularity.ociAutoPull = true + wave.enabled = true + wave.freeze = true + wave.strategy = 'conda,container' + } + emulate_amd64 { docker.runOptions = '-u $(id -u):$(id -g) --platform=linux/amd64' } singularity { @@ -148,28 +178,24 @@ profiles { wave.freeze = true wave.strategy = 'conda,container' } - gitpod { - executor.name = 'local' - executor.cpus = 4 - executor.memory = 8.GB - process { - resourceLimits = [ - memory: 8.GB, - cpus : 4, - time : 1.h - ] - } + gpu { + docker.runOptions = '-u $(id -u):$(id -g) --gpus all' + apptainer.runOptions = '--nv' + singularity.runOptions = '--nv' } test { includeConfig 'conf/test.config' } test_full { includeConfig 'conf/test_full.config' } } -// Load nf-core custom profiles from different Institutions -includeConfig !System.getenv('NXF_OFFLINE') && params.custom_config_base ? "${params.custom_config_base}/nfcore_custom.config" : "/dev/null" +// Load nf-core custom profiles from different institutions -// Load nf-core/dmscore custom profiles from different institutions. -// TODO nf-core: Optionally, you can add a pipeline-specific nf-core config at https://github.com/nf-core/configs -// includeConfig !System.getenv('NXF_OFFLINE') && params.custom_config_base ? "${params.custom_config_base}/pipeline/dmscore.config" : "/dev/null" +// If params.custom_config_base is set AND either the NXF_OFFLINE environment variable is not set or params.custom_config_base is a local path, the nfcore_custom.config file from the specified base path is included. +// Load nf-core/deepmutscan custom profiles from different institutions. +includeConfig params.custom_config_base && (!System.getenv('NXF_OFFLINE') || !params.custom_config_base.startsWith('http')) ? "${params.custom_config_base}/nfcore_custom.config" : "/dev/null" + + +// Load nf-core/deepmutscan custom profiles from different institutions. +includeConfig params.custom_config_base && (!System.getenv('NXF_OFFLINE') || !params.custom_config_base.startsWith('http')) ? "${params.custom_config_base}/pipeline/deepmutscan.config" : "/dev/null" // Set default registry for Apptainer, Docker, Podman, Charliecloud and Singularity independent of -profile // Will not be used unless Apptainer / Docker / Podman / Charliecloud / Singularity are enabled @@ -195,14 +221,14 @@ env { } // Set bash options -process.shell = """\ -bash - -set -e # Exit if a tool returns a non-zero status/exit code -set -u # Treat unset variables and parameters as an error -set -o pipefail # Returns the status of the last command to exit with a non-zero status or zero if all successfully execute -set -C # No clobber - prevent output redirection from overwriting files. -""" +process.shell = [ + "bash", + "-C", // No clobber - prevent output redirection from overwriting files. + "-e", // Exit if a tool returns a non-zero status/exit code + "-u", // Treat unset variables and parameters as an error + "-o", // Returns the status of the last command to exit.. + "pipefail" // ..with a non-zero status or zero if all successfully execute +] // Disable process selector warnings by default. Use debug profile to enable warnings. nextflow.enable.configProcessNamesValidation = false @@ -225,64 +251,63 @@ dag { } manifest { - name = 'nf-core/dmscore' - author = """Benjamin Wehnert & Max Stammnitz""" // The author field is deprecated from Nextflow version 24.10.0, use contributors instead + name = 'nf-core/deepmutscan' contributors = [ - // TODO nf-core: Update the field with the details of the contributors to your pipeline. New with Nextflow version 24.10.0 [ - name: 'Benjamin Wehnert & Max Stammnitz', - affiliation: '', - email: '', - github: '', - contribution: [], // List of contribution types ('author', 'maintainer' or 'contributor') - orcid: '' + name: 'Benjamin Wehnert', + affiliation: 'Centre for Genomic Regulation (CRG), Barcelona, Spain', + email: 'wehnertbenjamin@gmail.com', + github: '@BenjaminWehnert1008', + contribution: ['author', 'maintainer'], + orcid: '0009-0001-8012-6003' + ], + [ + name: 'Maximilian R. Stammnitz', + affiliation: 'Centre for Genomic Regulation (CRG), Barcelona, Spain', + email: 'maximilian.stammnitz@crg.eu', + github: '@MaximilianStammnitz', + contribution: ['author', 'maintainer'], + orcid: '0000-0002-1704-9199' + ], + [ + name: 'Fei Sang', + affiliation: 'Wellcome Sanger Institute, Hinxton, United Kingdom', + email: 'fs18@sanger.ac.uk', + github: '@fei-hgi', + contribution: ['contributor'], + orcid: '0000-0002-8746-5868' + ], + [ + name: 'JΓΊlia Mir-Pedrol', + affiliation: 'Centre for Genomic Regulation (CRG), Barcelona, Spain', + email: 'julia.mir@crg.eu', + github: '@mirpedrol', + contribution: ['contributor'], + orcid: '0000-0001-6104-9260' + ], + [ + name: 'Matthias HΓΆrtenhuber', + affiliation: 'SciLifeLab, Uppsala University, Uppsala, Sweden', + email: 'matthias.hortenhuber@scilifelab.uu.se', + github: '@mashehu', + contribution: ['contributor'], + orcid: '0000-0002-5599-5565' ], ] - homePage = 'https://github.com/nf-core/dmscore' - description = """Until now, most Deep Mutational Scanning (DMS) experiments relied on variant-specific barcoded libraries for sequencing. This method enabled DMS on large proteins and led to many great publications. Recently, efforts have increased to make use of the classic and more simple random fragmentation-based short-read sequencing (β€œshotgun-sequencing”). This saves time and money and due to its simpler experimental design is less prone to mistakes. dmscore handles the essential computational steps, processing the raw FASTQ files and generating a count table of variants. Along the way, it provides multiple QC metrics, enabling users to quickly evaluate the success of their experimental setup.""" + homePage = 'https://github.com/nf-core/deepmutscan' + description = """Processing and analysis of deep mutational scanning (DMS) data from shotgun and amplicon sequencing: read alignment, variant counting, sequencing-error correction, library QC and fitness estimation""" mainScript = 'main.nf' defaultBranch = 'master' - nextflowVersion = '!>=24.04.2' - version = '1.0.0dev' + nextflowVersion = '!>=25.10.4' + version = '1.0.0' doi = '' } // Nextflow plugins plugins { - id 'nf-schema@2.3.0' // Validation of pipeline parameters and creation of an input channel from a sample sheet + id 'nf-schema@2.5.1' // Validation of pipeline parameters and creation of an input channel from a sample sheet } -validation { - defaultIgnoreParams = ["genomes"] - monochromeLogs = params.monochrome_logs - help { - enabled = true - command = "nextflow run nf-core/dmscore -profile --input samplesheet.csv --outdir " - fullParameter = "help_full" - showHiddenParameter = "show_hidden" - beforeText = """ --\033[2m----------------------------------------------------\033[0m- - \033[0;32m,--.\033[0;30m/\033[0;32m,-.\033[0m -\033[0;34m ___ __ __ __ ___ \033[0;32m/,-._.--~\'\033[0m -\033[0;34m |\\ | |__ __ / ` / \\ |__) |__ \033[0;33m} {\033[0m -\033[0;34m | \\| | \\__, \\__/ | \\ |___ \033[0;32m\\`-._,-`-,\033[0m - \033[0;32m`._,._,\'\033[0m -\033[0;35m nf-core/dmscore ${manifest.version}\033[0m --\033[2m----------------------------------------------------\033[0m- -""" - afterText = """${manifest.doi ? "\n* The pipeline\n" : ""}${manifest.doi.tokenize(",").collect { " https://doi.org/${it.trim().replace('https://doi.org/','')}"}.join("\n")}${manifest.doi ? "\n" : ""} -* The nf-core framework - https://doi.org/10.1038/s41587-020-0439-x - -* Software dependencies - https://github.com/nf-core/dmscore/blob/master/CITATIONS.md -""" - } - summary { - beforeText = validation.help.beforeText - afterText = validation.help.afterText - } -} // Load modules.config for DSL2 module specific options includeConfig 'conf/modules.config' diff --git a/nextflow_schema.json b/nextflow_schema.json index e4e5523..35d8c62 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -1,8 +1,8 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/nf-core/dmscore/master/nextflow_schema.json", - "title": "nf-core/dmscore pipeline parameters", - "description": "Until now, most Deep Mutational Scanning (DMS) experiments relied on variant-specific barcoded libraries for sequencing. This method enabled DMS on large proteins and led to many great publications. Recently, efforts have increased to make use of the classic and more simple random fragmentation-based short-read sequencing (β€œshotgun-sequencing”). This saves time and money and due to its simpler experimental design is less prone to mistakes. dmscore handles the essential computational steps, processing the raw FASTQ files and generating a count table of variants. Along the way, it provides multiple QC metrics, enabling users to quickly evaluate the success of their experimental setup.", + "$id": "https://raw.githubusercontent.com/nf-core/deepmutscan/master/nextflow_schema.json", + "title": "nf-core/deepmutscan pipeline parameters", + "description": "Processing and analysis of deep mutational scanning (DMS) data from shotgun and amplicon sequencing: read alignment, variant counting, sequencing-error correction, library QC and fitness estimation", "type": "object", "$defs": { "input_output_options": { @@ -10,7 +10,7 @@ "type": "object", "fa_icon": "fas fa-terminal", "description": "Define where the pipeline should find input data and save output data.", - "required": ["input", "outdir"], + "required": ["input", "outdir", "fasta", "reading_frame"], "properties": { "input": { "type": "string", @@ -20,7 +20,7 @@ "mimetype": "text/csv", "pattern": "^\\S+\\.csv$", "description": "Path to comma-separated file containing information about the samples in the experiment.", - "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 columns, and a header row. See [usage docs](https://nf-co.re/dmscore/usage#samplesheet-input).", + "help_text": "You will need to create a samplesheet with information about the sequencing libraries in your experiment before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 5 columns (`sample`, `type`, `replicate`, `file1`, `file2`) and a header row. See [usage docs](https://nf-co.re/deepmutscan/usage#samplesheet-input).", "fa_icon": "fas fa-file-csv" }, "outdir": { @@ -40,6 +40,93 @@ "type": "string", "description": "MultiQC report title. Printed as page header, used for filename if not otherwise specified.", "fa_icon": "fas fa-file-signature" + }, + "reading_frame": { + "type": "string", + "description": "Nucleotide coordinates of the mutagenised open reading frame within the reference FASTA, in the format 'start-stop' (1-based, inclusive), e.g. '352-1383'.", + "pattern": "^\\d+-\\d+$" + }, + "min_counts": { + "type": "integer", + "description": "Minimum number of counts for a variant to be retained. Variants observed fewer times are removed from the count tables and shown as dropouts in the count heatmaps.", + "minimum": 1, + "default": 1 + }, + "base_qual": { + "type": "integer", + "description": "Minimum base quality (Phred) for a mismatch to be counted as a mutation, and for a base to contribute to sequencing coverage.", + "minimum": 0, + "default": 40 + }, + "min_flank": { + "type": "integer", + "description": "Minimum distance (nt) a mutation or covered base must be from a read edge; also excludes those bases from coverage. Set to 0 to disable edge trimming.", + "minimum": 0, + "default": 2 + }, + "error_correction": { + "type": "string", + "description": "Sequencing-error correction of variant counts. 'false_doubles' (default) estimates the error rate of every single-nucleotide variant from reads that carry it together with a nearby programmed multi-nucleotide codon variant; 'wildtype' subtracts the background measured by additional deep sequencing of the unmutated template (requires samplesheet rows with type 'wildtype'); 'none' disables correction.", + "enum": ["false_doubles", "none", "wildtype"], + "default": "false_doubles" + }, + "false_doubles_method": { + "type": "string", + "description": "Estimator used by the false-doubles correction. 'mle' (default) is a per-variant maximum-likelihood error rate; 'eb' is an empirical-Bayes estimate that pools across variants and shrinks per substitution class (steadier for sparse variants). Only applies when error_correction = 'false_doubles'.", + "enum": ["mle", "eb"], + "default": "mle" + }, + "false_doubles_codon_window": { + "type": "integer", + "description": "Window (in codons, upstream and downstream) within which programmed multi-nucleotide codon variants are used to estimate the error rate of a single-nucleotide variant during false-doubles correction.", + "minimum": 1, + "default": 40 + }, + "pdb": { + "type": "string", + "format": "file-path", + "exists": true, + "mimetype": "chemical/x-pdb", + "description": "Optional wildtype 3D structure (PDB). When supplied together with `--fitness`, an interactive variant effect inspection tool (self-contained HTML) is built that projects fitness, counts and error-correction biases onto the structure.", + "help_text": "Use a crystal structure, an AlphaFold DB model, or any precomputed PDB whose sequence matches the wildtype ORF. Structure prediction (nf-core/proteinfold) is not wired in this release." + }, + "sliding_window_size": { + "type": "integer", + "description": "Sliding-window size (in codons) used to smooth the positional coverage and count profiles in the library QC plots.", + "default": 10 + }, + "aimed_cov": { + "type": "integer", + "description": "Targeted number of counts per amino acid variant. Used to draw the sequencing coverage that would be required (assuming an even spread) in the positional coverage plots and the run report.", + "default": 100 + }, + "mutagenesis_type": { + "type": "string", + "description": "Codons programmed at each position of the library. Choose from nnk, nns, nnh, nnn, nnk_nns, nnk_nns_nnh or custom. When using 'custom', also provide '--custom_codon_library'.", + "default": "nnk" + }, + "custom_codon_library": { + "type": "string", + "format": "file-path", + "description": "Path to a .csv file defining a custom codon library. Required when '--mutagenesis_type custom' is set. Provide either one global comma-separated list of codons without header (e.g. 'AAA,AAC,AAG'), or a position-wise list with a header line containing 'Position', followed by one row per codon position with the position and its allowed codons (e.g. '1,ACG,AAA,ACA' and '2,AAA,TTT,ACA' on separate lines).", + "default": "/NULL" + }, + "dimsum": { + "type": ["boolean", "string"], + "description": "Additionally estimate fitness with DiMSum (Faure et al., 2020). Requires '--fitness'." + }, + "mutscan": { + "type": ["boolean", "string"], + "description": "Additionally estimate variant enrichment with mutscan (Soneson et al., 2023), using edgeR and limma. Requires '--fitness'." + }, + "fitness": { + "type": ["boolean", "string"], + "description": "Estimate variant fitness from matched 'input' and 'output' libraries of each sample, using the default log-ratio estimator." + }, + "run_seqdepth": { + "type": "boolean", + "default": true, + "description": "Estimate sequencing-depth saturation of every library by closed-form hypergeometric rarefaction (on by default)." } } }, @@ -51,7 +138,7 @@ "properties": { "genome": { "type": "string", - "description": "Name of iGenomes reference.", + "description": "Name of iGenomes reference. Not used for typical deep mutational scanning runs; provide `--fasta` instead.", "fa_icon": "fas fa-book", "help_text": "If using a reference genome configured in the pipeline using iGenomes, use this parameter to give the ID for the reference. This is then used to build the full paths for all required reference genome files e.g. `--genome GRCh38`. \n\nSee the [nf-core website docs](https://nf-co.re/usage/reference_genomes) for more details." }, @@ -61,8 +148,8 @@ "exists": true, "mimetype": "text/plain", "pattern": "^\\S+\\.fn?a(sta)?(\\.gz)?$", - "description": "Path to FASTA genome file.", - "help_text": "This parameter is *mandatory* if `--genome` is not specified. If you don't have a BWA index available this will be generated for you automatically. Combine with `--save_reference` to save BWA index for future runs.", + "description": "Path to a FASTA file with the wildtype reference sequence of the mutagenised gene (exactly one sequence).", + "help_text": "The reference may include flanking sequence around the open reading frame (set with `--reading_frame`). A BWA index is built automatically and saved under `intermediate_files/bwa/`.", "fa_icon": "far fa-file-code" }, "igenomes_ignore": { @@ -74,7 +161,6 @@ }, "igenomes_base": { "type": "string", - "format": "directory-path", "description": "The base path to the igenomes reference files", "fa_icon": "fas fa-ban", "hidden": true, @@ -180,13 +266,6 @@ "fa_icon": "fas fa-palette", "hidden": true }, - "hook_url": { - "type": "string", - "description": "Incoming hook URL for messaging service", - "fa_icon": "fas fa-people-group", - "help_text": "Incoming hook URL for messaging service. Currently, MS Teams and Slack are supported.", - "hidden": true - }, "multiqc_config": { "type": "string", "format": "file-path", @@ -224,6 +303,14 @@ "fa_icon": "far calendar", "description": "Suffix to add to the trace report filename. Default is the date and time in the format yyyy-MM-dd_HH-mm-ss.", "hidden": true + }, + "help_full": { + "type": "boolean", + "description": "Display the full detailed help message." + }, + "show_hidden": { + "type": "boolean", + "description": "Display hidden parameters in the help message (only works when --help or --help_full are provided)." } } } diff --git a/nf-test.config b/nf-test.config new file mode 100644 index 0000000..c0c14da --- /dev/null +++ b/nf-test.config @@ -0,0 +1,39 @@ +config { + // location for all nf-test tests + testsDir = "." + + // nf-test directory including temporary files for each test + workDir = System.getenv("NFT_WORKDIR") ?: ".nf-test" + + // location of an optional nextflow.config file specific for executing tests + configFile = "tests/nextflow.config" + + // ignore tests coming from the nf-core/modules repo + ignore = [ + 'modules/nf-core/**/tests/*', + 'subworkflows/nf-core/**/tests/*', + ] + + // run all test with defined profile(s) from the main nextflow.config + profile = "test" + + // list of filenames or patterns that should be trigger a full test run + triggers = [ + '.github/actions/nf-test/action.yml', + '.github/workflows/nf-test.yml', + 'assets/schema_input.json', + 'bin/*', + 'conf/test.config', + 'nextflow.config', + 'nextflow_schema.json', + 'nf-test.config', + 'tests/.nftignore', + 'tests/nextflow.config', + ] + + // load the necessary plugins + plugins { + load "nft-utils@0.0.3" + load "nft-csv@0.1.0" + } +} diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index 766d934..d6bf3c2 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -1,6 +1,6 @@ { "@context": [ - "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/crate/1.2/context", { "GithubService": "https://w3id.org/ro/terms/test#GithubService", "JenkinsService": "https://w3id.org/ro/terms/test#JenkinsService", @@ -21,9 +21,9 @@ { "@id": "./", "@type": "Dataset", - "creativeWorkStatus": "InProgress", - "datePublished": "2025-01-22T16:52:07+00:00", - "description": "

\n \n \n \"nf-core/dmscore\"\n \n

\n\n[![GitHub Actions CI Status](https://github.com/nf-core/dmscore/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/dmscore/actions/workflows/ci.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/dmscore/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/dmscore/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/dmscore/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX)\n[![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com)\n\n[![Nextflow](https://img.shields.io/badge/nextflow%20DSL2-%E2%89%A524.04.2-23aa62.svg)](https://www.nextflow.io/)\n[![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/)\n[![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/)\n[![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/)\n[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/nf-core/dmscore)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23dmscore-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/dmscore)[![Follow on Twitter](http://img.shields.io/badge/twitter-%40nf__core-1DA1F2?labelColor=000000&logo=twitter)](https://twitter.com/nf_core)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core)\n\n## Introduction\n\n**nf-core/dmscore** is a bioinformatics pipeline that ...\n\n\n\n\n1. Read QC ([`FastQC`](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/))2. Present QC for raw reads ([`MultiQC`](http://multiqc.info/))\n\n## Usage\n\n> [!NOTE]\n> If you are new to Nextflow and nf-core, please refer to [this page](https://nf-co.re/docs/usage/installation) on how to set-up Nextflow. Make sure to [test your setup](https://nf-co.re/docs/usage/introduction#how-to-run-a-pipeline) with `-profile test` before running the workflow on actual data.\n\n\n\nNow, you can run the pipeline using:\n\n\n\n```bash\nnextflow run nf-core/dmscore \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \n```\n\n> [!WARNING]\n> Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; see [docs](https://nf-co.re/docs/usage/getting_started/configuration#custom-configuration-files).\n\nFor more details and further functionality, please refer to the [usage documentation](https://nf-co.re/dmscore/usage) and the [parameter documentation](https://nf-co.re/dmscore/parameters).\n\n## Pipeline output\n\nTo see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/dmscore/results) tab on the nf-core website pipeline page.\nFor more details about the output files and reports, please refer to the\n[output documentation](https://nf-co.re/dmscore/output).\n\n## Credits\n\nnf-core/dmscore was originally written by Benjamin Wehnert & Max Stammnitz.\n\nWe thank the following people for their extensive assistance in the development of this pipeline:\n\n\n\n## Contributions and Support\n\nIf you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md).\n\nFor further information or help, don't hesitate to get in touch on the [Slack `#dmscore` channel](https://nfcore.slack.com/channels/dmscore) (you can join with [this invite](https://nf-co.re/join/slack)).\n\n## Citations\n\n\n\n\n\n\nAn extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file.\n\nYou can cite the `nf-core` publication as follows:\n\n> **The nf-core framework for community-curated bioinformatics pipelines.**\n>\n> Philip Ewels, Alexander Peltzer, Sven Fillinger, Harshil Patel, Johannes Alneberg, Andreas Wilm, Maxime Ulysse Garcia, Paolo Di Tommaso & Sven Nahnsen.\n>\n> _Nat Biotechnol._ 2020 Feb 13. doi: [10.1038/s41587-020-0439-x](https://dx.doi.org/10.1038/s41587-020-0439-x).\n", + "creativeWorkStatus": "Stable", + "datePublished": "2026-10-07T15:32:41+00:00", + "description": "

\n \n \n \"nf-core/deepmutscan\"\n \n

\n\n[![GitHub Actions CI Status](https://github.com/nf-core/deepmutscan/actions/workflows/ci.yml/badge.svg)](https://github.com/nf-core/deepmutscan/actions/workflows/ci.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/deepmutscan/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/deepmutscan/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/deepmutscan/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX)\n[![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com)\n\n[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.10.4-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/)\n[![nf-core template version](https://img.shields.io/badge/nf--core_template-4.0.2-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/4.0.2)\n[![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/)\n[![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/)\n[![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/)\n[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/nf-core/deepmutscan)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23deepmutscan-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/deepmutscan)[![Follow on Twitter](http://img.shields.io/badge/twitter-%40nf__core-1DA1F2?labelColor=000000&logo=twitter)](https://twitter.com/nf_core)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core)\n\n## Introduction\n\n**nf-core/deepmutscan** is a workflow designed for the analysis of deep mutational scanning (DMS) data. DMS enables researchers to experimentally measure the fitness effects of thousands of gene variants simultaneously, helping to classify disease-causing mutants in human and other species populations, and to learn the fundamental rules of protein architecture, small-molecule binding, mRNA splicing, viral evolution and many other quantifiable phenotypes.\n\nWhile DNA synthesis and sequencing technologies have advanced substantially, long open reading frame (ORF) targets still present a major challenge for DMS studies. Shotgun DNA sequencing of randomly fragmented variant libraries can greatly speed up the inference of long ORF mutant fitness landscapes, as it avoids the need for library barcoding or multi-tile sequencing. We have designed `nf-core/deepmutscan` to unlock shotgun sequencing-based DMS studies on long ORFs, and to simplify and standardise the bioinformatics steps involved in processing such experiments \u2013 from read alignment to QC reporting, variant count error correction and fitness landscape inference. Amplicon (tile) sequencing data can be processed in the same way.\n\n![nf-core/deepmutscan workflow](docs/images/pipeline.png)\n\n## Major features\n\n- End-to-end processing of DMS libraries from shotgun (randomly fragmented) or amplicon short-read sequencing\n- Light-weight variant counter with base-quality and read-edge filters, replacing `GATK AnalyzeSaturationMutagenesis`\n- Intrinsic sequencing-error correction of single-nucleotide variant counts from read-linked false double mutants (maximum-likelihood or empirical-Bayes estimators), or from additional wildtype template sequencing\n- Library quality control: mutant count heatmaps, positional coverage and mutation-type biases, sequencing-depth rarefaction\n- Fitness estimation with a built-in log-ratio estimator, plus optional [DiMSum](https://github.com/lehner-lab/DiMSum) and [mutscan](https://github.com/fmicompbio/mutscan)\n- Support for degenerate codon libraries (NNK, NNS, NNH, NNN and combinations), e.g. from nicking mutagenesis, and for custom (position-specific) codon libraries, e.g. from Twist tiles\n- A single, self-contained HTML report per run, and an optional interactive 3D variant effect inspection tool when a wildtype structure is supplied\n- Containerisation via Docker, Singularity/Apptainer and Conda; scalability across HPC and cloud systems\n\nFor more details on the individual steps and on planned extensions, please read the [usage documentation](https://nf-co.re/deepmutscan/usage).\n\n## Pipeline summary\n\n1. Raw read QC ([`FastQC`](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/))\n2. Alignment of reads to the reference ORF ([`BWA-MEM`](https://github.com/lh3/bwa))\n3. Filtering of unmapped, secondary, low mapping-quality and indel-containing alignments ([`samtools view`](https://www.htslib.org/))\n4. Merging of overlapping read pairs into consensus reads for base error reduction ([`vsearch --fastq_mergepairs`](https://github.com/torognes/vsearch)), re-alignment, sorting and indexing ([`samtools`](https://www.htslib.org/))\n5. Variant counting (custom counter built on [`pysam`](https://github.com/pysam-developers/pysam) and [`polars`](https://pola.rs))\n6. Annotation and filtering of variant counts against the programmed mutagenesis library\n7. Single-nucleotide variant sequencing-error correction via false double mutants or wildtype sequencing\n8. DMS library quality control and visualisation\n9. _Optional:_ fitness estimation from matched input/output samples (default estimator, [`DiMSum`](https://github.com/lehner-lab/DiMSum), [`mutscan`](https://github.com/fmicompbio/mutscan)) and interactive 3D variant effect inspection tool ([`3Dmol.js`](https://3dmol.csb.pitt.edu/))\n10. An all-in-one `deepmutscan_report.html`\n\n## Usage\n\n> [!NOTE]\n> If you are new to Nextflow and nf-core, please refer to [this page](https://nf-co.re/docs/get_started/environment_setup/overview) on how to set-up Nextflow. Make sure to [test your setup](https://nf-co.re/docs/get_started/run-your-first-pipeline) with `-profile test` before running the workflow on actual data.\n\nFirst, prepare a samplesheet with your input data. Each row represents one sequencing library as a pair of FASTQ files (paired-end), annotated with the biological sample, its type in the selection experiment (`input`, `output` or `wildtype`) and the replicate number:\n\n```csv title=\"samplesheet.csv\"\nsample,type,replicate,file1,file2\nORF1,input,1,/reads/input1_R1.fastq.gz,/reads/input1_R2.fastq.gz\nORF1,input,2,/reads/input2_R1.fastq.gz,/reads/input2_R2.fastq.gz\nORF1,output,1,/reads/output1_R1.fastq.gz,/reads/output1_R2.fastq.gz\nORF1,output,2,/reads/output2_R1.fastq.gz,/reads/output2_R2.fastq.gz\n```\n\nSecondly, provide the gene or gene region of interest as a reference FASTA file via `--fasta`, and the nucleotide coordinates of the mutagenised open reading frame within it via `--reading_frame` (1-based, inclusive, e.g. `1-300` for the first 100 codons).\n\nNow, you can run the pipeline using:\n\n```bash title=\"example_run.sh\"\nnextflow run nf-core/deepmutscan \\\n -profile \\\n --input ./samplesheet.csv \\\n --fasta ./ref.fa \\\n --reading_frame 1-300 \\\n --outdir ./results\n```\n\nAdd `--fitness` to estimate variant fitness from the input and output samples, and see the [usage documentation](https://nf-co.re/deepmutscan/usage) and the [parameter documentation](https://nf-co.re/deepmutscan/parameters) for all other options.\n\n> [!WARNING]\n> Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; see [docs](https://nf-co.re/docs/usage/getting_started/configuration#custom-configuration-files).\n\n## Pipeline output\n\nTo see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/deepmutscan/results) tab on the nf-core website pipeline page.\nFor more details about the output files and reports, please refer to the [output documentation](https://nf-co.re/deepmutscan/output).\n\n## Credits\n\nnf-core/deepmutscan was originally written by [Benjamin Wehnert](https://github.com/BenjaminWehnert1008) and [Maximilian Stammnitz](https://github.com/MaximilianStammnitz) at the [Centre for Genomic Regulation (CRG), Barcelona](https://www.crg.eu/), with the generous support of an EMBO Long-term Postdoctoral Fellowship, the Erasmus+ programme and a Marie Sk\u0142odowska-Curie grant by the European Union.\n\nWe thank the following people for their extensive assistance in the development of this pipeline:\n\n- [Fei Sang](https://github.com/fei-hgi) (Wellcome Sanger Institute) \u2013 original variant counting implementation\n- [J\u00falia Mir-Pedrol](https://github.com/mirpedrol) (CRG) \u2013 nf-core development guidance and code review\n- [Matthias H\u00f6rtenhuber](https://github.com/mashehu) (SciLifeLab) \u2013 nf-core development guidance and code review\n\n## Contributions and Support\n\nIf you would like to contribute to this pipeline, please see the [contributing guidelines](docs/CONTRIBUTING.md).\n\nFor further information or help, don't hesitate to get in touch on the [Slack `#deepmutscan` channel](https://nfcore.slack.com/channels/deepmutscan) (you can join with [this invite](https://nf-co.re/join/slack)). Bug reports and feature requests are welcome as GitHub [issues](https://github.com/nf-core/deepmutscan/issues).\n\nFor scientific discussions around the use of this pipeline (e.g. on experimental design or sequencing data requirements), please feel free to get in touch with us directly:\n\n- Benjamin Wehnert \u2014 wehnertbenjamin@gmail.com\n- Maximilian Stammnitz \u2014 maximilian.stammnitz@crg.eu\n\n## Citations\n\nIf you use `nf-core/deepmutscan` for your analysis, please cite it as follows:\n\n> Wehnert B, et al. _bioRxiv_ preprint (in preparation).\n\n\n\n\nAn extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file.\n\nYou can cite the `nf-core` publication as follows:\n\n> **The nf-core framework for community-curated bioinformatics pipelines.**\n>\n> Philip Ewels, Alexander Peltzer, Sven Fillinger, Harshil Patel, Johannes Alneberg, Andreas Wilm, Maxime Ulysse Garcia, Paolo Di Tommaso & Sven Nahnsen.\n>\n> _Nat Biotechnol._ 2020 Feb 13. doi: [10.1038/s41587-020-0439-x](https://dx.doi.org/10.1038/s41587-020-0439-x).\n", "hasPart": [ { "@id": "main.nf" @@ -43,6 +43,9 @@ { "@id": "modules/" }, + { + "@id": "modules/local/" + }, { "@id": "modules/nf-core/" }, @@ -92,17 +95,17 @@ "@id": ".prettierignore" } ], - "isBasedOn": "https://github.com/nf-core/dmscore", + "isBasedOn": "https://github.com/nf-core/deepmutscan", "license": "MIT", "mainEntity": { "@id": "main.nf" }, "mentions": [ { - "@id": "#f9815654-37bd-4781-a133-ab36324210f5" + "@id": "#1df6d957-4c1d-4a79-8427-43b7ac48f15d" } ], - "name": "nf-core/dmscore" + "name": "nf-core/deepmutscan" }, { "@id": "ro-crate-metadata.json", @@ -112,7 +115,7 @@ }, "conformsTo": [ { - "@id": "https://w3id.org/ro/crate/1.1" + "@id": "https://w3id.org/ro/crate/1.2" }, { "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" @@ -126,18 +129,49 @@ "SoftwareSourceCode", "ComputationalWorkflow" ], + "author": [ + { + "@id": "https://orcid.org/0009-0001-8012-6003" + }, + { + "@id": "https://orcid.org/0000-0002-1704-9199" + } + ], + "contributor": [ + { + "@id": "https://orcid.org/0000-0002-8746-5868" + }, + { + "@id": "https://orcid.org/0000-0001-6104-9260" + }, + { + "@id": "https://orcid.org/0000-0002-5599-5565" + } + ], "dateCreated": "", - "dateModified": "2025-01-22T17:52:07Z", + "dateModified": "2026-10-07T17:32:41Z", "dct:conformsTo": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE/", "keywords": [ "nf-core", - "nextflow" + "nextflow", + "community-workflow", + "deep-mutational-scanning", + "genotype-phenotype", + "shotgun-sequencing" ], "license": [ "MIT" ], + "maintainer": [ + { + "@id": "https://orcid.org/0009-0001-8012-6003" + }, + { + "@id": "https://orcid.org/0000-0002-1704-9199" + } + ], "name": [ - "nf-core/dmscore" + "nf-core/deepmutscan" ], "programmingLanguage": { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#nextflow" @@ -146,11 +180,11 @@ "@id": "https://nf-co.re/" }, "url": [ - "https://github.com/nf-core/dmscore", - "https://nf-co.re/dmscore/dev/" + "https://github.com/nf-core/deepmutscan", + "https://nf-co.re/deepmutscan/1.0.0/" ], "version": [ - "1.0.0dev" + "1.0.0" ] }, { @@ -163,26 +197,26 @@ "url": { "@id": "https://www.nextflow.io/" }, - "version": "!>=24.04.2" + "version": "!>=25.10.4" }, { - "@id": "#f9815654-37bd-4781-a133-ab36324210f5", + "@id": "#1df6d957-4c1d-4a79-8427-43b7ac48f15d", "@type": "TestSuite", "instance": [ { - "@id": "#34dab5c1-0a1b-41b0-80d1-74e2f2d38434" + "@id": "#f18407fe-cfe9-4efe-b4a7-0aac710d7349" } ], "mainEntity": { "@id": "main.nf" }, - "name": "Test suite for nf-core/dmscore" + "name": "Test suite for nf-core/deepmutscan" }, { - "@id": "#34dab5c1-0a1b-41b0-80d1-74e2f2d38434", + "@id": "#f18407fe-cfe9-4efe-b4a7-0aac710d7349", "@type": "TestInstance", - "name": "GitHub Actions workflow for testing nf-core/dmscore", - "resource": "repos/nf-core/dmscore/actions/workflows/ci.yml", + "name": "GitHub Actions workflow for testing nf-core/deepmutscan", + "resource": "repos/nf-core/deepmutscan/actions/workflows/nf-test.yml", "runsOn": { "@id": "https://w3id.org/ro/terms/test#GithubService" }, @@ -221,6 +255,11 @@ "@type": "Dataset", "description": "Modules used by the pipeline" }, + { + "@id": "modules/local/", + "@type": "Dataset", + "description": "Pipeline-specific modules" + }, { "@id": "modules/nf-core/", "@type": "Dataset", @@ -306,6 +345,46 @@ "@type": "Organization", "name": "nf-core", "url": "https://nf-co.re/" + }, + { + "@id": "https://orcid.org/0009-0001-8012-6003", + "@type": "Person", + "affiliation": "Centre for Genomic Regulation (CRG), Barcelona, Spain", + "email": "wehnertbenjamin@gmail.com", + "name": "Benjamin Wehnert", + "url": "https://github.com/BenjaminWehnert1008" + }, + { + "@id": "https://orcid.org/0000-0002-1704-9199", + "@type": "Person", + "affiliation": "Centre for Genomic Regulation (CRG), Barcelona, Spain", + "email": "maximilian.stammnitz@crg.eu", + "name": "Maximilian R. Stammnitz", + "url": "https://github.com/MaximilianStammnitz" + }, + { + "@id": "https://orcid.org/0000-0002-8746-5868", + "@type": "Person", + "affiliation": "Wellcome Sanger Institute, Hinxton, United Kingdom", + "email": "fs18@sanger.ac.uk", + "name": "Fei Sang", + "url": "https://github.com/fei-hgi" + }, + { + "@id": "https://orcid.org/0000-0001-6104-9260", + "@type": "Person", + "affiliation": "Centre for Genomic Regulation (CRG), Barcelona, Spain", + "email": "julia.mir@crg.eu", + "name": "J\u00falia Mir-Pedrol", + "url": "https://github.com/mirpedrol" + }, + { + "@id": "https://orcid.org/0000-0002-5599-5565", + "@type": "Person", + "affiliation": "SciLifeLab, Uppsala University, Uppsala, Sweden", + "email": "matthias.hortenhuber@scilifelab.uu.se", + "name": "Matthias H\u00f6rtenhuber", + "url": "https://github.com/mashehu" } ] -} \ No newline at end of file +} diff --git a/subworkflows/local/calculate_fitness/main.nf b/subworkflows/local/calculate_fitness/main.nf new file mode 100644 index 0000000..6f7b4c4 --- /dev/null +++ b/subworkflows/local/calculate_fitness/main.nf @@ -0,0 +1,151 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + IMPORT MODULES +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ +include { MERGE_COUNTS } from '../../../modules/local/fitness/merge_counts/main' +include { EXPDESIGN_FITNESS } from '../../../modules/local/fitness/fitness_experimental_design/main' +include { FIND_SYNONYMOUS_MUTATION } from '../../../modules/local/fitness/find_synonymous_mutation/main' +include { FITNESS_CALCULATION } from '../../../modules/local/fitness/fitness_calculation/main' +include { FITNESS_QC } from '../../../modules/local/fitness/fitness_QC/main' +include { FITNESS_HEATMAP } from '../../../modules/local/fitness/fitness_heatmap/main' +include { RUN_DIMSUM } from '../../../modules/local/fitness/run_dimsum/main' +include { RUN_MUTSCAN } from '../../../modules/local/fitness/run_mutscan/main' + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SUBWORKFLOW DEFINITION +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +workflow CALCULATE_FITNESS { + + take: + ch_fitness_input // channel: output from COUNTS_TO_FITNESS (grouped by sample/replicate logic) + ch_samplesheet_csv // channel: path to samplesheet csv + ch_fasta // channel: tuple([meta], path(fasta)) + val_reading_frame // value: reading frame param + ch_aa_seq // channel: output from DMSANALYSIS_AASEQ (for heatmap) + + main: + ch_versions = Channel.empty() + + // 1. Group per biological sample and merge counts + ch_fitness_input + .map { meta, tsv -> + def s = meta.sample as String + def id = meta.id as String + def base = s ? (s.replaceFirst(/_(input|output|quality)\d+$/, '')) + : (id?.tokenize('_')?.first()) + tuple(base as String, tuple(meta, tsv)) + } + .groupTuple() + .map { base, pairs -> + // sort by replicate so the staged file order is deterministic (cacheable) + def inputs = pairs.findAll { it[0].type == 'input' }.sort { it[0].replicate }.collect { it[1] } + def outputs = pairs.findAll { it[0].type == 'output' }.sort { it[0].replicate }.collect { it[1] } + tuple([sample: base], inputs, outputs) + } + .filter { smeta, ins, outs -> ins && outs } + .set { ch_fitness_bundled } + + // 2. Merge Counts + MERGE_COUNTS( ch_fitness_bundled ) + ch_versions = ch_versions.mix(MERGE_COUNTS.out.versions) + + // 3. Experimental Design + EXPDESIGN_FITNESS( ch_samplesheet_csv ) + ch_versions = ch_versions.mix(EXPDESIGN_FITNESS.out.versions) + + // 4. Find Synonymous Mutations + // Prepare inputs (broadcast logic) + def ch_fasta_path = ch_fasta.map { it[1] } // strip meta + + FIND_SYNONYMOUS_MUTATION( + MERGE_COUNTS.out.merged_counts, + ch_fasta_path.combine(MERGE_COUNTS.out.merged_counts).map { it[0] }, + val_reading_frame.combine(MERGE_COUNTS.out.merged_counts).map { it[0] } + ) + ch_versions = ch_versions.mix(FIND_SYNONYMOUS_MUTATION.out.versions) + + + // 5. Align Channels for Fitness Calculation + + // Key counts and WT by biological sample name + def ch_counts_keyed_d = MERGE_COUNTS.out.merged_counts + .map { smp, counts -> tuple(smp.sample as String, smp, counts) } + + def ch_wt_keyed_d = FIND_SYNONYMOUS_MUTATION.out.synonymous_wt + .map { smp, wt -> tuple(smp.sample as String, wt) } + + // Join by key + def ch_counts_wt_d = ch_counts_keyed_d.join(ch_wt_keyed_d) + .map { key, smp, counts, wt -> tuple(smp, counts, wt) } + + // Broadcast experimental design + def ch_exp_for_each_d = EXPDESIGN_FITNESS.out.experimental_design + .combine(ch_counts_wt_d) + .map { it[0] } + + // Final aligned channels + def ch_run_counts_d = ch_counts_wt_d.map { smp, counts, wt -> tuple(smp, counts) } + def ch_run_wt_d = ch_counts_wt_d.map { smp, counts, wt -> wt } + def ch_run_exp_d = ch_exp_for_each_d + + // 6. Run Fitness Calculation & QC + FITNESS_CALCULATION( + ch_run_counts_d, + ch_run_exp_d, + ch_run_wt_d + ) + ch_versions = ch_versions.mix(FITNESS_CALCULATION.out.versions) + + FITNESS_QC( FITNESS_CALCULATION.out.fitness_estimation ) + ch_versions = ch_versions.mix(FITNESS_QC.out.versions) + + FITNESS_HEATMAP( + FITNESS_CALCULATION.out.fitness_estimation, + ch_aa_seq + ) + ch_versions = ch_versions.mix(FITNESS_HEATMAP.out.versions) + + // 7. Run DiMSum (optional based on params inside subworkflow or handled by control logic) + // Note: Logic checking for 'params.dimsum' needs to be handled. + // Since subworkflows inherit params, we can check params.dimsum here. + + // DiMSum and mutscan are optional, so their artefact channels start empty and are only filled + // when the tool actually ran - the summary report then simply omits the section. + def ch_dimsum_dir = Channel.empty() + def ch_mutscan_plots = Channel.empty() + + if (params.dimsum) { + RUN_DIMSUM( + ch_run_counts_d, + ch_run_wt_d, + ch_run_exp_d + ) + ch_versions = ch_versions.mix(RUN_DIMSUM.out.versions) + ch_dimsum_dir = RUN_DIMSUM.out.results_dir + } + + // 8. Run Mutscan + if (params.mutscan) { + RUN_MUTSCAN( + ch_run_counts_d, + ch_run_wt_d, + ch_run_exp_d + ) + ch_versions = ch_versions.mix(RUN_MUTSCAN.out.versions) + ch_mutscan_plots = RUN_MUTSCAN.out.qc_plots + } + + emit: + fitness_estimation = FITNESS_CALCULATION.out.fitness_estimation + // artefacts the all-in-one report embeds + fitness_plots = FITNESS_QC.out.counts_corr_pdf + .mix(FITNESS_QC.out.fitness_corr_pdf) + .mix(FITNESS_HEATMAP.out.fitness_heatmap) + mutscan_plots = ch_mutscan_plots + dimsum_dir = ch_dimsum_dir + versions = ch_versions +} diff --git a/subworkflows/local/calculate_fitness/meta.yml b/subworkflows/local/calculate_fitness/meta.yml new file mode 100644 index 0000000..872ba1e --- /dev/null +++ b/subworkflows/local/calculate_fitness/meta.yml @@ -0,0 +1,42 @@ +name: "calculate_fitness" +description: Consolidates variant counts and calculates fitness scores. +keywords: + - "deep mutational scanning" + - "fitness estimation" + - "dms" + - "enrichment scores" +components: + - "merge/counts" + - "fitness/calculation" + - "fitness/heatmap" + - "fitness/qc" + - "run/dimsum" + - "run/mutscan" + - "expdesign/fitness" + - "find/synonymous/mutation" +input: + - ch_fitness_input: + type: channel + description: Variant count channels + - ch_samplesheet_csv: + type: channel + description: Pipeline samplesheet + - ch_fasta: + type: channel + description: Reference sequence channel + - val_reading_frame: + type: value + description: Reading frame parameter + - ch_aa_seq: + type: channel + description: Amino acid sequence channel +output: + - fitness_estimation: + type: channel + description: Output fitness metrics + - versions: + type: channel + description: Software versions +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/subworkflows/local/utils_nfcore_dmscore_pipeline/main.nf b/subworkflows/local/utils_nfcore_deepmutscan_pipeline/main.nf similarity index 73% rename from subworkflows/local/utils_nfcore_dmscore_pipeline/main.nf rename to subworkflows/local/utils_nfcore_deepmutscan_pipeline/main.nf index 14eba3c..50e4edb 100644 --- a/subworkflows/local/utils_nfcore_dmscore_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_deepmutscan_pipeline/main.nf @@ -1,5 +1,5 @@ // -// Subworkflow with functionality specific to the nf-core/dmscore pipeline +// Subworkflow with functionality specific to the nf-core/deepmutscan pipeline // /* @@ -10,10 +10,8 @@ include { UTILS_NFSCHEMA_PLUGIN } from '../../nf-core/utils_nfschema_plugin' include { paramsSummaryMap } from 'plugin/nf-schema' -include { samplesheetToList } from 'plugin/nf-schema' include { completionEmail } from '../../nf-core/utils_nfcore_pipeline' include { completionSummary } from '../../nf-core/utils_nfcore_pipeline' -include { imNotification } from '../../nf-core/utils_nfcore_pipeline' include { UTILS_NFCORE_PIPELINE } from '../../nf-core/utils_nfcore_pipeline' include { UTILS_NEXTFLOW_PIPELINE } from '../../nf-core/utils_nextflow_pipeline' @@ -50,10 +48,43 @@ workflow PIPELINE_INITIALISATION { // // Validate parameters and generate parameter summary to stdout // + + def before_text = "" + def after_text = "" + before_text = """ +-\033[2m----------------------------------------------------\033[0m- + \033[0;32m,--.\033[0;30m/\033[0;32m,-.\033[0m +\033[0;34m ___ __ __ __ ___ \033[0;32m/,-._.--~\'\033[0m +\033[0;34m |\\ | |__ __ / ` / \\ |__) |__ \033[0;33m} {\033[0m +\033[0;34m | \\| | \\__, \\__/ | \\ |___ \033[0;32m\\`-._,-`-,\033[0m + \033[0;32m`._,._,\'\033[0m +\033[0;35m nf-core/deepmutscan ${workflow.manifest.version}\033[0m +-\033[2m----------------------------------------------------\033[0m- +""" + after_text = """${workflow.manifest.doi ? "\n* The pipeline\n" : ""}${workflow.manifest.doi.tokenize(",").collect { doi -> " https://doi.org/${doi.trim().replace('https://doi.org/','')}"}.join("\n")}${workflow.manifest.doi ? "\n" : ""} +* The nf-core framework + https://doi.org/10.1038/s41587-020-0439-x + +* Software dependencies + https://github.com/nf-core/deepmutscan/blob/master/CITATIONS.md +""" + if (monochrome_logs) { + before_text = before_text.replaceAll(/\033\[[0-9;]*m/, '') + } + + command = "nextflow run ${workflow.manifest.name} -profile --input samplesheet.csv --outdir " + UTILS_NFSCHEMA_PLUGIN ( workflow, validate_params, - null + params.help, + params.help_full, + params.show_hidden, + null, + before_text, + after_text, + command, + "${projectDir}/nextflow_schema.json" ) // @@ -73,25 +104,39 @@ workflow PIPELINE_INITIALISATION { // Channel - .fromList(samplesheetToList(params.input, "${projectDir}/assets/schema_input.json")) - .map { - meta, fastq_1, fastq_2 -> - if (!fastq_2) { - return [ meta.id, meta + [ single_end:true ], [ fastq_1 ] ] - } else { - return [ meta.id, meta + [ single_end:false ], [ fastq_1, fastq_2 ] ] - } - } - .groupTuple() - .map { samplesheet -> - validateInputSamplesheet(samplesheet) + .fromPath(params.input) + .splitCsv(header: true) + .filter { row -> + // Skip rows where file1 or file2 are BAM files + !(row.file1.endsWith('.bam') || (row.file2 && row.file2.endsWith('.bam'))) + } + .map { row -> + // Determine suffix based on the presence of file2 + def suffix = row.file2 ? "_pe" : "_se" + + // Construct metadata object with updated ID + def meta = [ + id : "${row.sample}_${row.type}_${row.replicate}${suffix}", // Base ID with suffix + sample : row.sample, + type : row.type, + replicate : row.replicate as int + ] + + // Generate file paths based on the presence of file1 and file2 + def reads = [] + if (row.file1) { + reads << row.file1 // Add file1 path } - .map { - meta, fastqs -> - return [ meta, fastqs.flatten() ] + if (row.file2) { + reads << row.file2 // Add file2 path } - .set { ch_samplesheet } + // Return metadata and file paths as a tuple + return [meta, reads] + } + .set { ch_samplesheet } + + // Emit the samplesheet channel and an empty version channel for use in the workflow emit: samplesheet = ch_samplesheet versions = ch_versions @@ -111,7 +156,6 @@ workflow PIPELINE_COMPLETION { plaintext_email // boolean: Send plain-text email instead of HTML outdir // path: Path to output directory where results will be published monochrome_logs // boolean: Disable ANSI colour codes in log output - hook_url // string: hook URL for notifications multiqc_report // string: Path to MultiQC report main: @@ -135,13 +179,11 @@ workflow PIPELINE_COMPLETION { } completionSummary(monochrome_logs) - if (hook_url) { - imNotification(summary_params, hook_url) - } + } workflow.onError { - log.error "Pipeline failed. Please refer to troubleshooting docs: https://nf-co.re/docs/usage/troubleshooting" + log.error "Pipeline failed. Please refer to troubleshooting docs for common issues: https://nf-co.re/docs/running/troubleshooting" } } @@ -200,7 +242,6 @@ def genomeExistsError() { // Generate methods description for MultiQC // def toolCitationText() { - // TODO nf-core: Optionally add in-text citation tools to this list. // Can use ternary operators to dynamically construct based conditions, e.g. params["run_xyz"] ? "Tool (Foo et al. 2023)" : "", // Uncomment function in methodsDescriptionText to render in MultiQC report def citation_text = [ @@ -214,7 +255,6 @@ def toolCitationText() { } def toolBibliographyText() { - // TODO nf-core: Optionally add bibliographic entries to this list. // Can use ternary operators to dynamically construct based conditions, e.g. params["run_xyz"] ? "
  • Author (2023) Pub name, Journal, DOI
  • " : "", // Uncomment function in methodsDescriptionText to render in MultiQC report def reference_text = [ @@ -249,9 +289,8 @@ def methodsDescriptionText(mqc_methods_yaml) { meta["tool_citations"] = "" meta["tool_bibliography"] = "" - // TODO nf-core: Only uncomment below if logic in toolCitationText/toolBibliographyText has been filled! - // meta["tool_citations"] = toolCitationText().replaceAll(", \\.", ".").replaceAll("\\. \\.", ".").replaceAll(", \\.", ".") - // meta["tool_bibliography"] = toolBibliographyText() + meta["tool_citations"] = toolCitationText().replaceAll(", \\.", ".").replaceAll("\\. \\.", ".").replaceAll(", \\.", ".") + meta["tool_bibliography"] = toolBibliographyText() def methods_text = mqc_methods_yaml.text @@ -261,4 +300,3 @@ def methodsDescriptionText(mqc_methods_yaml) { return description_html.toString() } - diff --git a/subworkflows/local/utils_nfcore_deepmutscan_pipeline/meta.yml b/subworkflows/local/utils_nfcore_deepmutscan_pipeline/meta.yml new file mode 100644 index 0000000..2f5669a --- /dev/null +++ b/subworkflows/local/utils_nfcore_deepmutscan_pipeline/meta.yml @@ -0,0 +1,58 @@ +name: "PIPELINE_INITIALISATION" +description: Local utility functions, pipeline initialization, completion workflows, and parameter validation wrappers for the deepmutscan pipeline. +keywords: + - "utility" + - "validation" + - "manifest" + - "initialisation" + - "completion" +components: + - "utils_nfschema_plugin" + - "utils_nfcore_pipeline" + - "utils_nextflow_pipeline" + - "completionemail" + - "completionsummary" + +input: + - version: + type: boolean + description: Display version and exit + - validate_params: + type: boolean + description: Boolean whether to validate parameters against the schema at runtime + - monochrome_logs: + type: boolean + description: Do not use coloured log outputs + - nextflow_cli_args: + type: array + description: List of positional nextflow CLI args + - outdir: + type: string + description: The output directory where the results will be saved + - input: + type: string + description: Path to input samplesheet + - email: + type: string + description: Email address for completion summary + - email_on_fail: + type: string + description: Email address sent on pipeline failure + - plaintext_email: + type: boolean + description: Send plain-text email instead of HTML + - multiqc_report: + type: string + description: Path to MultiQC report + +output: + - samplesheet: + type: channel + description: Processed samplesheet channel containing metadata objects and file paths + - versions: + type: channel + description: Empty version channel for use in the workflow + +authors: + - "@BenjaminWehnert1008" + - "@MaximilianStammnitz" diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf index d6e593e..37939ac 100644 --- a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf @@ -73,11 +73,23 @@ def getWorkflowVersion() { def dumpParametersToJSON(outdir) { def timestamp = new java.util.Date().format('yyyy-MM-dd_HH-mm-ss') def filename = "params_${timestamp}.json" - def temp_pf = new File(workflow.launchDir.toString(), ".${filename}") - def jsonStr = groovy.json.JsonOutput.toJson(params) + def temp_pf = workflow.launchDir.resolve(".${filename}") + def jsonGenerator = new groovy.json.JsonGenerator.Options() + .excludeNulls() + .addConverter(Path) { Path path -> path.toUriString() } + .addConverter(Duration) { Duration duration -> duration.toMillis() } + .addConverter(MemoryUnit) { MemoryUnit memory -> memory.toBytes() } + .addConverter(nextflow.script.types.VersionNumber) { nextflow.script.types.VersionNumber version -> version.toString() } + .build() + def jsonStr = jsonGenerator.toJson(params) temp_pf.text = groovy.json.JsonOutput.prettyPrint(jsonStr) - - nextflow.extension.FilesEx.copyTo(temp_pf.toPath(), "${outdir}/pipeline_info/params_${timestamp}.json") + if (outdir instanceof Path) { + temp_pf.copyTo(outdir.resolve("pipeline_info/${filename}")) + } else if (outdir instanceof String) { + temp_pf.copyTo("${outdir}/pipeline_info/params_${timestamp}.json") + } else { + log.warn("Could not determine type of outdir, parameters JSON file will not be copied to output directory!") + } temp_pf.delete() } diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml b/subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml deleted file mode 100644 index f847611..0000000 --- a/subworkflows/nf-core/utils_nextflow_pipeline/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -subworkflows/utils_nextflow_pipeline: - - subworkflows/nf-core/utils_nextflow_pipeline/** diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf index bfd2587..afca543 100644 --- a/subworkflows/nf-core/utils_nfcore_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nfcore_pipeline/main.nf @@ -17,7 +17,7 @@ workflow UTILS_NFCORE_PIPELINE { checkProfileProvided(nextflow_cli_args) emit: - valid_config + valid_config = valid_config } /* @@ -98,7 +98,7 @@ def workflowVersionToYAML() { // Get channel of software versions used in pipeline in YAML format // def softwareVersionsToYAML(ch_versions) { - return ch_versions.unique().map { version -> processVersionsFromYAML(version) }.unique().mix(Channel.of(workflowVersionToYAML())) + return ch_versions.unique().map { version -> processVersionsFromYAML(version) }.unique().mix(channel.of(workflowVersionToYAML())) } // @@ -353,67 +353,3 @@ def completionSummary(monochrome_logs=true) { log.info("-${colors.purple}[${workflow.manifest.name}]${colors.red} Pipeline completed with errors${colors.reset}-") } } - -// -// Construct and send a notification to a web server as JSON e.g. Microsoft Teams and Slack -// -def imNotification(summary_params, hook_url) { - def summary = [:] - summary_params - .keySet() - .sort() - .each { group -> - summary << summary_params[group] - } - - def misc_fields = [:] - misc_fields['start'] = workflow.start - misc_fields['complete'] = workflow.complete - misc_fields['scriptfile'] = workflow.scriptFile - misc_fields['scriptid'] = workflow.scriptId - if (workflow.repository) { - misc_fields['repository'] = workflow.repository - } - if (workflow.commitId) { - misc_fields['commitid'] = workflow.commitId - } - if (workflow.revision) { - misc_fields['revision'] = workflow.revision - } - misc_fields['nxf_version'] = workflow.nextflow.version - misc_fields['nxf_build'] = workflow.nextflow.build - misc_fields['nxf_timestamp'] = workflow.nextflow.timestamp - - def msg_fields = [:] - msg_fields['version'] = getWorkflowVersion() - msg_fields['runName'] = workflow.runName - msg_fields['success'] = workflow.success - msg_fields['dateComplete'] = workflow.complete - msg_fields['duration'] = workflow.duration - msg_fields['exitStatus'] = workflow.exitStatus - msg_fields['errorMessage'] = (workflow.errorMessage ?: 'None') - msg_fields['errorReport'] = (workflow.errorReport ?: 'None') - msg_fields['commandLine'] = workflow.commandLine.replaceFirst(/ +--hook_url +[^ ]+/, "") - msg_fields['projectDir'] = workflow.projectDir - msg_fields['summary'] = summary << misc_fields - - // Render the JSON template - def engine = new groovy.text.GStringTemplateEngine() - // Different JSON depending on the service provider - // Defaults to "Adaptive Cards" (https://adaptivecards.io), except Slack which has its own format - def json_path = hook_url.contains("hooks.slack.com") ? "slackreport.json" : "adaptivecard.json" - def hf = new File("${workflow.projectDir}/assets/${json_path}") - def json_template = engine.createTemplate(hf).make(msg_fields) - def json_message = json_template.toString() - - // POST - def post = new URL(hook_url).openConnection() - post.setRequestMethod("POST") - post.setDoOutput(true) - post.setRequestProperty("Content-Type", "application/json") - post.getOutputStream().write(json_message.getBytes("UTF-8")) - def postRC = post.getResponseCode() - if (!postRC.equals(200)) { - log.warn(post.getErrorStream().getText()) - } -} diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.nf.test similarity index 100% rename from subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test rename to subworkflows/nf-core/utils_nfcore_pipeline/tests/main.nf.test diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test.snap b/subworkflows/nf-core/utils_nfcore_pipeline/tests/main.nf.test.snap similarity index 100% rename from subworkflows/nf-core/utils_nfcore_pipeline/tests/main.workflow.nf.test.snap rename to subworkflows/nf-core/utils_nfcore_pipeline/tests/main.nf.test.snap diff --git a/subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml b/subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml deleted file mode 100644 index ac8523c..0000000 --- a/subworkflows/nf-core/utils_nfcore_pipeline/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -subworkflows/utils_nfcore_pipeline: - - subworkflows/nf-core/utils_nfcore_pipeline/** diff --git a/subworkflows/nf-core/utils_nfschema_plugin/main.nf b/subworkflows/nf-core/utils_nfschema_plugin/main.nf index 4994303..9ff0681 100644 --- a/subworkflows/nf-core/utils_nfschema_plugin/main.nf +++ b/subworkflows/nf-core/utils_nfschema_plugin/main.nf @@ -4,6 +4,7 @@ include { paramsSummaryLog } from 'plugin/nf-schema' include { validateParameters } from 'plugin/nf-schema' +include { paramsHelp } from 'plugin/nf-schema' workflow UTILS_NFSCHEMA_PLUGIN { @@ -15,32 +16,62 @@ workflow UTILS_NFSCHEMA_PLUGIN { // when this input is empty it will automatically use the configured schema or // "${projectDir}/nextflow_schema.json" as default. This input should not be empty // for meta pipelines + help // boolean: show help message + help_full // boolean: show full help message + show_hidden // boolean: show hidden parameters in help message + before_text // string: text to show before the help message and parameters summary + after_text // string: text to show after the help message and parameters summary + command // string: an example command of the pipeline + cli_typecast // boolean: whether to perform typecasting of CLI parameters. Set this to `null` to use the default behaviour main: + if(help || help_full) { + help_options = [ + beforeText: before_text, + afterText: after_text, + command: command, + showHidden: show_hidden, + fullHelp: help_full, + ] + if(parameters_schema) { + help_options << [parameters_schema: parameters_schema] + } + log.info paramsHelp( + help_options, + (help instanceof String && help != "true") ? help : "", + ) + exit 0 + } + // // Print parameter summary to stdout. This will display the parameters // that differ from the default given in the JSON schema // + + summary_options = [:] if(parameters_schema) { - log.info paramsSummaryLog(input_workflow, parameters_schema:parameters_schema) - } else { - log.info paramsSummaryLog(input_workflow) + summary_options << [parameters_schema: parameters_schema] } + log.info before_text + log.info paramsSummaryLog(summary_options, input_workflow) + log.info after_text // // Validate the parameters using nextflow_schema.json or the schema // given via the validation.parametersSchema configuration option // if(validate_params) { + validateOptions = [:] if(parameters_schema) { - validateParameters(parameters_schema:parameters_schema) - } else { - validateParameters() + validateOptions << [parameters_schema: parameters_schema] + } + if(cli_typecast != null) { + validateOptions << [cast_cli_params: cli_typecast] } + validateParameters(validateOptions) } emit: dummy_emit = true } - diff --git a/subworkflows/nf-core/utils_nfschema_plugin/meta.yml b/subworkflows/nf-core/utils_nfschema_plugin/meta.yml index f7d9f02..1d8c75a 100644 --- a/subworkflows/nf-core/utils_nfschema_plugin/meta.yml +++ b/subworkflows/nf-core/utils_nfschema_plugin/meta.yml @@ -25,6 +25,30 @@ input: option. When this input is empty it will automatically use the configured schema or "${projectDir}/nextflow_schema.json" as default. The schema should not be given in this way for meta pipelines. + - help: + type: boolean, string + description: | + Show the help message and exit. When a parameter name is given, show the help message for that parameter instead of the general help message. + - help_full: + type: boolean + description: Show the full help message and exit. + - show_hidden: + type: boolean + description: Show hidden parameters in the help message. + - before_text: + type: string + description: Text to show before the parameters summary and help message. + - after_text: + type: string + description: Text to show after the parameters summary and help message. + - command: + type: string + description: An example command to run the pipeline, to show in the help message and the summary. + - cli_typecast: + type: boolean + description: | + Whether to apply typecasting to the parameters given via the CLI before validation. + Set this to `null` to use the default behavior. output: - dummy_emit: type: boolean diff --git a/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test b/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test index 8fb3016..1fd1eac 100644 --- a/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test +++ b/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test @@ -25,6 +25,13 @@ nextflow_workflow { input[0] = workflow input[1] = validate_params input[2] = "" + input[3] = false + input[4] = false + input[5] = false + input[6] = "" + input[7] = "" + input[8] = "" + input[9] = null """ } } @@ -51,6 +58,13 @@ nextflow_workflow { input[0] = workflow input[1] = validate_params input[2] = "" + input[3] = false + input[4] = false + input[5] = false + input[6] = "" + input[7] = "" + input[8] = "" + input[9] = null """ } } @@ -77,6 +91,13 @@ nextflow_workflow { input[0] = workflow input[1] = validate_params input[2] = "${projectDir}/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json" + input[3] = false + input[4] = false + input[5] = false + input[6] = "" + input[7] = "" + input[8] = "" + input[9] = null """ } } @@ -103,6 +124,13 @@ nextflow_workflow { input[0] = workflow input[1] = validate_params input[2] = "${projectDir}/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json" + input[3] = false + input[4] = false + input[5] = false + input[6] = "" + input[7] = "" + input[8] = "" + input[9] = null """ } } @@ -114,4 +142,37 @@ nextflow_workflow { ) } } + + test("Should create a help message") { + + when { + + params { + test_data = '' + outdir = null + } + + workflow { + """ + validate_params = true + input[0] = workflow + input[1] = validate_params + input[2] = "${projectDir}/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json" + input[3] = true + input[4] = false + input[5] = false + input[6] = "Before" + input[7] = "After" + input[8] = "nextflow run test/test" + input[9] = null + """ + } + } + + then { + assertAll( + { assert workflow.success } + ) + } + } } diff --git a/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config index 0907ac5..fd71cb8 100644 --- a/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config +++ b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config @@ -1,8 +1,8 @@ plugins { - id "nf-schema@2.1.0" + id "nf-schema@2.7.2" } validation { parametersSchema = "${projectDir}/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow_schema.json" monochromeLogs = true -} \ No newline at end of file +} diff --git a/tests/.nftignore b/tests/.nftignore new file mode 100644 index 0000000..0c8b5c9 --- /dev/null +++ b/tests/.nftignore @@ -0,0 +1,35 @@ +.DS_Store +multiqc/multiqc_data/fastqc_top_overrepresented_sequences_table.txt +multiqc/multiqc_data/multiqc.parquet +multiqc/multiqc_data/multiqc.log +multiqc/multiqc_data/multiqc_data.json +multiqc/multiqc_data/multiqc_sources.txt +multiqc/multiqc_data/multiqc_software_versions.txt +multiqc/multiqc_data/llms-full.txt +multiqc/multiqc_plots/{svg,pdf,png}/*.{svg,pdf,png} +multiqc/multiqc_report.html +fastqc/*_fastqc.{html,zip} +pipeline_info/*.{html,json,txt,yml} +library_QC/**/*.pdf +fitness/DiMSum_results/**/*.pdf +fitness/DiMSum_results/**/dimsum_results_workspace.RData +fitness/DiMSum_results/**/report.html +fitness/DiMSum_results/**/*.png +**/*.pdf +**/*.RData +**/*_error_correction_report.html +**/variant_effect_inspection_tool.html +deepmutscan_report.html +**/structure_data.json +fitness/DiMSum_results/**/report.html +**/fitness_estimation_mutscan_edgeR.tsv +**/fitness_estimation_mutscan_limma.tsv +**/dimsum_results_*_merge.tsv +**/fitness_synonymous.txt +**/*_fitness_input.tsv +**/counts_merged.tsv +**/fitness_estimation.tsv +**/*.variant_counts.tsv +**/annotated_variantCounts.csv +**/library_completed_variantCounts*.csv +**/variantCounts_filtered_by_library*.csv diff --git a/tests/default.nf.test b/tests/default.nf.test new file mode 100644 index 0000000..2fa68e0 --- /dev/null +++ b/tests/default.nf.test @@ -0,0 +1,44 @@ +nextflow_pipeline { + + name "Test pipeline" + script "../main.nf" + tag "pipeline" + + test("-profile test") { + + when { + params { + outdir = "$outputDir" + } + } + + then { + // stable_path: All files + folders in ${params.outdir}/ with a stable path (including file name) + // DiMSum's diagnostic plots are excluded from the path list as well as from the content + // hashes (tests/.nftignore): which of them DiMSum emits depends on its graphics stack, so + // their mere presence is not a stable contract to assert on. + def stable_path = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}', 'multiqc/multiqc_plots', 'multiqc/multiqc_plots/**', 'fitness/DiMSum_results/**/reports/*.{pdf,png}']) + // stable_content: All files in ${params.outdir}/ with stable content + def stable_content = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + // Parse the unstable TSVs to snapshot their structure instead of their hashes + def edger_tsv = path("$outputDir/fitness/mutscan_results/fitness_estimation_mutscan_edgeR.tsv").csv(sep: '\t') + def limma_tsv = path("$outputDir/fitness/mutscan_results/fitness_estimation_mutscan_limma.tsv").csv(sep: '\t') + assert workflow.success + assertAll( + { assert snapshot( + // pipeline versions.yml file for multiqc from which Nextflow version is removed because we test pipelines on multiple Nextflow versions + removeNextflowVersion("$outputDir/pipeline_info/nf_core_deepmutscan_software_mqc_versions.yml"), + // All stable path name, with a relative path + stable_path, + // All files with stable contents + stable_content, + // Partial structural snapshot of the unstable TSV files + [ + edger_rowCount: edger_tsv.rowCount, + limma_rowCount: limma_tsv.rowCount + ] + ).match() } + ) + } + } +} diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap new file mode 100644 index 0000000..7ff3166 --- /dev/null +++ b/tests/default.nf.test.snap @@ -0,0 +1,390 @@ +{ + "-profile test": { + "content": [ + { + "BWA_INDEX": { + "bwa": "0.7.19-r1273" + }, + "BWA_MEM": { + "bwa": "0.7.19-r1273", + "samtools": "1.22.1" + }, + "DIMSUM_RUN": { + "r-base": "4.4.2" + }, + "DMSANALYSIS_AASEQ": { + "r-base": "4.5.1", + "r-Biostrings": "2.78.0" + }, + "DMSANALYSIS_ERROR_CORRECTION_FALSE_DOUBLES": { + "r-base": "4.5.1", + "r-stringr": "1.6.0", + "r-Biostrings": "2.78.0" + }, + "DMSANALYSIS_POSSIBLE_MUTATIONS": { + "r-base": "4.5.1", + "biostrings": "2.78.0" + }, + "EXPDESIGN_FITNESS": { + "r-base": "4.5.1" + }, + "FASTQC": { + "fastqc": "0.12.1" + }, + "FIND_SYNONYMOUS_MUTATION": { + "r-base": "4.5.1", + "r-Biostrings": "2.78.0" + }, + "FITNESS_CALCULATION": { + "r-base": "4.5.1", + "r-Biostrings": "2.78.0" + }, + "FITNESS_HEATMAP": { + "r-base": "4.5.1", + "r-dplyr": "1.1.4", + "r-ggplot2": "4.0.2", + "r-methods": "4.5.1", + "r-grid": "4.5.1" + }, + "FITNESS_QC": { + "r-base": "4.5.1" + }, + "MERGE_COUNTS": { + "r-base": "4.5.1" + }, + "RUN_MUTSCAN": { + "r-base": "4.5.1", + "r-mutscan": "1.0.0", + "r-Biostrings": "2.78.0" + }, + "SAMTOOLS_INDEX": { + "samtools": "1.23.1" + }, + "SAMTOOLS_SORT": { + "samtools": "1.23.1" + }, + "VARIANTCOUNTING": { + "python": "3.12.13", + "pysam": "0.24.0", + "polars": "1.33.1", + "pyarrow": "24.0.0", + "numpy": "2.5.1", + "biopython": 1.87 + }, + "VARIANT_EFFECT_INSPECTION_TOOL": { + "python": "3.12.3", + "pandas": "2.2.1", + "numpy": "1.26.4" + }, + "VISUALIZATION_ERROR_CORRECTION_REPORT": { + "python": "3.12.3", + "pandas": "2.2.1", + "numpy": "1.26.4" + }, + "VISUALIZATION_SEQDEPTH": { + "r-base": "4.5.1", + "r-ggplot2": "4.0.2" + }, + "Workflow": { + "nf-core/deepmutscan": "v1.0.0" + } + }, + [ + "deepmutscan_report.html", + "fastqc", + "fastqc/GID1A_input_1_pe_1_fastqc.html", + "fastqc/GID1A_input_1_pe_1_fastqc.zip", + "fastqc/GID1A_input_1_pe_2_fastqc.html", + "fastqc/GID1A_input_1_pe_2_fastqc.zip", + "fastqc/GID1A_input_2_pe_1_fastqc.html", + "fastqc/GID1A_input_2_pe_1_fastqc.zip", + "fastqc/GID1A_input_2_pe_2_fastqc.html", + "fastqc/GID1A_input_2_pe_2_fastqc.zip", + "fastqc/GID1A_output_1_pe_1_fastqc.html", + "fastqc/GID1A_output_1_pe_1_fastqc.zip", + "fastqc/GID1A_output_1_pe_2_fastqc.html", + "fastqc/GID1A_output_1_pe_2_fastqc.zip", + "fastqc/GID1A_output_2_pe_1_fastqc.html", + "fastqc/GID1A_output_2_pe_1_fastqc.zip", + "fastqc/GID1A_output_2_pe_2_fastqc.html", + "fastqc/GID1A_output_2_pe_2_fastqc.zip", + "fitness", + "fitness/DiMSum_results", + "fitness/DiMSum_results/dimsum_results", + "fitness/DiMSum_results/dimsum_results/dimsum_results_fitness_intermediate.RData", + "fitness/DiMSum_results/dimsum_results/dimsum_results_fitness_replicates.RData", + "fitness/DiMSum_results/dimsum_results/dimsum_results_indel_variant_data_merge.tsv", + "fitness/DiMSum_results/dimsum_results/dimsum_results_nobarcode_variant_data_merge.tsv", + "fitness/DiMSum_results/dimsum_results/dimsum_results_rejected_variant_data_merge.tsv", + "fitness/DiMSum_results/dimsum_results/dimsum_results_sessionInfo.RData", + "fitness/DiMSum_results/dimsum_results/dimsum_results_variant_data_merge.RData", + "fitness/DiMSum_results/dimsum_results/dimsum_results_variant_data_merge.tsv", + "fitness/DiMSum_results/dimsum_results/dimsum_results_workspace.RData", + "fitness/DiMSum_results/dimsum_results/fitness_doubles.txt", + "fitness/DiMSum_results/dimsum_results/fitness_singles.txt", + "fitness/DiMSum_results/dimsum_results/fitness_singles_MaveDB.csv", + "fitness/DiMSum_results/dimsum_results/fitness_synonymous.txt", + "fitness/DiMSum_results/dimsum_results/fitness_wildtype.txt", + "fitness/DiMSum_results/dimsum_results/report.html", + "fitness/DiMSum_results/dimsum_results/reports", + "fitness/DiMSum_results/dimsum_results/reports/report.Rmd", + "fitness/DiMSum_results/dimsum_results/reports/report_settings.RData", + "fitness/DiMSum_results/dimsum_results/tmp", + "fitness/DiMSum_results/dimsum_results/tmp/mutation_stats_dicts.RData", + "fitness/DiMSum_results/single_rep_counts", + "fitness/DiMSum_results/single_rep_counts/GID1A_input_1_pe_fitness_input.tsv", + "fitness/DiMSum_results/single_rep_counts/GID1A_input_2_pe_fitness_input.tsv", + "fitness/DiMSum_results/single_rep_counts/GID1A_output_1_pe_fitness_input.tsv", + "fitness/DiMSum_results/single_rep_counts/GID1A_output_2_pe_fitness_input.tsv", + "fitness/counts_merged.tsv", + "fitness/default_results", + "fitness/default_results/fitness_estimation.tsv", + "fitness/default_results/fitness_estimation_count_correlation.pdf", + "fitness/default_results/fitness_estimation_fitness_correlation.pdf", + "fitness/default_results/fitness_heatmap.pdf", + "fitness/experimentalDesign.tsv", + "fitness/mutscan_results", + "fitness/mutscan_results/fitness_estimation_mutscan_edgeR.tsv", + "fitness/mutscan_results/fitness_estimation_mutscan_limma.tsv", + "fitness/mutscan_results/mutscan_counts_corr.pdf", + "fitness/mutscan_results/mutscan_edgeR_volcano.pdf", + "fitness/mutscan_results/mutscan_limma_volcano.pdf", + "fitness/synonymous_wt.txt", + "intermediate_files", + "intermediate_files/aa_seq.txt", + "intermediate_files/bam_files", + "intermediate_files/bam_files/bwa", + "intermediate_files/bam_files/bwa/mem", + "intermediate_files/bam_files/bwa/mem/GID1A_input_1_pe.bam", + "intermediate_files/bam_files/bwa/mem/GID1A_input_2_pe.bam", + "intermediate_files/bam_files/bwa/mem/GID1A_output_1_pe.bam", + "intermediate_files/bam_files/bwa/mem/GID1A_output_2_pe.bam", + "intermediate_files/bam_files/filtered", + "intermediate_files/bam_files/filtered/GID1A_input_1_pe_filtered.bam", + "intermediate_files/bam_files/filtered/GID1A_input_2_pe_filtered.bam", + "intermediate_files/bam_files/filtered/GID1A_output_1_pe_filtered.bam", + "intermediate_files/bam_files/filtered/GID1A_output_2_pe_filtered.bam", + "intermediate_files/bam_files/premerged", + "intermediate_files/bam_files/premerged/GID1A_input_1_pe_merged.bam", + "intermediate_files/bam_files/premerged/GID1A_input_2_pe_merged.bam", + "intermediate_files/bam_files/premerged/GID1A_output_1_pe_merged.bam", + "intermediate_files/bam_files/premerged/GID1A_output_2_pe_merged.bam", + "intermediate_files/bam_files/sorted", + "intermediate_files/bam_files/sorted/GID1A_input_1_pe.sorted.bam", + "intermediate_files/bam_files/sorted/GID1A_input_1_pe.sorted.bam.bai", + "intermediate_files/bam_files/sorted/GID1A_input_2_pe.sorted.bam", + "intermediate_files/bam_files/sorted/GID1A_input_2_pe.sorted.bam.bai", + "intermediate_files/bam_files/sorted/GID1A_output_1_pe.sorted.bam", + "intermediate_files/bam_files/sorted/GID1A_output_1_pe.sorted.bam.bai", + "intermediate_files/bam_files/sorted/GID1A_output_2_pe.sorted.bam", + "intermediate_files/bam_files/sorted/GID1A_output_2_pe.sorted.bam.bai", + "intermediate_files/bwa", + "intermediate_files/bwa/GID1A.amb", + "intermediate_files/bwa/GID1A.ann", + "intermediate_files/bwa/GID1A.bwt", + "intermediate_files/bwa/GID1A.pac", + "intermediate_files/bwa/GID1A.sa", + "intermediate_files/possible_mutations.csv", + "intermediate_files/processed_variant_counts", + "intermediate_files/processed_variant_counts/GID1A_input_1_pe", + "intermediate_files/processed_variant_counts/GID1A_input_1_pe/annotated_variantCounts.csv", + "intermediate_files/processed_variant_counts/GID1A_input_1_pe/library_completed_variantCounts.csv", + "intermediate_files/processed_variant_counts/GID1A_input_1_pe/library_completed_variantCounts_error_corrected.csv", + "intermediate_files/processed_variant_counts/GID1A_input_1_pe/seq_error_rate.csv", + "intermediate_files/processed_variant_counts/GID1A_input_1_pe/variantCounts_filtered_by_library.csv", + "intermediate_files/processed_variant_counts/GID1A_input_1_pe/variantCounts_filtered_by_library_error_corrected.csv", + "intermediate_files/processed_variant_counts/GID1A_input_1_pe/variantCounts_for_heatmaps.csv", + "intermediate_files/processed_variant_counts/GID1A_input_1_pe/variantCounts_for_heatmaps_error_corrected.csv", + "intermediate_files/processed_variant_counts/GID1A_input_2_pe", + "intermediate_files/processed_variant_counts/GID1A_input_2_pe/annotated_variantCounts.csv", + "intermediate_files/processed_variant_counts/GID1A_input_2_pe/library_completed_variantCounts.csv", + "intermediate_files/processed_variant_counts/GID1A_input_2_pe/library_completed_variantCounts_error_corrected.csv", + "intermediate_files/processed_variant_counts/GID1A_input_2_pe/seq_error_rate.csv", + "intermediate_files/processed_variant_counts/GID1A_input_2_pe/variantCounts_filtered_by_library.csv", + "intermediate_files/processed_variant_counts/GID1A_input_2_pe/variantCounts_filtered_by_library_error_corrected.csv", + "intermediate_files/processed_variant_counts/GID1A_input_2_pe/variantCounts_for_heatmaps.csv", + "intermediate_files/processed_variant_counts/GID1A_input_2_pe/variantCounts_for_heatmaps_error_corrected.csv", + "intermediate_files/processed_variant_counts/GID1A_output_1_pe", + "intermediate_files/processed_variant_counts/GID1A_output_1_pe/annotated_variantCounts.csv", + "intermediate_files/processed_variant_counts/GID1A_output_1_pe/library_completed_variantCounts.csv", + "intermediate_files/processed_variant_counts/GID1A_output_1_pe/library_completed_variantCounts_error_corrected.csv", + "intermediate_files/processed_variant_counts/GID1A_output_1_pe/seq_error_rate.csv", + "intermediate_files/processed_variant_counts/GID1A_output_1_pe/variantCounts_filtered_by_library.csv", + "intermediate_files/processed_variant_counts/GID1A_output_1_pe/variantCounts_filtered_by_library_error_corrected.csv", + "intermediate_files/processed_variant_counts/GID1A_output_1_pe/variantCounts_for_heatmaps.csv", + "intermediate_files/processed_variant_counts/GID1A_output_1_pe/variantCounts_for_heatmaps_error_corrected.csv", + "intermediate_files/processed_variant_counts/GID1A_output_2_pe", + "intermediate_files/processed_variant_counts/GID1A_output_2_pe/annotated_variantCounts.csv", + "intermediate_files/processed_variant_counts/GID1A_output_2_pe/library_completed_variantCounts.csv", + "intermediate_files/processed_variant_counts/GID1A_output_2_pe/library_completed_variantCounts_error_corrected.csv", + "intermediate_files/processed_variant_counts/GID1A_output_2_pe/seq_error_rate.csv", + "intermediate_files/processed_variant_counts/GID1A_output_2_pe/variantCounts_filtered_by_library.csv", + "intermediate_files/processed_variant_counts/GID1A_output_2_pe/variantCounts_filtered_by_library_error_corrected.csv", + "intermediate_files/processed_variant_counts/GID1A_output_2_pe/variantCounts_for_heatmaps.csv", + "intermediate_files/processed_variant_counts/GID1A_output_2_pe/variantCounts_for_heatmaps_error_corrected.csv", + "intermediate_files/variant_counts", + "intermediate_files/variant_counts/GID1A_input_1_pe", + "intermediate_files/variant_counts/GID1A_input_1_pe/GID1A_input_1_pe.variant_counts.tsv", + "intermediate_files/variant_counts/GID1A_input_1_pe/variant_counts_columns.tsv", + "intermediate_files/variant_counts/GID1A_input_2_pe", + "intermediate_files/variant_counts/GID1A_input_2_pe/GID1A_input_2_pe.variant_counts.tsv", + "intermediate_files/variant_counts/GID1A_input_2_pe/variant_counts_columns.tsv", + "intermediate_files/variant_counts/GID1A_output_1_pe", + "intermediate_files/variant_counts/GID1A_output_1_pe/GID1A_output_1_pe.variant_counts.tsv", + "intermediate_files/variant_counts/GID1A_output_1_pe/variant_counts_columns.tsv", + "intermediate_files/variant_counts/GID1A_output_2_pe", + "intermediate_files/variant_counts/GID1A_output_2_pe/GID1A_output_2_pe.variant_counts.tsv", + "intermediate_files/variant_counts/GID1A_output_2_pe/variant_counts_columns.tsv", + "library_QC", + "library_QC/GID1A_input_1_pe", + "library_QC/GID1A_input_1_pe/SeqDepth.pdf", + "library_QC/GID1A_input_1_pe/counts_heatmap.pdf", + "library_QC/GID1A_input_1_pe/counts_per_cov_heatmap.pdf", + "library_QC/GID1A_input_1_pe/logdiff_plot.pdf", + "library_QC/GID1A_input_1_pe/logdiff_varying_bases.pdf", + "library_QC/GID1A_input_1_pe/rolling_counts.pdf", + "library_QC/GID1A_input_1_pe/rolling_counts_per_cov.pdf", + "library_QC/GID1A_input_1_pe/rolling_coverage.pdf", + "library_QC/GID1A_input_1_pe/seqdepth_curve.csv", + "library_QC/GID1A_input_2_pe", + "library_QC/GID1A_input_2_pe/SeqDepth.pdf", + "library_QC/GID1A_input_2_pe/counts_heatmap.pdf", + "library_QC/GID1A_input_2_pe/counts_per_cov_heatmap.pdf", + "library_QC/GID1A_input_2_pe/logdiff_plot.pdf", + "library_QC/GID1A_input_2_pe/logdiff_varying_bases.pdf", + "library_QC/GID1A_input_2_pe/rolling_counts.pdf", + "library_QC/GID1A_input_2_pe/rolling_counts_per_cov.pdf", + "library_QC/GID1A_input_2_pe/rolling_coverage.pdf", + "library_QC/GID1A_input_2_pe/seqdepth_curve.csv", + "library_QC/GID1A_output_1_pe", + "library_QC/GID1A_output_1_pe/SeqDepth.pdf", + "library_QC/GID1A_output_1_pe/counts_heatmap.pdf", + "library_QC/GID1A_output_1_pe/counts_per_cov_heatmap.pdf", + "library_QC/GID1A_output_1_pe/logdiff_plot.pdf", + "library_QC/GID1A_output_1_pe/logdiff_varying_bases.pdf", + "library_QC/GID1A_output_1_pe/rolling_counts.pdf", + "library_QC/GID1A_output_1_pe/rolling_counts_per_cov.pdf", + "library_QC/GID1A_output_1_pe/rolling_coverage.pdf", + "library_QC/GID1A_output_1_pe/seqdepth_curve.csv", + "library_QC/GID1A_output_2_pe", + "library_QC/GID1A_output_2_pe/SeqDepth.pdf", + "library_QC/GID1A_output_2_pe/counts_heatmap.pdf", + "library_QC/GID1A_output_2_pe/counts_per_cov_heatmap.pdf", + "library_QC/GID1A_output_2_pe/logdiff_plot.pdf", + "library_QC/GID1A_output_2_pe/logdiff_varying_bases.pdf", + "library_QC/GID1A_output_2_pe/rolling_counts.pdf", + "library_QC/GID1A_output_2_pe/rolling_counts_per_cov.pdf", + "library_QC/GID1A_output_2_pe/rolling_coverage.pdf", + "library_QC/GID1A_output_2_pe/seqdepth_curve.csv", + "library_QC/error_correction_report.html", + "multiqc", + "multiqc/multiqc_data", + "multiqc/multiqc_data/fastqc-status-check-heatmap.txt", + "multiqc/multiqc_data/fastqc_adapter_content_plot.txt", + "multiqc/multiqc_data/fastqc_overrepresented_sequences_plot.txt", + "multiqc/multiqc_data/fastqc_per_base_n_content_plot.txt", + "multiqc/multiqc_data/fastqc_per_base_sequence_quality_plot.txt", + "multiqc/multiqc_data/fastqc_per_sequence_gc_content_plot_Counts.txt", + "multiqc/multiqc_data/fastqc_per_sequence_gc_content_plot_Percentages.txt", + "multiqc/multiqc_data/fastqc_per_sequence_quality_scores_plot.txt", + "multiqc/multiqc_data/fastqc_sequence_counts_plot.txt", + "multiqc/multiqc_data/fastqc_sequence_duplication_levels_plot.txt", + "multiqc/multiqc_data/fastqc_top_overrepresented_sequences_table.txt", + "multiqc/multiqc_data/llms-full.txt", + "multiqc/multiqc_data/multiqc.log", + "multiqc/multiqc_data/multiqc.parquet", + "multiqc/multiqc_data/multiqc_citations.txt", + "multiqc/multiqc_data/multiqc_data.json", + "multiqc/multiqc_data/multiqc_fastqc.txt", + "multiqc/multiqc_data/multiqc_general_stats.txt", + "multiqc/multiqc_data/multiqc_software_versions.txt", + "multiqc/multiqc_data/multiqc_sources.txt", + "multiqc/multiqc_report.html", + "pipeline_info", + "pipeline_info/nf_core_deepmutscan_software_mqc_versions.yml", + "variant_effect_inspection_tool", + "variant_effect_inspection_tool/structure_data.json", + "variant_effect_inspection_tool/variant_effect_inspection_tool.html" + ], + [ + "fitness_doubles.txt:md5,68b329da9893e34099c7d8ad5cb9c940", + "fitness_singles.txt:md5,df4effcdf5477327ab7e74034498ddc8", + "fitness_singles_MaveDB.csv:md5,160ad250f0774e770b5128840b62ba80", + "fitness_wildtype.txt:md5,934b2d1b64746b4130d99e7bd46d5595", + "report.Rmd:md5,a19acae87f16d0059c6699bc1e818d95", + "experimentalDesign.tsv:md5,f95dea4a3e6cf7e206e789a96e502604", + "synonymous_wt.txt:md5,bb94c5975185b80f8d63acebfbf4a0da", + "aa_seq.txt:md5,421287586f566191f7da4a7aad7d24a8", + "GID1A_input_1_pe.bam:md5,57f54fa2bad68dc8ff39d2bb79d94742", + "GID1A_input_2_pe.bam:md5,5da708382cddfaa90e7b53e3d79d8f8e", + "GID1A_output_1_pe.bam:md5,f888077ed2234b2b7b033ceea621b33f", + "GID1A_output_2_pe.bam:md5,5b147ba80d87ee3ff3be6c3f34d7f67d", + "GID1A_input_1_pe_filtered.bam:md5,9a8f3691ec0478928227fe0da590054b", + "GID1A_input_2_pe_filtered.bam:md5,50f50c79514041c8746fb95070fbf321", + "GID1A_output_1_pe_filtered.bam:md5,b743b1fd86941c392c7d42474a82217d", + "GID1A_output_2_pe_filtered.bam:md5,20e02c4c5f0ba2ff9f9b553e80a89343", + "GID1A_input_1_pe_merged.bam:md5,e83e32f077c1f8018dd7cda8a27828fd", + "GID1A_input_2_pe_merged.bam:md5,eeb0a34e585e1858d3b758d6bde3b8db", + "GID1A_output_1_pe_merged.bam:md5,ed034511d33464cd9f032b8824663842", + "GID1A_output_2_pe_merged.bam:md5,9bafd9eee748877ac559815b1420b865", + "GID1A_input_1_pe.sorted.bam:md5,80311313f5d8b1387130ba376bd8f2b9", + "GID1A_input_1_pe.sorted.bam.bai:md5,644255e89a4f621627ee67d5884fe79e", + "GID1A_input_2_pe.sorted.bam:md5,084adf0c95308ceef7eba177999ed6fe", + "GID1A_input_2_pe.sorted.bam.bai:md5,8f5580680b4aa02ea4800f422d366a8b", + "GID1A_output_1_pe.sorted.bam:md5,95a6016ac42a49925b3300ecb57465f3", + "GID1A_output_1_pe.sorted.bam.bai:md5,38e8757743fca3dbd3b1f44c1cb49475", + "GID1A_output_2_pe.sorted.bam:md5,73b5c65751b796b6565ba18b316935ab", + "GID1A_output_2_pe.sorted.bam.bai:md5,f032b0115c80e79b05eb757bd75445e1", + "GID1A.amb:md5,383ef17f6abb846ab669ebac30f5659b", + "GID1A.ann:md5,30c4a2204f9166d9594c790e0ca58793", + "GID1A.bwt:md5,120149e0239465625c2dd20a171701fd", + "GID1A.pac:md5,104f743d81d39b2527b5ae52db26654c", + "GID1A.sa:md5,1f697dcb3fedd2f3f29642598bdd795b", + "possible_mutations.csv:md5,9ce59fe91b726b1d88587a13fa899567", + "seq_error_rate.csv:md5,350a018c981eca2506ac6291a6e9e161", + "variantCounts_for_heatmaps.csv:md5,8794f9fc716405d1891d24af6b636a2f", + "variantCounts_for_heatmaps_error_corrected.csv:md5,8794f9fc716405d1891d24af6b636a2f", + "seq_error_rate.csv:md5,350a018c981eca2506ac6291a6e9e161", + "variantCounts_for_heatmaps.csv:md5,63c358cb04210b9dda2416ab3352494a", + "variantCounts_for_heatmaps_error_corrected.csv:md5,63c358cb04210b9dda2416ab3352494a", + "seq_error_rate.csv:md5,350a018c981eca2506ac6291a6e9e161", + "variantCounts_for_heatmaps.csv:md5,33c6c3af1825b9ea7fbc949a18bb38aa", + "variantCounts_for_heatmaps_error_corrected.csv:md5,33c6c3af1825b9ea7fbc949a18bb38aa", + "seq_error_rate.csv:md5,350a018c981eca2506ac6291a6e9e161", + "variantCounts_for_heatmaps.csv:md5,5277b202849d27c0b1773cacd8aae416", + "variantCounts_for_heatmaps_error_corrected.csv:md5,5277b202849d27c0b1773cacd8aae416", + "variant_counts_columns.tsv:md5,c94801255a553ce5ff4a00d3170f6768", + "variant_counts_columns.tsv:md5,c94801255a553ce5ff4a00d3170f6768", + "variant_counts_columns.tsv:md5,c94801255a553ce5ff4a00d3170f6768", + "variant_counts_columns.tsv:md5,c94801255a553ce5ff4a00d3170f6768", + "seqdepth_curve.csv:md5,a7217ad283c687d26d68c84c897f5f9e", + "seqdepth_curve.csv:md5,5daeef9c60240ac5eebafe632def9f65", + "seqdepth_curve.csv:md5,7fd3fd5ce9b8f3e5b2a336ee5719b5e2", + "seqdepth_curve.csv:md5,341bda014e405be6c6f572164daf5aae", + "error_correction_report.html:md5,802b1cca71ffb34758810cea748c73ea", + "fastqc-status-check-heatmap.txt:md5,58a9580c11af4210b1af65753383e120", + "fastqc_adapter_content_plot.txt:md5,2990ad34b6fa29151dd4b9a38828afbe", + "fastqc_overrepresented_sequences_plot.txt:md5,c703a9df0ea3a5c1c5f515a1fd88bbe8", + "fastqc_per_base_n_content_plot.txt:md5,6af419871cdec77149cf013b72e82417", + "fastqc_per_base_sequence_quality_plot.txt:md5,460b8e65cf8d58087b3143ba78b30e17", + "fastqc_per_sequence_gc_content_plot_Counts.txt:md5,7f1eacaa19e912e18a4419b23e749e3a", + "fastqc_per_sequence_gc_content_plot_Percentages.txt:md5,0e405d9394ae8aeaa6ae162e18f7c195", + "fastqc_per_sequence_quality_scores_plot.txt:md5,5d1ee338c146901ca9f1690f7ee47200", + "fastqc_sequence_counts_plot.txt:md5,8b438078b7f3475b6ce659cc4b8dca11", + "fastqc_sequence_duplication_levels_plot.txt:md5,a0839f96103959fd7265cbb986e2724a", + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_fastqc.txt:md5,72cb1ee38d354d674ced1b4b21bbeff4", + "multiqc_general_stats.txt:md5,7c2fd44ded06835128a208d3365aa23b" + ], + { + "edger_rowCount": 1230, + "limma_rowCount": 1230 + } + ], + "timestamp": "2026-10-06T14:37:13.192327", + "meta": { + "nf-test": "0.9.4", + "nextflow": "26.04.1" + } + } +} \ No newline at end of file diff --git a/tests/nextflow.config b/tests/nextflow.config new file mode 100644 index 0000000..b4075e0 --- /dev/null +++ b/tests/nextflow.config @@ -0,0 +1,29 @@ +/* +======================================================================================== + Nextflow config file for running nf-test tests +======================================================================================== +*/ + +process { + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '1.h' + ] +} + +params { + modules_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/' + pipelines_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/refs/heads/deepmutscan/' + + // Input data + input = params.pipelines_testdata_base_path + 'samplesheet/GID1A_test.csv' + fasta = params.pipelines_testdata_base_path + 'testdata/GID1A.fasta' + reading_frame = '352-1383' + min_counts = 2 + mutagenesis_type = 'nnk_nns' + run_seqdepth = true + fitness = true +} + +aws.client.anonymous = true // fixes S3 access issues on self-hosted runners diff --git a/workflows/deepmutscan.nf b/workflows/deepmutscan.nf new file mode 100644 index 0000000..36ed769 --- /dev/null +++ b/workflows/deepmutscan.nf @@ -0,0 +1,566 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + IMPORT MODULES / SUBWORKFLOWS / FUNCTIONS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ +include { FASTQC } from '../modules/nf-core/fastqc/main' +include { MULTIQC } from '../modules/nf-core/multiqc/main' +include { BWA_INDEX } from '../modules/nf-core/bwa/index/main' +include { BWA_MEM } from '../modules/nf-core/bwa/mem/main' +include { BAMFILTER_DMS } from '../modules/local/bamprocessing/bam_filter/main' +include { PREMERGE } from '../modules/local/bamprocessing/premerge/main' +include { SAMTOOLS_SORT } from '../modules/nf-core/samtools/sort/main' +include { SAMTOOLS_INDEX } from '../modules/nf-core/samtools/index/main' +include { VARIANTCOUNTING } from '../modules/local/variantcounting/main' +include { DMSANALYSIS_AASEQ } from '../modules/local/dmsanalysis/aa_seq/main' +include { DMSANALYSIS_POSSIBLE_MUTATIONS } from '../modules/local/dmsanalysis/possible_mutations/main' +include { DMSANALYSIS_PROCESS_VARIANT_COUNTS } from '../modules/local/dmsanalysis/process_variant_counts/main' +include { DMSANALYSIS_ERROR_CORRECTION_FALSE_DOUBLES } from '../modules/local/dmsanalysis/error_correction_false_doubles/main' +include { DMSANALYSIS_ERROR_CORRECTION_WILDTYPE } from '../modules/local/dmsanalysis/error_correction_wildtype/main' +include { VISUALIZATION_COUNTS_PER_COV } from '../modules/local/visualization/counts_per_cov/main' +include { VISUALIZATION_COUNTS_HEATMAP } from '../modules/local/visualization/counts_heatmap/main' +include { VISUALIZATION_GLOBAL_POS_BIASES_COUNTS } from '../modules/local/visualization/global_pos_biases_counts/main' +include { VISUALIZATION_GLOBAL_POS_BIASES_COV } from '../modules/local/visualization/global_pos_biases_cov/main' +include { VISUALIZATION_LOGDIFF } from '../modules/local/visualization/logdiff/main' +include { VISUALIZATION_SEQDEPTH } from '../modules/local/visualization/seqdepth/main' +include { VISUALIZATION_ERROR_CORRECTION_REPORT } from '../modules/local/visualization/error_correction_report/main' +include { VISUALIZATION_SUMMARY_REPORT } from '../modules/local/visualization/summary_report/main' +include { VARIANT_EFFECT_INSPECTION_TOOL } from '../modules/local/structure/variant_effect_inspection_tool/main' +include { COUNTS_TO_FITNESS } from '../modules/local/fitness/counts_to_fitness/main' + +include { CALCULATE_FITNESS } from '../subworkflows/local/calculate_fitness/main' + +include { paramsSummaryMap } from 'plugin/nf-schema' +include { paramsSummaryMultiqc } from '../subworkflows/nf-core/utils_nfcore_pipeline' +include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' +include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_deepmutscan_pipeline' + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + RUN MAIN WORKFLOW +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +workflow DEEPMUTSCAN { + + take: + ch_samplesheet // channel: samplesheet read in from --input + multiqc_config + multiqc_logo + multiqc_methods_description + outdir + + main: + + def ch_versions = Channel.empty() + def ch_multiqc_files = Channel.empty() + + // Define input channels from parameters + def ch_fasta = Channel + .fromPath(params.fasta, checkIfExists: true) + .map { fasta -> tuple( [id: 'ref'], fasta ) } + + def reading_frame_ch = Channel.value(params.reading_frame) + def min_counts_ch = Channel.value(params.min_counts) + def custom_codon_library_ch = Channel.value(params.custom_codon_library) + def mutagenesis_type_ch = Channel.value(params.mutagenesis_type) + def sliding_window_size_ch = Channel.value(params.sliding_window_size) + def aimed_cov_ch = Channel.value(params.aimed_cov) + def run_seqdepth_ch = Channel.value(params.run_seqdepth) + def base_qual_ch = Channel.value(params.base_qual) + def min_flank_ch = Channel.value(params.min_flank) + + // Raw samplesheet path channel for downstream subworkflows + def ch_samplesheet_csv = Channel.fromPath(params.input, checkIfExists: true) + + // '--dimsum' only has an effect together with '--fitness' + if (params.dimsum && !params.fitness) { + log.warn("'--dimsum true' only works together with '--fitness true'. DiMSum will be skipped.") + } + + // '--error_correction wildtype' needs a 'wildtype' sample for every biological sample + if (params.error_correction == 'wildtype') { + ch_samplesheet + .map { meta, _reads -> tuple(meta.sample as String, meta.type as String) } + .toList() + .map { pairs -> + pairs.groupBy { it[0] }.each { sample, rows -> + def types = rows.collect { it[1] } + if (types.any { it != 'wildtype' } && !types.contains('wildtype')) { + error( + "[--error_correction wildtype] No 'wildtype' sample found for '${sample}' in the samplesheet.\n" + + " Add a samplesheet row with type 'wildtype' and sample '${sample}' (deep wildtype-only sequencing),\n" + + " or use the internal correction instead (default: --error_correction false_doubles),\n" + + " or disable error correction with --error_correction none." + ) + } + } + pairs + } + .subscribe { } + } + + // Nudge fitness users who did not supply a structure towards the interactive tool + if (params.fitness && !params.pdb) { + log.info( + "\n" + + " β”Œβ”€ Optional: interactive variant effect inspection tool ───────────────────┐\n" + + " Supply a wildtype structure with '--pdb ' to also build an\n" + + " interactive HTML tool that projects fitness, counts and error-correction\n" + + " biases onto the 3D structure to support data interpretation.\n" + + " β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n" + ) + } + + // + // MODULE: Run FastQC + // + FASTQC(ch_samplesheet) + ch_multiqc_files = ch_multiqc_files.mix(FASTQC.out.zip.map{ _meta, file -> file }) + + // + // MODULE: BWA Index + // + BWA_INDEX ( + ch_fasta + ) + + // Broadcast index to all samples + def ch_bwa_index = BWA_INDEX.out.index + + // Broadcast the index to all samples + def ch_bwa_index_broadcast = ch_samplesheet + .combine(ch_bwa_index) + .map { [it[2], it[3]] } + + // Broadcast the fasta to all samples + def ch_fasta_broadcast = ch_fasta + .combine(ch_samplesheet) + .map { [it[0], it[1]] } + + // Broadcast the sort flag to all samples + def ch_sort_bam = ch_samplesheet.map { false } + + // Run BWA_MEM with all four inputs aligned + BWA_MEM( + ch_samplesheet, + ch_bwa_index_broadcast, + ch_fasta_broadcast, + ch_sort_bam + ) + + BAMFILTER_DMS ( + BWA_MEM.out.bam + ) + + // Broadcast the FASTA path to every BAM emitted by BAMFILTER_DMS + def ch_fasta_path_broadcast = ch_fasta + .combine(BAMFILTER_DMS.out.bam) // flattened item: [meta3, fasta, meta, bam] + .map { it[1] } // keep only the fasta path (N emissions) + + PREMERGE( + BAMFILTER_DMS.out.bam, // tuple(val(meta), path(bam)) + ch_fasta_path_broadcast // path(fasta) + ) + + // Coordinate-sort + index the premerged BAM (required by the read-based counter) + SAMTOOLS_SORT( + PREMERGE.out.bam, // tuple(val(meta), path(bam)) + PREMERGE.out.bam.map { _meta, _bam -> [ [:], [], [] ] }, // empty fasta tuple (BAM sort) + Channel.value('') // index_format '' -> index separately + ) + SAMTOOLS_INDEX(SAMTOOLS_SORT.out.bam) + + // Join sorted BAM with its index -> tuple(val(meta), path(bam), path(bai)) + def ch_sorted_indexed = SAMTOOLS_SORT.out.bam.join(SAMTOOLS_INDEX.out.index) + + // Broadcast singletons to N (one per sample), anchored on the sorted+indexed channel + def ch_fasta_for_counts = ch_fasta.combine(ch_sorted_indexed).map { it[1] } // path -- N + def ch_rf_for_counts = reading_frame_ch.combine(ch_sorted_indexed).map { it[0] } // val -- N + def ch_min_for_counts = min_counts_ch.combine(ch_sorted_indexed).map { it[0] } // val -- N + def ch_bq_for_counts = base_qual_ch.combine(ch_sorted_indexed).map { it[0] } // val -- N + def ch_flank_for_counts = min_flank_ch.combine(ch_sorted_indexed).map { it[0] } // val -- N + + VARIANTCOUNTING( + ch_sorted_indexed, // tuple(val(meta), path(bam), path(bai)) + ch_fasta_for_counts, // path(fasta) + ch_rf_for_counts, // val(reading_frame string, 1-based inclusive) + ch_min_for_counts, // val(min_counts) + ch_bq_for_counts, // val(base_qual) + ch_flank_for_counts // val(min_flank) + ) + ch_versions = ch_versions.mix(VARIANTCOUNTING.out.versions) + + DMSANALYSIS_AASEQ ( + ch_fasta, + reading_frame_ch + ) + ch_versions = ch_versions.mix(DMSANALYSIS_AASEQ.out.versions) + + DMSANALYSIS_POSSIBLE_MUTATIONS( + ch_fasta, + reading_frame_ch, // pos_range (as val) + mutagenesis_type_ch, // mutagenesis_type (as val) + custom_codon_library_ch // custom_codon_library (as path) + ) + ch_versions = ch_versions.mix(DMSANALYSIS_POSSIBLE_MUTATIONS.out.versions) + + // Anchor (N items; one per sample) + def ch_vc = VARIANTCOUNTING.out.variant_counts // tuple(val(meta), path) + + // Build per-sample inputs using inline combinations (replaces fanout) + def ch_possible_mut_for_proc = DMSANALYSIS_POSSIBLE_MUTATIONS.out.possible_mutations.map { it[1] }.combine(ch_vc).map { it[0] } + def ch_aa_seq_for_proc = DMSANALYSIS_AASEQ.out.aa_seq.map { it[1] }.combine(ch_vc).map { it[0] } + def ch_min_counts_for_proc = min_counts_ch.combine(ch_vc).map { it[0] } + + // Call with all inputs aligned (each has N items now) + DMSANALYSIS_PROCESS_VARIANT_COUNTS( + ch_vc, // tuple(val(meta), path(variantCounts)) -- N + ch_possible_mut_for_proc, // path(possible_mutations) -- N + ch_aa_seq_for_proc, // path(aa_seq) -- N + ch_min_counts_for_proc // val(min_counts) -- N + ) + + def annotated_variantCounts_ch = DMSANALYSIS_PROCESS_VARIANT_COUNTS.out.processed_variantCounts.map { meta, a, b, c, d -> tuple(meta, a) } + def variantCounts_filtered_by_library_ch = DMSANALYSIS_PROCESS_VARIANT_COUNTS.out.processed_variantCounts.map { meta, a, b, c, d -> tuple(meta, b) } + def library_completed_variantCounts_ch = DMSANALYSIS_PROCESS_VARIANT_COUNTS.out.processed_variantCounts.map { meta, a, b, c, d -> tuple(meta, c) } + def variantCounts_for_heatmaps_ch = DMSANALYSIS_PROCESS_VARIANT_COUNTS.out.processed_variantCounts.map { meta, a, b, c, d -> tuple(meta, d) } + + // + // Sequencing-error correction of single-codon counts (default: false_doubles). + // Replaces the filtered + heatmap channels with corrected versions; downstream fitness + // picks up correction via the `counts_corrected` column, count heatmaps via canonical names. + // + if (params.error_correction == 'false_doubles') { + // per sample: raw (pre-filter) counts + library-filtered counts + completed library table + def ch_fd_in = ch_vc.join(variantCounts_filtered_by_library_ch).join(library_completed_variantCounts_ch) // (meta, raw, filtered, completed) + def aa_seq_for_ec = DMSANALYSIS_AASEQ.out.aa_seq.map { it[1] }.combine(ch_fd_in).map { it[0] } + def min_for_ec = min_counts_ch.combine(ch_fd_in).map { it[0] } + // the reference the reads were aligned to (base_mut is in this fasta's coordinate frame), + // broadcast one-per-sample; the estimator and the mode/window are run-level constants. + // ch_fasta is tuple(meta, fasta), so the path is it[1] - it[0] is the [id:ref] meta map. + def fasta_for_ec = ch_fasta.combine(ch_fd_in).map { it[1] } + DMSANALYSIS_ERROR_CORRECTION_FALSE_DOUBLES( + ch_fd_in, fasta_for_ec, aa_seq_for_ec, min_for_ec, + params.false_doubles_method, params.false_doubles_codon_window + ) + ch_versions = ch_versions.mix(DMSANALYSIS_ERROR_CORRECTION_FALSE_DOUBLES.out.versions) + variantCounts_filtered_by_library_ch = DMSANALYSIS_ERROR_CORRECTION_FALSE_DOUBLES.out.corrected.map { meta, filt, heat, comp -> tuple(meta, filt) } + variantCounts_for_heatmaps_ch = DMSANALYSIS_ERROR_CORRECTION_FALSE_DOUBLES.out.corrected.map { meta, filt, heat, comp -> tuple(meta, heat) } + library_completed_variantCounts_ch = DMSANALYSIS_ERROR_CORRECTION_FALSE_DOUBLES.out.corrected.map { meta, filt, heat, comp -> tuple(meta, comp) } + } + else if (params.error_correction == 'wildtype') { + // pair each non-wildtype sample with the wildtype sample of the same `sample` base + def filt_comp = variantCounts_filtered_by_library_ch.join(library_completed_variantCounts_ch) // (meta, filtered, completed) + def wt_ch = filt_comp.filter { meta, f, c -> meta.type == 'wildtype' }.map { meta, f, c -> tuple(meta.sample, f) } + def tgt_ch = filt_comp.filter { meta, f, c -> meta.type != 'wildtype' }.map { meta, f, c -> tuple(meta.sample, meta, f, c) } + def ch_wt_in = tgt_ch.combine(wt_ch, by: 0).map { _base, meta, filt, comp, wtf -> tuple(meta, filt, wtf, comp) } // (meta, filtered, wt_filtered, completed) + def aa_seq_for_ec = DMSANALYSIS_AASEQ.out.aa_seq.map { it[1] }.combine(ch_wt_in).map { it[0] } + def min_for_ec = min_counts_ch.combine(ch_wt_in).map { it[0] } + DMSANALYSIS_ERROR_CORRECTION_WILDTYPE( ch_wt_in, aa_seq_for_ec, min_for_ec ) + ch_versions = ch_versions.mix(DMSANALYSIS_ERROR_CORRECTION_WILDTYPE.out.versions) + variantCounts_filtered_by_library_ch = DMSANALYSIS_ERROR_CORRECTION_WILDTYPE.out.corrected.map { meta, filt, heat, comp -> tuple(meta, filt) } + variantCounts_for_heatmaps_ch = DMSANALYSIS_ERROR_CORRECTION_WILDTYPE.out.corrected.map { meta, filt, heat, comp -> tuple(meta, heat) } + library_completed_variantCounts_ch = DMSANALYSIS_ERROR_CORRECTION_WILDTYPE.out.corrected.map { meta, filt, heat, comp -> tuple(meta, comp) } + } + // else 'none': keep the uncorrected channels + + // Artefacts the all-in-one report embeds, collected as [[kind, group, name], file]. Optional + // parts simply never mix anything in, and the report drops the corresponding section. + def ch_report_assets = Channel.empty() + + // + // MODULE: per-sample error-correction report (only when correction was applied) + // + if (params.error_correction != 'none') { + // One run-level report over every file. Sorting first keeps the id list and the staged files in + // the same order, which is the only thing tying a column of numbers to the file it came from. + def ch_ec = variantCounts_filtered_by_library_ch.toSortedList { a, b -> a[0].id <=> b[0].id } + + VISUALIZATION_ERROR_CORRECTION_REPORT( + ch_ec.map { rows -> rows[0][0].sample ?: 'run' }, + ch_ec.map { rows -> groovy.json.JsonOutput.toJson(rows.collect { [id: it[0].id, type: it[0].type, replicate: it[0].replicate] }).bytes.encodeBase64().toString() }, + ch_ec.map { rows -> rows.collect { it[1] } }, + params.error_correction, + params.false_doubles_method, + file("${projectDir}/assets/error_correction_report/ec_report_template.html", checkIfExists: true) + ) + ch_versions = ch_versions.mix(VISUALIZATION_ERROR_CORRECTION_REPORT.out.versions) + ch_report_assets = ch_report_assets.mix( + VISUALIZATION_ERROR_CORRECTION_REPORT.out.report.map { f -> [['report', 'ec', 'Error correction'], f] } + ) + } + + // --- For VISUALIZATION_COUNTS_PER_COV & HEATMAP (replaces fanoutTo) + def min_counts_for_cov_ch = min_counts_ch.combine(variantCounts_for_heatmaps_ch).map { it[0] } + def min_counts_for_heatmap_ch = min_counts_ch.combine(variantCounts_for_heatmaps_ch).map { it[0] } + + // --- For VISUALIZATION_GLOBAL_POS_BIASES_* + def aa_seq_for_bias_ch = DMSANALYSIS_AASEQ.out.aa_seq.map { it[1] }.combine(variantCounts_filtered_by_library_ch).map { it[0] } + def sliding_window_size_N = sliding_window_size_ch.combine(variantCounts_filtered_by_library_ch).map { it[0] } + def aimed_cov_N = aimed_cov_ch.combine(variantCounts_filtered_by_library_ch).map { it[0] } + + // --- For VISUALIZATION_SEQDEPTH + def possible_mutations_N = DMSANALYSIS_POSSIBLE_MUTATIONS.out.possible_mutations.map { it[1] }.combine(variantCounts_filtered_by_library_ch).map { it[0] } + def min_counts_for_seqdepth_ch = min_counts_ch.combine(variantCounts_filtered_by_library_ch).map { it[0] } + + VISUALIZATION_COUNTS_PER_COV( + variantCounts_for_heatmaps_ch, + min_counts_for_cov_ch + ) + + VISUALIZATION_COUNTS_HEATMAP( + variantCounts_for_heatmaps_ch, + min_counts_for_heatmap_ch + ) + + VISUALIZATION_GLOBAL_POS_BIASES_COUNTS( + variantCounts_filtered_by_library_ch, + aa_seq_for_bias_ch, + sliding_window_size_N + ) + + VISUALIZATION_GLOBAL_POS_BIASES_COV( + variantCounts_filtered_by_library_ch, + aa_seq_for_bias_ch, + sliding_window_size_N, + aimed_cov_N + ) + + VISUALIZATION_LOGDIFF( + library_completed_variantCounts_ch + ) + + // library QC plots (original R PDFs) + the tables the report re-plots interactively + ch_report_assets = ch_report_assets + .mix(VISUALIZATION_GLOBAL_POS_BIASES_COV.out.rolling_coverage.map { meta, f -> [['qc', meta.id, 'rolling_coverage.pdf'], f] }) + .mix(VISUALIZATION_GLOBAL_POS_BIASES_COUNTS.out.rolling_counts.map { meta, f -> [['qc', meta.id, 'rolling_counts.pdf'], f] }) + .mix(VISUALIZATION_GLOBAL_POS_BIASES_COUNTS.out.rolling_counts_per_cov.map { meta, f -> [['qc', meta.id, 'rolling_counts_per_cov.pdf'], f] }) + .mix(VISUALIZATION_COUNTS_HEATMAP.out.counts_heatmap.map { meta, f -> [['qc', meta.id, 'counts_heatmap.pdf'], f] }) + .mix(VISUALIZATION_COUNTS_PER_COV.out.counts_per_cov_heatmap.map { meta, f -> [['qc', meta.id, 'counts_per_cov_heatmap.pdf'], f] }) + .mix(VISUALIZATION_LOGDIFF.out.logdiff_plot.map { meta, f -> [['qc', meta.id, 'logdiff_plot.pdf'], f] }) + .mix(VISUALIZATION_LOGDIFF.out.logdiff_varying_bases.map { meta, f -> [['qc', meta.id, 'logdiff_varying_bases.pdf'], f] }) + .mix(variantCounts_filtered_by_library_ch.map { meta, f -> [['filtered', meta.id, 'counts.csv'], f] }) + .mix(variantCounts_for_heatmaps_ch.map { meta, f -> [['heatmap', meta.id, 'heatmap.csv'], f] }) + // Fixes the ORF axis at the wildtype length: positions with no library coverage still have to + // occupy a slot, or every rolling window downstream is computed over the wrong neighbourhood. + .mix(DMSANALYSIS_AASEQ.out.aa_seq.map { _meta, f -> [['aa_seq', 'run', 'aa_seq.txt'], f] }) + // Needed to name the exact nucleotide change behind each heatmap cell: the fitness table gives + // the variant ORFs, and the wildtype to diff them against only exists in the reference. + .mix(ch_fasta.map { _meta, f -> [['fasta', 'run', 'reference.fasta'], f] }) + + if (params.run_seqdepth) { + VISUALIZATION_SEQDEPTH( + variantCounts_filtered_by_library_ch, + possible_mutations_N, + min_counts_for_seqdepth_ch + ) + ch_versions = ch_versions.mix(VISUALIZATION_SEQDEPTH.out.versions) + // The rarefaction curve travels to the summary report, which re-plots it per file under + // Sequencing QC (the PDF is still written to the results folder for the record). + ch_report_assets = ch_report_assets.mix( + VISUALIZATION_SEQDEPTH.out.curve.map { meta, f -> [['seqdepth', meta.id, 'seqdepth.csv'], f] } + ) + } + + // Broadcast singletons to N (one per sample), anchored on variantCounts_filtered_by_library_ch + def ch_fasta_for_fitness = ch_fasta.combine(variantCounts_filtered_by_library_ch).map { it[1] } // path(fasta) -- N + def ch_rf_for_fitness = reading_frame_ch.combine(variantCounts_filtered_by_library_ch).map { it[0] } // val(range) -- N + + // Call with aligned inputs + COUNTS_TO_FITNESS( + variantCounts_filtered_by_library_ch, // tuple(val(meta), path) + ch_fasta_for_fitness, // path(fasta) + ch_rf_for_fitness // val(reading_frame) + ) + + // Execution of fitness subworkflow, if --fitness true + if (params.fitness) { + + CALCULATE_FITNESS ( + COUNTS_TO_FITNESS.out.fitness_input, // Input from previous step + ch_samplesheet_csv, // Path to samplesheet + ch_fasta, // The original Fasta tuple + reading_frame_ch, // Reading frame value channel + DMSANALYSIS_AASEQ.out.aa_seq // Amino Acid Sequence (for Heatmap) + ) + + // Collect versions + ch_versions = ch_versions.mix(CALCULATE_FITNESS.out.versions) + + ch_report_assets = ch_report_assets + .mix(CALCULATE_FITNESS.out.fitness_estimation.first().map { _m, f -> [['fitness_table', 'default', 'fitness_estimation.tsv'], f] }) + .mix(CALCULATE_FITNESS.out.fitness_plots.map { _m, f -> [['fitness_pdf', 'default', f.name], f] }) + // mutscan emits all of its plots as one list, so flatten to a file per entry + .mix(CALCULATE_FITNESS.out.mutscan_plots.transpose().map { _m, f -> [['mutscan_pdf', 'mutscan', f.name], f] }) + .mix(CALCULATE_FITNESS.out.dimsum_dir.flatten().filter { it.name == 'report.html' }.map { f -> [['report', 'dimsum', 'DiMSum'], f] }) + + // + // MODULE: interactive variant effect inspection tool (one self-contained HTML per sample). + // Runs only when a wildtype structure is supplied via --pdb. Projects fitness, counts and + // error-correction biases onto the 3D structure. (Structure prediction is not wired here.) + // + if (params.pdb) { + def ch_structure = Channel + .fromPath(params.pdb, checkIfExists: true) + .map { pdb -> tuple([id: 'wildtype'], pdb) } + def ch_viewer_input = CALCULATE_FITNESS.out.fitness_estimation + .combine(ch_structure) + .map { smeta, fitness_tsv, _stmeta, pdb -> tuple(smeta, pdb, fitness_tsv) } + VARIANT_EFFECT_INSPECTION_TOOL( + ch_viewer_input, + variantCounts_filtered_by_library_ch.map { _meta, file -> file }.collect(), + DMSANALYSIS_AASEQ.out.aa_seq.map { _meta, file -> file }.first(), + file("${projectDir}/assets/variant_effect_inspection_tool/3Dmol-min.js", checkIfExists: true), + file("${projectDir}/assets/variant_effect_inspection_tool/viewer_template.html", checkIfExists: true), + ) + ch_versions = ch_versions.mix(VARIANT_EFFECT_INSPECTION_TOOL.out.versions) + ch_report_assets = ch_report_assets.mix( + VARIANT_EFFECT_INSPECTION_TOOL.out.viewer.map { _meta, f -> [['report', 'viewer', 'Variant effect inspection tool'], f] } + ) + // The raw structure also travels to the summary report, which builds its own slow-spinning + // fitness-coloured viewer on the Variant effects page from it. + ch_report_assets = ch_report_assets.mix( + ch_structure.map { _meta, pdb -> [['pdb', 'structure', 'structure.pdb'], pdb] } + ) + } + } + + // + // Collate and save software versions + // + def topic_versions = channel.topic("versions") + .distinct() + .branch { entry -> + versions_file: entry instanceof Path + versions_tuple: true + } + + def topic_versions_string = topic_versions.versions_tuple + .map { process, tool, version -> + [ process[process.lastIndexOf(':')+1..-1], " ${tool}: ${version}" ] + } + .groupTuple(by:0) + .map { process, tool_versions -> + tool_versions.unique().sort() + "${process}:\n${tool_versions.join('\n')}" + } + + def ch_collated_versions = softwareVersionsToYAML(ch_versions.mix(topic_versions.versions_file)) + .mix(topic_versions_string) + .collectFile( + storeDir: "${outdir}/pipeline_info", + name: 'nf_core_' + 'deepmutscan_software_' + 'mqc_' + 'versions.yml', + sort: true, + newLine: true + ) + + // + // MODULE: MultiQC + // + ch_multiqc_files = ch_multiqc_files.mix(ch_collated_versions) + def ch_summary_params = paramsSummaryMap(workflow, parameters_schema: "nextflow_schema.json") + def ch_workflow_summary = channel.value(paramsSummaryMultiqc(ch_summary_params)) + ch_multiqc_files = ch_multiqc_files.mix(ch_workflow_summary.collectFile(name: 'workflow_summary_mqc.yaml')) + def ch_multiqc_custom_methods_description = multiqc_methods_description + ? file(multiqc_methods_description, checkIfExists: true) + : file("${projectDir}/assets/methods_description_template.yml", checkIfExists: true) + def ch_methods_description = channel.value(methodsDescriptionText(ch_multiqc_custom_methods_description)) + ch_multiqc_files = ch_multiqc_files.mix(ch_methods_description.collectFile(name: 'methods_description_mqc.yaml', sort: true)) + + MULTIQC( + ch_multiqc_files.flatten().collect().map { files -> + [ + [id: 'deepmutscan'], + files, + multiqc_config + ? file(multiqc_config, checkIfExists: true) + : file("${projectDir}/assets/multiqc_config.yml", checkIfExists: true), + multiqc_logo ? file(multiqc_logo, checkIfExists: true) : [], + [], + [], + ] + } + ) + + // + // MODULE: the single all-in-one report. Runs last, because it embeds every other artefact - + // the MultiQC report included - as a base64 data URI, so the file stands alone when shared. + // + ch_report_assets = ch_report_assets.mix( + MULTIQC.out.report.map { _meta, f -> [['report', 'multiqc', 'MultiQC'], f] } + ) + // The collated software versions drive the Citation page: it cites only the tools this run used. + ch_report_assets = ch_report_assets.mix( + ch_collated_versions.map { f -> [['versions', 'run', 'versions.yml'], f] } + ) + // Nextflow's own execution trace drives the Overview run-statistics table. It is still being + // appended to as the report is built, so it captures every compute-heavy task (all but the report + // task itself) - the page labels it as of report generation. checkIfExists is off because the file + // is engine-managed, not a process output. + ch_report_assets = ch_report_assets.mix( + Channel.fromPath("${outdir}/pipeline_info/execution_trace_${params.trace_report_suffix}.txt", checkIfExists: false) + .map { f -> [['trace', 'run', 'execution_trace.txt'], f] } + ) + + // Sorting keeps the manifest and the staged files in one deterministic, matching order. + def ch_summary_input = ch_report_assets + .toSortedList { a, b -> a[0].join('|') <=> b[0].join('|') } + .map { rows -> + def manifest = rows.collect { [kind: it[0][0], group: it[0][1], name: it[0][2]] } + tuple( + groovy.json.JsonOutput.toJson(manifest).bytes.encodeBase64().toString(), + rows.collect { it[1] }, + ) + } + + def ch_run_meta = variantCounts_filtered_by_library_ch + .first() + .map { meta, _f -> + tuple( + [id: 'run', sample: meta.sample], + groovy.json.JsonOutput.toJson([ + sample: meta.sample, + reading_frame: params.reading_frame, + mutagenesis_type: params.mutagenesis_type, + error_correction: params.error_correction, + version: workflow.manifest.version, + // The report smooths client-side with the same window the R plots use, and draws + // the same required-coverage reference line, so both must travel with the run. + sliding_window_size: params.sliding_window_size, + aimed_cov: params.aimed_cov, + fitness: params.fitness, + dimsum: params.dimsum, + mutscan: params.mutscan, + outdir: params.outdir, + // Lets the Overview link straight to Nextflow's own execution report + timeline, + // which sit next to this file as execution_{report,timeline}_.html. + trace_report_suffix: params.trace_report_suffix, + false_doubles_method: params.false_doubles_method, + ]).bytes.encodeBase64().toString(), + ) + } + + VISUALIZATION_SUMMARY_REPORT( + ch_run_meta, + ch_summary_input.map { manifest, _files -> manifest }, + ch_summary_input.map { _manifest, files -> files }, + file("${projectDir}/assets/summary_report/summary_report_template.html", checkIfExists: true), + file("${projectDir}/assets/variant_effect_inspection_tool/3Dmol-min.js", checkIfExists: true), + ) + ch_versions = ch_versions.mix(VISUALIZATION_SUMMARY_REPORT.out.versions) + + emit: + multiqc_report = MULTIQC.out.report.map { _meta, report -> [report] }.toList() // channel: /path/to/multiqc_report.html + versions = ch_versions // channel: [ path(versions.yml) ] + bwa_index = BWA_INDEX.out.index + aligned_bam = BWA_MEM.out.bam + filtered_bam = BAMFILTER_DMS.out.bam + premerged_bam = PREMERGE.out.bam +} + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + THE END +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ diff --git a/workflows/dmscore.nf b/workflows/dmscore.nf deleted file mode 100644 index 041e503..0000000 --- a/workflows/dmscore.nf +++ /dev/null @@ -1,97 +0,0 @@ -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - IMPORT MODULES / SUBWORKFLOWS / FUNCTIONS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/ -include { FASTQC } from '../modules/nf-core/fastqc/main' -include { MULTIQC } from '../modules/nf-core/multiqc/main' -include { paramsSummaryMap } from 'plugin/nf-schema' -include { paramsSummaryMultiqc } from '../subworkflows/nf-core/utils_nfcore_pipeline' -include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' -include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_dmscore_pipeline' - -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - RUN MAIN WORKFLOW -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/ - -workflow DMSCORE { - - take: - ch_samplesheet // channel: samplesheet read in from --input - main: - - ch_versions = Channel.empty() - ch_multiqc_files = Channel.empty() - // - // MODULE: Run FastQC - // - FASTQC ( - ch_samplesheet - ) - ch_multiqc_files = ch_multiqc_files.mix(FASTQC.out.zip.collect{it[1]}) - ch_versions = ch_versions.mix(FASTQC.out.versions.first()) - - // - // Collate and save software versions - // - softwareVersionsToYAML(ch_versions) - .collectFile( - storeDir: "${params.outdir}/pipeline_info", - name: 'nf_core_' + 'dmscore_software_' + 'mqc_' + 'versions.yml', - sort: true, - newLine: true - ).set { ch_collated_versions } - - - // - // MODULE: MultiQC - // - ch_multiqc_config = Channel.fromPath( - "$projectDir/assets/multiqc_config.yml", checkIfExists: true) - ch_multiqc_custom_config = params.multiqc_config ? - Channel.fromPath(params.multiqc_config, checkIfExists: true) : - Channel.empty() - ch_multiqc_logo = params.multiqc_logo ? - Channel.fromPath(params.multiqc_logo, checkIfExists: true) : - Channel.empty() - - summary_params = paramsSummaryMap( - workflow, parameters_schema: "nextflow_schema.json") - ch_workflow_summary = Channel.value(paramsSummaryMultiqc(summary_params)) - ch_multiqc_files = ch_multiqc_files.mix( - ch_workflow_summary.collectFile(name: 'workflow_summary_mqc.yaml')) - ch_multiqc_custom_methods_description = params.multiqc_methods_description ? - file(params.multiqc_methods_description, checkIfExists: true) : - file("$projectDir/assets/methods_description_template.yml", checkIfExists: true) - ch_methods_description = Channel.value( - methodsDescriptionText(ch_multiqc_custom_methods_description)) - - ch_multiqc_files = ch_multiqc_files.mix(ch_collated_versions) - ch_multiqc_files = ch_multiqc_files.mix( - ch_methods_description.collectFile( - name: 'methods_description_mqc.yaml', - sort: true - ) - ) - - MULTIQC ( - ch_multiqc_files.collect(), - ch_multiqc_config.toList(), - ch_multiqc_custom_config.toList(), - ch_multiqc_logo.toList(), - [], - [] - ) - - emit:multiqc_report = MULTIQC.out.report.toList() // channel: /path/to/multiqc_report.html - versions = ch_versions // channel: [ path(versions.yml) ] - -} - -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - THE END -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/